Skip to content

Commit 7a61e29

Browse files
Merge pull request #162 from jeffreylauwers/feature/page-footer
feat(PageFooter): paginavoettekst met 4-koloms grid en inverse colorScheme
2 parents a6f7a0e + 821666f commit 7a61e29

15 files changed

Lines changed: 1046 additions & 21 deletions

File tree

docs/03-components.md

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

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

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

@@ -1697,7 +1697,7 @@ const [isOpen, setIsOpen] = React.useState(false);
16971697

16981698
## Navigation Components
16991699

1700-
**Status:** Complete (HTML/CSS, React): 5 components total
1700+
**Status:** Complete (HTML/CSS, React): 6 components total
17011701

17021702
### Menu
17031703

@@ -2237,6 +2237,130 @@ const [isOpen, setIsOpen] = React.useState(false);
22372237

22382238
---
22392239

2240+
### PageFooter
2241+
2242+
**Status:** Complete (HTML/CSS, React)
2243+
2244+
**Location:** `packages/components-{html|react}/src/page-footer/` / `packages/components-react/src/PageFooter/`
2245+
2246+
**Tokens:** `tokens/components/page-footer.json`
2247+
2248+
**Props:** `logoSlot`, `secondarySlot`, `contentSlot`, `linksSlot`, `colorScheme` (`'default'` | `'inverse'`), `className`
2249+
2250+
**Features:**
2251+
2252+
- Paginavoettekst met `accent-1` achtergrond en dikke `border-block-start` (4px): visuele tegenhanger van de `PageHeader border-block-end`
2253+
- `<footer>` element met impliciet `role="contentinfo"` landmark — geen extra attribuut nodig
2254+
- 4-koloms grid via `dsn-grid` + `dsn-col-12 dsn-col-lg-3`: vier gelijke slots naast elkaar op ≥ 64em, verticaal gestapeld op mobiel
2255+
- `row-gap` tussen gestapelde slots op mobiel via `--dsn-page-footer-slot-gap`
2256+
- `colorScheme="inverse"`: schakelt naar `accent-1-inverse` achtergrond; tekst-, link- en logokleuren worden via CSS custom property overrides automatisch aangepast voor contrast
2257+
- Logo-slot: `max-block-size` via `--dsn-page-footer-logo-max-block-size` (2rem); `--dsn-logo-color-primary` en `--dsn-logo-color-label` omgedraaid bij inverse voor correct doorkijkje-effect
2258+
- `secondarySlot` (slot 2): optioneel; verdwijnt op mobiel via `:empty { display: none }` als leeg
2259+
2260+
**CSS-klassen:**
2261+
2262+
| Klasse | Element | Beschrijving |
2263+
| ----------------------------- | ---------- | -------------------------------------------------------- |
2264+
| `dsn-page-footer` | `<footer>` | Rootblok: accent-1 achtergrond, dikke border-block-start |
2265+
| `dsn-page-footer--inverse` | `<footer>` | Modifier: accent-1-inverse kleurenschaal |
2266+
| `dsn-page-footer__inner` | `<div>` | Padding-container voor het grid |
2267+
| `dsn-page-footer__empty-slot` | `<div>` | Optioneel slot 2: verborgen via `:empty` wanneer leeg |
2268+
2269+
**Design tokens:**
2270+
2271+
| Token | Waarde | Beschrijving |
2272+
| -------------------------------------------- | ------------------------------------- | ---------------------------------------- |
2273+
| `--dsn-page-footer-background-color` | `{dsn.color.accent-1.bg-default}` | Achtergrondkleur (default) |
2274+
| `--dsn-page-footer-border-block-start-width` | `{dsn.border.width.thick}` | Breedte topborder (4px) |
2275+
| `--dsn-page-footer-border-block-start-color` | `{dsn.color.accent-1.border-default}` | Kleur topborder (default) |
2276+
| `--dsn-page-footer-padding-block` | `{dsn.space.block.6xl}` | Verticale padding boven en onder (64px) |
2277+
| `--dsn-page-footer-padding-inline` | `{dsn.space.inline.xl}` | Horizontale padding |
2278+
| `--dsn-page-footer-slot-gap` | `{dsn.space.block.xl}` | Verticale ruimte tussen gestapelde slots |
2279+
| `--dsn-page-footer-logo-max-block-size` | `2rem` | Maximale logohoogte (32px) |
2280+
2281+
**Usage:**
2282+
2283+
```html
2284+
<!-- HTML/CSS -->
2285+
<footer class="dsn-page-footer">
2286+
<div class="dsn-page-footer__inner">
2287+
<div class="dsn-grid">
2288+
<div class="dsn-col-12 dsn-col-lg-3">
2289+
<a href="/">
2290+
<svg class="dsn-logo" aria-hidden="true"><!-- logo --></svg>
2291+
<span class="dsn-visually-hidden"
2292+
>Naam organisatie — terug naar homepage</span
2293+
>
2294+
</a>
2295+
</div>
2296+
<div class="dsn-col-12 dsn-col-lg-3 dsn-page-footer__empty-slot">
2297+
<p class="dsn-paragraph">
2298+
Korte beschrijving.
2299+
<a class="dsn-link" href="/about">Meer informatie</a>.
2300+
</p>
2301+
</div>
2302+
<div class="dsn-col-12 dsn-col-lg-3">
2303+
<ul class="dsn-unordered-list">
2304+
<li><a class="dsn-link" href="/nieuws">Nieuws</a></li>
2305+
<li><a class="dsn-link" href="/over-ons">Over ons</a></li>
2306+
</ul>
2307+
</div>
2308+
<div class="dsn-col-12 dsn-col-lg-3">
2309+
<ul class="dsn-unordered-list">
2310+
<li><a class="dsn-link" href="/privacy">Privacyverklaring</a></li>
2311+
<li>
2312+
<a class="dsn-link" href="/accessibility">Toegankelijkheid</a>
2313+
</li>
2314+
</ul>
2315+
</div>
2316+
</div>
2317+
</div>
2318+
</footer>
2319+
```
2320+
2321+
```tsx
2322+
// React
2323+
<PageFooter
2324+
logoSlot={
2325+
<a href="/">
2326+
<Logo aria-hidden={true} />
2327+
<span className="dsn-visually-hidden">
2328+
Naam organisatie — terug naar homepage
2329+
</span>
2330+
</a>
2331+
}
2332+
secondarySlot={
2333+
<Paragraph>
2334+
Korte beschrijving. <Link href="/about">Meer informatie</Link>.
2335+
</Paragraph>
2336+
}
2337+
contentSlot={
2338+
<UnorderedList>
2339+
<li>
2340+
<Link href="/nieuws">Nieuws</Link>
2341+
</li>
2342+
<li>
2343+
<Link href="/over-ons">Over ons</Link>
2344+
</li>
2345+
</UnorderedList>
2346+
}
2347+
linksSlot={
2348+
<UnorderedList>
2349+
<li>
2350+
<Link href="/privacy">Privacyverklaring</Link>
2351+
</li>
2352+
<li>
2353+
<Link href="/accessibility">Toegankelijkheid</Link>
2354+
</li>
2355+
</UnorderedList>
2356+
}
2357+
/>
2358+
```
2359+
2360+
**Tests:** React (11 tests)
2361+
2362+
---
2363+
22402364
## Branding Components
22412365

22422366
**Status:** Complete (HTML/CSS, React): 1 component total

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.26.0 (April 17, 2026)
10+
11+
### PageFooter component (issue #161, PR #162)
12+
13+
#### Added
14+
15+
- **PageFooter** component: paginavoettekst met `accent-1` achtergrond, dikke `border-block-start` (4px) en een responsive 4-koloms grid (PR #162)
16+
- `<footer>` element met impliciet `role="contentinfo"` landmark
17+
- 4 slots: `logoSlot` (slot 1), `secondarySlot` (slot 2, optioneel), `contentSlot` (slot 3), `linksSlot` (slot 4)
18+
- Op mobiel: slots verticaal gestapeld met `row-gap` via `--dsn-page-footer-slot-gap`; leeg `secondarySlot` verborgen via `:empty { display: none }`
19+
- Op ≥ 64em: vier gelijke kolommen naast elkaar via `dsn-col-12 dsn-col-lg-3`
20+
- `colorScheme="inverse"` modifier: schakelt naar `accent-1-inverse` achtergrond; tekst-, link- en logokleuren via CSS custom property overrides; `--dsn-logo-color-label` mee omgedraaid voor correct doorkijkje-effect
21+
- 7 design tokens in `tokens/components/page-footer.json`: `background-color`, `border-block-start-width`, `border-block-start-color`, `padding-block`, `padding-inline`, `slot-gap`, `logo.max-block-size`
22+
- 11 tests, 3 Storybook bestanden
23+
24+
---
25+
926
## Version 5.25.0 (April 14, 2026)
1027

1128
### Popover component (issue #155, PR #156)
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
/**
2+
* PageFooter Component
3+
* Paginavoettekst met accent-1 achtergrond, dikke border-block-start en
4+
* een 4-koloms grid. Mobile-first: slots stapelen verticaal; op grote
5+
* viewports (≥ 64em) vier gelijke kolommen naast elkaar.
6+
*
7+
* Structuur:
8+
* <footer class="dsn-page-footer">
9+
* <div class="dsn-page-footer__inner">
10+
* <div class="dsn-grid">
11+
* <div class="dsn-col-12 dsn-col-lg-3"><!-- logo slot --></div>
12+
* <div class="dsn-col-12 dsn-col-lg-3"><!-- slot 2 (optioneel) --></div>
13+
* <div class="dsn-col-12 dsn-col-lg-3"><!-- content slot --></div>
14+
* <div class="dsn-col-12 dsn-col-lg-3"><!-- links slot --></div>
15+
* </div>
16+
* </div>
17+
* </footer>
18+
*
19+
* <!-- Inverse colorScheme -->
20+
* <footer class="dsn-page-footer dsn-page-footer--inverse">...</footer>
21+
*/
22+
23+
/* =============================================================================
24+
Base
25+
============================================================================= */
26+
27+
.dsn-page-footer {
28+
background-color: var(--dsn-page-footer-background-color);
29+
border-block-start: var(--dsn-page-footer-border-block-start-width) solid
30+
var(--dsn-page-footer-border-block-start-color);
31+
}
32+
33+
/* =============================================================================
34+
Inner container — max-breedte + padding
35+
============================================================================= */
36+
37+
.dsn-page-footer__inner {
38+
padding-block: var(--dsn-page-footer-padding-block);
39+
padding-inline: var(--dsn-page-footer-padding-inline);
40+
}
41+
42+
/* =============================================================================
43+
Logo — max-block-size op directe child van het logo-slot
44+
============================================================================= */
45+
46+
.dsn-page-footer .dsn-logo {
47+
max-block-size: var(--dsn-page-footer-logo-max-block-size);
48+
inline-size: auto;
49+
}
50+
51+
/* =============================================================================
52+
Grid slot-gap — verticale ruimte tussen gestapelde slots op small viewport.
53+
Op large viewport (4 kolommen naast elkaar) is row-gap visueel niet storend.
54+
============================================================================= */
55+
56+
.dsn-page-footer .dsn-grid {
57+
row-gap: var(--dsn-page-footer-slot-gap);
58+
}
59+
60+
/* =============================================================================
61+
Inverse modifier — schakelt naar accent-1-inverse kleurenschaal
62+
============================================================================= */
63+
64+
.dsn-page-footer--inverse {
65+
--dsn-page-footer-background-color: var(
66+
--dsn-color-accent-1-inverse-bg-default
67+
);
68+
--dsn-page-footer-border-block-start-color: var(
69+
--dsn-color-accent-1-inverse-border-default
70+
);
71+
}
72+
73+
/* Tekst- en linkkleur-overrides op inverse achtergrond */
74+
.dsn-page-footer--inverse {
75+
--dsn-paragraph-color: var(--dsn-color-accent-1-inverse-color-default);
76+
--dsn-link-color: var(--dsn-color-accent-1-inverse-color-default);
77+
--dsn-link-text-decoration-color: var(
78+
--dsn-color-accent-1-inverse-color-default
79+
);
80+
--dsn-link-hover-color: var(--dsn-color-accent-1-inverse-color-hover);
81+
--dsn-unordered-list-color: var(--dsn-color-accent-1-inverse-color-default);
82+
--dsn-unordered-list-marker-color: var(
83+
--dsn-color-accent-1-inverse-color-default
84+
);
85+
}
86+
87+
/* Logo-kleuren aanpassen op inverse achtergrond:
88+
primary → wit (logo-rand zichtbaar op donkere achtergrond)
89+
label → donkerblauw (doorkijkje-effect: letters tonen de achtergrondkleur) */
90+
.dsn-page-footer--inverse .dsn-logo {
91+
--dsn-logo-color-primary: var(--dsn-color-accent-1-inverse-color-default);
92+
--dsn-logo-color-label: var(--dsn-color-accent-1-inverse-bg-default);
93+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
@import '../../../components-html/src/page-footer/page-footer.css';
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
import { describe, it, expect } from 'vitest';
2+
import { render } from '@testing-library/react';
3+
import { PageFooter } from './PageFooter';
4+
5+
const defaultLogo = (
6+
<a href="/">
7+
<span>Logo</span>
8+
<span className="dsn-visually-hidden">
9+
Starter Kit — terug naar homepage
10+
</span>
11+
</a>
12+
);
13+
14+
describe('PageFooter', () => {
15+
// ---------------------------------------------------------------------------
16+
// Structuur
17+
// ---------------------------------------------------------------------------
18+
19+
it('rendert een <footer>-element', () => {
20+
const { container } = render(<PageFooter slot1={defaultLogo} />);
21+
expect(container.querySelector('footer')).toBeTruthy();
22+
});
23+
24+
it('heeft de basis dsn-page-footer klasse', () => {
25+
const { container } = render(<PageFooter slot1={defaultLogo} />);
26+
expect(container.querySelector('footer')).toHaveClass('dsn-page-footer');
27+
});
28+
29+
it('rendert de inner-wrapper met dsn-page-footer__inner', () => {
30+
const { container } = render(<PageFooter slot1={defaultLogo} />);
31+
expect(container.querySelector('.dsn-page-footer__inner')).toBeTruthy();
32+
});
33+
34+
it('rendert een grid (dsn-grid) binnen de inner-wrapper', () => {
35+
const { container } = render(<PageFooter slot1={defaultLogo} />);
36+
expect(
37+
container.querySelector('.dsn-page-footer__inner .dsn-grid')
38+
).toBeTruthy();
39+
});
40+
41+
it('rendert slot1 in de footer', () => {
42+
const { container } = render(<PageFooter slot1={defaultLogo} />);
43+
expect(container.querySelector('footer a[href="/"]')).toBeTruthy();
44+
});
45+
46+
// ---------------------------------------------------------------------------
47+
// colorScheme
48+
// ---------------------------------------------------------------------------
49+
50+
it('heeft geen inverse modifier bij colorScheme default', () => {
51+
const { container } = render(<PageFooter slot1={defaultLogo} />);
52+
expect(container.querySelector('footer')).not.toHaveClass(
53+
'dsn-page-footer--inverse'
54+
);
55+
});
56+
57+
it('heeft de inverse modifier bij colorScheme inverse', () => {
58+
const { container } = render(
59+
<PageFooter slot1={defaultLogo} colorScheme="inverse" />
60+
);
61+
expect(container.querySelector('footer')).toHaveClass(
62+
'dsn-page-footer--inverse'
63+
);
64+
});
65+
66+
// ---------------------------------------------------------------------------
67+
// Slots
68+
// ---------------------------------------------------------------------------
69+
70+
it('rendert slot3 als dat meegegeven wordt', () => {
71+
const { container } = render(
72+
<PageFooter slot1={defaultLogo} slot3={<p>Lorem ipsum</p>} />
73+
);
74+
expect(container.querySelector('footer p')).toBeTruthy();
75+
});
76+
77+
it('rendert slot4 als dat meegegeven wordt', () => {
78+
const { container } = render(
79+
<PageFooter
80+
slot1={defaultLogo}
81+
slot4={
82+
<ul>
83+
<li>
84+
<a href="/privacy">Privacyverklaring</a>
85+
</li>
86+
</ul>
87+
}
88+
/>
89+
);
90+
expect(container.querySelector('footer ul')).toBeTruthy();
91+
expect(container.querySelector('footer a[href="/privacy"]')).toBeTruthy();
92+
});
93+
94+
it('rendert slot2 als dat meegegeven wordt', () => {
95+
const { container } = render(
96+
<PageFooter
97+
slot1={defaultLogo}
98+
slot2={<span data-testid="secondary">Extra</span>}
99+
/>
100+
);
101+
expect(container.querySelector('[data-testid="secondary"]')).toBeTruthy();
102+
});
103+
104+
// ---------------------------------------------------------------------------
105+
// className en ref
106+
// ---------------------------------------------------------------------------
107+
108+
it('voegt extra className toe aan het root element', () => {
109+
const { container } = render(
110+
<PageFooter slot1={defaultLogo} className="extra-class" />
111+
);
112+
expect(container.querySelector('footer')).toHaveClass('extra-class');
113+
});
114+
115+
it('stuurt HTML-attributen door naar het <footer>-element', () => {
116+
const { container } = render(
117+
<PageFooter slot1={defaultLogo} data-testid="page-footer" />
118+
);
119+
expect(container.querySelector('[data-testid="page-footer"]')).toBeTruthy();
120+
});
121+
122+
it('geeft ref door naar het <footer>-element', () => {
123+
const ref = { current: null as HTMLElement | null };
124+
render(<PageFooter slot1={defaultLogo} ref={ref} />);
125+
expect(ref.current).not.toBeNull();
126+
expect(ref.current?.tagName.toLowerCase()).toBe('footer');
127+
});
128+
});

0 commit comments

Comments
 (0)