From b3f88026f0c1e805a62023f71c19717069af908e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Sat, 16 May 2026 02:31:10 +0200 Subject: [PATCH] fix(a11y): lift new icon buttons to the 44 px tap-target floor The per-card Coach button on the targets page rode the shadcn icon variant (size-10, 40 px) and the HealthScore delta explainer trigger collapsed to a 16 px chip. Both sit under the project's 44 px floor (WCAG 2.5.5). The targets Coach button gains min-h-11 min-w-11 on the icon variant; the delta explainer keeps a 12 px visible glyph but inflates the hit surface to 44 px and pulls the row stride back via negative margin so the optical chip stays small. --- .../health-score-delta-explainer.test.tsx | 15 +++++++++++++++ .../insights/health-score-delta-explainer.tsx | 15 +++++++++------ .../__tests__/target-coach-button.test.tsx | 15 +++++++++++++++ src/components/targets/target-coach-button.tsx | 7 ++++++- 4 files changed, 45 insertions(+), 7 deletions(-) diff --git a/src/components/insights/__tests__/health-score-delta-explainer.test.tsx b/src/components/insights/__tests__/health-score-delta-explainer.test.tsx index 4f89d7c1d..b6d7ec0a1 100644 --- a/src/components/insights/__tests__/health-score-delta-explainer.test.tsx +++ b/src/components/insights/__tests__/health-score-delta-explainer.test.tsx @@ -55,6 +55,21 @@ describe("", () => { expect(trigger?.[0]).toContain('aria-label="What does this delta mean?"'); }); + it("lifts the tap target to the 44 px WCAG 2.5.5 floor", () => { + // The glyph stays 12 px but the click surface inflates to 44 px + // via `min-h-11 min-w-11` on the trigger; the parent row keeps + // its 16 px stride because `-my-3 -mx-2` swallows the extra + // reach. Pinning so a future refactor can't drop back to the + // visual-chip-as-hit-target shape that failed WCAG 2.5.5. + const html = ssr(); + const trigger = html.match( + /]*data-slot="health-score-delta-explainer-trigger"[^>]*>/, + ); + expect(trigger).not.toBeNull(); + expect(trigger?.[0]).toContain("min-h-11"); + expect(trigger?.[0]).toContain("min-w-11"); + }); + it("renders the trigger in German with the localised aria-label", () => { const html = ssr(, "de"); const trigger = html.match( diff --git a/src/components/insights/health-score-delta-explainer.tsx b/src/components/insights/health-score-delta-explainer.tsx index 38b0d96b7..96ed55f49 100644 --- a/src/components/insights/health-score-delta-explainer.tsx +++ b/src/components/insights/health-score-delta-explainer.tsx @@ -65,13 +65,16 @@ export function HealthScoreDeltaExplainer({ data-slot="health-score-delta-explainer-trigger" aria-label={triggerLabel} className={cn( - // Icon-only chip sized to read at the delta line's height - // without inflating the row. The 16 px hit target satisfies - // the existing 11 px delta line's reading rhythm while the - // outer focus ring still grants a comfortable tap target - // because the ring sits outside the box. + // Visible glyph stays 12 px so the chip reads at the delta + // line's height without inflating the row. The button itself + // sits at the 44 px WCAG 2.5.5 floor via `min-h-11 min-w-11`; + // negative `-my-3 -mx-2` collapses the surrounding row back + // to its 16 px stride so the optical chip stays small while + // the hit surface lifts to 44 px. Transparent padding owns + // the extra reach. "text-muted-foreground hover:text-foreground focus-visible:ring-ring/50", - "inline-flex h-4 w-4 shrink-0 items-center justify-center rounded-full", + "inline-flex min-h-11 min-w-11 shrink-0 items-center justify-center rounded-full", + "-my-3 -mx-2", "transition-colors focus-visible:ring-2 focus-visible:outline-none", className, )} diff --git a/src/components/targets/__tests__/target-coach-button.test.tsx b/src/components/targets/__tests__/target-coach-button.test.tsx index ea017ac77..25bfd10f5 100644 --- a/src/components/targets/__tests__/target-coach-button.test.tsx +++ b/src/components/targets/__tests__/target-coach-button.test.tsx @@ -37,6 +37,21 @@ describe("", () => { expect(html).not.toMatch(/>Ask Coach { + // The icon variant alone ships `size-10` (40 px); the per-card + // Coach button rides `min-h-11 min-w-11` on top so the hit + // surface matches the medication-history and Coach drawer + // buttons. Pinning the class so a future refactor can't drop + // back to the 40 px shadcn default. + const html = render({ ...baseProps, aiEnabled: true }); + const trigger = html.match( + /]*data-slot="target-coach-cta"[^>]*>/, + ); + expect(trigger).not.toBeNull(); + expect(trigger?.[0]).toContain("min-h-11"); + expect(trigger?.[0]).toContain("min-w-11"); + }); + it("renders NOTHING when aiEnabled is false (no broken-button state)", () => { const html = render({ ...baseProps, aiEnabled: false }); expect(html).toBe(""); diff --git a/src/components/targets/target-coach-button.tsx b/src/components/targets/target-coach-button.tsx index 2381351ef..beb09f2a0 100644 --- a/src/components/targets/target-coach-button.tsx +++ b/src/components/targets/target-coach-button.tsx @@ -3,6 +3,7 @@ import { MessageCircle } from "lucide-react"; import { Button } from "@/components/ui/button"; +import { cn } from "@/lib/utils"; import { useTranslations } from "@/lib/i18n/context"; import type { CoachScope, CoachScopeSource } from "@/lib/ai/coach/types"; @@ -78,7 +79,11 @@ export function TargetCoachButton({ data-slot="target-coach-cta" aria-label={accessibleLabel} title={accessibleLabel} - className={className} + // Lift the icon button to the 44 px WCAG 2.5.5 floor. The + // shadcn `icon` variant ships `size-10` (40 px); the project + // floor is 44 px (matches the medication-history button and the + // Coach drawer cluster). Glyph stays `size-4`. + className={cn("min-h-11 min-w-11", className)} >