Skip to content

Commit 67dd60c

Browse files
committed
🐛(frontend) clamp pip tooltips on right edge
prevent pip tooltips from overflowing the right side of the window
1 parent 401480f commit 67dd60c

1 file changed

Lines changed: 44 additions & 12 deletions

File tree

src/frontend/src/primitives/VisualOnlyTooltip.tsx

Lines changed: 44 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@ import {
22
type ReactElement,
33
cloneElement,
44
isValidElement,
5-
useMemo, useRef,
5+
useLayoutEffect,
6+
useMemo,
7+
useRef,
68
useState,
79
} from 'react'
810
import { createPortal } from 'react-dom'
@@ -16,8 +18,6 @@ export type VisualOnlyTooltipProps = {
1618
tooltipPosition?: 'top' | 'bottom'
1719
}
1820

19-
const TOOLTIP_VERTICAL_OFFSET_PX = 8
20-
2121
/**
2222
* Wrapper component that displays a tooltip visually only (not announced by screen readers).
2323
*
@@ -37,10 +37,15 @@ export const VisualOnlyTooltip = ({
3737
const [isVisible, setIsVisible] = useState(false)
3838
const { getContainer } = useUNSAFE_PortalContext()
3939
const wrapperRef = useRef<HTMLDivElement>(null)
40+
const tooltipRef = useRef<HTMLDivElement>(null)
4041
const [position, setPosition] = useState<{
4142
top: number
4243
left: number
4344
} | null>(null)
45+
const [computedStyle, setComputedStyle] = useState<{
46+
left: number
47+
arrowLeft: number
48+
} | null>(null)
4449

4550
const isBottom = tooltipPosition === 'bottom'
4651

@@ -57,9 +62,23 @@ export const VisualOnlyTooltip = ({
5762
const hideTooltip = () => {
5863
setIsVisible(false)
5964
setPosition(null)
65+
setComputedStyle(null)
6066
}
6167

62-
const tooltipData = isVisible && position ? { isVisible, position } : null
68+
useLayoutEffect(() => {
69+
if (!tooltipRef.current || !isVisible || !position) return
70+
const tooltipWidth = tooltipRef.current.getBoundingClientRect().width
71+
const doc = tooltipRef.current.ownerDocument
72+
const viewportWidth = doc.defaultView?.innerWidth ?? window.innerWidth
73+
const padding = 8
74+
const desiredLeft = position.left - tooltipWidth / 2
75+
const maxLeft = viewportWidth - padding - tooltipWidth
76+
if (desiredLeft <= maxLeft) {
77+
setComputedStyle(null)
78+
return
79+
}
80+
setComputedStyle({ left: maxLeft, arrowLeft: position.left - maxLeft })
81+
}, [isVisible, position])
6382

6483
const portalContainer = useMemo(() => {
6584
if (getContainer) return getContainer()
@@ -82,12 +101,14 @@ export const VisualOnlyTooltip = ({
82101
>
83102
{wrappedChild}
84103
</div>
85-
{tooltipData &&
104+
{isVisible &&
105+
position &&
86106
portalContainer &&
87107
createPortal(
88108
<div
89109
aria-hidden="true"
90110
role="presentation"
111+
ref={tooltipRef}
91112
className={css({
92113
position: 'fixed',
93114
padding: '2px 8px',
@@ -97,12 +118,12 @@ export const VisualOnlyTooltip = ({
97118
fontSize: 14,
98119
whiteSpace: 'nowrap',
99120
pointerEvents: 'none',
100-
zIndex: 9999,
121+
zIndex: 100001,
101122
boxShadow: '0 8px 20px rgba(0 0 0 / 0.1)',
102123
'&::after': {
103124
content: '""',
104125
position: 'absolute',
105-
left: '50%',
126+
left: 'var(--tooltip-arrow-left, 50%)',
106127
transform: 'translateX(-50%)',
107128
border: '4px solid transparent',
108129
...(isBottom
@@ -117,11 +138,22 @@ export const VisualOnlyTooltip = ({
117138
},
118139
})}
119140
style={{
120-
top: `${tooltipData.position.top}px`,
121-
left: `${tooltipData.position.left}px`,
122-
transform: isBottom
123-
? 'translate(-50%, 0)'
124-
: 'translate(-50%, -100%)',
141+
top: `${position.top}px`,
142+
left: computedStyle
143+
? `${computedStyle.left}px`
144+
: `${position.left}px`,
145+
transform: computedStyle
146+
? isBottom
147+
? 'translateY(0)'
148+
: 'translateY(-100%)'
149+
: isBottom
150+
? 'translate(-50%, 0)'
151+
: 'translate(-50%, -100%)',
152+
...(computedStyle
153+
? {
154+
'--tooltip-arrow-left': `${computedStyle.arrowLeft}px`,
155+
}
156+
: null),
125157
}}
126158
>
127159
{tooltip}

0 commit comments

Comments
 (0)