From 0fde5ebc3d2af29cf3b3031fd4d181ba8257b487 Mon Sep 17 00:00:00 2001 From: mck09 Date: Mon, 31 Aug 2026 11:03:57 +0200 Subject: [PATCH] fix(web): make clickable tag and label chips keyboard accessible --- packages/html-reporter/src/labels.css | 10 ++++++++++ packages/html-reporter/src/labels.tsx | 11 ++++++++--- packages/trace-viewer/src/ui/tag.css | 10 ++++++++++ packages/trace-viewer/src/ui/tag.tsx | 17 ++++++++++++----- .../ui-mode-test-filters.spec.ts | 17 +++++++++++++++++ 5 files changed, 57 insertions(+), 8 deletions(-) diff --git a/packages/html-reporter/src/labels.css b/packages/html-reporter/src/labels.css index ad9bdc3a36369..c9775add0ea69 100644 --- a/packages/html-reporter/src/labels.css +++ b/packages/html-reporter/src/labels.css @@ -30,6 +30,16 @@ cursor: pointer; } +.label-button { + font-family: inherit; + cursor: pointer; +} + +.label-button:focus-visible { + outline: 1px solid var(--color-accent-fg); + outline-offset: -1px; +} + .label-anchor { text-decoration: none; color: var(--color-fg-default); diff --git a/packages/html-reporter/src/labels.tsx b/packages/html-reporter/src/labels.tsx index 044e9ea074d9d..d5102f0a64703 100644 --- a/packages/html-reporter/src/labels.tsx +++ b/packages/html-reporter/src/labels.tsx @@ -28,9 +28,14 @@ export const Label: React.FC<{ onClick?: (e: React.MouseEvent, label: string) => void, colorIndex?: number, }> = ({ label, href, onClick, colorIndex, trimAtSymbolPrefix }) => { - const baseLabel = onClick(e, label) : undefined}> - {trimAtSymbolPrefix && label.startsWith('@') ? label.slice(1) : label} - ; + const className = clsx('label', 'label-color-' + (colorIndex !== undefined ? colorIndex : hashStringToInt(label))); + const text = trimAtSymbolPrefix && label.startsWith('@') ? label.slice(1) : label; + + // When there is an href the anchor below is already keyboard accessible, so only a + // standalone click handler needs a button of its own. + const baseLabel = onClick && !href + ? + : onClick(e, label) : undefined}>{text}; return href ? {baseLabel} diff --git a/packages/trace-viewer/src/ui/tag.css b/packages/trace-viewer/src/ui/tag.css index 41317c441aafe..0726ab6f44717 100644 --- a/packages/trace-viewer/src/ui/tag.css +++ b/packages/trace-viewer/src/ui/tag.css @@ -29,6 +29,16 @@ font-weight: 600; } +.tag-button { + font-family: inherit; + cursor: pointer; +} + +.tag-button:focus-visible { + outline: 1px solid var(--vscode-focusBorder); + outline-offset: -1px; +} + .tag-color-0 { background-color: #ddf4ff; color: #0550ae; diff --git a/packages/trace-viewer/src/ui/tag.tsx b/packages/trace-viewer/src/ui/tag.tsx index 057d70ac66d03..cd3dde43e3d4f 100644 --- a/packages/trace-viewer/src/ui/tag.tsx +++ b/packages/trace-viewer/src/ui/tag.tsx @@ -18,14 +18,21 @@ import { clsx } from '@web/uiUtils'; import './tag.css'; export const TagView = ({ tag, style, onClick }: { tag: string, style?: React.CSSProperties, onClick?: (e: React.MouseEvent) => void }) => { - return {tag}; + + return ; }; // hash string to integer in range [0, 6] for color index, to get same color for same tag diff --git a/tests/playwright-test/ui-mode-test-filters.spec.ts b/tests/playwright-test/ui-mode-test-filters.spec.ts index f549d150e4b09..7e2edd89831b5 100644 --- a/tests/playwright-test/ui-mode-test-filters.spec.ts +++ b/tests/playwright-test/ui-mode-test-filters.spec.ts @@ -72,6 +72,23 @@ test('should display native tags and filter by them on click', async ({ runUITes `); }); +test('should filter by tag from the keyboard', async ({ runUITest }) => { + const { page } = await runUITest({ + 'a.test.ts': ` + import { test, expect } from '@playwright/test'; + test('p', () => {}); + test('pwt', { tag: '@smoke' }, () => {}); + `, + }); + + const tag = page.locator('.ui-mode-tree-item-title').getByRole('button', { name: 'smoke' }); + await tag.focus(); + await expect(tag).toBeFocused(); + + await tag.press('Enter'); + await expect(page.getByPlaceholder('Filter (e.g. text, @tag)')).toHaveValue('@smoke'); +}); + test('should toggle filters from the keyboard', async ({ runUITest }) => { const { page } = await runUITest(basicTestTree); const summary = page.locator('.filter-summary');