diff --git a/packages/html-reporter/src/tabbedPane.tsx b/packages/html-reporter/src/tabbedPane.tsx index a2e8fab51cab2..76dc35c2d3c90 100644 --- a/packages/html-reporter/src/tabbedPane.tsx +++ b/packages/html-reporter/src/tabbedPane.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import { clsx, handleTabListKeyDown } from '@web/uiUtils'; +import { clsx } from '@web/uiUtils'; import './colors.css'; import './tabbedPane.css'; import * as React from 'react'; @@ -35,9 +35,24 @@ export const TabbedPane: React.FunctionComponent<{ const tabStripRef = React.useRef(null); const handleKeyDown = (e: React.KeyboardEvent) => { - const nextIndex = handleTabListKeyDown(e, tabStripRef.current); - if (nextIndex !== -1) - setSelectedTab(tabs[nextIndex].id); + 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
diff --git a/packages/trace-viewer/src/ui/networkFilters.tsx b/packages/trace-viewer/src/ui/networkFilters.tsx index df6fcbca58aeb..df0f0b9e7307a 100644 --- a/packages/trace-viewer/src/ui/networkFilters.tsx +++ b/packages/trace-viewer/src/ui/networkFilters.tsx @@ -14,8 +14,6 @@ * limitations under the License. */ -import * as React from 'react'; -import { handleTabListKeyDown } from '@web/uiUtils'; import './networkFilters.css'; const resourceTypes = ['Fetch', 'HTML', 'JS', 'CSS', 'Font', 'Image', 'WS'] as const; @@ -32,14 +30,6 @@ export const NetworkFilters = ({ filterState, onFilterStateChange }: { filterState: FilterState, onFilterStateChange: (filterState: FilterState) => void, }) => { - const tabListRef = React.useRef(null); - - const handleKeyDown = (e: React.KeyboardEvent) => { - handleTabListKeyDown(e, tabListRef.current); - }; - - const isAllSelected = filterState.resourceTypes.size === 0; - return (
onFilterStateChange({ ...filterState, searchValue: e.target.value })} /> -
+
onFilterStateChange({ ...filterState, resourceTypes: new Set() })} - className={`network-filters-resource-type ${isAllSelected ? 'selected' : ''}`} - role='tab' - tabIndex={isAllSelected ? 0 : -1} - aria-selected={isAllSelected} + className={`network-filters-resource-type ${filterState.resourceTypes.size === 0 ? 'selected' : ''}`} > All
@@ -77,7 +64,6 @@ export const NetworkFilters = ({ filterState, onFilterStateChange }: { }} className={`network-filters-resource-type ${filterState.resourceTypes.has(resourceType) ? 'selected' : ''}`} role='tab' - tabIndex={filterState.resourceTypes.has(resourceType) ? 0 : -1} aria-selected={filterState.resourceTypes.has(resourceType)} > {resourceType} diff --git a/packages/web/src/components/tabbedPane.tsx b/packages/web/src/components/tabbedPane.tsx index b0084b4abc6d5..e2d53e1ffb63e 100644 --- a/packages/web/src/components/tabbedPane.tsx +++ b/packages/web/src/components/tabbedPane.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import { clsx, handleTabListKeyDown } from '../uiUtils'; +import { clsx } from '../uiUtils'; import './tabbedPane.css'; import { Toolbar } from './toolbar'; import * as React from 'react'; @@ -45,9 +45,24 @@ export const TabbedPane: React.FunctionComponent<{ mode = 'default'; const handleKeyDown = (e: React.KeyboardEvent) => { - const nextIndex = handleTabListKeyDown(e, tabListRef.current); - if (nextIndex !== -1) - setSelectedTab?.(tabs[nextIndex].id); + 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
diff --git a/packages/web/src/uiUtils.ts b/packages/web/src/uiUtils.ts index bd2c73344e8e8..48b68f91e0a8a 100644 --- a/packages/web/src/uiUtils.ts +++ b/packages/web/src/uiUtils.ts @@ -210,27 +210,6 @@ export function scrollIntoViewIfNeeded(element: Element | undefined) { element?.scrollIntoView(); } -export function handleTabListKeyDown(e: React.KeyboardEvent, tabListElement: HTMLElement | null): number { - const tabElements = Array.from(tabListElement?.querySelectorAll('[role="tab"]') ?? []) as HTMLElement[]; - const currentIndex = tabElements.findIndex(el => el === document.activeElement); - if (currentIndex === -1) - return -1; - 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 -1; - e.preventDefault(); - tabElements[nextIndex].focus(); - return nextIndex; -} - const kControlCodesRe = '\\u0000-\\u0020\\u007f-\\u009f'; export const kWebLinkRe = new RegExp('(?:[a-zA-Z][a-zA-Z0-9+.-]{2,}:\\/\\/|www\\.)[^\\s' + kControlCodesRe + '"]{2,}[^\\s' + kControlCodesRe + '"\')}\\],:;.!?]', 'ug'); diff --git a/tests/playwright-test/ui-mode-test-network-tab.spec.ts b/tests/playwright-test/ui-mode-test-network-tab.spec.ts index 29d2bcd88eab4..406336e75cae2 100644 --- a/tests/playwright-test/ui-mode-test-network-tab.spec.ts +++ b/tests/playwright-test/ui-mode-test-network-tab.spec.ts @@ -478,51 +478,3 @@ test('should preserve selection during test run', async ({ runUITest, server }, await page.waitForTimeout(1000); await expect(headersPanel).toBeVisible(); }); - -test('should support keyboard navigation for resource type filters', async ({ runUITest, server }) => { - server.setRoute('/api/endpoint', (_, res) => res.setHeader('Content-Type', 'application/json').end()); - - const { page } = await runUITest({ - 'network-tab.test.ts': ` - import { test, expect } from '@playwright/test'; - test('network tab test', async ({ page }) => { - await page.goto('${server.PREFIX}/network-tab/network.html'); - await page.evaluate(() => (window as any).donePromise); - }); - `, - }); - - await page.getByRole('treeitem', { name: 'network tab test' }).dblclick(); - await expect(page.getByTestId('workbench-run-status')).toContainText('Passed'); - - await page.getByRole('tab', { name: 'Network' }).click(); - - const filters = page.locator('.network-filters-resource-types'); - - // Focus the "All" tab and navigate with arrow keys. - await filters.getByText('All', { exact: true }).focus(); - await page.keyboard.press('ArrowRight'); - await expect(filters.getByText('Fetch', { exact: true })).toBeFocused(); - - await page.keyboard.press('ArrowRight'); - await expect(filters.getByText('HTML', { exact: true })).toBeFocused(); - - await page.keyboard.press('ArrowRight'); - await expect(filters.getByText('JS', { exact: true })).toBeFocused(); - - // ArrowLeft goes back. - await page.keyboard.press('ArrowLeft'); - await expect(filters.getByText('HTML', { exact: true })).toBeFocused(); - - // Home jumps to first tab. - await page.keyboard.press('Home'); - await expect(filters.getByText('All', { exact: true })).toBeFocused(); - - // End jumps to last tab. - await page.keyboard.press('End'); - await expect(filters.getByText('Image', { exact: true })).toBeFocused(); - - // Wraps around from last to first. - await page.keyboard.press('ArrowRight'); - await expect(filters.getByText('All', { exact: true })).toBeFocused(); -});