@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,434 @@
1
+ import classNames from 'classnames';
2
+ import { Popover } from 'radix-ui';
3
+ import { useCallback, useContext, useId, useMemo, useRef } from 'react';
4
+ import { PopupParentContext } from 'Utils/PopupParentContext';
5
+ import { buildOptionGroups } from './buildListboxDisplayOptions';
6
+ import { ComboboxButtonTrigger } from './ComboboxButtonTrigger';
7
+ import { ComboboxListbox } from './ComboboxListbox';
8
+ import { ComboboxTrigger } from './ComboboxTrigger';
9
+ import type { ComboboxProps } from './types';
10
+ import { useChipSelection } from './useChipSelection';
11
+ import { useComboboxKeyboard } from './useComboboxKeyboard';
12
+ import { useComboboxListboxDom } from './useComboboxListboxDom';
13
+ import { useComboboxPopoverBehavior } from './useComboboxPopoverBehavior';
14
+ import { useComboboxState } from './useComboboxState';
15
+
16
+ const ComboboxRoot = (props: ComboboxProps): React.JSX.Element => {
17
+ const {
18
+ options,
19
+ multiple = false,
20
+ value: controlledValue,
21
+ defaultValue,
22
+ onValueChange,
23
+ onSearch,
24
+ searchType = 'prefix',
25
+ highlightStringMatches = false,
26
+ allowCreate = false,
27
+ onCreateNew,
28
+ onDeleteCreated,
29
+ placeholder = 'Select...',
30
+ disabled = false,
31
+ dropdownOnFocus = true,
32
+ triggerVariant = 'input',
33
+ showDropdownTrigger = true,
34
+ showSelectionCountBadge = triggerVariant === 'button' && multiple,
35
+ selectionCountA11yLabel,
36
+ loading = false,
37
+ hasError = false,
38
+ showClearAll = false,
39
+ clearAllLabel = 'Clear all',
40
+ renderOption,
41
+ getTagLabel,
42
+ id: externalId,
43
+ 'aria-describedby': ariaDescribedBy,
44
+ 'aria-invalid': ariaInvalid,
45
+ 'aria-label': ariaLabel,
46
+ } = props;
47
+
48
+ const generatedId = useId();
49
+ const comboboxId = externalId ?? generatedId;
50
+ const listboxId = `${comboboxId}-listbox`;
51
+
52
+ const state = useComboboxState({
53
+ options,
54
+ multiple,
55
+ value: controlledValue,
56
+ defaultValue,
57
+ onValueChange,
58
+ onSearch,
59
+ searchType,
60
+ allowCreate,
61
+ onCreateNew,
62
+ onDeleteCreated,
63
+ disabled,
64
+ });
65
+
66
+ const {
67
+ selectedValues,
68
+ selectedValuesSet,
69
+ query,
70
+ setQuery,
71
+ isOpen,
72
+ setIsOpen,
73
+ highlightIndex,
74
+ setHighlightIndex,
75
+ mergedOptions,
76
+ filteredOptions,
77
+ createdValuesSet,
78
+ showCreateRow,
79
+ totalItems,
80
+ selectOption,
81
+ removeValue,
82
+ clearAll,
83
+ handleCreate,
84
+ deleteCreatedOption,
85
+ openPopover,
86
+ isAsync,
87
+ } = state;
88
+
89
+ const inputRef = useRef<HTMLInputElement>(null);
90
+ const triggerRef = useRef<HTMLDivElement>(null);
91
+ const deleteButtonRefs = useRef(new Map<string, HTMLButtonElement>());
92
+ const popupParentRef = useContext(PopupParentContext);
93
+
94
+ const { listboxRef, getOptionId, scrollHighlightedIntoView } = useComboboxListboxDom({
95
+ comboboxId,
96
+ });
97
+
98
+ const {
99
+ selectedChipValues,
100
+ selectedChipValuesSet,
101
+ setSelectedChipValues,
102
+ clearChipSelection,
103
+ focusedChipIndex,
104
+ setFocusedChipIndex,
105
+ exitChipNav,
106
+ } = useChipSelection(selectedValues);
107
+
108
+ const {
109
+ shouldRenderPopoverContent,
110
+ shouldShowPopover,
111
+ showListboxLoading,
112
+ handleInputChange,
113
+ handleTriggerClick,
114
+ handleInputFocus,
115
+ handleInputBlur,
116
+ handleChevronClick,
117
+ handlePopoverInteractOutside,
118
+ } = useComboboxPopoverBehavior({
119
+ inputRef,
120
+ triggerRef,
121
+ listboxRef,
122
+ disabled,
123
+ dropdownOnFocus,
124
+ loading,
125
+ onSearch,
126
+ isAsync,
127
+ isOpen,
128
+ setIsOpen,
129
+ setQuery,
130
+ setHighlightIndex,
131
+ openPopover,
132
+ totalItems,
133
+ showCreateRow,
134
+ filteredOptionsLength: filteredOptions.length,
135
+ highlightIndex,
136
+ clearChipSelection,
137
+ exitChipNav,
138
+ triggerContainsInput: triggerVariant === 'input',
139
+ });
140
+
141
+ const optionGroups = useMemo(() => buildOptionGroups(filteredOptions), [filteredOptions]);
142
+
143
+ const optionIndexMap = useMemo(() => {
144
+ const map = new Map<string, number>();
145
+ filteredOptions.forEach((opt, i) => map.set(opt.value, i));
146
+ return map;
147
+ }, [filteredOptions]);
148
+
149
+ const resolveTagLabel = useCallback(
150
+ (opt: { tagLabel?: string; label: string }): string => {
151
+ if (getTagLabel) return getTagLabel(opt as Parameters<typeof getTagLabel>[0]);
152
+ return opt.tagLabel ?? opt.label;
153
+ },
154
+ [getTagLabel],
155
+ );
156
+
157
+ const registerDeleteButton = useCallback((value: string, node: HTMLButtonElement | null) => {
158
+ if (node) {
159
+ deleteButtonRefs.current.set(value, node);
160
+ }
161
+ else {
162
+ deleteButtonRefs.current.delete(value);
163
+ }
164
+ }, []);
165
+
166
+ const focusDeleteButtonForHighlighted = useCallback((): boolean => {
167
+ if (highlightIndex < 0 || highlightIndex >= filteredOptions.length) return false;
168
+
169
+ const highlightedOption = filteredOptions[highlightIndex];
170
+ if (!highlightedOption || !createdValuesSet.has(highlightedOption.value)) return false;
171
+
172
+ const button = deleteButtonRefs.current.get(highlightedOption.value);
173
+ if (!button) return false;
174
+
175
+ button.focus();
176
+ return true;
177
+ }, [createdValuesSet, filteredOptions, highlightIndex]);
178
+
179
+ const focusInputAtIndex = useCallback((index: number) => {
180
+ setHighlightIndex(index);
181
+ inputRef.current?.focus();
182
+ }, [setHighlightIndex]);
183
+
184
+ const handleDeleteCreatedFromList = useCallback((value: string, index: number) => {
185
+ const nextIndex = filteredOptions.length > 1
186
+ ? Math.min(index, filteredOptions.length - 2)
187
+ : -1;
188
+
189
+ deleteCreatedOption(value);
190
+ focusInputAtIndex(nextIndex);
191
+ }, [deleteCreatedOption, filteredOptions.length, focusInputAtIndex]);
192
+
193
+ const handleDeleteButtonKeyDown = useCallback((e: React.KeyboardEvent<HTMLButtonElement>, index: number) => {
194
+ if (e.key === 'ArrowLeft') {
195
+ e.preventDefault();
196
+ focusInputAtIndex(index);
197
+ }
198
+ else if (e.key === 'Tab' && e.shiftKey) {
199
+ e.preventDefault();
200
+ focusInputAtIndex(index);
201
+ }
202
+ }, [focusInputAtIndex]);
203
+
204
+ const handleKeyDown = useComboboxKeyboard({
205
+ inputRef,
206
+ allowCreate,
207
+ clearAll,
208
+ clearChipSelection,
209
+ exitChipNav,
210
+ focusDeleteButtonForHighlighted,
211
+ listboxOptions: filteredOptions,
212
+ focusedChipIndex,
213
+ handleCreate,
214
+ highlightIndex,
215
+ isOpen,
216
+ mergedOptions,
217
+ openPopover,
218
+ query,
219
+ removeValue,
220
+ scrollHighlightedIntoView,
221
+ selectOption,
222
+ selectedChipValues,
223
+ selectedValues,
224
+ setFocusedChipIndex,
225
+ setHighlightIndex,
226
+ setIsOpen,
227
+ setQuery,
228
+ setSelectedChipValues,
229
+ showCreateRow,
230
+ showListboxLoading,
231
+ totalItems,
232
+ });
233
+
234
+ const activeDescendant
235
+ = shouldShowPopover && !showListboxLoading && highlightIndex >= 0
236
+ ? getOptionId(highlightIndex)
237
+ : undefined;
238
+
239
+ const selectedChips = useMemo(
240
+ () =>
241
+ selectedValues
242
+ .map(v => mergedOptions.find(o => o.value === v))
243
+ .filter(Boolean) as typeof mergedOptions,
244
+ [selectedValues, mergedOptions],
245
+ );
246
+
247
+ const preventMouseDefault = useCallback((e: React.MouseEvent) => {
248
+ e.preventDefault();
249
+ }, []);
250
+ const renderSearchInputInListbox = triggerVariant === 'button';
251
+
252
+ const handlePopoverOpenAutoFocus = useCallback(
253
+ (e: Event) => {
254
+ e.preventDefault();
255
+ if (triggerVariant === 'button') {
256
+ requestAnimationFrame(() => {
257
+ inputRef.current?.focus();
258
+ });
259
+ }
260
+ },
261
+ [triggerVariant],
262
+ );
263
+
264
+ const resolvedSelectionCountA11yLabel = useMemo(() => {
265
+ if (!showSelectionCountBadge || selectedValues.length === 0) {
266
+ return undefined;
267
+ }
268
+ if (typeof selectionCountA11yLabel === 'function') {
269
+ return selectionCountA11yLabel(selectedValues.length);
270
+ }
271
+ if (selectionCountA11yLabel) {
272
+ return selectionCountA11yLabel;
273
+ }
274
+ return `${selectedValues.length} selected item${selectedValues.length === 1 ? '' : 's'}`;
275
+ }, [selectedValues.length, selectionCountA11yLabel, showSelectionCountBadge]);
276
+
277
+ const handleButtonTriggerKeyDown = useCallback(
278
+ (e: React.KeyboardEvent<HTMLDivElement>) => {
279
+ if (disabled) return;
280
+ if (e.key === 'Enter' || e.key === ' ' || e.key === 'ArrowDown') {
281
+ e.preventDefault();
282
+ handleTriggerClick();
283
+ }
284
+ },
285
+ [disabled, handleTriggerClick],
286
+ );
287
+
288
+ return (
289
+ <div
290
+ className={classNames('ds-combobox', {
291
+ 'ds-combobox--disabled': disabled,
292
+ 'ds-combobox--error': hasError,
293
+ })}
294
+ >
295
+ <Popover.Root open={isOpen} onOpenChange={setIsOpen}>
296
+ {triggerVariant === 'input'
297
+ ? (
298
+ <ComboboxTrigger
299
+ inputRef={inputRef}
300
+ triggerRef={triggerRef}
301
+ comboboxId={comboboxId}
302
+ listboxId={listboxId}
303
+ isOpen={isOpen}
304
+ hasError={hasError}
305
+ disabled={disabled}
306
+ placeholder={placeholder}
307
+ query={query}
308
+ shouldShowPopover={shouldShowPopover}
309
+ showListboxLoading={showListboxLoading}
310
+ activeDescendant={activeDescendant}
311
+ aria-describedby={ariaDescribedBy}
312
+ aria-invalid={ariaInvalid}
313
+ aria-label={ariaLabel}
314
+ showDropdownTrigger={showDropdownTrigger}
315
+ selectedChips={selectedChips}
316
+ selectedChipValuesSet={selectedChipValuesSet}
317
+ focusedChipIndex={focusedChipIndex}
318
+ resolveTagLabel={resolveTagLabel}
319
+ removeValue={removeValue}
320
+ handleTriggerClick={handleTriggerClick}
321
+ handleInputChange={handleInputChange}
322
+ handleInputFocus={handleInputFocus}
323
+ handleInputBlur={handleInputBlur}
324
+ handleKeyDown={handleKeyDown}
325
+ handleChevronClick={handleChevronClick}
326
+ />
327
+ )
328
+ : (
329
+ <ComboboxButtonTrigger
330
+ triggerRef={triggerRef}
331
+ listboxId={listboxId}
332
+ isOpen={isOpen}
333
+ hasError={hasError}
334
+ disabled={disabled}
335
+ placeholder={placeholder}
336
+ ariaLabel={ariaLabel}
337
+ ariaDescribedBy={ariaDescribedBy}
338
+ ariaInvalid={ariaInvalid}
339
+ showDropdownTrigger={showDropdownTrigger}
340
+ selectedChips={selectedChips}
341
+ selectedChipValuesSet={selectedChipValuesSet}
342
+ focusedChipIndex={focusedChipIndex}
343
+ resolveTagLabel={resolveTagLabel}
344
+ removeValue={removeValue}
345
+ handleTriggerClick={handleTriggerClick}
346
+ handleTriggerKeyDown={handleButtonTriggerKeyDown}
347
+ handleChevronClick={handleChevronClick}
348
+ showSelectionCountBadge={showSelectionCountBadge}
349
+ selectionCountA11yLabel={resolvedSelectionCountA11yLabel}
350
+ />
351
+ )}
352
+
353
+ <Popover.Portal container={popupParentRef.current}>
354
+ {shouldRenderPopoverContent && (
355
+ <Popover.Content
356
+ align="start"
357
+ sideOffset={4}
358
+ onOpenAutoFocus={handlePopoverOpenAutoFocus}
359
+ onCloseAutoFocus={e => e.preventDefault()}
360
+ onInteractOutside={handlePopoverInteractOutside}
361
+ className={classNames('ds-dropdown__content', 'ds-combobox__popover')}
362
+ >
363
+ <ComboboxListbox
364
+ listboxRef={listboxRef}
365
+ listboxId={listboxId}
366
+ multiple={multiple}
367
+ ariaLabel={ariaLabel}
368
+ showListboxLoading={showListboxLoading}
369
+ optionGroups={optionGroups}
370
+ optionIndexMap={optionIndexMap}
371
+ getOptionId={getOptionId}
372
+ highlightIndex={highlightIndex}
373
+ setHighlightIndex={setHighlightIndex}
374
+ query={query}
375
+ searchType={searchType}
376
+ highlightStringMatches={highlightStringMatches}
377
+ renderOption={renderOption}
378
+ selectedValuesSet={selectedValuesSet}
379
+ createdValuesSet={createdValuesSet}
380
+ selectOption={selectOption}
381
+ deleteCreatedOption={handleDeleteCreatedFromList}
382
+ handleCreate={handleCreate}
383
+ showCreateRow={showCreateRow}
384
+ filteredOptionsLength={filteredOptions.length}
385
+ preventMouseDefault={preventMouseDefault}
386
+ renderSearchInputInListbox={renderSearchInputInListbox}
387
+ inputRef={inputRef}
388
+ comboboxId={comboboxId}
389
+ shouldShowPopover={shouldShowPopover}
390
+ activeDescendant={activeDescendant}
391
+ ariaDescribedBy={ariaDescribedBy}
392
+ ariaInvalid={ariaInvalid}
393
+ placeholder={placeholder}
394
+ handleInputChange={handleInputChange}
395
+ handleInputFocus={handleInputFocus}
396
+ handleInputBlur={handleInputBlur}
397
+ handleKeyDown={handleKeyDown}
398
+ registerDeleteButton={registerDeleteButton}
399
+ handleDeleteButtonKeyDown={handleDeleteButtonKeyDown}
400
+ />
401
+ </Popover.Content>
402
+ )}
403
+ </Popover.Portal>
404
+ </Popover.Root>
405
+
406
+ {showClearAll && selectedValues.length > 0 && (
407
+ <div className="ds-combobox__clear-all-wrapper">
408
+ <button
409
+ type="button"
410
+ className="ds-combobox__clear-all"
411
+ onClick={clearAll}
412
+ disabled={disabled}
413
+ >
414
+ {clearAllLabel}
415
+ </button>
416
+ </div>
417
+ )}
418
+ </div>
419
+ );
420
+ };
421
+
422
+ ComboboxRoot.displayName = 'Combobox';
423
+
424
+ type ComboboxCompound = typeof ComboboxRoot & {
425
+ Trigger: typeof ComboboxTrigger;
426
+ ButtonTrigger: typeof ComboboxButtonTrigger;
427
+ Listbox: typeof ComboboxListbox;
428
+ };
429
+
430
+ export const Combobox = ComboboxRoot as ComboboxCompound;
431
+
432
+ Combobox.Trigger = ComboboxTrigger;
433
+ Combobox.ButtonTrigger = ComboboxButtonTrigger;
434
+ Combobox.Listbox = ComboboxListbox;
@@ -0,0 +1,195 @@
1
+ import classNames from 'classnames';
2
+ import { Badge } from 'Components/badge/Badge';
3
+ import { Icon } from 'Components/icon/Icon';
4
+ import { Tag } from 'Components/tag/Tag';
5
+ import { Popover } from 'radix-ui';
6
+ import { useMemo, useRef } from 'react';
7
+ import type { ComboboxAriaInvalid, ComboboxOption } from './types';
8
+ import { useElementWidth } from './useElementWidth';
9
+ import { useVisibleChips } from './useVisibleChips';
10
+
11
+ export type ComboboxButtonTriggerProps = {
12
+ triggerRef: React.RefObject<HTMLDivElement | null>;
13
+ listboxId: string;
14
+ isOpen: boolean;
15
+ hasError: boolean;
16
+ disabled: boolean;
17
+ placeholder: string;
18
+ ariaLabel?: string;
19
+ ariaDescribedBy?: string;
20
+ ariaInvalid?: ComboboxAriaInvalid;
21
+ showDropdownTrigger: boolean;
22
+ selectedChips: ComboboxOption[];
23
+ selectedChipValuesSet: Set<string>;
24
+ focusedChipIndex: number | null;
25
+ resolveTagLabel: (opt: ComboboxOption) => string;
26
+ removeValue: (value: string) => void;
27
+ handleTriggerClick: () => void;
28
+ handleTriggerKeyDown: (e: React.KeyboardEvent<HTMLDivElement>) => void;
29
+ handleChevronClick: (e: React.MouseEvent) => void;
30
+ showSelectionCountBadge: boolean;
31
+ selectionCountA11yLabel?: string;
32
+ };
33
+
34
+ export const ComboboxButtonTrigger = ({
35
+ triggerRef,
36
+ listboxId,
37
+ isOpen,
38
+ hasError,
39
+ disabled,
40
+ placeholder,
41
+ ariaLabel,
42
+ ariaDescribedBy,
43
+ ariaInvalid,
44
+ showDropdownTrigger,
45
+ selectedChips,
46
+ selectedChipValuesSet,
47
+ focusedChipIndex,
48
+ resolveTagLabel,
49
+ removeValue,
50
+ handleTriggerClick,
51
+ handleTriggerKeyDown,
52
+ handleChevronClick,
53
+ showSelectionCountBadge,
54
+ selectionCountA11yLabel,
55
+ }: ComboboxButtonTriggerProps): React.JSX.Element => {
56
+ const contentRef = useRef<HTMLDivElement>(null);
57
+ const measureTrackRef = useRef<HTMLDivElement>(null);
58
+ const ellipsisProbeRef = useRef<HTMLSpanElement>(null);
59
+ const badgeProbeRef = useRef<HTMLSpanElement>(null);
60
+
61
+ const chipWatchKey = useMemo(
62
+ () =>
63
+ selectedChips
64
+ .map(opt => `${opt.value}:${resolveTagLabel(opt)}:${opt.iconName ?? ''}`)
65
+ .join('|'),
66
+ [resolveTagLabel, selectedChips],
67
+ );
68
+
69
+ const shouldShowBadge = showSelectionCountBadge && selectedChips.length > 0;
70
+
71
+ const contentWidth = useElementWidth(contentRef, `${chipWatchKey}-${isOpen}-${shouldShowBadge}`);
72
+ const measurementTrackWidth = useElementWidth(measureTrackRef, `${chipWatchKey}-${isOpen}`);
73
+ const ellipsisWidth = useElementWidth(ellipsisProbeRef, `${chipWatchKey}-${isOpen}`);
74
+ const badgeWidth = useElementWidth(badgeProbeRef, `${chipWatchKey}-${isOpen}-${shouldShowBadge}`);
75
+ const measurementTrackElement = measureTrackRef.current;
76
+
77
+ const chipGap = useMemo(() => {
78
+ const el = measureTrackRef.current;
79
+ if (!el) return 0;
80
+ const styles = getComputedStyle(el);
81
+ const parsed = Number.parseFloat(styles.columnGap || styles.gap || '0');
82
+ return Number.isFinite(parsed) ? parsed : 0;
83
+ }, [measurementTrackElement, measurementTrackWidth]);
84
+
85
+ const chipWidths = useMemo(() => {
86
+ const el = measureTrackRef.current;
87
+ if (!el) return [];
88
+ return Array.from(el.children).map((child) => {
89
+ const width = (child as HTMLElement).getBoundingClientRect().width;
90
+ return Number.isFinite(width) ? width : 0;
91
+ });
92
+ }, [chipWatchKey, measurementTrackElement, measurementTrackWidth]);
93
+
94
+ const layout = useVisibleChips({
95
+ containerWidth: contentWidth,
96
+ chipWidths,
97
+ chipGap,
98
+ badgeWidth,
99
+ ellipsisWidth,
100
+ showBadge: shouldShowBadge,
101
+ });
102
+
103
+ const canMeasure = contentWidth > 0 && chipWidths.length === selectedChips.length;
104
+ const visibleChips = canMeasure
105
+ ? layout.visibleChipIndices.map(index => selectedChips[index]!).filter(Boolean)
106
+ : selectedChips;
107
+ const showEllipsis = canMeasure ? layout.showEllipsis : false;
108
+ const showBadge = canMeasure ? layout.showBadge : shouldShowBadge;
109
+
110
+ const renderSelectionTag = (opt: ComboboxOption, chipIdx: number, onRemove?: () => void) => (
111
+ <Tag
112
+ key={opt.value}
113
+ color="neutral"
114
+ selected={selectedChipValuesSet.has(opt.value) || focusedChipIndex === chipIdx}
115
+ slotStart={opt.iconName ? <Icon name={opt.iconName} size={12} /> : undefined}
116
+ onRemove={onRemove}
117
+ removeLabel={`Remove ${resolveTagLabel(opt)}`}
118
+ removeButtonTabIndex={-1}
119
+ >
120
+ {resolveTagLabel(opt)}
121
+ </Tag>
122
+ );
123
+
124
+ const renderSelectionCountBadge = (withA11yLabel: boolean) => (
125
+ <Badge colour="salmon" a11yLabel={withA11yLabel ? selectionCountA11yLabel : undefined}>
126
+ {selectedChips.length}
127
+ </Badge>
128
+ );
129
+
130
+ return (
131
+ <Popover.Anchor asChild>
132
+ <div
133
+ ref={triggerRef}
134
+ role="button"
135
+ tabIndex={disabled ? -1 : 0}
136
+ aria-haspopup="listbox"
137
+ aria-expanded={isOpen}
138
+ aria-controls={listboxId}
139
+ aria-label={ariaLabel}
140
+ aria-describedby={ariaDescribedBy}
141
+ aria-invalid={ariaInvalid}
142
+ className={classNames('ds-combobox__trigger', 'ds-combobox__trigger--button', {
143
+ 'ds-combobox__trigger--open': isOpen,
144
+ 'ds-combobox__trigger--error': hasError,
145
+ 'ds-combobox__trigger--disabled': disabled,
146
+ })}
147
+ onClick={handleTriggerClick}
148
+ onKeyDown={handleTriggerKeyDown}
149
+ >
150
+ <div className="ds-combobox__button-content" ref={contentRef}>
151
+ <div className="ds-combobox__button-tags-viewport">
152
+ <div className="ds-combobox__button-tags-track">
153
+ {selectedChips.length === 0 && (
154
+ <span className="ds-combobox__button-placeholder">{placeholder}</span>
155
+ )}
156
+ {visibleChips.map((opt, chipIdx) =>
157
+ renderSelectionTag(opt, chipIdx, disabled ? undefined : () => removeValue(opt.value)))}
158
+ </div>
159
+ {showEllipsis && <span className="ds-combobox__button-ellipsis" aria-hidden="true">…</span>}
160
+ </div>
161
+ {showBadge && renderSelectionCountBadge(true)}
162
+ </div>
163
+ {showDropdownTrigger && (
164
+ <button
165
+ type="button"
166
+ className="ds-combobox__chevron"
167
+ tabIndex={-1}
168
+ aria-label="Open suggestions"
169
+ onClick={handleChevronClick}
170
+ disabled={disabled}
171
+ >
172
+ <Icon name={isOpen ? 'chevron-up' : 'chevron-down'} size={16} />
173
+ </button>
174
+ )}
175
+
176
+ <div className="ds-combobox__measure" aria-hidden="true">
177
+ {/* Mirror the rendered chips off-screen so width calculations use the real Tag layout. */}
178
+ <div className="ds-combobox__button-tags-track" ref={measureTrackRef}>
179
+ {selectedChips.map((opt, chipIdx) => (
180
+ <span key={`measure-${opt.value}`} className="ds-combobox__measure-chip">
181
+ {renderSelectionTag(opt, chipIdx, disabled ? undefined : () => {})}
182
+ </span>
183
+ ))}
184
+ </div>
185
+ <span ref={ellipsisProbeRef} className="ds-combobox__button-ellipsis">…</span>
186
+ <span ref={badgeProbeRef}>
187
+ {shouldShowBadge ? renderSelectionCountBadge(false) : null}
188
+ </span>
189
+ </div>
190
+ </div>
191
+ </Popover.Anchor>
192
+ );
193
+ };
194
+
195
+ ComboboxButtonTrigger.displayName = 'Combobox.ButtonTrigger';