Skip to content

Commit a616fcf

Browse files
authored
Tooltip component (#3677)
* create draft 1 of tooltip * the monster is alive * forgot to animate the arrow * maintain backward compat for like 5mins
1 parent 39bce41 commit a616fcf

File tree

7 files changed

+323
-58
lines changed

7 files changed

+323
-58
lines changed

packages/components/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
"@codesandbox/template-icons": "^1.1.0",
3333
"@reach/auto-id": "^0.7.1",
3434
"@reach/menu-button": "^0.8.5",
35+
"@reach/tooltip": "^0.8.6",
3536
"@reach/visually-hidden": "^0.7.0",
3637
"@styled-system/css": "^5.1.4",
3738
"codesandbox-api": "0.0.24",
Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,18 @@
11
import React from 'react';
22
import { IconButton } from '.';
3+
import { Stack } from '../..';
34

45
export default {
56
title: 'components/IconButton',
67
component: IconButton,
78
};
89

9-
export const Basic = () => <IconButton name="filter" />;
10+
export const Basic = () => (
11+
<Stack justify="center">
12+
<IconButton label="Filter elements" name="filter" />
13+
</Stack>
14+
);
1015

11-
export const Disabled = () => <IconButton disabled name="filter" />;
16+
export const Disabled = () => (
17+
<IconButton label="Filter elements disabled" disabled name="filter" />
18+
);

packages/components/src/components/IconButton/index.tsx

Lines changed: 24 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import React from 'react';
33
import deepmerge from 'deepmerge';
44
import { Button } from '../Button';
55
import { Icon, IconNames } from '../Icon';
6+
import { Tooltip } from '../Tooltip';
67

78
type IconButtonProps = React.ButtonHTMLAttributes<HTMLButtonElement> & {
89
/** name of the icon */
@@ -21,27 +22,28 @@ export const IconButton: React.FC<IconButtonProps> = ({
2122
...props
2223
}) => (
2324
// @ts-ignore
24-
<Button
25-
title={title}
26-
variant="link"
27-
css={deepmerge(
28-
{
29-
width: '26px', // same width as (height of the button)
30-
padding: 0,
31-
borderRadius: '50%',
32-
':hover:not(:disabled)': {
33-
backgroundColor: 'secondaryButton.background',
25+
<Tooltip label={title}>
26+
<Button
27+
variant="link"
28+
css={deepmerge(
29+
{
30+
width: '26px', // same width as (height of the button)
31+
padding: 0,
32+
borderRadius: '50%',
33+
':hover:not(:disabled)': {
34+
backgroundColor: 'secondaryButton.background',
35+
},
36+
':focus:not(:disabled)': {
37+
outline: 'none',
38+
backgroundColor: 'secondaryButton.background',
39+
},
3440
},
35-
':focus:not(:disabled)': {
36-
outline: 'none',
37-
backgroundColor: 'secondaryButton.background',
38-
},
39-
},
40-
css
41-
)}
42-
// @ts-ignore
43-
{...props}
44-
>
45-
<Icon name={name} size={size} />
46-
</Button>
41+
css
42+
)}
43+
// @ts-ignore
44+
{...props}
45+
>
46+
<Icon name={name} size={size} />
47+
</Button>
48+
</Tooltip>
4749
);

packages/components/src/components/Menu/index.tsx

Lines changed: 34 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -26,42 +26,41 @@ const transitions = {
2626

2727
const MenuContext = React.createContext({ trigger: null });
2828

29-
const Menu = ({ ...props }) => {
30-
const PortalStyles = createGlobalStyle(
31-
css({
32-
'[data-reach-menu]': {
33-
zIndex: 2,
34-
},
35-
'[data-reach-menu-list][data-component=MenuList]': {
36-
minWidth: 100,
37-
backgroundColor: 'menuList.background',
38-
borderRadius: 3,
39-
boxShadow: 2,
40-
overflow: 'hidden',
41-
border: '1px solid',
42-
borderColor: 'menuList.border',
43-
':focus': { outline: 'none' },
44-
transform: 'translateY(4px)',
45-
// override reach ui styles
46-
padding: 0,
47-
},
48-
'[data-reach-menu-item][data-component=MenuItem]': {
49-
fontSize: 2,
50-
paddingY: 2,
51-
paddingX: 3,
52-
cursor: 'pointer',
29+
const PortalStyles = createGlobalStyle(
30+
css({
31+
'[data-reach-menu]': {
32+
zIndex: 2,
33+
},
34+
'[data-reach-menu-list][data-component=MenuList]': {
35+
minWidth: 100,
36+
backgroundColor: 'menuList.background',
37+
borderRadius: 3,
38+
boxShadow: 2,
39+
overflow: 'hidden',
40+
border: '1px solid',
41+
borderColor: 'menuList.border',
42+
':focus': { outline: 'none' },
43+
transform: 'translateY(4px)',
44+
// override reach ui styles
45+
padding: 0,
46+
},
47+
'[data-reach-menu-item][data-component=MenuItem]': {
48+
fontSize: 2,
49+
paddingY: 2,
50+
paddingX: 3,
51+
cursor: 'pointer',
52+
outline: 'none',
53+
color: 'menuList.foreground',
54+
'&[data-selected]': {
5355
outline: 'none',
56+
backgroundColor: 'menuList.hoverBackground',
5457
color: 'menuList.foreground',
55-
'&[data-selected]': {
56-
outline: 'none',
57-
backgroundColor: 'menuList.hoverBackground',
58-
color: 'menuList.foreground',
59-
},
60-
// override reach ui styles
61-
font: 'ineherit',
6258
},
63-
}),
64-
styledcss`
59+
// override reach ui styles
60+
font: 'ineherit',
61+
},
62+
}),
63+
styledcss`
6564
[data-reach-menu-list][data-trigger=MenuButton] {
6665
animation: ${transitions.slide} 150ms ease-out;
6766
transform-origin: top;
@@ -71,8 +70,9 @@ const Menu = ({ ...props }) => {
7170
transform-origin: top left;
7271
}
7372
`
74-
);
73+
);
7574

75+
const Menu = ({ ...props }) => {
7676
const trigger = props.children[0].type.name;
7777

7878
return (
Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
import React from 'react';
2+
import { useTooltip, TooltipPopup } from '@reach/tooltip';
3+
import '@reach/tooltip/styles.css';
4+
import Portal from '@reach/portal';
5+
import {
6+
createGlobalStyle,
7+
keyframes,
8+
css as styledcss,
9+
} from 'styled-components';
10+
import css from '@styled-system/css';
11+
import { Element } from '../..';
12+
13+
/** Lots of dragons in this file
14+
*
15+
* There are portals, global styles, animations, css triangles
16+
*
17+
* Some of these dragons can be hidden in abstraction layers
18+
* of the system where they will sit tamed. These abstractions
19+
* will appear once when cover all components that can pop up
20+
* out of other components
21+
* (Menu, Tooltip, Dialog, Modal)
22+
*
23+
* Sidenote: the zIndexes in this component are merely suggestions,
24+
* we will need to tweak them according to our app
25+
*
26+
* Until we do, proceed with caution.
27+
*/
28+
29+
/** Dragon number 1:
30+
* keyframes in styled-components do not work with
31+
* object styles. So we wrap them in css from styled-components
32+
* and then pass it to GlobalStyles as a second parameter
33+
*/
34+
35+
const transitions = {
36+
slide: keyframes({
37+
from: {
38+
opacity: 0,
39+
transform: 'translateY(-2px)',
40+
},
41+
}),
42+
};
43+
44+
const animation = styledcss`
45+
[data-reach-tooltip][data-component=Tooltip] {
46+
animation: ${transitions.slide} 150ms ease-out;
47+
}
48+
[data-component=TooltipTriangle] {
49+
animation: ${transitions.slide} 150ms ease-out;
50+
}
51+
`;
52+
53+
/** Dragon number 2:
54+
* wait global styles?
55+
* Because tooltips are creating in a portal,
56+
* styles applied from our theme provider do not work
57+
* because they are outside that tree - portal
58+
* so we apply global styles with their [data-reach-name]
59+
*/
60+
61+
const TooltipStyles = createGlobalStyle(
62+
css({
63+
'[data-reach-tooltip][data-component=Tooltip]': {
64+
backgroundColor: 'grays.900',
65+
border: '1px solid',
66+
borderColor: 'grays.600',
67+
color: 'grays.100',
68+
borderRadius: 'medium',
69+
paddingX: 2,
70+
paddingY: 1,
71+
fontSize: 3,
72+
lineHeight: 1,
73+
zIndex: 3,
74+
75+
// multiline
76+
maxWidth: 160,
77+
whiteSpace: 'normal',
78+
textAlign: 'center',
79+
},
80+
}),
81+
animation
82+
);
83+
84+
/** Dragon number 3:
85+
* to attach a triangle and transitions to the tooltip,
86+
* we have to drop one abstraction level deeper and use
87+
* TooltipPopup and create the triangle in another component
88+
*/
89+
90+
const Tooltip = props => {
91+
const [trigger, tooltip] = useTooltip();
92+
const { isVisible, triggerRect } = tooltip;
93+
94+
return (
95+
<>
96+
<TooltipStyles />
97+
{React.cloneElement(props.children, trigger)}
98+
<TooltipPopup
99+
{...tooltip}
100+
data-component="Tooltip"
101+
label={props.label}
102+
position={centered}
103+
/>
104+
{isVisible && <Triangle triggerRect={triggerRect} />}
105+
</>
106+
);
107+
};
108+
109+
// center the tooltip with respect to the trigger
110+
const centered = (triggerRect, tooltipRect) => {
111+
const triggerCenter = triggerRect.left + triggerRect.width / 2;
112+
const left = triggerCenter - tooltipRect.width / 2;
113+
const maxLeft = window.innerWidth - tooltipRect.width - 2;
114+
return {
115+
left: Math.min(Math.max(2, left), maxLeft) + window.scrollX,
116+
top: triggerRect.bottom + 8 + window.scrollY,
117+
};
118+
};
119+
120+
/** Dragon number 4:
121+
* We use a span to create the first triangle and position
122+
* another triangle(:after) on top of it to get the border
123+
* Also, why is the triangle using a portal??
124+
* Using a Portal may seem a little extreme, but we can keep the
125+
* positioning logic simpler here instead of needing to consider
126+
* the popup's position relative to the trigger and collisions
127+
* Implementation taken from https://reacttraining.com/reach-ui/tooltip/
128+
*/
129+
130+
const Triangle = ({ triggerRect }) => (
131+
<Portal>
132+
<Element
133+
as="span"
134+
data-component="TooltipTriangle"
135+
css={{
136+
position: 'absolute',
137+
left:
138+
triggerRect &&
139+
triggerRect.left - 10 + triggerRect.width / 2 + 3 + 'px',
140+
top: triggerRect && triggerRect.bottom + window.scrollY - 4 + 'px',
141+
width: 0,
142+
height: 0,
143+
border: '6px solid transparent',
144+
borderBottomColor: 'grays.600',
145+
zIndex: 4, // one heigher than the tooltip itself
146+
':after': {
147+
content: " ' '",
148+
border: '6px solid transparent',
149+
borderBottomColor: 'grays.900',
150+
height: 0,
151+
width: 0,
152+
position: 'absolute',
153+
left: '-6px',
154+
top: '-4px',
155+
},
156+
}}
157+
/>
158+
</Portal>
159+
);
160+
161+
export { Tooltip };
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import React from 'react';
2+
3+
import { Tooltip } from '.';
4+
import { Stack, IconButton } from '../..';
5+
6+
export default {
7+
title: 'components/Tooltip',
8+
component: Tooltip,
9+
};
10+
11+
export const Simple = () => (
12+
<Stack justify="center">
13+
<Tooltip label="Mark as resolved">
14+
<span>hover over me</span>
15+
</Tooltip>
16+
</Stack>
17+
);
18+
19+
export const Long = () => (
20+
<Stack justify="center">
21+
<Tooltip label="Mark as resolved because now it's job is done">
22+
<span>hover over me</span>
23+
</Tooltip>
24+
</Stack>
25+
);
26+
27+
export const Edges = () => (
28+
<Stack justify="space-between">
29+
<Tooltip label="Mark as resolved">
30+
<span>hover</span>
31+
</Tooltip>
32+
<Tooltip label="Mark as resolved">
33+
<span>hover</span>
34+
</Tooltip>
35+
</Stack>
36+
);
37+
38+
export const IconButtonHasTooltip = () => (
39+
<IconButton name="check" label="Mark as resolved" />
40+
);

0 commit comments

Comments
 (0)