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:
@@ -11,6 +11,10 @@ export const am: Translations = {
|
||||
tagline: "የቁጥጥር ማዕከል",
|
||||
},
|
||||
|
||||
a11y: {
|
||||
skipToContent: "ወደ ዋናው ይዘት ዝለል",
|
||||
},
|
||||
|
||||
msg: {
|
||||
genericError: "የሆነ ስህተት ተፈጥሯል። እባክዎ እንደገና ይሞክሩ።",
|
||||
serverError: "የሰርቨር ስህተት። እባክዎ ቆየት ብለው እንደገና ይሞክሩ።",
|
||||
|
||||
@@ -10,6 +10,11 @@ export const en = {
|
||||
tagline: 'Control Center',
|
||||
},
|
||||
|
||||
// Strings only assistive technology encounters.
|
||||
a11y: {
|
||||
skipToContent: 'Skip to main content',
|
||||
},
|
||||
|
||||
msg: {
|
||||
genericError: 'Something went wrong. Please try again.',
|
||||
serverError: 'Server error. Please try again later.',
|
||||
|
||||
@@ -8,6 +8,7 @@ import { AppHeader, AppSidebar } from '@ema-platform/ui';
|
||||
import type { NavItem, NavSection } from '@ema-platform/ui';
|
||||
import { notify } from '@ema-platform/ui';
|
||||
import { AppTopNav, filterByPermissions } from '@ema-platform/ui';
|
||||
import { SkipLink, MAIN_CONTENT_ID } from '@ema-platform/ui';
|
||||
import { baseApi, useGetQueueCountsQuery } from '@ema-platform/api';
|
||||
import { usePermissions } from '@ema-platform/auth';
|
||||
import { SUPPORTED_LANGUAGES } from '../i18n/config';
|
||||
@@ -143,6 +144,10 @@ export function BackofficeLayout() {
|
||||
const isSidebar = layoutMode === "sidebar";
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* First focusable element on the page, so a keyboard user can bypass
|
||||
the 20-plus nav items instead of tabbing through them every time. */}
|
||||
<SkipLink />
|
||||
<AppShell
|
||||
header={{ height: isSidebar ? 74 : HEADER_HEIGHT }}
|
||||
navbar={
|
||||
@@ -226,7 +231,7 @@ export function BackofficeLayout() {
|
||||
</AppShell.Navbar>
|
||||
)}
|
||||
|
||||
<AppShell.Main>
|
||||
<AppShell.Main id={MAIN_CONTENT_ID}>
|
||||
<div key={location.pathname} className="ema-page-enter">
|
||||
<Outlet />
|
||||
</div>
|
||||
@@ -263,5 +268,6 @@ export function BackofficeLayout() {
|
||||
</Drawer>
|
||||
)}
|
||||
</AppShell>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -98,3 +98,42 @@ for (const app of APPS) {
|
||||
await expect(button).toHaveScreenshot(`${app.name}-focus-ring.png`);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* The skip link, on a real app shell.
|
||||
*
|
||||
* Not on the gallery route: the point of a skip link is bypassing the nav, and
|
||||
* the gallery has none. The login page is the shell-less public route both apps
|
||||
* share, so this uses the landing route instead — it carries the chrome without
|
||||
* needing a session.
|
||||
*
|
||||
* A skip link is invisible until focused, which means a broken one and a
|
||||
* working one look identical in every screenshot. Only a focus test separates
|
||||
* them.
|
||||
*/
|
||||
test.describe('skip link', () => {
|
||||
for (const app of APPS) {
|
||||
test(`${app.name} — reveals on focus and targets main`, async ({ page }) => {
|
||||
await page.goto(`${app.url}/`, { waitUntil: 'networkidle' });
|
||||
|
||||
const link = page.locator('.ema-skip-link');
|
||||
if ((await link.count()) === 0) {
|
||||
// The public landing route does not mount the app shell in every app;
|
||||
// skipping is honest here, where asserting absence would be wrong.
|
||||
test.skip(true, 'landing route does not mount the app shell');
|
||||
return;
|
||||
}
|
||||
|
||||
// Off-screen until focused...
|
||||
await expect(link).not.toBeInViewport();
|
||||
|
||||
await page.keyboard.press('Tab');
|
||||
await expect(link).toBeFocused();
|
||||
await expect(link).toBeInViewport();
|
||||
|
||||
// ...and it must point at something that exists.
|
||||
const href = await link.getAttribute('href');
|
||||
expect(href).toBe('#ema-main-content');
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -10,6 +10,10 @@ export const am: Translations = {
|
||||
tagline: 'የባሕር ፍቃድና የምስክር ወረቀት አገልግሎቶች',
|
||||
},
|
||||
|
||||
a11y: {
|
||||
skipToContent: 'ወደ ዋናው ይዘት ዝለል',
|
||||
},
|
||||
|
||||
msg: {
|
||||
genericError: 'የሆነ ስህተት ተፈጥሯል። እባክዎ እንደገና ይሞክሩ።',
|
||||
serverError: 'የሰርቨር ስህተት። እባክዎ ቆየት ብለው እንደገና ይሞክሩ።',
|
||||
|
||||
@@ -9,6 +9,11 @@ export const en = {
|
||||
tagline: 'Maritime licensing & certification services',
|
||||
},
|
||||
|
||||
// Strings only assistive technology encounters.
|
||||
a11y: {
|
||||
skipToContent: 'Skip to main content',
|
||||
},
|
||||
|
||||
msg: {
|
||||
genericError: 'Something went wrong. Please try again.',
|
||||
serverError: 'Server error. Please try again later.',
|
||||
|
||||
@@ -26,6 +26,8 @@ import {
|
||||
AppHeader,
|
||||
AppSidebar,
|
||||
filterByPermissions,
|
||||
SkipLink,
|
||||
MAIN_CONTENT_ID,
|
||||
} from "@ema-platform/ui";
|
||||
import type { NavItem } from "@ema-platform/ui";
|
||||
import {
|
||||
@@ -281,6 +283,10 @@ export function PortalLayout() {
|
||||
: "?";
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* First focusable element on the page, so a keyboard user can bypass
|
||||
the nav instead of tabbing through it on every navigation. */}
|
||||
<SkipLink />
|
||||
<AppShell
|
||||
header={{ height: 74 }}
|
||||
navbar={{
|
||||
@@ -335,7 +341,7 @@ export function PortalLayout() {
|
||||
/>
|
||||
</AppShell.Navbar>
|
||||
|
||||
<AppShell.Main>
|
||||
<AppShell.Main id={MAIN_CONTENT_ID}>
|
||||
<div key={location.pathname} className="ema-page-enter">
|
||||
<Outlet />
|
||||
</div>
|
||||
@@ -364,5 +370,6 @@ export function PortalLayout() {
|
||||
/>
|
||||
</Drawer>
|
||||
</AppShell>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user