feat(a11y): add skip link for improved navigation accessibility

This commit is contained in:
fitse-yotor
2026-08-21 12:34:00 +03:00
parent e74ef1c672
commit 8f6ebdafb1
11 changed files with 114 additions and 5 deletions

View File

@@ -19,6 +19,7 @@ export * from "./lib/layout/BrandAvatar";
export * from "./lib/layout/ColorSchemeToggle";
export * from "./lib/layout/LanguageSwitcher";
export * from "./lib/layout/PageHeader";
export * from "./lib/layout/SkipLink";
export * from "./lib/input/PasswordRequirements";
export * from "./lib/input/CountrySelect";
export * from "./lib/input/PhoneInput";

View File

@@ -22,6 +22,7 @@ import {
Title,
useMantineTheme,
} from '@mantine/core';
import { SkipLink, MAIN_CONTENT_ID } from '../layout/SkipLink';
/**
* Every primitive the theme controls, on one page.
@@ -90,7 +91,11 @@ export function ThemeGallery() {
);
return (
<Box p="xl" style={{ maxWidth: 1100, margin: '0 auto' }}>
<>
{/* Mirrors the real app shells, so the skip-link contract is testable
without a session. */}
<SkipLink />
<Box id={MAIN_CONTENT_ID} p="xl" style={{ maxWidth: 1100, margin: '0 auto' }}>
<Stack gap="xl">
<Stack gap={4}>
<Title order={1}>Theme Gallery</Title>
@@ -308,5 +313,6 @@ export function ThemeGallery() {
</Section>
</Stack>
</Box>
</>
);
}

View File

@@ -111,6 +111,7 @@ export function PageLoader({
position: 'absolute',
top: 0,
bottom: 0,
left: 0,
width: '40%',
background: 'linear-gradient(90deg, #078930, #FCD116, #2563EB)',
borderRadius: '2px',
@@ -118,9 +119,13 @@ export function PageLoader({
}}
/>
<style>{`
/* Translating rather than animating \`left\`: the latter runs
layout on every frame of an animation that plays during page
loads, which is exactly when the main thread is busiest.
Reduced motion is handled globally in semantic.css. */
@keyframes ema-shimmer {
0% { left: -40%; }
100% { left: 100%; }
0% { transform: translateX(-100%); }
100% { transform: translateX(250%); }
}
`}</style>
</Box>

View File

@@ -0,0 +1,27 @@
import { useTranslation } from 'react-i18next';
/** The id the skip link targets. Exported so the main region cannot drift. */
export const MAIN_CONTENT_ID = 'ema-main-content';
/**
* "Skip to main content" — the first thing a keyboard user should reach.
*
* Both apps put a sidebar of 20-plus navigation items before the page body, so
* without this, reaching the actual content means tabbing through every one of
* them on every navigation. WCAG 2.4.1 asks for a bypass; there was none.
*
* Hidden until focused, which is why it is positioned off-screen rather than
* `display: none` — the latter would make it unfocusable and defeat the point.
* Styling lives in `semantic.css` as `.ema-skip-link`.
*
* Render it as the first child of the shell, before the header.
*/
export function SkipLink() {
const { t } = useTranslation();
return (
<a className="ema-skip-link" href={`#${MAIN_CONTENT_ID}`}>
{t('a11y.skipToContent', 'Skip to main content')}
</a>
);
}