onFilterStateChange({ ...filterState, resourceTypes: new Set() })}
- className={`network-filters-resource-type ${filterState.resourceTypes.size === 0 ? 'selected' : ''}`}
+ className={`network-filters-resource-type ${isAllSelected ? 'selected' : ''}`}
+ role='tab'
+ tabIndex={isAllSelected ? 0 : -1}
+ aria-selected={isAllSelected}
>
All
@@ -64,6 +77,7 @@ 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 e2d53e1ffb63e..b0084b4abc6d5 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 } from '../uiUtils';
+import { clsx, handleTabListKeyDown } from '../uiUtils';
import './tabbedPane.css';
import { Toolbar } from './toolbar';
import * as React from 'react';
@@ -45,24 +45,9 @@ export const TabbedPane: React.FunctionComponent<{
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);
+ const nextIndex = handleTabListKeyDown(e, tabListRef.current);
+ if (nextIndex !== -1)
+ setSelectedTab?.(tabs[nextIndex].id);
};
return
diff --git a/packages/web/src/uiUtils.ts b/packages/web/src/uiUtils.ts
index 48b68f91e0a8a..bd2c73344e8e8 100644
--- a/packages/web/src/uiUtils.ts
+++ b/packages/web/src/uiUtils.ts
@@ -210,6 +210,27 @@ 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 406336e75cae2..77225d56f41ad 100644
--- a/tests/playwright-test/ui-mode-test-network-tab.spec.ts
+++ b/tests/playwright-test/ui-mode-test-network-tab.spec.ts
@@ -478,3 +478,51 @@ 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('WS', { exact: true })).toBeFocused();
+
+ // Wraps around from last to first.
+ await page.keyboard.press('ArrowRight');
+ await expect(filters.getByText('All', { exact: true })).toBeFocused();
+});