From 3edc77fc943ae9398161fbde2de0b29cb8e56725 Mon Sep 17 00:00:00 2001 From: Padmaraj Nidagundi Date: Sat, 30 May 2026 03:15:54 +0300 Subject: [PATCH] fix(html-reporter): add keyboard navigation to TabbedPane component (#41042) --- 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}