From debd44a75b00fc4ffcd03a24710a4deb56105005 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Thu, 1 Oct 2026 22:18:28 +0200 Subject: [PATCH] fix(charts): keep axis unit labels on one line; 44 px date and time triggers Recharts word-wraps a tick label that it measures wider than its axis, and it measures in the page's body font rather than the tick's 11 px. "62 bpm" on the pulse chart under a workout and "106 mg/dL" on the glucose page therefore broke onto two lines at every width, and the pulse chart's second line sat on the first x tick. Axis units are now joined with a no-break space through `axisUnitSuffix`, so the label is one word and stays on one line. A guard fails when a chart axis joins a unit with a plain space. The calendar and clock buttons in the date and time fields paint at 32 px; a `::before` halo now gives them a 44 px touch target without changing the field. --- src/components/charts/health-chart.tsx | 5 +- .../insights/intraday-pulse-chart.tsx | 3 +- .../detail/efficacy/efficacy-chart.tsx | 3 +- .../ui/__tests__/date-field.test.tsx | 13 +++++ .../ui/__tests__/time-field.test.tsx | 12 ++++ src/components/ui/date-field.tsx | 4 +- src/components/ui/time-field.tsx | 4 +- src/lib/charts/__tests__/axis-unit.test.ts | 56 +++++++++++++++++++ src/lib/charts/axis-unit.ts | 15 +++++ 9 files changed, 109 insertions(+), 6 deletions(-) create mode 100644 src/lib/charts/__tests__/axis-unit.test.ts create mode 100644 src/lib/charts/axis-unit.ts diff --git a/src/components/charts/health-chart.tsx b/src/components/charts/health-chart.tsx index 229f38a21..60cfe4cb8 100644 --- a/src/components/charts/health-chart.tsx +++ b/src/components/charts/health-chart.tsx @@ -71,6 +71,7 @@ import { type MetricWindowStats, } from "@/lib/charts/window-stats"; import { shouldFireDataReady } from "@/lib/charts/data-ready-latch"; +import { axisUnitSuffix } from "@/lib/charts/axis-unit"; // The range tabs select a CALENDAR-DAY window ending now — `days: 7` is // "the last 7 days", not "the last 7 readings". The labels said "points" @@ -1982,8 +1983,8 @@ export function HealthChart({ : String(value) } unit={ - showYAxisUnit && (yAxisUnit ?? unit) - ? ` ${yAxisUnit ?? unit}` + showYAxisUnit + ? axisUnitSuffix(yAxisUnit ?? unit) : undefined } /> diff --git a/src/components/insights/intraday-pulse-chart.tsx b/src/components/insights/intraday-pulse-chart.tsx index 09e1ae79a..5fac93a99 100644 --- a/src/components/insights/intraday-pulse-chart.tsx +++ b/src/components/insights/intraday-pulse-chart.tsx @@ -24,6 +24,7 @@ import { TileHeader } from "@/components/insights/tile-header"; import { Button } from "@/components/ui/button"; import { QueryErrorCard } from "@/components/ui/query-error-card"; import { Skeleton } from "@/components/ui/skeleton"; +import { axisUnitSuffix } from "@/lib/charts/axis-unit"; import { BUCKET_MINUTES, type IntradayHrBucket, @@ -300,7 +301,7 @@ export function IntradayPulseChart({ tick={{ fontSize: 11, fill: "var(--muted-foreground)" }} tickLine={false} axisLine={false} - unit=" bpm" + unit={axisUnitSuffix("bpm")} width={52} /> ", () => { expect(html).toContain("sm:h-10"); }); + it("gives the calendar button a 44 px touch target at its 32 px size", () => { + const html = render(); + const button = /]*aria-label="Open date picker"[^>]*>/.exec( + html, + )?.[0]; + expect(button).toBeDefined(); + // Painted size unchanged; the ::before halo adds 6 px per edge. + expect(button).toContain("h-8 w-8"); + expect(button).toContain("relative"); + expect(button).toContain("before:-inset-1.5"); + expect(button).toContain("before:absolute"); + }); + it("exposes a labelled calendar picker affordance", () => { const html = render(); expect(html).toContain('aria-label="Open date picker"'); diff --git a/src/components/ui/__tests__/time-field.test.tsx b/src/components/ui/__tests__/time-field.test.tsx index fb031e85d..ed155c726 100644 --- a/src/components/ui/__tests__/time-field.test.tsx +++ b/src/components/ui/__tests__/time-field.test.tsx @@ -105,6 +105,18 @@ describe("formatHhmm", () => { }); describe(" SSR", () => { + it("gives the clock button a 44 px touch target at its 32 px size", () => { + const html = render(, "de"); + const button = /]*data-slot="time-field-trigger"[^>]*>/.exec( + html, + )?.[0]; + expect(button).toBeDefined(); + expect(button).toContain("h-8 w-8"); + expect(button).toContain("relative"); + expect(button).toContain("before:-inset-1.5"); + expect(button).toContain("before:absolute"); + }); + it("paints the value in 24h under the de AUTO locale and exposes the picker affordance", () => { const html = render(, "de"); expect(html).toContain('value="14:05"'); diff --git a/src/components/ui/date-field.tsx b/src/components/ui/date-field.tsx index 2dadadea8..42e3ab896 100644 --- a/src/components/ui/date-field.tsx +++ b/src/components/ui/date-field.tsx @@ -225,7 +225,9 @@ export const DateField = React.forwardRef( type="button" disabled={disabled} aria-label={t("common.openDatePicker")} - className="text-muted-foreground hover:text-foreground flex h-8 w-8 shrink-0 items-center justify-center rounded-md disabled:cursor-not-allowed disabled:opacity-50" + // The button paints at 32 px; the `::before` halo (6 px per edge) + // makes the touch target 44 px without changing the field. + className="text-muted-foreground hover:text-foreground relative flex h-8 w-8 shrink-0 items-center justify-center rounded-md before:absolute before:-inset-1.5 before:content-[''] disabled:cursor-not-allowed disabled:opacity-50" >