mirror of
https://github.com/Tria-plc/emaui.git
synced 2026-08-26 13:02:50 +00:00
feat(a11y): add skip link for improved navigation accessibility
This commit is contained in:
@@ -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";
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
27
libs/ui/src/lib/layout/SkipLink.tsx
Normal file
27
libs/ui/src/lib/layout/SkipLink.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user