Skip to content

Commit fa36df9

Browse files
committed
feat: advanced tooltip positioning
1 parent 9b6ae29 commit fa36df9

File tree

2 files changed

+106
-25
lines changed

2 files changed

+106
-25
lines changed

example/src/stories/Tooltip.stories.tsx

Lines changed: 39 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,47 @@ 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+
},
1948
},
2049
} as Meta<typeof Tooltip>
2150

22-
const Template: StoryFn<typeof Tooltip> = (args) => <Tooltip {...args} />
51+
const Template: StoryFn<typeof Tooltip> = (args) => (
52+
<Centering
53+
style={{
54+
height: 200,
55+
}}
56+
>
57+
<Tooltip {...args} />
58+
</Centering>
59+
)
2360

2461
export const Default = Template.bind({})
2562
Default.args = {

src/components/Tooltip.tsx

Lines changed: 67 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import {
1313
} from '@floating-ui/react'
1414
import { AnimatePresence, motion } from 'framer-motion'
1515
import { transparentize } from 'polished'
16-
import React, { ReactNode, useRef, useState } from 'react'
16+
import React, { CSSProperties, ReactNode, useRef, useState } from 'react'
1717
import { SolvedTheme, solvedThemes } from '../styles'
1818
import { Card, CardProps } from './Card'
1919

@@ -53,10 +53,20 @@ 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
6070
} & (
6171
| {
6272
noDefaultStyles: false
@@ -66,15 +76,56 @@ export type TooltipProps = {
6676
})
6777
)
6878

79+
const resolveArrowStyles = (
80+
arrowX: number | undefined | null,
81+
arrowY: number | undefined | null,
82+
arrowPosition: 'top' | 'bottom' | 'left' | 'right',
83+
padding = 16
84+
): CSSProperties => {
85+
if (arrowPosition === 'bottom') {
86+
return {
87+
left: arrowX ?? undefined,
88+
bottom: -padding,
89+
transform: `scaleY(-1)`,
90+
}
91+
}
92+
if (arrowPosition === 'top') {
93+
return {
94+
left: arrowX ?? undefined,
95+
top: -padding,
96+
}
97+
}
98+
if (arrowPosition === 'left') {
99+
return {
100+
top: arrowY ?? undefined,
101+
left: -16,
102+
transform: `rotate(-90deg)`,
103+
}
104+
}
105+
if (arrowPosition === 'right') {
106+
return {
107+
top: arrowY ?? undefined,
108+
right: -16,
109+
transform: `rotate(90deg)`,
110+
}
111+
}
112+
return {}
113+
}
114+
69115
export const Tooltip: React.FC<TooltipProps> = (props) => {
70116
const {
71117
title,
72118
theme,
73119
noDefaultStyles: noBackground,
74120
children,
121+
arrow: drawArrow = true,
122+
keepOpen = false,
123+
place,
75124
...cardProps
76125
} = props
77126
const [isOpen, setIsOpen] = useState(false)
127+
const renderTooltip = keepOpen || isOpen
128+
78129
const arrowRef = useRef(null)
79130

80131
const {
@@ -86,13 +137,14 @@ export const Tooltip: React.FC<TooltipProps> = (props) => {
86137
placement,
87138
middlewareData: { arrow: { x: arrowX, y: arrowY } = {} },
88139
} = useFloating({
140+
placement: place,
89141
strategy: 'fixed',
90142
open: isOpen,
91143
onOpenChange: setIsOpen,
92144
middleware: [
93-
offset(8),
145+
offset(16),
146+
shift({ padding: 16 }),
94147
flip(),
95-
shift({ padding: 8 }),
96148
arrow({ element: arrowRef }),
97149
],
98150
whileElementsMounted: (reference, floating, update) =>
@@ -112,6 +164,8 @@ export const Tooltip: React.FC<TooltipProps> = (props) => {
112164
const arrowPosition =
113165
renderSide[placement.split('-')[0] as keyof typeof renderSide]
114166

167+
console.log(arrowPosition)
168+
115169
return (
116170
<React.Fragment>
117171
<TooltipWrapper ref={refs.setReference} {...getReferenceProps()}>
@@ -120,7 +174,7 @@ export const Tooltip: React.FC<TooltipProps> = (props) => {
120174
<FloatingPortal>
121175
<ThemeProvider theme={theme || solvedThemes.dark}>
122176
<AnimatePresence>
123-
{isOpen && (
177+
{renderTooltip && (
124178
<React.Fragment>
125179
<RenderComponent
126180
ref={refs.setFloating}
@@ -133,27 +187,17 @@ export const Tooltip: React.FC<TooltipProps> = (props) => {
133187
})}
134188
{...cardProps}
135189
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 }}
190+
initial={{ opacity: 0, scale: 0.9 }}
191+
animate={{ opacity: 1, scale: 1 }}
192+
exit={{ opacity: 0, scale: 0.9 }}
139193
>
140194
{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-
/>
195+
{drawArrow && (
196+
<Arrow
197+
ref={arrowRef}
198+
style={resolveArrowStyles(arrowX, arrowY, arrowPosition)}
199+
/>
200+
)}
157201
</RenderComponent>
158202
</React.Fragment>
159203
)}

0 commit comments

Comments
 (0)