import { useState, useRef, useLayoutEffect, type ReactNode } from "react"; import { createPortal } from "react-dom"; interface TooltipProps { text: string; children?: ReactNode; } /** * A small circled question-mark icon that shows a tooltip on hover. * Uses a portal to render at `document.body` so the tooltip is never * clipped by ancestor `overflow: hidden` containers. */ export default function Tooltip({ text, children }: TooltipProps) { const [visible, setVisible] = useState(false); const [coords, setCoords] = useState({ top: 0, left: 0 }); const [, setPlacement] = useState<"top" | "bottom">("top"); const triggerRef = useRef(null); const tooltipRef = useRef(null); useLayoutEffect(() => { if (!visible || !triggerRef.current || !tooltipRef.current) return; const trigger = triggerRef.current.getBoundingClientRect(); const tooltip = tooltipRef.current.getBoundingClientRect(); const gap = 6; // Vertical: prefer above, fall back to below const above = trigger.top - tooltip.height - gap >= 4; const pos = above ? "top" : "bottom"; setPlacement(pos); const top = pos === "top" ? trigger.top - tooltip.height - gap : trigger.bottom + gap; // Horizontal: center on trigger, clamp to viewport let left = trigger.left + trigger.width / 2 - tooltip.width / 2; left = Math.max(4, Math.min(left, window.innerWidth - tooltip.width - 4)); setCoords({ top, left }); }, [visible]); return ( setVisible(true)} onMouseLeave={() => setVisible(false)} > {children ?? ( ? )} {visible && createPortal(
{text}
, document.body )}
); }