From e38d0861b11a754150e47de198a7ff152af699fa Mon Sep 17 00:00:00 2001 From: Cyril Date: Thu, 5 Feb 2026 10:01:02 +0100 Subject: [PATCH] =?UTF-8?q?=F0=9F=90=9B(frontend)=20clamp=20pip=20tooltips?= =?UTF-8?q?=20on=20right=20edge?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit prevent pip tooltips from overflowing the right side of the window --- .../src/primitives/VisualOnlyTooltip.tsx | 56 +++++++++++++++---- 1 file changed, 44 insertions(+), 12 deletions(-) diff --git a/src/frontend/src/primitives/VisualOnlyTooltip.tsx b/src/frontend/src/primitives/VisualOnlyTooltip.tsx index 3c9efeb0..68a2ef82 100644 --- a/src/frontend/src/primitives/VisualOnlyTooltip.tsx +++ b/src/frontend/src/primitives/VisualOnlyTooltip.tsx @@ -2,7 +2,9 @@ import { type ReactElement, cloneElement, isValidElement, - useMemo, useRef, + useLayoutEffect, + useMemo, + useRef, useState, } from 'react' import { createPortal } from 'react-dom' @@ -16,8 +18,6 @@ export type VisualOnlyTooltipProps = { tooltipPosition?: 'top' | 'bottom' } -const TOOLTIP_VERTICAL_OFFSET_PX = 8 - /** * 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 { getContainer } = useUNSAFE_PortalContext() const wrapperRef = useRef(null) + const tooltipRef = useRef(null) const [position, setPosition] = useState<{ top: number left: number } | null>(null) + const [computedStyle, setComputedStyle] = useState<{ + left: number + arrowLeft: number + } | null>(null) const isBottom = tooltipPosition === 'bottom' @@ -57,9 +62,23 @@ export const VisualOnlyTooltip = ({ const hideTooltip = () => { setIsVisible(false) 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(() => { if (getContainer) return getContainer() @@ -82,12 +101,14 @@ export const VisualOnlyTooltip = ({ > {wrappedChild} - {tooltipData && + {isVisible && + position && portalContainer && createPortal(