@arbor-education/design-system.components 3.2.0 → 3.3.0
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/.gather/skills/write-stories/README.md +8 -4
- package/.gather/skills/write-stories/SKILL.md +59 -5
- package/CHANGELOG.md +24 -0
- package/dist/components/breadcrumbs/Breadcrumbs.stories.d.ts +1 -0
- package/dist/components/breadcrumbs/Breadcrumbs.stories.d.ts.map +1 -1
- package/dist/components/breadcrumbs/Breadcrumbs.stories.js +103 -21
- package/dist/components/breadcrumbs/Breadcrumbs.stories.js.map +1 -1
- package/dist/components/card/Card.stories.d.ts.map +1 -1
- package/dist/components/card/Card.stories.js +46 -41
- package/dist/components/card/Card.stories.js.map +1 -1
- package/dist/components/editableText/EditableText.stories.d.ts.map +1 -1
- package/dist/components/editableText/EditableText.stories.js +19 -6
- package/dist/components/editableText/EditableText.stories.js.map +1 -1
- package/dist/components/filterBar/FilterBar.d.ts +2 -2
- package/dist/components/filterBar/FilterBar.d.ts.map +1 -1
- package/dist/components/filterBar/FilterBar.js +7 -3
- package/dist/components/filterBar/FilterBar.js.map +1 -1
- package/dist/components/filterBar/FilterBar.stories.d.ts +16 -0
- package/dist/components/filterBar/FilterBar.stories.d.ts.map +1 -1
- package/dist/components/filterBar/FilterBar.stories.js +101 -7
- package/dist/components/filterBar/FilterBar.stories.js.map +1 -1
- package/dist/components/filterBar/FilterBar.test.js +30 -0
- package/dist/components/filterBar/FilterBar.test.js.map +1 -1
- package/dist/components/formField/fieldset/Fieldset.stories.d.ts.map +1 -1
- package/dist/components/formField/fieldset/Fieldset.stories.js +5 -3
- package/dist/components/formField/fieldset/Fieldset.stories.js.map +1 -1
- package/dist/components/row/Row.stories.d.ts.map +1 -1
- package/dist/components/row/Row.stories.js +22 -13
- package/dist/components/row/Row.stories.js.map +1 -1
- package/dist/components/sidebar/Sidebar.stories.d.ts.map +1 -1
- package/dist/components/sidebar/Sidebar.stories.js +19 -24
- package/dist/components/sidebar/Sidebar.stories.js.map +1 -1
- package/dist/components/table/Table.d.ts.map +1 -1
- package/dist/components/table/Table.js +2 -1
- package/dist/components/table/Table.js.map +1 -1
- package/dist/components/table/Table.stories.d.ts +7 -0
- package/dist/components/table/Table.stories.d.ts.map +1 -1
- package/dist/components/table/Table.stories.js +60 -10
- package/dist/components/table/Table.stories.js.map +1 -1
- package/dist/components/table/Table.test.js +23 -0
- package/dist/components/table/Table.test.js.map +1 -1
- package/dist/components/table/cellRenderers/SelectDropdownCellRenderer.stories.d.ts.map +1 -1
- package/dist/components/table/cellRenderers/SelectDropdownCellRenderer.stories.js +8 -5
- package/dist/components/table/cellRenderers/SelectDropdownCellRenderer.stories.js.map +1 -1
- package/dist/components/table/tableControls/TableControls.d.ts +2 -0
- package/dist/components/table/tableControls/TableControls.d.ts.map +1 -1
- package/dist/components/table/tableControls/TableControls.js +2 -2
- package/dist/components/table/tableControls/TableControls.js.map +1 -1
- package/dist/components/table/tableControls/TableControls.stories.d.ts +26 -1
- package/dist/components/table/tableControls/TableControls.stories.d.ts.map +1 -1
- package/dist/components/table/tableControls/TableControls.stories.js +50 -21
- package/dist/components/table/tableControls/TableControls.stories.js.map +1 -1
- package/dist/components/table/tableControls/TableControls.test.js +16 -0
- package/dist/components/table/tableControls/TableControls.test.js.map +1 -1
- package/dist/components/table/useTableSettings.d.ts +2 -0
- package/dist/components/table/useTableSettings.d.ts.map +1 -1
- package/dist/components/table/useTableSettings.js +12 -7
- package/dist/components/table/useTableSettings.js.map +1 -1
- package/dist/components/widget/Widget.stories.d.ts +13 -0
- package/dist/components/widget/Widget.stories.d.ts.map +1 -1
- package/dist/components/widget/Widget.stories.js +45 -6
- package/dist/components/widget/Widget.stories.js.map +1 -1
- package/dist/components/widgetCalendar/WidgetCalendar.stories.d.ts +88 -16
- package/dist/components/widgetCalendar/WidgetCalendar.stories.d.ts.map +1 -1
- package/dist/components/widgetCalendar/WidgetCalendar.stories.js +443 -30
- package/dist/components/widgetCalendar/WidgetCalendar.stories.js.map +1 -1
- package/dist/index.css +1 -4
- package/dist/index.css.map +1 -1
- package/package.json +1 -1
- package/src/components/breadcrumbs/Breadcrumbs.stories.tsx +112 -21
- package/src/components/card/Card.stories.tsx +46 -41
- package/src/components/datePicker/datePicker.scss +0 -5
- package/src/components/editableText/EditableText.stories.tsx +26 -29
- package/src/components/filterBar/FilterBar.stories.tsx +126 -4
- package/src/components/filterBar/FilterBar.test.tsx +51 -0
- package/src/components/filterBar/FilterBar.tsx +18 -12
- package/src/components/formField/fieldset/Fieldset.stories.tsx +5 -3
- package/src/components/row/Row.stories.tsx +20 -14
- package/src/components/sidebar/Sidebar.stories.tsx +61 -112
- package/src/components/table/Table.stories.tsx +82 -10
- package/src/components/table/Table.test.tsx +42 -0
- package/src/components/table/Table.tsx +2 -0
- package/src/components/table/cellRenderers/SelectDropdownCellRenderer.stories.tsx +8 -5
- package/src/components/table/tableControls/TableControls.stories.tsx +54 -21
- package/src/components/table/tableControls/TableControls.test.tsx +20 -0
- package/src/components/table/tableControls/TableControls.tsx +6 -0
- package/src/components/table/useTableSettings.ts +20 -6
- package/src/components/widget/Widget.stories.tsx +61 -6
- package/src/components/widgetCalendar/WidgetCalendar.stories.tsx +547 -74
- package/src/tokens.scss +1 -0
|
@@ -341,7 +341,6 @@ const LongTextTemplate = () => {
|
|
|
341
341
|
const EscapeToCancelTemplate = () => {
|
|
342
342
|
const [text, setText] = useState('Mr. David Chen');
|
|
343
343
|
const [savedText, setSavedText] = useState('Mr. David Chen');
|
|
344
|
-
const [cancelCount, setCancelCount] = useState(0);
|
|
345
344
|
|
|
346
345
|
return (
|
|
347
346
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '1rem', maxWidth: '480px' }}>
|
|
@@ -352,38 +351,22 @@ const EscapeToCancelTemplate = () => {
|
|
|
352
351
|
setSavedText(val);
|
|
353
352
|
}}
|
|
354
353
|
/>
|
|
355
|
-
<
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
</p>
|
|
361
|
-
<p className="ds-text" style={{ margin: 0, color: 'var(--color-grey-600)', fontSize: '0.875rem' }}>
|
|
362
|
-
Escapes pressed:
|
|
363
|
-
{' '}
|
|
364
|
-
{cancelCount}
|
|
365
|
-
</p>
|
|
366
|
-
</div>
|
|
354
|
+
<p className="ds-text" style={{ margin: 0, color: 'var(--color-grey-600)', fontSize: '0.875rem' }}>
|
|
355
|
+
Saved value: "
|
|
356
|
+
{savedText}
|
|
357
|
+
"
|
|
358
|
+
</p>
|
|
367
359
|
<p className="ds-text" style={{ margin: 0, fontSize: '0.875rem' }}>
|
|
368
360
|
Try editing the text above, type something new, then press
|
|
369
361
|
{' '}
|
|
370
362
|
<kbd>Escape</kbd>
|
|
371
363
|
{' '}
|
|
372
|
-
— the
|
|
373
|
-
original value is restored without calling
|
|
364
|
+
— the saved value above won't change, because
|
|
374
365
|
{' '}
|
|
375
366
|
<code>onEditSave</code>
|
|
376
|
-
|
|
367
|
+
{' '}
|
|
368
|
+
is never called.
|
|
377
369
|
</p>
|
|
378
|
-
{/*
|
|
379
|
-
We track cancels by wrapping the component and watching for text changes
|
|
380
|
-
that don't match what we'd expect from a save — but EditableText does not
|
|
381
|
-
expose an onCancel callback. The counter below is illustrative only.
|
|
382
|
-
Reset the page to see the true saved state.
|
|
383
|
-
*/}
|
|
384
|
-
<Button variant="secondary" onClick={() => setCancelCount(c => c + 1)}>
|
|
385
|
-
Simulate cancel count (manual)
|
|
386
|
-
</Button>
|
|
387
370
|
</div>
|
|
388
371
|
);
|
|
389
372
|
};
|
|
@@ -424,12 +407,17 @@ import { useState } from 'react';
|
|
|
424
407
|
const [text, setText] = useState(
|
|
425
408
|
'Attendance requirements and absence notification procedures for all pupils enrolled in Year 10.',
|
|
426
409
|
);
|
|
410
|
+
const [lastSaved, setLastSaved] = useState(text);
|
|
427
411
|
|
|
428
412
|
<EditableText
|
|
429
413
|
text={text}
|
|
430
414
|
multiline
|
|
431
|
-
onEditSave={
|
|
415
|
+
onEditSave={(val) => {
|
|
416
|
+
setText(val);
|
|
417
|
+
setLastSaved(val);
|
|
418
|
+
}}
|
|
432
419
|
/>
|
|
420
|
+
<p>Last saved: "{lastSaved}"</p>
|
|
433
421
|
`.trim(),
|
|
434
422
|
},
|
|
435
423
|
},
|
|
@@ -573,7 +561,9 @@ const [text, setText] = useState(
|
|
|
573
561
|
'Behaviour policy and expectations for all students during lesson time, including mobile phone usage and dress code requirements',
|
|
574
562
|
);
|
|
575
563
|
|
|
576
|
-
<
|
|
564
|
+
<div style={{ maxWidth: '480px' }}>
|
|
565
|
+
<EditableText text={text} onEditSave={setText} />
|
|
566
|
+
</div>
|
|
577
567
|
`.trim(),
|
|
578
568
|
},
|
|
579
569
|
},
|
|
@@ -596,10 +586,17 @@ import { EditableText } from '@arbor-education/design-system.components';
|
|
|
596
586
|
import { useState } from 'react';
|
|
597
587
|
|
|
598
588
|
const [text, setText] = useState('Mr. David Chen');
|
|
589
|
+
const [savedText, setSavedText] = useState('Mr. David Chen');
|
|
599
590
|
|
|
600
591
|
// Pressing Escape while editing restores the pre-edit value
|
|
601
|
-
// and does NOT call onEditSave —
|
|
602
|
-
<EditableText
|
|
592
|
+
// and does NOT call onEditSave — savedText never changes.
|
|
593
|
+
<EditableText
|
|
594
|
+
text={text}
|
|
595
|
+
onEditSave={(val) => {
|
|
596
|
+
setText(val);
|
|
597
|
+
setSavedText(val);
|
|
598
|
+
}}
|
|
599
|
+
/>
|
|
603
600
|
`.trim(),
|
|
604
601
|
},
|
|
605
602
|
},
|
|
@@ -75,6 +75,12 @@ const DEVELOPER_NOTES = [
|
|
|
75
75
|
'compose custom `FilterBar.Tag` children when you need bespoke markup. Avoid mixing both patterns in',
|
|
76
76
|
'the same production instance.',
|
|
77
77
|
'',
|
|
78
|
+
'**`label` is optional.** Omit it when the value alone is self-explanatory, such as a free-text',
|
|
79
|
+
'search term or a single unambiguous criterion. Without a `label`, the tag renders just the value',
|
|
80
|
+
'(no `Label:` prefix) and the generated `actionLabel` drops the label segment too, e.g. `Open filter',
|
|
81
|
+
'Interventions` instead of `Open filter Event Type: Interventions`. Most applied tags in a filter,',
|
|
82
|
+
'sort, or group list benefit from a label, so reach for this only when it would be redundant.',
|
|
83
|
+
'',
|
|
78
84
|
'---',
|
|
79
85
|
'',
|
|
80
86
|
'### Accessibility',
|
|
@@ -106,7 +112,7 @@ const DEVELOPER_NOTES = [
|
|
|
106
112
|
'| `FilterBar.ActiveListProps` | Applied-item summary props |',
|
|
107
113
|
'| `FilterBar.TagProps` | Manually composed tag props |',
|
|
108
114
|
'| `FilterBar.TagItem` | Data shape for `ActiveList.items` |',
|
|
109
|
-
'| `FilterBarType` | Union of `filter`, `sort`, and `
|
|
115
|
+
'| `FilterBarType` | Union of `filter`, `sort`, `group`, and `add` |',
|
|
110
116
|
].join('\n');
|
|
111
117
|
|
|
112
118
|
const RELATED_COMPONENTS = [
|
|
@@ -134,7 +140,7 @@ const TOOLBAR_PROPS = [
|
|
|
134
140
|
const BUTTON_PROPS = [
|
|
135
141
|
'| Prop | Type | Default | Description |',
|
|
136
142
|
'|---|---|---|---|',
|
|
137
|
-
'| `type` | `` `filter` \\| `sort` \\| `group` `` | — | **Required.** Chooses the default label, icon, and count announcement text |',
|
|
143
|
+
'| `type` | `` `filter` \\| `sort` \\| `group` \\| `add` `` | — | **Required.** Chooses the default label, icon, and count announcement text |',
|
|
138
144
|
'| `label` | `string` | inferred from `type` | Override the visible button label |',
|
|
139
145
|
'| `count` | `number \\| null` | — | Shows a badge when `> 0`. The badge caps visually at `99+`, but the accessible label keeps the full number |',
|
|
140
146
|
'| `icon` | `IconName` | inferred from `type` | Override the default icon for that trigger type |',
|
|
@@ -163,8 +169,8 @@ const ACTIVE_LIST_PROPS = [
|
|
|
163
169
|
const TAG_PROPS = [
|
|
164
170
|
'| Prop | Type | Default | Description |',
|
|
165
171
|
'|---|---|---|---|',
|
|
166
|
-
'| `type` | `` `filter` \\| `sort` \\| `group` `` | — | **Required.** Sets the default icon and action language |',
|
|
167
|
-
'| `label` | `string` | — |
|
|
172
|
+
'| `type` | `` `filter` \\| `sort` \\| `group` \\| `add` `` | — | **Required.** Sets the default icon and action language |',
|
|
173
|
+
'| `label` | `string` | — | Criterion label, e.g. `Year group`. Optional — omit it when the value alone is self-explanatory; the tag then renders just the value with no `Label:` prefix, and the generated `actionLabel` drops the label segment too |',
|
|
168
174
|
'| `value` | `ReactNode` | — | **Required.** Current applied value, e.g. `Year 10` |',
|
|
169
175
|
'| `id` | `string` | derived from `fieldId` or label | Stable identifier for list rendering and event payloads |',
|
|
170
176
|
'| `fieldId` | `string` | — | Optional DOM id for the field that should receive focus after opening an editor |',
|
|
@@ -188,6 +194,7 @@ type FilterBarStoryArgs = FilterBar.Props & {
|
|
|
188
194
|
filterCount: number;
|
|
189
195
|
sortCount: number;
|
|
190
196
|
groupCount: number;
|
|
197
|
+
addCount: number;
|
|
191
198
|
wrap: boolean;
|
|
192
199
|
collapseOverflow: boolean;
|
|
193
200
|
overflowActionLabel: string;
|
|
@@ -229,6 +236,7 @@ const StoryShell = ({
|
|
|
229
236
|
const onOpenFilter = fn();
|
|
230
237
|
const onOpenSort = fn();
|
|
231
238
|
const onOpenGroup = fn();
|
|
239
|
+
const onOpenAdd = fn();
|
|
232
240
|
const onOpenAppliedItem = fn();
|
|
233
241
|
const onRemoveAppliedItem = fn();
|
|
234
242
|
const onShowOverflow = fn();
|
|
@@ -337,12 +345,14 @@ const PANEL_FIELD_IDS: Record<FilterBar.Type, string> = {
|
|
|
337
345
|
filter: 'attendance-filter-year-group',
|
|
338
346
|
sort: 'attendance-sort-order',
|
|
339
347
|
group: 'attendance-group-by',
|
|
348
|
+
add: 'attendance-add-criteria',
|
|
340
349
|
};
|
|
341
350
|
|
|
342
351
|
const PANEL_TITLES: Record<FilterBar.Type, string> = {
|
|
343
352
|
filter: 'Filters',
|
|
344
353
|
sort: 'Sorting',
|
|
345
354
|
group: 'Grouping',
|
|
355
|
+
add: 'Add criteria',
|
|
346
356
|
};
|
|
347
357
|
|
|
348
358
|
// ---------------------------------------------------------------------------
|
|
@@ -455,6 +465,14 @@ const meta = {
|
|
|
455
465
|
type: { summary: 'number' },
|
|
456
466
|
},
|
|
457
467
|
},
|
|
468
|
+
'addCount': {
|
|
469
|
+
control: { type: 'number', min: 0, max: 250 },
|
|
470
|
+
description: 'Applied add count shown on the Add trigger badge.',
|
|
471
|
+
table: {
|
|
472
|
+
category: 'FilterBar.Button',
|
|
473
|
+
type: { summary: 'number' },
|
|
474
|
+
},
|
|
475
|
+
},
|
|
458
476
|
'wrap': {
|
|
459
477
|
control: 'boolean',
|
|
460
478
|
description: 'Allows the applied-item summary to wrap onto multiple lines.',
|
|
@@ -514,6 +532,7 @@ const DefaultTemplate = ({
|
|
|
514
532
|
filterCount,
|
|
515
533
|
sortCount,
|
|
516
534
|
groupCount,
|
|
535
|
+
addCount,
|
|
517
536
|
wrap,
|
|
518
537
|
collapseOverflow,
|
|
519
538
|
overflowActionLabel,
|
|
@@ -525,6 +544,7 @@ const DefaultTemplate = ({
|
|
|
525
544
|
<FilterBar.Button type="filter" count={filterCount} onClick={onOpenFilter} />
|
|
526
545
|
<FilterBar.Button type="sort" count={sortCount} onClick={onOpenSort} />
|
|
527
546
|
<FilterBar.Button type="group" count={groupCount} onClick={onOpenGroup} />
|
|
547
|
+
<FilterBar.Button type="add" count={addCount} onClick={onOpenAdd} />
|
|
528
548
|
</FilterBar.Toolbar>
|
|
529
549
|
<FilterBar.ActiveList
|
|
530
550
|
items={DEFAULT_ITEMS}
|
|
@@ -580,6 +600,7 @@ const ModalWiringTemplate = () => {
|
|
|
580
600
|
filter: 0,
|
|
581
601
|
sort: 0,
|
|
582
602
|
group: 0,
|
|
603
|
+
add: 0,
|
|
583
604
|
});
|
|
584
605
|
|
|
585
606
|
const items: FilterBar.TagItem[] = APPLIED_ITEM_SEEDS.flatMap((item) => {
|
|
@@ -639,6 +660,14 @@ const ModalWiringTemplate = () => {
|
|
|
639
660
|
aria-haspopup="dialog"
|
|
640
661
|
onClick={() => openModal('group')}
|
|
641
662
|
/>
|
|
663
|
+
<FilterBar.Button
|
|
664
|
+
type="add"
|
|
665
|
+
count={counts.add}
|
|
666
|
+
aria-controls={modalId}
|
|
667
|
+
aria-expanded={open && activeType === 'add'}
|
|
668
|
+
aria-haspopup="dialog"
|
|
669
|
+
onClick={() => openModal('add')}
|
|
670
|
+
/>
|
|
642
671
|
</FilterBar.Toolbar>
|
|
643
672
|
<FilterBar.ActiveList
|
|
644
673
|
ariaLabel="Applied attendance filters, sorting, and grouping"
|
|
@@ -744,6 +773,7 @@ export const Default: Story = withDescription(
|
|
|
744
773
|
'filterCount': 3,
|
|
745
774
|
'sortCount': 1,
|
|
746
775
|
'groupCount': 1,
|
|
776
|
+
'addCount': 0,
|
|
747
777
|
'wrap': false,
|
|
748
778
|
'collapseOverflow': true,
|
|
749
779
|
'overflowActionLabel': 'Show more applied items',
|
|
@@ -783,6 +813,7 @@ function HighCountsExample() {
|
|
|
783
813
|
<FilterBar.Button type="filter" count={142} onClick={() => {}} />
|
|
784
814
|
<FilterBar.Button type="sort" count={103} onClick={() => {}} />
|
|
785
815
|
<FilterBar.Button type="group" count={12} onClick={() => {}} />
|
|
816
|
+
<FilterBar.Button type="add" onClick={() => {}} />
|
|
786
817
|
</FilterBar.Toolbar>
|
|
787
818
|
<FilterBar.ActiveList items={items} ariaLabel="Applied attendance filters, sorting, and grouping" />
|
|
788
819
|
</FilterBar>
|
|
@@ -801,6 +832,7 @@ export default HighCountsExample;
|
|
|
801
832
|
<FilterBar.Button type="filter" count={142} onClick={onOpenFilter} />
|
|
802
833
|
<FilterBar.Button type="sort" count={103} onClick={onOpenSort} />
|
|
803
834
|
<FilterBar.Button type="group" count={12} onClick={onOpenGroup} />
|
|
835
|
+
<FilterBar.Button type="add" onClick={onOpenAdd} />
|
|
804
836
|
</FilterBar.Toolbar>
|
|
805
837
|
<FilterBar.ActiveList
|
|
806
838
|
items={DEFAULT_ITEMS}
|
|
@@ -841,12 +873,14 @@ const panelFieldIds: Record<FilterBarType, string> = {
|
|
|
841
873
|
filter: 'attendance-filter-year-group',
|
|
842
874
|
sort: 'attendance-sort-order',
|
|
843
875
|
group: 'attendance-group-by',
|
|
876
|
+
add: 'attendance-add-criteria',
|
|
844
877
|
};
|
|
845
878
|
|
|
846
879
|
const PANEL_TITLES: Record<FilterBarType, string> = {
|
|
847
880
|
filter: 'Filters',
|
|
848
881
|
sort: 'Sorting',
|
|
849
882
|
group: 'Grouping',
|
|
883
|
+
add: 'Add criteria',
|
|
850
884
|
};
|
|
851
885
|
|
|
852
886
|
const seedItems: FilterBarTagItem[] = [
|
|
@@ -931,6 +965,7 @@ function FilterBarModalWiringExample() {
|
|
|
931
965
|
filter: 0,
|
|
932
966
|
sort: 0,
|
|
933
967
|
group: 0,
|
|
968
|
+
add: 0,
|
|
934
969
|
});
|
|
935
970
|
|
|
936
971
|
const items = seedItems.flatMap((item) => {
|
|
@@ -985,6 +1020,14 @@ function FilterBarModalWiringExample() {
|
|
|
985
1020
|
aria-haspopup="dialog"
|
|
986
1021
|
onClick={() => openModal('group')}
|
|
987
1022
|
/>
|
|
1023
|
+
<FilterBar.Button
|
|
1024
|
+
type="add"
|
|
1025
|
+
count={counts.add}
|
|
1026
|
+
aria-controls={modalId}
|
|
1027
|
+
aria-expanded={open && activeType === 'add'}
|
|
1028
|
+
aria-haspopup="dialog"
|
|
1029
|
+
onClick={() => openModal('add')}
|
|
1030
|
+
/>
|
|
988
1031
|
</FilterBar.Toolbar>
|
|
989
1032
|
|
|
990
1033
|
<FilterBar.ActiveList
|
|
@@ -1156,6 +1199,85 @@ export default OverflowExample;
|
|
|
1156
1199
|
].join(' '),
|
|
1157
1200
|
);
|
|
1158
1201
|
|
|
1202
|
+
export const WithoutLabels: Story = withDescription(
|
|
1203
|
+
{
|
|
1204
|
+
parameters: {
|
|
1205
|
+
controls: { disable: true },
|
|
1206
|
+
docs: {
|
|
1207
|
+
source: {
|
|
1208
|
+
language: 'tsx',
|
|
1209
|
+
code: `
|
|
1210
|
+
import { FilterBar, type FilterBarTagItem } from '@arbor-education/design-system.components';
|
|
1211
|
+
|
|
1212
|
+
const items: FilterBarTagItem[] = [
|
|
1213
|
+
{ id: 'search-term', type: 'filter', value: 'Jordan Casey' },
|
|
1214
|
+
{ id: 'year-group', type: 'filter', label: 'Year group', value: 'Year 10' },
|
|
1215
|
+
{ id: 'attendance-band', type: 'filter', label: 'Attendance band', value: 'Below 90%' },
|
|
1216
|
+
];
|
|
1217
|
+
|
|
1218
|
+
function WithoutLabelsExample() {
|
|
1219
|
+
return (
|
|
1220
|
+
<div style={{ width: '100%', maxWidth: 'min(100%, 72rem)', padding: 'var(--spacing-large)', boxSizing: 'border-box' }}>
|
|
1221
|
+
<FilterBar aria-label="Attendance view controls">
|
|
1222
|
+
<FilterBar.Toolbar aria-label="Filter, sort, and group controls">
|
|
1223
|
+
<FilterBar.Button type="filter" count={3} onClick={() => {}} />
|
|
1224
|
+
</FilterBar.Toolbar>
|
|
1225
|
+
<FilterBar.ActiveList items={items} ariaLabel="Applied attendance filters" />
|
|
1226
|
+
</FilterBar>
|
|
1227
|
+
</div>
|
|
1228
|
+
);
|
|
1229
|
+
}
|
|
1230
|
+
export default WithoutLabelsExample;
|
|
1231
|
+
`.trim(),
|
|
1232
|
+
},
|
|
1233
|
+
},
|
|
1234
|
+
},
|
|
1235
|
+
render: () => (
|
|
1236
|
+
<StoryShell>
|
|
1237
|
+
<FilterBar aria-label="Attendance view controls">
|
|
1238
|
+
<FilterBar.Toolbar aria-label="Filter, sort, and group controls">
|
|
1239
|
+
<FilterBar.Button type="filter" count={3} onClick={onOpenFilter} />
|
|
1240
|
+
</FilterBar.Toolbar>
|
|
1241
|
+
<FilterBar.ActiveList
|
|
1242
|
+
items={[
|
|
1243
|
+
{
|
|
1244
|
+
id: 'search-term',
|
|
1245
|
+
type: 'filter',
|
|
1246
|
+
value: 'Jordan Casey',
|
|
1247
|
+
onClick: onOpenAppliedItem,
|
|
1248
|
+
onRemove: onRemoveAppliedItem,
|
|
1249
|
+
},
|
|
1250
|
+
{
|
|
1251
|
+
id: 'year-group',
|
|
1252
|
+
type: 'filter',
|
|
1253
|
+
label: 'Year group',
|
|
1254
|
+
value: 'Year 10',
|
|
1255
|
+
onClick: onOpenAppliedItem,
|
|
1256
|
+
onRemove: onRemoveAppliedItem,
|
|
1257
|
+
},
|
|
1258
|
+
{
|
|
1259
|
+
id: 'attendance-band',
|
|
1260
|
+
type: 'filter',
|
|
1261
|
+
label: 'Attendance band',
|
|
1262
|
+
value: 'Below 90%',
|
|
1263
|
+
onClick: onOpenAppliedItem,
|
|
1264
|
+
onRemove: onRemoveAppliedItem,
|
|
1265
|
+
},
|
|
1266
|
+
]}
|
|
1267
|
+
ariaLabel="Applied attendance filters"
|
|
1268
|
+
/>
|
|
1269
|
+
</FilterBar>
|
|
1270
|
+
</StoryShell>
|
|
1271
|
+
),
|
|
1272
|
+
},
|
|
1273
|
+
[
|
|
1274
|
+
'Omit `label` when the value alone is self-explanatory, such as the free-text search term shown',
|
|
1275
|
+
'first here. That tag renders as just `Jordan Casey` with a shorter generated action label',
|
|
1276
|
+
'(`Open filter Jordan Casey` instead of `Open filter Search: Jordan Casey`), while the labelled',
|
|
1277
|
+
'tags alongside it keep their `Label: Value` layout for comparison.',
|
|
1278
|
+
].join(' '),
|
|
1279
|
+
);
|
|
1280
|
+
|
|
1159
1281
|
export const Empty: Story = withDescription(
|
|
1160
1282
|
{
|
|
1161
1283
|
parameters: {
|
|
@@ -171,6 +171,57 @@ describe('FilterBar', () => {
|
|
|
171
171
|
}
|
|
172
172
|
});
|
|
173
173
|
|
|
174
|
+
test('renders active list tags without a label and omits the label segment from the content and action label', async () => {
|
|
175
|
+
const user = userEvent.setup();
|
|
176
|
+
const onItemClick = vi.fn();
|
|
177
|
+
|
|
178
|
+
render(
|
|
179
|
+
<FilterBar>
|
|
180
|
+
<FilterBar.ActiveList
|
|
181
|
+
items={[
|
|
182
|
+
{
|
|
183
|
+
id: 'search',
|
|
184
|
+
type: 'filter',
|
|
185
|
+
value: 'Interventions',
|
|
186
|
+
onClick: onItemClick,
|
|
187
|
+
},
|
|
188
|
+
]}
|
|
189
|
+
/>
|
|
190
|
+
</FilterBar>,
|
|
191
|
+
);
|
|
192
|
+
|
|
193
|
+
const tagButton = screen.getByRole('button', { name: 'Open filter Interventions' });
|
|
194
|
+
expect(tagButton).toBeInTheDocument();
|
|
195
|
+
expect(screen.queryByText(':')).not.toBeInTheDocument();
|
|
196
|
+
|
|
197
|
+
await user.click(tagButton);
|
|
198
|
+
|
|
199
|
+
expect(onItemClick).toHaveBeenCalledOnce();
|
|
200
|
+
const [clickedItem] = onItemClick.mock.calls[0] ?? [];
|
|
201
|
+
expect(clickedItem).toMatchObject({ id: 'search', type: 'filter', value: 'Interventions' });
|
|
202
|
+
expect(clickedItem?.label).toBeUndefined();
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
test('renders a manually composed FilterBar.Tag without a label', async () => {
|
|
206
|
+
const user = userEvent.setup();
|
|
207
|
+
const onClick = vi.fn();
|
|
208
|
+
|
|
209
|
+
render(
|
|
210
|
+
<FilterBar>
|
|
211
|
+
<FilterBar.ActiveList>
|
|
212
|
+
<FilterBar.Tag type="sort" value="Priority first" onClick={onClick} />
|
|
213
|
+
</FilterBar.ActiveList>
|
|
214
|
+
</FilterBar>,
|
|
215
|
+
);
|
|
216
|
+
|
|
217
|
+
const tagButton = screen.getByRole('button', { name: 'Open sort Priority first' });
|
|
218
|
+
expect(screen.getByText('Priority first')).toBeInTheDocument();
|
|
219
|
+
expect(screen.queryByText(':')).not.toBeInTheDocument();
|
|
220
|
+
|
|
221
|
+
await user.click(tagButton);
|
|
222
|
+
expect(onClick).toHaveBeenCalledOnce();
|
|
223
|
+
});
|
|
224
|
+
|
|
174
225
|
test('returns focus to the first toolbar button after sequentially removing all tags from their remove buttons', async () => {
|
|
175
226
|
const user = userEvent.setup();
|
|
176
227
|
|
|
@@ -7,12 +7,12 @@ import { TagList } from 'Components/tagList/TagList';
|
|
|
7
7
|
import classNames from 'classnames';
|
|
8
8
|
import { Children, forwardRef, type ButtonHTMLAttributes, type HTMLAttributes, type MouseEventHandler, type RefObject } from 'react';
|
|
9
9
|
|
|
10
|
-
export type FilterBarType = 'filter' | 'sort' | 'group';
|
|
10
|
+
export type FilterBarType = 'filter' | 'sort' | 'group' | 'add';
|
|
11
11
|
|
|
12
12
|
export type FilterBarTagItem = {
|
|
13
13
|
id: string;
|
|
14
14
|
type: FilterBarType;
|
|
15
|
-
label
|
|
15
|
+
label?: string;
|
|
16
16
|
value: React.ReactNode;
|
|
17
17
|
fieldId?: string;
|
|
18
18
|
icon?: IconName;
|
|
@@ -64,12 +64,14 @@ const FILTER_BAR_TYPE_LABELS: Record<FilterBarType, string> = {
|
|
|
64
64
|
filter: 'Filter',
|
|
65
65
|
sort: 'Sort',
|
|
66
66
|
group: 'Group',
|
|
67
|
+
add: 'Add',
|
|
67
68
|
};
|
|
68
69
|
|
|
69
70
|
const FILTER_BAR_TYPE_ICONS: Record<FilterBarType, IconName> = {
|
|
70
71
|
filter: 'sliders-horizontal',
|
|
71
72
|
sort: 'sorting',
|
|
72
73
|
group: 'list-tree',
|
|
74
|
+
add: 'circle-plus',
|
|
73
75
|
};
|
|
74
76
|
|
|
75
77
|
const getVisibleCount = (count?: number | null): string | null => {
|
|
@@ -87,17 +89,21 @@ const getButtonA11yLabel = (type: FilterBarType, label: string, count?: number |
|
|
|
87
89
|
return `Open ${label.toLowerCase()}. ${count} ${type} item${count === 1 ? '' : 's'} applied`;
|
|
88
90
|
};
|
|
89
91
|
|
|
90
|
-
const getTagActionLabel = (item: FilterBarTagItem): string => `Open ${item.type} ${item.label}: ${item.value}`;
|
|
92
|
+
const getTagActionLabel = (item: FilterBarTagItem): string => item.label ? `Open ${item.type} ${item.label}: ${item.value}` : `Open ${item.type} ${item.value}`;
|
|
91
93
|
|
|
92
|
-
const FilterBarTagContent = ({ label, value }: Pick<FilterBarTagProps, 'label' | 'value'>) =>
|
|
93
|
-
|
|
94
|
-
<span className="ds-filter-bar__tag-
|
|
95
|
-
{label
|
|
96
|
-
|
|
94
|
+
const FilterBarTagContent = ({ label, value }: Pick<FilterBarTagProps, 'label' | 'value'>) => {
|
|
95
|
+
return (
|
|
96
|
+
<span className="ds-filter-bar__tag-content">
|
|
97
|
+
{label && (
|
|
98
|
+
<span className="ds-filter-bar__tag-label">
|
|
99
|
+
{label}
|
|
100
|
+
:
|
|
101
|
+
</span>
|
|
102
|
+
)}
|
|
103
|
+
<span className="ds-filter-bar__tag-value">{value}</span>
|
|
97
104
|
</span>
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
);
|
|
105
|
+
);
|
|
106
|
+
};
|
|
101
107
|
|
|
102
108
|
export const FilterBar = ({
|
|
103
109
|
children,
|
|
@@ -193,7 +199,7 @@ const FilterBarTag = ({
|
|
|
193
199
|
onRemove,
|
|
194
200
|
}: FilterBarTagProps): React.JSX.Element => {
|
|
195
201
|
const item: FilterBarTagItem = {
|
|
196
|
-
id: id ?? fieldId ?? `${type}-${label}`,
|
|
202
|
+
id: id ?? fieldId ?? `${type}-${label}-${value}`,
|
|
197
203
|
type,
|
|
198
204
|
label,
|
|
199
205
|
value,
|
|
@@ -658,7 +658,7 @@ export default function DisabledFieldset() {
|
|
|
658
658
|
);
|
|
659
659
|
|
|
660
660
|
export const DisabledWithCheckboxes: Story = withDescription(
|
|
661
|
-
'The most visually compelling demonstration of the `disabled` prop — all checkboxes are greyed out and non-interactive with a single `disabled` on the fieldset.
|
|
661
|
+
'The most visually compelling demonstration of the `disabled` prop — all checkboxes are greyed out and non-interactive with a single `disabled` on the fieldset. Checked state is preserved and rendered independently of the `disabled` attribute — note that the two already-checked boxes stay visually checked while disabled, showing consent decisions captured before the form was locked for review.',
|
|
662
662
|
{
|
|
663
663
|
parameters: {
|
|
664
664
|
controls: { disable: true },
|
|
@@ -669,6 +669,8 @@ import { Fieldset, CheckboxInput } from '@arbor-education/design-system.componen
|
|
|
669
669
|
|
|
670
670
|
export default function DisabledCheckboxesFieldset() {
|
|
671
671
|
return (
|
|
672
|
+
// checked state survives independently of disabled — these two were
|
|
673
|
+
// already consented to before the form was locked for review
|
|
672
674
|
<Fieldset legend="Consent and permissions" disabled>
|
|
673
675
|
<CheckboxInput
|
|
674
676
|
id="consent-trips-dis"
|
|
@@ -754,7 +756,7 @@ export default function DisabledRadioFieldset() {
|
|
|
754
756
|
name="diet"
|
|
755
757
|
value="halal"
|
|
756
758
|
label="Halal"
|
|
757
|
-
|
|
759
|
+
checked
|
|
758
760
|
/>
|
|
759
761
|
<RadioButtonInput
|
|
760
762
|
id="diet-vegetarian-dis"
|
|
@@ -788,7 +790,7 @@ export default function DisabledRadioFieldset() {
|
|
|
788
790
|
name="diet"
|
|
789
791
|
value="halal"
|
|
790
792
|
label="Halal"
|
|
791
|
-
|
|
793
|
+
checked
|
|
792
794
|
/>
|
|
793
795
|
<RadioButtonInput
|
|
794
796
|
id="diet-vegetarian-dis"
|
|
@@ -320,13 +320,11 @@ const MixedClickableAndStaticTemplate = () => (
|
|
|
320
320
|
<Row
|
|
321
321
|
label="SEN status"
|
|
322
322
|
value="SEN Support"
|
|
323
|
-
note="Review due May 2026"
|
|
324
323
|
onClick={() => {}}
|
|
325
324
|
/>
|
|
326
325
|
<Row
|
|
327
326
|
label="Attendance (YTD)"
|
|
328
327
|
value="88.3%"
|
|
329
|
-
note="Below 90% threshold"
|
|
330
328
|
onClick={() => {}}
|
|
331
329
|
/>
|
|
332
330
|
</Section>
|
|
@@ -778,13 +776,11 @@ function PupilDetailSection() {
|
|
|
778
776
|
<Row
|
|
779
777
|
label="SEN status"
|
|
780
778
|
value="SEN Support"
|
|
781
|
-
note="Review due May 2026"
|
|
782
779
|
onClick={() => navigateToSENRecord()}
|
|
783
780
|
/>
|
|
784
781
|
<Row
|
|
785
782
|
label="Attendance (YTD)"
|
|
786
783
|
value="88.3%"
|
|
787
|
-
note="Below 90% threshold"
|
|
788
784
|
onClick={() => navigateToAttendance()}
|
|
789
785
|
/>
|
|
790
786
|
</Section>
|
|
@@ -832,7 +828,7 @@ export default PersonalDetailsSection;
|
|
|
832
828
|
},
|
|
833
829
|
render: InSectionTemplate,
|
|
834
830
|
},
|
|
835
|
-
'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
|
|
831
|
+
'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` — one row demonstrates a linked value via `href` to show that pattern working inside a realistic panel too.',
|
|
836
832
|
);
|
|
837
833
|
|
|
838
834
|
export const LongContent: Story = withDescription(
|
|
@@ -857,7 +853,6 @@ function LongContentExample() {
|
|
|
857
853
|
label="Pupil premium eligibility reason"
|
|
858
854
|
value="Free school meals — ever 6"
|
|
859
855
|
note="Verified by LA"
|
|
860
|
-
onClick={() => navigateToEligibilityDetail()}
|
|
861
856
|
/>
|
|
862
857
|
</div>
|
|
863
858
|
);
|
|
@@ -878,7 +873,6 @@ export default LongContentExample;
|
|
|
878
873
|
label="Pupil premium eligibility reason"
|
|
879
874
|
value="Free school meals — ever 6"
|
|
880
875
|
note="Verified by LA"
|
|
881
|
-
onClick={fn()}
|
|
882
876
|
/>
|
|
883
877
|
</div>
|
|
884
878
|
),
|
|
@@ -1102,16 +1096,28 @@ export const WithTooltip: Story = withDescription(
|
|
|
1102
1096
|
code: `
|
|
1103
1097
|
import { Row } from '@arbor-education/design-system.components';
|
|
1104
1098
|
|
|
1105
|
-
function
|
|
1099
|
+
function ReadOnlyRows() {
|
|
1106
1100
|
return (
|
|
1107
|
-
<
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1101
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--spacing-small)' }}>
|
|
1102
|
+
<Row
|
|
1103
|
+
label="UPN"
|
|
1104
|
+
value="A823456789012"
|
|
1105
|
+
tooltipText="Unique Pupil Number — assigned by the DfE and cannot be changed here"
|
|
1106
|
+
/>
|
|
1107
|
+
<Row
|
|
1108
|
+
label="Admission number"
|
|
1109
|
+
value="001847"
|
|
1110
|
+
tooltipText="Set at admission — contact the office to request a change"
|
|
1111
|
+
/>
|
|
1112
|
+
{/* No tooltipText — hovering this row shows nothing */}
|
|
1113
|
+
<Row
|
|
1114
|
+
label="Year group"
|
|
1115
|
+
value="Year 9"
|
|
1116
|
+
/>
|
|
1117
|
+
</div>
|
|
1112
1118
|
);
|
|
1113
1119
|
}
|
|
1114
|
-
export default
|
|
1120
|
+
export default ReadOnlyRows;
|
|
1115
1121
|
`.trim(),
|
|
1116
1122
|
},
|
|
1117
1123
|
},
|