Skip to content

Commit 0f7bac5

Browse files
Merge pull request #149 from jeffreylauwers/feature/skip-link
feat(SkipLink): toegankelijkheidskoppeling om herhalende navigatie te omzeilen
2 parents 411c21f + 0e7809d commit 0f7bac5

18 files changed

Lines changed: 607 additions & 15 deletions

File tree

README.md

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ pnpm --filter @dsn/design-tokens watch
6565
# Start Storybook in development mode
6666
pnpm dev
6767

68-
# Run tests (1282 tests across 63 test suites)
68+
# Run tests (1292 tests across 64 test suites)
6969
pnpm test
7070

7171
# Run tests in watch mode
@@ -211,6 +211,12 @@ All components are fully typed with TypeScript and include comprehensive JSDoc d
211211
| **MenuLink** | Yes | Yes ||
212212
| **PageHeader** | Yes | Yes ||
213213

214+
**Accessibility Components (1)**
215+
216+
| Component | HTML/CSS | React | Web Component |
217+
| ------------ | -------- | ----- | ------------- |
218+
| **SkipLink** | Yes | Yes ||
219+
214220
**Form Components (25)**
215221

216222
| Component | HTML/CSS | React | Web Component |
@@ -384,7 +390,7 @@ Comprehensive documentation is available in the `/docs` folder:
384390

385391
- **Pre-commit hooks** via Husky + lint-staged (ESLint + Prettier)
386392
- **Type checking** across all packages (`pnpm type-check`)
387-
- **1282 tests** covering React components, Web Components, and utilities
393+
- **1292 tests** covering React components, Web Components, and utilities
388394
- **CI/CD** via GitHub Actions (lint, type-check, test, build)
389395

390396
## Tech Stack

docs/03-components.md

Lines changed: 68 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Components
22

3-
**Last Updated:** April 6, 2026
3+
**Last Updated:** April 9, 2026
44

55
Complete component specifications and guidelines for the Design System Starter Kit.
66

@@ -14,8 +14,9 @@ Complete component specifications and guidelines for the Design System Starter K
1414
4. [Display & Feedback Components](#display--feedback-components)
1515
5. [Navigation Components](#navigation-components)
1616
6. [Branding Components](#branding-components)
17-
7. [Form Components](#form-components)
18-
8. [Web Components Registration](#web-components-registration)
17+
7. [Accessibility Components](#accessibility-components)
18+
8. [Form Components](#form-components)
19+
9. [Web Components Registration](#web-components-registration)
1920

2021
---
2122

@@ -2200,6 +2201,66 @@ const [isOpen, setIsOpen] = React.useState(false);
22002201

22012202
---
22022203

2204+
## Accessibility Components
2205+
2206+
**Status:** Complete (HTML/CSS, React) — 1 component total
2207+
2208+
### SkipLink
2209+
2210+
**Status:** Complete (HTML/CSS, React)
2211+
2212+
**Location:** `packages/components-{html|react}/src/skip-link/` / `SkipLink/`
2213+
2214+
**Tokens:** `tokens/components/skip-link.json`
2215+
2216+
**Props:** `href` (required), `children` (default: `"Ga direct naar de hoofdinhoud"`), `className` + alle `React.AnchorHTMLAttributes<HTMLAnchorElement>` attributen
2217+
2218+
**Features:**
2219+
2220+
- Eerste focusbaar element op de pagina — plaatsen vóór `<header>` en `<nav>` in de DOM
2221+
- Standaard verborgen via `clip-path: inset(50%)` — blijft in de accessibility tree (screenreaders kunnen het vinden)
2222+
- Zichtbaar bij `:focus-visible` — gepositioneerd in de hoek van het viewport met focus-stijlen
2223+
- Z-index 600 — boven modals (500), drawer (500) en backdrop (400)
2224+
- Voldoet aan WCAG 2.1 succescriterium 2.4.1 (Bypass Blocks, Level A)
2225+
- `React.forwardRef<HTMLAnchorElement>`
2226+
2227+
**CSS-klassen:**
2228+
2229+
| Klasse | Element | Beschrijving |
2230+
| --------------- | ------- | -------------------------------------------------- |
2231+
| `dsn-skip-link` | `<a>` | Verbergt de link; onthult hem bij `:focus-visible` |
2232+
2233+
**Design Tokens:**
2234+
2235+
| Token | Waarde | Beschrijving |
2236+
| ------------------------------------- | ------------------------ | ---------------------------------------------------- |
2237+
| `--dsn-skip-link-z-index` | `600` | Boven modals en backdrop |
2238+
| `--dsn-skip-link-padding-block` | `{dsn.space.block.md}` | Verticale padding bij focus |
2239+
| `--dsn-skip-link-padding-inline` | `{dsn.space.inline.lg}` | Horizontale padding bij focus |
2240+
| `--dsn-skip-link-border-radius` | `{dsn.border.radius.md}` | Afgeronde hoeken bij focus |
2241+
| `--dsn-skip-link-offset-block-start` | `{dsn.space.block.md}` | Afstand van boven het viewport bij focus |
2242+
| `--dsn-skip-link-offset-inline-start` | `{dsn.space.inline.md}` | Afstand van de linkerkant van het viewport bij focus |
2243+
2244+
**Usage:**
2245+
2246+
```html
2247+
<!-- HTML/CSS — altijd als eerste element in <body> -->
2248+
<a href="#main-content" class="dsn-skip-link">Ga direct naar de hoofdinhoud</a>
2249+
<header>...</header>
2250+
<main id="main-content" tabindex="-1">...</main>
2251+
```
2252+
2253+
```tsx
2254+
// React
2255+
<SkipLink href="#main-content">Ga direct naar de hoofdinhoud</SkipLink>
2256+
<header>...</header>
2257+
<main id="main-content" tabIndex={-1}>...</main>
2258+
```
2259+
2260+
**Tests:** React (10 tests)
2261+
2262+
---
2263+
22032264
## Form Components
22042265

22052266
**Status:** Complete (HTML/CSS, React) — 25 components total
@@ -2511,15 +2572,15 @@ defineButton('my-custom-button');
25112572

25122573
## Component Statistics
25132574

2514-
**Total Components:** 49
2575+
**Total Components:** 50
25152576

25162577
**Implementations:**
25172578

2518-
- **HTML/CSS:** 49 components
2519-
- **React:** 49 components (1282 tests total, 63 test suites)
2579+
- **HTML/CSS:** 50 components
2580+
- **React:** 50 components (1292 tests total, 64 test suites)
25202581
- **Web Component:** 7 components (Button, Heading, Icon, Link, OrderedList, Paragraph, UnorderedList)
25212582

2522-
**Test Coverage:** 1282 tests across 63 test suites
2583+
**Test Coverage:** 1292 tests across 64 test suites
25232584

25242585
---
25252586

docs/README.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# Design System Documentation
22

3-
**Version:** 5.18.0
4-
**Last Updated:** April 4, 2026
3+
**Version:** 5.22.0
4+
**Last Updated:** April 9, 2026
55

66
Complete documentation voor het Design System Starter Kit.
77

@@ -91,8 +91,8 @@ Complete documentation voor het Design System Starter Kit.
9191

9292
- **Tokens per configuration:** ~1100 (400 semantic + 700 component)
9393
- **Configurations:** 8 (2 themes × 2 modes × 2 project types)
94-
- **Components:** 48 (5 layout + 10 content + 9 display/feedback + 1 branding + 3 navigation + 25 form; HTML/CSS + React)
95-
- **Tests:** 1248 across 62 test suites
94+
- **Components:** 50 (5 layout + 10 content + 9 display/feedback + 1 branding + 5 navigation + 25 form + 1 accessibility; HTML/CSS + React)
95+
- **Tests:** 1292 across 64 test suites
9696
- **Storybook stories:** 130+
9797

9898
---

docs/changelog.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,23 @@ All notable changes to this project are documented in this file.
66

77
---
88

9+
## Version 5.22.0 (April 9, 2026)
10+
11+
### SkipLink component (issue #148, PR #149)
12+
13+
#### Added
14+
15+
- **SkipLink** component — toegankelijkheidskoppeling als eerste focusbaar element op de pagina (PR #149)
16+
- Standaard verborgen via `clip-path: inset(50%)`; zichtbaar bij `:focus-visible` in de hoek van het viewport
17+
- Voldoet aan WCAG 2.1 succescriterium 2.4.1 (Bypass Blocks, Level A)
18+
- `href` prop (verplicht), `children` prop (default: `"Ga direct naar de hoofdinhoud"`), `React.forwardRef<HTMLAnchorElement>`
19+
- 6 design tokens: `z-index: 600`, padding-block/inline, border-radius, offset-block/inline-start
20+
- Z-index 500 toegevoegd aan `modal-dialog.json` en `drawer.json`
21+
- `backdrop.json` comment bijgewerkt met volledige z-index schaal: backdrop (400) → modal-dialog/drawer (500) → skip-link (600)
22+
- 10 React tests
23+
24+
---
25+
926
## Version 5.21.1 (April 6, 2026)
1027

1128
### Fix: PageHeader large viewport verfijningen (PR #146)
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
/**
2+
* SkipLink Component
3+
* Accessibility component — eerste focusbare element op de pagina.
4+
* Verborgen totdat de gebruiker er met Tab op focust.
5+
*/
6+
7+
.dsn-skip-link {
8+
/* Verborgen standaard — zelfde patroon als dsn-visually-hidden */
9+
position: absolute;
10+
clip-path: inset(50%);
11+
overflow: hidden;
12+
white-space: nowrap;
13+
14+
/* Hoge z-index zodat de link altijd boven andere elementen staat bij focus */
15+
z-index: var(--dsn-skip-link-z-index);
16+
17+
/* Structuur */
18+
display: inline-block;
19+
20+
/* Spacing */
21+
padding-block: var(--dsn-skip-link-padding-block);
22+
padding-inline: var(--dsn-skip-link-padding-inline);
23+
24+
/* Afronding */
25+
border-radius: var(--dsn-skip-link-border-radius);
26+
27+
/* Typografie */
28+
font: inherit;
29+
text-decoration: none;
30+
}
31+
32+
/* Focus state — link wordt zichtbaar en gepositioneerd */
33+
.dsn-skip-link:focus-visible {
34+
/* Clip verwijderen zodat de link zichtbaar wordt */
35+
clip-path: none;
36+
overflow: visible;
37+
white-space: normal;
38+
39+
/* Positionering in de hoek van het viewport */
40+
inset-block-start: var(--dsn-skip-link-offset-block-start);
41+
inset-inline-start: var(--dsn-skip-link-offset-inline-start);
42+
43+
/* Focus stijl — consistent met dsn-link focus */
44+
background-color: var(--dsn-focus-background-color);
45+
color: var(--dsn-focus-color);
46+
outline: var(--dsn-focus-outline-width) var(--dsn-focus-outline-style)
47+
var(--dsn-focus-outline-color);
48+
outline-offset: var(--dsn-focus-outline-offset);
49+
box-shadow: 0 0 0
50+
calc(var(--dsn-focus-outline-offset) + var(--dsn-focus-outline-width))
51+
var(--dsn-focus-inverse-outline-color);
52+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
/**
2+
* SkipLink Component Styles for React
3+
* Re-exports the base SkipLink styles from components-html
4+
*/
5+
6+
@import '../../../components-html/src/skip-link/skip-link.css';
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import { describe, it, expect } from 'vitest';
2+
import { render, screen } from '@testing-library/react';
3+
import { SkipLink } from './SkipLink';
4+
5+
describe('SkipLink', () => {
6+
it('renders children', () => {
7+
render(<SkipLink href="#main">Ga direct naar de hoofdinhoud</SkipLink>);
8+
expect(
9+
screen.getByRole('link', { name: 'Ga direct naar de hoofdinhoud' })
10+
).toBeInTheDocument();
11+
});
12+
13+
it('renders as an anchor element', () => {
14+
render(<SkipLink href="#main">Skip</SkipLink>);
15+
expect(screen.getByRole('link').tagName).toBe('A');
16+
});
17+
18+
it('sets href on the anchor', () => {
19+
render(<SkipLink href="#main-content">Skip</SkipLink>);
20+
expect(screen.getByRole('link')).toHaveAttribute('href', '#main-content');
21+
});
22+
23+
it('applies base dsn-skip-link class', () => {
24+
render(<SkipLink href="#main">Skip</SkipLink>);
25+
expect(screen.getByRole('link')).toHaveClass('dsn-skip-link');
26+
});
27+
28+
it('applies custom className', () => {
29+
render(
30+
<SkipLink href="#main" className="custom">
31+
Skip
32+
</SkipLink>
33+
);
34+
expect(screen.getByRole('link')).toHaveClass('dsn-skip-link', 'custom');
35+
});
36+
37+
it('uses default children text when no children provided', () => {
38+
render(<SkipLink href="#main" />);
39+
expect(
40+
screen.getByRole('link', { name: 'Ga direct naar de hoofdinhoud' })
41+
).toBeInTheDocument();
42+
});
43+
44+
it('renders custom children text', () => {
45+
render(<SkipLink href="#main">Sla navigatie over</SkipLink>);
46+
expect(
47+
screen.getByRole('link', { name: 'Sla navigatie over' })
48+
).toBeInTheDocument();
49+
});
50+
51+
it('forwards ref', () => {
52+
const ref = { current: null as HTMLAnchorElement | null };
53+
render(
54+
<SkipLink ref={ref} href="#main">
55+
Skip
56+
</SkipLink>
57+
);
58+
expect(ref.current).toBeInstanceOf(HTMLAnchorElement);
59+
});
60+
61+
it('passes through additional anchor attributes', () => {
62+
render(
63+
<SkipLink href="#main" data-testid="skip-link">
64+
Skip
65+
</SkipLink>
66+
);
67+
expect(screen.getByTestId('skip-link')).toBeInTheDocument();
68+
});
69+
70+
it('passes lang attribute', () => {
71+
render(
72+
<SkipLink href="#main" lang="en">
73+
Skip to main content
74+
</SkipLink>
75+
);
76+
expect(screen.getByRole('link')).toHaveAttribute('lang', 'en');
77+
});
78+
});
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import React from 'react';
2+
import { classNames } from '@dsn/core';
3+
import './SkipLink.css';
4+
5+
export interface SkipLinkProps extends React.AnchorHTMLAttributes<HTMLAnchorElement> {
6+
/**
7+
* Het doel-ID waarnaar de gebruiker gesprongen wordt (inclusief `#`).
8+
* Het element met dit ID moet `tabindex="-1"` hebben als het niet natively focusbaar is.
9+
* @example "#main-content"
10+
*/
11+
href: string;
12+
13+
/**
14+
* Tekst van de skip-link — zichtbaar voor screenreaders en bij focus.
15+
* @default "Ga direct naar de hoofdinhoud"
16+
*/
17+
children?: React.ReactNode;
18+
19+
/**
20+
* Extra CSS klassen
21+
*/
22+
className?: string;
23+
}
24+
25+
/**
26+
* SkipLink — toegankelijkheidskoppeling om herhalende navigatie te omzeilen.
27+
*
28+
* Plaatst de link als **eerste focusbaar element** in de DOM, vóór `<header>` en `<nav>`.
29+
* Voldoet aan WCAG 2.1 succescriterium 2.4.1 (Bypass Blocks, Level A).
30+
*
31+
* @example
32+
* ```tsx
33+
* // Standaard gebruik — eerste element in <body>
34+
* <SkipLink href="#main-content">Ga direct naar de hoofdinhoud</SkipLink>
35+
* <header>...</header>
36+
* <main id="main-content" tabIndex={-1}>...</main>
37+
* ```
38+
*/
39+
export const SkipLink = React.forwardRef<HTMLAnchorElement, SkipLinkProps>(
40+
(
41+
{ href, children = 'Ga direct naar de hoofdinhoud', className, ...props },
42+
ref
43+
) => {
44+
return (
45+
<a
46+
ref={ref}
47+
href={href}
48+
className={classNames('dsn-skip-link', className)}
49+
{...props}
50+
>
51+
{children}
52+
</a>
53+
);
54+
}
55+
);
56+
57+
SkipLink.displayName = 'SkipLink';
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './SkipLink';

packages/components-react/src/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,9 @@ export * from './Details';
6060
// Branding Components
6161
export * from './Logo';
6262

63+
// Accessibility Components
64+
export * from './SkipLink';
65+
6366
// Navigation Components
6467
export * from './BreadcrumbNavigation';
6568
export * from './Menu';

0 commit comments

Comments
 (0)