import { focusDialogOnOpen, handleDialogKeyDown, restoreDialogFocus, type DialogKeyboardEvent } from "./dialogInteractions"; export type DialogStackId = symbol; export type DialogStackRegistration = { id: DialogStackId; activationOrder: number; panel: HTMLElement; restoreFocus: HTMLElement | null; canClose: () => boolean; onClose?: () => void; }; type DialogStackEntry = DialogStackRegistration & { restoreTargets: HTMLElement[]; }; const openDialogs: DialogStackEntry[] = []; let activationSequence = 0; let listeningForKeyDown = false; function topDialog(): DialogStackEntry | null { return openDialogs[openDialogs.length - 1] ?? null; } function currentActiveElement(): Element | null { return typeof document === "undefined" ? null : document.activeElement; } function appendUniqueTargets(targets: HTMLElement[], additions: Array): HTMLElement[] { const next = [...targets]; additions.forEach((target) => { if (target && !next.includes(target)) next.push(target); }); return next; } function exposeAsTopmost(panel: HTMLElement): void { panel.removeAttribute("inert"); panel.removeAttribute("aria-hidden"); panel.setAttribute("aria-modal", "true"); panel.setAttribute("data-dialog-stack-state", "topmost"); } function hideAsUnderlying(panel: HTMLElement): void { panel.setAttribute("inert", ""); panel.setAttribute("aria-hidden", "true"); panel.removeAttribute("aria-modal"); panel.setAttribute("data-dialog-stack-state", "underlying"); } function syncStackAccessibility(): void { const topIndex = openDialogs.length - 1; openDialogs.forEach((entry, index) => { if (index === topIndex) exposeAsTopmost(entry.panel); else hideAsUnderlying(entry.panel); }); } function handleWindowKeyDown(event: KeyboardEvent): void { handleTopDialogKeyDown(event, currentActiveElement()); } function syncWindowKeyDownListener(): void { if (typeof window === "undefined") return; if (openDialogs.length > 0 && !listeningForKeyDown) { window.addEventListener("keydown", handleWindowKeyDown); listeningForKeyDown = true; } else if (openDialogs.length === 0 && listeningForKeyDown) { window.removeEventListener("keydown", handleWindowKeyDown); listeningForKeyDown = false; } } function focusAfterTopmostCloses(entry: DialogStackEntry): void { const nextTop = topDialog(); if (nextTop) { const targetInsideNextDialog = entry.restoreTargets.find( (target) => target.isConnected !== false && nextTop.panel.contains(target) ); if (targetInsideNextDialog && restoreDialogFocus(targetInsideNextDialog)) return; focusDialogOnOpen(nextTop.panel, currentActiveElement()); return; } entry.restoreTargets.some((target) => restoreDialogFocus(target)); } function unregisterDialog(id: DialogStackId): void { const index = openDialogs.findIndex((entry) => entry.id === id); if (index < 0) return; const wasTopmost = index === openDialogs.length - 1; const [entry] = openDialogs.splice(index, 1); exposeAsTopmost(entry.panel); syncStackAccessibility(); syncWindowKeyDownListener(); if (wasTopmost) focusAfterTopmostCloses(entry); } export function nextDialogActivationOrder(): number { activationSequence += 1; return activationSequence; } export function registerDialog( registration: DialogStackRegistration, activeElement: Element | null ): () => void { const existingIndex = openDialogs.findIndex((entry) => entry.id === registration.id); if (existingIndex >= 0) openDialogs.splice(existingIndex, 1); const entry: DialogStackEntry = { ...registration, restoreTargets: appendUniqueTargets([], [registration.restoreFocus]) }; openDialogs.push(entry); openDialogs.sort((left, right) => left.activationOrder - right.activationOrder); const entryIndex = openDialogs.indexOf(entry); const entryBelow = openDialogs[entryIndex - 1]; if (entryBelow) entry.restoreTargets = appendUniqueTargets(entry.restoreTargets, entryBelow.restoreTargets); for (let index = entryIndex + 1; index < openDialogs.length; index += 1) { openDialogs[index].restoreTargets = appendUniqueTargets(openDialogs[index].restoreTargets, entry.restoreTargets); } if (topDialog() === entry) focusDialogOnOpen(entry.panel, activeElement); syncStackAccessibility(); syncWindowKeyDownListener(); return () => unregisterDialog(registration.id); } export function handleTopDialogKeyDown( event: DialogKeyboardEvent, activeElement: Element | null ): boolean { const entry = topDialog(); if (!entry) return false; return handleDialogKeyDown(entry.panel, event, activeElement, entry.canClose(), entry.onClose); } export function dialogIsTopmost(id: DialogStackId): boolean { const entry = topDialog(); return !entry || entry.id === id; } export function resetDialogStackForTests(): void { openDialogs.splice(0).forEach((entry) => exposeAsTopmost(entry.panel)); activationSequence = 0; syncWindowKeyDownListener(); }