diff --git a/e2e/coach-layout.spec.ts b/e2e/coach-layout.spec.ts
index ae6bfbaec..75c1da37a 100644
--- a/e2e/coach-layout.spec.ts
+++ b/e2e/coach-layout.spec.ts
@@ -514,10 +514,8 @@ test.describe("Coach page frame", () => {
expect(await stripBox(strip(page))).toEqual(stripOpen);
await expect(expand(page)).toBeFocused();
await expect(expand(page)).toHaveAttribute("aria-expanded", "false");
- await expect(expand(page)).toHaveAttribute(
- "aria-label",
- "Show conversations",
- );
+ // Named by its visible label; the action is the tooltip (WCAG 2.5.3).
+ await expect(expand(page)).toHaveAttribute("title", "Show conversations");
await expect(
panel(page).locator('[data-slot="coach-history-select"]'),
).toHaveCount(0);
diff --git a/e2e/day-view.spec.ts b/e2e/day-view.spec.ts
index 0aae88187..c4e5f5fd4 100644
--- a/e2e/day-view.spec.ts
+++ b/e2e/day-view.spec.ts
@@ -192,7 +192,9 @@ test.describe("the day view", () => {
await expect(expand).toBeFocused();
await expect(expand).toHaveAttribute("aria-expanded", "false");
await expect(expand).toHaveAttribute("aria-controls", "day-docked-panel");
- expect(await expand.getAttribute("aria-label")).toMatch(/^Show /);
+ // Named by its visible date (WCAG 2.5.3); the action is the tooltip.
+ expect(await expand.getAttribute("aria-label")).toBeNull();
+ expect(await expand.getAttribute("title")).toMatch(/^Show /);
expect((await rail.boundingBox())!.width).toBeLessThanOrEqual(40);
// Another page: the edge is still there and brings the same day back,
diff --git a/messages/de.json b/messages/de.json
index 1b8503088..6c5b6ba81 100644
--- a/messages/de.json
+++ b/messages/de.json
@@ -11908,7 +11908,6 @@
},
"chat": {
"title": "Über dieses Dokument chatten",
- "openAria": "Coach über dieses Dokument fragen",
"openButton": "Coach fragen"
},
"readingConsent": {
diff --git a/messages/en.json b/messages/en.json
index 07b2f0e74..26878b719 100644
--- a/messages/en.json
+++ b/messages/en.json
@@ -11960,7 +11960,6 @@
},
"chat": {
"title": "Chat about this document",
- "openAria": "Ask the Coach about this document",
"openButton": "Ask the Coach"
},
"readingConsent": {
diff --git a/messages/es.json b/messages/es.json
index 36448140f..cddc6c79b 100644
--- a/messages/es.json
+++ b/messages/es.json
@@ -1556,12 +1556,12 @@
"weekdays": {
"label": "Días de la semana",
"short": {
- "mo": "Mo",
+ "mo": "Lu",
"tu": "Ma",
"we": "Mi",
"th": "Ju",
- "fr": "Fr",
- "sa": "Sa",
+ "fr": "Vi",
+ "sa": "Sá",
"su": "Do"
},
"long": {
@@ -11908,7 +11908,6 @@
},
"chat": {
"title": "Chatea sobre este documento",
- "openAria": "Pregunta al Coach sobre este documento",
"openButton": "Preguntar al Coach"
},
"readingConsent": {
diff --git a/messages/fr.json b/messages/fr.json
index a8236ec64..c11135c11 100644
--- a/messages/fr.json
+++ b/messages/fr.json
@@ -1556,11 +1556,11 @@
"weekdays": {
"label": "Jours de la semaine",
"short": {
- "mo": "Mo",
+ "mo": "Lu",
"tu": "Ma",
"we": "Me",
"th": "Je",
- "fr": "Fr",
+ "fr": "Ve",
"sa": "Sa",
"su": "Di"
},
@@ -11881,7 +11881,6 @@
},
"chat": {
"title": "Discuter de ce document",
- "openAria": "Interroger le Coach sur ce document",
"openButton": "Demander au Coach"
},
"readingConsent": {
diff --git a/messages/it.json b/messages/it.json
index 99ee64dd2..c6ad4adf8 100644
--- a/messages/it.json
+++ b/messages/it.json
@@ -1556,11 +1556,11 @@
"weekdays": {
"label": "Giorni della settimana",
"short": {
- "mo": "Mo",
+ "mo": "Lu",
"tu": "Ma",
"we": "Me",
"th": "Gi",
- "fr": "Fr",
+ "fr": "Ve",
"sa": "Sa",
"su": "Do"
},
@@ -11908,7 +11908,6 @@
},
"chat": {
"title": "Chatta su questo documento",
- "openAria": "Chiedi al Coach su questo documento",
"openButton": "Chiedi al Coach"
},
"readingConsent": {
diff --git a/messages/ko.json b/messages/ko.json
index 286e9d099..2485ebc17 100644
--- a/messages/ko.json
+++ b/messages/ko.json
@@ -11841,7 +11841,6 @@
},
"chat": {
"title": "이 문서에 대해 대화",
- "openAria": "이 문서에 대해 코치에게 묻기",
"openButton": "코치에게 묻기"
},
"readingConsent": {
diff --git a/messages/nl.json b/messages/nl.json
index 4a6ae7395..08d578b27 100644
--- a/messages/nl.json
+++ b/messages/nl.json
@@ -11841,7 +11841,6 @@
},
"chat": {
"title": "Chatten over dit document",
- "openAria": "De Coach een vraag stellen over dit document",
"openButton": "Vraag het de Coach"
},
"readingConsent": {
diff --git a/messages/pl.json b/messages/pl.json
index e82a5da9e..3dee081df 100644
--- a/messages/pl.json
+++ b/messages/pl.json
@@ -1556,13 +1556,13 @@
"weekdays": {
"label": "Dni tygodnia",
"short": {
- "mo": "Mo",
- "tu": "Wt",
- "we": "Śr",
- "th": "Cz",
- "fr": "Fr",
- "sa": "Sa",
- "su": "Nd"
+ "mo": "Pon",
+ "tu": "Wto",
+ "we": "Śro",
+ "th": "Czw",
+ "fr": "Pią",
+ "sa": "Sob",
+ "su": "Nie"
},
"long": {
"mo": "Poniedziałek",
@@ -11908,7 +11908,6 @@
},
"chat": {
"title": "Porozmawiaj o tym dokumencie",
- "openAria": "Zapytaj Coacha o ten dokument",
"openButton": "Zapytaj Coacha"
},
"readingConsent": {
diff --git a/messages/pt-BR.json b/messages/pt-BR.json
index 00030a2e0..e3e7ddb9a 100644
--- a/messages/pt-BR.json
+++ b/messages/pt-BR.json
@@ -11841,7 +11841,6 @@
},
"chat": {
"title": "Conversar sobre este documento",
- "openAria": "Perguntar ao Coach sobre este documento",
"openButton": "Perguntar ao Coach"
},
"readingConsent": {
diff --git a/src/__tests__/baselines/source-function-lines.json b/src/__tests__/baselines/source-function-lines.json
index 6ee015b92..fe8ee6eb1 100644
--- a/src/__tests__/baselines/source-function-lines.json
+++ b/src/__tests__/baselines/source-function-lines.json
@@ -91,7 +91,7 @@
"components/admin/host-metrics-chart.tsx :: HostMetricsChart": 232,
"components/admin/invite-tokens-section.tsx :: InviteTokensSection": 525,
"components/admin/key-backup-card.tsx :: KeyBackupCard": 238,
- "components/admin/login-overview-section.tsx :: LoginOverviewSection": 520,
+ "components/admin/login-overview-section.tsx :: LoginOverviewSection": 522,
"components/admin/pulse-day-mean-card.tsx :: PulseDayMeanCard": 305,
"components/admin/reminders-section.tsx :: RemindersSection": 309,
"components/admin/user-management-section.tsx :: UserManagementSection": 634,
@@ -111,11 +111,11 @@
"components/custom-metrics/custom-metric-form.tsx :: CustomMetricForm": 214,
"components/custom-metrics/custom-metric-history-list.tsx :: CustomMetricHistoryList": 302,
"components/cycle/bbt-chart.tsx :: BbtChart": 244,
- "components/cycle/cycle-calendar.tsx :: CycleCalendar": 303,
+ "components/cycle/cycle-calendar.tsx :: CycleCalendar": 304,
"components/cycle/cycle-settings.tsx :: CycleSettings": 228,
"components/cycle/cycle-view.tsx :: CycleView": 408,
"components/cycle/log-day-sheet.tsx :: LogDaySheet": 753,
- "components/daily/today-hero.tsx :: TodayHero": 490,
+ "components/daily/today-hero.tsx :: TodayHero": 492,
"components/dashboard/dashboard-header.tsx :: DashboardHeader": 215,
"components/dashboard/medication-intake-quick-add.tsx :: MedicationIntakeQuickAdd": 384,
"components/day/day-layer.tsx :: DayLayer": 374,
@@ -194,7 +194,7 @@
"components/medications/glp1-medication-card.tsx :: Glp1MedicationCard": 405,
"components/medications/injection-site-picker.tsx :: InjectionSitePicker": 287,
"components/medications/intake-history-editable.tsx :: IntakeHistoryEditable": 224,
- "components/medications/intake-history-list-v2.tsx :: IntakeHistoryListV2": 435,
+ "components/medications/intake-history-list-v2.tsx :: IntakeHistoryListV2": 437,
"components/medications/intake-import-dialog.tsx :: IntakeImportDialog": 208,
"components/medications/log-intake-dialog.tsx :: LogIntakeDialog": 267,
"components/medications/medication-card.tsx :: MedicationCard": 413,
diff --git a/src/components/admin/login-overview-section.tsx b/src/components/admin/login-overview-section.tsx
index a20d6578d..01cded22c 100644
--- a/src/components/admin/login-overview-section.tsx
+++ b/src/components/admin/login-overview-section.tsx
@@ -499,10 +499,12 @@ export function LoginOverviewSection() {
copyIp(entry.id, entry.ipAddress as string)
}
title={entry.ipAddress}
- aria-label={t("admin.copyIp")}
data-slot="login-overview-ip"
className="hover:text-foreground focus-visible:ring-ring inline-flex max-w-[10rem] items-center gap-1 rounded font-mono focus-visible:ring-2 focus-visible:outline-none"
>
+
+ {t("admin.copyIp")}
+
{entry.ipAddress}
diff --git a/src/components/admin/user-management-section.tsx b/src/components/admin/user-management-section.tsx
index 6f8f23a34..5ae5c41fb 100644
--- a/src/components/admin/user-management-section.tsx
+++ b/src/components/admin/user-management-section.tsx
@@ -202,14 +202,10 @@ export function UserManagementSection() {
? t("admin.demoteToUser")
: t("admin.promoteToAdmin")
}
- aria-label={
- u.role === "ADMIN"
- ? t("admin.demoteToUser")
- : t("admin.promoteToAdmin")
- }
>
{u.role === "ADMIN" ? t("admin.toUser") : t("admin.toAdmin")}
+ , {u.username}
{/* The three icon tools travel as one group: on a phone the row wraps
between the labelled toggles and the tools, never leaving the
diff --git a/src/components/cycle/__tests__/cycle-calendar.test.tsx b/src/components/cycle/__tests__/cycle-calendar.test.tsx
index 5b6753993..8cec062a3 100644
--- a/src/components/cycle/__tests__/cycle-calendar.test.tsx
+++ b/src/components/cycle/__tests__/cycle-calendar.test.tsx
@@ -40,10 +40,10 @@ function dayBase(date: string): CalendarDay {
};
}
-/** The opening tag of one day's cell, by its date in the aria-label. */
+/** The opening tag of one day's cell, by its `data-date`. */
function cellTag(html: string, date: string): string {
const match = html.match(
- new RegExp(`<(?:button|div)[^>]*aria-label="${date}[^"]*"[^>]*>`),
+ new RegExp(`<(?:button|div)[^>]*data-date="${date}"[^>]*>`),
);
if (!match) throw new Error(`no cell for ${date}`);
return match[0];
diff --git a/src/components/cycle/cycle-calendar.tsx b/src/components/cycle/cycle-calendar.tsx
index 27b21d011..72b431bf3 100644
--- a/src/components/cycle/cycle-calendar.tsx
+++ b/src/components/cycle/cycle-calendar.tsx
@@ -67,6 +67,32 @@ function mondayIndex(d: Date): number {
type Translate = ReturnType["t"];
+const CELL_DATE_FORMATS = new Map();
+
+/**
+ * A day cell's accessible name: the long date, then its markers. The long
+ * date contains the day number the cell shows (WCAG 2.5.3, label in name);
+ * the ISO key it used to read out ("2026-10-01") did not.
+ */
+function cellLabel(
+ date: string,
+ markers: readonly string[],
+ locale: Locale,
+): string {
+ let format = CELL_DATE_FORMATS.get(locale);
+ if (!format) {
+ format = new Intl.DateTimeFormat(resolveIntlLocale(locale), {
+ day: "numeric",
+ month: "long",
+ year: "numeric",
+ timeZone: "UTC",
+ });
+ CELL_DATE_FORMATS.set(locale, format);
+ }
+ const day = format.format(new Date(`${date}T12:00:00Z`));
+ return markers.length ? `${day}, ${markers.join(", ")}` : day;
+}
+
/** `LABEL (VALUE)`, or the bare label when there is no value to name. */
function named(t: Translate, labelKey: string, valueKey?: string): string {
return valueKey ? `${t(labelKey)} (${t(valueKey)})` : t(labelKey);
@@ -299,7 +325,7 @@ export function CycleCalendar({
const others = info ? otherEntryLabels(info, t) : [];
markers.push(...others);
- const aria = `${date}${markers.length ? `, ${markers.join(", ")}` : ""}`;
+ const aria = cellLabel(date, markers, locale);
const flowLevel =
info?.isPeriodLogged && info.flow && info.flow !== "NONE"
? info.flow
@@ -410,6 +436,7 @@ export function CycleCalendar({
role: "gridcell" as const,
"aria-label": aria,
"aria-current": isToday ? ("date" as const) : undefined,
+ "data-date": date,
"data-flow-level": flowLevel,
"data-fertile": info?.isFertileWindow ? "true" : undefined,
"data-predicted":
diff --git a/src/components/daily/today-hero.tsx b/src/components/daily/today-hero.tsx
index 9afeae06d..8fbe12224 100644
--- a/src/components/daily/today-hero.tsx
+++ b/src/components/daily/today-hero.tsx
@@ -333,11 +333,13 @@ export function TodayHero({
poster. */}
+
+ {t("daily.today.ringLink", {
+ metric: t("daily.today.scoreLabel"),
+ })}
+
-
+
{t("documents.chat.openButton")}
diff --git a/src/components/insights/__tests__/health-score-coverage-axis-guard.test.tsx b/src/components/insights/__tests__/health-score-coverage-axis-guard.test.tsx
index cf151288a..8a6553db3 100644
--- a/src/components/insights/__tests__/health-score-coverage-axis-guard.test.tsx
+++ b/src/components/insights/__tests__/health-score-coverage-axis-guard.test.tsx
@@ -288,7 +288,9 @@ describe("the axis override reaches the markup", () => {
expect(html).toContain("Covers the three areas the score needs");
expect(html).not.toContain(">3/3<");
- expect(html).toMatch(/aria-label="Covers the three areas the score needs/);
+ expect(html).toMatch(
+ /class="sr-only">Covers the three areas the score needs/,
+ );
});
it("keeps the fraction for every caller that does not override it", () => {
diff --git a/src/components/insights/correlation-card.tsx b/src/components/insights/correlation-card.tsx
index 1e96315e6..d8d254c3a 100644
--- a/src/components/insights/correlation-card.tsx
+++ b/src/components/insights/correlation-card.tsx
@@ -189,10 +189,12 @@ export function CorrelationCard({ result }: CorrelationCardProps) {
data-slot="correlation-card-cta"
className="gap-1.5"
title={t("insights.correlationRow.experimentTooltip")}
- aria-label={t("insights.correlationRow.experimentTooltip")}
>
{t("insights.correlationRow.experimentCta")}
+
+ , {t("insights.correlationRow.experimentTooltip")}
+
{/* v1.21.0 — the live Coach hand-off sits beside the
roadmap CTA: it scopes to both sources in the pair. */}
diff --git a/src/components/insights/derived/__tests__/coverage-meter.test.tsx b/src/components/insights/derived/__tests__/coverage-meter.test.tsx
index b73ca2a1b..82356ba51 100644
--- a/src/components/insights/derived/__tests__/coverage-meter.test.tsx
+++ b/src/components/insights/derived/__tests__/coverage-meter.test.tsx
@@ -79,7 +79,10 @@ describe("", () => {
it("carries an accessible summary label", () => {
const html = render();
- expect(html).toContain("aria-label");
+ // The visible ratio plus the sentence, never an aria-label that drops
+ // the ratio (WCAG 2.5.3).
+ expect(html).not.toContain("aria-label");
+ expect(html).toContain('class="sr-only">3 of 5 inputs (35%)');
expect(html).toContain("3");
expect(html).toContain("5");
});
diff --git a/src/components/insights/derived/coverage-meter.tsx b/src/components/insights/derived/coverage-meter.tsx
index ddcbe5294..46730ce0c 100644
--- a/src/components/insights/derived/coverage-meter.tsx
+++ b/src/components/insights/derived/coverage-meter.tsx
@@ -138,7 +138,6 @@ export function CoverageMeter({
data-present={present}
data-required={required}
data-percent={percent}
- aria-label={summaryLabel}
aria-describedby={tooltipId}
className={cn(
// 44px hit target via padding while the dots stay optically
@@ -183,6 +182,9 @@ export function CoverageMeter({
required,
})}
+ {/* The visible ratio names the button; the sentence adds what it
+ counts (WCAG 2.5.3: the name contains what is shown). */}
+ {summaryLabel} {
expect(html).toContain('data-slot="day-strip-toggle"');
expect(html).toContain('aria-expanded="false"');
expect(html).toContain('aria-controls="day-docked-panel"');
- expect(html).toContain('aria-label="Show Thursday, 4 June 2026"');
+ // Named by its visible label (WCAG 2.5.3); the action is the tooltip.
+ expect(html).not.toContain("aria-label=");
+ expect(html).toContain('title="Show Thursday, 4 June 2026"');
// The label reads top to bottom.
expect(html).toContain("[writing-mode:vertical-rl]");
expect(html).toContain("Thu, 4 Jun 2026");
@@ -53,7 +55,7 @@ describe("DockStrip", () => {
const html = strip(true);
expect(html).toContain('data-state="open"');
expect(html).toContain('aria-expanded="true"');
- expect(html).toContain('aria-label="Hide day"');
+ expect(html).toContain('title="Hide day"');
});
it("keeps the same shape open and closed, the full height of the window", () => {
diff --git a/src/components/layout/__tests__/sidebar-nav.test.tsx b/src/components/layout/__tests__/sidebar-nav.test.tsx
index 8195a4f9a..88809bc95 100644
--- a/src/components/layout/__tests__/sidebar-nav.test.tsx
+++ b/src/components/layout/__tests__/sidebar-nav.test.tsx
@@ -332,13 +332,17 @@ describe(" inside somebody else's record (#939)", () => {
it("keeps the account settings door in one's own record", () => {
const html = renderInRecord(null);
expect(settingsLinks(html)).toEqual(["/settings/account"]);
- expect(html).toContain(`aria-label="${accountSettingsLabel}"`);
+ expect(html).toContain(
+ `${accountSettingsLabel}`,
+ );
});
it("offers Settings inside a managed profile and keeps the identity block a plain block", () => {
const html = renderInRecord({ recordKind: "managed", level: "manage" });
expect(settingsLinks(html)).toEqual(["/settings/account"]);
- expect(html).not.toContain(`aria-label="${accountSettingsLabel}"`);
+ expect(html).not.toContain(
+ `${accountSettingsLabel}`,
+ );
expect(html).not.toContain('href="/notifications"');
});
diff --git a/src/components/layout/shell-dock.tsx b/src/components/layout/shell-dock.tsx
index efd690eec..fd4d68d69 100644
--- a/src/components/layout/shell-dock.tsx
+++ b/src/components/layout/shell-dock.tsx
@@ -80,7 +80,7 @@ export interface DockStripProps {
expanded: boolean;
/** The strip's visible, vertical label. */
label: string;
- /** The button's accessible name (what a click does). */
+ /** What a click does, as the button's tooltip. */
actionLabel: string;
icon: LucideIcon;
onToggle: () => void;
@@ -124,7 +124,8 @@ export function DockStrip({
data-slot={`${slot}-toggle`}
aria-expanded={expanded}
aria-controls={controls}
- aria-label={actionLabel}
+ // Named by its visible label (WCAG 2.5.3); `aria-expanded` carries
+ // open or closed, and the tooltip spells out the action.
title={actionLabel}
onClick={onToggle}
onPointerEnter={onPreload}
diff --git a/src/components/layout/sidebar-nav.tsx b/src/components/layout/sidebar-nav.tsx
index 2d3ee95da..2440845ec 100644
--- a/src/components/layout/sidebar-nav.tsx
+++ b/src/components/layout/sidebar-nav.tsx
@@ -121,11 +121,10 @@ function FooterIdentity({
return
{children}
;
}
return (
-
+
+ {/* The purpose leads the accessible name; the visible name and email
+ follow it, so the link is named by what it shows (WCAG 2.5.3). */}
+ {label}
{children}
);
@@ -272,11 +271,9 @@ function SidebarUserSection({ collapsed }: { collapsed: boolean }) {
>
-
-
+
+ {t("nav.userMenu")}
+
{avatarUrl && (
)}
@@ -302,7 +299,7 @@ function SidebarUserSection({ collapsed }: { collapsed: boolean }) {
sharedRecord={sharedRecord}
label={t("nav.accountSettings")}
>
-
+
{avatarUrl && }
{getInitials(user.username)}
diff --git a/src/components/layout/top-bar.tsx b/src/components/layout/top-bar.tsx
index 50c779eab..23988f100 100644
--- a/src/components/layout/top-bar.tsx
+++ b/src/components/layout/top-bar.tsx
@@ -132,7 +132,6 @@ export function TopBar() {
) : user ? (
-
+ {t("nav.userMenu")}
+
{avatarUrl && (
)}
diff --git a/src/components/measurements/__tests__/measurement-list-filter-select.test.tsx b/src/components/measurements/__tests__/measurement-list-filter-select.test.tsx
index b3fefea4f..46ec3b312 100644
--- a/src/components/measurements/__tests__/measurement-list-filter-select.test.tsx
+++ b/src/components/measurements/__tests__/measurement-list-filter-select.test.tsx
@@ -83,12 +83,12 @@ describe("MeasurementList — filter bar + multi-select chrome", () => {
it("renders the filter rail with type, source, date-range and value pills", () => {
const html = render("en");
expect(html).toContain('data-slot="filter-bar"');
- expect(html).toContain('aria-label="Type"');
- expect(html).toContain('aria-label="Source"');
- expect(html).toContain('aria-label="Date range"');
+ expect(html).toContain(">Type");
+ expect(html).toContain(">Source");
+ expect(html).toContain(">Date range");
// v1.18.5 — value-range pill rides the rail for the raw per-sample
// list (the default `ALL` type is not a day-grouped/sleep type).
- expect(html).toContain('aria-label="Value"');
+ expect(html).toContain(">Value");
const pills = html.match(/data-slot="filter-bar-pill"/g);
expect(pills?.length).toBe(4);
});
@@ -105,8 +105,8 @@ describe("MeasurementList — filter bar + multi-select chrome", () => {
it("localises the new chrome in German", () => {
const html = render("de");
- expect(html).toContain('aria-label="Quelle"');
- expect(html).toContain('aria-label="Zeitraum"');
+ expect(html).toContain(">Quelle");
+ expect(html).toContain(">Zeitraum");
expect(html).toContain('aria-label="Alle auf dieser Seite auswählen"');
});
});
diff --git a/src/components/measurements/__tests__/measurement-list-locked-type.test.tsx b/src/components/measurements/__tests__/measurement-list-locked-type.test.tsx
index 098ca83f9..237a69993 100644
--- a/src/components/measurements/__tests__/measurement-list-locked-type.test.tsx
+++ b/src/components/measurements/__tests__/measurement-list-locked-type.test.tsx
@@ -69,7 +69,7 @@ describe("MeasurementList — lockedType", () => {
// rail (type pill included); the locked one suppresses the whole
// rail (the metric is fixed by the route) and keeps only the count.
expect(unlocked).toContain('data-slot="filter-bar"');
- expect(unlocked).toContain('aria-label="Type"');
+ expect(unlocked).toContain(">Type");
expect(locked).not.toContain('data-slot="filter-bar"');
expect(locked).not.toContain('role="combobox"');
});
diff --git a/src/components/medications/__tests__/medication-card-header.test.tsx b/src/components/medications/__tests__/medication-card-header.test.tsx
index d74a8c3a8..892c92f4b 100644
--- a/src/components/medications/__tests__/medication-card-header.test.tsx
+++ b/src/components/medications/__tests__/medication-card-header.test.tsx
@@ -121,7 +121,12 @@ describe("", () => {
);
expect(html).toContain('data-slot="medication-card-header-link"');
expect(html).toContain('href="/medications/med-1"');
- expect(html).toContain('aria-label="Open medication detail page"');
+ // The link is named by what it shows, with the purpose added for
+ // screen readers (WCAG 2.5.3): no aria-label overriding the text.
+ expect(html).toContain(
+ 'Open medication detail page',
+ );
+ expect(html).not.toContain('aria-label="Open medication detail page"');
// The name + dose still ride inside the link region.
expect(html).toContain("Ramipril");
expect(html).toContain("5 mg");
diff --git a/src/components/medications/intake-history-list-v2.tsx b/src/components/medications/intake-history-list-v2.tsx
index c89c77180..a3fd99f3a 100644
--- a/src/components/medications/intake-history-list-v2.tsx
+++ b/src/components/medications/intake-history-list-v2.tsx
@@ -317,9 +317,11 @@ export function IntakeHistoryListV2({
type="button"
onClick={() => toggleSort("takenAt")}
className="focus-visible:ring-ring inline-flex min-h-11 items-center rounded-md font-medium hover:underline focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none sm:min-h-9"
- aria-label={t("medications.intakeHistorySortByTaken")}
>
{t("medications.intakeHistoryColTaken")}
+
+ , {t("medications.intakeHistorySortByTaken")}
+
{sortIndicator("takenAt")}
@@ -328,11 +330,11 @@ export function IntakeHistoryListV2({
type="button"
onClick={() => toggleSort("scheduledFor")}
className="focus-visible:ring-ring inline-flex min-h-11 items-center rounded-md font-medium hover:underline focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none sm:min-h-9"
- aria-label={t(
- "medications.intakeHistorySortByScheduled",
- )}
>
{t("medications.intakeHistoryColScheduled")}
+
+ , {t("medications.intakeHistorySortByScheduled")}
+
{sortIndicator("scheduledFor")}
diff --git a/src/components/medications/medication-card-header.tsx b/src/components/medications/medication-card-header.tsx
index b0334a120..60872c702 100644
--- a/src/components/medications/medication-card-header.tsx
+++ b/src/components/medications/medication-card-header.tsx
@@ -131,11 +131,11 @@ export function MedicationCardHeader({
{href ? (
{body}
+ {linkLabel ? {linkLabel} : null}
) : (
{body}
diff --git a/src/components/medications/medication-table.tsx b/src/components/medications/medication-table.tsx
index 5795b6d3e..40868142c 100644
--- a/src/components/medications/medication-table.tsx
+++ b/src/components/medications/medication-table.tsx
@@ -727,7 +727,6 @@ function MedicationTableRowItem({
@@ -737,6 +736,7 @@ function MedicationTableRowItem({
{formatDose(medication.dose, t)}
{!medication.active && <> · {t("common.inactive")}>}
+ {t("medications.openDetailPage")}{statusCell}
diff --git a/src/components/mental-health/instrument-card.tsx b/src/components/mental-health/instrument-card.tsx
index d56e313f1..61f66c6c9 100644
--- a/src/components/mental-health/instrument-card.tsx
+++ b/src/components/mental-health/instrument-card.tsx
@@ -134,7 +134,6 @@ export function InstrumentCard({
type="button"
onClick={onOpenDetail}
data-slot="instrument-card-open"
- aria-label={`${title}: ${t("mentalHealth.openDetail")}`}
className="focus-visible:ring-ring block w-full rounded-t-xl text-left focus-visible:ring-2 focus-visible:outline-none"
>
+ {t("mentalHealth.openDetail")}
{/* Label/value block in the Vorsorge card's next-last grammar. With
diff --git a/src/components/mood/__tests__/mood-list-filter-select.test.tsx b/src/components/mood/__tests__/mood-list-filter-select.test.tsx
index 51ea0a8f0..1239ffb3c 100644
--- a/src/components/mood/__tests__/mood-list-filter-select.test.tsx
+++ b/src/components/mood/__tests__/mood-list-filter-select.test.tsx
@@ -78,9 +78,9 @@ describe("MoodList — filter bar + multi-select chrome", () => {
it("renders the filter rail with mood, source and date-range pills", () => {
const html = render("en");
expect(html).toContain('data-slot="filter-bar"');
- expect(html).toContain('aria-label="Mood"');
- expect(html).toContain('aria-label="Source"');
- expect(html).toContain('aria-label="Date range"');
+ expect(html).toContain(">Mood");
+ expect(html).toContain(">Source");
+ expect(html).toContain(">Date range");
const pills = html.match(/data-slot="filter-bar-pill"/g);
expect(pills?.length).toBe(3);
});
diff --git a/src/components/ui/filter-bar.tsx b/src/components/ui/filter-bar.tsx
index 7e4dec28f..3291d1c7b 100644
--- a/src/components/ui/filter-bar.tsx
+++ b/src/components/ui/filter-bar.tsx
@@ -44,8 +44,12 @@ import { cn } from "@/lib/utils";
* - Purely presentational: filter state, query keys and fetching stay
* in the owning page/component — this file only renders the controls.
*
- * A11y: every pill keeps an explicit `aria-label`; the per-pill clear
- * button carries `dataList.clearFilter` with the dimension name; touch
+ * A11y: every pill is named by what it shows ("Label" or "Label: value"),
+ * so the accessible name always contains the visible text (WCAG 2.5.3); an
+ * `aria-label` of the bare dimension dropped the value once a filter was
+ * set. The select pill spells that name out (a combobox takes none from
+ * its content), the popover pills take it from their content. The per-pill
+ * clear button carries `dataList.clearFilter` with the dimension name; touch
* targets follow the dense-surface idiom (`min-h-11 sm:min-h-9`,
* ui-guidelines §5).
*/
@@ -159,7 +163,9 @@ export function FilterBarSelect({