From a671146b9172407d11240b8130eda9e92dfb2225 Mon Sep 17 00:00:00 2001 From: G30 <50341825+silentoplayz@users.noreply.github.com> Date: Tue, 3 Nov 2026 02:10:22 -0500 Subject: [PATCH] fix(a11y): windows opened from a menu return focus to the menu button when they close (#33601) --- .../components/common/ConfirmDialog.svelte | 21 ++++++++++++++++++- src/lib/components/common/Modal.svelte | 18 ++++++++++++++++ 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/src/lib/components/common/ConfirmDialog.svelte b/src/lib/components/common/ConfirmDialog.svelte index 0112523afe..1974a21075 100644 --- a/src/lib/components/common/ConfirmDialog.svelte +++ b/src/lib/components/common/ConfirmDialog.svelte @@ -41,6 +41,21 @@ let mounted = false; let focusTrap: FocusTrap.FocusTrap | null = null; + let returnFocusTarget: HTMLElement | null = null; + + const handleFocusOutside = (event: FocusEvent) => { + if ( + event.target instanceof HTMLElement && + !modalElement?.contains(event.target) && + !event.target.closest('[role="menu"], [role="listbox"]') + ) { + returnFocusTarget = event.target; + } + }; + + const getReturnFocus = (previous: HTMLElement | SVGElement) => + previous.isConnected ? previous : (returnFocusTarget ?? previous); + let previousOverflow: string | null = null; const restoreOverflow = () => { if (previousOverflow !== null) { @@ -93,16 +108,19 @@ $: if (mounted) { if (show && modalElement) { document.body.appendChild(modalElement); - focusTrap = FocusTrap.createFocusTrap(modalElement); + returnFocusTarget = null; + focusTrap = FocusTrap.createFocusTrap(modalElement, { setReturnFocus: getReturnFocus }); focusTrap.activate(); window.addEventListener('keydown', handleKeyDown); + window.addEventListener('focusin', handleFocusOutside, true); previousOverflow = document.body.style.overflow; document.body.style.overflow = 'hidden'; } else if (modalElement) { focusTrap.deactivate(); window.removeEventListener('keydown', handleKeyDown); + window.removeEventListener('focusin', handleFocusOutside, true); document.body.removeChild(modalElement); restoreOverflow(); @@ -113,6 +131,7 @@ show = false; restoreOverflow(); window.removeEventListener('keydown', handleKeyDown); + window.removeEventListener('focusin', handleFocusOutside, true); if (focusTrap) { focusTrap.deactivate(); } diff --git a/src/lib/components/common/Modal.svelte b/src/lib/components/common/Modal.svelte index cbef1d6976..f4fae6d0fc 100644 --- a/src/lib/components/common/Modal.svelte +++ b/src/lib/components/common/Modal.svelte @@ -20,6 +20,7 @@ // https://www.w3.org/WAI/WCAG21/Understanding/focus-order.html // https://www.w3.org/WAI/WCAG21/Understanding/keyboard.html let focusTrap: FocusTrap.FocusTrap | null = null; + let returnFocusTarget: HTMLElement | null = null; const sizeToWidth = (size: string) => { if (size === 'full') { @@ -58,6 +59,19 @@ } }; + const handleFocusOutside = (event: FocusEvent) => { + if ( + event.target instanceof HTMLElement && + !modalElement?.contains(event.target) && + !event.target.closest('[role="menu"], [role="listbox"]') + ) { + returnFocusTarget = event.target; + } + }; + + const getReturnFocus = (previous: HTMLElement | SVGElement) => + previous.isConnected ? previous : (returnFocusTarget ?? previous); + const isTopModal = () => { const modals = document.getElementsByClassName('modal'); return modals.length && modals[modals.length - 1] === modalElement; @@ -74,6 +88,7 @@ window.removeEventListener('focusin', handleOutsideInteraction, true); } if (handleModalFocusIn) modalElement?.removeEventListener('focusin', handleModalFocusIn); + window.removeEventListener('focusin', handleFocusOutside, true); window.removeEventListener('keydown', handleKeyDown); if (previousOverflow !== null) { document.body.style.overflow = previousOverflow; @@ -83,8 +98,10 @@ $: if (show && modalElement) { document.body.appendChild(modalElement); + returnFocusTarget = null; focusTrap = FocusTrap.createFocusTrap(modalElement, { escapeDeactivates: false, + setReturnFocus: getReturnFocus, allowOutsideClick: (e) => { return ( e.target instanceof Element && @@ -125,6 +142,7 @@ document.addEventListener('pointerdown', handleOutsideInteraction, true); window.addEventListener('focusin', handleOutsideInteraction, true); modalElement.addEventListener('focusin', handleModalFocusIn); + window.addEventListener('focusin', handleFocusOutside, true); window.addEventListener('keydown', handleKeyDown); previousOverflow = document.body.style.overflow;