import { useEffect, useMemo, useRef, type CSSProperties } from "react"; type PopoverAnchor = | { kind: "point"; x: number; y: number } | { kind: "element"; element: HTMLElement }; type PopoverSide = "bottom" | "right"; type PopoverAlign = "start" | "end" | "center"; interface UseViewportPopoverOptions { anchor: PopoverAnchor; width: number; estimatedHeight: number; onClose: () => void; side?: PopoverSide; align?: PopoverAlign; offset?: number; viewportPadding?: number; ignoreElements?: Array; } export function useViewportPopover({ anchor, width, estimatedHeight, onClose, side = "bottom", align = "start", offset = 8, viewportPadding = 16, ignoreElements = [], }: UseViewportPopoverOptions) { const ref = useRef(null); useEffect(() => { function handlePointerDown(event: MouseEvent) { const target = event.target as Node; if (ref.current?.contains(target)) { return; } if (ignoreElements.some((element) => element?.contains(target))) { return; } onClose(); } function handleEscape(event: KeyboardEvent) { if (event.key === "Escape") { onClose(); } } document.addEventListener("mousedown", handlePointerDown); window.addEventListener("keydown", handleEscape); return () => { document.removeEventListener("mousedown", handlePointerDown); window.removeEventListener("keydown", handleEscape); }; }, [ignoreElements, onClose]); const style = useMemo(() => { let left = 0; let top = 0; if (anchor.kind === "element") { const rect = anchor.element.getBoundingClientRect(); if (side === "right") { left = rect.right + offset; if (align === "end") { top = rect.bottom - estimatedHeight; } else if (align === "center") { top = rect.top + rect.height / 2 - estimatedHeight / 2; } else { top = rect.top; } } else { left = rect.left; if (align === "end") { left = rect.right - width; } else if (align === "center") { left = rect.left + rect.width / 2 - width / 2; } top = rect.bottom + offset; } } else { left = anchor.x; top = anchor.y + offset; if (align === "end") { left = anchor.x - width; } else if (align === "center") { left = anchor.x - width / 2; } } const maxLeft = Math.max(viewportPadding, window.innerWidth - width - viewportPadding); const maxTop = Math.max(viewportPadding, window.innerHeight - estimatedHeight - viewportPadding); return { position: "fixed", left: Math.min(Math.max(left, viewportPadding), maxLeft), top: Math.min(Math.max(top, viewportPadding), maxTop), width, zIndex: 60, }; }, [align, anchor, estimatedHeight, offset, side, viewportPadding, width]); return { ref, style }; }