import { useEffect, useRef, useState } from 'react' import { createPortal } from 'react-dom' const POPOVER_WIDTH = 256 const VIEWPORT_MARGIN = 8 /** * A small "i" affordance that reveals a block of help text on click — for a * concept that needs more than a one-line inline hint can carry, without * permanently taking up screen space. Click-toggled rather than hover-only * so it works the same on touch as with a mouse, and stays open long enough * to actually read instead of vanishing the moment the pointer drifts. * * Renders the popover through a portal to `document.body`, positioned via * the trigger's own bounding rect rather than as a normal absolutely- * positioned child — this component is mostly used inside a Modal, whose * scrollable body clips anything (including absolutely-positioned children) * that pokes past its edge, which cut the popover off before this existed. */ export default function InfoTooltip({ text }: { text: string }) { const [open, setOpen] = useState(false) const [position, setPosition] = useState<{ top: number; left: number } | null>(null) const buttonRef = useRef(null) const popoverRef = useRef(null) const toggle = () => { if (!open && buttonRef.current) { const rect = buttonRef.current.getBoundingClientRect() const left = Math.min(Math.max(rect.left, VIEWPORT_MARGIN), window.innerWidth - POPOVER_WIDTH - VIEWPORT_MARGIN) setPosition({ top: rect.bottom + 4, left }) } setOpen((v) => !v) } useEffect(() => { if (!open) return const onOutsideClick = (e: MouseEvent) => { const target = e.target as Node if (buttonRef.current?.contains(target) || popoverRef.current?.contains(target)) return setOpen(false) } // Also close on scroll (e.g. the modal body it's usually opened inside) // rather than tracking the trigger's position live — simplest way to // avoid a stale popover that's drifted away from its "i" icon. const close = () => setOpen(false) document.addEventListener('mousedown', onOutsideClick) document.addEventListener('scroll', close, true) return () => { document.removeEventListener('mousedown', onOutsideClick) document.removeEventListener('scroll', close, true) } }, [open]) return ( <> {open && position && createPortal(
{text}
, document.body, )} ) }