brackt/app/hooks/useFocusTrap.ts
Claude e25cba09ac
Fix code review findings from WCAG compliance pass
- Add Arrow key navigation + roving tabindex to all role=radiogroup
  components (AutodraftSettings x2, TimerModeSelector,
  OvernightPauseSettings, ScoringPresetPicker) per ARIA radio pattern
- Extract shared focus-trap logic into useFocusTrap hook; update
  ConnectionOverlay and AuthRecoveryOverlay to use it
- Add tabIndex={-1} to ConnectionOverlay Card so focus can land in
  spinner-only state (no interactive children)
- Replace aria-live on loading dots container with sr-only span so
  status changes are announced by text content, not aria-label
- Remove contradictory aria-hidden+role=columnheader from
  AvailableParticipantsSection visual-only header row
- Remove invalid scope="col" from div[role=columnheader] in
  DraftSummaryView (scope is only valid on <th>)
- Remove redundant aria-label from ParticipantSelectionDialog sport
  select (htmlFor label is sufficient)
- Change WizardStepper connector <li> to role=presentation
- Revert muted-foreground from 62% to 55% (original already passes
  contrast; footer was fixed separately via text-muted-foreground)

https://claude.ai/code/session_01JXajpFxhqLf8aPCncP81k3
2026-05-17 23:46:57 +00:00

31 lines
1.1 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]);
}