import { currentViewport, listenToViewportChanges } from "@chopin/viewport"; import { useCallback, useEffect, useLayoutEffect, useRef, useState } from "react"; import { useMenuDismissal } from "./menu-dismissal"; import type { CSSProperties } from "react"; type Position = Pick< CSSProperties, "height" | "left" | "maxHeight" | "top" | "transformOrigin" | "visibility" | "width" >; /** Shared portal geometry, focus, and dismissal for header picker controls. */ export function useAnchoredPicker( open: boolean, setOpen: (open: boolean) => void, contentKey: unknown, ) { let trigger = useRef(null); let panel = useRef(null); let search = useRef(null); let [position, setPosition] = useState({ visibility: "hidden" }); useLayoutEffect(() => { if (!open) return; function place() { let rect = trigger.current?.getBoundingClientRect(); if (!rect) return; let viewport = currentViewport(); let margin = 8; let gap = 4; let topEdge = viewport.top + margin; let bottomEdge = viewport.top + viewport.height - margin; let width = Math.max(0, Math.min(360, viewport.width - margin * 2)); let left = Math.max( viewport.left + margin, Math.min(rect.left, viewport.left + viewport.width - width - margin), ); let above = Math.max(0, Math.min(rect.top - gap, bottomEdge) - topEdge); let below = Math.max(0, bottomEdge - Math.max(rect.bottom + gap, topEdge)); let useAbove = above > below; let previousHeight = panel.current?.style.height; let previousMaxHeight = panel.current?.style.maxHeight; if (panel.current) { panel.current.style.height = "auto"; panel.current.style.maxHeight = "none"; } let naturalHeight = panel.current?.scrollHeight ?? 0; if (panel.current) { panel.current.style.height = previousHeight ?? ""; panel.current.style.maxHeight = previousMaxHeight ?? ""; } let maxHeight = Math.min(naturalHeight, useAbove ? above : below); let wantedTop = useAbove ? rect.top - gap - maxHeight : rect.bottom + gap; setPosition({ height: maxHeight, left, maxHeight, top: Math.max(topEdge, Math.min(wantedTop, bottomEdge - maxHeight)), transformOrigin: useAbove ? "bottom left" : "top left", visibility: "visible", width, }); } function placeForViewportChange(event?: Event) { let target = event?.target; if (target instanceof Node && panel.current?.contains(target)) return; place(); } place(); return listenToViewportChanges(placeForViewportChange, { observeDocumentScroll: true }); }, [contentKey, open]); useEffect(() => { if (!open) return; let frame = requestAnimationFrame(() => { if (!panel.current?.contains(document.activeElement)) search.current?.focus(); }); return () => cancelAnimationFrame(frame); }, [open]); let close = useCallback((restoreFocus: boolean) => { setOpen(false); if (restoreFocus) trigger.current?.focus(); }, [setOpen]); useMenuDismissal(open, [trigger, panel], close); return { panel, position, search, trigger }; }