diff --git a/app/components/InfoTooltip.tsx b/app/components/InfoTooltip.tsx index 902841d..eda02b7 100644 --- a/app/components/InfoTooltip.tsx +++ b/app/components/InfoTooltip.tsx @@ -1,27 +1,87 @@ "use client"; -import type { ReactNode } from "react"; +import { + type ReactNode, + useState, + useRef, + useLayoutEffect, + useCallback, +} from "react"; +import { createPortal } from "react-dom"; interface InfoTooltipProps { text: ReactNode; } export default function InfoTooltip({ text }: InfoTooltipProps) { + const [visible, setVisible] = useState(false); + const [positioned, setPositioned] = useState(false); + const [position, setPosition] = useState({ top: 0, left: 0 }); + const triggerRef = useRef(null); + const tooltipRef = useRef(null); + + const updatePosition = useCallback(() => { + if (!triggerRef.current || !tooltipRef.current) return; + + const triggerRect = triggerRef.current.getBoundingClientRect(); + const tooltipRect = tooltipRef.current.getBoundingClientRect(); + + let top = triggerRect.top - tooltipRect.height - 8; + let left = triggerRect.left + triggerRect.width / 2 - tooltipRect.width / 2; + + // Keep tooltip within viewport horizontally + if (left < 8) left = 8; + if (left + tooltipRect.width > window.innerWidth - 8) { + left = window.innerWidth - tooltipRect.width - 8; + } + + // If no room above, show below + if (top < 8) { + top = triggerRect.bottom + 8; + } + + setPosition({ top, left }); + setPositioned(true); + }, []); + + // useLayoutEffect to position before paint — prevents flash at (0,0) + useLayoutEffect(() => { + if (!visible) { + setPositioned(false); + return; + } + updatePosition(); + }, [visible, updatePosition]); + return ( - + -
- {text} - -
+ {visible && + createPortal( +
+ {text} +
, + document.body, + )}
); }