import { useLayoutEffect, useReducer, useRef } from "react"; import type { ContextualDetailBudget } from "../lib/contextual-detail-layout"; import { contextualDetailMode, contextualDetailReducer } from "../lib/contextual-detail-layout"; /** Mide la caja exterior estable; selección y datos siguen perteneciendo al consumidor. */ export function useContextualDetail({ budget, initialVisible = false, }: { budget: ContextualDetailBudget; initialVisible?: boolean; }) { const containerRef = useRef(null); const reopenRef = useRef(null); const triggerRef = useRef(null); const restorePanelFocusRef = useRef(false); const transferDialogFocusRef = useRef(false); const restoreFrameRef = useRef(undefined); const [state, dispatch] = useReducer(contextualDetailReducer, { mode: "overlay", visible: initialVisible, }); useLayoutEffect(() => { const root = containerRef.current; if (!root) { return; } let mode = root.dataset.detailMode === "inline" ? "inline" : "overlay"; const observer = new ResizeObserver((entries) => { const entry = entries[0]; if (!entry) { return; } const next = contextualDetailMode(entry.contentRect.width, budget); if (next === mode) { return; } if ( mode === "inline" && root.querySelector(":scope > [data-slot=selection-panel]")?.contains(document.activeElement) ) { restorePanelFocusRef.current = true; } if (mode === "overlay" && root.dataset.detailOpen === "true") { transferDialogFocusRef.current = true; } mode = next; dispatch({ type: "capacity", mode: next }); }); observer.observe(root); return () => { observer.disconnect(); if (restoreFrameRef.current !== undefined) { cancelAnimationFrame(restoreFrameRef.current); restoreFrameRef.current = undefined; } }; }, [budget]); useLayoutEffect(() => { if (!state.visible && restorePanelFocusRef.current) { restorePanelFocusRef.current = false; (reopenRef.current ?? containerRef.current)?.focus({ preventScroll: true }); } if (state.mode === "inline" && state.visible && transferDialogFocusRef.current) { transferDialogFocusRef.current = false; // Tras la liberación de modalidad y guardas de Base UI, enfocar el control equivalente. const frame = requestAnimationFrame(() => { containerRef.current ?.querySelector("[data-slot=selection-panel] header button") ?.focus({ preventScroll: true }); }); return () => cancelAnimationFrame(frame); } }, [state]); function setVisible(visible: boolean) { if (visible) { captureTrigger(); } else if ( containerRef.current ?.querySelector(":scope > [data-slot=selection-panel]") ?.contains(document.activeElement) ) { restorePanelFocusRef.current = true; } dispatch({ type: "visibility", visible }); if (!visible && state.mode === "overlay") { // finalFocus puede consultar el disparador antes de que vuelva a ser visible en el commit. restoreFocus(); } } function captureTrigger() { const active = document.activeElement; triggerRef.current = active instanceof HTMLElement && active !== document.body ? active : null; } function triggerFinalFocus() { const root = containerRef.current; const trigger = triggerRef.current; return ( (trigger?.isConnected && trigger.getClientRects().length > 0 && !trigger.matches(":disabled, [aria-disabled=true]") ? trigger : null) ?? (reopenRef.current?.getClientRects().length ? reopenRef.current : null) ?? root?.querySelector("input[data-quick-search]:enabled") ?? root ?? true ); } function finalFocus() { // El modal contextual que pasa a panel termina en su control de cierre equivalente. return ( containerRef.current?.querySelector( "[data-slot=selection-panel] header button" ) ?? triggerFinalFocus() ); } function restoreFocus() { if (restoreFrameRef.current !== undefined) { cancelAnimationFrame(restoreFrameRef.current); } // Un diálogo explícito puede retirar y reponer el panel sin cambiar el modo de capacidad. restoreFrameRef.current = requestAnimationFrame(() => { restoreFrameRef.current = undefined; const target = finalFocus(); if (target instanceof HTMLElement && target.isConnected) { target.focus({ preventScroll: true }); } }); } return { ...state, containerRef, reopenRef, setVisible, captureTrigger, finalFocus, triggerFinalFocus, restoreFocus, }; }