mirror of
https://github.com/suitenumerique/docs.git
synced 2026-08-17 21:25:43 +02:00
✨(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:
+4
-1
@@ -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');
|
||||
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user