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.
This commit is contained in:
Marc-André Bombeck committed 2026-05-16 02:31:10 +02:00
1 parent 5570971f3d
commit b3f88026f0
4 files changed
+45 -7

No files matched your search

@@ -55,6 +55,21 @@ describe("<HealthScoreDeltaExplainer>", () => {
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(<HealthScoreDeltaExplainer delta={-3} />);
const trigger = html.match(
/<button[^>]*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(<HealthScoreDeltaExplainer delta={2} />, "de");
const trigger = html.match(
@@ -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,
)}
@@ -37,6 +37,21 @@ describe("<TargetCoachButton>", () => {
expect(html).not.toMatch(/>Ask Coach</);
});
it("lifts the tap target to the 44 px WCAG 2.5.5 floor", () => {
// 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(
/<button[^>]*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("");
@@ -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)}
>
<MessageCircle className="size-4" aria-hidden="true" />
</Button>