From 28296294ac46a6f29cc8c536d0d089a2a04d05bd Mon Sep 17 00:00:00 2001 From: dezzy-music Date: Tue, 13 Oct 2026 10:20:13 -0600 Subject: [PATCH] fix(a11y): omit aria-controls on inactive tabs to prevent broken tabpanel ID references (#710) --- src/components/ui/Tabs.test.tsx | 22 ++++++++++++++++++++++ src/components/ui/Tabs.tsx | 2 +- 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/src/components/ui/Tabs.test.tsx b/src/components/ui/Tabs.test.tsx index aca11354..f4e28e2d 100644 --- a/src/components/ui/Tabs.test.tsx +++ b/src/components/ui/Tabs.test.tsx @@ -142,3 +142,25 @@ describe("Tabs — keyboard navigation (#24)", () => { expect(screen.getByRole("tab", { name: "First" })).toHaveFocus(); }); }); + +describe("Tabs — aria-controls (#710)", () => { + it("sets aria-controls on the active tab and omits it on inactive tabs", () => { + const { rerender } = render( + {}} />, + ); + + const activeTab = screen.getByRole("tab", { name: "Active" }); + const completedTab = screen.getByRole("tab", { name: "Completed" }); + const archivedTab = screen.getByRole("tab", { name: "Archived" }); + + expect(activeTab).toHaveAttribute("aria-controls", "tabpanel-active"); + expect(completedTab).not.toHaveAttribute("aria-controls"); + expect(archivedTab).not.toHaveAttribute("aria-controls"); + + rerender( {}} />); + + expect(activeTab).not.toHaveAttribute("aria-controls"); + expect(completedTab).toHaveAttribute("aria-controls", "tabpanel-completed"); + expect(archivedTab).not.toHaveAttribute("aria-controls"); + }); +}); diff --git a/src/components/ui/Tabs.tsx b/src/components/ui/Tabs.tsx index a5236b01..0375dd02 100644 --- a/src/components/ui/Tabs.tsx +++ b/src/components/ui/Tabs.tsx @@ -79,7 +79,7 @@ export function Tabs({ role="tab" id={`tab-${tab.key}`} aria-selected={isActive} - aria-controls={`tabpanel-${tab.key}`} + aria-controls={isActive ? `tabpanel-${tab.key}` : undefined} tabIndex={isActive ? 0 : -1} onClick={() => onChange(tab.key)} onKeyDown={(e) => handleKeyDown(e, tab.key)}