@arbor-education/design-system.components 0.15.0 → 0.16.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 (297) hide show
  1. package/.gather/skills/write-stories/SKILL.md +207 -271
  2. package/.storybook/preview.ts +5 -0
  3. package/CHANGELOG.md +17 -0
  4. package/README.md +8 -0
  5. package/component-library.md +144 -13
  6. package/dist/components/articleCard/ArticleCard.stories.d.ts +137 -11
  7. package/dist/components/articleCard/ArticleCard.stories.d.ts.map +1 -1
  8. package/dist/components/articleCard/ArticleCard.stories.js +358 -91
  9. package/dist/components/articleCard/ArticleCard.stories.js.map +1 -1
  10. package/dist/components/avatar/Avatar.stories.d.ts +6 -6
  11. package/dist/components/avatar/Avatar.stories.d.ts.map +1 -1
  12. package/dist/components/avatar/Avatar.stories.js +393 -49
  13. package/dist/components/avatar/Avatar.stories.js.map +1 -1
  14. package/dist/components/avatarGroup/AvatarGroup.stories.d.ts +9 -7
  15. package/dist/components/avatarGroup/AvatarGroup.stories.d.ts.map +1 -1
  16. package/dist/components/avatarGroup/AvatarGroup.stories.js +688 -65
  17. package/dist/components/avatarGroup/AvatarGroup.stories.js.map +1 -1
  18. package/dist/components/banner/Banner.stories.d.ts.map +1 -1
  19. package/dist/components/banner/Banner.stories.js +7 -3
  20. package/dist/components/banner/Banner.stories.js.map +1 -1
  21. package/dist/components/card/Card.stories.d.ts +105 -4
  22. package/dist/components/card/Card.stories.d.ts.map +1 -1
  23. package/dist/components/card/Card.stories.js +336 -18
  24. package/dist/components/card/Card.stories.js.map +1 -1
  25. package/dist/components/combobox/Combobox.stories.d.ts +134 -21
  26. package/dist/components/combobox/Combobox.stories.d.ts.map +1 -1
  27. package/dist/components/combobox/Combobox.stories.js +676 -175
  28. package/dist/components/combobox/Combobox.stories.js.map +1 -1
  29. package/dist/components/datePicker/DatePicker.stories.d.ts +119 -27
  30. package/dist/components/datePicker/DatePicker.stories.d.ts.map +1 -1
  31. package/dist/components/datePicker/DatePicker.stories.js +575 -47
  32. package/dist/components/datePicker/DatePicker.stories.js.map +1 -1
  33. package/dist/components/dateTimePicker/DateTimePicker.stories.d.ts +155 -39
  34. package/dist/components/dateTimePicker/DateTimePicker.stories.d.ts.map +1 -1
  35. package/dist/components/dateTimePicker/DateTimePicker.stories.js +674 -103
  36. package/dist/components/dateTimePicker/DateTimePicker.stories.js.map +1 -1
  37. package/dist/components/editableText/EditableText.stories.d.ts +53 -12
  38. package/dist/components/editableText/EditableText.stories.d.ts.map +1 -1
  39. package/dist/components/editableText/EditableText.stories.js +401 -64
  40. package/dist/components/editableText/EditableText.stories.js.map +1 -1
  41. package/dist/components/formField/FormField.d.ts +4 -0
  42. package/dist/components/formField/FormField.d.ts.map +1 -1
  43. package/dist/components/formField/FormField.js +2 -1
  44. package/dist/components/formField/FormField.js.map +1 -1
  45. package/dist/components/formField/FormField.test.js +5 -0
  46. package/dist/components/formField/FormField.test.js.map +1 -1
  47. package/dist/components/formField/fieldset/Fieldset.stories.d.ts +56 -4
  48. package/dist/components/formField/fieldset/Fieldset.stories.d.ts.map +1 -1
  49. package/dist/components/formField/fieldset/Fieldset.stories.js +534 -28
  50. package/dist/components/formField/fieldset/Fieldset.stories.js.map +1 -1
  51. package/dist/components/formField/inputs/checkbox/CheckboxGroup.d.ts +3 -1
  52. package/dist/components/formField/inputs/checkbox/CheckboxGroup.d.ts.map +1 -1
  53. package/dist/components/formField/inputs/checkbox/CheckboxInput.js +1 -1
  54. package/dist/components/formField/inputs/checkbox/CheckboxInput.js.map +1 -1
  55. package/dist/components/formField/inputs/colourPickerDropdown/ColourPickerDropdown.stories.d.ts +95 -1
  56. package/dist/components/formField/inputs/colourPickerDropdown/ColourPickerDropdown.stories.d.ts.map +1 -1
  57. package/dist/components/formField/inputs/colourPickerDropdown/ColourPickerDropdown.stories.js +386 -9
  58. package/dist/components/formField/inputs/colourPickerDropdown/ColourPickerDropdown.stories.js.map +1 -1
  59. package/dist/components/formField/inputs/radio/RadioButtonGroup.d.ts +6 -2
  60. package/dist/components/formField/inputs/radio/RadioButtonGroup.d.ts.map +1 -1
  61. package/dist/components/formField/inputs/radio/RadioButtonGroup.js.map +1 -1
  62. package/dist/components/formField/inputs/radio/RadioButtonInput.stories.d.ts.map +1 -1
  63. package/dist/components/formField/inputs/radio/RadioButtonInput.stories.js +61 -49
  64. package/dist/components/formField/inputs/radio/RadioButtonInput.stories.js.map +1 -1
  65. package/dist/components/formField/inputs/selectDropdown/SelectDropdown.stories.d.ts +188 -166
  66. package/dist/components/formField/inputs/selectDropdown/SelectDropdown.stories.d.ts.map +1 -1
  67. package/dist/components/formField/inputs/selectDropdown/SelectDropdown.stories.js +821 -160
  68. package/dist/components/formField/inputs/selectDropdown/SelectDropdown.stories.js.map +1 -1
  69. package/dist/components/formField/inputs/time/TimeInput.stories.d.ts +176 -22
  70. package/dist/components/formField/inputs/time/TimeInput.stories.d.ts.map +1 -1
  71. package/dist/components/formField/inputs/time/TimeInput.stories.js +851 -92
  72. package/dist/components/formField/inputs/time/TimeInput.stories.js.map +1 -1
  73. package/dist/components/formField/label/Label.stories.d.ts +54 -5
  74. package/dist/components/formField/label/Label.stories.d.ts.map +1 -1
  75. package/dist/components/formField/label/Label.stories.js +238 -4
  76. package/dist/components/formField/label/Label.stories.js.map +1 -1
  77. package/dist/components/icoText/IcoText.stories.d.ts +32 -6
  78. package/dist/components/icoText/IcoText.stories.d.ts.map +1 -1
  79. package/dist/components/icoText/IcoText.stories.js +309 -14
  80. package/dist/components/icoText/IcoText.stories.js.map +1 -1
  81. package/dist/components/kpiCard/KPICard.stories.d.ts +100 -2
  82. package/dist/components/kpiCard/KPICard.stories.d.ts.map +1 -1
  83. package/dist/components/kpiCard/KPICard.stories.js +354 -10
  84. package/dist/components/kpiCard/KPICard.stories.js.map +1 -1
  85. package/dist/components/kvpList/KVPList.stories.d.ts +57 -4
  86. package/dist/components/kvpList/KVPList.stories.d.ts.map +1 -1
  87. package/dist/components/kvpList/KVPList.stories.js +403 -10
  88. package/dist/components/kvpList/KVPList.stories.js.map +1 -1
  89. package/dist/components/modal/Modal.stories.d.ts +113 -9
  90. package/dist/components/modal/Modal.stories.d.ts.map +1 -1
  91. package/dist/components/modal/Modal.stories.js +633 -13
  92. package/dist/components/modal/Modal.stories.js.map +1 -1
  93. package/dist/components/modal/modalManager/ModalManager.stories.d.ts +34 -10
  94. package/dist/components/modal/modalManager/ModalManager.stories.d.ts.map +1 -1
  95. package/dist/components/modal/modalManager/ModalManager.stories.js +463 -85
  96. package/dist/components/modal/modalManager/ModalManager.stories.js.map +1 -1
  97. package/dist/components/pill/Pill.d.ts.map +1 -1
  98. package/dist/components/pill/Pill.js +1 -1
  99. package/dist/components/pill/Pill.js.map +1 -1
  100. package/dist/components/pill/Pill.stories.d.ts.map +1 -1
  101. package/dist/components/pill/Pill.stories.js +11 -13
  102. package/dist/components/pill/Pill.stories.js.map +1 -1
  103. package/dist/components/row/Row.stories.d.ts +1 -2
  104. package/dist/components/row/Row.stories.d.ts.map +1 -1
  105. package/dist/components/row/Row.stories.js +360 -50
  106. package/dist/components/row/Row.stories.js.map +1 -1
  107. package/dist/components/searchBar/SearchBar.stories.d.ts +52 -4
  108. package/dist/components/searchBar/SearchBar.stories.d.ts.map +1 -1
  109. package/dist/components/searchBar/SearchBar.stories.js +428 -36
  110. package/dist/components/searchBar/SearchBar.stories.js.map +1 -1
  111. package/dist/components/section/Section.stories.d.ts +11 -41
  112. package/dist/components/section/Section.stories.d.ts.map +1 -1
  113. package/dist/components/section/Section.stories.js +494 -56
  114. package/dist/components/section/Section.stories.js.map +1 -1
  115. package/dist/components/singleUser/SingleUser.stories.d.ts +5 -4
  116. package/dist/components/singleUser/SingleUser.stories.d.ts.map +1 -1
  117. package/dist/components/singleUser/SingleUser.stories.js +303 -31
  118. package/dist/components/singleUser/SingleUser.stories.js.map +1 -1
  119. package/dist/components/slideoverManager/SlideoverManager.stories.d.ts +32 -11
  120. package/dist/components/slideoverManager/SlideoverManager.stories.d.ts.map +1 -1
  121. package/dist/components/slideoverManager/SlideoverManager.stories.js +380 -84
  122. package/dist/components/slideoverManager/SlideoverManager.stories.js.map +1 -1
  123. package/dist/components/table/DSDefaultColDef.d.ts.map +1 -1
  124. package/dist/components/table/DSDefaultColDef.js +4 -3
  125. package/dist/components/table/DSDefaultColDef.js.map +1 -1
  126. package/dist/components/table/Table.d.ts +6 -1
  127. package/dist/components/table/Table.d.ts.map +1 -1
  128. package/dist/components/table/Table.js +8 -3
  129. package/dist/components/table/Table.js.map +1 -1
  130. package/dist/components/table/Table.stories.d.ts +2 -0
  131. package/dist/components/table/Table.stories.d.ts.map +1 -1
  132. package/dist/components/table/Table.stories.js +357 -3
  133. package/dist/components/table/Table.stories.js.map +1 -1
  134. package/dist/components/table/TableFooter.stories.d.ts +49 -0
  135. package/dist/components/table/TableFooter.stories.d.ts.map +1 -0
  136. package/dist/components/table/TableFooter.stories.js +137 -0
  137. package/dist/components/table/TableFooter.stories.js.map +1 -0
  138. package/dist/components/table/TableHeader.stories.d.ts +93 -0
  139. package/dist/components/table/TableHeader.stories.d.ts.map +1 -0
  140. package/dist/components/table/TableHeader.stories.js +176 -0
  141. package/dist/components/table/TableHeader.stories.js.map +1 -0
  142. package/dist/components/table/cellEditors/DateCellEditor.stories.d.ts +44 -0
  143. package/dist/components/table/cellEditors/DateCellEditor.stories.d.ts.map +1 -0
  144. package/dist/components/table/cellEditors/DateCellEditor.stories.js +186 -0
  145. package/dist/components/table/cellEditors/DateCellEditor.stories.js.map +1 -0
  146. package/dist/components/table/cellRenderers/BooleanCellRenderer.stories.d.ts +40 -0
  147. package/dist/components/table/cellRenderers/BooleanCellRenderer.stories.d.ts.map +1 -0
  148. package/dist/components/table/cellRenderers/BooleanCellRenderer.stories.js +209 -0
  149. package/dist/components/table/cellRenderers/BooleanCellRenderer.stories.js.map +1 -0
  150. package/dist/components/table/cellRenderers/ButtonCellRenderer.stories.d.ts +48 -0
  151. package/dist/components/table/cellRenderers/ButtonCellRenderer.stories.d.ts.map +1 -0
  152. package/dist/components/table/cellRenderers/ButtonCellRenderer.stories.js +244 -0
  153. package/dist/components/table/cellRenderers/ButtonCellRenderer.stories.js.map +1 -0
  154. package/dist/components/table/cellRenderers/CheckboxCellRenderer.d.ts.map +1 -1
  155. package/dist/components/table/cellRenderers/CheckboxCellRenderer.js +3 -1
  156. package/dist/components/table/cellRenderers/CheckboxCellRenderer.js.map +1 -1
  157. package/dist/components/table/cellRenderers/CheckboxCellRenderer.stories.d.ts +64 -0
  158. package/dist/components/table/cellRenderers/CheckboxCellRenderer.stories.d.ts.map +1 -0
  159. package/dist/components/table/cellRenderers/CheckboxCellRenderer.stories.js +241 -0
  160. package/dist/components/table/cellRenderers/CheckboxCellRenderer.stories.js.map +1 -0
  161. package/dist/components/table/cellRenderers/DefaultCellRenderer.stories.d.ts +55 -0
  162. package/dist/components/table/cellRenderers/DefaultCellRenderer.stories.d.ts.map +1 -0
  163. package/dist/components/table/cellRenderers/DefaultCellRenderer.stories.js +245 -0
  164. package/dist/components/table/cellRenderers/DefaultCellRenderer.stories.js.map +1 -0
  165. package/dist/components/table/cellRenderers/InlineTextCellRenderer.stories.d.ts +67 -0
  166. package/dist/components/table/cellRenderers/InlineTextCellRenderer.stories.d.ts.map +1 -0
  167. package/dist/components/table/cellRenderers/InlineTextCellRenderer.stories.js +221 -0
  168. package/dist/components/table/cellRenderers/InlineTextCellRenderer.stories.js.map +1 -0
  169. package/dist/components/table/cellRenderers/SelectDropdownCellRenderer.stories.d.ts +75 -0
  170. package/dist/components/table/cellRenderers/SelectDropdownCellRenderer.stories.d.ts.map +1 -0
  171. package/dist/components/table/cellRenderers/SelectDropdownCellRenderer.stories.js +270 -0
  172. package/dist/components/table/cellRenderers/SelectDropdownCellRenderer.stories.js.map +1 -0
  173. package/dist/components/table/columnFilters/BooleanFilter.stories.d.ts +57 -0
  174. package/dist/components/table/columnFilters/BooleanFilter.stories.d.ts.map +1 -0
  175. package/dist/components/table/columnFilters/BooleanFilter.stories.js +198 -0
  176. package/dist/components/table/columnFilters/BooleanFilter.stories.js.map +1 -0
  177. package/dist/components/table/columnFilters/TimeFilter.stories.d.ts +58 -0
  178. package/dist/components/table/columnFilters/TimeFilter.stories.d.ts.map +1 -0
  179. package/dist/components/table/columnFilters/TimeFilter.stories.js +207 -0
  180. package/dist/components/table/columnFilters/TimeFilter.stories.js.map +1 -0
  181. package/dist/components/table/pagination/PaginationPanel.stories.d.ts +113 -0
  182. package/dist/components/table/pagination/PaginationPanel.stories.d.ts.map +1 -0
  183. package/dist/components/table/pagination/PaginationPanel.stories.js +272 -0
  184. package/dist/components/table/pagination/PaginationPanel.stories.js.map +1 -0
  185. package/dist/components/table/tableControls/TableControls.stories.d.ts +151 -0
  186. package/dist/components/table/tableControls/TableControls.stories.d.ts.map +1 -0
  187. package/dist/components/table/tableControls/TableControls.stories.js +356 -0
  188. package/dist/components/table/tableControls/TableControls.stories.js.map +1 -0
  189. package/dist/components/table/tableControls/TableSettingsDropdown.d.ts +27 -1
  190. package/dist/components/table/tableControls/TableSettingsDropdown.d.ts.map +1 -1
  191. package/dist/components/table/tableControls/TableSettingsDropdown.js +53 -26
  192. package/dist/components/table/tableControls/TableSettingsDropdown.js.map +1 -1
  193. package/dist/components/table/tableControls/TableSettingsDropdown.test.d.ts +2 -0
  194. package/dist/components/table/tableControls/TableSettingsDropdown.test.d.ts.map +1 -0
  195. package/dist/components/table/tableControls/TableSettingsDropdown.test.js +178 -0
  196. package/dist/components/table/tableControls/TableSettingsDropdown.test.js.map +1 -0
  197. package/dist/components/tabs/Tabs.stories.d.ts +22 -4
  198. package/dist/components/tabs/Tabs.stories.d.ts.map +1 -1
  199. package/dist/components/tabs/Tabs.stories.js +398 -22
  200. package/dist/components/tabs/Tabs.stories.js.map +1 -1
  201. package/dist/components/tabs/TabsItem.stories.d.ts +54 -1
  202. package/dist/components/tabs/TabsItem.stories.d.ts.map +1 -1
  203. package/dist/components/tabs/TabsItem.stories.js +61 -9
  204. package/dist/components/tabs/TabsItem.stories.js.map +1 -1
  205. package/dist/components/toast/Toast.stories.d.ts +103 -10
  206. package/dist/components/toast/Toast.stories.d.ts.map +1 -1
  207. package/dist/components/toast/Toast.stories.js +409 -47
  208. package/dist/components/toast/Toast.stories.js.map +1 -1
  209. package/dist/components/toggle/Toggle.stories.d.ts +61 -46
  210. package/dist/components/toggle/Toggle.stories.d.ts.map +1 -1
  211. package/dist/components/toggle/Toggle.stories.js +311 -122
  212. package/dist/components/toggle/Toggle.stories.js.map +1 -1
  213. package/dist/components/tooltip/Tooltip.stories.d.ts +78 -6
  214. package/dist/components/tooltip/Tooltip.stories.d.ts.map +1 -1
  215. package/dist/components/tooltip/Tooltip.stories.js +413 -7
  216. package/dist/components/tooltip/Tooltip.stories.js.map +1 -1
  217. package/dist/components/tooltip/TooltipWrapper.stories.d.ts +71 -7
  218. package/dist/components/tooltip/TooltipWrapper.stories.d.ts.map +1 -1
  219. package/dist/components/tooltip/TooltipWrapper.stories.js +238 -10
  220. package/dist/components/tooltip/TooltipWrapper.stories.js.map +1 -1
  221. package/dist/index.css +8 -0
  222. package/dist/index.css.map +1 -1
  223. package/dist/utils/PopupParentContext.stories.d.ts +17 -0
  224. package/dist/utils/PopupParentContext.stories.d.ts.map +1 -0
  225. package/dist/utils/PopupParentContext.stories.js +266 -0
  226. package/dist/utils/PopupParentContext.stories.js.map +1 -0
  227. package/dist/utils/getDefaultPopupParent.d.ts.map +1 -1
  228. package/dist/utils/getDefaultPopupParent.js +6 -0
  229. package/dist/utils/getDefaultPopupParent.js.map +1 -1
  230. package/package.json +1 -1
  231. package/src/components/articleCard/ArticleCard.stories.tsx +524 -111
  232. package/src/components/avatar/Avatar.stories.tsx +504 -59
  233. package/src/components/avatarGroup/AvatarGroup.stories.tsx +977 -175
  234. package/src/components/banner/Banner.stories.tsx +7 -3
  235. package/src/components/card/Card.stories.tsx +466 -36
  236. package/src/components/combobox/Combobox.stories.tsx +867 -260
  237. package/src/components/datePicker/DatePicker.stories.tsx +777 -60
  238. package/src/components/dateTimePicker/DateTimePicker.stories.tsx +910 -132
  239. package/src/components/editableText/EditableText.stories.tsx +567 -91
  240. package/src/components/formField/FormField.test.tsx +6 -0
  241. package/src/components/formField/FormField.tsx +5 -0
  242. package/src/components/formField/fieldset/Fieldset.stories.tsx +761 -51
  243. package/src/components/formField/inputs/checkbox/CheckboxGroup.tsx +1 -1
  244. package/src/components/formField/inputs/checkbox/CheckboxInput.tsx +1 -1
  245. package/src/components/formField/inputs/colourPickerDropdown/ColourPickerDropdown.stories.tsx +504 -11
  246. package/src/components/formField/inputs/radio/RadioButtonGroup.tsx +17 -4
  247. package/src/components/formField/inputs/radio/RadioButtonInput.stories.tsx +71 -59
  248. package/src/components/formField/inputs/selectDropdown/SelectDropdown.stories.tsx +1079 -168
  249. package/src/components/formField/inputs/time/TimeInput.stories.tsx +1140 -104
  250. package/src/components/formField/label/Label.stories.tsx +317 -8
  251. package/src/components/icoText/IcoText.stories.tsx +442 -31
  252. package/src/components/kpiCard/KPICard.stories.tsx +475 -30
  253. package/src/components/kvpList/KVPList.stories.tsx +593 -26
  254. package/src/components/modal/Modal.stories.tsx +963 -26
  255. package/src/components/modal/modalManager/ModalManager.stories.tsx +612 -454
  256. package/src/components/pill/Pill.stories.tsx +11 -13
  257. package/src/components/pill/Pill.tsx +1 -0
  258. package/src/components/row/Row.stories.tsx +474 -58
  259. package/src/components/searchBar/SearchBar.stories.tsx +570 -38
  260. package/src/components/section/Section.stories.tsx +723 -70
  261. package/src/components/singleUser/SingleUser.stories.tsx +393 -34
  262. package/src/components/slideoverManager/SlideoverManager.stories.tsx +572 -342
  263. package/src/components/table/DSDefaultColDef.ts +25 -5
  264. package/src/components/table/Table.stories.tsx +411 -3
  265. package/src/components/table/Table.tsx +9 -2
  266. package/src/components/table/TableFooter.stories.tsx +196 -0
  267. package/src/components/table/TableHeader.stories.tsx +251 -0
  268. package/src/components/table/cellEditors/DateCellEditor.stories.tsx +245 -0
  269. package/src/components/table/cellRenderers/BooleanCellRenderer.stories.tsx +278 -0
  270. package/src/components/table/cellRenderers/ButtonCellRenderer.stories.tsx +333 -0
  271. package/src/components/table/cellRenderers/CheckboxCellRenderer.stories.tsx +337 -0
  272. package/src/components/table/cellRenderers/CheckboxCellRenderer.tsx +5 -1
  273. package/src/components/table/cellRenderers/DefaultCellRenderer.stories.tsx +342 -0
  274. package/src/components/table/cellRenderers/InlineTextCellRenderer.stories.tsx +292 -0
  275. package/src/components/table/cellRenderers/SelectDropdownCellRenderer.stories.tsx +369 -0
  276. package/src/components/table/columnFilters/BooleanFilter.stories.tsx +268 -0
  277. package/src/components/table/columnFilters/TimeFilter.stories.tsx +281 -0
  278. package/src/components/table/pagination/PaginationPanel.stories.tsx +327 -0
  279. package/src/components/table/tableControls/TableControls.stories.tsx +415 -0
  280. package/src/components/table/tableControls/TableSettingsDropdown.test.tsx +207 -0
  281. package/src/components/table/tableControls/TableSettingsDropdown.tsx +103 -39
  282. package/src/components/tabs/Tabs.stories.tsx +540 -60
  283. package/src/components/tabs/TabsItem.stories.tsx +82 -8
  284. package/src/components/toast/Toast.stories.tsx +539 -77
  285. package/src/components/toggle/Toggle.stories.tsx +371 -135
  286. package/src/components/tooltip/Tooltip.stories.tsx +606 -15
  287. package/src/components/tooltip/TooltipWrapper.stories.tsx +348 -12
  288. package/src/docs/Contributing.mdx +241 -0
  289. package/src/docs/UsingComponents.mdx +93 -0
  290. package/src/docs/Welcome.mdx +68 -0
  291. package/src/global.scss +7 -0
  292. package/src/utils/PopupParentContext.stories.tsx +367 -0
  293. package/src/utils/getDefaultPopupParent.ts +6 -0
  294. package/.ralph/storybook-upgrade/knowledge.md +0 -308
  295. package/.ralph/storybook-upgrade/prd.json +0 -777
  296. package/.ralph/storybook-upgrade/progress.md +0 -342
  297. package/src/components/table/TableWIP.mdx +0 -3
@@ -1,4 +1,13 @@
1
1
  import type { Meta, StoryObj } from '@storybook/react-vite';
2
+ import {
3
+ Controls,
4
+ Heading as DocHeading,
5
+ Markdown,
6
+ Primary as DocPrimary,
7
+ Stories,
8
+ Subtitle,
9
+ Title,
10
+ } from '@storybook/addon-docs/blocks';
2
11
  import { useRef, useState } from 'react';
3
12
  import { Icon } from 'Components/icon/Icon';
4
13
  import {
@@ -7,352 +16,950 @@ import {
7
16
  comboboxPeopleOptions,
8
17
  } from '../../mocks/comboboxStoryOptions';
9
18
  import { Combobox } from './Combobox';
10
- import type { ComboboxOption, ComboboxProps } from './types';
19
+ import type { ComboboxOption } from './types';
20
+
21
+ // ─── Docs-page content ────────────────────────────────────────────────────────
22
+
23
+ const DESCRIPTION_INTRO = `
24
+ The \`Combobox\` component is a searchable, accessible select field that supports
25
+ single-select, multi-select, grouped options, async search, and item creation —
26
+ built on Radix UI Popover for reliable portal-based positioning.
27
+ `.trim();
28
+
29
+ const USAGE_GUIDANCE = `
30
+ ### When to use
31
+
32
+ - Selecting one or more items from a searchable list (teachers, pupils, subjects, year groups).
33
+ - Filtering a dataset where the user needs to type to narrow down many options.
34
+ - Allowing users to create new values on the fly (e.g. adding a new tag or subject name).
35
+ - Async-loaded lists where options are fetched from an API as the user types.
36
+
37
+ ---
38
+
39
+ ### When NOT to use
40
+
41
+ | Situation | Use instead |
42
+ |-----------|-------------|
43
+ | A short list (≤ 6 items) with no need to search | \`SelectDropdown\` |
44
+ | Free-text search that triggers a navigation or query action | \`SearchBar\` |
45
+ | A simple true/false toggle | \`Checkbox\` or \`Toggle\` |
46
+ | Picking a date or time | \`DatePicker\` / \`TimePicker\` |
47
+ `.trim();
48
+
49
+ const DEVELOPER_NOTES = `
50
+ > **Built on Radix UI Popover.** The dropdown renders into a portal — do NOT place
51
+ > the Combobox inside a container with \`overflow: hidden\` or \`position: relative\`
52
+ > that clips its stacking context. This will cause the dropdown to be cut off.
53
+
54
+ ---
55
+
56
+ ### \`value\` is always \`string[]\`
57
+
58
+ > **Critical:** Even in single-select mode, \`value\` and \`defaultValue\` are
59
+ > **always arrays**. This is the most common integration bug.
60
+
61
+ \`\`\`tsx
62
+ // ✅ Correct — value is string[]
63
+ <Combobox value={['alice-johnson']} onValueChange={(vals) => setVal(vals)} />
64
+
65
+ // ❌ Wrong — value must not be a plain string
66
+ <Combobox value="alice-johnson" />
67
+ \`\`\`
68
+
69
+ ---
70
+
71
+ ### Controlled vs uncontrolled
72
+
73
+ Use \`value\` + \`onValueChange\` for controlled mode. Use \`defaultValue\` for
74
+ uncontrolled initial selection. Do not mix both.
75
+
76
+ \`\`\`tsx
77
+ // Controlled
78
+ const [selected, setSelected] = useState<string[]>([]);
79
+ <Combobox value={selected} onValueChange={setSelected} options={options} />
80
+
81
+ // Uncontrolled with initial value
82
+ <Combobox defaultValue={['alice-johnson']} options={options} />
83
+ \`\`\`
84
+
85
+ ---
86
+
87
+ ### Async search
88
+
89
+ When you pass \`onSearch\`, **all client-side filtering is disabled** — you are
90
+ responsible for updating the \`options\` array yourself (e.g. from an API response).
91
+ The component does NOT debounce; add your own debounce inside \`onSearch\`.
92
+
93
+ \`\`\`tsx
94
+ <Combobox
95
+ options={results}
96
+ onSearch={async (query) => {
97
+ const data = await fetchStaff(query);
98
+ setResults(data);
99
+ }}
100
+ />
101
+ \`\`\`
102
+
103
+ ---
104
+
105
+ ### Keyboard navigation
106
+
107
+ | Key | Action |
108
+ |-----|--------|
109
+ | \`↓\` / \`↑\` | Move focus through options |
110
+ | \`Enter\` | Select the focused option |
111
+ | \`Escape\` | Close the dropdown |
112
+ | \`Backspace\` | Remove the last selected tag (multi-select) |
113
+ | \`Tab\` | Close the dropdown and advance focus |
114
+
115
+ ---
116
+
117
+ ### Grouped options
118
+
119
+ Options with the same \`group\` string are rendered under a shared group heading.
120
+ The \`group\` string is **case-sensitive** and must match exactly across all options
121
+ in the same group.
122
+
123
+ \`\`\`tsx
124
+ const options = [
125
+ { value: 'alice', label: 'Alice Johnson', group: 'Teachers' },
126
+ { value: 'bob', label: 'Bob Smith', group: 'Teachers' }, // same group ✅
127
+ { value: 'carol', label: 'Carol White', group: 'teachers' }, // different group ❌
128
+ ];
129
+ \`\`\`
130
+
131
+ ---
132
+
133
+ ### Accessibility
11
134
 
12
- const meta: Meta<typeof Combobox> = {
135
+ - The trigger renders a combobox role with \`aria-expanded\`, \`aria-haspopup\`, and
136
+ \`aria-controls\` wired to the listbox.
137
+ - Options use \`aria-selected\` and \`aria-disabled\` where appropriate.
138
+ - Pass \`aria-label\` when there is no visible \`<label>\` element nearby.
139
+ - Pass \`aria-describedby\` to link to a \`<FormField>\` hint or error message.
140
+ - Pass \`aria-invalid\` alongside \`hasError\` to signal validation failure to
141
+ assistive technology.
142
+
143
+ ---
144
+
145
+ ### Sub-components (escape hatches)
146
+
147
+ The compound sub-components (\`Combobox.Trigger\`, \`Combobox.ButtonTrigger\`,
148
+ \`Combobox.Listbox\`) exist for advanced layout needs where the trigger and listbox
149
+ must be separated in the DOM. For 99% of use cases, use \`<Combobox />\` directly.
150
+ `.trim();
151
+
152
+ const RELATED_COMPONENTS = [
153
+ '## Related components',
154
+ '',
155
+ '[SelectDropdown](?path=/docs/components-formfield-inputs-selectdropdown--docs) · [SearchBar](?path=/docs/components-searchbar--docs) · [FormField](?path=/docs/components-formfield--docs) · [CheckboxInput](?path=/docs/components-formfield-inputs-checkbox--docs)',
156
+ ].join('\n');
157
+
158
+ const PROPS_INTRO = 'The preview below is wired to the **Controls** panel — tweak any prop to see the story update in real time.';
159
+
160
+ // ─── Docs page ────────────────────────────────────────────────────────────────
161
+
162
+ function ComboboxDocsPage() {
163
+ return (
164
+ <>
165
+ <Title />
166
+ <Subtitle />
167
+ <Markdown>{DESCRIPTION_INTRO}</Markdown>
168
+ <DocHeading>Interactive example</DocHeading>
169
+ <Markdown>{PROPS_INTRO}</Markdown>
170
+ <DocPrimary />
171
+ <Controls />
172
+ <DocHeading>Usage guidance</DocHeading>
173
+ <Markdown>{USAGE_GUIDANCE}</Markdown>
174
+ <DocHeading>Developer notes</DocHeading>
175
+ <Markdown>{DEVELOPER_NOTES}</Markdown>
176
+ <DocHeading>Examples</DocHeading>
177
+ <Stories title="" />
178
+ <Markdown>{RELATED_COMPONENTS}</Markdown>
179
+ </>
180
+ );
181
+ }
182
+
183
+ // ─── Meta ─────────────────────────────────────────────────────────────────────
184
+
185
+ const meta = {
13
186
  title: 'Components/Combobox',
14
187
  component: Combobox,
15
188
  tags: ['autodocs'],
16
189
  parameters: {
190
+ layout: 'padded',
17
191
  docs: {
18
- description: {
19
- component:
20
- 'Default usage is `<Combobox … />` with props such as `triggerVariant` and `multiple`. `Combobox.Trigger`, `Combobox.ButtonTrigger`, and `Combobox.Listbox` are composition escape hatches.',
21
- },
192
+ page: ComboboxDocsPage,
22
193
  },
23
194
  },
195
+ decorators: [
196
+ (Story: React.ComponentType) => (
197
+ <div
198
+ data-surface="base"
199
+ data-colour-mode="light"
200
+ style={{ padding: 'var(--size-space-400)', maxWidth: 480, minHeight: 300 }}
201
+ >
202
+ <Story />
203
+ </div>
204
+ ),
205
+ ],
24
206
  argTypes: {
25
- dropdownOnFocus: {
207
+ options: {
208
+ control: 'object',
209
+ description:
210
+ 'Array of `ComboboxOption` objects `{ value: string; label: string; tagLabel?: string; iconName?: IconName; disabled?: boolean; group?: string }`.',
211
+ },
212
+ multiple: {
26
213
  control: 'boolean',
27
- description: 'When true, focusing the input opens the dropdown if there are items to show.',
214
+ description: 'Allow multiple selections. Each selected value is shown as a tag chip.',
28
215
  },
29
- showDropdownTrigger: {
216
+ placeholder: {
217
+ control: 'text',
218
+ description: 'Placeholder text shown in the trigger when no value is selected.',
219
+ },
220
+ disabled: {
221
+ control: 'boolean',
222
+ description: 'Disables the entire combobox — trigger and dropdown become non-interactive.',
223
+ },
224
+ loading: {
225
+ control: 'boolean',
226
+ description: 'Shows a loading indicator inside the dropdown while options are being fetched.',
227
+ },
228
+ hasError: {
229
+ control: 'boolean',
230
+ description:
231
+ 'Applies error styling to the trigger. Pair with `aria-invalid` for full accessibility.',
232
+ },
233
+ searchType: {
234
+ control: 'inline-radio',
235
+ options: ['prefix', 'substring'],
236
+ description:
237
+ '`prefix` (default) matches from the start of the label. `substring` matches anywhere. You can also pass a custom function `(query, options) => ComboboxOption[]`.',
238
+ },
239
+ highlightStringMatches: {
240
+ control: 'boolean',
241
+ description:
242
+ 'Highlight the matching portion of each option label while the user is typing.',
243
+ },
244
+ selectedValueDisplay: {
245
+ control: 'inline-radio',
246
+ options: ['tags', 'text'],
247
+ description:
248
+ '`tags` (default) renders each selected value as a chip. `text` shows a comma-separated string — useful in space-constrained button triggers.',
249
+ },
250
+ showClearAll: {
30
251
  control: 'boolean',
31
- description: 'When true, shows the chevron trigger button.',
252
+ description: 'Show a "Clear all" button inside the trigger when one or more values are selected.',
253
+ },
254
+ clearAllLabel: {
255
+ control: 'text',
256
+ description: 'Label for the clear-all button. Defaults to `"Clear all"`.',
32
257
  },
33
258
  triggerVariant: {
34
259
  control: 'inline-radio',
35
260
  options: ['input', 'button'],
36
- description: 'Choose trigger style: input-in-trigger or button-style summary trigger.',
261
+ description:
262
+ '`input` (default) renders an inline text field. `button` renders a clickable button; the search field moves into the dropdown.',
263
+ },
264
+ allowCreate: {
265
+ control: 'boolean',
266
+ description:
267
+ 'Show a "Create X" row when the typed query does not match any existing option.',
268
+ },
269
+ showDropdownTrigger: {
270
+ control: 'boolean',
271
+ description: 'Show or hide the chevron button at the end of the trigger. Defaults to `true`.',
272
+ },
273
+ dropdownOnFocus: {
274
+ control: 'boolean',
275
+ description:
276
+ 'Open the dropdown automatically when the trigger receives focus. Defaults to `true`.',
37
277
  },
278
+ showSelectionCountBadge: {
279
+ control: 'boolean',
280
+ description:
281
+ 'Show a badge with the number of selected items (useful with `triggerVariant="button"`).',
282
+ },
283
+ // Callbacks — not controllable via the controls panel
284
+ onValueChange: { control: false },
285
+ onSearch: { control: false },
286
+ onCreateNew: { control: false },
287
+ onDeleteCreated: { control: false },
288
+ renderOption: { control: false },
289
+ getTagLabel: { control: false },
290
+ triggerEndContent: { control: false },
38
291
  },
39
- decorators: [
40
- Story => (
41
- <div data-surface="base" data-colour-mode="light" className="bg-surface text-on-surface-default" style={{ padding: 32, maxWidth: 420 }}>
42
- <Story />
43
- </div>
44
- ),
45
- ],
46
- };
292
+ } satisfies Meta<typeof Combobox>;
47
293
 
48
294
  export default meta;
49
-
50
295
  type Story = StoryObj<typeof Combobox>;
51
296
 
52
- const withDescription = (story: Story, description: string): Story => ({
53
- ...story,
54
- parameters: {
55
- ...story.parameters,
56
- docs: {
57
- ...story.parameters?.docs,
58
- description: {
59
- story: description,
297
+ // ─── Helpers ──────────────────────────────────────────────────────────────────
298
+
299
+ function withDescription(description: string, story: Story): Story {
300
+ return {
301
+ ...story,
302
+ parameters: {
303
+ ...story.parameters,
304
+ docs: {
305
+ ...story.parameters?.docs,
306
+ description: { story: description },
60
307
  },
61
308
  },
62
- },
63
- });
64
-
65
- /** Enough rows to exceed `.ds-combobox__listbox` max-height so keyboard nav scrolls. */
66
- const manyScrollableOptions: ComboboxOption[] = Array.from({ length: 45 }, (_, i) => ({
67
- value: `scroll-demo-${i + 1}`,
68
- label: `Option ${i + 1}`,
69
- iconName: 'user',
70
- }));
309
+ };
310
+ }
71
311
 
72
- const timeOptions: ComboboxOption[] = ['13:00', '13:30', '14:00', '14:30', '15:00'].map(time => ({
73
- value: time,
74
- label: time,
75
- }));
312
+ // ─── Stories ──────────────────────────────────────────────────────────────────
76
313
 
77
- export const ScrollableLongList: Story = withDescription({
78
- args: {
79
- options: manyScrollableOptions,
80
- placeholder: 'Open the list and move with arrow keys…',
81
- },
82
- }, 'Exercises a scrollable listbox with enough items to validate keyboard navigation and active-row scrolling.');
83
-
84
- export const ButtonTriggerSingleSelect: Story = withDescription({
314
+ // 1. Default wired to Controls
315
+ export const Default: Story = {
85
316
  args: {
86
317
  options: comboboxPeopleOptions,
87
- placeholder: 'Students',
88
- triggerVariant: 'button',
318
+ placeholder: 'Select a teacher...',
319
+ multiple: false,
320
+ disabled: false,
321
+ loading: false,
322
+ hasError: false,
323
+ searchType: 'prefix',
324
+ highlightStringMatches: false,
325
+ selectedValueDisplay: 'tags',
326
+ showClearAll: false,
89
327
  },
90
- }, 'Shows the button-style trigger in single-select mode before the search input moves into the popover.');
328
+ render: args => <Combobox {...args} />,
329
+ };
91
330
 
92
- export const ButtonTriggerPlainTextValue: Story = withDescription({
93
- args: {
94
- options: timeOptions,
95
- placeholder: 'Select time',
96
- triggerVariant: 'button',
97
- selectedValueDisplay: 'text',
98
- showDropdownTrigger: false,
99
- triggerEndContent: <Icon name="clock-3" size={16} />,
100
- defaultValue: ['14:30'],
101
- },
102
- }, 'Uses plain-text selected-value rendering instead of tags, which suits a single selected time or similarly compact values.');
331
+ // 2. ControlledSingleSelect
332
+ function ControlledSingleSelectTemplate() {
333
+ const [value, setValue] = useState<string[]>([]);
103
334
 
104
- export const ButtonTriggerMultiSelect: Story = withDescription({
105
- args: {
106
- options: manyScrollableOptions,
107
- defaultValue: ['scroll-demo-1', 'scroll-demo-2', 'scroll-demo-3'],
108
- placeholder: 'Students',
109
- multiple: true,
110
- triggerVariant: 'button',
111
- },
112
- }, 'Demonstrates the button trigger variant with multiple selected chips and the selection count badge.');
335
+ return (
336
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--size-space-300)' }}>
337
+ <Combobox
338
+ id="controlled-combobox"
339
+ options={comboboxPeopleOptions}
340
+ value={value}
341
+ onValueChange={setValue}
342
+ placeholder="Select a teacher..."
343
+ />
344
+ <p className="ds-text" style={{ margin: 0 }}>
345
+ Selected:
346
+ {' '}
347
+ {value.length > 0 ? value.join(', ') : '(none)'}
348
+ </p>
349
+ </div>
350
+ );
351
+ }
113
352
 
114
- export const SingleSelect: Story = withDescription({
115
- args: {
116
- options: comboboxPeopleOptions,
117
- placeholder: 'Search people...',
118
- },
119
- }, 'Basic single-select Combobox with built-in client-side prefix matching.');
353
+ export const ControlledSingleSelect = withDescription(
354
+ 'Controlled mode: `value` and `onValueChange` are managed by the parent component. **`value` is always `string[]`** — even for single-select. This is the most common integration mistake.',
355
+ {
356
+ render: () => <ControlledSingleSelectTemplate />,
357
+ parameters: {
358
+ docs: {
359
+ source: {
360
+ code: `
361
+ function ControlledExample() {
362
+ // value is ALWAYS string[] — even for single-select
363
+ const [value, setValue] = useState<string[]>([]);
120
364
 
121
- export const SubstringSearch: Story = withDescription({
122
- args: {
123
- options: comboboxPeopleOptions,
124
- placeholder: 'Search people...',
125
- searchType: 'substring',
365
+ return (
366
+ <FormField label="Class teacher" htmlFor="teacher-select">
367
+ <Combobox
368
+ id="teacher-select"
369
+ options={teacherOptions}
370
+ value={value}
371
+ onValueChange={setValue}
372
+ placeholder="Select a teacher..."
373
+ />
374
+ </FormField>
375
+ );
376
+ }
377
+ `.trim(),
378
+ },
379
+ },
380
+ },
126
381
  },
127
- }, 'Switches the built-in matching mode from prefix search to substring search.');
128
-
129
- export const HighlightedMatches: Story = withDescription({
130
- args: {
131
- options: comboboxPeopleOptions,
132
- placeholder: 'Search people...',
133
- searchType: 'substring',
134
- highlightStringMatches: true,
382
+ );
383
+
384
+ // 3. SingleSelect
385
+ export const SingleSelect = withDescription(
386
+ 'Single-select mode (default). Type to filter the list by prefix. Press Enter or click to select.',
387
+ {
388
+ args: {
389
+ options: comboboxPeopleOptions,
390
+ placeholder: 'Select a teacher...',
391
+ },
135
392
  },
136
- }, 'Highlights the matched portions of each option while using substring filtering.');
137
-
138
- export const MultiSelect: Story = withDescription({
139
- args: {
140
- options: comboboxPeopleOptions,
141
- multiple: true,
142
- placeholder: 'Search people...',
143
- showClearAll: true,
393
+ );
394
+
395
+ // 4. MultiSelect
396
+ export const MultiSelect = withDescription(
397
+ 'Set `multiple` to allow multiple selections — each selected value is shown as a removable tag chip. Press Backspace to remove the last tag.',
398
+ {
399
+ args: {
400
+ options: comboboxPeopleOptions,
401
+ multiple: true,
402
+ placeholder: 'Select teachers...',
403
+ },
404
+ parameters: {
405
+ docs: {
406
+ source: {
407
+ code: '<Combobox options={teacherOptions} multiple placeholder="Select teachers..." />',
408
+ },
409
+ },
410
+ },
144
411
  },
145
- }, 'Basic multi-select Combobox with removable chips and a clear-all action.');
146
-
147
- export const WithDefaultValue: Story = withDescription({
148
- args: {
149
- options: comboboxPeopleOptions,
150
- multiple: true,
151
- defaultValue: ['alice', 'bob', 'charlie'],
152
- placeholder: 'Search people...',
153
- showClearAll: true,
412
+ );
413
+
414
+ // 5. WithDefaultValue
415
+ export const WithDefaultValue = withDescription(
416
+ '`defaultValue` sets the initial selection in uncontrolled mode. Note that `defaultValue` must be a `string[]` even for single-select.',
417
+ {
418
+ args: {
419
+ options: comboboxPeopleOptions,
420
+ defaultValue: ['alice-johnson'],
421
+ },
422
+ parameters: {
423
+ docs: {
424
+ source: {
425
+ code: '<Combobox options={teacherOptions} defaultValue={[\'alice-johnson\']} />',
426
+ },
427
+ },
428
+ },
154
429
  },
155
- }, 'Starts with preselected values so chip rendering and removal can be reviewed immediately.');
156
-
157
- export const WithGroups: Story = withDescription({
158
- args: {
159
- options: comboboxGroupedPeopleOptions,
160
- multiple: true,
161
- placeholder: 'Search people...',
430
+ );
431
+
432
+ // 6. SubstringSearch
433
+ export const SubstringSearch = withDescription(
434
+ 'Pass `searchType="substring"` to match anywhere in the label — not just the start. Type "son" to see it match "Alice Johnson" and "Alice Williamson".',
435
+ {
436
+ args: {
437
+ options: comboboxPeopleOptions,
438
+ searchType: 'substring',
439
+ placeholder: 'Type part of a name...',
440
+ },
441
+ parameters: {
442
+ docs: {
443
+ source: {
444
+ code: '<Combobox options={teacherOptions} searchType="substring" placeholder="Type part of a name..." />',
445
+ },
446
+ },
447
+ },
162
448
  },
163
- }, 'Shows grouped options in the dropdown while keeping the same multi-select interaction model.');
164
-
165
- export const Disabled: Story = withDescription({
166
- args: {
167
- options: comboboxPeopleOptions,
168
- disabled: true,
169
- defaultValue: ['alice'],
170
- placeholder: 'Search people...',
449
+ );
450
+
451
+ // 7. HighlightedMatches
452
+ export const HighlightedMatches = withDescription(
453
+ 'Add `highlightStringMatches` to highlight the matching portion of each option label as the user types. Works best with `searchType="substring"`.',
454
+ {
455
+ args: {
456
+ options: comboboxPeopleOptions,
457
+ searchType: 'substring',
458
+ highlightStringMatches: true,
459
+ placeholder: 'Type to highlight matches...',
460
+ },
461
+ parameters: {
462
+ docs: {
463
+ source: {
464
+ code: '<Combobox options={teacherOptions} searchType="substring" highlightStringMatches placeholder="Type to highlight matches..." />',
465
+ },
466
+ },
467
+ },
171
468
  },
172
- }, 'Displays the disabled state with an existing selected value.');
173
-
174
- export const HiddenTrigger: Story = withDescription({
175
- args: {
176
- options: comboboxPeopleOptions,
177
- placeholder: 'Search people...',
178
- showDropdownTrigger: false,
469
+ );
470
+
471
+ // 8. WithGroups
472
+ export const WithGroups = withDescription(
473
+ 'Options with the same `group` string are visually grouped under a shared heading. The `group` value must match exactly (case-sensitive) across all options in the same group.',
474
+ {
475
+ args: {
476
+ options: comboboxGroupedPeopleOptions,
477
+ multiple: true,
478
+ placeholder: 'Select staff...',
479
+ },
480
+ parameters: {
481
+ docs: {
482
+ source: {
483
+ code: `
484
+ const options = [
485
+ { value: 'alice', label: 'Alice Johnson', group: 'Teachers' },
486
+ { value: 'bob', label: 'Bob Smith', group: 'Teachers' },
487
+ { value: 'carol', label: 'Carol White', group: 'Support Staff' },
488
+ { value: 'rachel', label: 'Rachel Green', group: 'Admin' },
489
+ ];
490
+
491
+ <Combobox options={options} multiple placeholder="Select staff..." />
492
+ `.trim(),
493
+ },
494
+ },
495
+ },
179
496
  },
180
- }, 'Hides the chevron trigger while keeping focus, typing, and keyboard opening behavior.');
181
-
182
- export const ManualOpenOnFocus: Story = withDescription({
183
- args: {
184
- options: comboboxPeopleOptions,
185
- placeholder: 'Search people...',
186
- dropdownOnFocus: false,
497
+ );
498
+
499
+ // 9. Disabled
500
+ export const Disabled = withDescription(
501
+ 'The entire combobox is non-interactive when `disabled` is set. The trigger becomes visually muted and cannot be clicked or focused.',
502
+ {
503
+ args: {
504
+ options: comboboxPeopleOptions,
505
+ disabled: true,
506
+ placeholder: 'Not available',
507
+ },
508
+ parameters: {
509
+ docs: {
510
+ source: {
511
+ code: '<Combobox options={teacherOptions} disabled placeholder="Not available" />',
512
+ },
513
+ },
514
+ },
187
515
  },
188
- }, 'Disables focus-open so the list appears only after typing or explicit keyboard interaction.');
189
-
190
- export const CustomOptionLayout: Story = withDescription({
191
- args: {
192
- options: comboboxPeopleOptions,
193
- placeholder: 'Search people...',
194
- multiple: true,
195
- renderOption: (option, selected) => (
196
- <>
197
- <span aria-hidden="true">
198
- <input type="checkbox" checked={selected} readOnly tabIndex={-1} />
199
- </span>
200
- <span className="ds-combobox__option-label">{option.label}</span>
201
- </>
202
- ),
516
+ );
517
+
518
+ // 10. ErrorState
519
+ export const ErrorState = withDescription(
520
+ 'Set `hasError` to apply error styling to the trigger. Always pair with `aria-invalid` so assistive technology can identify the invalid field. Wrap in `FormField` to display the error message.',
521
+ {
522
+ args: {
523
+ 'options': comboboxPeopleOptions,
524
+ 'hasError': true,
525
+ 'aria-invalid': true,
526
+ 'placeholder': 'Select a teacher...',
527
+ },
528
+ render: args => <Combobox {...args} />,
529
+ parameters: {
530
+ docs: {
531
+ source: {
532
+ code: `
533
+ <Combobox
534
+ options={teacherOptions}
535
+ hasError
536
+ aria-invalid
537
+ placeholder="Select a teacher..."
538
+ />
539
+ `.trim(),
540
+ },
541
+ },
542
+ },
543
+ },
544
+ );
545
+
546
+ // 11. LoadingState
547
+ export const LoadingState = withDescription(
548
+ 'Set `loading` to show a loading indicator inside the dropdown while options are being fetched. Use this during async search operations before the results arrive.',
549
+ {
550
+ args: {
551
+ options: [],
552
+ loading: true,
553
+ placeholder: 'Searching...',
554
+ },
555
+ parameters: {
556
+ docs: {
557
+ source: {
558
+ code: `
559
+ // Typically used while an async fetch is in-flight:
560
+ <Combobox
561
+ options={results}
562
+ loading={isFetching}
563
+ onSearch={handleSearch}
564
+ placeholder="Search staff..."
565
+ />
566
+ `.trim(),
567
+ },
568
+ },
569
+ },
570
+ },
571
+ );
572
+
573
+ // 12. ButtonTriggerSingleSelect
574
+ export const ButtonTriggerSingleSelect = withDescription(
575
+ '`triggerVariant="button"` renders a clickable button as the trigger instead of an inline input. The search field moves inside the dropdown. Ideal for toolbar filters and compact layouts.',
576
+ {
577
+ args: {
578
+ options: comboboxPeopleOptions,
579
+ triggerVariant: 'button',
580
+ placeholder: 'Filter by teacher',
581
+ },
582
+ parameters: {
583
+ docs: {
584
+ source: {
585
+ code: '<Combobox options={teacherOptions} triggerVariant="button" placeholder="Filter by teacher" />',
586
+ },
587
+ },
588
+ },
589
+ },
590
+ );
591
+
592
+ // 13. ButtonTriggerMultiSelect
593
+ export const ButtonTriggerMultiSelect = withDescription(
594
+ 'Button trigger with `multiple` enabled. Use `showSelectionCountBadge` to show a badge with the number of selected items — useful when the trigger label should not grow in size.',
595
+ {
596
+ args: {
597
+ options: comboboxPeopleOptions,
598
+ triggerVariant: 'button',
599
+ multiple: true,
600
+ placeholder: 'Filter by teachers',
601
+ showSelectionCountBadge: true,
602
+ },
603
+ parameters: {
604
+ docs: {
605
+ source: {
606
+ code: '<Combobox options={teacherOptions} triggerVariant="button" multiple placeholder="Filter by teachers" showSelectionCountBadge />',
607
+ },
608
+ },
609
+ },
203
610
  },
204
- }, 'Uses `renderOption` to swap the row content while the Combobox still owns selection and listbox behavior.');
611
+ );
612
+
613
+ // 14. ButtonTriggerPlainTextValue
614
+ export const ButtonTriggerPlainTextValue = withDescription(
615
+ '`selectedValueDisplay="text"` shows the selected value(s) as a comma-separated text string instead of tag chips — useful when space is limited inside a button trigger.',
616
+ {
617
+ args: {
618
+ options: comboboxPeopleOptions,
619
+ triggerVariant: 'button',
620
+ selectedValueDisplay: 'text',
621
+ multiple: true,
622
+ placeholder: 'Filter by teacher',
623
+ },
624
+ parameters: {
625
+ docs: {
626
+ source: {
627
+ code: '<Combobox options={teacherOptions} triggerVariant="button" selectedValueDisplay="text" multiple placeholder="Filter by teacher" />',
628
+ },
629
+ },
630
+ },
631
+ },
632
+ );
633
+
634
+ // 15. HiddenTrigger
635
+ export const HiddenTrigger = withDescription(
636
+ 'Set `showDropdownTrigger={false}` to hide the chevron button at the end of the input. The dropdown can still be opened by typing or focusing the field.',
637
+ {
638
+ args: {
639
+ options: comboboxPeopleOptions,
640
+ showDropdownTrigger: false,
641
+ placeholder: 'No chevron shown...',
642
+ },
643
+ parameters: {
644
+ docs: {
645
+ source: {
646
+ code: '<Combobox options={teacherOptions} showDropdownTrigger={false} placeholder="No chevron shown..." />',
647
+ },
648
+ },
649
+ },
650
+ },
651
+ );
652
+
653
+ // 16. ManualOpenOnFocus
654
+ export const ManualOpenOnFocus = withDescription(
655
+ 'Set `dropdownOnFocus={false}` to prevent the dropdown from opening automatically on focus — the user must start typing or click the chevron to open it.',
656
+ {
657
+ args: {
658
+ options: comboboxPeopleOptions,
659
+ dropdownOnFocus: false,
660
+ placeholder: 'Focus me — dropdown stays closed',
661
+ },
662
+ parameters: {
663
+ docs: {
664
+ source: {
665
+ code: '<Combobox options={teacherOptions} dropdownOnFocus={false} placeholder="Focus me — dropdown stays closed" />',
666
+ },
667
+ },
668
+ },
669
+ },
670
+ );
205
671
 
206
- const delay = (ms: number) => new Promise<void>((resolve) => {
207
- setTimeout(resolve, ms);
208
- });
672
+ // ─── Async search template ─────────────────────────────────────────────────────
209
673
 
210
- async function searchPeople(query: string): Promise<ComboboxOption[]> {
211
- await delay(700);
212
- const q = query.trim().toLowerCase();
213
- if (!q) return comboboxAsyncSearchOptions;
214
- return comboboxAsyncSearchOptions.filter(p => p.label.toLowerCase().includes(q));
215
- }
674
+ function AsyncSearchTemplate() {
675
+ const [options, setOptions] = useState(comboboxPeopleOptions);
676
+ const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
677
+
678
+ const handleSearch = (query: string) => {
679
+ if (timerRef.current) clearTimeout(timerRef.current);
680
+ timerRef.current = setTimeout(() => {
681
+ setOptions(
682
+ comboboxAsyncSearchOptions.filter(o =>
683
+ o.label.toLowerCase().startsWith(query.toLowerCase()),
684
+ ),
685
+ );
686
+ }, 300);
687
+ };
216
688
 
217
- type AsyncSearchTemplateProps = Pick<
218
- ComboboxProps,
219
- 'allowCreate' | 'searchType' | 'highlightStringMatches'
220
- >;
689
+ return <Combobox options={options} onSearch={handleSearch} placeholder="Search staff..." />;
690
+ }
221
691
 
222
- const AsyncSearchTemplate = ({
223
- allowCreate = false,
224
- searchType = 'substring',
225
- highlightStringMatches = true,
226
- }: AsyncSearchTemplateProps) => {
692
+ // 17. AsyncSearch
693
+ export const AsyncSearch = withDescription(
694
+ 'Pass `onSearch` to take control of filtering. The component disables all client-side matching — you update the `options` array yourself (e.g. via an API call). This example simulates a 300 ms network delay.',
695
+ {
696
+ render: () => <AsyncSearchTemplate />,
697
+ parameters: {
698
+ docs: {
699
+ source: {
700
+ code: `
701
+ function AsyncSearchExample() {
227
702
  const [options, setOptions] = useState<ComboboxOption[]>([]);
228
- const [loading, setLoading] = useState(false);
229
- const requestIdRef = useRef(0);
703
+ const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
230
704
 
231
705
  const handleSearch = (query: string) => {
232
- const id = ++requestIdRef.current;
233
- setLoading(true);
234
- void searchPeople(query).then((next) => {
235
- if (id !== requestIdRef.current) return;
236
- setOptions(next);
237
- setLoading(false);
238
- });
706
+ if (timerRef.current) clearTimeout(timerRef.current);
707
+ timerRef.current = setTimeout(async () => {
708
+ const results = await fetchStaff(query);
709
+ setOptions(results);
710
+ }, 300);
239
711
  };
240
712
 
241
713
  return (
242
714
  <Combobox
243
715
  options={options}
244
- multiple
245
- placeholder="Type to search..."
246
- loading={loading}
247
- allowCreate={allowCreate}
248
- searchType={searchType}
249
- highlightStringMatches={highlightStringMatches}
250
716
  onSearch={handleSearch}
717
+ placeholder="Search staff..."
251
718
  />
252
719
  );
253
- };
254
-
255
- export const AsyncSearch: Story = withDescription({
256
- args: {
257
- allowCreate: false,
258
- searchType: 'substring',
259
- highlightStringMatches: true,
720
+ }
721
+ `.trim(),
722
+ },
723
+ },
724
+ },
260
725
  },
261
- render: args => (
262
- <AsyncSearchTemplate
263
- allowCreate={args.allowCreate}
264
- searchType={args.searchType}
265
- highlightStringMatches={args.highlightStringMatches}
266
- />
267
- ),
268
- }, 'Simulates async search results with local data so loading, filtering, and optional create flows can be reviewed.');
726
+ );
269
727
 
270
- const CreateNewTemplate = () => {
271
- const [options, setOptions] = useState<ComboboxOption[]>(comboboxPeopleOptions);
728
+ // ─── Create new template ───────────────────────────────────────────────────────
272
729
 
273
- const handleCreate = (name: string) => {
274
- const newOpt: ComboboxOption = {
275
- value: name.toLowerCase().replace(/\s/g, '-'),
276
- label: name,
277
- iconName: 'user',
278
- };
279
- setOptions(prev => [...prev, newOpt]);
280
- return newOpt;
281
- };
730
+ function CreateNewTemplate() {
731
+ const [options, setOptions] = useState(comboboxPeopleOptions);
282
732
 
283
- const handleDelete = (value: string) => {
284
- setOptions(prev => prev.filter(opt => opt.value !== value));
733
+ const handleCreate = (input: string) => {
734
+ const newOption = { value: input.toLowerCase().replace(/\s+/g, '-'), label: input };
735
+ setOptions(prev => [...prev, newOption]);
736
+ return newOption;
285
737
  };
286
738
 
287
739
  return (
288
740
  <Combobox
289
741
  options={options}
290
742
  multiple
291
- placeholder="Search or create..."
292
743
  allowCreate
293
744
  onCreateNew={handleCreate}
294
- onDeleteCreated={handleDelete}
745
+ placeholder="Select or add a teacher..."
295
746
  />
296
747
  );
297
- };
298
-
299
- export const CreateNew: Story = withDescription({
300
- render: () => <CreateNewTemplate />,
301
- }, 'Demonstrates multi-select creation and deletion of user-added options.');
302
-
303
- const SingleSelectCreateTemplate = () => {
304
- const [options, setOptions] = useState<ComboboxOption[]>(comboboxPeopleOptions);
748
+ }
305
749
 
306
- const handleCreate = (name: string) => {
307
- const newOpt: ComboboxOption = {
308
- value: name.toLowerCase().replace(/\s/g, '-'),
309
- label: name,
310
- iconName: 'user',
750
+ // 18. CreateNew
751
+ export const CreateNew = withDescription(
752
+ 'Set `allowCreate` to show a "Create X" option when the typed query does not match any existing option. The `onCreateNew` callback receives the typed string and must return a new `ComboboxOption`.',
753
+ {
754
+ render: () => <CreateNewTemplate />,
755
+ parameters: {
756
+ docs: {
757
+ source: {
758
+ code: `
759
+ function CreateNewExample() {
760
+ const [options, setOptions] = useState(initialOptions);
761
+
762
+ const handleCreate = (input: string) => {
763
+ const newOption = {
764
+ value: input.toLowerCase().replace(/\\s+/g, '-'),
765
+ label: input,
311
766
  };
312
- setOptions(prev => [...prev, newOpt]);
313
- return newOpt;
767
+ setOptions((prev) => [...prev, newOption]);
768
+ return newOption;
314
769
  };
315
770
 
316
- const handleDelete = (value: string) => {
317
- setOptions(prev => prev.filter(opt => opt.value !== value));
771
+ return (
772
+ <Combobox
773
+ options={options}
774
+ multiple
775
+ allowCreate
776
+ onCreateNew={handleCreate}
777
+ placeholder="Select or add a teacher..."
778
+ />
779
+ );
780
+ }
781
+ `.trim(),
782
+ },
783
+ },
784
+ },
785
+ },
786
+ );
787
+
788
+ // ─── Single select create template ────────────────────────────────────────────
789
+
790
+ function SingleSelectCreateTemplate() {
791
+ const [options, setOptions] = useState(comboboxPeopleOptions);
792
+
793
+ const handleCreate = (input: string) => {
794
+ const newOption = { value: input.toLowerCase().replace(/\s+/g, '-'), label: input };
795
+ setOptions(prev => [...prev, newOption]);
796
+ return newOption;
318
797
  };
319
798
 
320
799
  return (
321
800
  <Combobox
322
801
  options={options}
323
- placeholder="Search or create..."
324
802
  allowCreate
325
803
  onCreateNew={handleCreate}
326
- onDeleteCreated={handleDelete}
804
+ placeholder="Select or add a teacher..."
327
805
  />
328
806
  );
329
- };
807
+ }
330
808
 
331
- export const SingleSelectCreate: Story = withDescription({
332
- render: () => <SingleSelectCreateTemplate />,
333
- }, 'Shows single-select creation where a newly created option becomes the current value immediately.');
809
+ // 19. SingleSelectCreate
810
+ export const SingleSelectCreate = withDescription(
811
+ '`allowCreate` works in single-select mode too useful for fields like "Add a subject" where the user can either pick from existing values or type a new one.',
812
+ {
813
+ render: () => <SingleSelectCreateTemplate />,
814
+ parameters: {
815
+ docs: {
816
+ source: {
817
+ code: `
818
+ function SingleSelectCreateExample() {
819
+ const [options, setOptions] = useState(initialOptions);
820
+
821
+ const handleCreate = (input: string) => {
822
+ const newOption = {
823
+ value: input.toLowerCase().replace(/\\s+/g, '-'),
824
+ label: input,
825
+ };
826
+ setOptions((prev) => [...prev, newOption]);
827
+ return newOption;
828
+ };
334
829
 
335
- export const CustomTagLabel: Story = withDescription({
336
- args: {
337
- options: [
338
- { value: 'u1', label: 'Alice Johnson (Year 10, Class A)', tagLabel: 'Alice J.', iconName: 'user' },
339
- { value: 'u2', label: 'Bob Smith (Year 11, Class B)', tagLabel: 'Bob S.', iconName: 'user' },
340
- { value: 'u3', label: 'Charlie Brown (Year 10, Class A)', tagLabel: 'Charlie B.', iconName: 'user' },
341
- ],
342
- multiple: true,
343
- placeholder: 'Search students...',
344
- showClearAll: true,
830
+ return (
831
+ <Combobox
832
+ options={options}
833
+ allowCreate
834
+ onCreateNew={handleCreate}
835
+ placeholder="Select or add a teacher..."
836
+ />
837
+ );
838
+ }
839
+ `.trim(),
840
+ },
841
+ },
842
+ },
345
843
  },
346
- }, 'Uses `tagLabel` to shorten selected chip text while preserving fuller option labels in the dropdown.');
844
+ );
845
+
846
+ // 20. CustomOptionLayout
847
+ export const CustomOptionLayout = withDescription(
848
+ 'Use `renderOption` to fully customise how each option is rendered inside the dropdown — useful for adding avatars, icons, or additional metadata alongside the label.',
849
+ {
850
+ args: {
851
+ options: comboboxPeopleOptions,
852
+ multiple: true,
853
+ placeholder: 'Select teachers...',
854
+ renderOption: (option: ComboboxOption) => (
855
+ <span style={{ display: 'flex', alignItems: 'center', gap: 'var(--size-space-200)' }}>
856
+ <Icon name="user" size={16} />
857
+ <span>{option.label}</span>
858
+ </span>
859
+ ),
860
+ },
861
+ parameters: {
862
+ docs: {
863
+ source: {
864
+ code: `
865
+ <Combobox
866
+ options={teacherOptions}
867
+ multiple
868
+ placeholder="Select teachers..."
869
+ renderOption={(option) => (
870
+ <span style={{ display: 'flex', alignItems: 'center', gap: 'var(--size-space-200)' }}>
871
+ <Icon name="user" size={16} />
872
+ <span>{option.label}</span>
873
+ </span>
874
+ )}
875
+ />
876
+ `.trim(),
877
+ },
878
+ },
879
+ },
880
+ },
881
+ );
882
+
883
+ // 21. CustomTagLabel
884
+ export const CustomTagLabel = withDescription(
885
+ 'Use `getTagLabel` to customise the label shown inside a selected tag chip — useful when the full option label is too long to display comfortably in the trigger.',
886
+ {
887
+ args: {
888
+ options: comboboxPeopleOptions,
889
+ multiple: true,
890
+ placeholder: 'Select teachers...',
891
+ getTagLabel: (option: ComboboxOption) => option.label.split(' ')[0] ?? option.label,
892
+ },
893
+ parameters: {
894
+ docs: {
895
+ source: {
896
+ code: `
897
+ <Combobox
898
+ options={teacherOptions}
899
+ multiple
900
+ placeholder="Select teachers..."
901
+ getTagLabel={(option) => option.label.split(' ')[0]}
902
+ />
903
+ `.trim(),
904
+ },
905
+ },
906
+ },
907
+ },
908
+ );
909
+
910
+ // 22. WithDisabledOptions
911
+ export const WithDisabledOptions = withDescription(
912
+ 'Individual options can be disabled by setting `disabled: true` on the option object. Disabled options are visible but cannot be selected.',
913
+ {
914
+ args: {
915
+ options: comboboxPeopleOptions.map((o, i) => ({ ...o, disabled: i % 3 === 1 })),
916
+ multiple: true,
917
+ placeholder: 'Select teachers...',
918
+ },
919
+ parameters: {
920
+ docs: {
921
+ source: {
922
+ code: `
923
+ const options = [
924
+ { value: 'alice', label: 'Alice Johnson' },
925
+ { value: 'bob', label: 'Bob Smith', disabled: true },
926
+ { value: 'carol', label: 'Carol White' },
927
+ { value: 'dan', label: 'Dan Brown', disabled: true },
928
+ ];
929
+
930
+ <Combobox options={options} multiple placeholder="Select teachers..." />
931
+ `.trim(),
932
+ },
933
+ },
934
+ },
935
+ },
936
+ );
937
+
938
+ // 23. ScrollableLongList
939
+ export const ScrollableLongList = withDescription(
940
+ 'The dropdown scrolls automatically when there are many options. Here we render 50 generated options to demonstrate the scrollable list and confirm search still works at scale.',
941
+ {
942
+ args: {
943
+ options: Array.from({ length: 50 }, (_, i) => ({
944
+ value: `option-${i}`,
945
+ label: `Option ${i + 1}`,
946
+ })),
947
+ multiple: true,
948
+ placeholder: 'Search 50 options...',
949
+ },
950
+ parameters: {
951
+ docs: {
952
+ source: {
953
+ code: `
954
+ const options = Array.from({ length: 50 }, (_, i) => ({
955
+ value: \`option-\${i}\`,
956
+ label: \`Option \${i + 1}\`,
957
+ }));
347
958
 
348
- export const WithDisabledOptions: Story = withDescription({
349
- args: {
350
- options: [
351
- { value: 'alice', label: 'Alice Johnson', iconName: 'user' },
352
- { value: 'bob', label: 'Bob Smith', iconName: 'user', disabled: true },
353
- { value: 'charlie', label: 'Charlie Brown', iconName: 'user' },
354
- ],
355
- multiple: true,
356
- placeholder: 'Search people...',
959
+ <Combobox options={options} multiple placeholder="Search 50 options..." />
960
+ `.trim(),
961
+ },
962
+ },
963
+ },
357
964
  },
358
- }, 'Includes a disabled option to show how unavailable rows appear and behave inside the listbox.');
965
+ );