From d5a7974f3a1431281fa10c69966d0e1628061b72 Mon Sep 17 00:00:00 2001 From: Marcel Date: Fri, 29 May 2026 18:50:54 +0200 Subject: [PATCH] fix(shared): trapFocus restores focus to the opener on destroy (#692) When the bottom sheet closes, focus returns to the element that was focused before it opened instead of being dropped to document.body (WCAG 2.4.3, Architect + UX review). Co-Authored-By: Claude Opus 4.8 --- .../lib/shared/actions/trapFocus.svelte.spec.ts | 15 +++++++++++++++ frontend/src/lib/shared/actions/trapFocus.ts | 4 ++++ 2 files changed, 19 insertions(+) diff --git a/frontend/src/lib/shared/actions/trapFocus.svelte.spec.ts b/frontend/src/lib/shared/actions/trapFocus.svelte.spec.ts index c35cf01d..fcca10a6 100644 --- a/frontend/src/lib/shared/actions/trapFocus.svelte.spec.ts +++ b/frontend/src/lib/shared/actions/trapFocus.svelte.spec.ts @@ -57,4 +57,19 @@ describe('trapFocus action', () => { // No trap after destroy → focus stays on the last button. expect(document.activeElement).toBe(buttons[1]); }); + + it('restores focus to the previously-focused element on destroy (WCAG 2.4.3)', () => { + const opener = document.createElement('button'); + document.body.appendChild(opener); + nodes.push(opener); + opener.focus(); + expect(document.activeElement).toBe(opener); + + const { node } = makeContainer(['one', 'two']); + const handle = trapFocus(node); + expect(document.activeElement).not.toBe(opener); + + handle.destroy(); + expect(document.activeElement).toBe(opener); + }); }); diff --git a/frontend/src/lib/shared/actions/trapFocus.ts b/frontend/src/lib/shared/actions/trapFocus.ts index 6c941978..fd393843 100644 --- a/frontend/src/lib/shared/actions/trapFocus.ts +++ b/frontend/src/lib/shared/actions/trapFocus.ts @@ -14,6 +14,9 @@ const FOCUSABLE_SELECTOR = [ ].join(','); export function trapFocus(node: HTMLElement) { + // Remember what had focus so it can be restored when the overlay closes + // (WCAG 2.4.3 — don't strand keyboard/AT users at the top of the page). + const previouslyFocused = document.activeElement as HTMLElement | null; const focusable = () => Array.from(node.querySelectorAll(FOCUSABLE_SELECTOR)); function onKeydown(event: KeyboardEvent) { @@ -38,6 +41,7 @@ export function trapFocus(node: HTMLElement) { return { destroy() { node.removeEventListener('keydown', onKeydown); + previouslyFocused?.focus?.(); } }; }