From 85b20067610666d4457c10c666ca346cf552379c Mon Sep 17 00:00:00 2001 From: smartcalsystems-sketch Date: Wed, 14 Oct 2026 00:19:54 +0530 Subject: [PATCH] fix: only set aria-controls on the active tab (closes #710) Every tab set aria-controls={`tabpanel-${tab.key}`}, but consumers like ContributorDashboardClient render only the active panel (a single id={`tabpanel-${tab}`}). Every inactive tab's aria-controls therefore pointed at an id that doesn't exist in the DOM -- an ARIA reference error flagged by axe and confusing for assistive tech following the relationship. Omit aria-controls entirely on inactive tabs rather than rendering every panel and hiding the inactive ones, since no consumer of this component currently renders more than the active panel. --- src/components/ui/Tabs.test.tsx | 31 +++++++++++++++++++++++++++++++ src/components/ui/Tabs.tsx | 8 +++++++- 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/src/components/ui/Tabs.test.tsx b/src/components/ui/Tabs.test.tsx index aca11354..b90a92eb 100644 --- a/src/components/ui/Tabs.test.tsx +++ b/src/components/ui/Tabs.test.tsx @@ -81,6 +81,37 @@ describe("Tabs — count badge", () => { }); }); +describe("Tabs — aria-controls only on the active tab (#710)", () => { + it("sets aria-controls on the active tab", () => { + render( {}} />); + expect(screen.getByRole("tab", { name: "Active" })).toHaveAttribute( + "aria-controls", + "tabpanel-active", + ); + }); + + it("omits aria-controls on inactive tabs, since their tabpanel isn't in the DOM", () => { + render( {}} />); + expect(screen.getByRole("tab", { name: "Completed" })).not.toHaveAttribute( + "aria-controls", + ); + expect(screen.getByRole("tab", { name: "Archived" })).not.toHaveAttribute( + "aria-controls", + ); + }); + + it("moves aria-controls to whichever tab becomes active", () => { + render( {}} />); + expect(screen.getByRole("tab", { name: "Completed" })).toHaveAttribute( + "aria-controls", + "tabpanel-completed", + ); + expect(screen.getByRole("tab", { name: "Active" })).not.toHaveAttribute( + "aria-controls", + ); + }); +}); + describe("Tabs — keyboard navigation (#24)", () => { const threeTabs = [ { key: "first", label: "First" }, diff --git a/src/components/ui/Tabs.tsx b/src/components/ui/Tabs.tsx index a5236b01..ac0a972d 100644 --- a/src/components/ui/Tabs.tsx +++ b/src/components/ui/Tabs.tsx @@ -79,7 +79,13 @@ export function Tabs({ role="tab" id={`tab-${tab.key}`} aria-selected={isActive} - aria-controls={`tabpanel-${tab.key}`} + // Only the active tab's panel actually exists in the DOM (a + // consumer like ContributorDashboardClient renders just the one + // tabpanel matching the current tab). aria-controls on an + // inactive tab pointed at a missing id -- an ARIA reference + // error flagged by axe and confusing for assistive tech + // following the relationship (#710). + aria-controls={isActive ? `tabpanel-${tab.key}` : undefined} tabIndex={isActive ? 0 : -1} onClick={() => onChange(tab.key)} onKeyDown={(e) => handleKeyDown(e, tab.key)}