import { useEffect, useState, type FocusEvent } from "react"; /** * "Peek open on hover or keyboard focus" for a container — the shared brain behind the nav rail * and the floating side panels, which sit slim until you point at them or tab into them. * * Spread `handlers` on the container; `active` is true while the pointer is over it (or any * descendant) or focus is inside it. * * Focus is tracked through the REACT tree, so a portaled menu rendered by a child (the language * switcher, the account popover) still counts as "inside" and holds the container open while you * use it. The one case that misses: such a portal unmounts while focused, and browsers fire NO blur * when a focused node is removed — focus silently falls to the body and `active` would latch on * forever. Hence the document-level safety net: when focus has landed nowhere, clear it. */ export function useHoverFocusWithin(): { active: boolean; handlers: { onMouseEnter: () => void; onMouseLeave: () => void; onFocus: () => void; onBlur: (e: FocusEvent) => void; }; } { const [hovered, setHovered] = useState(false); const [focused, setFocused] = useState(false); useEffect(() => { let timer: number | undefined; const clearIfFocusLandedNowhere = () => { const el = document.activeElement; if (!el || el === document.body || el === document.documentElement) setFocused(false); }; // Deferred a tick so activeElement has settled, and rescheduled rather than queued — only the // latest state matters. const soon = () => { window.clearTimeout(timer); timer = window.setTimeout(clearIfFocusLandedNowhere, 0); }; document.addEventListener("focusout", soon); document.addEventListener("click", soon); return () => { document.removeEventListener("focusout", soon); document.removeEventListener("click", soon); window.clearTimeout(timer); }; }, []); return { active: hovered || focused, handlers: { onMouseEnter: () => setHovered(true), onMouseLeave: () => setHovered(false), onFocus: () => setFocused(true), onBlur: (e: FocusEvent) => { if (!e.currentTarget.contains(e.relatedTarget as Node)) setFocused(false); }, }, }; }