Skip to content

Commit 9fb8ceb

Browse files
Merge pull request #47 from jeffreylauwers/feature/alert
feat(Alert): compact bericht met signaalkleur (#37)
2 parents 6be5393 + 69e7fd3 commit 9fb8ceb

11 files changed

Lines changed: 903 additions & 2 deletions

File tree

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
/**
2+
* Alert Component
3+
* Important message that informs the user about the current activity or state.
4+
*
5+
* Usage:
6+
* <!-- Info variant (default), with icon -->
7+
* <div class="dsn-alert" role="alert">
8+
* <svg class="dsn-icon dsn-alert__icon" aria-hidden="true"><!-- info-circle --></svg>
9+
* <strong class="dsn-alert__heading">Heading</strong>
10+
* <div class="dsn-alert__content">
11+
* <p>Body text or any content.</p>
12+
* </div>
13+
* </div>
14+
*
15+
* <!-- Positive variant -->
16+
* <div class="dsn-alert dsn-alert--positive" role="alert">
17+
* <svg class="dsn-icon dsn-alert__icon" aria-hidden="true"><!-- circle-check --></svg>
18+
* <strong class="dsn-alert__heading">Gelukt</strong>
19+
* <div class="dsn-alert__content">
20+
* <p>Uw aanvraag is succesvol ingediend.</p>
21+
* </div>
22+
* </div>
23+
*
24+
* <!-- Negative variant -->
25+
* <div class="dsn-alert dsn-alert--negative" role="alert">
26+
* <svg class="dsn-icon dsn-alert__icon" aria-hidden="true"><!-- exclamation-circle --></svg>
27+
* <strong class="dsn-alert__heading">Er is een fout opgetreden</strong>
28+
* <div class="dsn-alert__content">
29+
* <p>Controleer uw gegevens en probeer het opnieuw.</p>
30+
* </div>
31+
* </div>
32+
*
33+
* <!-- Warning variant -->
34+
* <div class="dsn-alert dsn-alert--warning" role="alert">
35+
* <svg class="dsn-icon dsn-alert__icon" aria-hidden="true"><!-- alert-triangle --></svg>
36+
* <strong class="dsn-alert__heading">Let op</strong>
37+
* <div class="dsn-alert__content">
38+
* <p>Uw sessie verloopt binnenkort.</p>
39+
* </div>
40+
* </div>
41+
*
42+
* <!-- Without icon -->
43+
* <div class="dsn-alert dsn-alert--no-icon" role="alert">
44+
* <strong class="dsn-alert__heading">Heading</strong>
45+
* <div class="dsn-alert__content">
46+
* <p>Body text without an icon.</p>
47+
* </div>
48+
* </div>
49+
*/
50+
51+
/* ===========================
52+
Local color tokens (info = default)
53+
=========================== */
54+
.dsn-alert {
55+
--dsn-alert-icon-color: var(--dsn-color-info-color-default);
56+
--dsn-alert-color: var(--dsn-color-info-color-document);
57+
--dsn-alert-background-color: var(--dsn-color-info-bg-default);
58+
--dsn-alert-border-color: var(--dsn-color-info-border-default);
59+
}
60+
61+
.dsn-alert--positive {
62+
--dsn-alert-icon-color: var(--dsn-color-positive-color-default);
63+
--dsn-alert-color: var(--dsn-color-positive-color-document);
64+
--dsn-alert-background-color: var(--dsn-color-positive-bg-default);
65+
--dsn-alert-border-color: var(--dsn-color-positive-border-default);
66+
}
67+
68+
.dsn-alert--negative {
69+
--dsn-alert-icon-color: var(--dsn-color-negative-color-default);
70+
--dsn-alert-color: var(--dsn-color-negative-color-document);
71+
--dsn-alert-background-color: var(--dsn-color-negative-bg-default);
72+
--dsn-alert-border-color: var(--dsn-color-negative-border-default);
73+
}
74+
75+
.dsn-alert--warning {
76+
--dsn-alert-icon-color: var(--dsn-color-warning-color-default);
77+
--dsn-alert-color: var(--dsn-color-warning-color-document);
78+
--dsn-alert-background-color: var(--dsn-color-warning-bg-default);
79+
--dsn-alert-border-color: var(--dsn-color-warning-border-default);
80+
}
81+
82+
/* ===========================
83+
Base layout
84+
=========================== */
85+
.dsn-alert {
86+
display: grid;
87+
grid-template-columns: var(--dsn-icon-size-xl) 1fr;
88+
column-gap: var(--dsn-alert-column-gap);
89+
row-gap: var(--dsn-alert-row-gap);
90+
padding-block: var(--dsn-alert-padding-block);
91+
padding-inline: var(--dsn-alert-padding-inline);
92+
border-radius: var(--dsn-alert-border-radius);
93+
border: var(--dsn-alert-border-width) solid var(--dsn-alert-border-color);
94+
background-color: var(--dsn-alert-background-color);
95+
color: var(--dsn-alert-color);
96+
}
97+
98+
/* ===========================
99+
Icon
100+
=========================== */
101+
.dsn-alert__icon {
102+
grid-column: 1;
103+
grid-row: 1;
104+
align-self: center;
105+
flex-shrink: 0;
106+
width: var(--dsn-icon-size-xl);
107+
height: var(--dsn-icon-size-xl);
108+
color: var(--dsn-alert-icon-color);
109+
}
110+
111+
/* ===========================
112+
Heading
113+
=========================== */
114+
.dsn-alert__heading {
115+
grid-column: 2;
116+
grid-row: 1;
117+
align-self: center;
118+
}
119+
120+
/* ===========================
121+
Content
122+
=========================== */
123+
.dsn-alert__content {
124+
grid-column: 2;
125+
grid-row: 2;
126+
}
127+
128+
/* ===========================
129+
No-icon modifier
130+
=========================== */
131+
.dsn-alert--no-icon {
132+
grid-template-columns: 1fr;
133+
}
134+
135+
.dsn-alert--no-icon .dsn-alert__heading,
136+
.dsn-alert--no-icon .dsn-alert__content {
137+
grid-column: 1;
138+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
/**
2+
* Alert Component Styles for React
3+
* Re-exports the base Alert styles from components-html
4+
*/
5+
6+
@import '../../../components-html/src/alert/alert.css';
Lines changed: 219 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,219 @@
1+
import { describe, it, expect } from 'vitest';
2+
import { render, screen } from '@testing-library/react';
3+
import { Alert } from './Alert';
4+
5+
describe('Alert', () => {
6+
// ===========================
7+
// Rendering
8+
// ===========================
9+
10+
it('renders heading', () => {
11+
render(<Alert heading="Uw aanvraag wordt verwerkt" />);
12+
expect(screen.getByText('Uw aanvraag wordt verwerkt')).toBeInTheDocument();
13+
});
14+
15+
it('renders children content', () => {
16+
render(
17+
<Alert heading="Bericht">
18+
<p>Dit kan enkele minuten duren.</p>
19+
</Alert>
20+
);
21+
expect(
22+
screen.getByText('Dit kan enkele minuten duren.')
23+
).toBeInTheDocument();
24+
});
25+
26+
it('renders without children', () => {
27+
render(<Alert heading="Alleen heading" />);
28+
expect(screen.getByText('Alleen heading')).toBeInTheDocument();
29+
expect(screen.queryByRole('region')).not.toBeInTheDocument();
30+
});
31+
32+
it('renders as a <div> element', () => {
33+
render(<Alert heading="Test" />);
34+
const alert = screen.getByRole('alert');
35+
expect(alert.tagName).toBe('DIV');
36+
});
37+
38+
it('has role="alert"', () => {
39+
render(<Alert heading="Test" />);
40+
expect(screen.getByRole('alert')).toBeInTheDocument();
41+
});
42+
43+
// ===========================
44+
// Classes
45+
// ===========================
46+
47+
it('always has base dsn-alert class', () => {
48+
render(<Alert heading="Test" />);
49+
expect(screen.getByRole('alert')).toHaveClass('dsn-alert');
50+
});
51+
52+
it('does not add variant modifier class for info variant (default)', () => {
53+
render(<Alert heading="Test" />);
54+
const el = screen.getByRole('alert');
55+
expect(el).not.toHaveClass('dsn-alert--info');
56+
});
57+
58+
it('does not add variant modifier class for explicit info variant', () => {
59+
render(<Alert variant="info" heading="Test" />);
60+
const el = screen.getByRole('alert');
61+
expect(el).not.toHaveClass('dsn-alert--info');
62+
});
63+
64+
it.each(['positive', 'negative', 'warning'] as const)(
65+
'applies variant modifier class for %s variant',
66+
(variant) => {
67+
render(<Alert variant={variant} heading="Test" />);
68+
expect(screen.getByRole('alert')).toHaveClass(`dsn-alert--${variant}`);
69+
}
70+
);
71+
72+
it('applies custom className', () => {
73+
render(<Alert heading="Test" className="custom-alert" />);
74+
const el = screen.getByRole('alert');
75+
expect(el).toHaveClass('dsn-alert');
76+
expect(el).toHaveClass('custom-alert');
77+
});
78+
79+
// ===========================
80+
// Heading
81+
// ===========================
82+
83+
it('renders heading as h2 by default', () => {
84+
render(<Alert heading="Standaard heading" />);
85+
expect(
86+
screen.getByRole('heading', { level: 2, name: 'Standaard heading' })
87+
).toBeInTheDocument();
88+
});
89+
90+
it.each([1, 2, 3, 4, 5, 6] as const)(
91+
'renders heading at level %i when headingLevel=%i',
92+
(level) => {
93+
render(<Alert heading="Heading" headingLevel={level} />);
94+
expect(
95+
screen.getByRole('heading', { level, name: 'Heading' })
96+
).toBeInTheDocument();
97+
}
98+
);
99+
100+
it('applies dsn-alert__heading class to heading', () => {
101+
render(<Alert heading="Heading" />);
102+
const heading = screen.getByRole('heading');
103+
expect(heading).toHaveClass('dsn-alert__heading');
104+
});
105+
106+
// ===========================
107+
// Icon
108+
// ===========================
109+
110+
it('renders preferred icon by default (info-circle for info)', () => {
111+
render(<Alert variant="info" heading="Info" />);
112+
const iconSpan = document.querySelector('.dsn-alert__icon');
113+
expect(iconSpan).toBeInTheDocument();
114+
});
115+
116+
it('renders preferred icon for each variant', () => {
117+
const variants = ['info', 'positive', 'negative', 'warning'] as const;
118+
for (const variant of variants) {
119+
const { unmount } = render(<Alert variant={variant} heading="Test" />);
120+
expect(document.querySelector('.dsn-alert__icon')).toBeInTheDocument();
121+
unmount();
122+
}
123+
});
124+
125+
it('renders no icon when iconStart={null}', () => {
126+
render(<Alert heading="Zonder icoon" iconStart={null} />);
127+
expect(document.querySelector('.dsn-alert__icon')).not.toBeInTheDocument();
128+
});
129+
130+
it('adds dsn-alert--no-icon class when iconStart={null}', () => {
131+
render(<Alert heading="Zonder icoon" iconStart={null} />);
132+
expect(screen.getByRole('alert')).toHaveClass('dsn-alert--no-icon');
133+
});
134+
135+
it('does not add dsn-alert--no-icon class when icon is shown', () => {
136+
render(<Alert heading="Met icoon" />);
137+
expect(screen.getByRole('alert')).not.toHaveClass('dsn-alert--no-icon');
138+
});
139+
140+
it('renders custom icon when iconStart is provided as ReactNode', () => {
141+
render(
142+
<Alert
143+
heading="Custom icoon"
144+
iconStart={<svg data-testid="custom-icon" />}
145+
/>
146+
);
147+
expect(screen.getByTestId('custom-icon')).toBeInTheDocument();
148+
const iconSpan = document.querySelector('.dsn-alert__icon');
149+
expect(iconSpan).toBeInTheDocument();
150+
});
151+
152+
// ===========================
153+
// Content
154+
// ===========================
155+
156+
it('wraps children in dsn-alert__content div', () => {
157+
render(
158+
<Alert heading="Test">
159+
<p>Inhoud</p>
160+
</Alert>
161+
);
162+
const content = document.querySelector('.dsn-alert__content');
163+
expect(content).toBeInTheDocument();
164+
expect(content?.querySelector('p')).toBeInTheDocument();
165+
});
166+
167+
it('does not render dsn-alert__content when no children', () => {
168+
render(<Alert heading="Test" />);
169+
expect(
170+
document.querySelector('.dsn-alert__content')
171+
).not.toBeInTheDocument();
172+
});
173+
174+
// ===========================
175+
// Ref + HTML attributes
176+
// ===========================
177+
178+
it('forwards ref to the div element', () => {
179+
const ref = { current: null as HTMLDivElement | null };
180+
render(<Alert ref={ref} heading="Test" />);
181+
expect(ref.current).toBeInstanceOf(HTMLDivElement);
182+
});
183+
184+
it('spreads additional HTML attributes', () => {
185+
render(<Alert heading="Test" id="alert-1" data-testid="my-alert" />);
186+
const el = screen.getByTestId('my-alert');
187+
expect(el).toHaveAttribute('id', 'alert-1');
188+
});
189+
190+
// ===========================
191+
// Content examples
192+
// ===========================
193+
194+
it('renders a validation error list as children', () => {
195+
render(
196+
<Alert variant="negative" heading="Er zijn fouten opgetreden">
197+
<ul>
198+
<li>Veld 1 is verplicht</li>
199+
<li>Veld 2 is ongeldig</li>
200+
</ul>
201+
</Alert>
202+
);
203+
expect(screen.getByText('Er zijn fouten opgetreden')).toBeInTheDocument();
204+
expect(screen.getByText('Veld 1 is verplicht')).toBeInTheDocument();
205+
expect(screen.getByText('Veld 2 is ongeldig')).toBeInTheDocument();
206+
});
207+
208+
it('renders success message correctly', () => {
209+
render(
210+
<Alert variant="positive" heading="Gelukt">
211+
Uw gegevens zijn opgeslagen.
212+
</Alert>
213+
);
214+
expect(screen.getByRole('heading', { name: 'Gelukt' })).toBeInTheDocument();
215+
expect(
216+
screen.getByText('Uw gegevens zijn opgeslagen.')
217+
).toBeInTheDocument();
218+
});
219+
});

0 commit comments

Comments
 (0)