feat(shared): add trapFocus action for modal overlays (#692)
Focuses the first focusable on mount and wraps Tab/Shift+Tab within the node. Used by the Stammbaum mobile bottom sheet (NFR-A11Y-004). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
43
frontend/src/lib/shared/actions/trapFocus.ts
Normal file
43
frontend/src/lib/shared/actions/trapFocus.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
/**
|
||||
* Trap keyboard focus within a node and move focus to its first focusable
|
||||
* element on mount. Used by modal-style overlays such as the Stammbaum mobile
|
||||
* bottom sheet (#692, NFR-A11Y-004). Tab from the last focusable wraps to the
|
||||
* first and Shift+Tab from the first wraps to the last.
|
||||
*/
|
||||
const FOCUSABLE_SELECTOR = [
|
||||
'a[href]',
|
||||
'button:not([disabled])',
|
||||
'input:not([disabled])',
|
||||
'select:not([disabled])',
|
||||
'textarea:not([disabled])',
|
||||
'[tabindex]:not([tabindex="-1"])'
|
||||
].join(',');
|
||||
|
||||
export function trapFocus(node: HTMLElement) {
|
||||
const focusable = () => Array.from(node.querySelectorAll<HTMLElement>(FOCUSABLE_SELECTOR));
|
||||
|
||||
function onKeydown(event: KeyboardEvent) {
|
||||
if (event.key !== 'Tab') return;
|
||||
const items = focusable();
|
||||
if (items.length === 0) return;
|
||||
const first = items[0];
|
||||
const last = items[items.length - 1];
|
||||
const active = document.activeElement;
|
||||
if (event.shiftKey && active === first) {
|
||||
event.preventDefault();
|
||||
last.focus();
|
||||
} else if (!event.shiftKey && active === last) {
|
||||
event.preventDefault();
|
||||
first.focus();
|
||||
}
|
||||
}
|
||||
|
||||
node.addEventListener('keydown', onKeydown);
|
||||
focusable()[0]?.focus();
|
||||
|
||||
return {
|
||||
destroy() {
|
||||
node.removeEventListener('keydown', onKeydown);
|
||||
}
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user