From 72f36b68cdbcd80504f766de70d10523b73531cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20Janou=C5=A1ek?= Date: Thu, 24 Sep 2026 20:20:16 +0200 Subject: [PATCH] fix: remove the page-wide outline from the route focus target Safari outlines the shared main landmark when navigation or a skip link focuses it, producing blue lines down both sides of the page. Suppress only that non-interactive landmark outline while preserving focus and scroll behavior and the visible focus indicators on actual controls. Extend the navigation regression to verify computed focus styles and use Option+Tab when testing link traversal in macOS WebKit. --- e2e/specs/app/route_focus_navigation.spec.ts | 11 +++++++++-- src/components/layout/MainContentAccessibility.tsx | 4 +++- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/e2e/specs/app/route_focus_navigation.spec.ts b/e2e/specs/app/route_focus_navigation.spec.ts index 7b5e4848..0317036d 100644 --- a/e2e/specs/app/route_focus_navigation.spec.ts +++ b/e2e/specs/app/route_focus_navigation.spec.ts @@ -31,7 +31,7 @@ async function settleAnimationFrames(page: Page) { await page.evaluate(() => new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(() => resolve())))); } -test('@pr-smoke @pr-smoke-mobile @smoke @smoke-mobile keyboard users can skip chrome and keep route focus predictable', async ({ page }, testInfo) => { +test('@pr-smoke @pr-smoke-mobile @smoke @smoke-mobile keyboard users can skip chrome and keep route focus predictable', async ({ page, browserName }, testInfo) => { await bootstrapVpsAdminWindow(page, { sessionToken: 'KEYBOARD_USER_SESSION' }); await installHaveApiMock(page, { user: { id: 10, login: 'alice', level: 100 }, @@ -53,11 +53,14 @@ test('@pr-smoke @pr-smoke-mobile @smoke @smoke-mobile keyboard users can skip ch await expect(page.getByTestId('app.dashboard.page')).toBeVisible(); await expect(main).not.toBeFocused(); - await page.keyboard.press('Tab'); + // WebKit on macOS uses Option+Tab to include links in keyboard traversal. + await page.keyboard.press(browserName === 'webkit' ? 'Alt+Tab' : 'Tab'); await expect(skipLink).toBeFocused(); await expect(skipLink).toBeVisible(); + await expect(skipLink).not.toHaveCSS('box-shadow', 'none'); await page.keyboard.press('Enter'); await expect(main).toBeFocused(); + await expect(main).toHaveCSS('outline-style', 'none'); await page.evaluate(() => window.history.replaceState(window.history.state, '', '/app')); await expect(page).toHaveURL(/\/app$/); @@ -94,6 +97,7 @@ test('@pr-smoke @pr-smoke-mobile @smoke @smoke-mobile keyboard users can skip ch await expect(page).toHaveURL(/\/app\/vps$/); await expect(page.getByTestId('vps.list')).toBeVisible(); await expect(main).toBeFocused(); + await expect(main).toHaveCSS('outline-style', 'none'); await expect.poll(() => page.evaluate(() => window.scrollY)).toBe(0); await settleAnimationFrames(page); expect(await scrollToCallCount(page)).toBe(1); @@ -109,12 +113,14 @@ test('@pr-smoke @pr-smoke-mobile @smoke @smoke-mobile keyboard users can skip ch await expect(page).toHaveURL(/\/app$/); await expect(page.getByTestId('nav.drawer')).toBeHidden(); await expect(main).toBeFocused(); + await expect(main).toHaveCSS('outline-style', 'none'); await settleAnimationFrames(page); expect(await scrollToCallCount(page)).toBe(0); await page.goForward(); await expect(page).toHaveURL(/\/app\/vps(?:\?.*)?$/); await expect(main).toBeFocused(); + await expect(main).toHaveCSS('outline-style', 'none'); await settleAnimationFrames(page); expect(await scrollToCallCount(page)).toBe(0); } @@ -125,6 +131,7 @@ test('@pr-smoke @pr-smoke-mobile @smoke @smoke-mobile keyboard users can skip ch await smartFilter.press('Enter'); await expect(page).toHaveURL(/\/app\/vps\?.*q=alpha/); await expect(smartFilter).toBeFocused(); + await expect(smartFilter).not.toHaveCSS('box-shadow', 'none'); await settleAnimationFrames(page); expect(await scrollToCallCount(page)).toBe(0); diff --git a/src/components/layout/MainContentAccessibility.tsx b/src/components/layout/MainContentAccessibility.tsx index 1256cc16..2c4c22c4 100644 --- a/src/components/layout/MainContentAccessibility.tsx +++ b/src/components/layout/MainContentAccessibility.tsx @@ -21,7 +21,9 @@ type MainContentProps = Omit, 'id' | 'tab /** * Shared focus target for skip links and client-side route changes. The scroll * margin keeps the destination clear of the sticky public and app headers. + * Keep this non-interactive landmark focusable without outlining the entire + * page; keyboard focus indicators on the skip link and controls remain intact. */ export function MainContent({ className, ...mainProps }: MainContentProps) { - return
; + return
; }