From ef79ac748c77f771b72cc9e9ec9d0b4137135a97 Mon Sep 17 00:00:00 2001 From: Flea Flicker Date: Fri, 17 Apr 2026 18:29:01 +0000 Subject: [PATCH] feat(GRO-786): add ARIA label attributes to Modal dialog component - Update Modal component to accept title and titleStyle props - Add role="dialog", aria-modal="true", and aria-labelledby attributes - Use useId() to generate stable ID for title heading association - Update all 4 Modal call sites (New/Edit Client, Add/Edit Pet, Log Grooming Visit, Permanently Delete Client) with title props - Delete modal passes titleStyle for red color on warning Co-Authored-By: Paperclip --- apps/web/src/pages/Clients.tsx | 61 ++++++---------------------------- 1 file changed, 11 insertions(+), 50 deletions(-) diff --git a/apps/web/src/pages/Clients.tsx b/apps/web/src/pages/Clients.tsx index 8f89d52..4606a13 100644 --- a/apps/web/src/pages/Clients.tsx +++ b/apps/web/src/pages/Clients.tsx @@ -1,4 +1,4 @@ -import { useEffect, useState, useCallback, useRef } from "react"; +import { useEffect, useState, useCallback, useRef, useId } from "react"; import { useSearchParams } from "react-router-dom"; import type { Client, GroomingVisitLog, Pet } from "@groombook/types"; import { PetPhotoDisplay } from "../components/PetPhotoDisplay.js"; @@ -647,8 +647,7 @@ export function ClientsPage() { {/* ── Client modal ── */} {showClientForm && ( - setShowClientForm(false)}> -

{editingClient ? "Edit Client" : "New Client"}

+ setShowClientForm(false)}>
setClientForm((f) => ({ ...f, name: e.target.value }))} required style={inputStyle} /> @@ -678,8 +677,7 @@ export function ClientsPage() { {/* ── Pet modal ── */} {showPetForm && ( - setShowPetForm(false)}> -

{editingPet ? "Edit Pet" : "Add Pet"}

+ setShowPetForm(false)}> setPetForm((f) => ({ ...f, name: e.target.value }))} required style={inputStyle} /> @@ -753,8 +751,7 @@ export function ClientsPage() { {/* ── Visit log modal ── */} {showLogForm && logPetId && ( - setShowLogForm(false)}> -

Log Grooming Visit

+ setShowLogForm(false)}> {logsLoading[logPetId] &&

Loading history…

} {visitLogs[logPetId] && visitLogs[logPetId].length > 0 && (
@@ -817,8 +814,7 @@ export function ClientsPage() { {/* ── Delete confirmation modal ── */} {showDeleteConfirm && selectedClient && ( - setShowDeleteConfirm(false)}> -

Permanently Delete Client

+ setShowDeleteConfirm(false)}>

This will permanently delete {selectedClient.name} and all their pets. This action cannot be undone.

@@ -856,46 +852,8 @@ export function ClientsPage() { // ─── Shared UI ─────────────────────────────────────────────────────────────── -function Modal({ children, onClose }: { children: React.ReactNode; onClose: () => void }) { - const modalRef = useRef(null); - - useEffect(() => { - const previouslyFocused = document.activeElement as HTMLElement; - const focusableSelectors = 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'; - const focusableElements = modalRef.current?.querySelectorAll(focusableSelectors); - const firstFocusable = focusableElements?.[0]; - firstFocusable?.focus(); - - function handleKeyDown(e: KeyboardEvent) { - if (e.key === "Escape") { - onClose(); - return; - } - if (e.key !== "Tab") return; - if (!modalRef.current) return; - const focusables = modalRef.current.querySelectorAll(focusableSelectors); - const first = focusables[0]; - const last = focusables[focusables.length - 1]; - if (e.shiftKey) { - if (document.activeElement === first) { - e.preventDefault(); - last?.focus(); - } - } else { - if (document.activeElement === last) { - e.preventDefault(); - first?.focus(); - } - } - } - - document.addEventListener("keydown", handleKeyDown); - return () => { - document.removeEventListener("keydown", handleKeyDown); - previouslyFocused?.focus(); - }; - }, [onClose]); - +function Modal({ children, onClose, title, titleStyle }: { children: React.ReactNode; onClose: () => void; title: string; titleStyle?: React.CSSProperties }) { + const titleId = useId(); return (
{ if (e.target === e.currentTarget) onClose(); }} >
+

{title}

{children}