@arbor-education/design-system.components 0.25.0 → 0.25.2

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 (235) hide show
  1. package/.gather/instructions/changesets.md +12 -0
  2. package/.gather/instructions/read-contributing.md +14 -0
  3. package/.storybook/DocsTemplate.tsx +3 -1
  4. package/.storybook/preview.ts +6 -2
  5. package/CHANGELOG.md +14 -0
  6. package/CONTRIBUTING.md +20 -0
  7. package/dist/components/avatarGroup/AvatarGroup.stories.d.ts.map +1 -1
  8. package/dist/components/avatarGroup/AvatarGroup.stories.js +4 -2
  9. package/dist/components/avatarGroup/AvatarGroup.stories.js.map +1 -1
  10. package/dist/components/card/Card.d.ts.map +1 -1
  11. package/dist/components/card/Card.js +2 -1
  12. package/dist/components/card/Card.js.map +1 -1
  13. package/dist/components/combobox/Combobox.d.ts.map +1 -1
  14. package/dist/components/combobox/Combobox.js +10 -5
  15. package/dist/components/combobox/Combobox.js.map +1 -1
  16. package/dist/components/combobox/Combobox.stories.d.ts.map +1 -1
  17. package/dist/components/combobox/Combobox.stories.js +2 -1
  18. package/dist/components/combobox/Combobox.stories.js.map +1 -1
  19. package/dist/components/combobox/Combobox.test.js +18 -9
  20. package/dist/components/combobox/Combobox.test.js.map +1 -1
  21. package/dist/components/combobox/ComboboxButtonTrigger.d.ts.map +1 -1
  22. package/dist/components/combobox/ComboboxButtonTrigger.js +3 -1
  23. package/dist/components/combobox/ComboboxButtonTrigger.js.map +1 -1
  24. package/dist/components/combobox/ComboboxListbox.d.ts.map +1 -1
  25. package/dist/components/combobox/ComboboxListbox.js +8 -4
  26. package/dist/components/combobox/ComboboxListbox.js.map +1 -1
  27. package/dist/components/combobox/buildListboxDisplayOptions.d.ts.map +1 -1
  28. package/dist/components/combobox/buildListboxDisplayOptions.js +4 -2
  29. package/dist/components/combobox/buildListboxDisplayOptions.js.map +1 -1
  30. package/dist/components/combobox/highlightLabel.d.ts.map +1 -1
  31. package/dist/components/combobox/highlightLabel.js +2 -1
  32. package/dist/components/combobox/highlightLabel.js.map +1 -1
  33. package/dist/components/combobox/useChipSelection.d.ts.map +1 -1
  34. package/dist/components/combobox/useChipSelection.js +2 -1
  35. package/dist/components/combobox/useChipSelection.js.map +1 -1
  36. package/dist/components/combobox/useComboboxListboxKeyboard.d.ts.map +1 -1
  37. package/dist/components/combobox/useComboboxListboxKeyboard.js +8 -4
  38. package/dist/components/combobox/useComboboxListboxKeyboard.js.map +1 -1
  39. package/dist/components/combobox/useComboboxPopoverBehavior.d.ts.map +1 -1
  40. package/dist/components/combobox/useComboboxPopoverBehavior.js +16 -8
  41. package/dist/components/combobox/useComboboxPopoverBehavior.js.map +1 -1
  42. package/dist/components/combobox/useComboboxState.d.ts.map +1 -1
  43. package/dist/components/combobox/useComboboxState.js +22 -11
  44. package/dist/components/combobox/useComboboxState.js.map +1 -1
  45. package/dist/components/datePicker/DatePickerCalendarHeader.d.ts.map +1 -1
  46. package/dist/components/datePicker/DatePickerCalendarHeader.js +4 -2
  47. package/dist/components/datePicker/DatePickerCalendarHeader.js.map +1 -1
  48. package/dist/components/dateTimePicker/DateTimePicker.d.ts.map +1 -1
  49. package/dist/components/dateTimePicker/DateTimePicker.js +2 -1
  50. package/dist/components/dateTimePicker/DateTimePicker.js.map +1 -1
  51. package/dist/components/dateTimePicker/DateTimePicker.test.js.map +1 -1
  52. package/dist/components/dropdown/Dropdown.stories.d.ts.map +1 -1
  53. package/dist/components/dropdown/Dropdown.stories.js +10 -5
  54. package/dist/components/dropdown/Dropdown.stories.js.map +1 -1
  55. package/dist/components/filterBar/FilterBar.d.ts.map +1 -1
  56. package/dist/components/filterBar/FilterBar.js +4 -2
  57. package/dist/components/filterBar/FilterBar.js.map +1 -1
  58. package/dist/components/filterBar/FilterBar.stories.d.ts.map +1 -1
  59. package/dist/components/filterBar/FilterBar.stories.js +6 -3
  60. package/dist/components/filterBar/FilterBar.stories.js.map +1 -1
  61. package/dist/components/filterBar/FilterBar.test.js +6 -3
  62. package/dist/components/filterBar/FilterBar.test.js.map +1 -1
  63. package/dist/components/formField/fieldset/Fieldset.stories.d.ts.map +1 -1
  64. package/dist/components/formField/fieldset/Fieldset.stories.js.map +1 -1
  65. package/dist/components/formField/inputs/radio/RadioButtonGroup.d.ts +1 -1
  66. package/dist/components/formField/inputs/radio/RadioButtonGroup.d.ts.map +1 -1
  67. package/dist/components/formField/inputs/radio/RadioButtonGroup.js +1 -1
  68. package/dist/components/formField/inputs/radio/RadioButtonGroup.js.map +1 -1
  69. package/dist/components/formField/inputs/selectDropdown/SelectDropdown.d.ts.map +1 -1
  70. package/dist/components/formField/inputs/selectDropdown/SelectDropdown.js +2 -1
  71. package/dist/components/formField/inputs/selectDropdown/SelectDropdown.js.map +1 -1
  72. package/dist/components/formField/inputs/textArea/TextArea.stories.d.ts.map +1 -1
  73. package/dist/components/formField/inputs/textArea/TextArea.stories.js +10 -5
  74. package/dist/components/formField/inputs/textArea/TextArea.stories.js.map +1 -1
  75. package/dist/components/formField/inputs/time/TimeInput.js +3 -4
  76. package/dist/components/formField/inputs/time/TimeInput.js.map +1 -1
  77. package/dist/components/formField/inputs/time/TimeInput.test.js.map +1 -1
  78. package/dist/components/heading/Heading.stories.d.ts.map +1 -1
  79. package/dist/components/heading/Heading.stories.js.map +1 -1
  80. package/dist/components/modal/Modal.stories.d.ts.map +1 -1
  81. package/dist/components/modal/Modal.stories.js.map +1 -1
  82. package/dist/components/progress/Progress.stories.d.ts.map +1 -1
  83. package/dist/components/progress/Progress.stories.js +2 -1
  84. package/dist/components/progress/Progress.stories.js.map +1 -1
  85. package/dist/components/row/Row.d.ts +4 -4
  86. package/dist/components/row/Row.d.ts.map +1 -1
  87. package/dist/components/row/Row.stories.d.ts +3 -0
  88. package/dist/components/row/Row.stories.d.ts.map +1 -1
  89. package/dist/components/row/Row.stories.js +96 -6
  90. package/dist/components/row/Row.stories.js.map +1 -1
  91. package/dist/components/row/Row.test.js +20 -0
  92. package/dist/components/row/Row.test.js.map +1 -1
  93. package/dist/components/sidebarNav/SidebarNavItem.d.ts.map +1 -1
  94. package/dist/components/sidebarNav/SidebarNavItem.js +12 -11
  95. package/dist/components/sidebarNav/SidebarNavItem.js.map +1 -1
  96. package/dist/components/sidebarNav/SidebarNavRailItem.d.ts.map +1 -1
  97. package/dist/components/sidebarNav/SidebarNavRailItem.js +11 -10
  98. package/dist/components/sidebarNav/SidebarNavRailItem.js.map +1 -1
  99. package/dist/components/sidebarNav/useControllableBoolean.d.ts.map +1 -1
  100. package/dist/components/sidebarNav/useControllableBoolean.js +2 -1
  101. package/dist/components/sidebarNav/useControllableBoolean.js.map +1 -1
  102. package/dist/components/table/Table.d.ts.map +1 -1
  103. package/dist/components/table/Table.js +11 -1
  104. package/dist/components/table/Table.js.map +1 -1
  105. package/dist/components/table/Table.stories.d.ts.map +1 -1
  106. package/dist/components/table/Table.stories.js +6 -2
  107. package/dist/components/table/Table.stories.js.map +1 -1
  108. package/dist/components/table/Table.test.js +9 -3
  109. package/dist/components/table/Table.test.js.map +1 -1
  110. package/dist/components/table/cellRenderers/ComboboxCellRenderer.d.ts.map +1 -1
  111. package/dist/components/table/cellRenderers/ComboboxCellRenderer.js +16 -8
  112. package/dist/components/table/cellRenderers/ComboboxCellRenderer.js.map +1 -1
  113. package/dist/components/table/columnFilters/TimeFilter/TimeFilter.d.ts.map +1 -1
  114. package/dist/components/table/columnFilters/TimeFilter/TimeFilter.js +2 -1
  115. package/dist/components/table/columnFilters/TimeFilter/TimeFilter.js.map +1 -1
  116. package/dist/components/table/tableControls/HideColumnsDropdown.d.ts.map +1 -1
  117. package/dist/components/table/tableControls/HideColumnsDropdown.js +2 -1
  118. package/dist/components/table/tableControls/HideColumnsDropdown.js.map +1 -1
  119. package/dist/components/table/tableControls/TableControls.d.ts.map +1 -1
  120. package/dist/components/table/tableControls/TableControls.js +2 -1
  121. package/dist/components/table/tableControls/TableControls.js.map +1 -1
  122. package/dist/components/tag/Tag.test.js.map +1 -1
  123. package/dist/components/tagList/TagList.d.ts.map +1 -1
  124. package/dist/components/tagList/TagList.js +8 -4
  125. package/dist/components/tagList/TagList.js.map +1 -1
  126. package/dist/components/tagList/TagList.test.js +6 -3
  127. package/dist/components/tagList/TagList.test.js.map +1 -1
  128. package/dist/components/toggleGroup/ToggleGroup.d.ts +29 -0
  129. package/dist/components/toggleGroup/ToggleGroup.d.ts.map +1 -0
  130. package/dist/components/toggleGroup/ToggleGroup.js +26 -0
  131. package/dist/components/toggleGroup/ToggleGroup.js.map +1 -0
  132. package/dist/components/toggleGroup/ToggleGroup.stories.d.ts +86 -0
  133. package/dist/components/toggleGroup/ToggleGroup.stories.d.ts.map +1 -0
  134. package/dist/components/toggleGroup/ToggleGroup.stories.js +342 -0
  135. package/dist/components/toggleGroup/ToggleGroup.stories.js.map +1 -0
  136. package/dist/components/toggleGroup/ToggleGroup.test.d.ts +2 -0
  137. package/dist/components/toggleGroup/ToggleGroup.test.d.ts.map +1 -0
  138. package/dist/components/toggleGroup/ToggleGroup.test.js +123 -0
  139. package/dist/components/toggleGroup/ToggleGroup.test.js.map +1 -0
  140. package/dist/components/toggleGroup/ToggleGroupItem.d.ts +10 -0
  141. package/dist/components/toggleGroup/ToggleGroupItem.d.ts.map +1 -0
  142. package/dist/components/toggleGroup/ToggleGroupItem.js +5 -0
  143. package/dist/components/toggleGroup/ToggleGroupItem.js.map +1 -0
  144. package/dist/components/treeRow/TreeRowSection.d.ts.map +1 -1
  145. package/dist/components/treeRow/TreeRowSection.js +2 -1
  146. package/dist/components/treeRow/TreeRowSection.js.map +1 -1
  147. package/dist/components/userDropdown/UserDropdown.stories.d.ts.map +1 -1
  148. package/dist/components/userDropdown/UserDropdown.stories.js +66 -22
  149. package/dist/components/userDropdown/UserDropdown.stories.js.map +1 -1
  150. package/dist/components/userDropdown/internal/UserDropdownCollapsibleSection.d.ts.map +1 -1
  151. package/dist/components/userDropdown/internal/UserDropdownCollapsibleSection.js +3 -1
  152. package/dist/components/userDropdown/internal/UserDropdownCollapsibleSection.js.map +1 -1
  153. package/dist/index.css +33 -0
  154. package/dist/index.css.map +1 -1
  155. package/dist/index.d.ts +1 -0
  156. package/dist/index.d.ts.map +1 -1
  157. package/dist/index.js +1 -0
  158. package/dist/index.js.map +1 -1
  159. package/dist/utils/getTextContrast.d.ts.map +1 -1
  160. package/dist/utils/getTextContrast.js +4 -2
  161. package/dist/utils/getTextContrast.js.map +1 -1
  162. package/dist/utils/hooks/useElementWidth.d.ts.map +1 -1
  163. package/dist/utils/hooks/useElementWidth.js +2 -1
  164. package/dist/utils/hooks/useElementWidth.js.map +1 -1
  165. package/dist/utils/hooks/useMeasuredChildWidths.d.ts.map +1 -1
  166. package/dist/utils/hooks/useMeasuredChildWidths.js +4 -2
  167. package/dist/utils/hooks/useMeasuredChildWidths.js.map +1 -1
  168. package/dist/utils/hooks/useRovingFocus.d.ts.map +1 -1
  169. package/dist/utils/hooks/useRovingFocus.js +8 -4
  170. package/dist/utils/hooks/useRovingFocus.js.map +1 -1
  171. package/dist/utils/spacedWidths.d.ts.map +1 -1
  172. package/dist/utils/spacedWidths.js +6 -3
  173. package/dist/utils/spacedWidths.js.map +1 -1
  174. package/eslint.config.mts +32 -1
  175. package/package.json +1 -1
  176. package/src/components/avatarGroup/AvatarGroup.stories.tsx +6 -2
  177. package/src/components/card/Card.tsx +3 -1
  178. package/src/components/combobox/Combobox.stories.tsx +3 -1
  179. package/src/components/combobox/Combobox.test.tsx +39 -11
  180. package/src/components/combobox/Combobox.tsx +15 -5
  181. package/src/components/combobox/ComboboxButtonTrigger.tsx +3 -1
  182. package/src/components/combobox/ComboboxListbox.tsx +12 -4
  183. package/src/components/combobox/buildListboxDisplayOptions.ts +6 -2
  184. package/src/components/combobox/highlightLabel.tsx +2 -1
  185. package/src/components/combobox/useChipSelection.ts +3 -1
  186. package/src/components/combobox/useComboboxListboxKeyboard.ts +12 -4
  187. package/src/components/combobox/useComboboxPopoverBehavior.ts +24 -8
  188. package/src/components/combobox/useComboboxState.ts +28 -11
  189. package/src/components/datePicker/DatePickerCalendarHeader.tsx +6 -2
  190. package/src/components/dateTimePicker/DateTimePicker.test.tsx +2 -0
  191. package/src/components/dateTimePicker/DateTimePicker.tsx +3 -1
  192. package/src/components/dropdown/Dropdown.stories.tsx +19 -8
  193. package/src/components/filterBar/FilterBar.stories.tsx +9 -3
  194. package/src/components/filterBar/FilterBar.test.tsx +9 -3
  195. package/src/components/filterBar/FilterBar.tsx +6 -2
  196. package/src/components/formField/fieldset/Fieldset.stories.tsx +18 -3
  197. package/src/components/formField/inputs/radio/RadioButtonGroup.tsx +1 -1
  198. package/src/components/formField/inputs/selectDropdown/SelectDropdown.tsx +3 -1
  199. package/src/components/formField/inputs/textArea/TextArea.stories.tsx +10 -5
  200. package/src/components/formField/inputs/time/TimeInput.test.tsx +2 -0
  201. package/src/components/formField/inputs/time/TimeInput.tsx +4 -4
  202. package/src/components/heading/Heading.stories.tsx +12 -3
  203. package/src/components/modal/Modal.stories.tsx +15 -5
  204. package/src/components/progress/Progress.stories.tsx +3 -1
  205. package/src/components/row/Row.stories.tsx +141 -6
  206. package/src/components/row/Row.test.tsx +23 -0
  207. package/src/components/row/Row.tsx +4 -4
  208. package/src/components/sidebarNav/SidebarNavItem.tsx +27 -25
  209. package/src/components/sidebarNav/SidebarNavRailItem.tsx +34 -35
  210. package/src/components/sidebarNav/useControllableBoolean.ts +3 -1
  211. package/src/components/table/Table.stories.tsx +16 -12
  212. package/src/components/table/Table.test.tsx +10 -4
  213. package/src/components/table/Table.tsx +15 -5
  214. package/src/components/table/cellRenderers/ComboboxCellRenderer.tsx +19 -8
  215. package/src/components/table/columnFilters/TimeFilter/TimeFilter.tsx +3 -1
  216. package/src/components/table/tableControls/HideColumnsDropdown.tsx +3 -1
  217. package/src/components/table/tableControls/TableControls.tsx +3 -1
  218. package/src/components/tag/Tag.test.tsx +40 -8
  219. package/src/components/tagList/TagList.test.tsx +9 -3
  220. package/src/components/tagList/TagList.tsx +12 -4
  221. package/src/components/toggleGroup/ToggleGroup.stories.tsx +429 -0
  222. package/src/components/toggleGroup/ToggleGroup.test.tsx +155 -0
  223. package/src/components/toggleGroup/ToggleGroup.tsx +74 -0
  224. package/src/components/toggleGroup/ToggleGroupItem.tsx +24 -0
  225. package/src/components/toggleGroup/toggleGroup.scss +43 -0
  226. package/src/components/treeRow/TreeRowSection.tsx +3 -1
  227. package/src/components/userDropdown/UserDropdown.stories.tsx +66 -22
  228. package/src/components/userDropdown/internal/UserDropdownCollapsibleSection.tsx +3 -1
  229. package/src/index.scss +1 -0
  230. package/src/index.ts +1 -0
  231. package/src/utils/getTextContrast.ts +6 -2
  232. package/src/utils/hooks/useElementWidth.ts +3 -1
  233. package/src/utils/hooks/useMeasuredChildWidths.ts +6 -2
  234. package/src/utils/hooks/useRovingFocus.ts +12 -4
  235. package/src/utils/spacedWidths.ts +9 -3
@@ -33,14 +33,18 @@ export const DatePickerCalendarHeader = ({
33
33
 
34
34
  const handleMonthSelection = (selectedValues: string[]) => {
35
35
  const nextMonthIndex = Number.parseInt(selectedValues[0] ?? String(month.getMonth()), 10);
36
- if (Number.isNaN(nextMonthIndex)) return;
36
+ if (Number.isNaN(nextMonthIndex)) {
37
+ return;
38
+ }
37
39
 
38
40
  onMonthChange(new Date(month.getFullYear(), nextMonthIndex, 1));
39
41
  };
40
42
 
41
43
  const handleYearSelection = (selectedValues: string[]) => {
42
44
  const nextYear = Number.parseInt(selectedValues[0] ?? String(month.getFullYear()), 10);
43
- if (Number.isNaN(nextYear)) return;
45
+ if (Number.isNaN(nextYear)) {
46
+ return;
47
+ }
44
48
 
45
49
  onMonthChange(new Date(nextYear, month.getMonth(), 1));
46
50
  };
@@ -9,7 +9,9 @@ describe('DateTimePicker', () => {
9
9
  beforeEach(() => {
10
10
  globalThis.ResizeObserver = class {
11
11
  observe() {}
12
+
12
13
  unobserve() {}
14
+
13
15
  disconnect() {}
14
16
  } as unknown as typeof ResizeObserver;
15
17
  });
@@ -199,6 +199,8 @@ export const DateTimePicker = (props: DateTimePickerProps) => {
199
199
 
200
200
  const popupParentRef = useContext(PopupParentContext);
201
201
 
202
+ const nativeStep = granularity === 'second' ? 1 : 60;
203
+
202
204
  return (
203
205
  <div className={classNames('ds-date-time-picker', className)}>
204
206
  <div
@@ -215,7 +217,7 @@ export const DateTimePicker = (props: DateTimePickerProps) => {
215
217
  onPointerDown={onNativeFieldHintPointerDown}
216
218
  readOnly={hasCustomEmptyHint}
217
219
  id={id}
218
- step={isNative ? (granularity === 'second' ? 1 : 60) : undefined}
220
+ step={isNative ? nativeStep : undefined}
219
221
  placeholder={showFieldHint ? '' : resolvedPlaceholder}
220
222
  hasError={hasError}
221
223
  aria-describedby={ariaDescribedBy}
@@ -392,11 +392,16 @@ const SchoolContextExampleTemplate = () => {
392
392
  year11 && 'Year 11',
393
393
  ].filter(Boolean);
394
394
 
395
- const label = allYears
396
- ? 'All year groups'
397
- : selectedYears.length === 0
398
- ? 'Select year groups'
399
- : selectedYears.join(', ');
395
+ let label: string;
396
+ if (allYears) {
397
+ label = 'All year groups';
398
+ }
399
+ else if (selectedYears.length === 0) {
400
+ label = 'Select year groups';
401
+ }
402
+ else {
403
+ label = selectedYears.join(', ');
404
+ }
400
405
 
401
406
  return (
402
407
  <div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--spacing-large)', padding: 'var(--spacing-xlarge)', minWidth: '280px' }}>
@@ -450,9 +455,15 @@ export const Default: Story = withDescription(
450
455
  <Button variant="primary">Open menu</Button>
451
456
  </Dropdown.Trigger>
452
457
  <Dropdown.Content>
453
- <Dropdown.Item onSelect={() => {}}>View student profile</Dropdown.Item>
454
- <Dropdown.Item onSelect={() => {}}>Edit assessment details</Dropdown.Item>
455
- <Dropdown.Item onSelect={() => {}}>Archive record</Dropdown.Item>
458
+ <Dropdown.Item onSelect={() => {}}>
459
+ View student profile
460
+ </Dropdown.Item>
461
+ <Dropdown.Item onSelect={() => {}}>
462
+ Edit assessment details
463
+ </Dropdown.Item>
464
+ <Dropdown.Item onSelect={() => {}}>
465
+ Archive record
466
+ </Dropdown.Item>
456
467
  </Dropdown.Content>
457
468
  </Dropdown>
458
469
  </div>
@@ -553,7 +553,9 @@ const ModalWiringTemplate = () => {
553
553
  };
554
554
 
555
555
  useEffect(() => {
556
- if (!open) return;
556
+ if (!open) {
557
+ return;
558
+ }
557
559
 
558
560
  const frame = requestAnimationFrame(() => {
559
561
  document.getElementById(pendingFocusFieldId)?.focus();
@@ -582,7 +584,9 @@ const ModalWiringTemplate = () => {
582
584
 
583
585
  const items: FilterBar.TagItem[] = APPLIED_ITEM_SEEDS.flatMap((item) => {
584
586
  const value = fieldValues[item.fieldId]?.trim();
585
- if (!value) return [];
587
+ if (!value) {
588
+ return [];
589
+ }
586
590
 
587
591
  return [{
588
592
  ...item,
@@ -593,7 +597,9 @@ const ModalWiringTemplate = () => {
593
597
  removeLabel: `Remove ${item.label.toLowerCase()} ${item.type}`,
594
598
  onClick: clickedItem => openModal(clickedItem.type, clickedItem.fieldId ?? PANEL_FIELD_IDS[clickedItem.type]),
595
599
  onRemove: (removedItem) => {
596
- if (!removedItem.fieldId) return;
600
+ if (!removedItem.fieldId) {
601
+ return;
602
+ }
597
603
  updateFieldValue(removedItem.fieldId, '');
598
604
  },
599
605
  }];
@@ -122,9 +122,15 @@ describe('FilterBar', () => {
122
122
  const rectSpy = vi.spyOn(HTMLElement.prototype, 'getBoundingClientRect').mockImplementation(function mockRect(this: HTMLElement) {
123
123
  let width = 0;
124
124
 
125
- if (this.classList.contains('ds-tag-list__viewport')) width = 220;
126
- else if (this.classList.contains('ds-tag-list__item')) width = 84;
127
- else if (this.classList.contains('ds-tag-list__overflow')) width = 76;
125
+ if (this.classList.contains('ds-tag-list__viewport')) {
126
+ width = 220;
127
+ }
128
+ else if (this.classList.contains('ds-tag-list__item')) {
129
+ width = 84;
130
+ }
131
+ else if (this.classList.contains('ds-tag-list__overflow')) {
132
+ width = 76;
133
+ }
128
134
 
129
135
  return {
130
136
  width,
@@ -73,7 +73,9 @@ const FILTER_BAR_TYPE_ICONS: Record<FilterBarType, IconName> = {
73
73
  };
74
74
 
75
75
  const getVisibleCount = (count?: number | null): string | null => {
76
- if (typeof count !== 'number' || count <= 0) return null;
76
+ if (typeof count !== 'number' || count <= 0) {
77
+ return null;
78
+ }
77
79
  return count > 99 ? '99+' : `${count}`;
78
80
  };
79
81
 
@@ -267,7 +269,9 @@ const FilterBarActiveList = ({
267
269
  }
268
270
 
269
271
  if (!children) {
270
- if (!emptyState) return null;
272
+ if (!emptyState) {
273
+ return null;
274
+ }
271
275
  return <div className={classNames('ds-filter-bar__empty-state', className)}>{emptyState}</div>;
272
276
  }
273
277
 
@@ -320,9 +320,24 @@ export const Default: Story = {
320
320
  },
321
321
  render: args => (
322
322
  <Fieldset {...args}>
323
- <CheckboxInput id="consent-trips" name="consent" label="Consent for school trips" onChange={() => {}} />
324
- <CheckboxInput id="consent-photos" name="consent" label="Consent for photographs" onChange={() => {}} />
325
- <CheckboxInput id="consent-data" name="consent" label="Consent for data sharing with partner schools" onChange={() => {}} />
323
+ <CheckboxInput
324
+ id="consent-trips"
325
+ name="consent"
326
+ label="Consent for school trips"
327
+ onChange={() => {}}
328
+ />
329
+ <CheckboxInput
330
+ id="consent-photos"
331
+ name="consent"
332
+ label="Consent for photographs"
333
+ onChange={() => {}}
334
+ />
335
+ <CheckboxInput
336
+ id="consent-data"
337
+ name="consent"
338
+ label="Consent for data sharing with partner schools"
339
+ onChange={() => {}}
340
+ />
326
341
  </Fieldset>
327
342
  ),
328
343
  };
@@ -1,6 +1,6 @@
1
1
  import type { ChangeEvent } from 'react';
2
2
  import { RadioButtonInput } from './RadioButtonInput.js';
3
- import { Fieldset, type FieldsetProps } from '../../fieldset/Fieldset.js';
3
+ import { Fieldset, type FieldsetProps } from 'Components/formField/fieldset/Fieldset.js';
4
4
 
5
5
  export type RadioGroupOption = {
6
6
  id: string;
@@ -72,7 +72,9 @@ export const SelectDropdown = (props: SelectDropdownInputProps) => {
72
72
  const UNGROUPED_KEY = 'Ungrouped';
73
73
  const groupedOptions = options.reduce((acc, option) => {
74
74
  const key = option.group ?? UNGROUPED_KEY;
75
- if (!acc[key]) acc[key] = [];
75
+ if (!acc[key]) {
76
+ acc[key] = [];
77
+ }
76
78
  acc[key].push(option);
77
79
  return acc;
78
80
  }, {} as Record<string, SelectDropdownItemProps[]>);
@@ -353,11 +353,16 @@ const ControlledWithCharacterCountTemplate = () => {
353
353
  const isApproaching = remaining <= 100 && remaining > 0;
354
354
  const isAtLimit = remaining <= 0;
355
355
 
356
- const countColour = isAtLimit
357
- ? 'var(--color-semantic-destructive-600)'
358
- : isApproaching
359
- ? 'var(--color-semantic-caution-600)'
360
- : 'var(--color-grey-600)';
356
+ let countColour: string;
357
+ if (isAtLimit) {
358
+ countColour = 'var(--color-semantic-destructive-600)';
359
+ }
360
+ else if (isApproaching) {
361
+ countColour = 'var(--color-semantic-caution-600)';
362
+ }
363
+ else {
364
+ countColour = 'var(--color-grey-600)';
365
+ }
361
366
 
362
367
  return (
363
368
  <div style={{ maxWidth: '28rem', padding: 'var(--spacing-xlarge)', display: 'flex', flexDirection: 'column', gap: 'var(--spacing-xsmall)' }}>
@@ -8,7 +8,9 @@ describe('TimeInput', () => {
8
8
  beforeEach(() => {
9
9
  globalThis.ResizeObserver = class {
10
10
  observe() {}
11
+
11
12
  unobserve() {}
13
+
12
14
  disconnect() {}
13
15
  } as unknown as typeof ResizeObserver;
14
16
  });
@@ -82,16 +82,16 @@ export const TimeInput = forwardRef<HTMLInputElement, TimeInputProps>((props, re
82
82
 
83
83
  const focusAndSelectHours = useCallback(() => {
84
84
  const input = inputRef.current;
85
- if (!input) return;
85
+ if (!input) {
86
+ return;
87
+ }
86
88
 
87
89
  input.focus();
88
90
 
89
91
  try {
90
92
  input.setSelectionRange(0, 2);
91
93
  }
92
- catch {
93
- // Some browsers do not expose selection APIs on time inputs.
94
- }
94
+ catch { /* Some browsers do not expose selection APIs on time inputs. */ }
95
95
  }, []);
96
96
 
97
97
  const timeOptions = useMemo<ComboboxOption[]>(
@@ -368,7 +368,10 @@ const WithBackControlLegacyTemplate = () => (
368
368
  const WithEditableTitleTemplate = () => (
369
369
  <div style={{ padding: 'var(--spacing-xlarge)' }}>
370
370
  <Heading level={1}>
371
- <EditableText text="Form 7B Registration" onEditSave={() => {}} />
371
+ <EditableText
372
+ text="Form 7B Registration"
373
+ onEditSave={() => {}}
374
+ />
372
375
  </Heading>
373
376
  </div>
374
377
  );
@@ -377,7 +380,10 @@ const WithEditableTitleAndActionTemplate = () => (
377
380
  <div style={{ padding: 'var(--spacing-xlarge)' }}>
378
381
  <Heading level={1}>
379
382
  <Heading.InnerContainer>
380
- <EditableText text="Form 7B Registration" onEditSave={() => {}} />
383
+ <EditableText
384
+ text="Form 7B Registration"
385
+ onEditSave={() => {}}
386
+ />
381
387
  </Heading.InnerContainer>
382
388
  <Heading.InnerContainer>
383
389
  <Button variant="primary" type="button">Save changes</Button>
@@ -393,7 +399,10 @@ const WithEditableTitleAndMultipleActionsTemplate = () => (
393
399
  <Button variant="tertiary" type="button" iconLeftName="chevron-left" iconLeftScreenReaderText="Go back">
394
400
  Back
395
401
  </Button>
396
- <EditableText text="Autumn term report" onEditSave={() => {}} />
402
+ <EditableText
403
+ text="Autumn term report"
404
+ onEditSave={() => {}}
405
+ />
397
406
  </Heading.InnerContainer>
398
407
  <Heading.InnerContainer className="medium-spacing-gap">
399
408
  <Button variant="secondary" type="button">Export to CSV</Button>
@@ -548,11 +548,21 @@ const WithDropdownInsideModalTemplate = () => {
548
548
  <Button variant="dropdown">Select a teacher</Button>
549
549
  </Dropdown.Trigger>
550
550
  <Dropdown.Content>
551
- <Dropdown.Item onSelect={() => {}}>Ms A. Johnson — English</Dropdown.Item>
552
- <Dropdown.Item onSelect={() => {}}>Mr B. PatelMathematics</Dropdown.Item>
553
- <Dropdown.Item onSelect={() => {}}>Mrs C. Williams — Science</Dropdown.Item>
554
- <Dropdown.Item onSelect={() => {}}>Mr D. Thompson — History</Dropdown.Item>
555
- <Dropdown.Item onSelect={() => {}}>Ms E. OkonkwoArt</Dropdown.Item>
551
+ <Dropdown.Item onSelect={() => {}}>
552
+ Ms A. JohnsonEnglish
553
+ </Dropdown.Item>
554
+ <Dropdown.Item onSelect={() => {}}>
555
+ Mr B. PatelMathematics
556
+ </Dropdown.Item>
557
+ <Dropdown.Item onSelect={() => {}}>
558
+ Mrs C. Williams — Science
559
+ </Dropdown.Item>
560
+ <Dropdown.Item onSelect={() => {}}>
561
+ Mr D. Thompson — History
562
+ </Dropdown.Item>
563
+ <Dropdown.Item onSelect={() => {}}>
564
+ Ms E. Okonkwo — Art
565
+ </Dropdown.Item>
556
566
  </Dropdown.Content>
557
567
  </Dropdown>
558
568
  </Modal.Body>
@@ -274,7 +274,9 @@ const InteractiveLoadingTemplate = (args: React.ComponentProps<typeof Progress>)
274
274
  const [running, setRunning] = useState(true);
275
275
 
276
276
  useEffect(() => {
277
- if (!running) return;
277
+ if (!running) {
278
+ return;
279
+ }
278
280
  if (progress >= 100) {
279
281
  setRunning(false);
280
282
  return;
@@ -11,6 +11,9 @@ import {
11
11
  import { fn } from 'storybook/test';
12
12
 
13
13
  import { Row } from './Row.js';
14
+ import { Badge } from 'Components/badge/Badge';
15
+ import { Tag } from 'Components/tag/Tag';
16
+ import { Icon } from 'Components/icon/Icon';
14
17
 
15
18
  // ---------------------------------------------------------------------------
16
19
  // Content strings for the custom DocsPage
@@ -85,12 +88,13 @@ const DEVELOPER_NOTES = [
85
88
  '```ts',
86
89
  "import { Row } from '@arbor-education/design-system.components';",
87
90
  "import type { RowProps } from '@arbor-education/design-system.components';",
91
+ "import type { ReactNode } from 'react';",
88
92
  '',
89
93
  'type RowProps = {',
90
94
  ' className?: string;',
91
- ' label?: string;',
92
- ' value?: string;',
93
- ' note?: string;',
95
+ ' label?: ReactNode;',
96
+ ' value?: ReactNode;',
97
+ ' note?: ReactNode;',
94
98
  ' onClick?: MouseEventHandler<HTMLDivElement>;',
95
99
  '};',
96
100
  '```',
@@ -148,7 +152,7 @@ const meta: Meta<typeof Row> = {
148
152
  ].join(' '),
149
153
  control: 'text',
150
154
  table: {
151
- type: { summary: 'string' },
155
+ type: { summary: 'ReactNode' },
152
156
  defaultValue: { summary: 'undefined' },
153
157
  },
154
158
  },
@@ -159,7 +163,7 @@ const meta: Meta<typeof Row> = {
159
163
  ].join(' '),
160
164
  control: 'text',
161
165
  table: {
162
- type: { summary: 'string' },
166
+ type: { summary: 'ReactNode' },
163
167
  defaultValue: { summary: 'undefined' },
164
168
  },
165
169
  },
@@ -170,7 +174,7 @@ const meta: Meta<typeof Row> = {
170
174
  ].join(' '),
171
175
  control: 'text',
172
176
  table: {
173
- type: { summary: 'string' },
177
+ type: { summary: 'ReactNode' },
174
178
  defaultValue: { summary: 'undefined' },
175
179
  },
176
180
  },
@@ -499,3 +503,134 @@ export default PupilDetailPanel;
499
503
  },
500
504
  'A realistic pupil detail panel — the most common real-world usage. Static and clickable rows can be freely mixed. The `gap: var(--spacing-small)` wrapper token matches the spacing the component uses internally, keeping visual rhythm consistent.',
501
505
  );
506
+
507
+ export const ReactNodeInValue: Story = withDescription(
508
+ {
509
+ parameters: {
510
+ controls: { disable: true },
511
+ docs: {
512
+ source: {
513
+ language: 'tsx',
514
+ code: `
515
+ import { Row, Badge } from '@arbor-education/design-system.components';
516
+
517
+ function SENStatusRows() {
518
+ return (
519
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--spacing-small)' }}>
520
+ <Row label="SEN status" value={<Badge colour="orange">SEN Support</Badge>} />
521
+ <Row label="Admission type" value={<Badge colour="blue">Looked after child</Badge>} />
522
+ </div>
523
+ );
524
+ }
525
+ export default SENStatusRows;
526
+ `.trim(),
527
+ },
528
+ },
529
+ },
530
+ render: () => (
531
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--spacing-small)' }}>
532
+ <Row label="SEN status" value={<Badge colour="orange">SEN Support</Badge>} />
533
+ <Row label="Admission type" value={<Badge colour="blue">Looked after child</Badge>} />
534
+ </div>
535
+ ),
536
+ },
537
+ '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.',
538
+ );
539
+
540
+ export const ReactNodeInNote: Story = withDescription(
541
+ {
542
+ parameters: {
543
+ controls: { disable: true },
544
+ docs: {
545
+ source: {
546
+ language: 'tsx',
547
+ code: `
548
+ import { Row, Tag } from '@arbor-education/design-system.components';
549
+
550
+ function AttendanceRows() {
551
+ return (
552
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--spacing-small)' }}>
553
+ <Row label="Attendance this term" value="91.4%" note={<Tag color="orange">Below target</Tag>} />
554
+ <Row label="Reading age" value="8y 4m" note={<Tag color="green">On track</Tag>} />
555
+ </div>
556
+ );
557
+ }
558
+ export default AttendanceRows;
559
+ `.trim(),
560
+ },
561
+ },
562
+ },
563
+ render: () => (
564
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--spacing-small)' }}>
565
+ <Row label="Attendance this term" value="91.4%" note={<Tag color="orange">Below target</Tag>} />
566
+ <Row label="Reading age" value="8y 4m" note={<Tag color="green">On track</Tag>} />
567
+ </div>
568
+ ),
569
+ },
570
+ '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.',
571
+ );
572
+
573
+ export const ReactNodeInLabel: Story = withDescription(
574
+ {
575
+ parameters: {
576
+ controls: { disable: true },
577
+ docs: {
578
+ source: {
579
+ language: 'tsx',
580
+ code: `
581
+ import { Row, Icon } from '@arbor-education/design-system.components';
582
+
583
+ function AnnotatedLabelRows() {
584
+ return (
585
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--spacing-small)' }}>
586
+ <Row
587
+ label={
588
+ <span style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
589
+ Pupil premium <Icon name="info" size={16} />
590
+ </span>
591
+ }
592
+ value="Free school meals — ever 6"
593
+ />
594
+ <Row
595
+ label={
596
+ <span style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
597
+ EHCP status <Icon name="info" size={16} />
598
+ </span>
599
+ }
600
+ value="EHC plan in place"
601
+ />
602
+ </div>
603
+ );
604
+ }
605
+ export default AnnotatedLabelRows;
606
+ `.trim(),
607
+ },
608
+ },
609
+ },
610
+ render: () => (
611
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--spacing-small)' }}>
612
+ <Row
613
+ label={(
614
+ <span style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
615
+ Pupil premium
616
+ {' '}
617
+ <Icon name="info" size={16} />
618
+ </span>
619
+ )}
620
+ value="Free school meals — ever 6"
621
+ />
622
+ <Row
623
+ label={(
624
+ <span style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
625
+ EHCP status
626
+ {' '}
627
+ <Icon name="info" size={16} />
628
+ </span>
629
+ )}
630
+ value="EHC plan in place"
631
+ />
632
+ </div>
633
+ ),
634
+ },
635
+ '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.',
636
+ );
@@ -79,4 +79,27 @@ describe('Row component', () => {
79
79
  expect(handleClick.mock.calls[0]?.[0]).toBeDefined();
80
80
  });
81
81
  });
82
+
83
+ describe('ReactNode content', () => {
84
+ test('accepts ReactNode label', () => {
85
+ render(<Row label={<span className="foo">bar</span>} />);
86
+ const label = screen.getByText('bar');
87
+ expect(label).toBeInTheDocument();
88
+ expect(label).toHaveClass('foo');
89
+ });
90
+
91
+ test('accepts ReactNode value', () => {
92
+ render(<Row value={<span className="foo">bar</span>} />);
93
+ const value = screen.getByText('bar');
94
+ expect(value).toBeInTheDocument();
95
+ expect(value).toHaveClass('foo');
96
+ });
97
+
98
+ test('accepts ReactNode note', () => {
99
+ render(<Row note={<span className="foo">bar</span>} />);
100
+ const note = screen.getByText('bar');
101
+ expect(note).toBeInTheDocument();
102
+ expect(note).toHaveClass('foo');
103
+ });
104
+ });
82
105
  });
@@ -1,14 +1,14 @@
1
1
  import classNames from 'classnames';
2
2
  import { Icon } from 'Components/icon/Icon';
3
- import type { CSSProperties, MouseEvent, MouseEventHandler } from 'react';
3
+ import type { CSSProperties, MouseEvent, MouseEventHandler, ReactNode } from 'react';
4
4
  import { ENTER_KEY, SPACE_KEY } from 'Utils/keyboardConstants';
5
5
 
6
6
  export type RowProps = {
7
7
  className?: string;
8
8
  style?: CSSProperties;
9
- label?: string;
10
- value?: string;
11
- note?: string;
9
+ label?: ReactNode;
10
+ value?: ReactNode;
11
+ note?: ReactNode;
12
12
  onClick?: MouseEventHandler<HTMLDivElement>;
13
13
  };
14
14
 
@@ -97,6 +97,32 @@ export const SidebarNavItem = (props: SidebarNavItemProps) => {
97
97
  linkElementProps as AnchorHTMLAttributes<HTMLAnchorElement> | undefined,
98
98
  );
99
99
 
100
+ const renderAnchorElement = () => renderLink
101
+ ? renderLink({
102
+ href: href!,
103
+ className: linkClassName,
104
+ ariaCurrent: current ? 'page' : undefined,
105
+ linkProps,
106
+ linkElementProps: anchorLinkElementProps as Omit<
107
+ AnchorHTMLAttributes<HTMLAnchorElement>,
108
+ 'href' | 'className' | 'children'
109
+ > | undefined,
110
+ onClick: handleClick,
111
+ children: linkChildren,
112
+ })
113
+ : (
114
+ <a
115
+ href={href}
116
+ className={linkClassName}
117
+ aria-current={current ? 'page' : undefined}
118
+ {...linkProps}
119
+ {...anchorLinkElementProps}
120
+ onClick={handleClick as MouseEventHandler<HTMLAnchorElement>}
121
+ >
122
+ {linkChildren}
123
+ </a>
124
+ );
125
+
100
126
  const linkElementNode = linkElement === 'button'
101
127
  ? (
102
128
  <button
@@ -109,31 +135,7 @@ export const SidebarNavItem = (props: SidebarNavItemProps) => {
109
135
  {linkChildren}
110
136
  </button>
111
137
  )
112
- : renderLink
113
- ? renderLink({
114
- href: href!,
115
- className: linkClassName,
116
- ariaCurrent: current ? 'page' : undefined,
117
- linkProps,
118
- linkElementProps: anchorLinkElementProps as Omit<
119
- AnchorHTMLAttributes<HTMLAnchorElement>,
120
- 'href' | 'className' | 'children'
121
- > | undefined,
122
- onClick: handleClick,
123
- children: linkChildren,
124
- })
125
- : (
126
- <a
127
- href={href}
128
- className={linkClassName}
129
- aria-current={current ? 'page' : undefined}
130
- {...linkProps}
131
- {...anchorLinkElementProps}
132
- onClick={handleClick as MouseEventHandler<HTMLAnchorElement>}
133
- >
134
- {linkChildren}
135
- </a>
136
- );
138
+ : renderAnchorElement();
137
139
 
138
140
  return (
139
141
  <li