@arbor-education/design-system.components 0.25.1 → 0.25.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +14 -0
- package/dist/components/row/Row.d.ts +5 -4
- package/dist/components/row/Row.d.ts.map +1 -1
- package/dist/components/row/Row.js +4 -2
- package/dist/components/row/Row.js.map +1 -1
- package/dist/components/row/Row.stories.d.ts +114 -4
- package/dist/components/row/Row.stories.d.ts.map +1 -1
- package/dist/components/row/Row.stories.js +451 -63
- package/dist/components/row/Row.stories.js.map +1 -1
- package/dist/components/row/Row.test.js +39 -0
- package/dist/components/row/Row.test.js.map +1 -1
- package/dist/components/toggleGroup/ToggleGroup.d.ts +29 -0
- package/dist/components/toggleGroup/ToggleGroup.d.ts.map +1 -0
- package/dist/components/toggleGroup/ToggleGroup.js +26 -0
- package/dist/components/toggleGroup/ToggleGroup.js.map +1 -0
- package/dist/components/toggleGroup/ToggleGroup.stories.d.ts +86 -0
- package/dist/components/toggleGroup/ToggleGroup.stories.d.ts.map +1 -0
- package/dist/components/toggleGroup/ToggleGroup.stories.js +342 -0
- package/dist/components/toggleGroup/ToggleGroup.stories.js.map +1 -0
- package/dist/components/toggleGroup/ToggleGroup.test.d.ts +2 -0
- package/dist/components/toggleGroup/ToggleGroup.test.d.ts.map +1 -0
- package/dist/components/toggleGroup/ToggleGroup.test.js +123 -0
- package/dist/components/toggleGroup/ToggleGroup.test.js.map +1 -0
- package/dist/components/toggleGroup/ToggleGroupItem.d.ts +10 -0
- package/dist/components/toggleGroup/ToggleGroupItem.d.ts.map +1 -0
- package/dist/components/toggleGroup/ToggleGroupItem.js +5 -0
- package/dist/components/toggleGroup/ToggleGroupItem.js.map +1 -0
- package/dist/index.css +33 -0
- package/dist/index.css.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/components/row/Row.stories.tsx +638 -80
- package/src/components/row/Row.test.tsx +48 -0
- package/src/components/row/Row.tsx +15 -5
- package/src/components/toggleGroup/ToggleGroup.stories.tsx +429 -0
- package/src/components/toggleGroup/ToggleGroup.test.tsx +155 -0
- package/src/components/toggleGroup/ToggleGroup.tsx +74 -0
- package/src/components/toggleGroup/ToggleGroupItem.tsx +24 -0
- package/src/components/toggleGroup/toggleGroup.scss +43 -0
- package/src/index.scss +1 -0
- package/src/index.ts +1 -0
|
@@ -9,28 +9,33 @@ import {
|
|
|
9
9
|
Title,
|
|
10
10
|
} from '@storybook/addon-docs/blocks';
|
|
11
11
|
import { fn } from 'storybook/test';
|
|
12
|
+
import { Section } from 'Components/section/Section.js';
|
|
12
13
|
|
|
13
14
|
import { Row } from './Row.js';
|
|
15
|
+
import { Badge } from 'Components/badge/Badge';
|
|
16
|
+
import { Tag } from 'Components/tag/Tag';
|
|
17
|
+
import { Icon } from 'Components/icon/Icon';
|
|
14
18
|
|
|
15
19
|
// ---------------------------------------------------------------------------
|
|
16
|
-
//
|
|
20
|
+
// Docs page content
|
|
17
21
|
// ---------------------------------------------------------------------------
|
|
18
22
|
|
|
19
23
|
const DESCRIPTION_INTRO = [
|
|
20
|
-
'`Row` is a single-line display component
|
|
21
|
-
'
|
|
24
|
+
'`Row` is a single-line display component for presenting a labelled piece of data inside summary panels, detail cards, or section-style layouts.',
|
|
25
|
+
'Pass an `onClick` handler to make the row fully interactive — it enters the tab order, gains hover and focus styles, and renders a pair of animated directional icons.',
|
|
26
|
+
'Pass `href` to turn the value into a native anchor link, supporting any protocol including `mailto:` and `tel:`.',
|
|
22
27
|
].join(' ');
|
|
23
28
|
|
|
24
|
-
const PROPS_INTRO
|
|
25
|
-
= 'The preview below is wired to the **Controls** panel — tweak any prop to see the story update in real time.';
|
|
29
|
+
const PROPS_INTRO = 'The preview below is wired to the **Controls** panel — tweak any prop to see the story update in real time.';
|
|
26
30
|
|
|
27
31
|
const USAGE_GUIDANCE = [
|
|
28
32
|
'### When to use',
|
|
29
33
|
'',
|
|
30
|
-
'- Displaying a labelled
|
|
34
|
+
'- Displaying a labelled key-value pair in a detail panel or summary card (e.g. "Year group · Year 9").',
|
|
31
35
|
'- Rendering a list of named data points where a consistent label/value layout improves scannability.',
|
|
32
|
-
'- Providing a
|
|
33
|
-
'-
|
|
36
|
+
'- Providing a clickable drill-down link to a related record when `onClick` is supplied.',
|
|
37
|
+
'- Wrapping a value in a native link (URL, email address, phone number) via the `href` prop.',
|
|
38
|
+
'- Showing supplementary context alongside a value (e.g. a threshold note or timestamp in the `note` slot).',
|
|
34
39
|
'',
|
|
35
40
|
'---',
|
|
36
41
|
'',
|
|
@@ -38,45 +43,76 @@ const USAGE_GUIDANCE = [
|
|
|
38
43
|
'',
|
|
39
44
|
'| Situation | Use instead |',
|
|
40
45
|
'|---|---|',
|
|
41
|
-
'| Navigation links | Native `<a>` or router `<Link>` — a `div` with `onClick` is not a true anchor |',
|
|
42
46
|
'| Tabular data with sortable columns | [`Table`](?path=/docs/components-table--docs) |',
|
|
47
|
+
'| Form controls with labels | [`FormField`](?path=/docs/components-formfield--docs) |',
|
|
43
48
|
'| Toggling or selecting items | Checkbox or radio group |',
|
|
44
|
-
'|
|
|
49
|
+
'| Navigation links standing alone | Native `<a>` or router `<Link>` |',
|
|
45
50
|
].join('\n');
|
|
46
51
|
|
|
47
52
|
const DEVELOPER_NOTES = [
|
|
48
53
|
'### Clickable vs static rows',
|
|
49
54
|
'',
|
|
50
|
-
'The `onClick` prop is the **sole toggle** for all
|
|
55
|
+
'The `onClick` prop is the **sole toggle** for all interactive behaviour — there is no separate `isClickable` prop.',
|
|
51
56
|
'',
|
|
52
57
|
'| State | `tabIndex` | CSS modifier | Icons rendered |',
|
|
53
58
|
'|---|---|---|---|',
|
|
54
59
|
'| Static | `-1` (not in tab order) | — | None |',
|
|
55
60
|
'| Clickable | `0` (in tab order) | `ds-row--clickable` | `chevron-right` + `arrow-right` |',
|
|
56
61
|
'',
|
|
57
|
-
'**Dual-icon swap:** Both icons are always
|
|
62
|
+
'**Dual-icon swap:** Both icons are always in the DOM when `onClick` is provided. CSS controls visibility —',
|
|
63
|
+
'at rest `chevron-right` shows; on hover/focus `arrow-right` shows. No JavaScript state is involved.',
|
|
58
64
|
'',
|
|
59
|
-
'
|
|
60
|
-
'
|
|
65
|
+
'---',
|
|
66
|
+
'',
|
|
67
|
+
'### The `href` prop and value fallback',
|
|
68
|
+
'',
|
|
69
|
+
'When `href` is provided, the value is wrapped in a native `<a>` element:',
|
|
70
|
+
'',
|
|
71
|
+
'```tsx',
|
|
72
|
+
'// value becomes the link text',
|
|
73
|
+
'<Row label="Email" value="admin@school.com" href="mailto:admin@school.com" />',
|
|
74
|
+
'',
|
|
75
|
+
'// href becomes the link text when value is omitted — avoid with special protocols',
|
|
76
|
+
'<Row label="Profile" href="/students/123" />',
|
|
77
|
+
'```',
|
|
61
78
|
'',
|
|
62
|
-
'
|
|
79
|
+
'> **⚠️ Always provide `value` when using `mailto:` or `tel:` hrefs.** Without `value`, the raw protocol',
|
|
80
|
+
'> string (e.g. `"mailto:admin@school.com"`) is used as link text.',
|
|
63
81
|
'',
|
|
64
82
|
'---',
|
|
65
83
|
'',
|
|
66
84
|
'### Keyboard activation',
|
|
67
85
|
'',
|
|
68
|
-
'Clickable rows respond to **Enter** and **Space** keydown events
|
|
86
|
+
'Clickable rows respond to **Enter** and **Space** keydown events.',
|
|
87
|
+
'The focus ring uses `box-shadow: 0 0 0 3px var(--color-brand-300)` — applied on `:focus`.',
|
|
69
88
|
'',
|
|
70
89
|
'---',
|
|
71
90
|
'',
|
|
72
91
|
'### Accessibility gap',
|
|
73
92
|
'',
|
|
74
|
-
'> **Known limitation:**
|
|
75
|
-
'> Screen readers will not announce
|
|
93
|
+
'> **Known limitation:** The clickable row renders as a `<div>` with no `role="button"` attribute.',
|
|
94
|
+
'> Screen readers will not announce it as interactive. Keyboard users can still activate it, but',
|
|
95
|
+
'> VoiceOver/NVDA users have no indication the row is actionable.',
|
|
76
96
|
'',
|
|
77
|
-
'`
|
|
78
|
-
'
|
|
79
|
-
'
|
|
97
|
+
'`href` rows render a real `<a>` tag — full native link semantics and announced correctly by screen readers.',
|
|
98
|
+
'',
|
|
99
|
+
'`Row` does **not** spread arbitrary HTML attributes. `className` and `style` are the only escape hatches.',
|
|
100
|
+
'',
|
|
101
|
+
'---',
|
|
102
|
+
'',
|
|
103
|
+
'### Component tokens',
|
|
104
|
+
'',
|
|
105
|
+
'Row is themed entirely through `--section-list-row-*` component tokens:',
|
|
106
|
+
'',
|
|
107
|
+
'| Token | Purpose |',
|
|
108
|
+
'|---|---|',
|
|
109
|
+
'| `--section-list-row-default-color-background` | Resting background colour |',
|
|
110
|
+
'| `--section-list-row-hover-hover-bg` | Hover background (clickable rows only) |',
|
|
111
|
+
'| `--section-list-row-spacing-vertical` | Top/bottom padding |',
|
|
112
|
+
'| `--section-list-row-spacing-horizontal` | Left/right padding |',
|
|
113
|
+
'| `--section-list-row-radius` | Border radius |',
|
|
114
|
+
'| `--section-list-row-default-color-icon-arrow` | Chevron/arrow icon stroke at rest |',
|
|
115
|
+
'| `--section-list-row-hover-color-icon-arrow` | Chevron/arrow icon stroke on hover |',
|
|
80
116
|
'',
|
|
81
117
|
'---',
|
|
82
118
|
'',
|
|
@@ -85,12 +121,15 @@ const DEVELOPER_NOTES = [
|
|
|
85
121
|
'```ts',
|
|
86
122
|
"import { Row } from '@arbor-education/design-system.components';",
|
|
87
123
|
"import type { RowProps } from '@arbor-education/design-system.components';",
|
|
124
|
+
"import type { ReactNode } from 'react';",
|
|
88
125
|
'',
|
|
89
126
|
'type RowProps = {',
|
|
90
127
|
' className?: string;',
|
|
91
|
-
'
|
|
92
|
-
'
|
|
93
|
-
'
|
|
128
|
+
' style?: CSSProperties;',
|
|
129
|
+
' label?: ReactNode;',
|
|
130
|
+
' value?: ReactNode;',
|
|
131
|
+
' href?: string;',
|
|
132
|
+
' note?: ReactNode;',
|
|
94
133
|
' onClick?: MouseEventHandler<HTMLDivElement>;',
|
|
95
134
|
'};',
|
|
96
135
|
'```',
|
|
@@ -99,7 +138,7 @@ const DEVELOPER_NOTES = [
|
|
|
99
138
|
const RELATED_COMPONENTS = [
|
|
100
139
|
'## Related components',
|
|
101
140
|
'',
|
|
102
|
-
'[Section](?path=/docs/components-section--docs) · [Card](?path=/docs/components-card--docs) · [Table](?path=/docs/components-table--docs)',
|
|
141
|
+
'[Section](?path=/docs/components-section--docs) · [Card](?path=/docs/components-card--docs) · [Table](?path=/docs/components-table--docs) · [FormField](?path=/docs/components-formfield--docs)',
|
|
103
142
|
].join('\n');
|
|
104
143
|
|
|
105
144
|
// ---------------------------------------------------------------------------
|
|
@@ -131,11 +170,12 @@ function RowDocsPage() {
|
|
|
131
170
|
// Meta
|
|
132
171
|
// ---------------------------------------------------------------------------
|
|
133
172
|
|
|
134
|
-
const meta
|
|
173
|
+
const meta = {
|
|
135
174
|
title: 'Components/Row',
|
|
136
175
|
component: Row,
|
|
137
176
|
tags: ['autodocs'],
|
|
138
177
|
parameters: {
|
|
178
|
+
layout: 'padded',
|
|
139
179
|
docs: {
|
|
140
180
|
page: RowDocsPage,
|
|
141
181
|
},
|
|
@@ -143,12 +183,12 @@ const meta: Meta<typeof Row> = {
|
|
|
143
183
|
argTypes: {
|
|
144
184
|
label: {
|
|
145
185
|
description: [
|
|
146
|
-
'
|
|
147
|
-
'
|
|
186
|
+
'Left-column text identifying the data point.',
|
|
187
|
+
'Rendered in a `<span>` with semi-bold weight and a maximum width of 190 px so labels align consistently across stacked rows.',
|
|
148
188
|
].join(' '),
|
|
149
189
|
control: 'text',
|
|
150
190
|
table: {
|
|
151
|
-
type: { summary: '
|
|
191
|
+
type: { summary: 'ReactNode' },
|
|
152
192
|
defaultValue: { summary: 'undefined' },
|
|
153
193
|
},
|
|
154
194
|
},
|
|
@@ -156,6 +196,20 @@ const meta: Meta<typeof Row> = {
|
|
|
156
196
|
description: [
|
|
157
197
|
'The right-hand value corresponding to the label.',
|
|
158
198
|
'Renders in the centre column with `flex-grow: 1`, filling available width.',
|
|
199
|
+
'When `href` is provided, this becomes the anchor text.',
|
|
200
|
+
'If `href` is set and `value` is omitted, the href string itself is used as the link text.',
|
|
201
|
+
].join(' '),
|
|
202
|
+
control: 'text',
|
|
203
|
+
table: {
|
|
204
|
+
type: { summary: 'ReactNode' },
|
|
205
|
+
defaultValue: { summary: 'undefined' },
|
|
206
|
+
},
|
|
207
|
+
},
|
|
208
|
+
href: {
|
|
209
|
+
description: [
|
|
210
|
+
'When provided, wraps `value` in a native `<a>` element pointing to this URL.',
|
|
211
|
+
'Supports any protocol — `https://`, `mailto:`, `tel:`, etc.',
|
|
212
|
+
'Always provide `value` when using `mailto:` or `tel:` — without it the raw protocol string becomes the link text.',
|
|
159
213
|
].join(' '),
|
|
160
214
|
control: 'text',
|
|
161
215
|
table: {
|
|
@@ -165,8 +219,9 @@ const meta: Meta<typeof Row> = {
|
|
|
165
219
|
},
|
|
166
220
|
note: {
|
|
167
221
|
description: [
|
|
168
|
-
'
|
|
169
|
-
'
|
|
222
|
+
'Supplementary italic text rendered right-aligned after the value.',
|
|
223
|
+
'When present, the CSS `:has()` selector reduces the value column\'s `flex-basis` from 70% to 50% to make room.',
|
|
224
|
+
'Useful for thresholds, last-updated timestamps, or secondary context.',
|
|
170
225
|
].join(' '),
|
|
171
226
|
control: 'text',
|
|
172
227
|
table: {
|
|
@@ -176,9 +231,9 @@ const meta: Meta<typeof Row> = {
|
|
|
176
231
|
},
|
|
177
232
|
onClick: {
|
|
178
233
|
description: [
|
|
179
|
-
'When provided,
|
|
180
|
-
'gains the `ds-row--clickable` modifier, shows hover/focus styles, and renders dual directional
|
|
181
|
-
'
|
|
234
|
+
'When provided, activates all clickable behaviour: the row enters the tab order (`tabIndex={0}`),',
|
|
235
|
+
'gains the `ds-row--clickable` modifier, shows hover/focus styles, and renders the dual directional icon pair.',
|
|
236
|
+
'Responds to **Enter** and **Space** keyboard events.',
|
|
182
237
|
].join(' '),
|
|
183
238
|
action: 'onClick',
|
|
184
239
|
control: false,
|
|
@@ -188,21 +243,26 @@ const meta: Meta<typeof Row> = {
|
|
|
188
243
|
},
|
|
189
244
|
},
|
|
190
245
|
className: {
|
|
191
|
-
description:
|
|
192
|
-
'Additional CSS class names applied to the root `<div>`.',
|
|
193
|
-
'This is the only HTML-attribute escape hatch — arbitrary HTML attributes cannot be passed via props.',
|
|
194
|
-
].join(' '),
|
|
246
|
+
description: 'Additional CSS class names applied to the root `<div>`. The only HTML-attribute escape hatch — arbitrary attributes like `aria-label` or `data-testid` cannot be passed without modifying the component.',
|
|
195
247
|
control: 'text',
|
|
196
248
|
table: {
|
|
197
249
|
type: { summary: 'string' },
|
|
198
250
|
defaultValue: { summary: 'undefined' },
|
|
199
251
|
},
|
|
200
252
|
},
|
|
253
|
+
style: {
|
|
254
|
+
description: 'Inline styles applied to the root `<div>`.',
|
|
255
|
+
control: false,
|
|
256
|
+
table: {
|
|
257
|
+
type: { summary: 'CSSProperties' },
|
|
258
|
+
defaultValue: { summary: 'undefined' },
|
|
259
|
+
},
|
|
260
|
+
},
|
|
201
261
|
},
|
|
202
|
-
}
|
|
262
|
+
} satisfies Meta<typeof Row>;
|
|
203
263
|
|
|
204
264
|
export default meta;
|
|
205
|
-
type Story = StoryObj<typeof
|
|
265
|
+
type Story = StoryObj<typeof Row>;
|
|
206
266
|
|
|
207
267
|
// ---------------------------------------------------------------------------
|
|
208
268
|
// Helper
|
|
@@ -212,10 +272,97 @@ const withDescription = (story: Story, description: string): Story => ({
|
|
|
212
272
|
...story,
|
|
213
273
|
parameters: {
|
|
214
274
|
...story.parameters,
|
|
215
|
-
docs: {
|
|
275
|
+
docs: {
|
|
276
|
+
...story.parameters?.docs,
|
|
277
|
+
description: {
|
|
278
|
+
story: description,
|
|
279
|
+
},
|
|
280
|
+
},
|
|
216
281
|
},
|
|
217
282
|
});
|
|
218
283
|
|
|
284
|
+
// ---------------------------------------------------------------------------
|
|
285
|
+
// Template components
|
|
286
|
+
// ---------------------------------------------------------------------------
|
|
287
|
+
|
|
288
|
+
const MixedClickableAndStaticTemplate = () => (
|
|
289
|
+
<Section title="Pupil details">
|
|
290
|
+
<Row label="Name" value="Amara Osei-Bonsu" />
|
|
291
|
+
<Row label="Year group" value="Year 10" />
|
|
292
|
+
<Row label="Form" value="10JH" />
|
|
293
|
+
<Row
|
|
294
|
+
label="SEN status"
|
|
295
|
+
value="SEN Support"
|
|
296
|
+
note="Review due May 2026"
|
|
297
|
+
onClick={() => {}}
|
|
298
|
+
/>
|
|
299
|
+
<Row
|
|
300
|
+
label="Attendance (YTD)"
|
|
301
|
+
value="88.3%"
|
|
302
|
+
note="Below 90% threshold"
|
|
303
|
+
onClick={() => {}}
|
|
304
|
+
/>
|
|
305
|
+
</Section>
|
|
306
|
+
);
|
|
307
|
+
|
|
308
|
+
const InSectionTemplate = () => (
|
|
309
|
+
<Section title="Personal details">
|
|
310
|
+
<Row label="Pupil" value="Amara Osei-Bonsu" />
|
|
311
|
+
<Row label="Date of birth" value="14 March 2010" />
|
|
312
|
+
<Row label="Admission number" value="001847" />
|
|
313
|
+
<Row label="UPN" value="A823456789012" />
|
|
314
|
+
<Row
|
|
315
|
+
label="Email address"
|
|
316
|
+
value="a.osei-bonsu@pupil.school.example"
|
|
317
|
+
href="mailto:a.osei-bonsu@pupil.school.example"
|
|
318
|
+
/>
|
|
319
|
+
</Section>
|
|
320
|
+
);
|
|
321
|
+
|
|
322
|
+
const AccessibilityNotesTemplate = () => (
|
|
323
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--spacing-large)' }}>
|
|
324
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--spacing-xsmall)' }}>
|
|
325
|
+
<p style={{ margin: 0, color: 'var(--color-grey-600)', fontSize: 'var(--font-size-3-14)' }}>
|
|
326
|
+
Static row —
|
|
327
|
+
{' '}
|
|
328
|
+
<code>tabIndex="-1"</code>
|
|
329
|
+
, no ARIA role, not in tab order
|
|
330
|
+
</p>
|
|
331
|
+
<Row label="Year group" value="Year 9" />
|
|
332
|
+
</div>
|
|
333
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--spacing-xsmall)' }}>
|
|
334
|
+
<p style={{ margin: 0, color: 'var(--color-grey-600)', fontSize: 'var(--font-size-3-14)' }}>
|
|
335
|
+
Clickable row —
|
|
336
|
+
{' '}
|
|
337
|
+
<code>tabIndex="0"</code>
|
|
338
|
+
, keyboard-activatable, but
|
|
339
|
+
{' '}
|
|
340
|
+
<strong>
|
|
341
|
+
no
|
|
342
|
+
<code>role="button"</code>
|
|
343
|
+
</strong>
|
|
344
|
+
{' '}
|
|
345
|
+
— screen readers may not announce it as interactive
|
|
346
|
+
</p>
|
|
347
|
+
<Row label="SEN record" value="SEN Support" onClick={() => {}} />
|
|
348
|
+
</div>
|
|
349
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--spacing-xsmall)' }}>
|
|
350
|
+
<p style={{ margin: 0, color: 'var(--color-grey-600)', fontSize: 'var(--font-size-3-14)' }}>
|
|
351
|
+
href row — renders a real
|
|
352
|
+
{' '}
|
|
353
|
+
<code><a></code>
|
|
354
|
+
{' '}
|
|
355
|
+
tag with full native link semantics ✓
|
|
356
|
+
</p>
|
|
357
|
+
<Row
|
|
358
|
+
label="Email address"
|
|
359
|
+
value="admin@school.example"
|
|
360
|
+
href="mailto:admin@school.example"
|
|
361
|
+
/>
|
|
362
|
+
</div>
|
|
363
|
+
</div>
|
|
364
|
+
);
|
|
365
|
+
|
|
219
366
|
// ---------------------------------------------------------------------------
|
|
220
367
|
// Stories
|
|
221
368
|
// ---------------------------------------------------------------------------
|
|
@@ -230,7 +377,7 @@ export const Default: Story = withDescription(
|
|
|
230
377
|
},
|
|
231
378
|
render: args => <Row {...args} />,
|
|
232
379
|
},
|
|
233
|
-
'The
|
|
380
|
+
'The interactive canvas — every prop is wired to the Controls panel. By default the row is clickable — clear `onClick` from the controls to switch to static mode and observe how the icons and hover behaviour disappear.',
|
|
234
381
|
);
|
|
235
382
|
|
|
236
383
|
export const LabelOnly: Story = withDescription(
|
|
@@ -253,10 +400,10 @@ export default EnrolmentStatusRow;
|
|
|
253
400
|
},
|
|
254
401
|
render: () => <Row label="Enrolment status" />,
|
|
255
402
|
},
|
|
256
|
-
'Minimum meaningful render — a label with no value or note.
|
|
403
|
+
'Minimum meaningful render — a label with no value, href, or note. The value and note `<span>` elements still render in the DOM but remain empty. The row holds its correct minimum height.',
|
|
257
404
|
);
|
|
258
405
|
|
|
259
|
-
export const
|
|
406
|
+
export const ValueOnly: Story = withDescription(
|
|
260
407
|
{
|
|
261
408
|
parameters: {
|
|
262
409
|
controls: { disable: true },
|
|
@@ -266,17 +413,17 @@ export const LabelAndValue: Story = withDescription(
|
|
|
266
413
|
code: `
|
|
267
414
|
import { Row } from '@arbor-education/design-system.components';
|
|
268
415
|
|
|
269
|
-
function
|
|
270
|
-
return <Row
|
|
416
|
+
function ActiveStatusRow() {
|
|
417
|
+
return <Row value="Active" />;
|
|
271
418
|
}
|
|
272
|
-
export default
|
|
419
|
+
export default ActiveStatusRow;
|
|
273
420
|
`.trim(),
|
|
274
421
|
},
|
|
275
422
|
},
|
|
276
423
|
},
|
|
277
|
-
render: () => <Row
|
|
424
|
+
render: () => <Row value="Active" />,
|
|
278
425
|
},
|
|
279
|
-
'
|
|
426
|
+
'A value with no label — the label column renders empty. Valid but uncommon; most real-world rows pair a label with every value.',
|
|
280
427
|
);
|
|
281
428
|
|
|
282
429
|
export const WithNote: Story = withDescription(
|
|
@@ -311,7 +458,138 @@ export default AttendanceRow;
|
|
|
311
458
|
/>
|
|
312
459
|
),
|
|
313
460
|
},
|
|
314
|
-
'Adding
|
|
461
|
+
'Adding `note` renders supplementary italic text right-aligned after the value. The CSS `:has()` selector automatically shrinks the value column from 70% to 50% flex-basis to make room. Use notes for thresholds, last-updated timestamps, or secondary context that helps interpret the value.',
|
|
462
|
+
);
|
|
463
|
+
|
|
464
|
+
export const WithHref: Story = withDescription(
|
|
465
|
+
{
|
|
466
|
+
parameters: {
|
|
467
|
+
controls: { disable: true },
|
|
468
|
+
docs: {
|
|
469
|
+
source: {
|
|
470
|
+
language: 'tsx',
|
|
471
|
+
code: `
|
|
472
|
+
import { Row } from '@arbor-education/design-system.components';
|
|
473
|
+
|
|
474
|
+
function ProfileLinkRow() {
|
|
475
|
+
return (
|
|
476
|
+
<Row
|
|
477
|
+
label="Student profile"
|
|
478
|
+
value="View full profile"
|
|
479
|
+
href="/students/amara-osei-bonsu"
|
|
480
|
+
/>
|
|
481
|
+
);
|
|
482
|
+
}
|
|
483
|
+
export default ProfileLinkRow;
|
|
484
|
+
`.trim(),
|
|
485
|
+
},
|
|
486
|
+
},
|
|
487
|
+
},
|
|
488
|
+
render: () => (
|
|
489
|
+
<Row
|
|
490
|
+
label="Student profile"
|
|
491
|
+
value="View full profile"
|
|
492
|
+
href="/students/amara-osei-bonsu"
|
|
493
|
+
/>
|
|
494
|
+
),
|
|
495
|
+
},
|
|
496
|
+
'When `href` is provided alongside `value`, the value text becomes the anchor text. The `<a>` element carries full native link semantics — right-click opens the browser context menu, the link can be opened in a new tab, and screen readers announce it correctly as a link.',
|
|
497
|
+
);
|
|
498
|
+
|
|
499
|
+
export const WithHrefFallback: Story = withDescription(
|
|
500
|
+
{
|
|
501
|
+
parameters: {
|
|
502
|
+
controls: { disable: true },
|
|
503
|
+
docs: {
|
|
504
|
+
source: {
|
|
505
|
+
language: 'tsx',
|
|
506
|
+
code: `
|
|
507
|
+
import { Row } from '@arbor-education/design-system.components';
|
|
508
|
+
|
|
509
|
+
// ⚠️ href without value — the href string becomes the link text
|
|
510
|
+
function HrefFallbackRow() {
|
|
511
|
+
return <Row label="Profile" href="/students/123" />;
|
|
512
|
+
}
|
|
513
|
+
export default HrefFallbackRow;
|
|
514
|
+
`.trim(),
|
|
515
|
+
},
|
|
516
|
+
},
|
|
517
|
+
},
|
|
518
|
+
render: () => <Row label="Profile" href="/students/123" />,
|
|
519
|
+
},
|
|
520
|
+
'When `href` is set but `value` is omitted, the href string itself is used as the link text (`value || href`). For plain paths like `/students/123` this is acceptable. For `mailto:` or `tel:` hrefs, the raw URI string is exposed to the user — always pair those with an explicit `value`. See the **WithHrefMailto** story for the correct email pattern.',
|
|
521
|
+
);
|
|
522
|
+
|
|
523
|
+
export const WithHrefMailto: Story = withDescription(
|
|
524
|
+
{
|
|
525
|
+
parameters: {
|
|
526
|
+
controls: { disable: true },
|
|
527
|
+
docs: {
|
|
528
|
+
source: {
|
|
529
|
+
language: 'tsx',
|
|
530
|
+
code: `
|
|
531
|
+
import { Row } from '@arbor-education/design-system.components';
|
|
532
|
+
|
|
533
|
+
function EmailRow() {
|
|
534
|
+
return (
|
|
535
|
+
<Row
|
|
536
|
+
label="Email address"
|
|
537
|
+
value="j.smith@school.example"
|
|
538
|
+
href="mailto:j.smith@school.example"
|
|
539
|
+
/>
|
|
540
|
+
);
|
|
541
|
+
}
|
|
542
|
+
export default EmailRow;
|
|
543
|
+
`.trim(),
|
|
544
|
+
},
|
|
545
|
+
},
|
|
546
|
+
},
|
|
547
|
+
render: () => (
|
|
548
|
+
<Row
|
|
549
|
+
label="Email address"
|
|
550
|
+
value="j.smith@school.example"
|
|
551
|
+
href="mailto:j.smith@school.example"
|
|
552
|
+
/>
|
|
553
|
+
),
|
|
554
|
+
},
|
|
555
|
+
'The canonical email-address pattern: `href="mailto:..."` opens the user\'s mail client and `value` is the human-readable address shown as link text. Always provide `value` with `mailto:` hrefs — omitting it exposes the raw protocol string (e.g. `"mailto:j.smith@school.example"`) as link text.',
|
|
556
|
+
);
|
|
557
|
+
|
|
558
|
+
export const WithHrefAndNote: Story = withDescription(
|
|
559
|
+
{
|
|
560
|
+
parameters: {
|
|
561
|
+
controls: { disable: true },
|
|
562
|
+
docs: {
|
|
563
|
+
source: {
|
|
564
|
+
language: 'tsx',
|
|
565
|
+
code: `
|
|
566
|
+
import { Row } from '@arbor-education/design-system.components';
|
|
567
|
+
|
|
568
|
+
function GuardianEmailRow() {
|
|
569
|
+
return (
|
|
570
|
+
<Row
|
|
571
|
+
label="Guardian email"
|
|
572
|
+
value="s.okafor@guardian.example"
|
|
573
|
+
href="mailto:s.okafor@guardian.example"
|
|
574
|
+
note="Primary contact"
|
|
575
|
+
/>
|
|
576
|
+
);
|
|
577
|
+
}
|
|
578
|
+
export default GuardianEmailRow;
|
|
579
|
+
`.trim(),
|
|
580
|
+
},
|
|
581
|
+
},
|
|
582
|
+
},
|
|
583
|
+
render: () => (
|
|
584
|
+
<Row
|
|
585
|
+
label="Guardian email"
|
|
586
|
+
value="s.okafor@guardian.example"
|
|
587
|
+
href="mailto:s.okafor@guardian.example"
|
|
588
|
+
note="Primary contact"
|
|
589
|
+
/>
|
|
590
|
+
),
|
|
591
|
+
},
|
|
592
|
+
'`href` and `note` used together. The anchor sits in the value column while the note stays right-aligned. The CSS `:has()` reflow is active — the value column narrows to 50% flex-basis to accommodate the note.',
|
|
315
593
|
);
|
|
316
594
|
|
|
317
595
|
export const Clickable: Story = withDescription(
|
|
@@ -346,7 +624,7 @@ export default ClassRow;
|
|
|
346
624
|
/>
|
|
347
625
|
),
|
|
348
626
|
},
|
|
349
|
-
'Providing
|
|
627
|
+
'Providing `onClick` activates all clickable behaviour: `ds-row--clickable` modifier, pointer cursor, hover background, and the dual icon pair. **Hover the row** to see `chevron-right` swap to `arrow-right` — a pure CSS transition with no JavaScript state. Press **Tab** to focus, then **Enter** or **Space** to fire the handler.',
|
|
350
628
|
);
|
|
351
629
|
|
|
352
630
|
export const ClickableWithNote: Story = withDescription(
|
|
@@ -383,7 +661,83 @@ export default GuardianRow;
|
|
|
383
661
|
/>
|
|
384
662
|
),
|
|
385
663
|
},
|
|
386
|
-
'
|
|
664
|
+
'Clickable row with all three right-side elements active: value, note, and the chevron/arrow icon pair. The note takes 20% flex-basis; the value narrows to 50%; the icon is flex-shrunk. The secondary context ("Primary contact") persists alongside the navigation affordance.',
|
|
665
|
+
);
|
|
666
|
+
|
|
667
|
+
export const MixedClickableAndStatic: Story = withDescription(
|
|
668
|
+
{
|
|
669
|
+
parameters: {
|
|
670
|
+
controls: { disable: true },
|
|
671
|
+
docs: {
|
|
672
|
+
source: {
|
|
673
|
+
language: 'tsx',
|
|
674
|
+
code: `
|
|
675
|
+
import { Row, Section } from '@arbor-education/design-system.components';
|
|
676
|
+
|
|
677
|
+
function PupilDetailSection() {
|
|
678
|
+
return (
|
|
679
|
+
<Section title="Pupil details">
|
|
680
|
+
<Row label="Name" value="Amara Osei-Bonsu" />
|
|
681
|
+
<Row label="Year group" value="Year 10" />
|
|
682
|
+
<Row label="Form" value="10JH" />
|
|
683
|
+
<Row
|
|
684
|
+
label="SEN status"
|
|
685
|
+
value="SEN Support"
|
|
686
|
+
note="Review due May 2026"
|
|
687
|
+
onClick={() => navigateToSENRecord()}
|
|
688
|
+
/>
|
|
689
|
+
<Row
|
|
690
|
+
label="Attendance (YTD)"
|
|
691
|
+
value="88.3%"
|
|
692
|
+
note="Below 90% threshold"
|
|
693
|
+
onClick={() => navigateToAttendance()}
|
|
694
|
+
/>
|
|
695
|
+
</Section>
|
|
696
|
+
);
|
|
697
|
+
}
|
|
698
|
+
export default PupilDetailSection;
|
|
699
|
+
`.trim(),
|
|
700
|
+
},
|
|
701
|
+
},
|
|
702
|
+
},
|
|
703
|
+
render: MixedClickableAndStaticTemplate,
|
|
704
|
+
},
|
|
705
|
+
'Static and clickable rows freely mixed inside a `Section`. Static rows display data; clickable rows provide navigation affordance. The visual contrast — pointer cursor, hover background, directional icon — makes the interactive rows immediately distinguishable without extra labelling.',
|
|
706
|
+
);
|
|
707
|
+
|
|
708
|
+
export const InSection: Story = withDescription(
|
|
709
|
+
{
|
|
710
|
+
parameters: {
|
|
711
|
+
controls: { disable: true },
|
|
712
|
+
docs: {
|
|
713
|
+
source: {
|
|
714
|
+
language: 'tsx',
|
|
715
|
+
code: `
|
|
716
|
+
import { Row, Section } from '@arbor-education/design-system.components';
|
|
717
|
+
|
|
718
|
+
function PersonalDetailsSection() {
|
|
719
|
+
return (
|
|
720
|
+
<Section title="Personal details">
|
|
721
|
+
<Row label="Pupil" value="Amara Osei-Bonsu" />
|
|
722
|
+
<Row label="Date of birth" value="14 March 2010" />
|
|
723
|
+
<Row label="Admission number" value="001847" />
|
|
724
|
+
<Row label="UPN" value="A823456789012" />
|
|
725
|
+
<Row
|
|
726
|
+
label="Email address"
|
|
727
|
+
value="a.osei-bonsu@pupil.school.example"
|
|
728
|
+
href="mailto:a.osei-bonsu@pupil.school.example"
|
|
729
|
+
/>
|
|
730
|
+
</Section>
|
|
731
|
+
);
|
|
732
|
+
}
|
|
733
|
+
export default PersonalDetailsSection;
|
|
734
|
+
`.trim(),
|
|
735
|
+
},
|
|
736
|
+
},
|
|
737
|
+
},
|
|
738
|
+
render: InSectionTemplate,
|
|
739
|
+
},
|
|
740
|
+
'The canonical real-world usage: `Row` components as children of a `Section`. The `Section` provides the container heading and background; each `Row` handles the label/value layout. Most product uses of `Row` are inside a `Section`.',
|
|
387
741
|
);
|
|
388
742
|
|
|
389
743
|
export const LongContent: Story = withDescription(
|
|
@@ -398,7 +752,7 @@ import { Row } from '@arbor-education/design-system.components';
|
|
|
398
752
|
|
|
399
753
|
function LongContentExample() {
|
|
400
754
|
return (
|
|
401
|
-
<div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--spacing-
|
|
755
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--spacing-xsmall)' }}>
|
|
402
756
|
<Row
|
|
403
757
|
label="Special educational needs and disabilities coordinator"
|
|
404
758
|
value="Mrs Jacqueline Abernathy-Thornton"
|
|
@@ -419,7 +773,7 @@ export default LongContentExample;
|
|
|
419
773
|
},
|
|
420
774
|
},
|
|
421
775
|
render: () => (
|
|
422
|
-
<div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--spacing-
|
|
776
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--spacing-xsmall)' }}>
|
|
423
777
|
<Row
|
|
424
778
|
label="Special educational needs and disabilities coordinator"
|
|
425
779
|
value="Mrs Jacqueline Abernathy-Thornton"
|
|
@@ -434,7 +788,7 @@ export default LongContentExample;
|
|
|
434
788
|
</div>
|
|
435
789
|
),
|
|
436
790
|
},
|
|
437
|
-
'Long label and value strings exercise the flex layout. The label column
|
|
791
|
+
'Long label and value strings exercise the flex layout. The label column enforces a 190 px maximum width — long labels wrap rather than compress the value column. Test your specific container widths if you have long copy in narrow panels.',
|
|
438
792
|
);
|
|
439
793
|
|
|
440
794
|
export const MultipleRows: Story = withDescription(
|
|
@@ -447,55 +801,259 @@ export const MultipleRows: Story = withDescription(
|
|
|
447
801
|
code: `
|
|
448
802
|
import { Row } from '@arbor-education/design-system.components';
|
|
449
803
|
|
|
450
|
-
function
|
|
804
|
+
function PupilDataList() {
|
|
451
805
|
return (
|
|
452
|
-
<div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--spacing-
|
|
806
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--spacing-xsmall)' }}>
|
|
453
807
|
<Row label="Pupil" value="Amara Osei-Bonsu" />
|
|
454
808
|
<Row label="Year group" value="Year 10" />
|
|
455
809
|
<Row label="Form" value="10JH" />
|
|
456
810
|
<Row label="Admission number" value="001847" />
|
|
457
811
|
<Row label="UPN" value="A823456789012" />
|
|
458
|
-
<Row
|
|
459
|
-
label="SEN status"
|
|
460
|
-
value="SEN Support"
|
|
461
|
-
note="Review due May 2026"
|
|
462
|
-
onClick={() => navigateToSENRecord()}
|
|
463
|
-
/>
|
|
464
|
-
<Row
|
|
465
|
-
label="Attendance (YTD)"
|
|
466
|
-
value="88.3%"
|
|
467
|
-
note="Below 90% threshold"
|
|
468
|
-
onClick={() => navigateToAttendance()}
|
|
469
|
-
/>
|
|
470
812
|
</div>
|
|
471
813
|
);
|
|
472
814
|
}
|
|
473
|
-
export default
|
|
815
|
+
export default PupilDataList;
|
|
474
816
|
`.trim(),
|
|
475
817
|
},
|
|
476
818
|
},
|
|
477
819
|
},
|
|
478
820
|
render: () => (
|
|
479
|
-
<div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--spacing-
|
|
821
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--spacing-xsmall)' }}>
|
|
480
822
|
<Row label="Pupil" value="Amara Osei-Bonsu" />
|
|
481
823
|
<Row label="Year group" value="Year 10" />
|
|
482
824
|
<Row label="Form" value="10JH" />
|
|
483
825
|
<Row label="Admission number" value="001847" />
|
|
484
826
|
<Row label="UPN" value="A823456789012" />
|
|
827
|
+
</div>
|
|
828
|
+
),
|
|
829
|
+
},
|
|
830
|
+
'Multiple static rows stacked with `gap: var(--spacing-xsmall)`. Each `Row` carries its own background and padding — the gap sits between rows. Use `Section` as the outer container in production to get the correct heading and container treatment.',
|
|
831
|
+
);
|
|
832
|
+
|
|
833
|
+
export const ReactNodeInValue: Story = withDescription(
|
|
834
|
+
{
|
|
835
|
+
parameters: {
|
|
836
|
+
controls: { disable: true },
|
|
837
|
+
docs: {
|
|
838
|
+
source: {
|
|
839
|
+
language: 'tsx',
|
|
840
|
+
code: `
|
|
841
|
+
import { Row, Badge } from '@arbor-education/design-system.components';
|
|
842
|
+
|
|
843
|
+
function SENStatusRows() {
|
|
844
|
+
return (
|
|
845
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--spacing-small)' }}>
|
|
846
|
+
<Row label="SEN status" value={<Badge colour="orange">SEN Support</Badge>} />
|
|
847
|
+
<Row label="Admission type" value={<Badge colour="blue">Looked after child</Badge>} />
|
|
848
|
+
</div>
|
|
849
|
+
);
|
|
850
|
+
}
|
|
851
|
+
export default SENStatusRows;
|
|
852
|
+
`.trim(),
|
|
853
|
+
},
|
|
854
|
+
},
|
|
855
|
+
},
|
|
856
|
+
render: () => (
|
|
857
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--spacing-small)' }}>
|
|
858
|
+
<Row label="SEN status" value={<Badge colour="orange">SEN Support</Badge>} />
|
|
859
|
+
<Row label="Admission type" value={<Badge colour="blue">Looked after child</Badge>} />
|
|
860
|
+
</div>
|
|
861
|
+
),
|
|
862
|
+
},
|
|
863
|
+
'The `value` prop accepts any `ReactNode`, so status badges, coloured indicators, or formatted markup can replace plain strings. A `Badge` communicates urgency or category at a glance without the reader having to interpret the text alone.',
|
|
864
|
+
);
|
|
865
|
+
|
|
866
|
+
export const ReactNodeInNote: Story = withDescription(
|
|
867
|
+
{
|
|
868
|
+
parameters: {
|
|
869
|
+
controls: { disable: true },
|
|
870
|
+
docs: {
|
|
871
|
+
source: {
|
|
872
|
+
language: 'tsx',
|
|
873
|
+
code: `
|
|
874
|
+
import { Row, Tag } from '@arbor-education/design-system.components';
|
|
875
|
+
|
|
876
|
+
function AttendanceRows() {
|
|
877
|
+
return (
|
|
878
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--spacing-small)' }}>
|
|
879
|
+
<Row label="Attendance this term" value="91.4%" note={<Tag color="orange">Below target</Tag>} />
|
|
880
|
+
<Row label="Reading age" value="8y 4m" note={<Tag color="green">On track</Tag>} />
|
|
881
|
+
</div>
|
|
882
|
+
);
|
|
883
|
+
}
|
|
884
|
+
export default AttendanceRows;
|
|
885
|
+
`.trim(),
|
|
886
|
+
},
|
|
887
|
+
},
|
|
888
|
+
},
|
|
889
|
+
render: () => (
|
|
890
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--spacing-small)' }}>
|
|
891
|
+
<Row label="Attendance this term" value="91.4%" note={<Tag color="orange">Below target</Tag>} />
|
|
892
|
+
<Row label="Reading age" value="8y 4m" note={<Tag color="green">On track</Tag>} />
|
|
893
|
+
</div>
|
|
894
|
+
),
|
|
895
|
+
},
|
|
896
|
+
'The `note` slot accepts any `ReactNode` — a coloured `Tag` conveys urgency or status at a glance without burying it in prose. Pair with a plain-text `value` so the primary reading stays clean while the note provides a quick visual cue about whether the value is within an expected range.',
|
|
897
|
+
);
|
|
898
|
+
|
|
899
|
+
export const ReactNodeInLabel: Story = withDescription(
|
|
900
|
+
{
|
|
901
|
+
parameters: {
|
|
902
|
+
controls: { disable: true },
|
|
903
|
+
docs: {
|
|
904
|
+
source: {
|
|
905
|
+
language: 'tsx',
|
|
906
|
+
code: `
|
|
907
|
+
import { Row, Icon } from '@arbor-education/design-system.components';
|
|
908
|
+
|
|
909
|
+
function AnnotatedLabelRows() {
|
|
910
|
+
return (
|
|
911
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--spacing-small)' }}>
|
|
912
|
+
<Row
|
|
913
|
+
label={
|
|
914
|
+
<span style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
|
|
915
|
+
Pupil premium <Icon name="info" size={16} />
|
|
916
|
+
</span>
|
|
917
|
+
}
|
|
918
|
+
value="Free school meals — ever 6"
|
|
919
|
+
/>
|
|
920
|
+
<Row
|
|
921
|
+
label={
|
|
922
|
+
<span style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
|
|
923
|
+
EHCP status <Icon name="info" size={16} />
|
|
924
|
+
</span>
|
|
925
|
+
}
|
|
926
|
+
value="EHC plan in place"
|
|
927
|
+
/>
|
|
928
|
+
</div>
|
|
929
|
+
);
|
|
930
|
+
}
|
|
931
|
+
export default AnnotatedLabelRows;
|
|
932
|
+
`.trim(),
|
|
933
|
+
},
|
|
934
|
+
},
|
|
935
|
+
},
|
|
936
|
+
render: () => (
|
|
937
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--spacing-small)' }}>
|
|
485
938
|
<Row
|
|
486
|
-
label=
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
939
|
+
label={(
|
|
940
|
+
<span style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
|
|
941
|
+
Pupil premium
|
|
942
|
+
{' '}
|
|
943
|
+
<Icon name="info" size={16} />
|
|
944
|
+
</span>
|
|
945
|
+
)}
|
|
946
|
+
value="Free school meals — ever 6"
|
|
490
947
|
/>
|
|
491
948
|
<Row
|
|
492
|
-
label=
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
949
|
+
label={(
|
|
950
|
+
<span style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
|
|
951
|
+
EHCP status
|
|
952
|
+
{' '}
|
|
953
|
+
<Icon name="info" size={16} />
|
|
954
|
+
</span>
|
|
955
|
+
)}
|
|
956
|
+
value="EHC plan in place"
|
|
496
957
|
/>
|
|
497
958
|
</div>
|
|
498
959
|
),
|
|
499
960
|
},
|
|
500
|
-
'
|
|
961
|
+
'The `label` slot accepts any `ReactNode` — inline icons can annotate labels that require clarification. An `Icon` placed inline with the label text signals to the user that more information is available, without cluttering the value column.',
|
|
962
|
+
);
|
|
963
|
+
|
|
964
|
+
export const AccessibilityNotes: Story = withDescription(
|
|
965
|
+
{
|
|
966
|
+
parameters: {
|
|
967
|
+
controls: { disable: true },
|
|
968
|
+
docs: {
|
|
969
|
+
source: {
|
|
970
|
+
language: 'tsx',
|
|
971
|
+
code: `
|
|
972
|
+
import { Row } from '@arbor-education/design-system.components';
|
|
973
|
+
|
|
974
|
+
// Static: tabIndex="-1", no ARIA role, not keyboard-focusable
|
|
975
|
+
<Row label="Year group" value="Year 9" />
|
|
976
|
+
|
|
977
|
+
// Clickable: tabIndex="0", Enter/Space activate onClick
|
|
978
|
+
// ⚠️ No role="button" — screen readers may not announce as interactive
|
|
979
|
+
<Row label="SEN record" value="SEN Support" onClick={handleClick} />
|
|
980
|
+
|
|
981
|
+
// href: real <a> tag — full native link semantics, announced as a link ✓
|
|
982
|
+
<Row label="Email address" value="admin@school.example" href="mailto:admin@school.example" />
|
|
983
|
+
`.trim(),
|
|
984
|
+
},
|
|
985
|
+
},
|
|
986
|
+
},
|
|
987
|
+
render: AccessibilityNotesTemplate,
|
|
988
|
+
},
|
|
989
|
+
[
|
|
990
|
+
'**Known accessibility gap:** Clickable rows (`onClick` provided) render as a `<div>` with no `role="button"`.',
|
|
991
|
+
'Screen readers will not announce them as interactive.',
|
|
992
|
+
'Keyboard users can still activate via Tab + Enter/Space, but VoiceOver/NVDA users have no indication the row is actionable.',
|
|
993
|
+
'',
|
|
994
|
+
'`href` rows are the exception — they render a real `<a>` tag with full native link semantics.',
|
|
995
|
+
'',
|
|
996
|
+
'If your use case requires screen-reader announcements on clickable rows, pass `className` with a workaround or request a component update.',
|
|
997
|
+
].join(' '),
|
|
998
|
+
);
|
|
999
|
+
|
|
1000
|
+
export const CustomClassAndStyle: Story = withDescription(
|
|
1001
|
+
{
|
|
1002
|
+
parameters: {
|
|
1003
|
+
controls: { disable: true },
|
|
1004
|
+
docs: {
|
|
1005
|
+
source: {
|
|
1006
|
+
language: 'tsx',
|
|
1007
|
+
code: `
|
|
1008
|
+
import { Row } from '@arbor-education/design-system.components';
|
|
1009
|
+
|
|
1010
|
+
function HighlightedRow() {
|
|
1011
|
+
return (
|
|
1012
|
+
<Row
|
|
1013
|
+
label="Safeguarding flag"
|
|
1014
|
+
value="CP Plan — Stage 3"
|
|
1015
|
+
className="my-highlighted-row"
|
|
1016
|
+
style={{ borderLeft: '4px solid var(--color-semantic-destructive-600)' }}
|
|
1017
|
+
/>
|
|
1018
|
+
);
|
|
1019
|
+
}
|
|
1020
|
+
export default HighlightedRow;
|
|
1021
|
+
`.trim(),
|
|
1022
|
+
},
|
|
1023
|
+
},
|
|
1024
|
+
},
|
|
1025
|
+
render: () => (
|
|
1026
|
+
<Row
|
|
1027
|
+
label="Safeguarding flag"
|
|
1028
|
+
value="CP Plan — Stage 3"
|
|
1029
|
+
className="my-highlighted-row"
|
|
1030
|
+
style={{ borderLeft: '4px solid var(--color-semantic-destructive-600)' }}
|
|
1031
|
+
/>
|
|
1032
|
+
),
|
|
1033
|
+
},
|
|
1034
|
+
'`className` and `style` are the only HTML-attribute escape hatches — `Row` does not spread arbitrary HTML attributes. Use `className` to apply component-token overrides or modifier classes; use `style` for one-off inline tweaks.',
|
|
1035
|
+
);
|
|
1036
|
+
|
|
1037
|
+
export const EmptyRow: Story = withDescription(
|
|
1038
|
+
{
|
|
1039
|
+
parameters: {
|
|
1040
|
+
controls: { disable: true },
|
|
1041
|
+
docs: {
|
|
1042
|
+
source: {
|
|
1043
|
+
language: 'tsx',
|
|
1044
|
+
code: `
|
|
1045
|
+
import { Row } from '@arbor-education/design-system.components';
|
|
1046
|
+
|
|
1047
|
+
// All props omitted — renders three empty spans in a flex container
|
|
1048
|
+
function EmptyRowExample() {
|
|
1049
|
+
return <Row />;
|
|
1050
|
+
}
|
|
1051
|
+
export default EmptyRowExample;
|
|
1052
|
+
`.trim(),
|
|
1053
|
+
},
|
|
1054
|
+
},
|
|
1055
|
+
},
|
|
1056
|
+
render: () => <Row />,
|
|
1057
|
+
},
|
|
1058
|
+
'All props omitted. The component renders without crashing — three empty `<span>` elements in a flex container at the default row height. A `Row` with no props is visible as an empty strip, not invisible, so conditionally-rendered row content produces a layout placeholder rather than disappearing silently.',
|
|
501
1059
|
);
|