Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions e2e/specs/app/route_focus_navigation.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ async function settleAnimationFrames(page: Page) {
await page.evaluate(() => new Promise<void>((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 },
Expand All @@ -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$/);

Expand Down Expand Up @@ -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);
Expand All @@ -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);
}
Expand All @@ -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);

Expand Down
4 changes: 3 additions & 1 deletion src/components/layout/MainContentAccessibility.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@ type MainContentProps = Omit<React.ComponentPropsWithoutRef<'main'>, '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 <main {...mainProps} id={MAIN_CONTENT_ID} className={clsx('scroll-mt-24', className)} tabIndex={-1} />;
return <main {...mainProps} id={MAIN_CONTENT_ID} className={clsx('scroll-mt-24 focus:outline-none', className)} tabIndex={-1} />;
}
Loading