-
Notifications
You must be signed in to change notification settings - Fork 4.4k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
🪟 🎨 Update Tooltip component to match design library #14816
Changes from 16 commits
8f77b12
176874c
9052dda
d168b82
230895f
a5769b9
c043fad
3055b9b
b2a1a69
e80fc63
f4af838
00ca040
3ec31ef
cb2aa14
b8d7666
111e072
f211b87
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
@use "../../scss/colors"; | ||
@use "../../scss/variables"; | ||
@use "../../scss/z-indices"; | ||
|
||
.container { | ||
display: inline; | ||
position: relative; | ||
} | ||
|
||
.tooltip { | ||
font-size: 12px; | ||
line-height: initial; | ||
|
||
padding: variables.$spacing-md; | ||
border-radius: 5px; | ||
max-width: 300px; | ||
z-index: z-indices.$tooltip; | ||
box-shadow: 0px 2px 4px rgba(colors.$dark-blue, 0.12); | ||
background: rgba(colors.$dark-blue, 0.9); | ||
color: colors.$white; | ||
|
||
a { | ||
color: rgba(colors.$white, 0.5); | ||
} | ||
|
||
&.light { | ||
background: rgba(colors.$white, 0.9); | ||
color: colors.$dark-blue; | ||
|
||
a { | ||
color: colors.$blue; | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,51 +1,80 @@ | ||
import React from "react"; | ||
import styled from "styled-components"; | ||
|
||
interface ToolTipProps { | ||
control: React.ReactNode; | ||
className?: string; | ||
disabled?: boolean; | ||
cursor?: "pointer" | "help" | "not-allowed"; | ||
} | ||
|
||
const Control = styled.div<{ $cursor?: "pointer" | "help" | "not-allowed"; $showCursor?: boolean }>` | ||
display: inline; | ||
position: relative; | ||
${({ $cursor, $showCursor = true }) => ($showCursor && $cursor ? `cursor: ${$cursor}` : "")}; | ||
`; | ||
|
||
const ToolTipView = styled.div<{ $disabled?: boolean }>` | ||
display: none; | ||
font-size: 14px; | ||
line-height: initial; | ||
position: absolute; | ||
padding: 9px 8px 8px; | ||
box-shadow: 0 24px 38px rgba(53, 53, 66, 0.14), 0 9px 46px rgba(53, 53, 66, 0.12), 0 11px 15px rgba(53, 53, 66, 0.2); | ||
border-radius: 4px; | ||
background: rgba(26, 26, 33, 0.9); | ||
color: ${({ theme }) => theme.whiteColor}; | ||
top: calc(100% + 10px); | ||
left: -50px; | ||
min-width: 100px; | ||
width: max-content; | ||
max-width: 380px; | ||
z-index: 10; | ||
|
||
div:hover > &&, | ||
&&:hover { | ||
display: ${({ $disabled }) => ($disabled ? "none" : "block")}; | ||
} | ||
`; | ||
|
||
const ToolTip: React.FC<ToolTipProps> = ({ children, control, className, disabled, cursor }) => { | ||
import { flip, offset, shift, useFloating } from "@floating-ui/react-dom"; | ||
import classNames from "classnames"; | ||
import React, { useState, useEffect } from "react"; | ||
|
||
import { tooltipContext } from "./context"; | ||
import styles from "./ToolTip.module.scss"; | ||
import { ToolTipProps } from "./types"; | ||
|
||
const MOUSE_OUT_TIMEOUT_MS = 50; | ||
|
||
export const ToolTip: React.FC<ToolTipProps> = (props) => { | ||
const { children, control, className, disabled, cursor, theme = "dark", placement = "bottom" } = props; | ||
|
||
const [isMouseOver, setIsMouseOver] = useState(false); | ||
const [isVisible, setIsVisible] = useState(false); | ||
|
||
const { x, y, reference, floating, strategy } = useFloating({ | ||
placement, | ||
middleware: [ | ||
offset(5), // $spacing-sm | ||
flip(), | ||
shift(), | ||
], | ||
}); | ||
|
||
useEffect(() => { | ||
if (isMouseOver) { | ||
setIsVisible(true); | ||
return; | ||
} | ||
|
||
const timeout = window.setTimeout(() => { | ||
setIsVisible(false); | ||
}, MOUSE_OUT_TIMEOUT_MS); | ||
|
||
return () => { | ||
window.clearTimeout(timeout); | ||
}; | ||
}, [isMouseOver]); | ||
|
||
const canShowTooltip = isVisible && !disabled; | ||
|
||
const onMouseOver = () => { | ||
setIsMouseOver(true); | ||
}; | ||
|
||
const onMouseOut = () => { | ||
setIsMouseOver(false); | ||
}; | ||
|
||
return ( | ||
<Control $cursor={cursor} $showCursor={!disabled}> | ||
{control} | ||
<ToolTipView className={className} $disabled={disabled}> | ||
{children} | ||
</ToolTipView> | ||
</Control> | ||
<> | ||
<div | ||
ref={reference} | ||
className={styles.container} | ||
style={disabled ? undefined : { cursor }} | ||
onMouseOver={onMouseOver} | ||
onMouseOut={onMouseOut} | ||
> | ||
{control} | ||
</div> | ||
{canShowTooltip && ( | ||
<div | ||
role="tooltip" | ||
ref={floating} | ||
className={classNames(styles.tooltip, theme === "light" && styles.light, className)} | ||
style={{ | ||
position: strategy, | ||
top: y ?? 0, | ||
left: x ?? 0, | ||
}} | ||
onMouseOver={onMouseOver} | ||
onMouseOut={onMouseOut} | ||
> | ||
<tooltipContext.Provider value={props}>{children}</tooltipContext.Provider> | ||
</div> | ||
)} | ||
</> | ||
); | ||
}; | ||
|
||
export default ToolTip; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
@use "../../scss/variables"; | ||
|
||
.container { | ||
margin-top: variables.$spacing-md; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
import { FormattedMessage } from "react-intl"; | ||
|
||
import styles from "./TooltipLearnMoreLink.module.scss"; | ||
|
||
interface TooltipLearnMoreLinkProps { | ||
url: string; | ||
} | ||
|
||
export const TooltipLearnMoreLink: React.VFC<TooltipLearnMoreLinkProps> = ({ url }) => ( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Just curious, why did you use There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. VFC (Void FunctionComponent) is a type of component that does not allow children to be passed through it. It's something I discovered recently while working on this. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ah I see, and we don't want this component to have any children because it is just a simple div with a link |
||
<div className={styles.container}> | ||
<a href={url} target="_blank" rel="noreferrer"> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Probably not worth the effort, nor would this be the place, but I'd love to have a component that would always tack on |
||
<FormattedMessage id="ui.learnMore" /> | ||
</a> | ||
</div> | ||
); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
@use "../../scss/colors"; | ||
@use "../../scss/variables"; | ||
|
||
.label { | ||
color: rgba(colors.$white, 0.7); | ||
padding-right: variables.$spacing-sm; | ||
} | ||
|
||
.light { | ||
.label { | ||
color: rgba(colors.$dark-blue, 0.7); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
import { useTooltipContext } from "./context"; | ||
import styles from "./TooltipTable.module.scss"; | ||
|
||
interface TooltipTableProps { | ||
rows: React.ReactNode[][]; | ||
} | ||
|
||
export const TooltipTable: React.VFC<TooltipTableProps> = ({ rows }) => { | ||
const { theme } = useTooltipContext(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For my own learning, could you briefly explain why you chose to use a context here? Is this because the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I did this in order to understand the context of the tooltip without having to pass the props twice. Otherwise in the component that implements you you'd have to pass the same theme: <Tooltip theme="light">
<TooltipTable theme="light" />
</Tooltip>
// Nothing stopping you from doing:
<Tooltip theme="light">
<TooltipTable /> // If you don't pass the theme and have no context, the theme of the table will be "dark"
</Tooltip> There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ah okay, so this is basically forcing the TooltipTable to match the same theme as its parent Tooltip through the use of context, removing that burden from the developer who adds the |
||
|
||
return rows.length > 0 ? ( | ||
krishnaglick marked this conversation as resolved.
Show resolved
Hide resolved
|
||
<table className={theme === "light" ? styles.light : undefined}> | ||
<tbody> | ||
{rows?.map((cols) => ( | ||
<tr> | ||
{cols.map((col, index) => ( | ||
<td className={index === 0 ? styles.label : undefined}>{col}</td> | ||
))} | ||
</tr> | ||
))} | ||
</tbody> | ||
</table> | ||
) : null; | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
import { createContext, useContext } from "react"; | ||
|
||
import { TooltipContext } from "./types"; | ||
|
||
export const tooltipContext = createContext<TooltipContext | null>(null); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We use both camelCase and PascalCase for context's. We should decide one way or the other just for consistency. |
||
|
||
export const useTooltipContext = () => { | ||
const ctx = useContext(tooltipContext); | ||
|
||
if (!ctx) { | ||
throw new Error("useTooltipContext should be used within tooltipContext.Provider"); | ||
} | ||
|
||
return ctx; | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,57 @@ | ||
import { ComponentStory, ComponentMeta } from "@storybook/react"; | ||
|
||
import { ToolTip } from "./ToolTip"; | ||
import { TooltipLearnMoreLink } from "./TooltipLearnMoreLink"; | ||
import { TooltipTable } from "./TooltipTable"; | ||
|
||
export default { | ||
title: "Ui/ToolTip", | ||
component: ToolTip, | ||
argTypes: { | ||
control: { type: { name: "string", required: true } }, | ||
children: { type: { name: "string", required: true } }, | ||
}, | ||
} as ComponentMeta<typeof ToolTip>; | ||
|
||
const Template: ComponentStory<typeof ToolTip> = (args) => ( | ||
<div style={{ display: "flex", height: "100%", alignItems: "center", justifyContent: "center" }}> | ||
<ToolTip {...args} /> | ||
</div> | ||
); | ||
|
||
export const Primary = Template.bind({}); | ||
Primary.args = { | ||
control: "Hover to see Tooltip", | ||
children: ( | ||
<> | ||
Looking for a job?{" "} | ||
<a href="https://www.airbyte.com/careers" target="_blank" rel="noreferrer"> | ||
Apply at Airbyte! | ||
</a> | ||
</> | ||
), | ||
}; | ||
|
||
export const WithLearnMoreUrl = Template.bind({}); | ||
WithLearnMoreUrl.args = { | ||
control: "Hover to see Tooltip with Body", | ||
children: ( | ||
<> | ||
Airbyte is hiring! <TooltipLearnMoreLink url="https://www.airbyte.com/careers" /> | ||
</> | ||
), | ||
}; | ||
|
||
export const WithTable = Template.bind({}); | ||
WithTable.args = { | ||
control: "Hover to see Tooltip with Table", | ||
children: ( | ||
<TooltipTable | ||
rows={[ | ||
["String", "Value"], | ||
["Number", 32768], | ||
["With a longer label", "And here is a longer value"], | ||
]} | ||
/> | ||
), | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
While reviewing the design with @Upmitt, it was decided that it didn't make sense to show the help cursor here.