From 8f5b5d6e545c4b1b9482eb71e5c78a19f221ad84 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Thu, 8 Oct 2026 21:43:37 +0200 Subject: [PATCH] fix(a11y): keep disclosure panels in the tree so aria-controls resolves The encryption column table and the source-priority groups, overrides and device ladder rendered their panel only while open, so a collapsed toggle's aria-controls pointed at nothing. The panels now stay mounted, hidden and empty, while closed. --- .../encryption-legacy-badge.test.tsx | 6 + src/components/admin/encryption-section.tsx | 164 ++++----- .../__tests__/sources-section-in-use.test.tsx | 9 + src/components/settings/sources-section.tsx | 330 +++++++++--------- 4 files changed, 274 insertions(+), 235 deletions(-) diff --git a/src/components/admin/__tests__/encryption-legacy-badge.test.tsx b/src/components/admin/__tests__/encryption-legacy-badge.test.tsx index 07c461003..055ac5f63 100644 --- a/src/components/admin/__tests__/encryption-legacy-badge.test.tsx +++ b/src/components/admin/__tests__/encryption-legacy-badge.test.tsx @@ -131,6 +131,12 @@ describe("encryption coverage — the per-column table", () => { expect(html).not.toContain('data-slot="encryption-column-rows"'); }); + it("keeps the toggle's aria-controls target in the tree while collapsed", () => { + const html = renderCoverage(false); + expect(html).toContain('aria-controls="encryption-columns-panel"'); + expect(html).toMatch(/ ); } diff --git a/src/components/settings/__tests__/sources-section-in-use.test.tsx b/src/components/settings/__tests__/sources-section-in-use.test.tsx index e2b2dbb16..a99f7a2a8 100644 --- a/src/components/settings/__tests__/sources-section-in-use.test.tsx +++ b/src/components/settings/__tests__/sources-section-in-use.test.tsx @@ -45,6 +45,15 @@ describe("Settings → Source priority, sources in use", () => { expect(html).toContain('aria-expanded="false"'); }); + it("names an element with every toggle's aria-controls, open or not", () => { + const html = render(["WITHINGS", "MANUAL"]); + const controls = [...html.matchAll(/aria-controls="([^"]+)"/g)].map( + (m) => m[1]!, + ); + expect(controls.length).toBeGreaterThan(1); + for (const id of controls) expect(html).toContain(`id="${id}"`); + }); + it("says so when no source is in use at all", () => { expect(render([])).toContain('data-testid="sources-none-in-use"'); }); diff --git a/src/components/settings/sources-section.tsx b/src/components/settings/sources-section.tsx index 57df0ca70..69dd2b3ae 100644 --- a/src/components/settings/sources-section.tsx +++ b/src/components/settings/sources-section.tsx @@ -420,55 +420,66 @@ export function SourcesSection() { ) : null} - {open ? ( - ); })} @@ -497,46 +508,49 @@ export function SourcesSection() { count: overriddenMetrics.length, })} - {showPerMetric && ( -
-

- {t("settings.sections.sources.perMetricHelp")} -

- {overriddenMetrics.length === 0 ? ( - - ) : ( -
    - {overriddenMetrics.map((metric) => ( -
  • - - {t(METRIC_LABEL_KEYS[metric])} - - -
  • - ))} -
- )} -
- )} + + {t(METRIC_LABEL_KEYS[metric])} + + + + ))} + + )} + + ) : null} + )} @@ -563,78 +577,82 @@ export function SourcesSection() { count: deviceTypeOverrideCount, })} - {showDeviceType && ( -
-

- {t("settings.sections.sources.deviceTypeHelp")} -

-
- - {t("settings.sections.sources.deviceTypeDefault")} - - -
-
- )} +
+ + +
+ + ))} + + + ) : null} + )}