Skip to content

Commit cbb5c79

Browse files
committed
✨(a11y) add skip to content button for keyboard accessibility
add SkipToContent component to meet RGAA skiplink requirement Signed-off-by: Cyril <[email protected]>
1 parent 2e64298 commit cbb5c79

File tree

6 files changed

+135
-1
lines changed

6 files changed

+135
-1
lines changed
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
import { useTranslation } from 'react-i18next';
2+
import styled, { css } from 'styled-components';
3+
4+
import { useCunninghamTheme } from '@/cunningham';
5+
import { MAIN_LAYOUT_ID } from '@/layouts/conf';
6+
7+
import { Box } from './Box';
8+
9+
const SkipLink = styled(Box)<{
10+
$colorsTokens: Record<string, string>;
11+
$spacingsTokens: Record<string, string>;
12+
}>`
13+
${({ $colorsTokens, $spacingsTokens }) => css`
14+
position: fixed;
15+
top: 0.5rem;
16+
/* Position: padding header + logo(32px) + gap(3xs≈4px) + text "Docs"(≈70px) + 12px */
17+
left: calc(
18+
${$spacingsTokens['base']} + 32px + ${$spacingsTokens['3xs']} + 70px +
19+
12px
20+
);
21+
z-index: 9999;
22+
23+
/* Figma specs - Layout */
24+
display: inline-flex;
25+
padding: ${$spacingsTokens['xs']} ${$spacingsTokens['xs']};
26+
flex-direction: column;
27+
justify-content: center;
28+
align-items: flex-start;
29+
gap: ${$spacingsTokens['4xs']};
30+
31+
/* Figma specs - Style */
32+
border-radius: ${$spacingsTokens['3xs']};
33+
border: 1px solid
34+
var(--c--theme--colors--primary-300, ${$colorsTokens['primary-300']});
35+
background: var(
36+
--c--theme--colors--primary-100,
37+
${$colorsTokens['primary-100']}
38+
);
39+
box-shadow: 0 6px 18px 0 rgba(0, 0, 145, 0.05);
40+
41+
/* Figma specs - Typography */
42+
color: ${$colorsTokens['primary-600']};
43+
font-family: var(--c--theme--font--families--base, 'Marianne Variable');
44+
font-size: 14px;
45+
font-style: normal;
46+
font-weight: 500;
47+
line-height: 18px;
48+
49+
/* Skip link behavior - Fondu enchainé */
50+
text-decoration: none;
51+
white-space: nowrap;
52+
opacity: 0;
53+
pointer-events: none;
54+
transition: opacity 0.3s ease-in-out;
55+
56+
&:focus-visible {
57+
opacity: 1;
58+
pointer-events: auto;
59+
outline: 2px solid var(--c--theme--colors--primary-400);
60+
outline-offset: ${$spacingsTokens['4xs']};
61+
}
62+
63+
&:hover {
64+
background: var(
65+
--c--theme--colors--primary-200,
66+
${$colorsTokens['primary-200']}
67+
);
68+
color: ${$colorsTokens['primary-700']};
69+
}
70+
`}
71+
`;
72+
73+
export const SkipToContent = () => {
74+
const { t } = useTranslation();
75+
const { colorsTokens, spacingsTokens } = useCunninghamTheme();
76+
77+
const handleClick = (e: React.MouseEvent<HTMLAnchorElement>) => {
78+
e.preventDefault();
79+
const mainContent = document.getElementById(MAIN_LAYOUT_ID);
80+
if (mainContent) {
81+
mainContent.focus();
82+
mainContent.scrollIntoView({ behavior: 'smooth', block: 'start' });
83+
}
84+
};
85+
86+
return (
87+
<SkipLink
88+
as="a"
89+
href={`#${MAIN_LAYOUT_ID}`}
90+
onClick={handleClick}
91+
$colorsTokens={colorsTokens}
92+
$spacingsTokens={spacingsTokens}
93+
>
94+
{t('Go to content')}
95+
</SkipLink>
96+
);
97+
};

src/frontend/apps/impress/src/components/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,5 +11,6 @@ export * from './Loading';
1111
export * from './modal';
1212
export * from './Overlayer';
1313
export * from './separators';
14+
export * from './SkipToContent';
1415
export * from './Text';
1516
export * from './TextErrors';

src/frontend/apps/impress/src/features/home/components/HomeContent.tsx

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { Box, Icon, Text } from '@/components';
66
import { useCunninghamTheme } from '@/cunningham';
77
import { Footer } from '@/features/footer';
88
import { LeftPanel } from '@/features/left-panel';
9+
import { MAIN_LAYOUT_ID } from '@/layouts/conf';
910
import { useResponsiveStore } from '@/stores';
1011

1112
import SC1ResponsiveEn from '../assets/SC1-responsive-en.png';
@@ -36,8 +37,19 @@ export function HomeContent() {
3637
<Box
3738
as="main"
3839
role="main"
40+
id={MAIN_LAYOUT_ID}
41+
tabIndex={-1}
3942
className="--docs--home-content"
4043
aria-label={t('Main content')}
44+
$css={css`
45+
&:focus {
46+
outline: 3px solid ${colorsTokens['primary-600']};
47+
outline-offset: -3px;
48+
}
49+
&:focus:not(:focus-visible) {
50+
outline: none;
51+
}
52+
`}
4153
>
4254
<HomeHeader />
4355
{isSmallMobile && (

src/frontend/apps/impress/src/layouts/MainLayout.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ export function MainLayoutContent({
6363
role="main"
6464
aria-label={t('Main content')}
6565
id={MAIN_LAYOUT_ID}
66+
tabIndex={-1}
6667
$align="center"
6768
$flex={1}
6869
$width="100%"
@@ -79,6 +80,13 @@ export function MainLayoutContent({
7980
$css={css`
8081
overflow-y: auto;
8182
overflow-x: clip;
83+
&:focus {
84+
outline: 3px solid ${colorsTokens['primary-600']};
85+
outline-offset: -3px;
86+
}
87+
&:focus:not(:focus-visible) {
88+
outline: none;
89+
}
8290
`}
8391
>
8492
<Skeleton>

src/frontend/apps/impress/src/layouts/PageLayout.tsx

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
11
import { PropsWithChildren } from 'react';
22
import { useTranslation } from 'react-i18next';
3+
import { css } from 'styled-components';
34

45
import { Box } from '@/components';
56
import { Footer } from '@/features/footer';
67
import { HEADER_HEIGHT, Header } from '@/features/header';
78
import { LeftPanel } from '@/features/left-panel';
89
import { useResponsiveStore } from '@/stores';
910

11+
import { MAIN_LAYOUT_ID } from './conf';
12+
1013
interface PageLayoutProps {
1114
withFooter?: boolean;
1215
}
@@ -27,8 +30,19 @@ export function PageLayout({
2730
<Box
2831
as="main"
2932
role="main"
33+
id={MAIN_LAYOUT_ID}
34+
tabIndex={-1}
3035
$width="100%"
31-
$css="flex-grow:1;"
36+
$css={css`
37+
flex-grow: 1;
38+
&:focus {
39+
outline: 3px solid var(--c--theme--colors--primary-600);
40+
outline-offset: -3px;
41+
}
42+
&:focus:not(:focus-visible) {
43+
outline: none;
44+
}
45+
`}
3246
aria-label={t('Main content')}
3347
>
3448
{!isDesktop && <LeftPanel />}

src/frontend/apps/impress/src/pages/_app.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import type { AppProps } from 'next/app';
22
import Head from 'next/head';
33
import { useTranslation } from 'react-i18next';
44

5+
import { SkipToContent } from '@/components';
56
import { AppProvider } from '@/core/';
67
import { useCunninghamTheme } from '@/cunningham';
78
import { useOffline, useSWRegister } from '@/features/service-worker/';
@@ -49,6 +50,7 @@ export default function App({ Component, pageProps }: AppPropsWithLayout) {
4950
/>
5051
<meta name="viewport" content="width=device-width, initial-scale=1" />
5152
</Head>
53+
<SkipToContent />
5254
<AppProvider>{getLayout(<Component {...pageProps} />)}</AppProvider>
5355
</>
5456
);

0 commit comments

Comments
 (0)