Skip to content

Commit ea55459

Browse files
authored
Merge pull request #61 from solved-ac/feature/advanced-tooltip
Advanced tooltips
2 parents 9b6ae29 + ff66221 commit ea55459

File tree

2 files changed

+118
-26
lines changed

2 files changed

+118
-26
lines changed

example/src/stories/Tooltip.stories.tsx

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Button, Tooltip } from '@solved-ac/ui-react'
1+
import { Button, Centering, Tooltip } from '@solved-ac/ui-react'
22
import { Meta, StoryFn } from '@storybook/react'
33
import React from 'react'
44

@@ -16,10 +16,53 @@ export default {
1616
description: 'Whether to use the default styles',
1717
defaultValue: false,
1818
},
19+
arrow: {
20+
control: 'boolean',
21+
description: 'Whether to show the arrow',
22+
defaultValue: true,
23+
},
24+
keepOpen: {
25+
control: 'boolean',
26+
description: 'Whether to keep the tooltip open',
27+
defaultValue: false,
28+
},
29+
place: {
30+
control: 'select',
31+
options: [
32+
'top',
33+
'top-start',
34+
'top-end',
35+
'right',
36+
'right-start',
37+
'right-end',
38+
'bottom',
39+
'bottom-start',
40+
'bottom-end',
41+
'left',
42+
'left-start',
43+
'left-end',
44+
],
45+
description: 'The placement of the tooltip',
46+
defaultValue: 'top',
47+
},
48+
interactive: {
49+
control: 'boolean',
50+
description:
51+
'Whether to make the tooltip interactive - if set to true, the tooltip contents will receive pointer events',
52+
defaultValue: false,
53+
},
1954
},
2055
} as Meta<typeof Tooltip>
2156

22-
const Template: StoryFn<typeof Tooltip> = (args) => <Tooltip {...args} />
57+
const Template: StoryFn<typeof Tooltip> = (args) => (
58+
<Centering
59+
style={{
60+
height: 200,
61+
}}
62+
>
63+
<Tooltip {...args} />
64+
</Centering>
65+
)
2366

2467
export const Default = Template.bind({})
2568
Default.args = {

src/components/Tooltip.tsx

Lines changed: 73 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,15 @@ import {
66
flip,
77
FloatingPortal,
88
offset,
9+
safePolygon,
910
shift,
1011
useFloating,
1112
useHover,
1213
useInteractions,
1314
} from '@floating-ui/react'
1415
import { AnimatePresence, motion } from 'framer-motion'
1516
import { transparentize } from 'polished'
16-
import React, { ReactNode, useRef, useState } from 'react'
17+
import React, { CSSProperties, ReactNode, useRef, useState } from 'react'
1718
import { SolvedTheme, solvedThemes } from '../styles'
1819
import { Card, CardProps } from './Card'
1920

@@ -27,7 +28,6 @@ const TooltipContainer = styled(motion(Card))`
2728
border: ${({ theme }) => theme.styles.border()};
2829
box-shadow: ${({ theme }) => theme.styles.shadow(undefined, 16)};
2930
z-index: 30000;
30-
pointer-events: none;
3131
backdrop-filter: blur(4px);
3232
font-size: initial;
3333
font-weight: initial;
@@ -53,10 +53,21 @@ const renderSide = {
5353
left: 'right',
5454
} as const
5555

56+
type TooltipPlacementBasic = 'top' | 'right' | 'bottom' | 'left'
57+
type TooltipPlacementRelative = 'start' | 'end'
58+
59+
export type TooltipPlacement =
60+
| `${TooltipPlacementBasic}-${TooltipPlacementRelative}`
61+
| TooltipPlacementBasic
62+
5663
export type TooltipProps = {
5764
title?: ReactNode
5865
theme?: SolvedTheme
5966
children?: ReactNode
67+
arrow?: boolean
68+
keepOpen?: boolean
69+
place?: TooltipPlacement
70+
interactive?: boolean
6071
} & (
6172
| {
6273
noDefaultStyles: false
@@ -66,15 +77,57 @@ export type TooltipProps = {
6677
})
6778
)
6879

80+
const resolveArrowStyles = (
81+
arrowX: number | undefined | null,
82+
arrowY: number | undefined | null,
83+
arrowPosition: 'top' | 'bottom' | 'left' | 'right',
84+
padding = 16
85+
): CSSProperties => {
86+
if (arrowPosition === 'bottom') {
87+
return {
88+
left: arrowX ?? undefined,
89+
bottom: -padding,
90+
transform: `scaleY(-1)`,
91+
}
92+
}
93+
if (arrowPosition === 'top') {
94+
return {
95+
left: arrowX ?? undefined,
96+
top: -padding,
97+
}
98+
}
99+
if (arrowPosition === 'left') {
100+
return {
101+
top: arrowY ?? undefined,
102+
left: -16,
103+
transform: `rotate(-90deg)`,
104+
}
105+
}
106+
if (arrowPosition === 'right') {
107+
return {
108+
top: arrowY ?? undefined,
109+
right: -16,
110+
transform: `rotate(90deg)`,
111+
}
112+
}
113+
return {}
114+
}
115+
69116
export const Tooltip: React.FC<TooltipProps> = (props) => {
70117
const {
71118
title,
72119
theme,
73120
noDefaultStyles: noBackground,
74121
children,
122+
arrow: drawArrow = true,
123+
keepOpen = false,
124+
place,
125+
interactive = false,
75126
...cardProps
76127
} = props
77128
const [isOpen, setIsOpen] = useState(false)
129+
const renderTooltip = keepOpen || isOpen
130+
78131
const arrowRef = useRef(null)
79132

80133
const {
@@ -86,13 +139,14 @@ export const Tooltip: React.FC<TooltipProps> = (props) => {
86139
placement,
87140
middlewareData: { arrow: { x: arrowX, y: arrowY } = {} },
88141
} = useFloating({
142+
placement: place,
89143
strategy: 'fixed',
90144
open: isOpen,
91145
onOpenChange: setIsOpen,
92146
middleware: [
93-
offset(8),
147+
offset(16),
148+
shift({ padding: 16 }),
94149
flip(),
95-
shift({ padding: 8 }),
96150
arrow({ element: arrowRef }),
97151
],
98152
whileElementsMounted: (reference, floating, update) =>
@@ -104,6 +158,10 @@ export const Tooltip: React.FC<TooltipProps> = (props) => {
104158
const { getReferenceProps, getFloatingProps } = useInteractions([
105159
useHover(context, {
106160
delay: 200,
161+
move: true,
162+
handleClose: safePolygon({
163+
buffer: 1,
164+
}),
107165
}),
108166
])
109167

@@ -120,7 +178,7 @@ export const Tooltip: React.FC<TooltipProps> = (props) => {
120178
<FloatingPortal>
121179
<ThemeProvider theme={theme || solvedThemes.dark}>
122180
<AnimatePresence>
123-
{isOpen && (
181+
{renderTooltip && (
124182
<React.Fragment>
125183
<RenderComponent
126184
ref={refs.setFloating}
@@ -129,31 +187,22 @@ export const Tooltip: React.FC<TooltipProps> = (props) => {
129187
position: strategy,
130188
top: y || 0,
131189
left: x || 0,
190+
pointerEvents: interactive ? 'auto' : 'none',
132191
},
133192
})}
134193
{...cardProps}
135194
transition={{ duration: 0.2, ease: 'easeInOut' }}
136-
initial={{ opacity: 0, y: 0, scale: 0.9 }}
137-
animate={{ opacity: 1, y: 8, scale: 1 }}
138-
exit={{ opacity: 0, y: 0, scale: 0.9 }}
195+
initial={{ opacity: 0, scale: 0.9 }}
196+
animate={{ opacity: 1, scale: 1 }}
197+
exit={{ opacity: 0, scale: 0.9 }}
139198
>
140199
{title}
141-
<Arrow
142-
ref={arrowRef}
143-
style={
144-
arrowPosition === 'bottom'
145-
? {
146-
left: arrowX ?? undefined,
147-
[arrowPosition]: -16,
148-
transform: `scaleY(-1)`,
149-
}
150-
: {
151-
top:
152-
arrowY !== null ? (arrowY || 0) - 16 : undefined,
153-
left: arrowX ?? undefined,
154-
}
155-
}
156-
/>
200+
{drawArrow && (
201+
<Arrow
202+
ref={arrowRef}
203+
style={resolveArrowStyles(arrowX, arrowY, arrowPosition)}
204+
/>
205+
)}
157206
</RenderComponent>
158207
</React.Fragment>
159208
)}

0 commit comments

Comments
 (0)