function assert(condition: unknown, message = "assertion failed"): void { if (!condition) throw new Error(message); } function assertEqual(actual: T, expected: T, message = "values should be equal"): void { if (actual !== expected) throw new Error(`${message}: expected ${String(expected)}, got ${String(actual)}`); } import { renderToStaticMarkup } from "react-dom/server"; import Dialog from "../src/components/Dialog"; import { focusDialogOnOpen, handleDialogKeyDown, restoreDialogFocus, shouldCloseDialogOnBackdrop, trapDialogFocus } from "../src/components/dialogInteractions"; import { handleTopDialogKeyDown, nextDialogActivationOrder, registerDialog, resetDialogStackForTests } from "../src/components/dialogStack"; type FocusableFixture = HTMLElement & { focusCount: number; attributes: Map }; function focusableFixture({ connected = true, hidden = false }: { connected?: boolean; hidden?: boolean } = {}): FocusableFixture { const attributes = new Map(); if (hidden) attributes.set("hidden", ""); const fixture = { attributes, focusCount: 0, tabIndex: 0, isConnected: connected, getAttribute: (name: string) => attributes.get(name) ?? null, hasAttribute: (name: string) => attributes.has(name), setAttribute: (name: string, value: string) => attributes.set(name, value), removeAttribute: (name: string) => attributes.delete(name), closest: () => attributes.has("hidden") || attributes.has("inert") || attributes.get("aria-hidden") === "true" ? fixture : null, focus() { fixture.focusCount += 1; } }; return fixture as unknown as FocusableFixture; } function panelFixture(elements: FocusableFixture[], autofocus?: FocusableFixture) { const panel = focusableFixture(); panel.tabIndex = -1; Object.assign(panel, { contains: (element: Element | null) => element === panel || elements.includes(element as FocusableFixture), querySelectorAll: () => elements, querySelector: (selector: string) => selector === "[autofocus]" ? (autofocus ?? null) : null }); return panel; } function keyboardEvent(key: string, shiftKey = false) { const fixture = { key, shiftKey, prevented: 0, preventDefault() { fixture.prevented += 1; } }; return fixture; } const markup = renderToStaticMarkup( undefined}> ); assert(markup.includes('role="dialog"'), "dialog semantics are preserved"); assert(markup.includes('aria-modal="true"'), "modal semantics are preserved"); assert(markup.includes('data-dialog-stack-state="topmost"'), "dialog exposes its initial stack state"); assert(markup.includes('tabindex="-1"'), "dialog panel can receive fallback focus"); const first = focusableFixture(); const requested = focusableFixture(); const entryPanel = panelFixture([first, requested], requested); assertEqual(focusDialogOnOpen(entryPanel, null), requested, "autofocus target receives initial focus"); assertEqual(requested.focusCount, 1, "autofocus target is focused once"); const defaultEntryPanel = panelFixture([first]); assertEqual(focusDialogOnOpen(defaultEntryPanel, null), first, "first available control receives initial focus"); assertEqual(first.focusCount, 1, "first available control is focused once"); const retained = focusableFixture(); const retainedPanel = panelFixture([retained]); assertEqual(focusDialogOnOpen(retainedPanel, retained), retained, "existing focus inside the dialog is retained"); assertEqual(retained.focusCount, 0, "retained focus is not moved redundantly"); const emptyPanel = panelFixture([]); assertEqual(focusDialogOnOpen(emptyPanel, null), emptyPanel, "panel receives focus when no control is available"); assertEqual(emptyPanel.focusCount, 1, "fallback panel is focused"); const boundaryFirst = focusableFixture(); const boundaryMiddle = focusableFixture(); const boundaryLast = focusableFixture(); const boundaryPanel = panelFixture([boundaryFirst, boundaryMiddle, boundaryLast]); const forwardWrap = keyboardEvent("Tab"); assert(trapDialogFocus(boundaryPanel, forwardWrap, boundaryLast), "forward Tab wraps at the last control"); assertEqual(forwardWrap.prevented, 1, "forward wrap prevents focus from leaving the dialog"); assertEqual(boundaryFirst.focusCount, 1, "forward wrap focuses the first control"); const backwardWrap = keyboardEvent("Tab", true); assert(trapDialogFocus(boundaryPanel, backwardWrap, boundaryFirst), "Shift+Tab wraps at the first control"); assertEqual(backwardWrap.prevented, 1, "backward wrap prevents focus from leaving the dialog"); assertEqual(boundaryLast.focusCount, 1, "backward wrap focuses the last control"); const interiorTab = keyboardEvent("Tab"); assert(!trapDialogFocus(boundaryPanel, interiorTab, boundaryMiddle), "interior Tab keeps native focus order"); assertEqual(interiorTab.prevented, 0, "interior Tab is not prevented"); const outsideTab = keyboardEvent("Tab"); assert(trapDialogFocus(boundaryPanel, outsideTab, focusableFixture()), "focus cannot enter the modal from outside its boundary"); assertEqual(boundaryFirst.focusCount, 2, "outside focus is redirected to the first control"); let closeCount = 0; const escape = keyboardEvent("Escape"); assert(handleDialogKeyDown(boundaryPanel, escape, boundaryFirst, true, () => { closeCount += 1; }), "Escape closes a closable dialog"); assertEqual(closeCount, 1, "Escape invokes onClose once"); assertEqual(escape.prevented, 0, "Escape preserves the existing default-event behavior"); const blockedEscape = keyboardEvent("Escape"); assert(!handleDialogKeyDown(boundaryPanel, blockedEscape, boundaryFirst, false, () => { closeCount += 1; }), "Escape is ignored while closing is disabled"); assertEqual(closeCount, 1, "disabled Escape does not invoke onClose"); const opener = focusableFixture(); assert(restoreDialogFocus(opener), "connected opener focus is restored"); assertEqual(opener.focusCount, 1, "opener receives restored focus once"); assert(!restoreDialogFocus(focusableFixture({ connected: false })), "removed opener is not focused"); const backdrop = {} as EventTarget; const child = {} as EventTarget; assert(shouldCloseDialogOnBackdrop(backdrop, backdrop, true, true), "direct backdrop press closes a closable dialog"); assert(!shouldCloseDialogOnBackdrop(child, backdrop, true, true), "presses inside the panel do not close the dialog"); assert(!shouldCloseDialogOnBackdrop(backdrop, backdrop, false, true), "backdrop closing can be disabled"); assert(!shouldCloseDialogOnBackdrop(backdrop, backdrop, true, false), "backdrop press respects closeDisabled"); resetDialogStackForTests(); const outerOpener = focusableFixture(); const parentOpener = focusableFixture(); const parentLast = focusableFixture(); const parentPanel = panelFixture([parentOpener, parentLast]); const childFirst = focusableFixture(); const childLast = focusableFixture(); const childPanel = panelFixture([childFirst, childLast]); const parentId = Symbol("parent-dialog"); const childId = Symbol("child-dialog"); const parentOrder = nextDialogActivationOrder(); const childOrder = nextDialogActivationOrder(); let parentCloseCount = 0; let childCloseCount = 0; let childCanClose = false; // React may commit a nested child's effect before its parent effect. Activation // order is allocated during render so the child must remain topmost either way. const unregisterChild = registerDialog({ id: childId, activationOrder: childOrder, panel: childPanel, restoreFocus: parentOpener, canClose: () => childCanClose, onClose: () => { childCloseCount += 1; } }, parentOpener); const unregisterParent = registerDialog({ id: parentId, activationOrder: parentOrder, panel: parentPanel, restoreFocus: outerOpener, canClose: () => true, onClose: () => { parentCloseCount += 1; } }, childFirst); assertEqual(parentPanel.getAttribute("data-dialog-stack-state"), "underlying", "parent dialog is marked as underlying"); assert(parentPanel.hasAttribute("inert"), "parent dialog is inert while a child is open"); assertEqual(parentPanel.getAttribute("aria-hidden"), "true", "parent dialog is hidden from assistive technology"); assertEqual(parentPanel.getAttribute("aria-modal"), null, "underlying dialog no longer claims modal semantics"); assertEqual(childPanel.getAttribute("data-dialog-stack-state"), "topmost", "child dialog is marked as topmost"); assertEqual(childPanel.getAttribute("aria-modal"), "true", "child dialog owns modal semantics"); const stackedTab = keyboardEvent("Tab"); const parentFocusBeforeTab = parentOpener.focusCount; assert(handleTopDialogKeyDown(stackedTab, childLast), "topmost dialog traps Tab"); assertEqual(stackedTab.prevented, 1, "stacked Tab wrapping is prevented once"); assertEqual(parentOpener.focusCount, parentFocusBeforeTab, "underlying dialog does not process Tab"); const disabledStackedEscape = keyboardEvent("Escape"); assert(!handleTopDialogKeyDown(disabledStackedEscape, childFirst), "disabled child ignores Escape without falling through"); assertEqual(childCloseCount, 0, "disabled child remains open"); assertEqual(parentCloseCount, 0, "disabled child Escape does not close the parent"); childCanClose = true; const stackedEscape = keyboardEvent("Escape"); assert(handleTopDialogKeyDown(stackedEscape, childFirst), "topmost dialog handles Escape"); assertEqual(childCloseCount, 1, "one Escape closes the child once"); assertEqual(parentCloseCount, 0, "one Escape does not close the parent"); const parentFocusBeforeChildClose = parentOpener.focusCount; unregisterChild(); assertEqual(parentOpener.focusCount, parentFocusBeforeChildClose + 1, "closing the child restores focus inside the parent"); assert(!parentPanel.hasAttribute("inert"), "parent dialog becomes interactive after the child closes"); assertEqual(parentPanel.getAttribute("aria-hidden"), null, "parent dialog returns to the accessibility tree"); assertEqual(parentPanel.getAttribute("aria-modal"), "true", "parent dialog regains modal semantics"); assertEqual(parentPanel.getAttribute("data-dialog-stack-state"), "topmost", "parent dialog becomes topmost"); const parentEscape = keyboardEvent("Escape"); assert(handleTopDialogKeyDown(parentEscape, parentOpener), "parent handles Escape after child cleanup"); assertEqual(parentCloseCount, 1, "parent closes only on its own Escape handling"); unregisterParent(); assertEqual(outerOpener.focusCount, 1, "closing the final dialog restores the outer opener"); assert(!handleTopDialogKeyDown(keyboardEvent("Escape"), outerOpener), "keyboard handling stops when the stack is empty"); resetDialogStackForTests(); const forcedOuterOpener = focusableFixture(); const removedParentOpener = focusableFixture(); const removedParentPanel = panelFixture([removedParentOpener]); const forcedChildControl = focusableFixture(); const forcedChildPanel = panelFixture([forcedChildControl]); const unregisterForcedParent = registerDialog({ id: Symbol("forced-parent-dialog"), activationOrder: nextDialogActivationOrder(), panel: removedParentPanel, restoreFocus: forcedOuterOpener, canClose: () => true }, forcedOuterOpener); const unregisterForcedChild = registerDialog({ id: Symbol("forced-child-dialog"), activationOrder: nextDialogActivationOrder(), panel: forcedChildPanel, restoreFocus: removedParentOpener, canClose: () => true }, removedParentOpener); unregisterForcedParent(); Object.assign(removedParentOpener, { isConnected: false }); unregisterForcedChild(); assertEqual(forcedOuterOpener.focusCount, 1, "removing a dialog tree restores the nearest connected outer opener"); resetDialogStackForTests(); const strictModeOpener = focusableFixture(); const strictModeControl = focusableFixture(); const strictModePanel = panelFixture([strictModeControl]); const strictModeRegistration = { id: Symbol("strict-mode-dialog"), activationOrder: nextDialogActivationOrder(), panel: strictModePanel, restoreFocus: strictModeOpener, canClose: () => true }; const unregisterStrictModeFirstPass = registerDialog(strictModeRegistration, strictModeOpener); unregisterStrictModeFirstPass(); const unregisterStrictModeReplay = registerDialog(strictModeRegistration, strictModeOpener); assertEqual(strictModeControl.focusCount, 2, "StrictMode effect replay returns focus to the dialog"); assertEqual(strictModeOpener.focusCount, 1, "StrictMode cleanup restores the opener before replay"); unregisterStrictModeReplay(); assertEqual(strictModeOpener.focusCount, 2, "StrictMode replay cleanup restores focus without leaving a stack entry"); assert(!handleTopDialogKeyDown(keyboardEvent("Escape"), strictModeOpener), "StrictMode replay leaves no stale topmost dialog"); resetDialogStackForTests(); let keydownListenerAdds = 0; let keydownListenerRemovals = 0; Object.defineProperty(globalThis, "window", { configurable: true, value: { addEventListener(type: string) { if (type === "keydown") keydownListenerAdds += 1; }, removeEventListener(type: string) { if (type === "keydown") keydownListenerRemovals += 1; }, getComputedStyle() { return { display: "block", visibility: "visible" }; } } }); try { const listenerOuter = focusableFixture(); const listenerParentPanel = panelFixture([focusableFixture()]); const listenerChildPanel = panelFixture([focusableFixture()]); const unregisterListenerParent = registerDialog({ id: Symbol("listener-parent"), activationOrder: nextDialogActivationOrder(), panel: listenerParentPanel, restoreFocus: listenerOuter, canClose: () => true }, listenerOuter); const unregisterListenerChild = registerDialog({ id: Symbol("listener-child"), activationOrder: nextDialogActivationOrder(), panel: listenerChildPanel, restoreFocus: listenerParentPanel, canClose: () => true }, listenerParentPanel); assertEqual(keydownListenerAdds, 1, "one window keyboard listener serves the whole dialog stack"); unregisterListenerChild(); assertEqual(keydownListenerRemovals, 0, "keyboard listener remains while a parent dialog is open"); unregisterListenerParent(); assertEqual(keydownListenerRemovals, 1, "keyboard listener is removed after the final dialog closes"); } finally { resetDialogStackForTests(); Reflect.deleteProperty(globalThis, "window"); }