(frontend) restore skip to content link after header redesign

Re-add skip link with responsive positioning and document title focus.
This commit is contained in:
Cyril
2026-07-13 09:41:42 +02:00
parent 57ad4f4be8
commit 8d2dd5b565
5 changed files with 57 additions and 12 deletions
+4 -1
View File
@@ -6,6 +6,10 @@ and this project adheres to
## [Unreleased]
### Added
- ♿️(frontend) restore skip to content link after header redesign #2510
## [v5.4.1] - 2026-07-09
### Changed
@@ -17,7 +21,6 @@ and this project adheres to
- 💄(frontend) fix some UI/UX in the left panel #2516
- 🐛(frontend) fix tree dnd firefox #2516
## [v5.4.0] - 2026-07-07
### Added
@@ -1,10 +1,7 @@
import { expect, test } from '@playwright/test';
test.describe('Header', () => {
/**
* TODO: add back the skip link
*/
test.skip('it displays skip link on first TAB and focuses page heading on click', async ({
test('it displays skip link on first TAB and focuses page heading on click', async ({
page,
}) => {
await page.goto('/');
@@ -1,31 +1,66 @@
import { Button } from '@gouvfr-lasuite/cunningham-react';
import { useRouter } from 'next/router';
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Box } from '@/components';
import { Box } from '@/components/Box';
import { useCunninghamTheme } from '@/cunningham';
import { useLeftPanelStore } from '@/features/left-panel/stores/useLeftPanelStore';
import { MAIN_LAYOUT_ID } from '@/layouts/conf';
import { focusMainContentStart } from '@/layouts/utils';
import { useResponsiveStore } from '@/stores';
const LEFT_PANEL_CLOSE_TRANSITION_MS = 200;
const HEADER_ROW_HEIGHT = '68px';
const HEADER_ICON_WIDTH = '32px';
const COLLAPSE_BUTTON_WIDTH = '48px';
export const SkipToContent = () => {
const { t } = useTranslation();
const { pathname } = useRouter();
const { spacingsTokens } = useCunninghamTheme();
const { isMobile } = useResponsiveStore();
const { isPanelOpen, closePanel } = useLeftPanelStore();
const [isVisible, setIsVisible] = useState(false);
const handleClick = (e: React.MouseEvent) => {
e.preventDefault();
const focusTarget = focusMainContentStart();
if (focusTarget instanceof HTMLElement) {
focusTarget.scrollIntoView({ behavior: 'smooth', block: 'start' });
const focusContent = () => {
const focusTarget = focusMainContentStart();
if (focusTarget instanceof HTMLElement) {
focusTarget.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
};
if (isMobile && isPanelOpen) {
closePanel();
window.setTimeout(focusContent, LEFT_PANEL_CLOSE_TRANSITION_MS);
return;
}
focusContent();
};
const leftBesideLogo = `calc(${spacingsTokens['sm']} + ${HEADER_ICON_WIDTH} + ${spacingsTokens['4xs']} + 70px + 12px)`;
const isMobilePanelClosed = isMobile && !isPanelOpen;
const isDocEditorPage = pathname === '/docs/[id]';
const left = isMobilePanelClosed
? isDocEditorPage
? `calc(${spacingsTokens['sm']} + ${COLLAPSE_BUTTON_WIDTH} + ${spacingsTokens['2xs']})`
: `calc(${spacingsTokens['sm']} + ${HEADER_ICON_WIDTH} + ${spacingsTokens['2xs']})`
: leftBesideLogo;
const top = `calc(${HEADER_ROW_HEIGHT} / 2)`;
return (
<Box>
<Button
href={`#${MAIN_LAYOUT_ID}`}
color="brand"
size="small"
className="--docs--skip-to-content"
onClick={handleClick}
onFocus={() => setIsVisible(true)}
@@ -34,8 +69,9 @@ export const SkipToContent = () => {
opacity: isVisible ? 1 : 0,
pointerEvents: isVisible ? 'auto' : 'none',
position: 'fixed',
top: spacingsTokens['2xs'],
left: `calc(${spacingsTokens['base']} + 32px + ${spacingsTokens['3xs']} + 70px + 12px)`,
top,
left,
transform: 'translateY(-50%)',
zIndex: 9999,
whiteSpace: 'nowrap',
}}
@@ -12,6 +12,11 @@ export const getMainContentFocusTarget = (): HTMLElement | null => {
return null;
}
const docTitle = mainContent.querySelector('.--docs--doc-title');
if (docTitle instanceof HTMLElement) {
return docTitle;
}
const firstHeading =
mainContent.querySelector('h1') ?? mainContent.querySelector('h2');
+5 -1
View File
@@ -2,6 +2,7 @@ import type { AppProps } from 'next/app';
import Head from 'next/head';
import { useTranslation } from 'react-i18next';
import { SkipToContent } from '@/components';
import { AppProvider } from '@/core/';
import { useOffline, useSWRegister } from '@/features/service-worker/';
import '@/i18n/initI18n';
@@ -32,7 +33,10 @@ export default function App({ Component, pageProps }: AppPropsWithLayout) {
/>
<meta name="viewport" content="width=device-width, initial-scale=1" />
</Head>
<AppProvider>{getLayout(<Component {...pageProps} />)}</AppProvider>
<AppProvider>
<SkipToContent />
{getLayout(<Component {...pageProps} />)}
</AppProvider>
</>
);
}