️(frontend) focus docs list title after filter navigation

Explain focus shift to match skip-to-content behavior.

hook useRouteChangeCompleteFocus

Positionne the focus on the first target or main element after a route change.
This commit is contained in:
Cyril
2026-02-02 12:14:00 +01:00
parent 7cf42e6404
commit 48df68195a
6 changed files with 62 additions and 2 deletions

View File

@@ -6,6 +6,10 @@ and this project adheres to
## [Unreleased]
### Changed
- ♿️(frontend) Focus main container after navigation #1854
### Fixed
🐛(frontend) fix broadcast store sync #1846

View File

@@ -18,6 +18,20 @@ test.describe('Left panel desktop', () => {
await expect(page.getByTestId('home-button')).toBeVisible();
});
test('focuses main content after switching the docs filter', async ({
page,
}) => {
await page.goto('/');
const myDocsLink = page.getByRole('link', { name: 'My docs' });
await myDocsLink.focus();
await page.keyboard.press('Enter');
await expect(page).toHaveURL(/target=my_docs/);
const mainContent = page.locator('main#mainContent');
await expect(mainContent).toBeFocused();
});
test('checks resize handle is present and functional on document page', async ({
page,
browserName,

View File

@@ -9,6 +9,7 @@ import { useEffect } from 'react';
import { useCunninghamTheme } from '@/cunningham';
import { Auth, KEY_AUTH, setAuthUrl } from '@/features/auth';
import { useRouteChangeCompleteFocus } from '@/hooks/useRouteChangeCompleteFocus';
import { useResponsiveStore } from '@/stores/';
import { ConfigProvider } from './config/';
@@ -51,6 +52,7 @@ const queryClient = new QueryClient({
export function AppProvider({ children }: { children: React.ReactNode }) {
const { theme } = useCunninghamTheme();
const { replace } = useRouter();
useRouteChangeCompleteFocus();
const initializeResizeListener = useResponsiveStore(
(state) => state.initializeResizeListener,

View File

@@ -237,7 +237,7 @@ const DocGridTitleBar = ({
>
<Box $direction="row" $gap="xs" $align="center">
{icon}
<Text as="h2" $size="h4" $margin="none">
<Text as="h2" $size="h4" $margin="none" tabIndex={-1}>
{title}
</Text>
</Box>

View File

@@ -70,7 +70,9 @@ export const LeftPanelTargetFilters = () => {
href={href}
aria-label={query.label}
aria-current={isActive ? 'page' : undefined}
onClick={handleClick}
onClick={() => {
handleClick();
}}
$css={css`
display: flex;
align-items: center;

View File

@@ -0,0 +1,38 @@
import { useRouter } from 'next/router';
import { useEffect } from 'react';
import { MAIN_LAYOUT_ID } from '@/layouts/conf';
export const useRouteChangeCompleteFocus = () => {
const router = useRouter();
useEffect(() => {
const handleRouteChangeComplete = () => {
requestAnimationFrame(() => {
const mainContent =
document.getElementById(MAIN_LAYOUT_ID) ??
document.getElementsByTagName('main')[0];
if (!mainContent) {
return;
}
const firstHeading = mainContent.querySelector('h1, h2, h3');
const prefersReducedMotion = window.matchMedia(
'(prefers-reduced-motion: reduce)',
).matches;
(mainContent as HTMLElement | null)?.focus({ preventScroll: true });
(firstHeading ?? mainContent)?.scrollIntoView({
behavior: prefersReducedMotion ? 'auto' : 'smooth',
block: 'start',
});
});
};
router.events.on('routeChangeComplete', handleRouteChangeComplete);
return () => {
router.events.off('routeChangeComplete', handleRouteChangeComplete);
};
}, [router.events]);
};