@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.
Files changed (90) hide show
  1. package/.gather/skills/write-stories/README.md +8 -4
  2. package/.gather/skills/write-stories/SKILL.md +59 -5
  3. package/CHANGELOG.md +24 -0
  4. package/dist/components/breadcrumbs/Breadcrumbs.stories.d.ts +1 -0
  5. package/dist/components/breadcrumbs/Breadcrumbs.stories.d.ts.map +1 -1
  6. package/dist/components/breadcrumbs/Breadcrumbs.stories.js +103 -21
  7. package/dist/components/breadcrumbs/Breadcrumbs.stories.js.map +1 -1
  8. package/dist/components/card/Card.stories.d.ts.map +1 -1
  9. package/dist/components/card/Card.stories.js +46 -41
  10. package/dist/components/card/Card.stories.js.map +1 -1
  11. package/dist/components/editableText/EditableText.stories.d.ts.map +1 -1
  12. package/dist/components/editableText/EditableText.stories.js +19 -6
  13. package/dist/components/editableText/EditableText.stories.js.map +1 -1
  14. package/dist/components/filterBar/FilterBar.d.ts +2 -2
  15. package/dist/components/filterBar/FilterBar.d.ts.map +1 -1
  16. package/dist/components/filterBar/FilterBar.js +7 -3
  17. package/dist/components/filterBar/FilterBar.js.map +1 -1
  18. package/dist/components/filterBar/FilterBar.stories.d.ts +16 -0
  19. package/dist/components/filterBar/FilterBar.stories.d.ts.map +1 -1
  20. package/dist/components/filterBar/FilterBar.stories.js +101 -7
  21. package/dist/components/filterBar/FilterBar.stories.js.map +1 -1
  22. package/dist/components/filterBar/FilterBar.test.js +30 -0
  23. package/dist/components/filterBar/FilterBar.test.js.map +1 -1
  24. package/dist/components/formField/fieldset/Fieldset.stories.d.ts.map +1 -1
  25. package/dist/components/formField/fieldset/Fieldset.stories.js +5 -3
  26. package/dist/components/formField/fieldset/Fieldset.stories.js.map +1 -1
  27. package/dist/components/row/Row.stories.d.ts.map +1 -1
  28. package/dist/components/row/Row.stories.js +22 -13
  29. package/dist/components/row/Row.stories.js.map +1 -1
  30. package/dist/components/sidebar/Sidebar.stories.d.ts.map +1 -1
  31. package/dist/components/sidebar/Sidebar.stories.js +19 -24
  32. package/dist/components/sidebar/Sidebar.stories.js.map +1 -1
  33. package/dist/components/table/Table.d.ts.map +1 -1
  34. package/dist/components/table/Table.js +2 -1
  35. package/dist/components/table/Table.js.map +1 -1
  36. package/dist/components/table/Table.stories.d.ts +7 -0
  37. package/dist/components/table/Table.stories.d.ts.map +1 -1
  38. package/dist/components/table/Table.stories.js +60 -10
  39. package/dist/components/table/Table.stories.js.map +1 -1
  40. package/dist/components/table/Table.test.js +23 -0
  41. package/dist/components/table/Table.test.js.map +1 -1
  42. package/dist/components/table/cellRenderers/SelectDropdownCellRenderer.stories.d.ts.map +1 -1
  43. package/dist/components/table/cellRenderers/SelectDropdownCellRenderer.stories.js +8 -5
  44. package/dist/components/table/cellRenderers/SelectDropdownCellRenderer.stories.js.map +1 -1
  45. package/dist/components/table/tableControls/TableControls.d.ts +2 -0
  46. package/dist/components/table/tableControls/TableControls.d.ts.map +1 -1
  47. package/dist/components/table/tableControls/TableControls.js +2 -2
  48. package/dist/components/table/tableControls/TableControls.js.map +1 -1
  49. package/dist/components/table/tableControls/TableControls.stories.d.ts +26 -1
  50. package/dist/components/table/tableControls/TableControls.stories.d.ts.map +1 -1
  51. package/dist/components/table/tableControls/TableControls.stories.js +50 -21
  52. package/dist/components/table/tableControls/TableControls.stories.js.map +1 -1
  53. package/dist/components/table/tableControls/TableControls.test.js +16 -0
  54. package/dist/components/table/tableControls/TableControls.test.js.map +1 -1
  55. package/dist/components/table/useTableSettings.d.ts +2 -0
  56. package/dist/components/table/useTableSettings.d.ts.map +1 -1
  57. package/dist/components/table/useTableSettings.js +12 -7
  58. package/dist/components/table/useTableSettings.js.map +1 -1
  59. package/dist/components/widget/Widget.stories.d.ts +13 -0
  60. package/dist/components/widget/Widget.stories.d.ts.map +1 -1
  61. package/dist/components/widget/Widget.stories.js +45 -6
  62. package/dist/components/widget/Widget.stories.js.map +1 -1
  63. package/dist/components/widgetCalendar/WidgetCalendar.stories.d.ts +88 -16
  64. package/dist/components/widgetCalendar/WidgetCalendar.stories.d.ts.map +1 -1
  65. package/dist/components/widgetCalendar/WidgetCalendar.stories.js +443 -30
  66. package/dist/components/widgetCalendar/WidgetCalendar.stories.js.map +1 -1
  67. package/dist/index.css +1 -4
  68. package/dist/index.css.map +1 -1
  69. package/package.json +1 -1
  70. package/src/components/breadcrumbs/Breadcrumbs.stories.tsx +112 -21
  71. package/src/components/card/Card.stories.tsx +46 -41
  72. package/src/components/datePicker/datePicker.scss +0 -5
  73. package/src/components/editableText/EditableText.stories.tsx +26 -29
  74. package/src/components/filterBar/FilterBar.stories.tsx +126 -4
  75. package/src/components/filterBar/FilterBar.test.tsx +51 -0
  76. package/src/components/filterBar/FilterBar.tsx +18 -12
  77. package/src/components/formField/fieldset/Fieldset.stories.tsx +5 -3
  78. package/src/components/row/Row.stories.tsx +20 -14
  79. package/src/components/sidebar/Sidebar.stories.tsx +61 -112
  80. package/src/components/table/Table.stories.tsx +82 -10
  81. package/src/components/table/Table.test.tsx +42 -0
  82. package/src/components/table/Table.tsx +2 -0
  83. package/src/components/table/cellRenderers/SelectDropdownCellRenderer.stories.tsx +8 -5
  84. package/src/components/table/tableControls/TableControls.stories.tsx +54 -21
  85. package/src/components/table/tableControls/TableControls.test.tsx +20 -0
  86. package/src/components/table/tableControls/TableControls.tsx +6 -0
  87. package/src/components/table/useTableSettings.ts +20 -6
  88. package/src/components/widget/Widget.stories.tsx +61 -6
  89. package/src/components/widgetCalendar/WidgetCalendar.stories.tsx +547 -74
  90. 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
- <div style={{ display: 'flex', flexDirection: 'column', gap: '0.25rem' }}>
356
- <p className="ds-text" style={{ margin: 0, color: 'var(--color-grey-600)', fontSize: '0.875rem' }}>
357
- Saved value: &quot;
358
- {savedText}
359
- &quot;
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: &quot;
356
+ {savedText}
357
+ &quot;
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&apos;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={setText}
415
+ onEditSave={(val) => {
416
+ setText(val);
417
+ setLastSaved(val);
418
+ }}
432
419
  />
420
+ <p>Last saved: &quot;{lastSaved}&quot;</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
- <EditableText text={text} onEditSave={setText} />
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 — no network request is made.
602
- <EditableText text={text} onEditSave={setText} />
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 `group` |',
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` | — | **Required.** Criterion label, e.g. `Year group` |',
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: string;
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
- <span className="ds-filter-bar__tag-content">
94
- <span className="ds-filter-bar__tag-label">
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
- <span className="ds-filter-bar__tag-value">{value}</span>
99
- </span>
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. This is how Arbor locks consent forms during a pending approval workflow.',
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
- defaultChecked
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
- defaultChecked
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 ReadOnlyRow() {
1099
+ function ReadOnlyRows() {
1106
1100
  return (
1107
- <Row
1108
- label="UPN"
1109
- value="A823456789012"
1110
- tooltipText="Unique Pupil Number — assigned by the DfE and cannot be changed here"
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 ReadOnlyRow;
1120
+ export default ReadOnlyRows;
1115
1121
  `.trim(),
1116
1122
  },
1117
1123
  },