From 72fcdc656fb3e53f753b10d489f232b4b900207d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Thu, 14 May 2026 16:55:10 +0200 Subject: [PATCH] =?UTF-8?q?fix(a11y):=20WCAG=20touch-target=20floor=20+=20?= =?UTF-8?q?ARIA=20pairing=20across=20W4=E2=80=93W9=20surfaces?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Extends the W8.3 44px touch-target rule and the W8e accordion ARIA contract to surfaces that shipped without them: - Insights tab-strip pills: pills are primary navigation; promote to `min-h-11` so a thumb tap on "Stimmung" cannot bleed into the neighbour two pixels below. - Injection-site picker: raise the invisible hit-target radius from 11 to 12 SVG units so the rendered diameter (\~25.6 CSS px) clears the WCAG 2.5.8 Level AA 24×24 floor. The abdomen left/right pair at \Δx=24 caps the upper bound — larger circles would collide. - Source-priority reorder arrows (per-metric + device-type axes): promote four `h-7 w-7` icon buttons to `h-11 w-11`. - Sub-page heading: replace the bare `focus-visible:outline-none` with the same ring vocabulary used by insights pills + the Coach drawer so sighted-keyboard users get a visible indicator on programmatic focus. - Source-priority per-metric + device-type accordions: pair `aria-expanded` with `aria-controls` + matching `id` on the panel, mirroring the W8e Provenance contract. - Maintainership banner dismiss button: 22×22 hit zone replaced by a 44×44 inline-flex button that keeps the same icon size. - Injection-site picker group: `aria-label` was leaking the cadence-preset string; introduce `medications.injectionSitePickerAriaLabel` and translate it across all six locales. - Body-outline SVG: hardcoded English "Body outline" replaced by `medications.injectionSiteBodyOutlineAriaLabel` with native translations in DE/FR/IT/ES/PL. --- messages/de.json | 2 ++ messages/en.json | 2 ++ messages/es.json | 2 ++ messages/fr.json | 2 ++ messages/it.json | 2 ++ messages/pl.json | 2 ++ src/components/i18n/maintainership-banner.tsx | 4 +++- .../insights/insights-tab-strip.tsx | 14 +++++++----- src/components/insights/sub-page-shell.tsx | 6 ++++- .../medications/injection-site-picker.tsx | 15 ++++++++----- src/components/settings/sources-section.tsx | 22 ++++++++++++++----- 11 files changed, 54 insertions(+), 19 deletions(-) diff --git a/messages/de.json b/messages/de.json index 98a22e99b..e76f04971 100644 --- a/messages/de.json +++ b/messages/de.json @@ -573,6 +573,8 @@ "glp1Inventory": "{pens} Pens übrig · ~{weeks} Wochen Reichweite", "glp1InventoryLow": "Niedriger Bestand — bald nachbestellen", "glp1LogSideEffect": "Nebenwirkung loggen", + "injectionSiteBodyOutlineAriaLabel": "Körperumriss", + "injectionSitePickerAriaLabel": "Injektionsstelle auswählen", "siteAbdomenLeft": "Bauch, unten links", "siteAbdomenRight": "Bauch, unten rechts", "siteAbdomenUpperLeft": "Bauch, oben links", diff --git a/messages/en.json b/messages/en.json index c63da8fa7..fcc96abec 100644 --- a/messages/en.json +++ b/messages/en.json @@ -573,6 +573,8 @@ "glp1Inventory": "{pens} pens left · ~{weeks} weeks of supply", "glp1InventoryLow": "Low stock — refill soon", "glp1LogSideEffect": "Log side effect", + "injectionSiteBodyOutlineAriaLabel": "Body outline", + "injectionSitePickerAriaLabel": "Injection site picker", "siteAbdomenLeft": "Abdomen, lower left", "siteAbdomenRight": "Abdomen, lower right", "siteAbdomenUpperLeft": "Abdomen, upper left", diff --git a/messages/es.json b/messages/es.json index dcefbb9f9..84b1a3401 100644 --- a/messages/es.json +++ b/messages/es.json @@ -573,6 +573,8 @@ "glp1Inventory": "{pens} pens left · ~{weeks} weeks of supply", "glp1InventoryLow": "Low stock — refill soon", "glp1LogSideEffect": "Log side effect", + "injectionSiteBodyOutlineAriaLabel": "Silueta del cuerpo", + "injectionSitePickerAriaLabel": "Selector de sitio de inyección", "siteAbdomenLeft": "Abdomen, lower left", "siteAbdomenRight": "Abdomen, lower right", "siteAbdomenUpperLeft": "Abdomen, upper left", diff --git a/messages/fr.json b/messages/fr.json index f51755eda..391fb2282 100644 --- a/messages/fr.json +++ b/messages/fr.json @@ -573,6 +573,8 @@ "glp1Inventory": "{pens} pens left · ~{weeks} weeks of supply", "glp1InventoryLow": "Low stock — refill soon", "glp1LogSideEffect": "Log side effect", + "injectionSiteBodyOutlineAriaLabel": "Silhouette du corps", + "injectionSitePickerAriaLabel": "Sélecteur de site d'injection", "siteAbdomenLeft": "Abdomen, lower left", "siteAbdomenRight": "Abdomen, lower right", "siteAbdomenUpperLeft": "Abdomen, upper left", diff --git a/messages/it.json b/messages/it.json index 67b02a25b..e1b616912 100644 --- a/messages/it.json +++ b/messages/it.json @@ -573,6 +573,8 @@ "glp1Inventory": "{pens} pens left · ~{weeks} weeks of supply", "glp1InventoryLow": "Low stock — refill soon", "glp1LogSideEffect": "Log side effect", + "injectionSiteBodyOutlineAriaLabel": "Sagoma del corpo", + "injectionSitePickerAriaLabel": "Selettore del sito di iniezione", "siteAbdomenLeft": "Abdomen, lower left", "siteAbdomenRight": "Abdomen, lower right", "siteAbdomenUpperLeft": "Abdomen, upper left", diff --git a/messages/pl.json b/messages/pl.json index f2a472673..569f95b3b 100644 --- a/messages/pl.json +++ b/messages/pl.json @@ -573,6 +573,8 @@ "glp1Inventory": "{pens} pens left · ~{weeks} weeks of supply", "glp1InventoryLow": "Low stock — refill soon", "glp1LogSideEffect": "Log side effect", + "injectionSiteBodyOutlineAriaLabel": "Sylwetka ciała", + "injectionSitePickerAriaLabel": "Wybór miejsca iniekcji", "siteAbdomenLeft": "Abdomen, lower left", "siteAbdomenRight": "Abdomen, lower right", "siteAbdomenUpperLeft": "Abdomen, upper left", diff --git a/src/components/i18n/maintainership-banner.tsx b/src/components/i18n/maintainership-banner.tsx index 64acbe73a..329c678ec 100644 --- a/src/components/i18n/maintainership-banner.tsx +++ b/src/components/i18n/maintainership-banner.tsx @@ -109,7 +109,9 @@ export function MaintainershipBanner() { type="button" onClick={handleDismiss} aria-label={t("i18n.maintainershipBanner.dismiss")} - className="hover:text-foreground -mr-1 -mt-0.5 shrink-0 rounded-sm p-1 transition-colors" + // 44×44 touch-target floor (WCAG 2.5.5). The icon stays small — + // the surrounding hit area is what the user actually taps. + className="hover:text-foreground focus-visible:ring-ring/50 -mr-2 inline-flex h-11 w-11 shrink-0 items-center justify-center rounded-sm transition-colors focus-visible:ring-2 focus-visible:outline-none" > diff --git a/src/components/insights/insights-tab-strip.tsx b/src/components/insights/insights-tab-strip.tsx index dbf4b5743..e989a6a82 100644 --- a/src/components/insights/insights-tab-strip.tsx +++ b/src/components/insights/insights-tab-strip.tsx @@ -130,7 +130,10 @@ export function InsightsTabStrip({ data-slot="insights-tab-strip-pill" data-active={isActive ? "true" : undefined} className={cn( - "shrink-0 rounded-full border px-3 py-1 text-xs font-medium transition-colors", + // 44px touch-target floor (W8.3) — pills are primary + // navigation; the regenerate icon-button to the right + // already meets the same minimum. + "inline-flex min-h-11 shrink-0 items-center rounded-full border px-3 py-1 text-xs font-medium transition-colors", "focus-visible:ring-ring/50 focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none", isActive ? "border-primary bg-primary/10 text-primary" @@ -151,11 +154,10 @@ export function InsightsTabStrip({ title={regenerateLabel} data-slot="insights-tab-strip-regenerate" className={cn( - // 44×44 touch target — Marc directive 2026-05-14. The - // pill row above sits 28px tall; the button breaks the - // band height intentionally so the touch target meets - // the WCAG 2.5.5 minimum without forcing the pills to - // grow. + // 44×44 touch target — same WCAG 2.5.5 floor the pill row + // now honours via `min-h-11`. The button stays circular so + // its visual weight matches the existing top-bar icon + // buttons. "inline-flex h-11 w-11 shrink-0 items-center justify-center rounded-full", "text-muted-foreground hover:text-foreground hover:bg-accent", "transition-colors", diff --git a/src/components/insights/sub-page-shell.tsx b/src/components/insights/sub-page-shell.tsx index cc8abfc7a..3699d1068 100644 --- a/src/components/insights/sub-page-shell.tsx +++ b/src/components/insights/sub-page-shell.tsx @@ -68,7 +68,11 @@ export function SubPageShell({ tabIndex={-1} className={cn( "text-xl font-semibold sm:text-2xl", - "focus-visible:outline-none", + // a11y: sighted-keyboard users (e.g. "Skip to content") + // need a visible focus indicator on the programmatic + // `headingRef.focus()` call below. Match the focus ring + // vocabulary used on insights pills + Coach affordances. + "rounded-sm focus-visible:ring-ring/50 focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none", )} > {title} diff --git a/src/components/medications/injection-site-picker.tsx b/src/components/medications/injection-site-picker.tsx index 735dd3470..f4246728a 100644 --- a/src/components/medications/injection-site-picker.tsx +++ b/src/components/medications/injection-site-picker.tsx @@ -51,13 +51,13 @@ export function InjectionSitePicker({
{/* Head */} - {/* Invisible 22px hit-target for touch — meets WCAG 2.5.5 - without crowding the visible 12px dot. */} + {/* Invisible 24-unit hit-target for touch. The SVG renders + at ≈1.07× scale (320px box / 300-unit viewBox height), + so r=12 gives ≈25.6 CSS px diameter — clearing the + WCAG 2.5.8 Level AA 24×24 floor. Going larger would + overlap the abdomen-left / abdomen-right pair at + Δx=24 units; the picker stays interactive without + spacing collisions. */} moveSource(metric, index, -1)} disabled={index === 0 || saveMutation.isPending} aria-label={t("settings.sections.sources.moveUp")} @@ -347,7 +347,7 @@ export function SourcesSection() { type="button" variant="ghost" size="icon" - className="h-7 w-7" + className="h-11 w-11" onClick={() => moveSource(metric, index, 1)} disabled={ index === list.length - 1 || saveMutation.isPending @@ -375,6 +375,7 @@ export function SourcesSection() { type="button" onClick={() => setShowPerMetric((prev) => !prev)} aria-expanded={showPerMetric} + aria-controls="sources-per-metric-panel" className="text-foreground hover:text-primary flex items-center gap-2 text-sm font-medium transition-colors" > {showPerMetric && ( -
+

{t("settings.sections.sources.perMetricHelp")}

@@ -434,6 +439,7 @@ export function SourcesSection() { type="button" onClick={() => setShowDeviceType((prev) => !prev)} aria-expanded={showDeviceType} + aria-controls="sources-device-type-panel" className="text-foreground hover:text-primary flex items-center gap-2 text-sm font-medium transition-colors" > {showDeviceType && ( -
+

{t("settings.sections.sources.deviceTypeHelp")}

@@ -484,7 +494,7 @@ export function SourcesSection() { type="button" variant="ghost" size="icon" - className="h-7 w-7" + className="h-11 w-11" onClick={() => moveDeviceType("__default__", index, -1) } @@ -497,7 +507,7 @@ export function SourcesSection() { type="button" variant="ghost" size="icon" - className="h-7 w-7" + className="h-11 w-11" onClick={() => moveDeviceType("__default__", index, 1)} disabled={ index === list.length - 1 || saveMutation.isPending