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:
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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?.();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user