fix(a11y): keyboard-reachable admin log rows, range-bar marker, heatmap cells

Admin app-log rows opened their JSON detail dialog through a click-only
<tr> with no keyboard path; the Action cell now wraps a real <button>
that carries the same handler plus a descriptive aria-label, leaving
the row onClick as a pointer convenience.

The target range-bar's current-value marker showed value + band only
in a hover-only tooltip on a non-focusable div. It is now focusable
(tabIndex, role=img) with an aria-label that reuses the same value /
range / delta copy the tooltip already renders, so focus opens the
tooltip and a screen reader gets the text alternative either way.

Mood and medication-compliance heatmap day cells were pointer-only SVG
rects. Each cell is now focusable with its own per-day aria-label
(reusing the existing tooltip copy), and Escape dismisses any open
tooltip — pinned (touch) or focus-shown.

Reference: 2026-07-17 a11y audit, H1 + M1 + M2.
This commit is contained in:
Marc-André Bombeck committed 2026-07-18 01:23:37 +02:00
1 parent 247bc4f33c
commit 4c436d7dfc
15 files changed
+243 -27

No files matched your search

@@ -57,4 +57,16 @@ describe("<RangeBar>", () => {
});
expect(html).toContain("var(--destructive)");
});
it("makes the marker focusable with a text alternative for the value + range (2026-07-17 a11y audit M1)", () => {
const html = render({
value: 72,
min: 60,
max: 100,
unit: "bpm",
});
expect(html).toContain('tabindex="0"');
expect(html).toContain('role="img"');
expect(html).toContain('aria-label="Current: 72 bpm');
});
});
+15 -1
View File
@@ -92,6 +92,17 @@ export function RangeBar({
: t("targets.aboveTarget", { delta: delta.toFixed(1), unit })
: t("targets.inTarget");
// 2026-07-17 a11y audit (M1) — the marker's value + range live only in a
// hover-only Radix tooltip on a non-focusable `<div>`. Reusing the same
// three strings the tooltip already shows as one text alternative closes
// 1.1.1 (no separate copy to drift) — `tabIndex` below makes the trigger
// focusable, which is all Radix needs to also open the tooltip on focus.
const markerAriaLabel = [
t("targets.currentValue", { value: String(value), unit }),
t("targets.targetRangeValue", { min: String(min), max: String(max), unit }),
deltaText,
].join(". ");
return (
<div className="space-y-1.5" data-slot="target-range-bar">
<div className="bg-muted/50 relative h-3 w-full overflow-hidden rounded-full">
@@ -129,7 +140,10 @@ export function RangeBar({
<Tooltip>
<TooltipTrigger asChild>
<div
className="absolute top-1/2 h-5 w-5 -translate-x-1/2 -translate-y-1/2 cursor-pointer rounded-full border-2 shadow-sm"
role="img"
tabIndex={0}
aria-label={markerAriaLabel}
className="focus-visible:ring-ring absolute top-1/2 h-5 w-5 -translate-x-1/2 -translate-y-1/2 cursor-pointer rounded-full border-2 shadow-sm focus-visible:ring-2 focus-visible:outline-none"
style={{
left: `${position}%`,
backgroundColor: markerColor,