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 <noreply@anthropic.com>
This commit is contained in:
Marcel
2026-05-29 18:50:54 +02:00
parent 53660eadc9
commit d5a7974f3a
2 changed files with 19 additions and 0 deletions

View File

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

View File

@@ -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<HTMLElement>(FOCUSABLE_SELECTOR));
function onKeydown(event: KeyboardEvent) {
@@ -38,6 +41,7 @@ export function trapFocus(node: HTMLElement) {
return {
destroy() {
node.removeEventListener('keydown', onKeydown);
previouslyFocused?.focus?.();
}
};
}