@arbor-education/design-system.components 0.6.0 → 0.8.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 (311) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/bin/createComponent.sh +2 -2
  3. package/dist/components/avatar/Avatar.d.ts +1 -1
  4. package/dist/components/avatar/Avatar.d.ts.map +1 -1
  5. package/dist/components/avatar/Avatar.js +1 -1
  6. package/dist/components/avatar/Avatar.js.map +1 -1
  7. package/dist/components/avatar/Avatar.stories.d.ts.map +1 -1
  8. package/dist/components/avatar/Avatar.stories.js +7 -0
  9. package/dist/components/avatar/Avatar.stories.js.map +1 -1
  10. package/dist/components/badge/Badge.d.ts +12 -0
  11. package/dist/components/badge/Badge.d.ts.map +1 -0
  12. package/dist/components/badge/Badge.js +6 -0
  13. package/dist/components/badge/Badge.js.map +1 -0
  14. package/dist/components/badge/Badge.stories.d.ts +10 -0
  15. package/dist/components/badge/Badge.stories.d.ts.map +1 -0
  16. package/dist/components/badge/Badge.stories.js +51 -0
  17. package/dist/components/badge/Badge.stories.js.map +1 -0
  18. package/dist/components/badge/Badge.test.d.ts +2 -0
  19. package/dist/components/badge/Badge.test.d.ts.map +1 -0
  20. package/dist/components/badge/Badge.test.js +23 -0
  21. package/dist/components/badge/Badge.test.js.map +1 -0
  22. package/dist/components/card/Card.js +1 -1
  23. package/dist/components/card/Card.js.map +1 -1
  24. package/dist/components/combobox/Combobox.d.ts +16 -0
  25. package/dist/components/combobox/Combobox.d.ts.map +1 -0
  26. package/dist/components/combobox/Combobox.js +195 -0
  27. package/dist/components/combobox/Combobox.js.map +1 -0
  28. package/dist/components/combobox/Combobox.stories.d.ts +24 -0
  29. package/dist/components/combobox/Combobox.stories.d.ts.map +1 -0
  30. package/dist/components/combobox/Combobox.stories.js +246 -0
  31. package/dist/components/combobox/Combobox.stories.js.map +1 -0
  32. package/dist/components/combobox/Combobox.test.d.ts +2 -0
  33. package/dist/components/combobox/Combobox.test.d.ts.map +1 -0
  34. package/dist/components/combobox/Combobox.test.js +798 -0
  35. package/dist/components/combobox/Combobox.test.js.map +1 -0
  36. package/dist/components/combobox/ComboboxButtonTrigger.d.ts +28 -0
  37. package/dist/components/combobox/ComboboxButtonTrigger.d.ts.map +1 -0
  38. package/dist/components/combobox/ComboboxButtonTrigger.js +64 -0
  39. package/dist/components/combobox/ComboboxButtonTrigger.js.map +1 -0
  40. package/dist/components/combobox/ComboboxListbox.d.ts +44 -0
  41. package/dist/components/combobox/ComboboxListbox.d.ts.map +1 -0
  42. package/dist/components/combobox/ComboboxListbox.js +37 -0
  43. package/dist/components/combobox/ComboboxListbox.js.map +1 -0
  44. package/dist/components/combobox/ComboboxOptionRow.d.ts +23 -0
  45. package/dist/components/combobox/ComboboxOptionRow.d.ts.map +1 -0
  46. package/dist/components/combobox/ComboboxOptionRow.js +27 -0
  47. package/dist/components/combobox/ComboboxOptionRow.js.map +1 -0
  48. package/dist/components/combobox/ComboboxTrigger.d.ts +35 -0
  49. package/dist/components/combobox/ComboboxTrigger.d.ts.map +1 -0
  50. package/dist/components/combobox/ComboboxTrigger.js +15 -0
  51. package/dist/components/combobox/ComboboxTrigger.js.map +1 -0
  52. package/dist/components/combobox/buildListboxDisplayOptions.d.ts +3 -0
  53. package/dist/components/combobox/buildListboxDisplayOptions.d.ts.map +1 -0
  54. package/dist/components/combobox/buildListboxDisplayOptions.js +13 -0
  55. package/dist/components/combobox/buildListboxDisplayOptions.js.map +1 -0
  56. package/dist/components/combobox/buildListboxDisplayOptions.test.d.ts +2 -0
  57. package/dist/components/combobox/buildListboxDisplayOptions.test.d.ts.map +1 -0
  58. package/dist/components/combobox/buildListboxDisplayOptions.test.js +22 -0
  59. package/dist/components/combobox/buildListboxDisplayOptions.test.js.map +1 -0
  60. package/dist/components/combobox/comboboxKeyboardTypes.d.ts +41 -0
  61. package/dist/components/combobox/comboboxKeyboardTypes.d.ts.map +1 -0
  62. package/dist/components/combobox/comboboxKeyboardTypes.js +2 -0
  63. package/dist/components/combobox/comboboxKeyboardTypes.js.map +1 -0
  64. package/dist/components/combobox/highlightLabel.d.ts +10 -0
  65. package/dist/components/combobox/highlightLabel.d.ts.map +1 -0
  66. package/dist/components/combobox/highlightLabel.js +18 -0
  67. package/dist/components/combobox/highlightLabel.js.map +1 -0
  68. package/dist/components/combobox/normaliseComboboxQuery.d.ts +2 -0
  69. package/dist/components/combobox/normaliseComboboxQuery.d.ts.map +1 -0
  70. package/dist/components/combobox/normaliseComboboxQuery.js +2 -0
  71. package/dist/components/combobox/normaliseComboboxQuery.js.map +1 -0
  72. package/dist/components/combobox/types.d.ts +46 -0
  73. package/dist/components/combobox/types.d.ts.map +1 -0
  74. package/dist/components/combobox/types.js +2 -0
  75. package/dist/components/combobox/types.js.map +1 -0
  76. package/dist/components/combobox/useChipSelection.d.ts +11 -0
  77. package/dist/components/combobox/useChipSelection.d.ts.map +1 -0
  78. package/dist/components/combobox/useChipSelection.js +35 -0
  79. package/dist/components/combobox/useChipSelection.js.map +1 -0
  80. package/dist/components/combobox/useComboboxChipKeyboard.d.ts +3 -0
  81. package/dist/components/combobox/useComboboxChipKeyboard.d.ts.map +1 -0
  82. package/dist/components/combobox/useComboboxChipKeyboard.js +103 -0
  83. package/dist/components/combobox/useComboboxChipKeyboard.js.map +1 -0
  84. package/dist/components/combobox/useComboboxChipKeyboard.test.d.ts +2 -0
  85. package/dist/components/combobox/useComboboxChipKeyboard.test.d.ts.map +1 -0
  86. package/dist/components/combobox/useComboboxChipKeyboard.test.js +116 -0
  87. package/dist/components/combobox/useComboboxChipKeyboard.test.js.map +1 -0
  88. package/dist/components/combobox/useComboboxKeyboard.d.ts +4 -0
  89. package/dist/components/combobox/useComboboxKeyboard.d.ts.map +1 -0
  90. package/dist/components/combobox/useComboboxKeyboard.js +68 -0
  91. package/dist/components/combobox/useComboboxKeyboard.js.map +1 -0
  92. package/dist/components/combobox/useComboboxListboxDom.d.ts +11 -0
  93. package/dist/components/combobox/useComboboxListboxDom.d.ts.map +1 -0
  94. package/dist/components/combobox/useComboboxListboxDom.js +15 -0
  95. package/dist/components/combobox/useComboboxListboxDom.js.map +1 -0
  96. package/dist/components/combobox/useComboboxListboxKeyboard.d.ts +3 -0
  97. package/dist/components/combobox/useComboboxListboxKeyboard.d.ts.map +1 -0
  98. package/dist/components/combobox/useComboboxListboxKeyboard.js +143 -0
  99. package/dist/components/combobox/useComboboxListboxKeyboard.js.map +1 -0
  100. package/dist/components/combobox/useComboboxListboxKeyboard.test.d.ts +2 -0
  101. package/dist/components/combobox/useComboboxListboxKeyboard.test.d.ts.map +1 -0
  102. package/dist/components/combobox/useComboboxListboxKeyboard.test.js +152 -0
  103. package/dist/components/combobox/useComboboxListboxKeyboard.test.js.map +1 -0
  104. package/dist/components/combobox/useComboboxPopoverBehavior.d.ts +38 -0
  105. package/dist/components/combobox/useComboboxPopoverBehavior.d.ts.map +1 -0
  106. package/dist/components/combobox/useComboboxPopoverBehavior.js +104 -0
  107. package/dist/components/combobox/useComboboxPopoverBehavior.js.map +1 -0
  108. package/dist/components/combobox/useComboboxState.d.ts +27 -0
  109. package/dist/components/combobox/useComboboxState.d.ts.map +1 -0
  110. package/dist/components/combobox/useComboboxState.js +122 -0
  111. package/dist/components/combobox/useComboboxState.js.map +1 -0
  112. package/dist/components/combobox/useElementWidth.d.ts +2 -0
  113. package/dist/components/combobox/useElementWidth.d.ts.map +1 -0
  114. package/dist/components/combobox/useElementWidth.js +31 -0
  115. package/dist/components/combobox/useElementWidth.js.map +1 -0
  116. package/dist/components/combobox/useVisibleChips.d.ts +21 -0
  117. package/dist/components/combobox/useVisibleChips.d.ts.map +1 -0
  118. package/dist/components/combobox/useVisibleChips.js +59 -0
  119. package/dist/components/combobox/useVisibleChips.js.map +1 -0
  120. package/dist/components/combobox/useVisibleChips.test.d.ts +2 -0
  121. package/dist/components/combobox/useVisibleChips.test.d.ts.map +1 -0
  122. package/dist/components/combobox/useVisibleChips.test.js +81 -0
  123. package/dist/components/combobox/useVisibleChips.test.js.map +1 -0
  124. package/dist/components/dot/Dot.d.ts +8 -0
  125. package/dist/components/dot/Dot.d.ts.map +1 -0
  126. package/dist/components/dot/Dot.js +6 -0
  127. package/dist/components/dot/Dot.js.map +1 -0
  128. package/dist/components/dot/Dot.stories.d.ts +15 -0
  129. package/dist/components/dot/Dot.stories.d.ts.map +1 -0
  130. package/dist/components/dot/Dot.stories.js +25 -0
  131. package/dist/components/dot/Dot.stories.js.map +1 -0
  132. package/dist/components/dot/Dot.test.d.ts +2 -0
  133. package/dist/components/dot/Dot.test.d.ts.map +1 -0
  134. package/dist/components/dot/Dot.test.js +19 -0
  135. package/dist/components/dot/Dot.test.js.map +1 -0
  136. package/dist/components/formField/FormField.d.ts +8 -4
  137. package/dist/components/formField/FormField.d.ts.map +1 -1
  138. package/dist/components/formField/FormField.js +7 -6
  139. package/dist/components/formField/FormField.js.map +1 -1
  140. package/dist/components/formField/FormField.stories.d.ts +1 -0
  141. package/dist/components/formField/FormField.stories.d.ts.map +1 -1
  142. package/dist/components/formField/FormField.stories.js +13 -1
  143. package/dist/components/formField/FormField.stories.js.map +1 -1
  144. package/dist/components/formField/FormField.test.js +10 -0
  145. package/dist/components/formField/FormField.test.js.map +1 -1
  146. package/dist/components/icon/allowedIcons.d.ts +1 -0
  147. package/dist/components/icon/allowedIcons.d.ts.map +1 -1
  148. package/dist/components/icon/allowedIcons.js +2 -1
  149. package/dist/components/icon/allowedIcons.js.map +1 -1
  150. package/dist/components/progress/Progress.stories.d.ts +49 -49
  151. package/dist/components/row/Row.d.ts +10 -0
  152. package/dist/components/row/Row.d.ts.map +1 -0
  153. package/dist/components/row/Row.js +17 -0
  154. package/dist/components/row/Row.js.map +1 -0
  155. package/dist/components/row/Row.stories.d.ts +15 -0
  156. package/dist/components/row/Row.stories.d.ts.map +1 -0
  157. package/dist/components/row/Row.stories.js +65 -0
  158. package/dist/components/row/Row.stories.js.map +1 -0
  159. package/dist/components/row/Row.test.d.ts +2 -0
  160. package/dist/components/row/Row.test.d.ts.map +1 -0
  161. package/dist/components/row/Row.test.js +62 -0
  162. package/dist/components/row/Row.test.js.map +1 -0
  163. package/dist/components/section/Section.stories.d.ts +27 -0
  164. package/dist/components/section/Section.stories.d.ts.map +1 -1
  165. package/dist/components/section/Section.stories.js +45 -1
  166. package/dist/components/section/Section.stories.js.map +1 -1
  167. package/dist/components/singleUser/SingleUser.d.ts +15 -0
  168. package/dist/components/singleUser/SingleUser.d.ts.map +1 -0
  169. package/dist/components/singleUser/SingleUser.js +9 -0
  170. package/dist/components/singleUser/SingleUser.js.map +1 -0
  171. package/dist/components/singleUser/SingleUser.stories.d.ts +11 -0
  172. package/dist/components/singleUser/SingleUser.stories.d.ts.map +1 -0
  173. package/dist/components/singleUser/SingleUser.stories.js +52 -0
  174. package/dist/components/singleUser/SingleUser.stories.js.map +1 -0
  175. package/dist/components/singleUser/SingleUser.test.d.ts +2 -0
  176. package/dist/components/singleUser/SingleUser.test.d.ts.map +1 -0
  177. package/dist/components/singleUser/SingleUser.test.js +30 -0
  178. package/dist/components/singleUser/SingleUser.test.js.map +1 -0
  179. package/dist/components/table/cellRenderers/SelectDropdownCellRenderer.d.ts.map +1 -1
  180. package/dist/components/table/cellRenderers/SelectDropdownCellRenderer.js +9 -3
  181. package/dist/components/table/cellRenderers/SelectDropdownCellRenderer.js.map +1 -1
  182. package/dist/components/tabs/TabsItem.stories.d.ts +2 -2
  183. package/dist/components/tag/Tag.d.ts +9 -6
  184. package/dist/components/tag/Tag.d.ts.map +1 -1
  185. package/dist/components/tag/Tag.js +8 -2
  186. package/dist/components/tag/Tag.js.map +1 -1
  187. package/dist/components/tag/Tag.stories.d.ts +11 -6
  188. package/dist/components/tag/Tag.stories.d.ts.map +1 -1
  189. package/dist/components/tag/Tag.stories.js +68 -4
  190. package/dist/components/tag/Tag.stories.js.map +1 -1
  191. package/dist/components/tag/Tag.test.js +86 -50
  192. package/dist/components/tag/Tag.test.js.map +1 -1
  193. package/dist/components/toggle/Toggle.d.ts +3 -0
  194. package/dist/components/toggle/Toggle.d.ts.map +1 -0
  195. package/dist/components/toggle/Toggle.js +8 -0
  196. package/dist/components/toggle/Toggle.js.map +1 -0
  197. package/dist/components/toggle/Toggle.stories.d.ts +97 -0
  198. package/dist/components/toggle/Toggle.stories.d.ts.map +1 -0
  199. package/dist/components/toggle/Toggle.stories.js +186 -0
  200. package/dist/components/toggle/Toggle.stories.js.map +1 -0
  201. package/dist/components/toggle/Toggle.test.d.ts +2 -0
  202. package/dist/components/toggle/Toggle.test.d.ts.map +1 -0
  203. package/dist/components/toggle/Toggle.test.js +58 -0
  204. package/dist/components/toggle/Toggle.test.js.map +1 -0
  205. package/dist/index.css +703 -25
  206. package/dist/index.css.map +1 -1
  207. package/dist/index.d.ts +35 -25
  208. package/dist/index.d.ts.map +1 -1
  209. package/dist/index.js +31 -25
  210. package/dist/index.js.map +1 -1
  211. package/dist/mocks/comboboxStoryOptions.d.ts +5 -0
  212. package/dist/mocks/comboboxStoryOptions.d.ts.map +1 -0
  213. package/dist/mocks/comboboxStoryOptions.js +22 -0
  214. package/dist/mocks/comboboxStoryOptions.js.map +1 -0
  215. package/dist/utils/isSelectAllChord.d.ts +5 -0
  216. package/dist/utils/isSelectAllChord.d.ts.map +1 -0
  217. package/dist/utils/isSelectAllChord.js +7 -0
  218. package/dist/utils/isSelectAllChord.js.map +1 -0
  219. package/dist/utils/isSelectAllChord.test.d.ts +2 -0
  220. package/dist/utils/isSelectAllChord.test.d.ts.map +1 -0
  221. package/dist/utils/isSelectAllChord.test.js +19 -0
  222. package/dist/utils/isSelectAllChord.test.js.map +1 -0
  223. package/dist/utils/nextCircularIndex.d.ts +3 -0
  224. package/dist/utils/nextCircularIndex.d.ts.map +1 -0
  225. package/dist/utils/nextCircularIndex.js +10 -0
  226. package/dist/utils/nextCircularIndex.js.map +1 -0
  227. package/dist/utils/nextCircularIndex.test.d.ts +2 -0
  228. package/dist/utils/nextCircularIndex.test.d.ts.map +1 -0
  229. package/dist/utils/nextCircularIndex.test.js +23 -0
  230. package/dist/utils/nextCircularIndex.test.js.map +1 -0
  231. package/dist/utils/scrollElementIntoViewById.d.ts +2 -0
  232. package/dist/utils/scrollElementIntoViewById.d.ts.map +1 -0
  233. package/dist/utils/scrollElementIntoViewById.js +16 -0
  234. package/dist/utils/scrollElementIntoViewById.js.map +1 -0
  235. package/dist/utils/scrollElementIntoViewById.test.d.ts +2 -0
  236. package/dist/utils/scrollElementIntoViewById.test.d.ts.map +1 -0
  237. package/dist/utils/scrollElementIntoViewById.test.js +31 -0
  238. package/dist/utils/scrollElementIntoViewById.test.js.map +1 -0
  239. package/package.json +1 -1
  240. package/src/components/avatar/Avatar.stories.tsx +8 -0
  241. package/src/components/avatar/Avatar.tsx +3 -3
  242. package/src/components/badge/Badge.stories.tsx +74 -0
  243. package/src/components/badge/Badge.test.tsx +28 -0
  244. package/src/components/badge/Badge.tsx +35 -0
  245. package/src/components/badge/badge.scss +86 -0
  246. package/src/components/card/Card.tsx +1 -1
  247. package/src/components/combobox/Combobox.stories.tsx +340 -0
  248. package/src/components/combobox/Combobox.test.tsx +1160 -0
  249. package/src/components/combobox/Combobox.tsx +434 -0
  250. package/src/components/combobox/ComboboxButtonTrigger.tsx +195 -0
  251. package/src/components/combobox/ComboboxListbox.tsx +224 -0
  252. package/src/components/combobox/ComboboxOptionRow.tsx +128 -0
  253. package/src/components/combobox/ComboboxTrigger.tsx +134 -0
  254. package/src/components/combobox/buildListboxDisplayOptions.test.ts +24 -0
  255. package/src/components/combobox/buildListboxDisplayOptions.ts +12 -0
  256. package/src/components/combobox/combobox.scss +390 -0
  257. package/src/components/combobox/comboboxKeyboardTypes.ts +45 -0
  258. package/src/components/combobox/highlightLabel.tsx +42 -0
  259. package/src/components/combobox/normaliseComboboxQuery.ts +1 -0
  260. package/src/components/combobox/types.ts +53 -0
  261. package/src/components/combobox/useChipSelection.ts +53 -0
  262. package/src/components/combobox/useComboboxChipKeyboard.test.tsx +141 -0
  263. package/src/components/combobox/useComboboxChipKeyboard.ts +121 -0
  264. package/src/components/combobox/useComboboxKeyboard.ts +108 -0
  265. package/src/components/combobox/useComboboxListboxDom.ts +36 -0
  266. package/src/components/combobox/useComboboxListboxKeyboard.test.tsx +186 -0
  267. package/src/components/combobox/useComboboxListboxKeyboard.ts +172 -0
  268. package/src/components/combobox/useComboboxPopoverBehavior.ts +179 -0
  269. package/src/components/combobox/useComboboxState.ts +232 -0
  270. package/src/components/combobox/useElementWidth.ts +40 -0
  271. package/src/components/combobox/useVisibleChips.test.tsx +91 -0
  272. package/src/components/combobox/useVisibleChips.ts +100 -0
  273. package/src/components/dot/Dot.stories.tsx +41 -0
  274. package/src/components/dot/Dot.test.tsx +21 -0
  275. package/src/components/dot/Dot.tsx +18 -0
  276. package/src/components/dot/dot.scss +35 -0
  277. package/src/components/formField/FormField.stories.tsx +30 -1
  278. package/src/components/formField/FormField.test.tsx +20 -0
  279. package/src/components/formField/FormField.tsx +11 -5
  280. package/src/components/formField/inputs/number/numberInput.scss +12 -4
  281. package/src/components/icon/allowedIcons.tsx +2 -0
  282. package/src/components/pill/pill.scss +4 -6
  283. package/src/components/row/Row.stories.tsx +85 -0
  284. package/src/components/row/Row.test.tsx +82 -0
  285. package/src/components/row/Row.tsx +54 -0
  286. package/src/components/row/row.scss +61 -0
  287. package/src/components/section/Section.stories.tsx +56 -0
  288. package/src/components/singleUser/SingleUser.stories.tsx +63 -0
  289. package/src/components/singleUser/SingleUser.test.tsx +61 -0
  290. package/src/components/singleUser/SingleUser.tsx +45 -0
  291. package/src/components/singleUser/singleUser.scss +14 -0
  292. package/src/components/table/cellRenderers/SelectDropdownCellRenderer.tsx +19 -3
  293. package/src/components/tag/Tag.stories.tsx +88 -6
  294. package/src/components/tag/Tag.test.tsx +110 -44
  295. package/src/components/tag/Tag.tsx +38 -14
  296. package/src/components/tag/tag.scss +45 -30
  297. package/src/components/toggle/Toggle.stories.tsx +239 -0
  298. package/src/components/toggle/Toggle.test.tsx +66 -0
  299. package/src/components/toggle/Toggle.tsx +12 -0
  300. package/src/components/toggle/toggle.scss +126 -0
  301. package/src/index.scss +6 -0
  302. package/src/index.ts +48 -31
  303. package/src/mocks/comboboxStoryOptions.ts +25 -0
  304. package/src/tokens.scss +33 -4
  305. package/src/utils/isSelectAllChord.test.ts +24 -0
  306. package/src/utils/isSelectAllChord.ts +8 -0
  307. package/src/utils/nextCircularIndex.test.ts +26 -0
  308. package/src/utils/nextCircularIndex.ts +15 -0
  309. package/src/utils/scrollElementIntoViewById.test.ts +38 -0
  310. package/src/utils/scrollElementIntoViewById.ts +20 -0
  311. package/tokens/json/Arbor.json +3828 -3704
@@ -0,0 +1,100 @@
1
+ import { useMemo } from 'react';
2
+
3
+ type ComputeTriggerLayoutModelParams = {
4
+ containerWidth: number;
5
+ chipWidths: number[];
6
+ chipGap: number;
7
+ badgeWidth: number;
8
+ ellipsisWidth: number;
9
+ showBadge: boolean;
10
+ safetyBuffer?: number;
11
+ };
12
+
13
+ export type TriggerLayoutModel = {
14
+ visibleChipIndices: number[];
15
+ hiddenChipCount: number;
16
+ showBadge: boolean;
17
+ showEllipsis: boolean;
18
+ hasOverflow: boolean;
19
+ };
20
+
21
+ const fitCount = (
22
+ availableWidth: number,
23
+ chipWidths: number[],
24
+ chipGap: number,
25
+ ): number => {
26
+ if (availableWidth <= 0 || chipWidths.length === 0) return 0;
27
+
28
+ let used = 0;
29
+ let count = 0;
30
+ for (let i = 0; i < chipWidths.length; i += 1) {
31
+ const required = chipWidths[i]! + (i > 0 ? chipGap : 0);
32
+ if (used + required > availableWidth) break;
33
+ used += required;
34
+ count += 1;
35
+ }
36
+ return count;
37
+ };
38
+
39
+ export const computeTriggerLayoutModel = ({
40
+ containerWidth,
41
+ chipWidths,
42
+ chipGap,
43
+ badgeWidth,
44
+ ellipsisWidth,
45
+ showBadge,
46
+ safetyBuffer = 1,
47
+ }: ComputeTriggerLayoutModelParams): TriggerLayoutModel => {
48
+ const selectedCount = chipWidths.length;
49
+ const shouldShowBadge = showBadge && selectedCount > 0;
50
+
51
+ if (selectedCount === 0 || containerWidth <= 0) {
52
+ return {
53
+ visibleChipIndices: [],
54
+ hiddenChipCount: 0,
55
+ showBadge: shouldShowBadge,
56
+ showEllipsis: false,
57
+ hasOverflow: false,
58
+ };
59
+ }
60
+
61
+ const badgeReserve = shouldShowBadge
62
+ ? badgeWidth + chipGap
63
+ : 0;
64
+
65
+ const availableWithoutEllipsis = Math.max(0, containerWidth - badgeReserve - safetyBuffer);
66
+ const countWithoutEllipsis = fitCount(availableWithoutEllipsis, chipWidths, chipGap);
67
+ const hasOverflow = countWithoutEllipsis < selectedCount;
68
+
69
+ const ellipsisReserve = hasOverflow
70
+ ? ellipsisWidth + chipGap
71
+ : 0;
72
+ const availableForChips = Math.max(0, containerWidth - badgeReserve - ellipsisReserve - safetyBuffer);
73
+ const visibleCount = fitCount(availableForChips, chipWidths, chipGap);
74
+
75
+ return {
76
+ visibleChipIndices: Array.from({ length: visibleCount }, (_, index) => index),
77
+ hiddenChipCount: Math.max(0, selectedCount - visibleCount),
78
+ showBadge: shouldShowBadge,
79
+ showEllipsis: visibleCount < selectedCount,
80
+ hasOverflow: visibleCount < selectedCount,
81
+ };
82
+ };
83
+
84
+ type UseVisibleChipsParams = ComputeTriggerLayoutModelParams;
85
+
86
+ export const useVisibleChips = (params: UseVisibleChipsParams): TriggerLayoutModel =>
87
+ // Deps list each field instead of `[params]`: callers often pass a new `params` object every
88
+ // render; depending on object identity would rerun the layout every time even when values are unchanged.
89
+ useMemo(
90
+ () => computeTriggerLayoutModel(params),
91
+ [
92
+ params.badgeWidth,
93
+ params.chipGap,
94
+ params.chipWidths,
95
+ params.containerWidth,
96
+ params.ellipsisWidth,
97
+ params.safetyBuffer,
98
+ params.showBadge,
99
+ ],
100
+ );
@@ -0,0 +1,41 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-vite';
2
+ import { Dot } from './Dot';
3
+
4
+ const meta: Meta<typeof Dot> = {
5
+ tags: ['autodocs'],
6
+ title: 'Components/Dot',
7
+ component: Dot,
8
+ };
9
+
10
+ export default meta;
11
+
12
+ type Story = StoryObj<typeof Dot>;
13
+
14
+ export const Purple: Story = { args: { colour: 'purple' } };
15
+ export const Salmon: Story = { args: { colour: 'salmon' } };
16
+ export const Teal: Story = { args: { colour: 'teal' } };
17
+ export const Yellow: Story = { args: { colour: 'yellow' } };
18
+ export const Green: Story = { args: { colour: 'green' } };
19
+ export const Orange: Story = { args: { colour: 'orange' } };
20
+ export const Blue: Story = { args: { colour: 'blue' } };
21
+
22
+ export const Labeled: Story = {
23
+ args: {
24
+ colour: 'purple',
25
+ label: 'Priority indicator',
26
+ },
27
+ };
28
+
29
+ export const AllColours: Story = {
30
+ render: () => (
31
+ <div style={{ display: 'flex', gap: 8, alignItems: 'center' }}>
32
+ <Dot colour="purple" />
33
+ <Dot colour="salmon" />
34
+ <Dot colour="teal" />
35
+ <Dot colour="yellow" />
36
+ <Dot colour="green" />
37
+ <Dot colour="orange" />
38
+ <Dot colour="blue" />
39
+ </div>
40
+ ),
41
+ };
@@ -0,0 +1,21 @@
1
+ import { render, screen } from '@testing-library/react';
2
+ import '@testing-library/jest-dom/vitest';
3
+ import { describe, expect, test } from 'vitest';
4
+ import { Dot } from './Dot';
5
+
6
+ describe('Dot', () => {
7
+ test('renders a dot with an explicit label', () => {
8
+ const { container } = render(<Dot colour="purple" label="Priority indicator" />);
9
+
10
+ expect(screen.getByLabelText('Priority indicator')).toBeInTheDocument();
11
+ expect(container.firstChild).toHaveClass('ds-dot', 'ds-dot--purple');
12
+ });
13
+
14
+ test('is decorative when label is not provided', () => {
15
+ const { container } = render(<Dot colour="teal" />);
16
+
17
+ expect(screen.queryByLabelText('teal dot')).not.toBeInTheDocument();
18
+ expect(container.firstChild).toHaveAttribute('aria-hidden', 'true');
19
+ expect(container.firstChild).not.toHaveAttribute('aria-label');
20
+ });
21
+ });
@@ -0,0 +1,18 @@
1
+ import classNames from 'classnames';
2
+
3
+ export type DotColour = 'purple' | 'salmon' | 'teal' | 'yellow' | 'green' | 'orange' | 'blue';
4
+
5
+ type DotProps = {
6
+ colour: DotColour;
7
+ label?: string;
8
+ };
9
+
10
+ export const Dot = ({ colour, label }: DotProps): React.JSX.Element => {
11
+ return (
12
+ <span
13
+ aria-hidden={label ? undefined : 'true'}
14
+ aria-label={label}
15
+ className={classNames('ds-dot', `ds-dot--${colour}`)}
16
+ />
17
+ );
18
+ };
@@ -0,0 +1,35 @@
1
+ .ds-dot {
2
+ display: inline-block;
3
+ width: 10px;
4
+ height: 10px;
5
+ border-radius: 50%;
6
+ flex-shrink: 0;
7
+
8
+ &--purple {
9
+ background-color: var(--color-extended-colours-purple-500);
10
+ }
11
+
12
+ &--salmon {
13
+ background-color: var(--color-extended-colours-salmon-500);
14
+ }
15
+
16
+ &--teal {
17
+ background-color: var(--color-extended-colours-teal-500);
18
+ }
19
+
20
+ &--yellow {
21
+ background-color: var(--color-extended-colours-yellow-500);
22
+ }
23
+
24
+ &--green {
25
+ background-color: var(--color-extended-colours-green-500);
26
+ }
27
+
28
+ &--orange {
29
+ background-color: var(--color-extended-colours-orange-500);
30
+ }
31
+
32
+ &--blue {
33
+ background-color: var(--color-extended-colours-blue-500);
34
+ }
35
+ }
@@ -1,6 +1,7 @@
1
1
  import type { Meta, StoryObj } from '@storybook/react-vite';
2
2
  import { fn } from 'storybook/test';
3
3
 
4
+ import { comboboxPeopleOptions } from '../../mocks/comboboxStoryOptions';
4
5
  import { FormField } from './FormField';
5
6
 
6
7
  const meta: Meta<typeof FormField> = {
@@ -41,7 +42,7 @@ export const Default = {
41
42
  },
42
43
  'inputType': {
43
44
  control: 'select',
44
- options: ['text', 'textarea', 'number', 'datePicker'],
45
+ options: ['text', 'textarea', 'number', 'colourPicker', 'selectDropdown', 'datePicker', 'combobox'],
45
46
  description: 'Input type',
46
47
  },
47
48
  'inputProps.size': {
@@ -130,6 +131,16 @@ export const FormExample: Story = {
130
131
  onSelectionChange: fn(),
131
132
  }}
132
133
  />
134
+ <FormField
135
+ id="assignee"
136
+ label="Assignee"
137
+ inputType="combobox"
138
+ inputProps={{
139
+ options: comboboxPeopleOptions,
140
+ placeholder: 'Search people...',
141
+ onValueChange: fn(),
142
+ }}
143
+ />
133
144
  <FormField
134
145
  id="date-of-birth"
135
146
  label="Date of Birth"
@@ -140,4 +151,22 @@ export const FormExample: Story = {
140
151
  ),
141
152
  };
142
153
 
154
+ export const Combobox: Story = {
155
+ render: () => (
156
+ <div data-surface="base" data-colour-mode="light" className="bg-surface text-on-surface-default" style={{ padding: 32, maxWidth: 420 }}>
157
+ <FormField
158
+ id="form-field-combobox"
159
+ label="Assignee"
160
+ fieldDescription="Search and select a person."
161
+ inputType="combobox"
162
+ inputProps={{
163
+ options: comboboxPeopleOptions,
164
+ placeholder: 'Search people...',
165
+ onValueChange: fn(),
166
+ }}
167
+ />
168
+ </div>
169
+ ),
170
+ };
171
+
143
172
  export default meta;
@@ -62,4 +62,24 @@ describe('FormField component', () => {
62
62
  expect(screen.getByRole('textbox')).toBeInTheDocument();
63
63
  expect(screen.getByRole('button', { name: 'Open date picker' })).toBeInTheDocument();
64
64
  });
65
+
66
+ test('passes error state through to Combobox when errorText is provided', () => {
67
+ render(
68
+ <FormField
69
+ id="student-combobox"
70
+ label="Student"
71
+ inputType="combobox"
72
+ errorText="Choose a student"
73
+ inputProps={{
74
+ options: [{ value: 'alice', label: 'Alice Johnson' }],
75
+ placeholder: 'Search students',
76
+ }}
77
+ />,
78
+ );
79
+
80
+ const input = screen.getByRole('combobox', { name: 'Student' });
81
+ expect(input).toHaveAttribute('aria-invalid', 'true');
82
+ expect(input.closest('.ds-combobox__trigger')).toHaveClass('ds-combobox__trigger--error');
83
+ expect(screen.getByText('Choose a student')).toBeInTheDocument();
84
+ });
65
85
  });
@@ -1,12 +1,14 @@
1
1
  import classNames from 'classnames';
2
- import { Label } from './label/Label';
2
+ import { Combobox } from 'Components/combobox/Combobox';
3
+ import type { ComboboxProps } from 'Components/combobox/types';
4
+ import { DatePicker, type DatePickerProps } from 'Components/datePicker/DatePicker';
3
5
  import { Icon } from '../icon/Icon';
4
- import { TextInput, type TextInputProps } from './inputs/text/TextInput';
5
- import { TextArea, type TextAreaProps } from './inputs/textArea/TextArea';
6
- import { NumberInput, type NumberInputProps } from './inputs/number/NumberInput';
7
6
  import { ColourPickerDropdown, type ColourPickerDropdownProps } from './inputs/colourPickerDropdown/ColourPickerDropdown';
7
+ import { NumberInput, type NumberInputProps } from './inputs/number/NumberInput';
8
8
  import { SelectDropdown, type SelectDropdownInputProps } from './inputs/selectDropdown/SelectDropdown';
9
- import { DatePicker, type DatePickerProps } from 'Components/datePicker/DatePicker';
9
+ import { TextInput, type TextInputProps } from './inputs/text/TextInput';
10
+ import { TextArea, type TextAreaProps } from './inputs/textArea/TextArea';
11
+ import { Label } from './label/Label';
10
12
 
11
13
  type FormFieldProps = {
12
14
  className?: string;
@@ -23,6 +25,7 @@ type FormFieldProps = {
23
25
  | { inputType?: 'colourPicker'; inputProps?: ColourPickerDropdownProps }
24
26
  | { inputType?: 'selectDropdown'; inputProps?: SelectDropdownInputProps }
25
27
  | { inputType?: 'datePicker'; inputProps?: DatePickerProps }
28
+ | { inputType?: 'combobox'; inputProps?: ComboboxProps }
26
29
  );
27
30
 
28
31
  export const FormField = (props: FormFieldProps) => {
@@ -71,6 +74,9 @@ export const FormField = (props: FormFieldProps) => {
71
74
  {inputType === 'datePicker' && (
72
75
  <DatePicker {...sharedProps} {...(inputProps as DatePickerProps)} />
73
76
  )}
77
+ {inputType === 'combobox' && (
78
+ <Combobox {...sharedProps} {...(inputProps as ComboboxProps)} />
79
+ )}
74
80
  {((helperLinkText && helperLinkUrl) || errorText) && (
75
81
  <div className="ds-form-field__message">
76
82
  {errorText && (
@@ -1,10 +1,13 @@
1
+ // Match single-line text field height (ds-input--M), not textarea / padded blocks.
1
2
  .ds-number-input__container {
2
3
  display: flex;
3
- flex-wrap: wrap;
4
+ flex-wrap: nowrap;
4
5
  justify-content: center;
5
- align-items: center;
6
- padding: var(--form-field-spacing-vertical) var(--form-field-spacing-padding-x);
7
- gap: var(--form-field-spacing-horizontal) var(--form-field-spacing-vertical-gap);
6
+ align-items: stretch;
7
+ box-sizing: border-box;
8
+ height: var(--form-field-text-medium-height);
9
+ padding: 0 var(--form-field-spacing-horizontal);
10
+ gap: var(--form-field-spacing-horizontal);
8
11
  border-radius: var(--form-field-radius);
9
12
  border: var(--border-weight) solid var(--form-field-text-placeholder-color-border);
10
13
  background: var(--form-field-text-placeholder-color-background);
@@ -40,6 +43,9 @@
40
43
  text-align: center;
41
44
  border: none;
42
45
  flex: 1;
46
+ min-width: 0;
47
+ min-height: 0;
48
+ height: 100%;
43
49
  padding: 0;
44
50
  color: var(--form-field-text-default-color-text);
45
51
 
@@ -50,11 +56,13 @@
50
56
 
51
57
  .ds-number-input__spinner-button {
52
58
  display: flex;
59
+ flex-shrink: 0;
53
60
  border: none;
54
61
  cursor: pointer;
55
62
  background: none;
56
63
  justify-content: center;
57
64
  align-items: center;
65
+ align-self: center;
58
66
  width: var(--icon-size-medium);
59
67
  height: var(--icon-size-medium);
60
68
  color: var(--form-field-icon-default-color-icon);
@@ -55,6 +55,7 @@ import {
55
55
  Link,
56
56
  List,
57
57
  ListFilterPlus,
58
+ LoaderCircle,
58
59
  Lock,
59
60
  LockOpen,
60
61
  LogOut, type LucideProps,
@@ -155,6 +156,7 @@ export const allowedIcons = {
155
156
  'link': Link,
156
157
  'list-filter-plus': ListFilterPlus,
157
158
  'list': List,
159
+ 'loader': LoaderCircle,
158
160
  'lock-open': LockOpen,
159
161
  'lock': Lock,
160
162
  'log-out': LogOut,
@@ -1,17 +1,15 @@
1
1
  .ds-pill {
2
2
  display: flex;
3
- padding: var(--tag-spacing-vertical) var(--tag-spacing-horizontal);
3
+ padding: var(--pill-spacing-vertical) var(--pill-spacing-horizontal);
4
4
  align-items: center;
5
- gap: var(--tag-spacing-gap-horizontal);
6
- border-radius: var(--tag-radius);
5
+ gap: var(--pill-spacing-gap-horizontal);
6
+ border-radius: var(--pill-radius);
7
7
  border: 1px solid var(--pill-single-filter-default-color-border);
8
8
  flex-grow: 0;
9
9
  width: fit-content;
10
10
  cursor: pointer;
11
-
12
- /* typography/body/p1-reg */
13
11
  font-style: normal;
14
- line-height: 150%; /* 19.5px */
12
+ line-height: 150%;
15
13
 
16
14
  &__inactive{
17
15
  background-color: var(--pill-single-filter-default-color-background);
@@ -0,0 +1,85 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-vite';
2
+ import { Row } from './Row';
3
+
4
+ const meta: Meta<typeof Row> = {
5
+ title: 'Components/Row',
6
+ component: Row,
7
+ };
8
+
9
+ type Story = StoryObj<typeof meta>;
10
+
11
+ export const Default: Story = {
12
+ args: {
13
+ label: 'Label Text',
14
+ value: 'Value',
15
+ note: 'Note',
16
+ },
17
+ };
18
+
19
+ export const LabelOnly: Story = {
20
+ args: {
21
+ label: 'Full Name',
22
+ },
23
+ };
24
+
25
+ export const LabelAndValue: Story = {
26
+ args: {
27
+ label: 'Email Address',
28
+ value: 'jacob.black@forks.edu',
29
+ },
30
+ };
31
+
32
+ export const WithNote: Story = {
33
+ args: {
34
+ label: 'Date of Birth',
35
+ value: '13/01/1990',
36
+ note: 'Age 36',
37
+ },
38
+ };
39
+
40
+ export const Clickable: Story = {
41
+ args: {
42
+ label: 'View Profile',
43
+ value: 'Jacob Black',
44
+ onClick: () => { console.log('click!'); },
45
+ },
46
+ };
47
+
48
+ export const ClickableWithNote: Story = {
49
+ args: {
50
+ label: 'Attendance Record',
51
+ value: '96%',
52
+ note: 'Last updated today',
53
+ onClick: () => { console.log('click!'); },
54
+ },
55
+ };
56
+
57
+ export const WithCustomClassName: Story = {
58
+ args: {
59
+ label: 'Custom Styled',
60
+ value: 'With extra class',
61
+ className: 'custom-row-class',
62
+ },
63
+ };
64
+
65
+ export const LongContent: Story = {
66
+ args: {
67
+ label: 'A very long label that might wrap depending on the container width',
68
+ value: 'A very long value that contains lots of information and might also need to wrap',
69
+ note: 'This is a longer note with additional context',
70
+ },
71
+ };
72
+
73
+ export const MultipleRows: Story = {
74
+ render: () => (
75
+ <div style={{ display: 'flex', flexDirection: 'column', gap: '4px' }}>
76
+ <Row label="First Name" value="Jacob" />
77
+ <Row label="Last Name" value="Black" />
78
+ <Row label="Year Group" value="Year 10" note="2024/2025" />
79
+ <Row label="Form Group" value="10A" />
80
+ <Row label="Attendance" value="96%" note="Above target" onClick={() => { console.log('click!'); }} />
81
+ </div>
82
+ ),
83
+ };
84
+
85
+ export default meta;
@@ -0,0 +1,82 @@
1
+ import { expect, test, describe, vi } from 'vitest';
2
+ import { render, screen, fireEvent } from '@testing-library/react';
3
+ import { Row } from './Row';
4
+ import '@testing-library/jest-dom/vitest';
5
+
6
+ describe('Row component', () => {
7
+ test('renders with label, value, and note', () => {
8
+ render(<Row label="Full Name" value="Jacob Black" note="Team Jacob forever" />);
9
+
10
+ expect(screen.getByText('Full Name')).toBeInTheDocument();
11
+ expect(screen.getByText('Jacob Black')).toBeInTheDocument();
12
+ expect(screen.getByText('Team Jacob forever')).toBeInTheDocument();
13
+ });
14
+
15
+ test('renders with only a label', () => {
16
+ render(<Row label="Just a label" />);
17
+
18
+ expect(screen.getByText('Just a label')).toBeInTheDocument();
19
+ });
20
+
21
+ test('renders with only a value', () => {
22
+ render(<Row value="Just a value" />);
23
+
24
+ expect(screen.getByText('Just a value')).toBeInTheDocument();
25
+ });
26
+
27
+ test('renders with only a note', () => {
28
+ render(<Row note="Just a note" />);
29
+
30
+ expect(screen.getByText('Just a note')).toBeInTheDocument();
31
+ });
32
+
33
+ test('renders empty row without crashing', () => {
34
+ const { container } = render(<Row />);
35
+
36
+ expect(container.querySelector('.ds-row')).toBeInTheDocument();
37
+ });
38
+
39
+ test('applies custom className', () => {
40
+ const { container } = render(<Row className="custom-row" />);
41
+
42
+ const row = container.querySelector('.ds-row');
43
+ expect(row).toHaveClass('ds-row', 'custom-row');
44
+ });
45
+
46
+ describe('click behaviour', () => {
47
+ test('calls onClick handler when clicked', () => {
48
+ const handleClick = vi.fn();
49
+ const { container } = render(<Row label="Clickable" onClick={handleClick} />);
50
+
51
+ const row = container.querySelector('.ds-row')!;
52
+ fireEvent.click(row);
53
+
54
+ expect(handleClick).toHaveBeenCalledTimes(1);
55
+ });
56
+
57
+ test('renders chevron icons when onClick is provided', () => {
58
+ const { container } = render(<Row label="Clickable" onClick={vi.fn()} />);
59
+
60
+ const icon = container.querySelector('.ds-row__icon-click');
61
+ expect(icon).toBeInTheDocument();
62
+ });
63
+
64
+ test('does not render chevron icons when onClick is not provided', () => {
65
+ const { container } = render(<Row label="Not clickable" />);
66
+
67
+ const icon = container.querySelector('.ds-row__icon-click');
68
+ expect(icon).not.toBeInTheDocument();
69
+ });
70
+
71
+ test('passes click event to handler', () => {
72
+ const handleClick = vi.fn();
73
+ const { container } = render(<Row onClick={handleClick} />);
74
+
75
+ const row = container.querySelector('.ds-row')!;
76
+ fireEvent.click(row);
77
+
78
+ expect(handleClick).toHaveBeenCalledTimes(1);
79
+ expect(handleClick.mock.calls[0]?.[0]).toBeDefined();
80
+ });
81
+ });
82
+ });
@@ -0,0 +1,54 @@
1
+ import classNames from 'classnames';
2
+ import { Icon } from 'Components/icon/Icon';
3
+ import type { MouseEvent, MouseEventHandler } from 'react';
4
+ import { ENTER_KEY, SPACE_KEY } from 'Utils/keyboardConstants';
5
+
6
+ export type RowProps = {
7
+ className?: string;
8
+ label?: string;
9
+ value?: string;
10
+ note?: string;
11
+ onClick?: MouseEventHandler<HTMLDivElement>;
12
+ };
13
+
14
+ export const Row = (props: RowProps) => {
15
+ const {
16
+ className,
17
+ label,
18
+ value,
19
+ note,
20
+ onClick,
21
+ } = props;
22
+
23
+ const isClickable = !!onClick;
24
+
25
+ return (
26
+ <div
27
+ className={classNames(
28
+ 'ds-row',
29
+ {
30
+ 'ds-row--clickable': isClickable,
31
+ },
32
+ className,
33
+ )}
34
+ onClick={onClick}
35
+ onKeyDown={(e) => {
36
+ if (isClickable && [ENTER_KEY, SPACE_KEY].includes(e.key)) {
37
+ e.preventDefault();
38
+ onClick(e as unknown as MouseEvent<HTMLDivElement>);
39
+ }
40
+ }}
41
+ tabIndex={isClickable ? 0 : -1}
42
+ >
43
+ <span className="ds-row__label">{label}</span>
44
+ <span className="ds-row__value">{value}</span>
45
+ <span className="ds-row__note">{note}</span>
46
+ {isClickable && (
47
+ <>
48
+ <Icon name="chevron-right" className="ds-row__icon-click" size={16} />
49
+ <Icon name="arrow-right" className="ds-row__icon-click" size={16} />
50
+ </>
51
+ )}
52
+ </div>
53
+ );
54
+ };