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.
This commit is contained in:
Marc-André Bombeck committed 2026-10-01 22:18:28 +02:00
1 parent 801295f55a
commit debd44a75b
9 files changed
+109 -6

No files matched your search

+3 -2
View File
@@ -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
}
/>
@@ -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}
/>
<Tooltip
@@ -26,6 +26,7 @@ import {
} from "recharts";
import { makeFormatters } from "@/lib/format-locale";
import { useDateFormatPreference, useTranslations } from "@/lib/i18n/context";
import { axisUnitSuffix } from "@/lib/charts/axis-unit";
export interface EfficacyChartTarget {
label: string;
@@ -169,7 +170,7 @@ export function EfficacyChart({
tickLine={false}
axisLine={false}
width={40}
unit={unitSuffix ? ` ${unitSuffix}` : undefined}
unit={axisUnitSuffix(unitSuffix)}
/>
<YAxis
yAxisId="adherence"
@@ -93,6 +93,19 @@ describe("<DateField>", () => {
expect(html).toContain("sm:h-10");
});
it("gives the calendar button a 44 px touch target at its 32 px size", () => {
const html = render(<DateField value="2026-02-19" />);
const button = /<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(<DateField value="2026-02-19" />);
expect(html).toContain('aria-label="Open date picker"');
@@ -105,6 +105,18 @@ describe("formatHhmm", () => {
});
describe("<TimeField> SSR", () => {
it("gives the clock button a 44 px touch target at its 32 px size", () => {
const html = render(<TimeField value="14:05" />, "de");
const button = /<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(<TimeField value="14:05" />, "de");
expect(html).toContain('value="14:05"');
+3 -1
View File
@@ -225,7 +225,9 @@ export const DateField = React.forwardRef<HTMLInputElement, DateFieldProps>(
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"
>
<CalendarIcon className="h-4 w-4" aria-hidden="true" />
</button>
+3 -1
View File
@@ -189,7 +189,9 @@ export const TimeField = React.forwardRef<HTMLInputElement, TimeFieldProps>(
tabIndex={-1}
disabled={disabled}
aria-label={t("common.openTimePicker")}
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"
data-slot="time-field-trigger"
>
<Clock className="h-4 w-4" aria-hidden="true" />