- Replace el! non-null assertion with optional chaining in useFocusTrap - Update AutodraftSettings tests to query role="radio" instead of role="button" (buttons have an explicit radio role since the WCAG pass) - Update AvailableParticipantsSection watchlist tests to use getByRole/getAllByRole instead of getByTitle/getAllByTitle (watchlist buttons now use aria-label instead of title) https://claude.ai/code/session_01JXajpFxhqLf8aPCncP81k3
31 lines
1.2 KiB
TypeScript
31 lines
1.2 KiB
TypeScript
import { useEffect, type RefObject } from "react";
|
|
|
|
const FOCUSABLE_SELECTOR =
|
|
'button:not([disabled]), [href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])';
|
|
|
|
export function useFocusTrap(ref: RefObject<HTMLElement | null>, active: boolean) {
|
|
useEffect(() => {
|
|
if (!active) return;
|
|
const el = ref.current;
|
|
if (!el) return;
|
|
|
|
const first = el.querySelector<HTMLElement>(FOCUSABLE_SELECTOR) ?? el;
|
|
first.focus();
|
|
|
|
function onKeyDown(e: KeyboardEvent) {
|
|
if (e.key !== "Tab") return;
|
|
const focusables = Array.from(el?.querySelectorAll<HTMLElement>(FOCUSABLE_SELECTOR) ?? []);
|
|
if (focusables.length === 0) return;
|
|
const firstEl = focusables[0];
|
|
const lastEl = focusables[focusables.length - 1];
|
|
if (e.shiftKey) {
|
|
if (document.activeElement === firstEl) { e.preventDefault(); lastEl.focus(); }
|
|
} else {
|
|
if (document.activeElement === lastEl) { e.preventDefault(); firstEl.focus(); }
|
|
}
|
|
}
|
|
|
|
document.addEventListener("keydown", onKeyDown);
|
|
return () => document.removeEventListener("keydown", onKeyDown);
|
|
}, [ref, active]);
|
|
}
|