import { useEffect, useRef, useState, type ReactNode, type RefObject } from "react"; import { createPortal } from "react-dom"; export function FloatingPortal({ align = "left", anchorRef, children, open, width, }: { align?: "center" | "left" | "right"; anchorRef: RefObject; children: ReactNode; open: boolean; width?: number | string; }) { const overlayRef = useRef(null); const [position, setPosition] = useState({ left: 0, top: 0, ready: false }); useEffect(() => { if (!open) { return; } function updatePosition() { const anchor = anchorRef.current; const overlay = overlayRef.current; if (!anchor || !overlay) { return; } const anchorBounds = anchor.getBoundingClientRect(); const overlayBounds = overlay.getBoundingClientRect(); const gap = 8; const alignedLeft = align === "right" ? anchorBounds.right - overlayBounds.width : align === "center" ? anchorBounds.left + (anchorBounds.width - overlayBounds.width) / 2 : anchorBounds.left; const maxLeft = Math.max(gap, window.innerWidth - overlayBounds.width - gap); const left = Math.min(Math.max(gap, alignedLeft), maxLeft); const belowTop = anchorBounds.bottom + gap; const aboveTop = anchorBounds.top - overlayBounds.height - gap; const top = belowTop + overlayBounds.height <= window.innerHeight - gap ? belowTop : Math.max(gap, aboveTop); setPosition({ left, top, ready: true }); } const frame = window.requestAnimationFrame(updatePosition); window.addEventListener("resize", updatePosition); window.addEventListener("scroll", updatePosition, true); return () => { window.cancelAnimationFrame(frame); window.removeEventListener("resize", updatePosition); window.removeEventListener("scroll", updatePosition, true); }; }, [align, anchorRef, open, width]); if (!open || typeof document === "undefined") { return null; } const root = document.querySelector("[data-uhuru-root]") ?? document.body; return createPortal(
event.stopPropagation()} ref={overlayRef} style={{ boxSizing: "border-box", left: position.left, maxWidth: "calc(100vw - 16px)", top: position.top, visibility: position.ready ? "visible" : "hidden", width, }} > {children}
, root, ); }