🐛(frontend) clamp pip tooltips on right edge

prevent pip tooltips from overflowing the right side of the window
This commit is contained in:
Cyril
2026-02-05 10:01:02 +01:00
parent 0ab2e2f18f
commit e38d0861b1
@@ -2,7 +2,9 @@ import {
type ReactElement, type ReactElement,
cloneElement, cloneElement,
isValidElement, isValidElement,
useMemo, useRef, useLayoutEffect,
useMemo,
useRef,
useState, useState,
} from 'react' } from 'react'
import { createPortal } from 'react-dom' import { createPortal } from 'react-dom'
@@ -16,8 +18,6 @@ export type VisualOnlyTooltipProps = {
tooltipPosition?: 'top' | 'bottom' tooltipPosition?: 'top' | 'bottom'
} }
const TOOLTIP_VERTICAL_OFFSET_PX = 8
/** /**
* Wrapper component that displays a tooltip visually only (not announced by screen readers). * Wrapper component that displays a tooltip visually only (not announced by screen readers).
* *
@@ -37,10 +37,15 @@ export const VisualOnlyTooltip = ({
const [isVisible, setIsVisible] = useState(false) const [isVisible, setIsVisible] = useState(false)
const { getContainer } = useUNSAFE_PortalContext() const { getContainer } = useUNSAFE_PortalContext()
const wrapperRef = useRef<HTMLDivElement>(null) const wrapperRef = useRef<HTMLDivElement>(null)
const tooltipRef = useRef<HTMLDivElement>(null)
const [position, setPosition] = useState<{ const [position, setPosition] = useState<{
top: number top: number
left: number left: number
} | null>(null) } | null>(null)
const [computedStyle, setComputedStyle] = useState<{
left: number
arrowLeft: number
} | null>(null)
const isBottom = tooltipPosition === 'bottom' const isBottom = tooltipPosition === 'bottom'
@@ -57,9 +62,23 @@ export const VisualOnlyTooltip = ({
const hideTooltip = () => { const hideTooltip = () => {
setIsVisible(false) setIsVisible(false)
setPosition(null) setPosition(null)
setComputedStyle(null)
} }
const tooltipData = isVisible && position ? { isVisible, position } : null useLayoutEffect(() => {
if (!tooltipRef.current || !isVisible || !position) return
const tooltipWidth = tooltipRef.current.getBoundingClientRect().width
const doc = tooltipRef.current.ownerDocument
const viewportWidth = doc.defaultView?.innerWidth ?? window.innerWidth
const padding = 8
const desiredLeft = position.left - tooltipWidth / 2
const maxLeft = viewportWidth - padding - tooltipWidth
if (desiredLeft <= maxLeft) {
setComputedStyle(null)
return
}
setComputedStyle({ left: maxLeft, arrowLeft: position.left - maxLeft })
}, [isVisible, position])
const portalContainer = useMemo(() => { const portalContainer = useMemo(() => {
if (getContainer) return getContainer() if (getContainer) return getContainer()
@@ -82,12 +101,14 @@ export const VisualOnlyTooltip = ({
> >
{wrappedChild} {wrappedChild}
</div> </div>
{tooltipData && {isVisible &&
position &&
portalContainer && portalContainer &&
createPortal( createPortal(
<div <div
aria-hidden="true" aria-hidden="true"
role="presentation" role="presentation"
ref={tooltipRef}
className={css({ className={css({
position: 'fixed', position: 'fixed',
padding: '2px 8px', padding: '2px 8px',
@@ -97,12 +118,12 @@ export const VisualOnlyTooltip = ({
fontSize: 14, fontSize: 14,
whiteSpace: 'nowrap', whiteSpace: 'nowrap',
pointerEvents: 'none', pointerEvents: 'none',
zIndex: 9999, zIndex: 100001,
boxShadow: '0 8px 20px rgba(0 0 0 / 0.1)', boxShadow: '0 8px 20px rgba(0 0 0 / 0.1)',
'&::after': { '&::after': {
content: '""', content: '""',
position: 'absolute', position: 'absolute',
left: '50%', left: 'var(--tooltip-arrow-left, 50%)',
transform: 'translateX(-50%)', transform: 'translateX(-50%)',
border: '4px solid transparent', border: '4px solid transparent',
...(isBottom ...(isBottom
@@ -117,11 +138,22 @@ export const VisualOnlyTooltip = ({
}, },
})} })}
style={{ style={{
top: `${tooltipData.position.top}px`, top: `${position.top}px`,
left: `${tooltipData.position.left}px`, left: computedStyle
transform: isBottom ? `${computedStyle.left}px`
? 'translate(-50%, 0)' : `${position.left}px`,
: 'translate(-50%, -100%)', transform: computedStyle
? isBottom
? 'translateY(0)'
: 'translateY(-100%)'
: isBottom
? 'translate(-50%, 0)'
: 'translate(-50%, -100%)',
...(computedStyle
? {
'--tooltip-arrow-left': `${computedStyle.arrowLeft}px`,
}
: null),
}} }}
> >
{tooltip} {tooltip}