From bfb1b43af2515ce75c5ed787328747ce6f2a8848 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Sat, 16 May 2026 17:46:21 +0200 Subject: [PATCH] fix(insights): promote coach-rail labels to semantic h3 headings The history and sources rails mount inline next to the message thread on lg+ / xl+ viewports, where no SheetTitle wrapper covers them. Both rails previously rendered their section labels as with uppercase styling, so the drawer's semantic outline disappeared above the message thread on desktop. Screen-reader users navigating by heading skipped straight from the drawer's

title to the message-thread region with no intermediate landmark. Promote both labels to

while keeping the existing visual treatment intact. The mobile rail-tray (Sheet-wrapped) still surfaces its SheetTitle

, so the rail's own

simply nests below it on small screens. The desktop layout now carries a coherent h2 (drawer) > h3 (rail) hierarchy. Regression tests assert that each rail renders its label inside a real

element so the semantic outline cannot silently drop again. --- .../__tests__/history-rail.test.tsx | 15 +++++++++++++++ .../__tests__/sources-rail.test.tsx | 11 +++++++++++ .../insights/coach-panel/history-rail.tsx | 18 +++++++++++++----- .../insights/coach-panel/sources-rail.tsx | 18 +++++++++++++----- 4 files changed, 52 insertions(+), 10 deletions(-) diff --git a/src/components/insights/coach-panel/__tests__/history-rail.test.tsx b/src/components/insights/coach-panel/__tests__/history-rail.test.tsx index d1e8d0678..4f22bb61c 100644 --- a/src/components/insights/coach-panel/__tests__/history-rail.test.tsx +++ b/src/components/insights/coach-panel/__tests__/history-rail.test.tsx @@ -66,6 +66,21 @@ describe("", () => { expect(html).toContain("Conversations"); }); + it("renders the rail label as a real `

` heading (v1.4.33)", () => { + // The rail mounts inline on `lg+` desktop where no `SheetTitle` + // wrapper covers it. Without a real heading element the drawer + // lost its semantic outline above the message thread — fixed in + // v1.4.33 by promoting the rail label to `

`. + const client = makeClientWithConversations(samplePage); + const html = render( + {}} />, + client, + ); + expect(html).toMatch( + /]*data-slot="coach-history-rail-heading"[^>]*>[\s\S]*Conversations[\s\S]*<\/h3>/, + ); + }); + it("renders one row per conversation", () => { const client = makeClientWithConversations(samplePage); const html = render( diff --git a/src/components/insights/coach-panel/__tests__/sources-rail.test.tsx b/src/components/insights/coach-panel/__tests__/sources-rail.test.tsx index 84373b10f..3e4b974ff 100644 --- a/src/components/insights/coach-panel/__tests__/sources-rail.test.tsx +++ b/src/components/insights/coach-panel/__tests__/sources-rail.test.tsx @@ -17,6 +17,17 @@ describe("", () => { expect(html).toContain("What I can see"); }); + it("renders the rail label as a real `

` heading (v1.4.33)", () => { + // The rail mounts inline on `xl+` desktop where no `SheetTitle` + // wrapper covers it. Promoting the label to `

` restores the + // drawer's semantic outline so screen-reader users can navigate + // by heading. + const html = render(); + expect(html).toMatch( + /]*data-slot="coach-sources-rail-heading"[^>]*>[\s\S]*What I can see[\s\S]*<\/h3>/, + ); + }); + it("lists exactly five rows (BP, weight, pulse, mood, compliance)", () => { const html = render(); const rows = html.match(/data-slot="coach-sources-row"/g) ?? []; diff --git a/src/components/insights/coach-panel/history-rail.tsx b/src/components/insights/coach-panel/history-rail.tsx index c0442440e..96d1fa99a 100644 --- a/src/components/insights/coach-panel/history-rail.tsx +++ b/src/components/insights/coach-panel/history-rail.tsx @@ -69,15 +69,23 @@ export function HistoryRail({ data-slot="coach-history-rail" className={cn("flex h-full min-h-0 flex-col gap-2 p-3", className)} > -
+ {/* v1.4.33 — promote the rail label from a `` to a real + `

` so the drawer carries a semantic outline on desktop + where the rail is mounted inline (no `SheetTitle` wrapper). + The mobile rail-tray still wraps the rail inside its own + `SheetTitle`, so screen-reader users hear the section twice + when the tray is open — that's intentional Radix behaviour + (the rail still has to stand alone on desktop). */} +

+ {t("insights.coach.historyTitle")} +

{/* Window selector — every row below is filtered to the picked window when the next Coach message goes out. 36px touch