Skip to content
This repository was archived by the owner on Mar 25, 2025. It is now read-only.

Commit 096fa13

Browse files
boilundTigge
authored andcommitted
feat(tooltip): support for left-right variant of expanded tooltip
1 parent 2b0373f commit 096fa13

File tree

6 files changed

+280
-75
lines changed

6 files changed

+280
-75
lines changed

packages/core/src/Tooltip/__snapshots__/index.test.tsx.snap

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,3 +152,54 @@ exports[`Tooltip Expanded 2`] = `
152152
</div>
153153
</div>
154154
`;
155+
156+
exports[`Tooltip Expanded 3`] = `
157+
.c3 {
158+
position: fixed;
159+
top: 0;
160+
width: 360px;
161+
border-radius: 4px;
162+
margin: 8px 16px;
163+
right: 0;
164+
}
165+
166+
.c0 {
167+
color: rgb(41,41,41);
168+
}
169+
170+
.c1 {
171+
position: absolute;
172+
z-index: 0;
173+
}
174+
175+
.c2 {
176+
position: absolute;
177+
z-index: 1;
178+
}
179+
180+
<div
181+
className="c0"
182+
id="practical-root"
183+
>
184+
<div>
185+
<div
186+
className="c1"
187+
id="layer-7"
188+
>
189+
<span>
190+
Test
191+
</span>
192+
</div>
193+
</div>
194+
<div>
195+
<div
196+
className="c2"
197+
id="layer-8"
198+
>
199+
<div
200+
className="c3"
201+
/>
202+
</div>
203+
</div>
204+
</div>
205+
`;

packages/core/src/Tooltip/index.test.tsx

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,5 +45,21 @@ describe('Tooltip', () => {
4545
</Tooltip>
4646
)
4747
expect(tree2).toMatchSnapshot()
48+
49+
const tree3 = TestRender(
50+
<Tooltip
51+
variant="expanded"
52+
placement="left-right"
53+
tipTitle="title"
54+
contents={
55+
<ExpandedTooltipTypography>
56+
Expanded tooltip test text
57+
</ExpandedTooltipTypography>
58+
}
59+
>
60+
<TestTextWithForwardRef />
61+
</Tooltip>
62+
)
63+
expect(tree3).toMatchSnapshot()
4864
})
4965
})

packages/core/src/Tooltip/index.tsx

Lines changed: 148 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ const BaseTooltipWrapper = styled.div`
3636
const TooltipWrapper = styled(BaseTooltipWrapper)`
3737
align-items: center;
3838
39-
margin-top: ${spacing.small};
39+
margin: ${spacing.small};
4040
padding: ${spacing.small} ${spacing.medium};
4141
4242
min-height: ${componentSize.mini};
@@ -64,7 +64,7 @@ const ExpandedTooltipWrapper = styled(BaseTooltipWrapper)`
6464
align-items: flex-start;
6565
gap: ${spacing.medium};
6666
67-
margin: ${spacing.medium} ${spacing.small};
67+
margin: ${spacing.medium};
6868
padding: ${spacing.medium};
6969
7070
height: auto;
@@ -91,6 +91,7 @@ const ExpandedTooltipTitle = styled(Typography).attrs({
9191
variant: 'chip-tag-text',
9292
})`
9393
font-weight: ${font.fontWeight.semibold};
94+
white-space: nowrap;
9495
`
9596

9697
const ExpandedTooltipExtraInfo = styled(Typography).attrs({
@@ -109,23 +110,98 @@ export const ExpandedTooltipTypography: React.FC<
109110
<StyledExpandedTooltipTypography>{children}</StyledExpandedTooltipTypography>
110111
)
111112

112-
const ToolTipUpArrow = styled.div`
113+
const upDownArrowBase = css`
113114
width: 0;
114115
height: 0;
115-
margin: 3px ${spacing.medium} 0 ${spacing.medium};
116116
border-left: 5px solid transparent;
117117
border-right: 5px solid transparent;
118+
`
119+
const TooltipUpArrow = styled.div`
120+
${upDownArrowBase};
121+
margin-top: 3px;
118122
border-bottom: 5px solid ${({ theme }) => theme.color.background00()};
119123
`
120-
const ToolTipDownArrow = styled.div`
124+
const TooltipDownArrow = styled.div`
125+
${upDownArrowBase};
126+
margin-bottom: 3px;
127+
border-top: 5px solid ${({ theme }) => theme.color.background00()};
128+
`
129+
130+
const leftRightArrowBase = css`
121131
width: 0;
122132
height: 0;
123-
margin: 0 ${spacing.medium} 3px ${spacing.medium};
124-
border-left: 5px solid transparent;
125-
border-right: 5px solid transparent;
126-
border-top: 5px solid ${({ theme }) => theme.color.background00()};
133+
border-top: 5px solid transparent;
134+
border-bottom: 5px solid transparent;
135+
`
136+
const TooltipLeftArrow = styled.div`
137+
${leftRightArrowBase};
138+
margin-left: 3px;
139+
border-right: 5px solid ${({ theme }) => theme.color.background00()};
140+
`
141+
const TooltipRightArrow = styled.div`
142+
${leftRightArrowBase};
143+
margin-right: 3px;
144+
border-left: 5px solid ${({ theme }) => theme.color.background00()};
127145
`
128146

147+
type Placement = 'up' | 'right' | 'down' | 'left'
148+
149+
const pointInBounds = (pos: readonly [number, number]) =>
150+
pos[0] >= 0 &&
151+
pos[0] <= document.documentElement.clientWidth &&
152+
pos[1] >= 0 &&
153+
pos[1] <= document.documentElement.clientHeight
154+
155+
const rectInBounds = (
156+
pos: readonly [number, number],
157+
size: readonly [number, number]
158+
) => pointInBounds(pos) && pointInBounds([pos[0] + size[0], pos[1] + size[1]])
159+
160+
const alignments: Record<
161+
Placement,
162+
Required<
163+
Pick<
164+
PopOverProps,
165+
| 'horizontalPosition'
166+
| 'horizontalAlignment'
167+
| 'verticalPosition'
168+
| 'verticalAlignment'
169+
>
170+
>
171+
> = {
172+
up: {
173+
horizontalPosition: 'center',
174+
horizontalAlignment: 'center',
175+
verticalPosition: 'top',
176+
verticalAlignment: 'bottom',
177+
},
178+
down: {
179+
horizontalPosition: 'center',
180+
horizontalAlignment: 'center',
181+
verticalPosition: 'bottom',
182+
verticalAlignment: 'top',
183+
},
184+
left: {
185+
horizontalPosition: 'left',
186+
horizontalAlignment: 'right',
187+
verticalPosition: 'center',
188+
verticalAlignment: 'center',
189+
},
190+
right: {
191+
horizontalPosition: 'right',
192+
horizontalAlignment: 'left',
193+
verticalPosition: 'center',
194+
verticalAlignment: 'center',
195+
},
196+
}
197+
198+
const arrows: Record<Placement, ReactElement> = {
199+
left: <TooltipRightArrow />,
200+
right: <TooltipLeftArrow />,
201+
up: <TooltipDownArrow />,
202+
down: <TooltipUpArrow />,
203+
}
204+
129205
interface TooltipProps extends Omit<PopOverProps, 'anchorEl'> {
130206
/**
131207
* Optional Tooltip variant.
@@ -143,6 +219,11 @@ interface ExpandedTooltipProps extends Omit<PopOverProps, 'anchorEl'> {
143219
* Required Tooltip variant.
144220
*/
145221
readonly variant: 'expanded'
222+
/**
223+
* Optional placement.
224+
* Default: `up-down`
225+
*/
226+
readonly placement?: 'up-down' | 'left-right'
146227
/**
147228
* Optional semibold title text inside the tooltip.
148229
*/
@@ -162,16 +243,14 @@ export const Tooltip: React.FC<TooltipProps | ExpandedTooltipProps> = ({
162243
children,
163244
...props
164245
}) => {
246+
const placement =
247+
(props.variant === 'expanded' ? props.placement : undefined) ?? 'up-down'
165248
const child = Children.only(children) as ReactElement
166249
const [anchorEl, setAnchorEl] = useState<HTMLElement | null>(null)
167250

168251
const [visible, show, hide] = useBoolean(false)
169-
170252
const [debouncedVisible, setDebouncedVisible] = useState(visible)
171-
const [hasOverflow, setHasOverflow] = useState(false)
172-
const [horizontalLayout, setHorizontalLayout] = useState<
173-
'left' | 'right' | 'center'
174-
>('center')
253+
const [layout, setLayout] = useState<Placement>('down')
175254
const [tooltipEl, setTooltipEl] = useState<HTMLDivElement | null>(null)
176255

177256
useEffect(() => {
@@ -199,20 +278,51 @@ export const Tooltip: React.FC<TooltipProps | ExpandedTooltipProps> = ({
199278
return
200279
}
201280

202-
const { bottom } = anchorEl.getBoundingClientRect()
203-
204-
const bottomSpace = document.documentElement.clientHeight - bottom
205-
// See if `bottomSpace` is smaller than Tooltip height.
206-
// "8" is margin of the TooltipWrapper.
207-
setHasOverflow(tooltipEl.clientHeight + 8 > bottomSpace)
281+
const bounds = anchorEl.getBoundingClientRect()
282+
283+
// "16" is for space of margin of ExpandedTooltipWrapper + arrow size.
284+
const tooltipSize: [number, number] = [
285+
tooltipEl.clientWidth + 16,
286+
tooltipEl.clientHeight + 16,
287+
]
288+
const tooltipMid = [
289+
bounds.left + (bounds.right - bounds.left) / 2,
290+
bounds.top + (bounds.bottom - bounds.top) / 2,
291+
]
292+
293+
const spaces: Record<Placement, boolean> = {
294+
down: rectInBounds(
295+
[tooltipMid[0] - tooltipSize[0] / 2, bounds.bottom],
296+
tooltipSize
297+
),
298+
up: rectInBounds(
299+
[tooltipMid[0] - tooltipSize[0] / 2, bounds.top - tooltipSize[1]],
300+
tooltipSize
301+
),
302+
left: rectInBounds(
303+
[bounds.left - tooltipSize[0], tooltipMid[1] - tooltipSize[1] / 2],
304+
tooltipSize
305+
),
306+
right: rectInBounds(
307+
[bounds.right, tooltipMid[1] - tooltipSize[1] / 2],
308+
tooltipSize
309+
),
310+
}
208311

209-
const { left, right } = tooltipEl.getBoundingClientRect()
210-
if (left < 0) {
211-
setHorizontalLayout('left')
212-
} else if (right > document.documentElement.clientWidth) {
213-
setHorizontalLayout('right')
312+
if (placement === 'up-down') {
313+
if (spaces.up || spaces.down) {
314+
setLayout(spaces.down ? 'down' : 'up')
315+
} else {
316+
setLayout(spaces.right ? 'right' : 'left')
317+
}
318+
} else if (placement === 'left-right') {
319+
if (spaces.right || spaces.left) {
320+
setLayout(spaces.right ? 'right' : 'left')
321+
} else {
322+
setLayout(spaces.up ? 'up' : 'down')
323+
}
214324
}
215-
}, [anchorEl, tooltipEl])
325+
}, [anchorEl, tooltipEl, props, placement])
216326

217327
if (props.variant !== 'expanded') {
218328
return (
@@ -221,14 +331,7 @@ export const Tooltip: React.FC<TooltipProps | ExpandedTooltipProps> = ({
221331
ref: setAnchorEl,
222332
})}
223333
{debouncedVisible ? (
224-
<PopOver
225-
anchorEl={anchorEl}
226-
horizontalPosition={horizontalLayout}
227-
horizontalAlignment={horizontalLayout}
228-
verticalPosition={hasOverflow ? 'top' : 'bottom'}
229-
verticalAlignment={hasOverflow ? 'bottom' : 'top'}
230-
{...props}
231-
>
334+
<PopOver anchorEl={anchorEl} {...alignments[layout]} {...props}>
232335
<TooltipWrapper ref={setTooltipEl}>
233336
<Typography variant="chip-tag-text">{props.text}</Typography>
234337
</TooltipWrapper>
@@ -238,47 +341,34 @@ export const Tooltip: React.FC<TooltipProps | ExpandedTooltipProps> = ({
238341
)
239342
}
240343

344+
const { tipTitle, extraInfo, contents } = props
345+
241346
return (
242347
<>
243348
{React.cloneElement(child, {
244349
ref: setAnchorEl,
245350
})}
246351
{debouncedVisible ? (
247352
<>
248-
<PopOver
249-
anchorEl={anchorEl}
250-
horizontalPosition={horizontalLayout}
251-
horizontalAlignment={horizontalLayout}
252-
verticalPosition={hasOverflow ? 'top' : 'bottom'}
253-
verticalAlignment={hasOverflow ? 'bottom' : 'top'}
254-
{...props}
255-
>
353+
<PopOver anchorEl={anchorEl} {...alignments[layout]} {...props}>
256354
<ExpandedTooltipWrapper ref={setTooltipEl}>
257-
{props.tipTitle !== undefined || props.extraInfo !== undefined ? (
258-
props.extraInfo !== undefined ? (
355+
{tipTitle !== undefined || extraInfo !== undefined ? (
356+
extraInfo !== undefined ? (
259357
<ExpandedTooltipTop>
260-
<ExpandedTooltipTitle>
261-
{props.tipTitle}
262-
</ExpandedTooltipTitle>
358+
<ExpandedTooltipTitle>{tipTitle}</ExpandedTooltipTitle>
263359
<ExpandedTooltipExtraInfo>
264-
{props.extraInfo}
360+
{extraInfo}
265361
</ExpandedTooltipExtraInfo>
266362
</ExpandedTooltipTop>
267363
) : (
268-
<ExpandedTooltipTitle>{props.tipTitle}</ExpandedTooltipTitle>
364+
<ExpandedTooltipTitle>{tipTitle}</ExpandedTooltipTitle>
269365
)
270366
) : null}
271-
{props.contents}
367+
{contents}
272368
</ExpandedTooltipWrapper>
273369
</PopOver>
274-
<PopOver
275-
anchorEl={anchorEl}
276-
horizontalPosition="center"
277-
horizontalAlignment="center"
278-
verticalPosition={hasOverflow ? 'top' : 'bottom'}
279-
verticalAlignment={hasOverflow ? 'bottom' : 'top'}
280-
>
281-
{hasOverflow ? <ToolTipDownArrow /> : <ToolTipUpArrow />}
370+
<PopOver anchorEl={anchorEl} {...alignments[layout]}>
371+
{arrows[layout]}
282372
</PopOver>
283373
</>
284374
) : null}

0 commit comments

Comments
 (0)