From 68ac10552f037ea7beeba3c3fc84f8da8e46856f Mon Sep 17 00:00:00 2001 From: padmarajnidagundi Date: Fri, 29 May 2026 08:35:38 +0300 Subject: [PATCH] fix(html-reporter): add keyboard navigation to TabbedPane component Add WCAG 2.1 compliant keyboard navigation to TabbedPane in the HTML reporter and trace viewer. Previously, the tab components only responded to mouse clicks, preventing keyboard-only and screen reader users from navigating between tabs. - Add Arrow Right/Left to move focus between tabs - Add Home/End to jump to first/last tab - Use roving tabIndex pattern (0 for selected tab, -1 for others) Fixes #41005 --- packages/html-reporter/src/tabbedPane.tsx | 26 ++++++++++++++++++- packages/web/src/components/tabbedPane.tsx | 30 ++++++++++++++++++++-- 2 files changed, 53 insertions(+), 3 deletions(-) diff --git a/packages/html-reporter/src/tabbedPane.tsx b/packages/html-reporter/src/tabbedPane.tsx index a1e533348b873..76dc35c2d3c90 100644 --- a/packages/html-reporter/src/tabbedPane.tsx +++ b/packages/html-reporter/src/tabbedPane.tsx @@ -32,16 +32,40 @@ export const TabbedPane: React.FunctionComponent<{ setSelectedTab: (tab: string) => void }> = ({ tabs, selectedTab, setSelectedTab }) => { const idPrefix = React.useId(); + const tabStripRef = React.useRef(null); + + const handleKeyDown = (e: React.KeyboardEvent) => { + const tabElements = Array.from(tabStripRef.current?.querySelectorAll('[role="tab"]') ?? []) as HTMLElement[]; + const currentIndex = tabElements.findIndex(el => el === document.activeElement); + if (currentIndex === -1) + return; + let nextIndex = currentIndex; + if (e.key === 'ArrowRight') + nextIndex = (currentIndex + 1) % tabElements.length; + else if (e.key === 'ArrowLeft') + nextIndex = (currentIndex - 1 + tabElements.length) % tabElements.length; + else if (e.key === 'Home') + nextIndex = 0; + else if (e.key === 'End') + nextIndex = tabElements.length - 1; + else + return; + e.preventDefault(); + tabElements[nextIndex].focus(); + setSelectedTab(tabs[nextIndex].id); + }; + return
-
{ +
{ tabs.map(tab => (
setSelectedTab(tab.id)} id={`${idPrefix}-${tab.id}`} key={tab.id} role='tab' + tabIndex={selectedTab === tab.id ? 0 : -1} aria-selected={selectedTab === tab.id}>
{tab.title}
diff --git a/packages/web/src/components/tabbedPane.tsx b/packages/web/src/components/tabbedPane.tsx index a031cdaa077ba..e2d53e1ffb63e 100644 --- a/packages/web/src/components/tabbedPane.tsx +++ b/packages/web/src/components/tabbedPane.tsx @@ -38,17 +38,40 @@ export const TabbedPane: React.FunctionComponent<{ mode?: 'default' | 'select', }> = ({ tabs, selectedTab, setSelectedTab, leftToolbar, rightToolbar, dataTestId, mode }) => { const id = React.useId(); + const tabListRef = React.useRef(null); if (!selectedTab) selectedTab = tabs[0].id; if (!mode) mode = 'default'; + + const handleKeyDown = (e: React.KeyboardEvent) => { + const tabElements = Array.from(tabListRef.current?.querySelectorAll('[role="tab"]') ?? []) as HTMLElement[]; + const currentIndex = tabElements.findIndex(el => el === document.activeElement); + if (currentIndex === -1) + return; + let nextIndex = currentIndex; + if (e.key === 'ArrowRight') + nextIndex = (currentIndex + 1) % tabElements.length; + else if (e.key === 'ArrowLeft') + nextIndex = (currentIndex - 1 + tabElements.length) % tabElements.length; + else if (e.key === 'Home') + nextIndex = 0; + else if (e.key === 'End') + nextIndex = tabElements.length - 1; + else + return; + e.preventDefault(); + tabElements[nextIndex].focus(); + setSelectedTab?.(tabs[nextIndex].id); + }; + return
{ leftToolbar &&
{...leftToolbar}
} - {mode === 'default' &&
+ {mode === 'default' &&
{[...tabs.map(tab => ( )), ]}
} @@ -101,11 +125,13 @@ export const TabbedPaneTab: React.FunctionComponent<{ selected?: boolean, onSelect?: (id: string) => void, ariaControls?: string, -}> = ({ id, title, count, errorCount, selected, onSelect, ariaControls }) => { + tabIndex?: number, +}> = ({ id, title, count, errorCount, selected, onSelect, ariaControls, tabIndex }) => { return
onSelect?.(id)} role='tab' title={title} + tabIndex={tabIndex ?? (selected ? 0 : -1)} aria-controls={ariaControls} aria-selected={selected}>
{title}