@arbor-education/design-system.components 0.6.0 → 0.7.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 (285) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/dist/components/avatar/Avatar.d.ts +1 -1
  3. package/dist/components/avatar/Avatar.d.ts.map +1 -1
  4. package/dist/components/avatar/Avatar.js +1 -1
  5. package/dist/components/avatar/Avatar.js.map +1 -1
  6. package/dist/components/avatar/Avatar.stories.d.ts.map +1 -1
  7. package/dist/components/avatar/Avatar.stories.js +7 -0
  8. package/dist/components/avatar/Avatar.stories.js.map +1 -1
  9. package/dist/components/badge/Badge.d.ts +12 -0
  10. package/dist/components/badge/Badge.d.ts.map +1 -0
  11. package/dist/components/badge/Badge.js +6 -0
  12. package/dist/components/badge/Badge.js.map +1 -0
  13. package/dist/components/badge/Badge.stories.d.ts +10 -0
  14. package/dist/components/badge/Badge.stories.d.ts.map +1 -0
  15. package/dist/components/badge/Badge.stories.js +51 -0
  16. package/dist/components/badge/Badge.stories.js.map +1 -0
  17. package/dist/components/badge/Badge.test.d.ts +2 -0
  18. package/dist/components/badge/Badge.test.d.ts.map +1 -0
  19. package/dist/components/badge/Badge.test.js +23 -0
  20. package/dist/components/badge/Badge.test.js.map +1 -0
  21. package/dist/components/card/Card.js +1 -1
  22. package/dist/components/card/Card.js.map +1 -1
  23. package/dist/components/combobox/Combobox.d.ts +16 -0
  24. package/dist/components/combobox/Combobox.d.ts.map +1 -0
  25. package/dist/components/combobox/Combobox.js +195 -0
  26. package/dist/components/combobox/Combobox.js.map +1 -0
  27. package/dist/components/combobox/Combobox.stories.d.ts +24 -0
  28. package/dist/components/combobox/Combobox.stories.d.ts.map +1 -0
  29. package/dist/components/combobox/Combobox.stories.js +246 -0
  30. package/dist/components/combobox/Combobox.stories.js.map +1 -0
  31. package/dist/components/combobox/Combobox.test.d.ts +2 -0
  32. package/dist/components/combobox/Combobox.test.d.ts.map +1 -0
  33. package/dist/components/combobox/Combobox.test.js +798 -0
  34. package/dist/components/combobox/Combobox.test.js.map +1 -0
  35. package/dist/components/combobox/ComboboxButtonTrigger.d.ts +28 -0
  36. package/dist/components/combobox/ComboboxButtonTrigger.d.ts.map +1 -0
  37. package/dist/components/combobox/ComboboxButtonTrigger.js +64 -0
  38. package/dist/components/combobox/ComboboxButtonTrigger.js.map +1 -0
  39. package/dist/components/combobox/ComboboxListbox.d.ts +44 -0
  40. package/dist/components/combobox/ComboboxListbox.d.ts.map +1 -0
  41. package/dist/components/combobox/ComboboxListbox.js +37 -0
  42. package/dist/components/combobox/ComboboxListbox.js.map +1 -0
  43. package/dist/components/combobox/ComboboxOptionRow.d.ts +23 -0
  44. package/dist/components/combobox/ComboboxOptionRow.d.ts.map +1 -0
  45. package/dist/components/combobox/ComboboxOptionRow.js +27 -0
  46. package/dist/components/combobox/ComboboxOptionRow.js.map +1 -0
  47. package/dist/components/combobox/ComboboxTrigger.d.ts +35 -0
  48. package/dist/components/combobox/ComboboxTrigger.d.ts.map +1 -0
  49. package/dist/components/combobox/ComboboxTrigger.js +15 -0
  50. package/dist/components/combobox/ComboboxTrigger.js.map +1 -0
  51. package/dist/components/combobox/buildListboxDisplayOptions.d.ts +3 -0
  52. package/dist/components/combobox/buildListboxDisplayOptions.d.ts.map +1 -0
  53. package/dist/components/combobox/buildListboxDisplayOptions.js +13 -0
  54. package/dist/components/combobox/buildListboxDisplayOptions.js.map +1 -0
  55. package/dist/components/combobox/buildListboxDisplayOptions.test.d.ts +2 -0
  56. package/dist/components/combobox/buildListboxDisplayOptions.test.d.ts.map +1 -0
  57. package/dist/components/combobox/buildListboxDisplayOptions.test.js +22 -0
  58. package/dist/components/combobox/buildListboxDisplayOptions.test.js.map +1 -0
  59. package/dist/components/combobox/comboboxKeyboardTypes.d.ts +41 -0
  60. package/dist/components/combobox/comboboxKeyboardTypes.d.ts.map +1 -0
  61. package/dist/components/combobox/comboboxKeyboardTypes.js +2 -0
  62. package/dist/components/combobox/comboboxKeyboardTypes.js.map +1 -0
  63. package/dist/components/combobox/highlightLabel.d.ts +10 -0
  64. package/dist/components/combobox/highlightLabel.d.ts.map +1 -0
  65. package/dist/components/combobox/highlightLabel.js +18 -0
  66. package/dist/components/combobox/highlightLabel.js.map +1 -0
  67. package/dist/components/combobox/normaliseComboboxQuery.d.ts +2 -0
  68. package/dist/components/combobox/normaliseComboboxQuery.d.ts.map +1 -0
  69. package/dist/components/combobox/normaliseComboboxQuery.js +2 -0
  70. package/dist/components/combobox/normaliseComboboxQuery.js.map +1 -0
  71. package/dist/components/combobox/types.d.ts +46 -0
  72. package/dist/components/combobox/types.d.ts.map +1 -0
  73. package/dist/components/combobox/types.js +2 -0
  74. package/dist/components/combobox/types.js.map +1 -0
  75. package/dist/components/combobox/useChipSelection.d.ts +11 -0
  76. package/dist/components/combobox/useChipSelection.d.ts.map +1 -0
  77. package/dist/components/combobox/useChipSelection.js +35 -0
  78. package/dist/components/combobox/useChipSelection.js.map +1 -0
  79. package/dist/components/combobox/useComboboxChipKeyboard.d.ts +3 -0
  80. package/dist/components/combobox/useComboboxChipKeyboard.d.ts.map +1 -0
  81. package/dist/components/combobox/useComboboxChipKeyboard.js +103 -0
  82. package/dist/components/combobox/useComboboxChipKeyboard.js.map +1 -0
  83. package/dist/components/combobox/useComboboxChipKeyboard.test.d.ts +2 -0
  84. package/dist/components/combobox/useComboboxChipKeyboard.test.d.ts.map +1 -0
  85. package/dist/components/combobox/useComboboxChipKeyboard.test.js +116 -0
  86. package/dist/components/combobox/useComboboxChipKeyboard.test.js.map +1 -0
  87. package/dist/components/combobox/useComboboxKeyboard.d.ts +4 -0
  88. package/dist/components/combobox/useComboboxKeyboard.d.ts.map +1 -0
  89. package/dist/components/combobox/useComboboxKeyboard.js +68 -0
  90. package/dist/components/combobox/useComboboxKeyboard.js.map +1 -0
  91. package/dist/components/combobox/useComboboxListboxDom.d.ts +11 -0
  92. package/dist/components/combobox/useComboboxListboxDom.d.ts.map +1 -0
  93. package/dist/components/combobox/useComboboxListboxDom.js +15 -0
  94. package/dist/components/combobox/useComboboxListboxDom.js.map +1 -0
  95. package/dist/components/combobox/useComboboxListboxKeyboard.d.ts +3 -0
  96. package/dist/components/combobox/useComboboxListboxKeyboard.d.ts.map +1 -0
  97. package/dist/components/combobox/useComboboxListboxKeyboard.js +143 -0
  98. package/dist/components/combobox/useComboboxListboxKeyboard.js.map +1 -0
  99. package/dist/components/combobox/useComboboxListboxKeyboard.test.d.ts +2 -0
  100. package/dist/components/combobox/useComboboxListboxKeyboard.test.d.ts.map +1 -0
  101. package/dist/components/combobox/useComboboxListboxKeyboard.test.js +152 -0
  102. package/dist/components/combobox/useComboboxListboxKeyboard.test.js.map +1 -0
  103. package/dist/components/combobox/useComboboxPopoverBehavior.d.ts +38 -0
  104. package/dist/components/combobox/useComboboxPopoverBehavior.d.ts.map +1 -0
  105. package/dist/components/combobox/useComboboxPopoverBehavior.js +104 -0
  106. package/dist/components/combobox/useComboboxPopoverBehavior.js.map +1 -0
  107. package/dist/components/combobox/useComboboxState.d.ts +27 -0
  108. package/dist/components/combobox/useComboboxState.d.ts.map +1 -0
  109. package/dist/components/combobox/useComboboxState.js +122 -0
  110. package/dist/components/combobox/useComboboxState.js.map +1 -0
  111. package/dist/components/combobox/useElementWidth.d.ts +2 -0
  112. package/dist/components/combobox/useElementWidth.d.ts.map +1 -0
  113. package/dist/components/combobox/useElementWidth.js +31 -0
  114. package/dist/components/combobox/useElementWidth.js.map +1 -0
  115. package/dist/components/combobox/useVisibleChips.d.ts +21 -0
  116. package/dist/components/combobox/useVisibleChips.d.ts.map +1 -0
  117. package/dist/components/combobox/useVisibleChips.js +59 -0
  118. package/dist/components/combobox/useVisibleChips.js.map +1 -0
  119. package/dist/components/combobox/useVisibleChips.test.d.ts +2 -0
  120. package/dist/components/combobox/useVisibleChips.test.d.ts.map +1 -0
  121. package/dist/components/combobox/useVisibleChips.test.js +81 -0
  122. package/dist/components/combobox/useVisibleChips.test.js.map +1 -0
  123. package/dist/components/dot/Dot.d.ts +8 -0
  124. package/dist/components/dot/Dot.d.ts.map +1 -0
  125. package/dist/components/dot/Dot.js +6 -0
  126. package/dist/components/dot/Dot.js.map +1 -0
  127. package/dist/components/dot/Dot.stories.d.ts +15 -0
  128. package/dist/components/dot/Dot.stories.d.ts.map +1 -0
  129. package/dist/components/dot/Dot.stories.js +25 -0
  130. package/dist/components/dot/Dot.stories.js.map +1 -0
  131. package/dist/components/dot/Dot.test.d.ts +2 -0
  132. package/dist/components/dot/Dot.test.d.ts.map +1 -0
  133. package/dist/components/dot/Dot.test.js +19 -0
  134. package/dist/components/dot/Dot.test.js.map +1 -0
  135. package/dist/components/formField/FormField.d.ts +8 -4
  136. package/dist/components/formField/FormField.d.ts.map +1 -1
  137. package/dist/components/formField/FormField.js +7 -6
  138. package/dist/components/formField/FormField.js.map +1 -1
  139. package/dist/components/formField/FormField.stories.d.ts +1 -0
  140. package/dist/components/formField/FormField.stories.d.ts.map +1 -1
  141. package/dist/components/formField/FormField.stories.js +13 -1
  142. package/dist/components/formField/FormField.stories.js.map +1 -1
  143. package/dist/components/formField/FormField.test.js +10 -0
  144. package/dist/components/formField/FormField.test.js.map +1 -1
  145. package/dist/components/icon/allowedIcons.d.ts +1 -0
  146. package/dist/components/icon/allowedIcons.d.ts.map +1 -1
  147. package/dist/components/icon/allowedIcons.js +2 -1
  148. package/dist/components/icon/allowedIcons.js.map +1 -1
  149. package/dist/components/progress/Progress.stories.d.ts +49 -49
  150. package/dist/components/singleUser/SingleUser.d.ts +15 -0
  151. package/dist/components/singleUser/SingleUser.d.ts.map +1 -0
  152. package/dist/components/singleUser/SingleUser.js +9 -0
  153. package/dist/components/singleUser/SingleUser.js.map +1 -0
  154. package/dist/components/singleUser/SingleUser.stories.d.ts +11 -0
  155. package/dist/components/singleUser/SingleUser.stories.d.ts.map +1 -0
  156. package/dist/components/singleUser/SingleUser.stories.js +52 -0
  157. package/dist/components/singleUser/SingleUser.stories.js.map +1 -0
  158. package/dist/components/singleUser/SingleUser.test.d.ts +2 -0
  159. package/dist/components/singleUser/SingleUser.test.d.ts.map +1 -0
  160. package/dist/components/singleUser/SingleUser.test.js +30 -0
  161. package/dist/components/singleUser/SingleUser.test.js.map +1 -0
  162. package/dist/components/tabs/TabsItem.stories.d.ts +2 -2
  163. package/dist/components/tag/Tag.d.ts +9 -6
  164. package/dist/components/tag/Tag.d.ts.map +1 -1
  165. package/dist/components/tag/Tag.js +8 -2
  166. package/dist/components/tag/Tag.js.map +1 -1
  167. package/dist/components/tag/Tag.stories.d.ts +11 -6
  168. package/dist/components/tag/Tag.stories.d.ts.map +1 -1
  169. package/dist/components/tag/Tag.stories.js +68 -4
  170. package/dist/components/tag/Tag.stories.js.map +1 -1
  171. package/dist/components/tag/Tag.test.js +86 -50
  172. package/dist/components/tag/Tag.test.js.map +1 -1
  173. package/dist/components/toggle/Toggle.d.ts +3 -0
  174. package/dist/components/toggle/Toggle.d.ts.map +1 -0
  175. package/dist/components/toggle/Toggle.js +8 -0
  176. package/dist/components/toggle/Toggle.js.map +1 -0
  177. package/dist/components/toggle/Toggle.stories.d.ts +97 -0
  178. package/dist/components/toggle/Toggle.stories.d.ts.map +1 -0
  179. package/dist/components/toggle/Toggle.stories.js +186 -0
  180. package/dist/components/toggle/Toggle.stories.js.map +1 -0
  181. package/dist/components/toggle/Toggle.test.d.ts +2 -0
  182. package/dist/components/toggle/Toggle.test.d.ts.map +1 -0
  183. package/dist/components/toggle/Toggle.test.js +58 -0
  184. package/dist/components/toggle/Toggle.test.js.map +1 -0
  185. package/dist/index.css +656 -25
  186. package/dist/index.css.map +1 -1
  187. package/dist/index.d.ts +34 -25
  188. package/dist/index.d.ts.map +1 -1
  189. package/dist/index.js +30 -25
  190. package/dist/index.js.map +1 -1
  191. package/dist/mocks/comboboxStoryOptions.d.ts +5 -0
  192. package/dist/mocks/comboboxStoryOptions.d.ts.map +1 -0
  193. package/dist/mocks/comboboxStoryOptions.js +22 -0
  194. package/dist/mocks/comboboxStoryOptions.js.map +1 -0
  195. package/dist/utils/isSelectAllChord.d.ts +5 -0
  196. package/dist/utils/isSelectAllChord.d.ts.map +1 -0
  197. package/dist/utils/isSelectAllChord.js +7 -0
  198. package/dist/utils/isSelectAllChord.js.map +1 -0
  199. package/dist/utils/isSelectAllChord.test.d.ts +2 -0
  200. package/dist/utils/isSelectAllChord.test.d.ts.map +1 -0
  201. package/dist/utils/isSelectAllChord.test.js +19 -0
  202. package/dist/utils/isSelectAllChord.test.js.map +1 -0
  203. package/dist/utils/nextCircularIndex.d.ts +3 -0
  204. package/dist/utils/nextCircularIndex.d.ts.map +1 -0
  205. package/dist/utils/nextCircularIndex.js +10 -0
  206. package/dist/utils/nextCircularIndex.js.map +1 -0
  207. package/dist/utils/nextCircularIndex.test.d.ts +2 -0
  208. package/dist/utils/nextCircularIndex.test.d.ts.map +1 -0
  209. package/dist/utils/nextCircularIndex.test.js +23 -0
  210. package/dist/utils/nextCircularIndex.test.js.map +1 -0
  211. package/dist/utils/scrollElementIntoViewById.d.ts +2 -0
  212. package/dist/utils/scrollElementIntoViewById.d.ts.map +1 -0
  213. package/dist/utils/scrollElementIntoViewById.js +16 -0
  214. package/dist/utils/scrollElementIntoViewById.js.map +1 -0
  215. package/dist/utils/scrollElementIntoViewById.test.d.ts +2 -0
  216. package/dist/utils/scrollElementIntoViewById.test.d.ts.map +1 -0
  217. package/dist/utils/scrollElementIntoViewById.test.js +31 -0
  218. package/dist/utils/scrollElementIntoViewById.test.js.map +1 -0
  219. package/package.json +1 -1
  220. package/src/components/avatar/Avatar.stories.tsx +8 -0
  221. package/src/components/avatar/Avatar.tsx +3 -3
  222. package/src/components/badge/Badge.stories.tsx +74 -0
  223. package/src/components/badge/Badge.test.tsx +28 -0
  224. package/src/components/badge/Badge.tsx +35 -0
  225. package/src/components/badge/badge.scss +86 -0
  226. package/src/components/card/Card.tsx +1 -1
  227. package/src/components/combobox/Combobox.stories.tsx +340 -0
  228. package/src/components/combobox/Combobox.test.tsx +1160 -0
  229. package/src/components/combobox/Combobox.tsx +434 -0
  230. package/src/components/combobox/ComboboxButtonTrigger.tsx +195 -0
  231. package/src/components/combobox/ComboboxListbox.tsx +224 -0
  232. package/src/components/combobox/ComboboxOptionRow.tsx +128 -0
  233. package/src/components/combobox/ComboboxTrigger.tsx +134 -0
  234. package/src/components/combobox/buildListboxDisplayOptions.test.ts +24 -0
  235. package/src/components/combobox/buildListboxDisplayOptions.ts +12 -0
  236. package/src/components/combobox/combobox.scss +390 -0
  237. package/src/components/combobox/comboboxKeyboardTypes.ts +45 -0
  238. package/src/components/combobox/highlightLabel.tsx +42 -0
  239. package/src/components/combobox/normaliseComboboxQuery.ts +1 -0
  240. package/src/components/combobox/types.ts +53 -0
  241. package/src/components/combobox/useChipSelection.ts +53 -0
  242. package/src/components/combobox/useComboboxChipKeyboard.test.tsx +141 -0
  243. package/src/components/combobox/useComboboxChipKeyboard.ts +121 -0
  244. package/src/components/combobox/useComboboxKeyboard.ts +108 -0
  245. package/src/components/combobox/useComboboxListboxDom.ts +36 -0
  246. package/src/components/combobox/useComboboxListboxKeyboard.test.tsx +186 -0
  247. package/src/components/combobox/useComboboxListboxKeyboard.ts +172 -0
  248. package/src/components/combobox/useComboboxPopoverBehavior.ts +179 -0
  249. package/src/components/combobox/useComboboxState.ts +232 -0
  250. package/src/components/combobox/useElementWidth.ts +40 -0
  251. package/src/components/combobox/useVisibleChips.test.tsx +91 -0
  252. package/src/components/combobox/useVisibleChips.ts +100 -0
  253. package/src/components/dot/Dot.stories.tsx +41 -0
  254. package/src/components/dot/Dot.test.tsx +21 -0
  255. package/src/components/dot/Dot.tsx +18 -0
  256. package/src/components/dot/dot.scss +35 -0
  257. package/src/components/formField/FormField.stories.tsx +30 -1
  258. package/src/components/formField/FormField.test.tsx +20 -0
  259. package/src/components/formField/FormField.tsx +11 -5
  260. package/src/components/formField/inputs/number/numberInput.scss +12 -4
  261. package/src/components/icon/allowedIcons.tsx +2 -0
  262. package/src/components/pill/pill.scss +4 -6
  263. package/src/components/singleUser/SingleUser.stories.tsx +63 -0
  264. package/src/components/singleUser/SingleUser.test.tsx +61 -0
  265. package/src/components/singleUser/SingleUser.tsx +45 -0
  266. package/src/components/singleUser/singleUser.scss +14 -0
  267. package/src/components/tag/Tag.stories.tsx +88 -6
  268. package/src/components/tag/Tag.test.tsx +110 -44
  269. package/src/components/tag/Tag.tsx +38 -14
  270. package/src/components/tag/tag.scss +45 -30
  271. package/src/components/toggle/Toggle.stories.tsx +239 -0
  272. package/src/components/toggle/Toggle.test.tsx +66 -0
  273. package/src/components/toggle/Toggle.tsx +12 -0
  274. package/src/components/toggle/toggle.scss +126 -0
  275. package/src/index.scss +5 -0
  276. package/src/index.ts +47 -31
  277. package/src/mocks/comboboxStoryOptions.ts +25 -0
  278. package/src/tokens.scss +33 -4
  279. package/src/utils/isSelectAllChord.test.ts +24 -0
  280. package/src/utils/isSelectAllChord.ts +8 -0
  281. package/src/utils/nextCircularIndex.test.ts +26 -0
  282. package/src/utils/nextCircularIndex.ts +15 -0
  283. package/src/utils/scrollElementIntoViewById.test.ts +38 -0
  284. package/src/utils/scrollElementIntoViewById.ts +20 -0
  285. 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,63 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-vite';
2
+ import { SingleUser } from './SingleUser';
3
+
4
+ const meta: Meta<typeof SingleUser> = {
5
+ title: 'Components/SingleUser',
6
+ component: SingleUser,
7
+ tags: ['autodocs'],
8
+ };
9
+
10
+ export default meta;
11
+
12
+ type Story = StoryObj<typeof SingleUser>;
13
+
14
+ const withDescription = (story: Story, description: string): Story => ({
15
+ ...story,
16
+ parameters: {
17
+ ...story.parameters,
18
+ docs: {
19
+ ...story.parameters?.docs,
20
+ description: {
21
+ story: description,
22
+ },
23
+ },
24
+ },
25
+ });
26
+
27
+ const sampleSrc = 'https://i.pravatar.cc/150?img=12';
28
+
29
+ export const WithImage: Story = withDescription({
30
+ args: {
31
+ label: 'Jordan Matthews',
32
+ src: sampleSrc,
33
+ },
34
+ }, 'Displays `SingleUser` with an avatar image and the default small layout.');
35
+
36
+ export const WithInitials: Story = withDescription({
37
+ args: {
38
+ label: 'Riley Chen',
39
+ initials: 'RC',
40
+ },
41
+ }, 'Uses initials when no image source is provided.');
42
+
43
+ export const Placeholder: Story = withDescription({
44
+ args: {
45
+ label: 'Invited user',
46
+ },
47
+ }, 'Shows the placeholder avatar treatment when neither image nor initials are supplied.');
48
+
49
+ export const MediumAvatar: Story = withDescription({
50
+ name: 'Size override (medium)',
51
+ args: {
52
+ label: 'Taylor Brooks',
53
+ initials: 'TB',
54
+ size: 'medium',
55
+ },
56
+ }, 'Demonstrates overriding the default avatar size within the SingleUser pill.');
57
+
58
+ export const LongLabel: Story = withDescription({
59
+ args: {
60
+ label: 'Dr. Alexandrina Constantinopolous-Worthington',
61
+ initials: 'AC',
62
+ },
63
+ }, 'Exercises the component with a longer label so text wrapping and spacing can be reviewed.');
@@ -0,0 +1,61 @@
1
+ import '@testing-library/jest-dom/vitest';
2
+ import { render, screen } from '@testing-library/react';
3
+ import { describe, expect, test } from 'vitest';
4
+ import { SingleUser } from './SingleUser';
5
+
6
+ describe('SingleUser', () => {
7
+ test('renders label text', () => {
8
+ render(
9
+ <SingleUser
10
+ label="Jamie Smith"
11
+ initials="JS"
12
+ />,
13
+ );
14
+ expect(screen.getByText('Jamie Smith')).toBeInTheDocument();
15
+ });
16
+
17
+ test('hides Avatar from screen readers', () => {
18
+ const { container } = render(
19
+ <SingleUser
20
+ label="Pat Lee"
21
+ initials="PL"
22
+ />,
23
+ );
24
+ const avatar = container.querySelector('.ds-avatar');
25
+ expect(avatar).toHaveAttribute('aria-hidden', 'true');
26
+ });
27
+
28
+ test('defaults Avatar to small size', () => {
29
+ const { container } = render(
30
+ <SingleUser
31
+ label="Sam"
32
+ initials="S"
33
+ />,
34
+ );
35
+ expect(container.querySelector('.ds-avatar--small')).toBeInTheDocument();
36
+ });
37
+
38
+ test('allows size override', () => {
39
+ const { container } = render(
40
+ <SingleUser
41
+ label="Sam"
42
+ initials="S"
43
+ size="medium"
44
+ />,
45
+ );
46
+ expect(container.querySelector('.ds-avatar--medium')).toBeInTheDocument();
47
+ });
48
+
49
+ test('applies className to root and avatarClassName to Avatar', () => {
50
+ const { container } = render(
51
+ <SingleUser
52
+ label="User"
53
+ initials="U"
54
+ className="my-chip"
55
+ avatarClassName="my-avatar"
56
+ />,
57
+ );
58
+ expect(container.querySelector('.ds-single-user.my-chip')).toBeInTheDocument();
59
+ expect(container.querySelector('.ds-avatar.my-avatar')).toBeInTheDocument();
60
+ });
61
+ });
@@ -0,0 +1,45 @@
1
+ import classNames from 'classnames';
2
+ import { Avatar, type AvatarProps, type AvatarSize } from 'Components/avatar/Avatar';
3
+ import React from 'react';
4
+
5
+ export type SingleUserProps = {
6
+ label: string;
7
+ className?: string;
8
+ avatarClassName?: string;
9
+ size?: AvatarSize;
10
+ } & Omit<AvatarProps, 'className' | 'alt' | 'size'>;
11
+
12
+ export const SingleUser = React.forwardRef<HTMLSpanElement, SingleUserProps>(
13
+ (
14
+ {
15
+ label,
16
+ className,
17
+ avatarClassName,
18
+ size = 'small',
19
+ ...avatarProps
20
+ },
21
+ ref,
22
+ ) => {
23
+ return (
24
+ <span
25
+ ref={ref}
26
+ className={classNames('ds-single-user', className)}
27
+ >
28
+
29
+ <Avatar
30
+ {...avatarProps}
31
+ size={size}
32
+ aria-hidden="true"
33
+ alt=""
34
+ className={avatarClassName}
35
+ />
36
+
37
+ <span className="ds-single-user__label">
38
+ {label}
39
+ </span>
40
+ </span>
41
+ );
42
+ },
43
+ );
44
+
45
+ SingleUser.displayName = 'SingleUser';
@@ -0,0 +1,14 @@
1
+ .ds-single-user {
2
+ box-sizing: border-box;
3
+ display: inline-flex;
4
+ align-items: center;
5
+ gap: var(--single-user-spacing-gap);
6
+ padding-block: 0;
7
+ padding-inline: 0 var(--single-user-spacing-padding-inline-end);
8
+ border-radius: var(--single-user-radius);
9
+ border: var(--single-user-border-width) solid var(--single-user-color-border);
10
+ background-color: var(--single-user-color-background);
11
+ color: var(--single-user-color-text);
12
+ font-size: var(--single-user-font-size);
13
+ line-height: var(--line-height-tight);
14
+ }