♿️(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:
@@ -6,6 +6,10 @@ and this project adheres to
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- ♿️(frontend) Focus main container after navigation #1854
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
🐛(frontend) fix broadcast store sync #1846
|
🐛(frontend) fix broadcast store sync #1846
|
||||||
|
|||||||
@@ -18,6 +18,20 @@ test.describe('Left panel desktop', () => {
|
|||||||
await expect(page.getByTestId('home-button')).toBeVisible();
|
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 ({
|
test('checks resize handle is present and functional on document page', async ({
|
||||||
page,
|
page,
|
||||||
browserName,
|
browserName,
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { useEffect } from 'react';
|
|||||||
|
|
||||||
import { useCunninghamTheme } from '@/cunningham';
|
import { useCunninghamTheme } from '@/cunningham';
|
||||||
import { Auth, KEY_AUTH, setAuthUrl } from '@/features/auth';
|
import { Auth, KEY_AUTH, setAuthUrl } from '@/features/auth';
|
||||||
|
import { useRouteChangeCompleteFocus } from '@/hooks/useRouteChangeCompleteFocus';
|
||||||
import { useResponsiveStore } from '@/stores/';
|
import { useResponsiveStore } from '@/stores/';
|
||||||
|
|
||||||
import { ConfigProvider } from './config/';
|
import { ConfigProvider } from './config/';
|
||||||
@@ -51,6 +52,7 @@ const queryClient = new QueryClient({
|
|||||||
export function AppProvider({ children }: { children: React.ReactNode }) {
|
export function AppProvider({ children }: { children: React.ReactNode }) {
|
||||||
const { theme } = useCunninghamTheme();
|
const { theme } = useCunninghamTheme();
|
||||||
const { replace } = useRouter();
|
const { replace } = useRouter();
|
||||||
|
useRouteChangeCompleteFocus();
|
||||||
|
|
||||||
const initializeResizeListener = useResponsiveStore(
|
const initializeResizeListener = useResponsiveStore(
|
||||||
(state) => state.initializeResizeListener,
|
(state) => state.initializeResizeListener,
|
||||||
|
|||||||
@@ -237,7 +237,7 @@ const DocGridTitleBar = ({
|
|||||||
>
|
>
|
||||||
<Box $direction="row" $gap="xs" $align="center">
|
<Box $direction="row" $gap="xs" $align="center">
|
||||||
{icon}
|
{icon}
|
||||||
<Text as="h2" $size="h4" $margin="none">
|
<Text as="h2" $size="h4" $margin="none" tabIndex={-1}>
|
||||||
{title}
|
{title}
|
||||||
</Text>
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -70,7 +70,9 @@ export const LeftPanelTargetFilters = () => {
|
|||||||
href={href}
|
href={href}
|
||||||
aria-label={query.label}
|
aria-label={query.label}
|
||||||
aria-current={isActive ? 'page' : undefined}
|
aria-current={isActive ? 'page' : undefined}
|
||||||
onClick={handleClick}
|
onClick={() => {
|
||||||
|
handleClick();
|
||||||
|
}}
|
||||||
$css={css`
|
$css={css`
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
@@ -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]);
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user