Skip to content

Commit e380c26

Browse files
authored
🪟 🎨 Update Tooltip component to match design library (#14816)
* Add ToolTip storybook * Update ToolTip to scss * gitignore storybook-static dir * Move InfoIcon from ToolTip to icons and remove duplicate * Fix disabled class name in ToolTip * Update ToolTip colors and spacing to match updated design and add light mode * Update ToolTip to show from any side perfectly centered * Add react-tether dependency * Update ToolTip to use react-tether * Add align property to ToolTip * Add link colors to tooltip links and update storybook * Fix font-size in Tooltip * Add minor tweaks to Tooltip component * Remove "help" cursor from state badge Update tooltip corner to 5px Update package-lock with react-tether * Tooltip mode -> Tooltip theme * Add tooltip helper components for learn more url and table * Add tooltip context * Update tooltip stories to demo with new components * REname tooltip index from tsx to ts * Update tooltip to use floating-ui instead of react-tether * Move z-index values to own z-indices file * Update InformationTooltip to use regular tooltip style
1 parent 313ac11 commit e380c26

File tree

22 files changed

+325
-98
lines changed

22 files changed

+325
-98
lines changed

airbyte-webapp/.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,5 +27,7 @@ yarn-error.log*
2727
.env.development
2828
.env.production
2929

30+
storybook-static/
31+
3032
# Ignore generated API client, since it's automatically generated
3133
/src/core/request/AirbyteClient.ts

airbyte-webapp/package-lock.json

Lines changed: 47 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

airbyte-webapp/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
"validate-links": "ts-node --skip-project ./scripts/validate-links.ts"
2323
},
2424
"dependencies": {
25+
"@floating-ui/react-dom": "^1.0.0",
2526
"@fortawesome/fontawesome-svg-core": "^6.1.1",
2627
"@fortawesome/free-brands-svg-icons": "^6.1.1",
2728
"@fortawesome/free-regular-svg-icons": "^6.1.1",

airbyte-webapp/src/components/ReleaseStageBadge/ReleaseStageBadge.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ export const ReleaseStageBadge: React.FC<ReleaseStageBadgeProps> = ({ stage, sma
4141
);
4242

4343
return tooltip ? (
44-
<ToolTip control={badge} cursor="help">
44+
<ToolTip control={badge}>
4545
<FormattedMessage id={`connector.releaseStage.${stage}.description`} />
4646
</ToolTip>
4747
) : (
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
@use "../../scss/colors";
2+
@use "../../scss/variables";
3+
@use "../../scss/z-indices";
4+
5+
.container {
6+
display: inline;
7+
position: relative;
8+
}
9+
10+
.tooltip {
11+
font-size: 12px;
12+
line-height: initial;
13+
14+
padding: variables.$spacing-md;
15+
border-radius: 5px;
16+
max-width: 300px;
17+
z-index: z-indices.$tooltip;
18+
box-shadow: 0px 2px 4px rgba(colors.$dark-blue, 0.12);
19+
background: rgba(colors.$dark-blue, 0.9);
20+
color: colors.$white;
21+
22+
a {
23+
color: rgba(colors.$white, 0.5);
24+
}
25+
26+
&.light {
27+
background: rgba(colors.$white, 0.9);
28+
color: colors.$dark-blue;
29+
30+
a {
31+
color: colors.$blue;
32+
}
33+
}
34+
}

airbyte-webapp/src/components/ToolTip/ToolTip.tsx

Lines changed: 77 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,51 +1,80 @@
1-
import React from "react";
2-
import styled from "styled-components";
3-
4-
interface ToolTipProps {
5-
control: React.ReactNode;
6-
className?: string;
7-
disabled?: boolean;
8-
cursor?: "pointer" | "help" | "not-allowed";
9-
}
10-
11-
const Control = styled.div<{ $cursor?: "pointer" | "help" | "not-allowed"; $showCursor?: boolean }>`
12-
display: inline;
13-
position: relative;
14-
${({ $cursor, $showCursor = true }) => ($showCursor && $cursor ? `cursor: ${$cursor}` : "")};
15-
`;
16-
17-
const ToolTipView = styled.div<{ $disabled?: boolean }>`
18-
display: none;
19-
font-size: 14px;
20-
line-height: initial;
21-
position: absolute;
22-
padding: 9px 8px 8px;
23-
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);
24-
border-radius: 4px;
25-
background: rgba(26, 26, 33, 0.9);
26-
color: ${({ theme }) => theme.whiteColor};
27-
top: calc(100% + 10px);
28-
left: -50px;
29-
min-width: 100px;
30-
width: max-content;
31-
max-width: 380px;
32-
z-index: 10;
33-
34-
div:hover > &&,
35-
&&:hover {
36-
display: ${({ $disabled }) => ($disabled ? "none" : "block")};
37-
}
38-
`;
39-
40-
const ToolTip: React.FC<ToolTipProps> = ({ children, control, className, disabled, cursor }) => {
1+
import { flip, offset, shift, useFloating } from "@floating-ui/react-dom";
2+
import classNames from "classnames";
3+
import React, { useState, useEffect } from "react";
4+
5+
import { tooltipContext } from "./context";
6+
import styles from "./ToolTip.module.scss";
7+
import { ToolTipProps } from "./types";
8+
9+
const MOUSE_OUT_TIMEOUT_MS = 50;
10+
11+
export const ToolTip: React.FC<ToolTipProps> = (props) => {
12+
const { children, control, className, disabled, cursor, theme = "dark", placement = "bottom" } = props;
13+
14+
const [isMouseOver, setIsMouseOver] = useState(false);
15+
const [isVisible, setIsVisible] = useState(false);
16+
17+
const { x, y, reference, floating, strategy } = useFloating({
18+
placement,
19+
middleware: [
20+
offset(5), // $spacing-sm
21+
flip(),
22+
shift(),
23+
],
24+
});
25+
26+
useEffect(() => {
27+
if (isMouseOver) {
28+
setIsVisible(true);
29+
return;
30+
}
31+
32+
const timeout = window.setTimeout(() => {
33+
setIsVisible(false);
34+
}, MOUSE_OUT_TIMEOUT_MS);
35+
36+
return () => {
37+
window.clearTimeout(timeout);
38+
};
39+
}, [isMouseOver]);
40+
41+
const canShowTooltip = isVisible && !disabled;
42+
43+
const onMouseOver = () => {
44+
setIsMouseOver(true);
45+
};
46+
47+
const onMouseOut = () => {
48+
setIsMouseOver(false);
49+
};
50+
4151
return (
42-
<Control $cursor={cursor} $showCursor={!disabled}>
43-
{control}
44-
<ToolTipView className={className} $disabled={disabled}>
45-
{children}
46-
</ToolTipView>
47-
</Control>
52+
<>
53+
<div
54+
ref={reference}
55+
className={styles.container}
56+
style={disabled ? undefined : { cursor }}
57+
onMouseOver={onMouseOver}
58+
onMouseOut={onMouseOut}
59+
>
60+
{control}
61+
</div>
62+
{canShowTooltip && (
63+
<div
64+
role="tooltip"
65+
ref={floating}
66+
className={classNames(styles.tooltip, theme === "light" && styles.light, className)}
67+
style={{
68+
position: strategy,
69+
top: y ?? 0,
70+
left: x ?? 0,
71+
}}
72+
onMouseOver={onMouseOver}
73+
onMouseOut={onMouseOut}
74+
>
75+
<tooltipContext.Provider value={props}>{children}</tooltipContext.Provider>
76+
</div>
77+
)}
78+
</>
4879
);
4980
};
50-
51-
export default ToolTip;
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
@use "../../scss/variables";
2+
3+
.container {
4+
margin-top: variables.$spacing-md;
5+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { FormattedMessage } from "react-intl";
2+
3+
import styles from "./TooltipLearnMoreLink.module.scss";
4+
5+
interface TooltipLearnMoreLinkProps {
6+
url: string;
7+
}
8+
9+
export const TooltipLearnMoreLink: React.VFC<TooltipLearnMoreLinkProps> = ({ url }) => (
10+
<div className={styles.container}>
11+
<a href={url} target="_blank" rel="noreferrer">
12+
<FormattedMessage id="ui.learnMore" />
13+
</a>
14+
</div>
15+
);
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
@use "../../scss/colors";
2+
@use "../../scss/variables";
3+
4+
.label {
5+
color: rgba(colors.$white, 0.7);
6+
padding-right: variables.$spacing-sm;
7+
}
8+
9+
.light {
10+
.label {
11+
color: rgba(colors.$dark-blue, 0.7);
12+
}
13+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { useTooltipContext } from "./context";
2+
import styles from "./TooltipTable.module.scss";
3+
4+
interface TooltipTableProps {
5+
rows: React.ReactNode[][];
6+
}
7+
8+
export const TooltipTable: React.VFC<TooltipTableProps> = ({ rows }) => {
9+
const { theme } = useTooltipContext();
10+
11+
return rows.length > 0 ? (
12+
<table className={theme === "light" ? styles.light : undefined}>
13+
<tbody>
14+
{rows?.map((cols) => (
15+
<tr>
16+
{cols.map((col, index) => (
17+
<td className={index === 0 ? styles.label : undefined}>{col}</td>
18+
))}
19+
</tr>
20+
))}
21+
</tbody>
22+
</table>
23+
) : null;
24+
};
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { createContext, useContext } from "react";
2+
3+
import { TooltipContext } from "./types";
4+
5+
export const tooltipContext = createContext<TooltipContext | null>(null);
6+
7+
export const useTooltipContext = () => {
8+
const ctx = useContext(tooltipContext);
9+
10+
if (!ctx) {
11+
throw new Error("useTooltipContext should be used within tooltipContext.Provider");
12+
}
13+
14+
return ctx;
15+
};
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import { ComponentStory, ComponentMeta } from "@storybook/react";
2+
3+
import { ToolTip } from "./ToolTip";
4+
import { TooltipLearnMoreLink } from "./TooltipLearnMoreLink";
5+
import { TooltipTable } from "./TooltipTable";
6+
7+
export default {
8+
title: "Ui/ToolTip",
9+
component: ToolTip,
10+
argTypes: {
11+
control: { type: { name: "string", required: true } },
12+
children: { type: { name: "string", required: true } },
13+
},
14+
} as ComponentMeta<typeof ToolTip>;
15+
16+
const Template: ComponentStory<typeof ToolTip> = (args) => (
17+
<div style={{ display: "flex", height: "100%", alignItems: "center", justifyContent: "center" }}>
18+
<ToolTip {...args} />
19+
</div>
20+
);
21+
22+
export const Primary = Template.bind({});
23+
Primary.args = {
24+
control: "Hover to see Tooltip",
25+
children: (
26+
<>
27+
Looking for a job?{" "}
28+
<a href="https://www.airbyte.com/careers" target="_blank" rel="noreferrer">
29+
Apply at Airbyte!
30+
</a>
31+
</>
32+
),
33+
};
34+
35+
export const WithLearnMoreUrl = Template.bind({});
36+
WithLearnMoreUrl.args = {
37+
control: "Hover to see Tooltip with Body",
38+
children: (
39+
<>
40+
Airbyte is hiring! <TooltipLearnMoreLink url="https://www.airbyte.com/careers" />
41+
</>
42+
),
43+
};
44+
45+
export const WithTable = Template.bind({});
46+
WithTable.args = {
47+
control: "Hover to see Tooltip with Table",
48+
children: (
49+
<TooltipTable
50+
rows={[
51+
["String", "Value"],
52+
["Number", 32768],
53+
["With a longer label", "And here is a longer value"],
54+
]}
55+
/>
56+
),
57+
};

0 commit comments

Comments
 (0)