From 698fbc19ccec7f00effd3cbafcce6b7e5918aba4 Mon Sep 17 00:00:00 2001 From: saravanakumardb1 Date: Sat, 18 Apr 2026 18:00:00 -0700 Subject: [PATCH] feat(web): TODO-003 accessibility focus trap + ARIA roles in modals --- web/src/components/AlarmOverlay.tsx | 40 ++++++++++++++++++++++++- web/src/components/CreateTimerModal.tsx | 33 +++++++++++++++++++- 2 files changed, 71 insertions(+), 2 deletions(-) diff --git a/web/src/components/AlarmOverlay.tsx b/web/src/components/AlarmOverlay.tsx index 37dc3e8..9d81880 100644 --- a/web/src/components/AlarmOverlay.tsx +++ b/web/src/components/AlarmOverlay.tsx @@ -1,8 +1,10 @@ 'use client'; +import { useEffect, useRef, useCallback } from 'react'; import { useTimerStore } from '@/lib/store'; import { getUrgencyConfig } from '@/lib/urgency'; import { formatTime } from '@/lib/format'; +import { alertLabel } from '@bytelyst/accessibility'; import { Bell, BellOff } from 'lucide-react'; export function AlarmOverlay() { @@ -11,15 +13,48 @@ export function AlarmOverlay() { const firingTimers = timers.filter((t) => t.state === 'firing'); + const overlayRef = useRef(null); + + const trapFocus = useCallback((e: KeyboardEvent) => { + if (e.key !== 'Tab' || !overlayRef.current) return; + const focusable = overlayRef.current.querySelectorAll( + 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])' + ); + if (focusable.length === 0) return; + const first = focusable[0]; + const last = focusable[focusable.length - 1]; + if (e.shiftKey && document.activeElement === first) { + e.preventDefault(); + last.focus(); + } else if (!e.shiftKey && document.activeElement === last) { + e.preventDefault(); + first.focus(); + } + }, []); + + useEffect(() => { + if (firingTimers.length === 0) return; + document.addEventListener('keydown', trapFocus); + // Auto-focus dismiss button + const el = overlayRef.current?.querySelector('button'); + el?.focus(); + return () => document.removeEventListener('keydown', trapFocus); + }, [firingTimers.length, trapFocus]); + if (firingTimers.length === 0) return null; // Show the most urgent firing timer const timer = firingTimers[0]; const urgencyConfig = getUrgencyConfig(timer.urgency); const isCritical = timer.urgency === 'critical'; + const a11yAlert = alertLabel(isCritical ? 'danger' : 'info', `Timer fired: ${timer.label}`); return ( -
+
{/* Backdrop */}