fix(a11y): windows opened from a menu return focus to the menu button when they close (#33601)

This commit is contained in:
G30 2026-11-03 02:10:22 -05:00 • committed by GitHub
parent 0171427a10
commit a671146b91
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 38 additions and 1 deletions

View file

@ -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();
}

View file

@ -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;