@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,37 +1,974 @@
1
- import type { Meta } from '@storybook/react-vite';
2
- import { fn } from 'storybook/test';
3
- import { Modal, type ModalProps } from './Modal';
4
- import { Button } from 'Components/button/Button';
5
1
  import { useState } from 'react';
2
+ import type { Meta, StoryObj } from '@storybook/react-vite';
3
+ import {
4
+ Controls,
5
+ Heading as DocHeading,
6
+ Markdown,
7
+ Primary as DocPrimary,
8
+ Stories,
9
+ Subtitle,
10
+ Title,
11
+ } from '@storybook/addon-docs/blocks';
12
+ import { Modal } from './Modal';
13
+ import { Button } from 'Components/button/Button';
14
+ import { FormField } from 'Components/formField/FormField';
15
+ import { Dropdown } from 'Components/dropdown/Dropdown';
16
+
17
+ // ---------------------------------------------------------------------------
18
+ // Docs page content
19
+ // ---------------------------------------------------------------------------
20
+
21
+ const DESCRIPTION_INTRO = [
22
+ 'Modal is a focused, interruptive dialog that blocks all background interaction until the user',
23
+ 'takes an explicit action or dismisses it.',
24
+ '',
25
+ '> **Tip:** For programmatic modal control — opening modals from event handlers, PubSub events,',
26
+ '> or outside the React tree — use [`ModalManager`](?path=/docs/components-modals-modalmanager--docs)',
27
+ '> with `ModalUtils.addModal()` instead of wiring `open` state manually.',
28
+ '',
29
+ '> **Built on [Radix UI Dialog](https://www.radix-ui.com/primitives/docs/components/dialog).**',
30
+ '> Modal renders via a portal. The overlay registers itself as a `PopupParentContext`, so any',
31
+ '> floating elements (Dropdown, Tooltip) inside the modal automatically portal into the overlay.',
32
+ ].join('\n');
33
+
34
+ const USAGE_GUIDANCE = [
35
+ '### When to use',
36
+ '',
37
+ '- The user must make a decision or complete a short task before continuing (e.g. a delete',
38
+ ' confirmation, a quick-add form)',
39
+ '- You need to fully interrupt the flow to prevent data loss or an irreversible action',
40
+ '- The content is self-contained and does not require the user to reference the underlying page',
41
+ '- Multi-step flows where each step is short and sequential',
42
+ '',
43
+ '---',
44
+ '',
45
+ '### When NOT to use',
46
+ '',
47
+ '| Situation | Use instead |',
48
+ '|---|---|',
49
+ '| User needs to reference the background page while editing | [Slideover](?path=/docs/components-modals-slideover--docs) |',
50
+ '| Non-blocking contextual info anchored to a trigger | [Dropdown](?path=/docs/components-dropdown--docs) |',
51
+ '| Brief status feedback that auto-dismisses | [Toast](?path=/docs/components-toast--docs) |',
52
+ '| Content too complex for a dialog | A dedicated page or Slideover |',
53
+ ].join('\n');
54
+
55
+ const DEVELOPER_NOTES = [
56
+ '### Critical usage patterns',
57
+ '',
58
+ '**`closeHandler` gates everything.** With no `closeHandler`:',
59
+ '- The X close button is **not rendered** (even if `hideCloseButton` is `false`)',
60
+ '- Pressing **Escape** does nothing',
61
+ '- Clicking the overlay does nothing (overlay clicks never dismiss — intentional)',
62
+ '',
63
+ 'A modal without `closeHandler` can only be closed by action buttons you render inside it. Use',
64
+ 'this pattern for destructive confirmations where an accidental dismiss would be dangerous.',
65
+ '',
66
+ '**`hideCloseButton` vs no `closeHandler`:**',
67
+ '',
68
+ '| Scenario | `closeHandler` | `hideCloseButton` | Result |',
69
+ '|---|---|---|---|',
70
+ '| Standard dismissible modal | provided | `false` | X button shown; Escape works |',
71
+ '| Escape only, no visible X | provided | `true` | X button hidden; Escape still works |',
72
+ '| Forced decision (no escape route) | omitted | n/a | No X, no Escape, no overlay dismiss |',
73
+ '',
74
+ '**`title` prop shortcut.** Passing `title="My Title"` auto-renders `Modal.Header` + `Modal.Title`.',
75
+ 'Do **not** also render a manual `<Modal.Header>` — you will get a double header.',
76
+ '',
77
+ '**Width is consumer-controlled.** There is no `size` prop. Pass `className` to control width.',
78
+ 'Standard modals: 480–600 px; delete confirmations: 400–480 px.',
79
+ '',
80
+ '---',
81
+ '',
82
+ '### Accessibility',
83
+ '',
84
+ '- The dialog root receives `role="dialog"` and `aria-modal="true"` (Radix UI)',
85
+ '- `Modal.Title` renders an `<h2>` and is linked to the dialog via `aria-labelledby`',
86
+ '- Focus is **trapped** inside the modal — Tab and Shift+Tab cycle only within the modal',
87
+ '- Focus returns to the trigger element when the modal closes',
88
+ '- Always provide `Modal.Title` (or the `title` prop) — a dialog without a label breaks screen',
89
+ ' reader announcement',
90
+ '',
91
+ '---',
92
+ '',
93
+ '### Sub-component props',
94
+ '',
95
+ '| Sub-component | Props | Description |',
96
+ '|---|---|---|',
97
+ '| `Modal.Header` | `className?`, `children?` | Flex row header bar — put `Modal.Title` here |',
98
+ '| `Modal.Body` | `className?`, `children?` | Scrollable content area with grey background |',
99
+ '| `Modal.Footer` | `className?`, `children?` | Flex row footer — put action `Button`s here |',
100
+ '| `Modal.Title` | `className?`, `children?`, all `DialogTitleProps` | `<h2>` dialog title — required for accessibility |',
101
+ '| `Modal.CloseButton` | all `ButtonProps` | Reads `closeHandler` from context — renders `null` if no `closeHandler` |',
102
+ '',
103
+ '---',
104
+ '',
105
+ '### Design tokens used',
106
+ '',
107
+ '| Token | Usage |',
108
+ '|---|---|',
109
+ '| `--modal-overlay-color-background` | Overlay backdrop colour |',
110
+ '| `--modal-color-background` | Dialog box background |',
111
+ '| `--modal-radius` | Dialog border radius |',
112
+ '| `--modal-header-color-background` | Header section background |',
113
+ '| `--modal-header-spacing-padding` | Header padding |',
114
+ '| `--modal-footer-color-background` | Footer section background |',
115
+ '| `--modal-footer-spacing-padding` | Footer padding |',
116
+ ].join('\n');
117
+
118
+ const RELATED_COMPONENTS = [
119
+ '## Related components',
120
+ '',
121
+ '[Slideover](?path=/docs/components-modals-slideover--docs) · [Dropdown](?path=/docs/components-dropdown--docs) · [Toast](?path=/docs/components-toast--docs) · [Button](?path=/docs/components-button--docs)',
122
+ ].join('\n');
123
+
124
+ const PROPS_INTRO = 'The preview below is wired to the **Controls** panel — tweak any prop to see the story update in real time.';
125
+
126
+ // ---------------------------------------------------------------------------
127
+ // Docs page
128
+ // ---------------------------------------------------------------------------
6
129
 
7
- const meta: Meta<typeof Modal> = {
130
+ function ModalDocsPage() {
131
+ return (
132
+ <>
133
+ <Title />
134
+ <Subtitle />
135
+ <Markdown>{DESCRIPTION_INTRO}</Markdown>
136
+ <DocHeading>Interactive example</DocHeading>
137
+ <Markdown>{PROPS_INTRO}</Markdown>
138
+ <DocPrimary />
139
+ <Controls />
140
+ <DocHeading>Usage guidance</DocHeading>
141
+ <Markdown>{USAGE_GUIDANCE}</Markdown>
142
+ <DocHeading>Developer notes</DocHeading>
143
+ <Markdown>{DEVELOPER_NOTES}</Markdown>
144
+ <DocHeading>Examples</DocHeading>
145
+ <Stories title="" />
146
+ <Markdown>{RELATED_COMPONENTS}</Markdown>
147
+ </>
148
+ );
149
+ }
150
+
151
+ // ---------------------------------------------------------------------------
152
+ // Meta
153
+ // ---------------------------------------------------------------------------
154
+
155
+ const meta = {
8
156
  title: 'Components/Modals/Modal',
9
157
  component: Modal,
158
+ parameters: {
159
+ layout: 'padded',
160
+ docs: {
161
+ page: ModalDocsPage,
162
+ },
163
+ },
10
164
  tags: ['autodocs'],
165
+ argTypes: {
166
+ open: {
167
+ control: 'boolean',
168
+ description:
169
+ 'Controls whether the modal is visible. Pair with `closeHandler` for a fully controlled component.',
170
+ table: {
171
+ type: { summary: 'boolean' },
172
+ defaultValue: { summary: 'undefined' },
173
+ },
174
+ },
175
+ closeHandler: {
176
+ control: false,
177
+ description: [
178
+ 'Callback fired when the modal requests to close (Escape key or X button).',
179
+ '**Omitting this prop disables all dismiss mechanisms** — no X button, no Escape key.',
180
+ ].join(' '),
181
+ table: {
182
+ type: { summary: '() => void' },
183
+ defaultValue: { summary: 'undefined' },
184
+ },
185
+ },
186
+ hideCloseButton: {
187
+ control: 'boolean',
188
+ description:
189
+ 'Hides the X close button while keeping Escape-key dismiss active (requires `closeHandler`).',
190
+ table: {
191
+ type: { summary: 'boolean' },
192
+ defaultValue: { summary: 'false' },
193
+ },
194
+ },
195
+ title: {
196
+ control: 'text',
197
+ description: [
198
+ 'Shortcut that auto-renders `Modal.Header` + `Modal.Title`.',
199
+ 'Do **not** combine with a manual `<Modal.Header>` — you will get a double header.',
200
+ ].join(' '),
201
+ table: {
202
+ type: { summary: 'string' },
203
+ defaultValue: { summary: 'undefined' },
204
+ },
205
+ },
206
+ className: {
207
+ control: 'text',
208
+ description:
209
+ 'CSS class applied to the dialog element. Use this to control modal width — there is no `size` prop.',
210
+ table: {
211
+ type: { summary: 'string' },
212
+ defaultValue: { summary: 'undefined' },
213
+ },
214
+ },
215
+ overlayClassName: {
216
+ control: 'text',
217
+ description: 'CSS class applied to the overlay / backdrop element.',
218
+ table: {
219
+ type: { summary: 'string' },
220
+ defaultValue: { summary: 'undefined' },
221
+ },
222
+ },
223
+ portalTarget: {
224
+ control: false,
225
+ description:
226
+ 'DOM element into which the modal portal mounts. Defaults to `PopupParentContext` value (`div#ds-popup-parent` in body).',
227
+ table: {
228
+ type: { summary: 'HTMLElement | null' },
229
+ defaultValue: { summary: 'undefined' },
230
+ },
231
+ },
232
+ },
233
+ } satisfies Meta<typeof Modal>;
234
+
235
+ export default meta;
236
+ type Story = StoryObj<typeof meta>;
237
+
238
+ // ---------------------------------------------------------------------------
239
+ // Helper
240
+ // ---------------------------------------------------------------------------
241
+
242
+ const withDescription = (story: Story, description: string): Story => ({
243
+ ...story,
244
+ parameters: {
245
+ ...story.parameters,
246
+ docs: {
247
+ ...story.parameters?.docs,
248
+ description: {
249
+ story: description,
250
+ },
251
+ },
252
+ },
253
+ });
254
+
255
+ // ---------------------------------------------------------------------------
256
+ // Templates
257
+ // ---------------------------------------------------------------------------
258
+
259
+ const DefaultTemplate = () => {
260
+ const [open, setOpen] = useState(false);
261
+ return (
262
+ <>
263
+ <Button variant="primary" onClick={() => setOpen(true)}>
264
+ Add Assessment Period
265
+ </Button>
266
+ <Modal open={open} closeHandler={() => setOpen(false)} title="Add Assessment Period">
267
+ <Modal.Body>
268
+ <p>
269
+ Create a new assessment period for the current academic year. Assessment periods define
270
+ the date ranges used when recording and reporting pupil attainment data.
271
+ </p>
272
+ </Modal.Body>
273
+ <Modal.Footer>
274
+ <Button variant="secondary" onClick={() => setOpen(false)}>
275
+ Cancel
276
+ </Button>
277
+ <Button variant="primary" onClick={() => setOpen(false)}>
278
+ Add Assessment Period
279
+ </Button>
280
+ </Modal.Footer>
281
+ </Modal>
282
+ </>
283
+ );
284
+ };
285
+
286
+ const NoCloseHandlerTemplate = () => {
287
+ const [open, setOpen] = useState(false);
288
+ return (
289
+ <>
290
+ <Button variant="primary" onClick={() => setOpen(true)}>
291
+ Open Session Timeout Warning
292
+ </Button>
293
+ <Modal open={open} title="Your session is about to expire">
294
+ <Modal.Body>
295
+ <p>
296
+ For security reasons, your session will expire in 2 minutes due to inactivity. Choose an
297
+ option below to continue — you cannot dismiss this dialog by pressing Escape or clicking
298
+ outside it.
299
+ </p>
300
+ </Modal.Body>
301
+ <Modal.Footer>
302
+ <Button variant="secondary" onClick={() => setOpen(false)}>
303
+ Log out now
304
+ </Button>
305
+ <Button variant="primary" onClick={() => setOpen(false)}>
306
+ Stay logged in
307
+ </Button>
308
+ </Modal.Footer>
309
+ </Modal>
310
+ </>
311
+ );
312
+ };
313
+
314
+ const HideCloseButtonTemplate = () => {
315
+ const [open, setOpen] = useState(false);
316
+ return (
317
+ <>
318
+ <Button variant="primary" onClick={() => setOpen(true)}>
319
+ Review Data Export
320
+ </Button>
321
+ <Modal
322
+ open={open}
323
+ closeHandler={() => setOpen(false)}
324
+ hideCloseButton
325
+ title="Export Pupil Data"
326
+ >
327
+ <Modal.Body>
328
+ <p>
329
+ You are about to export sensitive pupil data. Please review the export settings before
330
+ proceeding. Press
331
+ {' '}
332
+ <strong>Escape</strong>
333
+ {' '}
334
+ or select an action button to close this
335
+ dialog — the X button is intentionally hidden for this flow.
336
+ </p>
337
+ </Modal.Body>
338
+ <Modal.Footer>
339
+ <Button variant="secondary" onClick={() => setOpen(false)}>
340
+ Cancel
341
+ </Button>
342
+ <Button variant="primary" onClick={() => setOpen(false)}>
343
+ Export Data
344
+ </Button>
345
+ </Modal.Footer>
346
+ </Modal>
347
+ </>
348
+ );
349
+ };
350
+
351
+ const ManualHeaderCompositionTemplate = () => {
352
+ const [open, setOpen] = useState(false);
353
+ return (
354
+ <>
355
+ <Button variant="primary" onClick={() => setOpen(true)}>
356
+ Edit Year Group Settings
357
+ </Button>
358
+ {/* hideCloseButton suppresses the auto top-right X so the manual one in the header is the only dismiss button */}
359
+ <Modal open={open} closeHandler={() => setOpen(false)} hideCloseButton>
360
+ <Modal.Header>
361
+ <div
362
+ style={{
363
+ display: 'flex',
364
+ flexDirection: 'column',
365
+ gap: 'var(--spacing-xsmall)',
366
+ flex: 1,
367
+ }}
368
+ >
369
+ <Modal.Title>Year Group Settings</Modal.Title>
370
+ <p style={{ color: 'var(--color-grey-600)', margin: 0, fontSize: 'var(--font-size-2-13)' }}>
371
+ Year 9 · Autumn Term 2024
372
+ </p>
373
+ </div>
374
+ <Modal.CloseButton />
375
+ </Modal.Header>
376
+ <Modal.Body>
377
+ <p>
378
+ Adjust the settings for this year group. Changes will apply to all classes within Year 9
379
+ for the current term.
380
+ </p>
381
+ </Modal.Body>
382
+ <Modal.Footer>
383
+ <Button variant="secondary" onClick={() => setOpen(false)}>
384
+ Cancel
385
+ </Button>
386
+ <Button variant="primary" onClick={() => setOpen(false)}>
387
+ Save Settings
388
+ </Button>
389
+ </Modal.Footer>
390
+ </Modal>
391
+ </>
392
+ );
393
+ };
394
+
395
+ const LongScrollingContentTemplate = () => {
396
+ const [open, setOpen] = useState(false);
397
+ return (
398
+ <>
399
+ <Button variant="primary" onClick={() => setOpen(true)}>
400
+ View Attendance Policy
401
+ </Button>
402
+ <Modal open={open} closeHandler={() => setOpen(false)} title="Attendance Policy">
403
+ <Modal.Body>
404
+ {Array.from({ length: 12 }).map((_, i) => (
405
+ <p key={i} style={{ marginBottom: 'var(--spacing-large)' }}>
406
+ Section
407
+ {' '}
408
+ {i + 1}
409
+ : Attendance records must be completed by 9:30 am each school day.
410
+ Persistent absence is defined as missing 10% or more of sessions. Parents and carers
411
+ must be notified when a pupil&apos;s attendance falls below 90%. Schools are required
412
+ to keep accurate records for inspection and reporting purposes.
413
+ </p>
414
+ ))}
415
+ </Modal.Body>
416
+ <Modal.Footer>
417
+ <Button variant="secondary" onClick={() => setOpen(false)}>
418
+ Close
419
+ </Button>
420
+ <Button variant="primary" onClick={() => setOpen(false)}>
421
+ Acknowledge Policy
422
+ </Button>
423
+ </Modal.Footer>
424
+ </Modal>
425
+ </>
426
+ );
427
+ };
428
+
429
+ const WithFormContentTemplate = () => {
430
+ const [open, setOpen] = useState(false);
431
+ const [firstName, setFirstName] = useState('');
432
+ const [lastName, setLastName] = useState('');
433
+ const [yearGroup, setYearGroup] = useState('');
434
+
435
+ const handleSave = () => {
436
+ setOpen(false);
437
+ setFirstName('');
438
+ setLastName('');
439
+ setYearGroup('');
440
+ };
441
+
442
+ return (
443
+ <>
444
+ <Button variant="primary" onClick={() => setOpen(true)}>
445
+ Add New Pupil
446
+ </Button>
447
+ <Modal open={open} closeHandler={() => setOpen(false)} title="Add New Pupil">
448
+ <Modal.Body>
449
+ <p style={{ marginBottom: 'var(--spacing-large)' }}>
450
+ Enter the pupil&apos;s details below. Tab through fields to navigate — focus is trapped
451
+ within this dialog.
452
+ </p>
453
+ <FormField
454
+ label="First name"
455
+ id="pupil-first-name"
456
+ inputType="text"
457
+ inputProps={{
458
+ value: firstName,
459
+ onChange: e => setFirstName(e.target.value),
460
+ placeholder: 'e.g. Emily',
461
+ }}
462
+ />
463
+ <FormField
464
+ label="Last name"
465
+ id="pupil-last-name"
466
+ inputType="text"
467
+ inputProps={{
468
+ value: lastName,
469
+ onChange: e => setLastName(e.target.value),
470
+ placeholder: 'e.g. Clarke',
471
+ }}
472
+ />
473
+ <FormField
474
+ label="Year group"
475
+ id="pupil-year-group"
476
+ inputType="text"
477
+ inputProps={{
478
+ value: yearGroup,
479
+ onChange: e => setYearGroup(e.target.value),
480
+ placeholder: 'e.g. Year 7',
481
+ }}
482
+ />
483
+ </Modal.Body>
484
+ <Modal.Footer>
485
+ <Button variant="secondary" onClick={() => setOpen(false)}>
486
+ Cancel
487
+ </Button>
488
+ <Button variant="primary" onClick={handleSave}>
489
+ Add Pupil
490
+ </Button>
491
+ </Modal.Footer>
492
+ </Modal>
493
+ </>
494
+ );
495
+ };
496
+
497
+ const DeleteConfirmationTemplate = () => {
498
+ const [open, setOpen] = useState(false);
499
+ return (
500
+ <>
501
+ <Button variant="secondary-destructive" onClick={() => setOpen(true)}>
502
+ Remove Pupil
503
+ </Button>
504
+ {/* No closeHandler — the user must explicitly choose Cancel or Remove */}
505
+ <Modal open={open} title="Remove Emily Clarke?">
506
+ <Modal.Body>
507
+ <p style={{ marginBottom: 'var(--spacing-small)' }}>
508
+ You are about to permanently remove
509
+ {' '}
510
+ <strong>Emily Clarke (Year 9)</strong>
511
+ {' '}
512
+ from the
513
+ register. This action cannot be undone.
514
+ </p>
515
+ <p>
516
+ All associated assessment records, attendance data, and report history will be archived
517
+ and will no longer appear in active reports.
518
+ </p>
519
+ </Modal.Body>
520
+ <Modal.Footer>
521
+ <Button variant="secondary" onClick={() => setOpen(false)}>
522
+ Cancel
523
+ </Button>
524
+ <Button variant="primary-destructive" onClick={() => setOpen(false)}>
525
+ Remove Pupil
526
+ </Button>
527
+ </Modal.Footer>
528
+ </Modal>
529
+ </>
530
+ );
11
531
  };
12
532
 
13
- export const Default = {
14
- args: {
15
- open: false,
16
- closeHandler: fn(),
17
- title: 'Modal Title',
533
+ const WithDropdownInsideModalTemplate = () => {
534
+ const [open, setOpen] = useState(false);
535
+ return (
536
+ <>
537
+ <Button variant="primary" onClick={() => setOpen(true)}>
538
+ Assign Class Teacher
539
+ </Button>
540
+ <Modal open={open} closeHandler={() => setOpen(false)} title="Assign Class Teacher">
541
+ <Modal.Body>
542
+ <p style={{ marginBottom: 'var(--spacing-large)' }}>
543
+ Select a teacher to assign as the lead for this class. The dropdown portals into the
544
+ modal overlay automatically — no additional configuration required.
545
+ </p>
546
+ <Dropdown>
547
+ <Dropdown.Trigger>
548
+ <Button variant="dropdown">Select a teacher</Button>
549
+ </Dropdown.Trigger>
550
+ <Dropdown.Content>
551
+ <Dropdown.Item onSelect={() => {}}>Ms A. Johnson — English</Dropdown.Item>
552
+ <Dropdown.Item onSelect={() => {}}>Mr B. Patel — Mathematics</Dropdown.Item>
553
+ <Dropdown.Item onSelect={() => {}}>Mrs C. Williams — Science</Dropdown.Item>
554
+ <Dropdown.Item onSelect={() => {}}>Mr D. Thompson — History</Dropdown.Item>
555
+ <Dropdown.Item onSelect={() => {}}>Ms E. Okonkwo — Art</Dropdown.Item>
556
+ </Dropdown.Content>
557
+ </Dropdown>
558
+ </Modal.Body>
559
+ <Modal.Footer>
560
+ <Button variant="secondary" onClick={() => setOpen(false)}>
561
+ Cancel
562
+ </Button>
563
+ <Button variant="primary" onClick={() => setOpen(false)}>
564
+ Assign Teacher
565
+ </Button>
566
+ </Modal.Footer>
567
+ </Modal>
568
+ </>
569
+ );
570
+ };
571
+
572
+ // ---------------------------------------------------------------------------
573
+ // Stories
574
+ // ---------------------------------------------------------------------------
575
+
576
+ export const Default: Story = withDescription(
577
+ {
578
+ parameters: {
579
+ controls: { disable: true },
580
+ docs: {
581
+ source: {
582
+ language: 'tsx',
583
+ code: `
584
+ import { useState } from 'react';
585
+ import { Button, Modal } from '@arbor-education/design-system.components';
586
+
587
+ function AddAssessmentPeriodModal() {
588
+ const [open, setOpen] = useState(false);
589
+ return (
590
+ <>
591
+ <Button variant="primary" onClick={() => setOpen(true)}>
592
+ Add Assessment Period
593
+ </Button>
594
+ <Modal open={open} closeHandler={() => setOpen(false)} title="Add Assessment Period">
595
+ <Modal.Body>
596
+ <p>
597
+ Create a new assessment period for the current academic year.
598
+ </p>
599
+ </Modal.Body>
600
+ <Modal.Footer>
601
+ <Button variant="secondary" onClick={() => setOpen(false)}>Cancel</Button>
602
+ <Button variant="primary" onClick={() => setOpen(false)}>Add Assessment Period</Button>
603
+ </Modal.Footer>
604
+ </Modal>
605
+ </>
606
+ );
607
+ }
608
+ export default AddAssessmentPeriodModal;
609
+ `.trim(),
610
+ },
611
+ },
612
+ },
613
+ render: () => <DefaultTemplate />,
18
614
  },
19
- render: (args: ModalProps) => {
20
- const [showModal, setShowModal] = useState(args.open);
21
- return (
22
- <>
23
- <Button type="primary" onClick={() => setShowModal(true)}>Open Modal</Button>
24
- <Modal {...args} open={showModal} closeHandler={() => setShowModal(false)}>
25
- <Modal.Body>Cheddar and Pickle were Norwich terriers who lived next door. Cheddar, the older, was cautious and methodical; Pickle, the younger, was bold and impulsive. One morning, Pickle spotted a squirrel in the garden and gave chase, while Cheddar watched from the porch. When Pickle got stuck under the neighbor’s fence, Cheddar squeezed through a gap and nudged him free. From then on, they worked as a team: Pickle scouted, and Cheddar planned. They became inseparable, exploring the neighborhood together and always looking out for each other.</Modal.Body>
26
- <Modal.Footer>
27
- <Button variant="tertiary" onClick={() => setShowModal(false)}>Close</Button>
28
- <Button variant="primary" onClick={() => setShowModal(false)}>Primary Action</Button>
29
- </Modal.Footer>
30
- </Modal>
31
- </>
615
+ 'The standard controlled modal pattern. A trigger button opens the modal; `closeHandler` enables the X button and Escape key. The `title` prop auto-renders `Modal.Header` + `Modal.Title` with no manual composition needed.',
616
+ );
32
617
 
33
- );
618
+ export const NoCloseHandler: Story = withDescription(
619
+ {
620
+ parameters: {
621
+ controls: { disable: true },
622
+ docs: {
623
+ source: {
624
+ language: 'tsx',
625
+ code: `
626
+ import { useState } from 'react';
627
+ import { Button, Modal } from '@arbor-education/design-system.components';
628
+
629
+ // No closeHandler — no X button, no Escape key, no overlay dismiss.
630
+ // The user MUST choose an action button.
631
+ function SessionTimeoutModal() {
632
+ const [open, setOpen] = useState(false);
633
+ return (
634
+ <>
635
+ <Button variant="primary" onClick={() => setOpen(true)}>
636
+ Open Session Timeout Warning
637
+ </Button>
638
+ <Modal open={open} title="Your session is about to expire">
639
+ <Modal.Body>
640
+ <p>
641
+ For security reasons, your session will expire in 2 minutes.
642
+ Choose an option below to continue.
643
+ </p>
644
+ </Modal.Body>
645
+ <Modal.Footer>
646
+ <Button variant="secondary" onClick={() => setOpen(false)}>Log out now</Button>
647
+ <Button variant="primary" onClick={() => setOpen(false)}>Stay logged in</Button>
648
+ </Modal.Footer>
649
+ </Modal>
650
+ </>
651
+ );
652
+ }
653
+ export default SessionTimeoutModal;
654
+ `.trim(),
655
+ },
656
+ },
657
+ },
658
+ render: () => <NoCloseHandlerTemplate />,
34
659
  },
35
- };
660
+ [
661
+ '**No `closeHandler` provided.** The X button is not rendered, Escape does nothing, and clicking',
662
+ 'the overlay does nothing. The user must select one of the action buttons. Use this pattern for',
663
+ 'flows where an accidental dismiss would be confusing or dangerous — session timeouts, required',
664
+ 'acknowledgements, and critical confirmations.',
665
+ ].join(' '),
666
+ );
36
667
 
37
- export default meta;
668
+ export const HideCloseButton: Story = withDescription(
669
+ {
670
+ parameters: {
671
+ controls: { disable: true },
672
+ docs: {
673
+ source: {
674
+ language: 'tsx',
675
+ code: `
676
+ import { useState } from 'react';
677
+ import { Button, Modal } from '@arbor-education/design-system.components';
678
+
679
+ function ExportModal() {
680
+ const [open, setOpen] = useState(false);
681
+ return (
682
+ <>
683
+ <Button variant="primary" onClick={() => setOpen(true)}>Review Data Export</Button>
684
+ <Modal
685
+ open={open}
686
+ closeHandler={() => setOpen(false)}
687
+ hideCloseButton
688
+ title="Export Pupil Data"
689
+ >
690
+ <Modal.Body>
691
+ <p>Review the export settings below. Press Escape to cancel.</p>
692
+ </Modal.Body>
693
+ <Modal.Footer>
694
+ <Button variant="secondary" onClick={() => setOpen(false)}>Cancel</Button>
695
+ <Button variant="primary" onClick={() => setOpen(false)}>Export Data</Button>
696
+ </Modal.Footer>
697
+ </Modal>
698
+ </>
699
+ );
700
+ }
701
+ export default ExportModal;
702
+ `.trim(),
703
+ },
704
+ },
705
+ },
706
+ render: () => <HideCloseButtonTemplate />,
707
+ },
708
+ [
709
+ '`hideCloseButton` removes the top-right X button while keeping the Escape key active (because',
710
+ '`closeHandler` is still provided). Use this for flows that want a cleaner look without a visible',
711
+ 'close affordance — the user can still press Escape to dismiss.',
712
+ ].join(' '),
713
+ );
714
+
715
+ export const ManualHeaderComposition: Story = withDescription(
716
+ {
717
+ parameters: {
718
+ controls: { disable: true },
719
+ docs: {
720
+ source: {
721
+ language: 'tsx',
722
+ code: `
723
+ import { useState } from 'react';
724
+ import { Button, Modal } from '@arbor-education/design-system.components';
725
+
726
+ function YearGroupSettingsModal() {
727
+ const [open, setOpen] = useState(false);
728
+ return (
729
+ <>
730
+ <Button variant="primary" onClick={() => setOpen(true)}>Edit Year Group Settings</Button>
731
+ {/* hideCloseButton suppresses the auto top-right X; the manual CloseButton in the header takes its place */}
732
+ <Modal open={open} closeHandler={() => setOpen(false)} hideCloseButton>
733
+ <Modal.Header>
734
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--spacing-xsmall)', flex: 1 }}>
735
+ <Modal.Title>Year Group Settings</Modal.Title>
736
+ <p style={{ color: 'var(--color-grey-600)', margin: 0 }}>
737
+ Year 9 · Autumn Term 2024
738
+ </p>
739
+ </div>
740
+ <Modal.CloseButton />
741
+ </Modal.Header>
742
+ <Modal.Body>
743
+ <p>Adjust the settings for this year group.</p>
744
+ </Modal.Body>
745
+ <Modal.Footer>
746
+ <Button variant="secondary" onClick={() => setOpen(false)}>Cancel</Button>
747
+ <Button variant="primary" onClick={() => setOpen(false)}>Save Settings</Button>
748
+ </Modal.Footer>
749
+ </Modal>
750
+ </>
751
+ );
752
+ }
753
+ export default YearGroupSettingsModal;
754
+ `.trim(),
755
+ },
756
+ },
757
+ },
758
+ render: () => <ManualHeaderCompositionTemplate />,
759
+ },
760
+ [
761
+ 'Full header composition using `Modal.Header` + `Modal.Title` instead of the `title` prop shortcut.',
762
+ 'Use this pattern when the header needs more than a plain title string — a subtitle, an icon, or',
763
+ 'custom header controls. Pass `hideCloseButton` on `<Modal>` to suppress the auto top-right X,',
764
+ 'then place `<Modal.CloseButton />` inside the header where you want it.',
765
+ ].join(' '),
766
+ );
767
+
768
+ export const LongScrollingContent: Story = withDescription(
769
+ {
770
+ parameters: {
771
+ controls: { disable: true },
772
+ docs: {
773
+ source: {
774
+ language: 'tsx',
775
+ code: `
776
+ import { useState } from 'react';
777
+ import { Button, Modal } from '@arbor-education/design-system.components';
778
+
779
+ function AttendancePolicyModal() {
780
+ const [open, setOpen] = useState(false);
781
+ return (
782
+ <>
783
+ <Button variant="primary" onClick={() => setOpen(true)}>View Attendance Policy</Button>
784
+ <Modal open={open} closeHandler={() => setOpen(false)} title="Attendance Policy">
785
+ <Modal.Body>
786
+ {/* Long content — Modal.Body scrolls independently; header and footer stay pinned */}
787
+ {sections.map((text, i) => <p key={i}>{text}</p>)}
788
+ </Modal.Body>
789
+ <Modal.Footer>
790
+ <Button variant="secondary" onClick={() => setOpen(false)}>Close</Button>
791
+ <Button variant="primary" onClick={() => setOpen(false)}>Acknowledge Policy</Button>
792
+ </Modal.Footer>
793
+ </Modal>
794
+ </>
795
+ );
796
+ }
797
+ export default AttendancePolicyModal;
798
+ `.trim(),
799
+ },
800
+ },
801
+ },
802
+ render: () => <LongScrollingContentTemplate />,
803
+ },
804
+ [
805
+ '`Modal.Body` scrolls independently when content overflows — the header and footer remain pinned.',
806
+ 'The modal container enforces `max-height: calc(100vh - var(--spacing-medium))` so the dialog',
807
+ 'never exceeds the viewport. No extra CSS needed on the consumer side.',
808
+ ].join(' '),
809
+ );
810
+
811
+ export const WithFormContent: Story = withDescription(
812
+ {
813
+ parameters: {
814
+ controls: { disable: true },
815
+ docs: {
816
+ source: {
817
+ language: 'tsx',
818
+ code: `
819
+ import { useState } from 'react';
820
+ import { Button, FormField, Modal } from '@arbor-education/design-system.components';
821
+
822
+ function AddPupilModal() {
823
+ const [open, setOpen] = useState(false);
824
+ const [firstName, setFirstName] = useState('');
825
+ const [lastName, setLastName] = useState('');
826
+
827
+ return (
828
+ <>
829
+ <Button variant="primary" onClick={() => setOpen(true)}>Add New Pupil</Button>
830
+ <Modal open={open} closeHandler={() => setOpen(false)} title="Add New Pupil">
831
+ <Modal.Body>
832
+ <FormField
833
+ label="First name"
834
+ id="pupil-first-name"
835
+ inputType="text"
836
+ inputProps={{
837
+ value: firstName,
838
+ onChange: (e) => setFirstName(e.target.value),
839
+ placeholder: 'e.g. Emily',
840
+ }}
841
+ />
842
+ <FormField
843
+ label="Last name"
844
+ id="pupil-last-name"
845
+ inputType="text"
846
+ inputProps={{
847
+ value: lastName,
848
+ onChange: (e) => setLastName(e.target.value),
849
+ placeholder: 'e.g. Clarke',
850
+ }}
851
+ />
852
+ </Modal.Body>
853
+ <Modal.Footer>
854
+ <Button variant="secondary" onClick={() => setOpen(false)}>Cancel</Button>
855
+ <Button variant="primary" onClick={() => { setOpen(false); }}>Add Pupil</Button>
856
+ </Modal.Footer>
857
+ </Modal>
858
+ </>
859
+ );
860
+ }
861
+ export default AddPupilModal;
862
+ `.trim(),
863
+ },
864
+ },
865
+ },
866
+ render: () => <WithFormContentTemplate />,
867
+ },
868
+ [
869
+ 'Form fields inside `Modal.Body` demonstrating the focus trap — Tab and Shift+Tab cycle only',
870
+ 'within the open dialog. Radix UI Dialog handles this automatically; no additional `aria` or',
871
+ '`tabIndex` props are required. Use `FormField` with `inputType` + `inputProps` for consistent',
872
+ 'layout and accessibility wiring.',
873
+ ].join(' '),
874
+ );
875
+
876
+ export const DeleteConfirmation: Story = withDescription(
877
+ {
878
+ parameters: {
879
+ controls: { disable: true },
880
+ docs: {
881
+ source: {
882
+ language: 'tsx',
883
+ code: `
884
+ import { useState } from 'react';
885
+ import { Button, Modal } from '@arbor-education/design-system.components';
886
+
887
+ function RemovePupilConfirmation() {
888
+ const [open, setOpen] = useState(false);
889
+ return (
890
+ <>
891
+ <Button variant="secondary-destructive" onClick={() => setOpen(true)}>Remove Pupil</Button>
892
+ {/* No closeHandler — user must explicitly choose Cancel or Remove */}
893
+ <Modal open={open} title="Remove Emily Clarke?">
894
+ <Modal.Body>
895
+ <p>
896
+ You are about to permanently remove <strong>Emily Clarke (Year 9)</strong> from the
897
+ register. This action cannot be undone.
898
+ </p>
899
+ </Modal.Body>
900
+ <Modal.Footer>
901
+ <Button variant="secondary" onClick={() => setOpen(false)}>Cancel</Button>
902
+ <Button variant="primary-destructive" onClick={() => setOpen(false)}>Remove Pupil</Button>
903
+ </Modal.Footer>
904
+ </Modal>
905
+ </>
906
+ );
907
+ }
908
+ export default RemovePupilConfirmation;
909
+ `.trim(),
910
+ },
911
+ },
912
+ },
913
+ render: () => <DeleteConfirmationTemplate />,
914
+ },
915
+ [
916
+ 'The destructive confirmation pattern. No `closeHandler` is provided — the user must explicitly',
917
+ 'choose "Cancel" or "Remove Pupil". There is no X button and Escape does nothing, preventing an',
918
+ 'accidental dismiss of a destructive action. Use `variant="primary-destructive"` on the confirm',
919
+ 'button and `variant="secondary"` on the cancel button.',
920
+ ].join(' '),
921
+ );
922
+
923
+ export const WithDropdownInsideModal: Story = withDescription(
924
+ {
925
+ parameters: {
926
+ controls: { disable: true },
927
+ docs: {
928
+ source: {
929
+ language: 'tsx',
930
+ code: `
931
+ import { useState } from 'react';
932
+ import { Button, Dropdown, Modal } from '@arbor-education/design-system.components';
933
+
934
+ function AssignTeacherModal() {
935
+ const [open, setOpen] = useState(false);
936
+ return (
937
+ <>
938
+ <Button variant="primary" onClick={() => setOpen(true)}>Assign Class Teacher</Button>
939
+ <Modal open={open} closeHandler={() => setOpen(false)} title="Assign Class Teacher">
940
+ <Modal.Body>
941
+ <p>Select a teacher to assign as the lead for this class.</p>
942
+ <Dropdown>
943
+ <Dropdown.Trigger>
944
+ <Button variant="dropdown">Select a teacher</Button>
945
+ </Dropdown.Trigger>
946
+ <Dropdown.Content>
947
+ <Dropdown.Item onSelect={() => {}}>Ms A. Johnson — English</Dropdown.Item>
948
+ <Dropdown.Item onSelect={() => {}}>Mr B. Patel — Mathematics</Dropdown.Item>
949
+ <Dropdown.Item onSelect={() => {}}>Mrs C. Williams — Science</Dropdown.Item>
950
+ </Dropdown.Content>
951
+ </Dropdown>
952
+ </Modal.Body>
953
+ <Modal.Footer>
954
+ <Button variant="secondary" onClick={() => setOpen(false)}>Cancel</Button>
955
+ <Button variant="primary" onClick={() => setOpen(false)}>Assign Teacher</Button>
956
+ </Modal.Footer>
957
+ </Modal>
958
+ </>
959
+ );
960
+ }
961
+ export default AssignTeacherModal;
962
+ `.trim(),
963
+ },
964
+ },
965
+ },
966
+ render: () => <WithDropdownInsideModalTemplate />,
967
+ },
968
+ [
969
+ 'A `Dropdown` inside `Modal.Body`. Because `Modal` registers its overlay ref as a',
970
+ '`PopupParentContext`, the Dropdown menu portals into the modal overlay automatically — it will',
971
+ 'never be clipped by the modal boundary or appear behind the overlay. No extra configuration',
972
+ 'is required.',
973
+ ].join(' '),
974
+ );