Skip to content

Commit 0615f49

Browse files
Merge pull request #167 from jeffreylauwers/feature/page-body
feat(PageBody): Storybook documentatie
2 parents 45ba9d3 + 237e12d commit 0615f49

3 files changed

Lines changed: 380 additions & 0 deletions

File tree

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
# PageBody
2+
3+
Structurele inhoudcontainer die de ruimte tussen `PageHeader` en `PageFooter` opvult.
4+
5+
## Doel
6+
7+
`PageBody` is het flex-item met `flex: 1` binnen `PageLayout`. Het zorgt ervoor dat de footer altijd onderaan de viewport staat, ongeacht de hoeveelheid inhoud. In de eenvoudigste template bevat `PageBody` alleen een `<main>`. In complexere templates kan het ook breadcrumbs, een zijnavigatie of andere structurele elementen bevatten die geen onderdeel zijn van `<main>` zelf.
8+
9+
`PageBody` is een transparante structuurlaag zonder eigen visuele stijl.
10+
11+
<!-- VOORBEELD -->
12+
13+
## Use when
14+
15+
- Je de ruimte tussen `PageHeader` en `PageFooter` wilt opvullen zodat de footer altijd onderaan staat.
16+
- Je structurele elementen zoals `<main>`, breadcrumbs of een side navigation wilt groeperen.
17+
- Je een layoutpunt nodig hebt voor complexere paginastructuren.
18+
19+
## Don't use when
20+
21+
- Je `PageBody` buiten een `PageLayout` plaatst: gebruik dan `Stack`, `Grid` of `Container`.
22+
- Je alleen een kaart of sectie wilt opmaken: gebruik dan `Stack` of `Grid`.
23+
24+
## Best practices
25+
26+
### `<main>` en skip-link
27+
28+
De `<main>` met `id="main-content"` is een verantwoordelijkheid van de **template**, niet van `PageBody` zelf. Plaatst `<main id="main-content" tabIndex={-1}>` als directe child zodat de skip-link ernaar kan springen. Het `tabIndex={-1}` is nodig zodat programmatische focus werkt ook al is `<main>` niet natively focusbaar.
29+
30+
```html
31+
<div class="dsn-page-body">
32+
<main id="main-content" tabindex="-1">
33+
<!-- pagina-inhoud -->
34+
</main>
35+
</div>
36+
```
37+
38+
```tsx
39+
<PageBody>
40+
<main id="main-content" tabIndex={-1}>
41+
<Container>...</Container>
42+
</main>
43+
</PageBody>
44+
```
45+
46+
### Toekomstige uitbreiding: side navigation
47+
48+
`dsn-page-body__content` (nog niet geïmplementeerd) biedt straks een inner wrapper voor side nav + main naast elkaar:
49+
50+
```html
51+
<div class="dsn-page-body">
52+
<nav class="dsn-breadcrumbs" aria-label="Kruimelpad">...</nav>
53+
<div class="dsn-page-body__content">
54+
<nav class="dsn-side-nav" aria-label="Sectienavigatie">...</nav>
55+
<main id="main-content" tabindex="-1">...</main>
56+
</div>
57+
</div>
58+
```
59+
60+
## Design tokens
61+
62+
`PageBody` heeft geen component-specifieke tokens. `flex: 1` is een layout-constante.
63+
64+
| Keuze | Reden |
65+
| --------- | ------------------------------------------------------------------------------------------------------------------------------- |
66+
| `flex: 1` | Shorthand voor `flex-grow: 1; flex-shrink: 1; flex-basis: 0%`. Vult alle beschikbare ruimte op zonder vaste hoogte op te geven. |
67+
68+
## Accessibility
69+
70+
### Transparante structuurlaag
71+
72+
`PageBody` heeft geen `role`, `aria-label` of andere ARIA-attributen. Screenreaders navigeren via de children: de `<main>` is het relevante landmark.
73+
74+
### Skip-link bestemming
75+
76+
De skip-link wijst altijd naar de `<main id="main-content">` binnen `PageBody`, niet naar `PageBody` zelf. Zorg dat de `<main>` het `id` heeft en niet de `PageBody`-wrapper.
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { Meta, Story, Markdown } from '@storybook/blocks';
2+
import * as PageBodyStories from './PageBody.stories';
3+
import docs from './PageBody.docs.md?raw';
4+
import { PreviewFrame, CodeTabs } from './components';
5+
6+
export const [intro, rest] = docs.split('<!-- VOORBEELD -->');
7+
8+
<Meta of={PageBodyStories} />
9+
10+
<Markdown>{intro}</Markdown>
11+
12+
## Voorbeeld
13+
14+
<PreviewFrame>
15+
<Story of={PageBodyStories.Default} />
16+
</PreviewFrame>
17+
18+
<CodeTabs
19+
of={PageBodyStories.Default}
20+
react={`<PageBody>
21+
<main id="main-content" tabIndex={-1}>
22+
<Container>...</Container>
23+
</main>
24+
</PageBody>`}
25+
html={`<div class="dsn-page-body">
26+
<main id="main-content" tabindex="-1">
27+
<!-- pagina-inhoud -->
28+
</main>
29+
</div>`}
30+
/>
31+
32+
<Markdown>{rest}</Markdown>
Lines changed: 272 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,272 @@
1+
import React from 'react';
2+
import type { Meta, StoryObj } from '@storybook/react';
3+
import {
4+
Button,
5+
Logo,
6+
Menu,
7+
MenuLink,
8+
PageBody,
9+
PageFooter,
10+
PageHeader,
11+
PageLayout,
12+
Link,
13+
Paragraph,
14+
SearchInput,
15+
SkipLink,
16+
UnorderedList,
17+
} from '@dsn/components-react';
18+
19+
// =============================================================================
20+
// GEDEELDE CONTENT (identiek aan PageLayout/PageHeader/PageFooter stories)
21+
// =============================================================================
22+
23+
const logoSlot = (
24+
<a href="/">
25+
<Logo aria-hidden={true} />
26+
<span className="dsn-visually-hidden">
27+
Starter Kit — terug naar homepage
28+
</span>
29+
</a>
30+
);
31+
32+
function PrimaryNavigation() {
33+
const [exp1b, setExp1b] = React.useState(false);
34+
const [exp2b, setExp2b] = React.useState(false);
35+
const [exp3b, setExp3b] = React.useState(false);
36+
37+
return (
38+
<Menu orientation="vertical">
39+
<MenuLink href="/level-1a" level={1} current>
40+
Level 1a
41+
</MenuLink>
42+
<MenuLink
43+
href="/level-1b"
44+
level={1}
45+
subItems
46+
expanded={exp1b}
47+
onExpandToggle={() => setExp1b((v) => !v)}
48+
>
49+
Level 1b
50+
</MenuLink>
51+
{exp1b && (
52+
<>
53+
<MenuLink href="/level-2a" level={2}>
54+
Level 2a
55+
</MenuLink>
56+
<MenuLink
57+
href="/level-2b"
58+
level={2}
59+
subItems
60+
expanded={exp2b}
61+
onExpandToggle={() => setExp2b((v) => !v)}
62+
>
63+
Level 2b
64+
</MenuLink>
65+
{exp2b && (
66+
<>
67+
<MenuLink href="/level-3a" level={3}>
68+
Level 3a
69+
</MenuLink>
70+
<MenuLink
71+
href="/level-3b"
72+
level={3}
73+
subItems
74+
expanded={exp3b}
75+
onExpandToggle={() => setExp3b((v) => !v)}
76+
>
77+
Level 3b
78+
</MenuLink>
79+
{exp3b && (
80+
<>
81+
<MenuLink href="/level-4a" level={4}>
82+
Level 4a
83+
</MenuLink>
84+
<MenuLink href="/level-4b" level={4}>
85+
Level 4b
86+
</MenuLink>
87+
</>
88+
)}
89+
<MenuLink href="/level-3c" level={3}>
90+
Level 3c
91+
</MenuLink>
92+
<MenuLink href="/level-3d" level={3}>
93+
Level 3d
94+
</MenuLink>
95+
</>
96+
)}
97+
<MenuLink href="/level-2c" level={2}>
98+
Level 2c
99+
</MenuLink>
100+
<MenuLink href="/level-2d" level={2}>
101+
Level 2d
102+
</MenuLink>
103+
</>
104+
)}
105+
<MenuLink href="/level-1c" level={1}>
106+
Level 1c
107+
</MenuLink>
108+
<MenuLink href="/level-1d" level={1}>
109+
Level 1d
110+
</MenuLink>
111+
</Menu>
112+
);
113+
}
114+
115+
const primaryNavigationLarge = (
116+
<Menu orientation="horizontal">
117+
<MenuLink href="/level-1a" level={1} current>
118+
Level 1a
119+
</MenuLink>
120+
<MenuLink href="/level-1b" level={1}>
121+
Level 1b
122+
</MenuLink>
123+
<MenuLink href="/level-1c" level={1}>
124+
Level 1c
125+
</MenuLink>
126+
<MenuLink href="/level-1d" level={1}>
127+
Level 1d
128+
</MenuLink>
129+
</Menu>
130+
);
131+
132+
const secondaryNavigation = (
133+
<Menu orientation="vertical">
134+
<MenuLink href="/english" level={1}>
135+
English
136+
</MenuLink>
137+
<MenuLink href="/mijn-omgeving" level={1}>
138+
Mijn omgeving
139+
</MenuLink>
140+
</Menu>
141+
);
142+
143+
const secondaryNavigationLarge = (
144+
<Menu orientation="horizontal">
145+
<MenuLink href="/english" level={1}>
146+
English
147+
</MenuLink>
148+
<MenuLink href="/mijn-omgeving" level={1}>
149+
Mijn omgeving
150+
</MenuLink>
151+
</Menu>
152+
);
153+
154+
const searchSlot = (
155+
<>
156+
<SearchInput placeholder="Zoeken…" aria-label="Zoekopdracht" />
157+
<Button variant="strong">Zoeken</Button>
158+
</>
159+
);
160+
161+
const footerSlot1 = (
162+
<a href="/">
163+
<Logo aria-hidden={true} />
164+
<span className="dsn-visually-hidden">
165+
Starter Kit — terug naar homepage
166+
</span>
167+
</a>
168+
);
169+
170+
const footerSlot2 = (
171+
<Paragraph>
172+
Dit is een voorbeeldorganisatie. <Link href="/about">Meer informatie</Link>.
173+
</Paragraph>
174+
);
175+
176+
const footerSlot3 = (
177+
<UnorderedList>
178+
<li>
179+
<Link href="/nieuws">Nieuws</Link>
180+
</li>
181+
<li>
182+
<Link href="/over-ons">Over ons</Link>
183+
</li>
184+
<li>
185+
<Link href="/werken-bij">Werken bij</Link>
186+
</li>
187+
<li>
188+
<Link href="/klachten">Klachten</Link>
189+
</li>
190+
</UnorderedList>
191+
);
192+
193+
const footerSlot4 = (
194+
<UnorderedList>
195+
<li>
196+
<Link href="/privacy">Privacyverklaring</Link>
197+
</li>
198+
<li>
199+
<Link href="/accessibility">Toegankelijkheid</Link>
200+
</li>
201+
<li>
202+
<Link href="/cookies">Cookies</Link>
203+
</li>
204+
<li>
205+
<Link href="/contact">Contact</Link>
206+
</li>
207+
</UnorderedList>
208+
);
209+
210+
// =============================================================================
211+
// META
212+
// =============================================================================
213+
214+
const meta: Meta<typeof PageBody> = {
215+
title: 'Components/PageBody',
216+
component: PageBody,
217+
parameters: {
218+
layout: 'fullscreen',
219+
dsn: {
220+
htmlTemplate: () => `<div class="dsn-page-body">
221+
<main id="main-content" tabindex="-1">
222+
<!-- pagina-inhoud -->
223+
</main>
224+
</div>`,
225+
},
226+
},
227+
argTypes: {
228+
className: { control: false },
229+
children: { control: false },
230+
},
231+
};
232+
233+
export default meta;
234+
235+
type Story = StoryObj<typeof PageBody>;
236+
237+
// =============================================================================
238+
// STORIES
239+
// =============================================================================
240+
241+
export const Default: Story = {
242+
name: 'Default',
243+
render: () => (
244+
<>
245+
<SkipLink href="#main-content" />
246+
<PageLayout>
247+
<PageHeader
248+
logoSlot={logoSlot}
249+
primaryNavigation={<PrimaryNavigation />}
250+
primaryNavigationLarge={primaryNavigationLarge}
251+
secondaryNavigation={secondaryNavigation}
252+
secondaryNavigationLarge={secondaryNavigationLarge}
253+
searchSlot={searchSlot}
254+
/>
255+
<PageBody>
256+
<main id="main-content" tabIndex={-1} style={{ padding: '2rem' }}>
257+
<p>
258+
Paginainhoud staat hier. De footer staat altijd onderaan de
259+
viewport.
260+
</p>
261+
</main>
262+
</PageBody>
263+
<PageFooter
264+
slot1={footerSlot1}
265+
slot2={footerSlot2}
266+
slot3={footerSlot3}
267+
slot4={footerSlot4}
268+
/>
269+
</PageLayout>
270+
</>
271+
),
272+
};

0 commit comments

Comments
 (0)