merge: testing → main (reconcile 2-week divergence)
This commit is contained in:
49
decnet_web/src/hooks/useFocusTrap.ts
Normal file
49
decnet_web/src/hooks/useFocusTrap.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import { useEffect, type RefObject } from 'react';
|
||||
|
||||
const FOCUSABLE =
|
||||
'input:not([disabled]), button:not([disabled]), textarea:not([disabled]), select:not([disabled]), a[href], [tabindex]:not([tabindex="-1"])';
|
||||
|
||||
export function useFocusTrap(
|
||||
ref: RefObject<HTMLElement | null>,
|
||||
active: boolean,
|
||||
): void {
|
||||
useEffect(() => {
|
||||
if (!active || !ref.current) return;
|
||||
const root = ref.current;
|
||||
const previouslyFocused = document.activeElement as HTMLElement | null;
|
||||
|
||||
const focusables = () =>
|
||||
Array.from(root.querySelectorAll<HTMLElement>(FOCUSABLE)).filter(
|
||||
(el) => !el.hasAttribute('aria-hidden') && el.offsetParent !== null,
|
||||
);
|
||||
|
||||
const autoFocus =
|
||||
root.querySelector<HTMLElement>('[data-autofocus]') ?? focusables()[0];
|
||||
autoFocus?.focus();
|
||||
|
||||
const handler = (e: KeyboardEvent) => {
|
||||
if (e.key !== 'Tab') return;
|
||||
const items = focusables();
|
||||
if (items.length === 0) {
|
||||
e.preventDefault();
|
||||
return;
|
||||
}
|
||||
const first = items[0];
|
||||
const last = items[items.length - 1];
|
||||
const current = document.activeElement as HTMLElement | null;
|
||||
if (e.shiftKey && current === first) {
|
||||
e.preventDefault();
|
||||
last.focus();
|
||||
} else if (!e.shiftKey && current === last) {
|
||||
e.preventDefault();
|
||||
first.focus();
|
||||
}
|
||||
};
|
||||
|
||||
root.addEventListener('keydown', handler);
|
||||
return () => {
|
||||
root.removeEventListener('keydown', handler);
|
||||
previouslyFocused?.focus?.();
|
||||
};
|
||||
}, [ref, active]);
|
||||
}
|
||||
Reference in New Issue
Block a user