@12-apps/ui 1.0.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 (633) hide show
  1. package/components-catalog.md +806 -0
  2. package/components-guidelines.md +347 -0
  3. package/dist/index.cjs +2 -0
  4. package/dist/index.cjs.map +1 -0
  5. package/dist/index.d.cts +2 -0
  6. package/dist/index.d.ts +2 -0
  7. package/dist/index.js +1 -0
  8. package/dist/index.js.map +1 -0
  9. package/eslint.config.js +77 -0
  10. package/package.json +247 -0
  11. package/src/__tests__/package-wiring.test.ts +37 -0
  12. package/src/__tests__/setup.ts +1 -0
  13. package/src/__tests__/social-login-button.test.tsx +186 -0
  14. package/src/__tests__/user-avatar.test.tsx +195 -0
  15. package/src/button.tsx +22 -0
  16. package/src/components/command/__tests__/command.test.tsx +112 -0
  17. package/src/components/data-display/Alert/Alert.md +209 -0
  18. package/src/components/data-display/Alert/Alert.stories.tsx +561 -0
  19. package/src/components/data-display/Alert/Alert.test.stories.tsx +709 -0
  20. package/src/components/data-display/Alert/Alert.tsx +449 -0
  21. package/src/components/data-display/Alert/Alert.types.ts +79 -0
  22. package/src/components/data-display/Alert/index.ts +2 -0
  23. package/src/components/data-display/AlertDialog/AlertDialog.md +408 -0
  24. package/src/components/data-display/AlertDialog/AlertDialog.stories.tsx +360 -0
  25. package/src/components/data-display/AlertDialog/AlertDialog.test.stories.tsx +753 -0
  26. package/src/components/data-display/AlertDialog/AlertDialog.tsx +273 -0
  27. package/src/components/data-display/AlertDialog/AlertDialog.types.ts +74 -0
  28. package/src/components/data-display/AlertDialog/index.ts +2 -0
  29. package/src/components/data-display/AsyncStateContainer/AsyncStateContainer.stories.tsx +230 -0
  30. package/src/components/data-display/AsyncStateContainer/AsyncStateContainer.test.stories.tsx +270 -0
  31. package/src/components/data-display/AsyncStateContainer/AsyncStateContainer.tsx +116 -0
  32. package/src/components/data-display/AsyncStateContainer/AsyncStateContainer.types.ts +70 -0
  33. package/src/components/data-display/AsyncStateContainer/index.ts +2 -0
  34. package/src/components/data-display/Avatar/Avatar.md +568 -0
  35. package/src/components/data-display/Avatar/Avatar.stories.tsx +718 -0
  36. package/src/components/data-display/Avatar/Avatar.test.stories.tsx +820 -0
  37. package/src/components/data-display/Avatar/Avatar.tsx +507 -0
  38. package/src/components/data-display/Avatar/Avatar.types.ts +88 -0
  39. package/src/components/data-display/Avatar/index.ts +2 -0
  40. package/src/components/data-display/Badge/Badge.md +469 -0
  41. package/src/components/data-display/Badge/Badge.stories.tsx +1136 -0
  42. package/src/components/data-display/Badge/Badge.test.stories.tsx +1193 -0
  43. package/src/components/data-display/Badge/Badge.tsx +606 -0
  44. package/src/components/data-display/Badge/Badge.types.ts +118 -0
  45. package/src/components/data-display/Badge/index.ts +2 -0
  46. package/src/components/data-display/Banner/Banner.stories.tsx +524 -0
  47. package/src/components/data-display/Banner/Banner.test.stories.tsx +503 -0
  48. package/src/components/data-display/Banner/Banner.tsx +350 -0
  49. package/src/components/data-display/Banner/Banner.types.ts +41 -0
  50. package/src/components/data-display/Banner/banner.md +141 -0
  51. package/src/components/data-display/Banner/index.ts +2 -0
  52. package/src/components/data-display/Carousel/Carousel.md +815 -0
  53. package/src/components/data-display/Carousel/Carousel.stories.tsx +607 -0
  54. package/src/components/data-display/Carousel/Carousel.test.stories.tsx +691 -0
  55. package/src/components/data-display/Carousel/Carousel.tsx +765 -0
  56. package/src/components/data-display/Carousel/Carousel.types.ts +79 -0
  57. package/src/components/data-display/Carousel/index.ts +8 -0
  58. package/src/components/data-display/Chart/Chart.md +352 -0
  59. package/src/components/data-display/Chart/Chart.stories.tsx +568 -0
  60. package/src/components/data-display/Chart/Chart.test.stories.tsx +174 -0
  61. package/src/components/data-display/Chart/Chart.tsx +520 -0
  62. package/src/components/data-display/Chart/Chart.types.ts +65 -0
  63. package/src/components/data-display/Chart/index.ts +2 -0
  64. package/src/components/data-display/Chip/Chip.stories.tsx +251 -0
  65. package/src/components/data-display/Chip/Chip.test.stories.tsx +263 -0
  66. package/src/components/data-display/Chip/Chip.tsx +134 -0
  67. package/src/components/data-display/Chip/Chip.types.ts +51 -0
  68. package/src/components/data-display/Chip/chip.md +395 -0
  69. package/src/components/data-display/Chip/index.ts +2 -0
  70. package/src/components/data-display/DataGrid/DataGrid.md +602 -0
  71. package/src/components/data-display/DataGrid/DataGrid.stories.tsx +515 -0
  72. package/src/components/data-display/DataGrid/DataGrid.test.stories.tsx +659 -0
  73. package/src/components/data-display/DataGrid/DataGrid.tsx +743 -0
  74. package/src/components/data-display/DataGrid/DataGrid.types.ts +171 -0
  75. package/src/components/data-display/DataGrid/index.ts +21 -0
  76. package/src/components/data-display/DateTimeDisplay/DateTimeDisplay.tsx +197 -0
  77. package/src/components/data-display/DateTimeDisplay/DateTimeDisplay.types.ts +46 -0
  78. package/src/components/data-display/DateTimeDisplay/index.ts +2 -0
  79. package/src/components/data-display/DescriptionItem/DescriptionItem.md +226 -0
  80. package/src/components/data-display/DescriptionItem/DescriptionItem.stories.tsx +254 -0
  81. package/src/components/data-display/DescriptionItem/DescriptionItem.tsx +100 -0
  82. package/src/components/data-display/DescriptionItem/DescriptionItem.types.ts +1 -0
  83. package/src/components/data-display/DescriptionItem/index.ts +2 -0
  84. package/src/components/data-display/EmptyState/EmptyState.md +265 -0
  85. package/src/components/data-display/EmptyState/EmptyState.stories.tsx +282 -0
  86. package/src/components/data-display/EmptyState/EmptyState.test.stories.tsx +441 -0
  87. package/src/components/data-display/EmptyState/EmptyState.tsx +180 -0
  88. package/src/components/data-display/EmptyState/EmptyState.types.ts +31 -0
  89. package/src/components/data-display/EmptyState/index.ts +2 -0
  90. package/src/components/data-display/ErrorState/ErrorState.stories.tsx +110 -0
  91. package/src/components/data-display/ErrorState/ErrorState.test.stories.tsx +133 -0
  92. package/src/components/data-display/ErrorState/ErrorState.tsx +118 -0
  93. package/src/components/data-display/ErrorState/ErrorState.types.ts +45 -0
  94. package/src/components/data-display/ErrorState/index.ts +2 -0
  95. package/src/components/data-display/HoverCard/HoverCard.md +389 -0
  96. package/src/components/data-display/HoverCard/HoverCard.stories.tsx +648 -0
  97. package/src/components/data-display/HoverCard/HoverCard.test.stories.tsx +929 -0
  98. package/src/components/data-display/HoverCard/HoverCard.tsx +657 -0
  99. package/src/components/data-display/HoverCard/HoverCard.types.ts +116 -0
  100. package/src/components/data-display/HoverCard/index.ts +2 -0
  101. package/src/components/data-display/InteractiveTooltip/InteractiveTooltip.md +532 -0
  102. package/src/components/data-display/InteractiveTooltip/InteractiveTooltip.stories.tsx +411 -0
  103. package/src/components/data-display/InteractiveTooltip/InteractiveTooltip.tsx +245 -0
  104. package/src/components/data-display/InteractiveTooltip/InteractiveTooltip.types.ts +64 -0
  105. package/src/components/data-display/InteractiveTooltip/index.ts +2 -0
  106. package/src/components/data-display/LazyImage/LazyImage.md +356 -0
  107. package/src/components/data-display/LazyImage/LazyImage.stories.tsx +387 -0
  108. package/src/components/data-display/LazyImage/LazyImage.tsx +323 -0
  109. package/src/components/data-display/LazyImage/LazyImage.types.ts +205 -0
  110. package/src/components/data-display/LazyImage/index.ts +2 -0
  111. package/src/components/data-display/Lightbox/Lightbox.stories.tsx +315 -0
  112. package/src/components/data-display/Lightbox/Lightbox.test.stories.tsx +605 -0
  113. package/src/components/data-display/Lightbox/Lightbox.tsx +666 -0
  114. package/src/components/data-display/Lightbox/Lightbox.types.ts +49 -0
  115. package/src/components/data-display/Lightbox/index.ts +2 -0
  116. package/src/components/data-display/Lightbox/lightbox.md +298 -0
  117. package/src/components/data-display/LoadingState/LoadingState.stories.tsx +124 -0
  118. package/src/components/data-display/LoadingState/LoadingState.test.stories.tsx +99 -0
  119. package/src/components/data-display/LoadingState/LoadingState.tsx +103 -0
  120. package/src/components/data-display/LoadingState/LoadingState.types.ts +37 -0
  121. package/src/components/data-display/LoadingState/index.ts +2 -0
  122. package/src/components/data-display/MapPreview/MapPreview.md +154 -0
  123. package/src/components/data-display/MapPreview/MapPreview.stories.tsx +458 -0
  124. package/src/components/data-display/MapPreview/MapPreview.test.stories.tsx +390 -0
  125. package/src/components/data-display/MapPreview/MapPreview.tsx +922 -0
  126. package/src/components/data-display/MapPreview/MapPreview.types.ts +56 -0
  127. package/src/components/data-display/MapPreview/index.ts +2 -0
  128. package/src/components/data-display/Popover/Popover.md +245 -0
  129. package/src/components/data-display/Popover/Popover.stories.tsx +295 -0
  130. package/src/components/data-display/Popover/Popover.test.stories.tsx +1168 -0
  131. package/src/components/data-display/Popover/Popover.tsx +132 -0
  132. package/src/components/data-display/Popover/Popover.types.ts +33 -0
  133. package/src/components/data-display/Popover/index.ts +2 -0
  134. package/src/components/data-display/Progress/Progress.md +450 -0
  135. package/src/components/data-display/Progress/Progress.stories.tsx +481 -0
  136. package/src/components/data-display/Progress/Progress.test.stories.tsx +2070 -0
  137. package/src/components/data-display/Progress/Progress.tsx +332 -0
  138. package/src/components/data-display/Progress/Progress.types.ts +61 -0
  139. package/src/components/data-display/Progress/index.ts +2 -0
  140. package/src/components/data-display/SectionOnboarding/SectionOnboarding.md +108 -0
  141. package/src/components/data-display/SectionOnboarding/SectionOnboarding.stories.tsx +160 -0
  142. package/src/components/data-display/SectionOnboarding/SectionOnboarding.test.stories.tsx +190 -0
  143. package/src/components/data-display/SectionOnboarding/SectionOnboarding.tsx +224 -0
  144. package/src/components/data-display/SectionOnboarding/SectionOnboarding.types.ts +101 -0
  145. package/src/components/data-display/SectionOnboarding/index.ts +7 -0
  146. package/src/components/data-display/Sheet/Sheet.md +475 -0
  147. package/src/components/data-display/Sheet/Sheet.stories.tsx +883 -0
  148. package/src/components/data-display/Sheet/Sheet.test.stories.tsx +1101 -0
  149. package/src/components/data-display/Sheet/Sheet.tsx +978 -0
  150. package/src/components/data-display/Sheet/Sheet.types.ts +93 -0
  151. package/src/components/data-display/Sheet/index.ts +8 -0
  152. package/src/components/data-display/Stepper/Stepper.md +542 -0
  153. package/src/components/data-display/Stepper/Stepper.stories.tsx +540 -0
  154. package/src/components/data-display/Stepper/Stepper.test.stories.tsx +607 -0
  155. package/src/components/data-display/Stepper/Stepper.tsx +328 -0
  156. package/src/components/data-display/Stepper/Stepper.types.ts +97 -0
  157. package/src/components/data-display/Stepper/index.ts +2 -0
  158. package/src/components/data-display/Table/Table.md +243 -0
  159. package/src/components/data-display/Table/Table.stories.tsx +801 -0
  160. package/src/components/data-display/Table/Table.test.stories.tsx +369 -0
  161. package/src/components/data-display/Table/Table.tsx +831 -0
  162. package/src/components/data-display/Table/Table.types.ts +335 -0
  163. package/src/components/data-display/Table/index.ts +2 -0
  164. package/src/components/data-display/Timeline/Timeline.md +201 -0
  165. package/src/components/data-display/Timeline/Timeline.stories.tsx +444 -0
  166. package/src/components/data-display/Timeline/Timeline.test.stories.tsx +944 -0
  167. package/src/components/data-display/Timeline/Timeline.tsx +348 -0
  168. package/src/components/data-display/Timeline/Timeline.types.ts +26 -0
  169. package/src/components/data-display/Timeline/index.ts +2 -0
  170. package/src/components/data-display/TimingDiagram/TimingDiagram.md +162 -0
  171. package/src/components/data-display/TimingDiagram/TimingDiagram.stories.tsx +475 -0
  172. package/src/components/data-display/TimingDiagram/TimingDiagram.test.stories.tsx +734 -0
  173. package/src/components/data-display/TimingDiagram/TimingDiagram.tsx +399 -0
  174. package/src/components/data-display/TimingDiagram/TimingDiagram.types.ts +19 -0
  175. package/src/components/data-display/TimingDiagram/index.ts +2 -0
  176. package/src/components/data-display/Tooltip/Tooltip.md +397 -0
  177. package/src/components/data-display/Tooltip/Tooltip.stories.tsx +465 -0
  178. package/src/components/data-display/Tooltip/Tooltip.test.stories.tsx +1168 -0
  179. package/src/components/data-display/Tooltip/Tooltip.tsx +161 -0
  180. package/src/components/data-display/Tooltip/Tooltip.types.ts +33 -0
  181. package/src/components/data-display/Tooltip/index.ts +2 -0
  182. package/src/components/data-display/index.ts +29 -0
  183. package/src/components/feedback/Dialog/Dialog.md +527 -0
  184. package/src/components/feedback/Dialog/Dialog.stories.tsx +833 -0
  185. package/src/components/feedback/Dialog/Dialog.styles.ts +171 -0
  186. package/src/components/feedback/Dialog/Dialog.test.stories.tsx +955 -0
  187. package/src/components/feedback/Dialog/Dialog.tsx +256 -0
  188. package/src/components/feedback/Dialog/Dialog.types.ts +46 -0
  189. package/src/components/feedback/Dialog/index.ts +9 -0
  190. package/src/components/feedback/Modal/Modal.md +629 -0
  191. package/src/components/feedback/Modal/Modal.stories.tsx +765 -0
  192. package/src/components/feedback/Modal/Modal.test.stories.tsx +251 -0
  193. package/src/components/feedback/Modal/Modal.tsx +279 -0
  194. package/src/components/feedback/Modal/Modal.types.ts +26 -0
  195. package/src/components/feedback/Modal/index.ts +7 -0
  196. package/src/components/feedback/Sonner/Sonner.stories.tsx +698 -0
  197. package/src/components/feedback/Sonner/Sonner.test.stories.tsx +896 -0
  198. package/src/components/feedback/Sonner/Sonner.tsx +493 -0
  199. package/src/components/feedback/Sonner/Sonner.types.ts +65 -0
  200. package/src/components/feedback/Sonner/index.ts +8 -0
  201. package/src/components/feedback/StackedModal/StackedModal.md +273 -0
  202. package/src/components/feedback/StackedModal/StackedModal.stories.tsx +1785 -0
  203. package/src/components/feedback/StackedModal/StackedModal.test.stories.tsx +975 -0
  204. package/src/components/feedback/StackedModal/StackedModal.tsx +664 -0
  205. package/src/components/feedback/StackedModal/StackedModal.types.ts +70 -0
  206. package/src/components/feedback/StackedModal/index.ts +11 -0
  207. package/src/components/feedback/Toast/Toast.md +547 -0
  208. package/src/components/feedback/Toast/Toast.stories.tsx +548 -0
  209. package/src/components/feedback/Toast/Toast.test.stories.tsx +834 -0
  210. package/src/components/feedback/Toast/Toast.tsx +279 -0
  211. package/src/components/feedback/Toast/Toast.types.ts +59 -0
  212. package/src/components/feedback/Toast/index.ts +8 -0
  213. package/src/components/feedback/TutorialOverlay/TutorialOverlay.md +610 -0
  214. package/src/components/feedback/TutorialOverlay/TutorialOverlay.stories.tsx +691 -0
  215. package/src/components/feedback/TutorialOverlay/TutorialOverlay.test.stories.tsx +634 -0
  216. package/src/components/feedback/TutorialOverlay/TutorialOverlay.tsx +554 -0
  217. package/src/components/feedback/TutorialOverlay/TutorialOverlay.types.ts +30 -0
  218. package/src/components/feedback/TutorialOverlay/index.ts +2 -0
  219. package/src/components/feedback/index.ts +6 -0
  220. package/src/components/form/AddressAutocomplete/AddressAutocomplete.md +633 -0
  221. package/src/components/form/AddressAutocomplete/AddressAutocomplete.stories.tsx +663 -0
  222. package/src/components/form/AddressAutocomplete/AddressAutocomplete.test.stories.tsx +723 -0
  223. package/src/components/form/AddressAutocomplete/AddressAutocomplete.tsx +836 -0
  224. package/src/components/form/AddressAutocomplete/AddressAutocomplete.types.ts +37 -0
  225. package/src/components/form/AddressAutocomplete/index.ts +2 -0
  226. package/src/components/form/Autocomplete/Autocomplete.helpers.ts +115 -0
  227. package/src/components/form/Autocomplete/Autocomplete.keys.ts +134 -0
  228. package/src/components/form/Autocomplete/Autocomplete.parts.tsx +337 -0
  229. package/src/components/form/Autocomplete/Autocomplete.stories.tsx +715 -0
  230. package/src/components/form/Autocomplete/Autocomplete.test.stories.tsx +825 -0
  231. package/src/components/form/Autocomplete/Autocomplete.tsx +166 -0
  232. package/src/components/form/Autocomplete/Autocomplete.types.ts +116 -0
  233. package/src/components/form/Autocomplete/autocomplete.md +789 -0
  234. package/src/components/form/Autocomplete/index.ts +8 -0
  235. package/src/components/form/Autocomplete/useAutocomplete.ts +311 -0
  236. package/src/components/form/Button/Button.md +411 -0
  237. package/src/components/form/Button/Button.stories.tsx +362 -0
  238. package/src/components/form/Button/Button.test.stories.tsx +603 -0
  239. package/src/components/form/Button/Button.tsx +326 -0
  240. package/src/components/form/Button/Button.types.ts +74 -0
  241. package/src/components/form/Button/index.ts +2 -0
  242. package/src/components/form/Calendar/Calendar.md +807 -0
  243. package/src/components/form/Calendar/Calendar.stories.tsx +349 -0
  244. package/src/components/form/Calendar/Calendar.test.stories.tsx +465 -0
  245. package/src/components/form/Calendar/Calendar.tsx +571 -0
  246. package/src/components/form/Calendar/Calendar.types.ts +81 -0
  247. package/src/components/form/Calendar/index.ts +11 -0
  248. package/src/components/form/Checkbox/Checkbox.md +200 -0
  249. package/src/components/form/Checkbox/Checkbox.stories.tsx +265 -0
  250. package/src/components/form/Checkbox/Checkbox.test.stories.tsx +181 -0
  251. package/src/components/form/Checkbox/Checkbox.tsx +170 -0
  252. package/src/components/form/Checkbox/Checkbox.types.ts +20 -0
  253. package/src/components/form/Checkbox/index.ts +2 -0
  254. package/src/components/form/CodeEditor/CodeEditor.md +350 -0
  255. package/src/components/form/CodeEditor/CodeEditor.stories.tsx +871 -0
  256. package/src/components/form/CodeEditor/CodeEditor.test.stories.tsx +575 -0
  257. package/src/components/form/CodeEditor/CodeEditor.tsx +415 -0
  258. package/src/components/form/CodeEditor/CodeEditor.types.ts +38 -0
  259. package/src/components/form/CodeEditor/index.ts +2 -0
  260. package/src/components/form/Command/Command.md +468 -0
  261. package/src/components/form/Command/Command.stories.tsx +580 -0
  262. package/src/components/form/Command/Command.test.stories.tsx +215 -0
  263. package/src/components/form/Command/Command.tsx +545 -0
  264. package/src/components/form/Command/Command.types.ts +116 -0
  265. package/src/components/form/Command/index.ts +12 -0
  266. package/src/components/form/CreatableSelect/CreatableSelect.md +114 -0
  267. package/src/components/form/CreatableSelect/CreatableSelect.stories.tsx +112 -0
  268. package/src/components/form/CreatableSelect/CreatableSelect.test.stories.tsx +134 -0
  269. package/src/components/form/CreatableSelect/CreatableSelect.tsx +149 -0
  270. package/src/components/form/CreatableSelect/CreatableSelect.types.ts +37 -0
  271. package/src/components/form/CreatableSelect/index.ts +2 -0
  272. package/src/components/form/Form/Form.md +325 -0
  273. package/src/components/form/Form/Form.stories.tsx +687 -0
  274. package/src/components/form/Form/Form.test.stories.tsx +601 -0
  275. package/src/components/form/Form/Form.tsx +136 -0
  276. package/src/components/form/Form/Form.types.ts +42 -0
  277. package/src/components/form/Form/index.ts +8 -0
  278. package/src/components/form/Input/Input.md +252 -0
  279. package/src/components/form/Input/Input.stories.tsx +256 -0
  280. package/src/components/form/Input/Input.test.stories.tsx +1184 -0
  281. package/src/components/form/Input/Input.tsx +263 -0
  282. package/src/components/form/Input/Input.types.ts +24 -0
  283. package/src/components/form/Input/index.ts +2 -0
  284. package/src/components/form/InputOTP/InputOTP.md +419 -0
  285. package/src/components/form/InputOTP/InputOTP.stories.tsx +774 -0
  286. package/src/components/form/InputOTP/InputOTP.test.stories.tsx +596 -0
  287. package/src/components/form/InputOTP/InputOTP.tsx +231 -0
  288. package/src/components/form/InputOTP/InputOTP.types.ts +68 -0
  289. package/src/components/form/InputOTP/index.ts +2 -0
  290. package/src/components/form/Label/Label.md +426 -0
  291. package/src/components/form/Label/Label.stories.tsx +473 -0
  292. package/src/components/form/Label/Label.test.stories.tsx +909 -0
  293. package/src/components/form/Label/Label.tsx +367 -0
  294. package/src/components/form/Label/Label.types.ts +35 -0
  295. package/src/components/form/Label/index.ts +2 -0
  296. package/src/components/form/Menubar/Menubar.md +275 -0
  297. package/src/components/form/Menubar/Menubar.stories.tsx +472 -0
  298. package/src/components/form/Menubar/Menubar.test.stories.tsx +588 -0
  299. package/src/components/form/Menubar/Menubar.tsx +467 -0
  300. package/src/components/form/Menubar/Menubar.types.ts +71 -0
  301. package/src/components/form/Menubar/index.ts +8 -0
  302. package/src/components/form/PasswordStrength/PasswordStrength.md +139 -0
  303. package/src/components/form/PasswordStrength/PasswordStrength.stories.tsx +562 -0
  304. package/src/components/form/PasswordStrength/PasswordStrength.test.stories.tsx +327 -0
  305. package/src/components/form/PasswordStrength/PasswordStrength.tsx +419 -0
  306. package/src/components/form/PasswordStrength/PasswordStrength.types.ts +18 -0
  307. package/src/components/form/PasswordStrength/index.ts +2 -0
  308. package/src/components/form/PhoneInput/PhoneInput.md +578 -0
  309. package/src/components/form/PhoneInput/PhoneInput.stories.tsx +495 -0
  310. package/src/components/form/PhoneInput/PhoneInput.test.stories.tsx +666 -0
  311. package/src/components/form/PhoneInput/PhoneInput.tsx +376 -0
  312. package/src/components/form/PhoneInput/PhoneInput.types.ts +25 -0
  313. package/src/components/form/PhoneInput/index.ts +2 -0
  314. package/src/components/form/RadioGroup/RadioGroup.md +477 -0
  315. package/src/components/form/RadioGroup/RadioGroup.stories.tsx +518 -0
  316. package/src/components/form/RadioGroup/RadioGroup.test.stories.tsx +685 -0
  317. package/src/components/form/RadioGroup/RadioGroup.tsx +664 -0
  318. package/src/components/form/RadioGroup/RadioGroup.types.ts +82 -0
  319. package/src/components/form/RadioGroup/index.ts +2 -0
  320. package/src/components/form/RichTextEditor/RichTextEditor.md +710 -0
  321. package/src/components/form/RichTextEditor/RichTextEditor.stories.tsx +261 -0
  322. package/src/components/form/RichTextEditor/RichTextEditor.test.stories.tsx +498 -0
  323. package/src/components/form/RichTextEditor/RichTextEditor.tsx +468 -0
  324. package/src/components/form/RichTextEditor/RichTextEditor.types.ts +72 -0
  325. package/src/components/form/RichTextEditor/index.ts +2 -0
  326. package/src/components/form/SearchPalette/SearchPalette.md +69 -0
  327. package/src/components/form/SearchPalette/SearchPalette.parts.tsx +358 -0
  328. package/src/components/form/SearchPalette/SearchPalette.stories.tsx +135 -0
  329. package/src/components/form/SearchPalette/SearchPalette.tsx +141 -0
  330. package/src/components/form/SearchPalette/SearchPalette.types.ts +82 -0
  331. package/src/components/form/SearchPalette/index.ts +7 -0
  332. package/src/components/form/Select/Select.md +254 -0
  333. package/src/components/form/Select/Select.stories.tsx +364 -0
  334. package/src/components/form/Select/Select.test.stories.tsx +977 -0
  335. package/src/components/form/Select/Select.tsx +239 -0
  336. package/src/components/form/Select/Select.types.ts +57 -0
  337. package/src/components/form/Select/index.ts +2 -0
  338. package/src/components/form/Slider/Slider.md +326 -0
  339. package/src/components/form/Slider/Slider.stories.tsx +418 -0
  340. package/src/components/form/Slider/Slider.test.stories.tsx +1044 -0
  341. package/src/components/form/Slider/Slider.tsx +374 -0
  342. package/src/components/form/Slider/Slider.types.ts +113 -0
  343. package/src/components/form/Slider/index.ts +2 -0
  344. package/src/components/form/Switch/Switch.md +565 -0
  345. package/src/components/form/Switch/Switch.stories.tsx +697 -0
  346. package/src/components/form/Switch/Switch.test.stories.tsx +597 -0
  347. package/src/components/form/Switch/Switch.tsx +578 -0
  348. package/src/components/form/Switch/Switch.types.ts +130 -0
  349. package/src/components/form/Switch/index.ts +2 -0
  350. package/src/components/form/Textarea/Textarea.stories.tsx +473 -0
  351. package/src/components/form/Textarea/Textarea.test.stories.tsx +708 -0
  352. package/src/components/form/Textarea/Textarea.tsx +678 -0
  353. package/src/components/form/Textarea/Textarea.types.ts +84 -0
  354. package/src/components/form/Textarea/index.ts +2 -0
  355. package/src/components/form/Toggle/Toggle.md +234 -0
  356. package/src/components/form/Toggle/Toggle.stories.tsx +638 -0
  357. package/src/components/form/Toggle/Toggle.test.stories.tsx +1167 -0
  358. package/src/components/form/Toggle/Toggle.tsx +265 -0
  359. package/src/components/form/Toggle/Toggle.types.ts +79 -0
  360. package/src/components/form/Toggle/index.ts +2 -0
  361. package/src/components/form/ToggleGroup/ToggleGroup.md +329 -0
  362. package/src/components/form/ToggleGroup/ToggleGroup.stories.tsx +617 -0
  363. package/src/components/form/ToggleGroup/ToggleGroup.test.stories.tsx +918 -0
  364. package/src/components/form/ToggleGroup/ToggleGroup.tsx +129 -0
  365. package/src/components/form/ToggleGroup/ToggleGroup.types.ts +53 -0
  366. package/src/components/form/ToggleGroup/index.ts +2 -0
  367. package/src/components/form/UploadButton/UploadButton.md +44 -0
  368. package/src/components/form/UploadButton/UploadButton.stories.tsx +366 -0
  369. package/src/components/form/UploadButton/UploadButton.test.stories.tsx +454 -0
  370. package/src/components/form/UploadButton/UploadButton.tsx +312 -0
  371. package/src/components/form/UploadButton/UploadButton.types.ts +58 -0
  372. package/src/components/form/UploadButton/index.ts +2 -0
  373. package/src/components/form/index.ts +25 -0
  374. package/src/components/form/total-form/Fields/SelectField.tsx +57 -0
  375. package/src/components/form/total-form/Fields/TextField.tsx +58 -0
  376. package/src/components/form/total-form/Fields/index.ts +4 -0
  377. package/src/components/form/total-form/FormContainer.tsx +104 -0
  378. package/src/components/form/total-form/FormContext.tsx +38 -0
  379. package/src/components/form/total-form/FormErrorSnackbar.tsx +35 -0
  380. package/src/components/form/total-form/SubmitButton.tsx +27 -0
  381. package/src/components/form/total-form/index.ts +12 -0
  382. package/src/components/form/total-form/total-form.test.tsx +97 -0
  383. package/src/components/index.ts +7 -0
  384. package/src/components/layout/Accordion/Accordion.md +725 -0
  385. package/src/components/layout/Accordion/Accordion.stories.tsx +690 -0
  386. package/src/components/layout/Accordion/Accordion.test.stories.tsx +1362 -0
  387. package/src/components/layout/Accordion/Accordion.tsx +242 -0
  388. package/src/components/layout/Accordion/Accordion.types.ts +32 -0
  389. package/src/components/layout/Accordion/index.ts +8 -0
  390. package/src/components/layout/Card/Card.md +533 -0
  391. package/src/components/layout/Card/Card.stories.tsx +593 -0
  392. package/src/components/layout/Card/Card.test.stories.tsx +1134 -0
  393. package/src/components/layout/Card/Card.tsx +404 -0
  394. package/src/components/layout/Card/Card.types.ts +57 -0
  395. package/src/components/layout/Card/index.ts +8 -0
  396. package/src/components/layout/Collapsible/Collapsible.md +135 -0
  397. package/src/components/layout/Collapsible/Collapsible.stories.tsx +180 -0
  398. package/src/components/layout/Collapsible/Collapsible.test.stories.tsx +304 -0
  399. package/src/components/layout/Collapsible/Collapsible.tsx +198 -0
  400. package/src/components/layout/Collapsible/Collapsible.types.ts +35 -0
  401. package/src/components/layout/Collapsible/index.ts +6 -0
  402. package/src/components/layout/Container/Container.md +420 -0
  403. package/src/components/layout/Container/Container.stories.tsx +557 -0
  404. package/src/components/layout/Container/Container.test.stories.tsx +750 -0
  405. package/src/components/layout/Container/Container.tsx +72 -0
  406. package/src/components/layout/Container/Container.types.ts +11 -0
  407. package/src/components/layout/Container/index.ts +2 -0
  408. package/src/components/layout/ContentToolbar/ColumnsMenu.tsx +99 -0
  409. package/src/components/layout/ContentToolbar/ContentToolbar.md +75 -0
  410. package/src/components/layout/ContentToolbar/ContentToolbar.stories.tsx +129 -0
  411. package/src/components/layout/ContentToolbar/ContentToolbar.test.stories.tsx +251 -0
  412. package/src/components/layout/ContentToolbar/ContentToolbar.tsx +108 -0
  413. package/src/components/layout/ContentToolbar/ContentToolbar.types.ts +229 -0
  414. package/src/components/layout/ContentToolbar/FilterTrigger.tsx +71 -0
  415. package/src/components/layout/ContentToolbar/MultiSelectDropdown.tsx +297 -0
  416. package/src/components/layout/ContentToolbar/SavedViewsMenu.tsx +334 -0
  417. package/src/components/layout/ContentToolbar/SortByDropdown.tsx +213 -0
  418. package/src/components/layout/ContentToolbar/ViewSelector.tsx +98 -0
  419. package/src/components/layout/ContentToolbar/__tests__/ColumnsMenu.test.tsx +39 -0
  420. package/src/components/layout/ContentToolbar/__tests__/MultiSelectDropdown.test.tsx +110 -0
  421. package/src/components/layout/ContentToolbar/__tests__/SavedViewsMenu.test.tsx +99 -0
  422. package/src/components/layout/ContentToolbar/index.ts +24 -0
  423. package/src/components/layout/ContentToolbar/tests.md +42 -0
  424. package/src/components/layout/ContentToolbar/track.md +41 -0
  425. package/src/components/layout/Dashboard/Dashboard.md +71 -0
  426. package/src/components/layout/Dashboard/Dashboard.stories.tsx +104 -0
  427. package/src/components/layout/Dashboard/Dashboard.test.stories.tsx +164 -0
  428. package/src/components/layout/Dashboard/Dashboard.tsx +297 -0
  429. package/src/components/layout/Dashboard/Dashboard.types.ts +157 -0
  430. package/src/components/layout/Dashboard/DashboardContext.tsx +47 -0
  431. package/src/components/layout/Dashboard/DashboardParts.tsx +216 -0
  432. package/src/components/layout/Dashboard/index.ts +18 -0
  433. package/src/components/layout/Dashboard/tests.md +46 -0
  434. package/src/components/layout/Dashboard/track.md +50 -0
  435. package/src/components/layout/Drawer/Drawer.md +479 -0
  436. package/src/components/layout/Drawer/Drawer.stories.tsx +660 -0
  437. package/src/components/layout/Drawer/Drawer.test.stories.tsx +1339 -0
  438. package/src/components/layout/Drawer/Drawer.tsx +150 -0
  439. package/src/components/layout/Drawer/Drawer.types.ts +34 -0
  440. package/src/components/layout/Drawer/index.ts +2 -0
  441. package/src/components/layout/Resizable/Resizable.md +308 -0
  442. package/src/components/layout/Resizable/Resizable.stories.tsx +1039 -0
  443. package/src/components/layout/Resizable/Resizable.test.stories.tsx +750 -0
  444. package/src/components/layout/Resizable/Resizable.tsx +190 -0
  445. package/src/components/layout/Resizable/Resizable.types.ts +34 -0
  446. package/src/components/layout/Resizable/index.ts +2 -0
  447. package/src/components/layout/ScrollArea/ScrollArea.md +122 -0
  448. package/src/components/layout/ScrollArea/ScrollArea.stories.tsx +651 -0
  449. package/src/components/layout/ScrollArea/ScrollArea.test.stories.tsx +619 -0
  450. package/src/components/layout/ScrollArea/ScrollArea.tsx +449 -0
  451. package/src/components/layout/ScrollArea/ScrollArea.types.ts +80 -0
  452. package/src/components/layout/ScrollArea/index.ts +7 -0
  453. package/src/components/layout/Separator/Separator.stories.tsx +563 -0
  454. package/src/components/layout/Separator/Separator.test.stories.tsx +527 -0
  455. package/src/components/layout/Separator/Separator.tsx +187 -0
  456. package/src/components/layout/Separator/Separator.types.ts +17 -0
  457. package/src/components/layout/Separator/index.ts +2 -0
  458. package/src/components/layout/SettingsLayout/SettingsLayout.md +139 -0
  459. package/src/components/layout/SettingsLayout/SettingsLayout.parts.tsx +357 -0
  460. package/src/components/layout/SettingsLayout/SettingsLayout.stories.tsx +194 -0
  461. package/src/components/layout/SettingsLayout/SettingsLayout.test.stories.tsx +175 -0
  462. package/src/components/layout/SettingsLayout/SettingsLayout.tsx +69 -0
  463. package/src/components/layout/SettingsLayout/SettingsLayout.types.ts +61 -0
  464. package/src/components/layout/SettingsLayout/index.ts +6 -0
  465. package/src/components/layout/Sidebar/Sidebar.md +367 -0
  466. package/src/components/layout/Sidebar/Sidebar.stories.tsx +611 -0
  467. package/src/components/layout/Sidebar/Sidebar.test.stories.tsx +1326 -0
  468. package/src/components/layout/Sidebar/Sidebar.tsx +131 -0
  469. package/src/components/layout/Sidebar/Sidebar.types.ts +30 -0
  470. package/src/components/layout/Sidebar/index.ts +7 -0
  471. package/src/components/layout/Skeleton/Skeleton.md +240 -0
  472. package/src/components/layout/Skeleton/Skeleton.stories.tsx +689 -0
  473. package/src/components/layout/Skeleton/Skeleton.test.stories.tsx +521 -0
  474. package/src/components/layout/Skeleton/Skeleton.tsx +166 -0
  475. package/src/components/layout/Skeleton/Skeleton.types.ts +19 -0
  476. package/src/components/layout/Skeleton/index.ts +2 -0
  477. package/src/components/layout/Spacer/Spacer.md +84 -0
  478. package/src/components/layout/Spacer/Spacer.stories.tsx +502 -0
  479. package/src/components/layout/Spacer/Spacer.test.stories.tsx +472 -0
  480. package/src/components/layout/Spacer/Spacer.tsx +68 -0
  481. package/src/components/layout/Spacer/Spacer.types.ts +12 -0
  482. package/src/components/layout/Spacer/index.ts +2 -0
  483. package/src/components/layout/TableFilter/TableFilter.md +55 -0
  484. package/src/components/layout/TableFilter/TableFilter.stories.tsx +82 -0
  485. package/src/components/layout/TableFilter/TableFilter.test.stories.tsx +87 -0
  486. package/src/components/layout/TableFilter/TableFilter.tsx +383 -0
  487. package/src/components/layout/TableFilter/TableFilter.types.ts +87 -0
  488. package/src/components/layout/TableFilter/TableFilterContext.tsx +29 -0
  489. package/src/components/layout/TableFilter/index.ts +15 -0
  490. package/src/components/layout/TableFilter/tests.md +37 -0
  491. package/src/components/layout/TableFilter/track.md +36 -0
  492. package/src/components/layout/index.ts +14 -0
  493. package/src/components/navigation/Breadcrumbs/Breadcrumbs.md +558 -0
  494. package/src/components/navigation/Breadcrumbs/Breadcrumbs.stories.tsx +640 -0
  495. package/src/components/navigation/Breadcrumbs/Breadcrumbs.test.stories.tsx +1007 -0
  496. package/src/components/navigation/Breadcrumbs/Breadcrumbs.tsx +516 -0
  497. package/src/components/navigation/Breadcrumbs/Breadcrumbs.types.ts +132 -0
  498. package/src/components/navigation/Breadcrumbs/index.ts +2 -0
  499. package/src/components/navigation/CommandPalette/CommandPalette.md +201 -0
  500. package/src/components/navigation/CommandPalette/CommandPalette.stories.tsx +384 -0
  501. package/src/components/navigation/CommandPalette/CommandPalette.test.stories.tsx +437 -0
  502. package/src/components/navigation/CommandPalette/CommandPalette.tsx +434 -0
  503. package/src/components/navigation/CommandPalette/CommandPalette.types.ts +25 -0
  504. package/src/components/navigation/CommandPalette/index.ts +2 -0
  505. package/src/components/navigation/ContextMenu/ContextMenu.md +535 -0
  506. package/src/components/navigation/ContextMenu/ContextMenu.stories.tsx +611 -0
  507. package/src/components/navigation/ContextMenu/ContextMenu.test.stories.tsx +850 -0
  508. package/src/components/navigation/ContextMenu/ContextMenu.tsx +254 -0
  509. package/src/components/navigation/ContextMenu/ContextMenu.types.ts +96 -0
  510. package/src/components/navigation/ContextMenu/index.ts +2 -0
  511. package/src/components/navigation/DropdownMenu/DropdownMenu.md +514 -0
  512. package/src/components/navigation/DropdownMenu/DropdownMenu.stories.tsx +580 -0
  513. package/src/components/navigation/DropdownMenu/DropdownMenu.test.stories.tsx +660 -0
  514. package/src/components/navigation/DropdownMenu/DropdownMenu.tsx +305 -0
  515. package/src/components/navigation/DropdownMenu/DropdownMenu.types.ts +116 -0
  516. package/src/components/navigation/DropdownMenu/index.ts +2 -0
  517. package/src/components/navigation/NavigationMenu/NavigationMenu.md +580 -0
  518. package/src/components/navigation/NavigationMenu/NavigationMenu.tsx +874 -0
  519. package/src/components/navigation/NavigationMenu/NavigationMenu.types.ts +135 -0
  520. package/src/components/navigation/NavigationMenu/index.ts +2 -0
  521. package/src/components/navigation/NavigationMenu/stories/NavigationMenu.stories.tsx +518 -0
  522. package/src/components/navigation/NavigationMenu/stories/NavigationMenu.test.stories.tsx +931 -0
  523. package/src/components/navigation/NavigationMenu/stories/NavigationMenuHorizontal.stories.tsx +503 -0
  524. package/src/components/navigation/NavigationMenu/stories/NavigationMenuVertical.stories.tsx +455 -0
  525. package/src/components/navigation/Pagination/Pagination.md +699 -0
  526. package/src/components/navigation/Pagination/Pagination.stories.tsx +604 -0
  527. package/src/components/navigation/Pagination/Pagination.test.stories.tsx +520 -0
  528. package/src/components/navigation/Pagination/Pagination.tsx +356 -0
  529. package/src/components/navigation/Pagination/Pagination.types.ts +129 -0
  530. package/src/components/navigation/Pagination/index.ts +2 -0
  531. package/src/components/navigation/Tabs/Tabs.md +774 -0
  532. package/src/components/navigation/Tabs/Tabs.stories.tsx +1006 -0
  533. package/src/components/navigation/Tabs/Tabs.test.stories.tsx +1225 -0
  534. package/src/components/navigation/Tabs/Tabs.tsx +433 -0
  535. package/src/components/navigation/Tabs/Tabs.types.ts +239 -0
  536. package/src/components/navigation/Tabs/index.ts +2 -0
  537. package/src/components/navigation/index.ts +7 -0
  538. package/src/components/typography/Blockquote/Blockquote.md +130 -0
  539. package/src/components/typography/Blockquote/Blockquote.stories.tsx +359 -0
  540. package/src/components/typography/Blockquote/Blockquote.test.stories.tsx +432 -0
  541. package/src/components/typography/Blockquote/Blockquote.tsx +166 -0
  542. package/src/components/typography/Blockquote/Blockquote.types.ts +11 -0
  543. package/src/components/typography/Blockquote/index.ts +2 -0
  544. package/src/components/typography/Code/Code.md +182 -0
  545. package/src/components/typography/Code/Code.stories.tsx +686 -0
  546. package/src/components/typography/Code/Code.test.stories.tsx +703 -0
  547. package/src/components/typography/Code/Code.tsx +228 -0
  548. package/src/components/typography/Code/Code.types.ts +12 -0
  549. package/src/components/typography/Code/index.ts +2 -0
  550. package/src/components/typography/Heading/Heading.md +143 -0
  551. package/src/components/typography/Heading/Heading.stories.tsx +451 -0
  552. package/src/components/typography/Heading/Heading.test.stories.tsx +830 -0
  553. package/src/components/typography/Heading/Heading.tsx +178 -0
  554. package/src/components/typography/Heading/Heading.types.ts +11 -0
  555. package/src/components/typography/Heading/index.ts +2 -0
  556. package/src/components/typography/Paragraph/Paragraph.md +80 -0
  557. package/src/components/typography/Paragraph/Paragraph.stories.tsx +280 -0
  558. package/src/components/typography/Paragraph/Paragraph.test.stories.tsx +238 -0
  559. package/src/components/typography/Paragraph/Paragraph.tsx +99 -0
  560. package/src/components/typography/Paragraph/Paragraph.types.ts +10 -0
  561. package/src/components/typography/Paragraph/index.ts +2 -0
  562. package/src/components/typography/Text/Text.md +129 -0
  563. package/src/components/typography/Text/Text.stories.tsx +359 -0
  564. package/src/components/typography/Text/Text.test.stories.tsx +762 -0
  565. package/src/components/typography/Text/Text.tsx +159 -0
  566. package/src/components/typography/Text/Text.types.ts +15 -0
  567. package/src/components/typography/Text/index.ts +2 -0
  568. package/src/components/typography/index.ts +5 -0
  569. package/src/components/utility/AnimatedIcon/AnimatedIcon.md +359 -0
  570. package/src/components/utility/AnimatedIcon/AnimatedIcon.stories.tsx +947 -0
  571. package/src/components/utility/AnimatedIcon/AnimatedIcon.test.stories.tsx +596 -0
  572. package/src/components/utility/AnimatedIcon/AnimatedIcon.tsx +595 -0
  573. package/src/components/utility/AnimatedIcon/AnimatedIcon.types.ts +98 -0
  574. package/src/components/utility/AnimatedIcon/index.ts +2 -0
  575. package/src/components/utility/AspectRatio/AspectRatio.md +200 -0
  576. package/src/components/utility/AspectRatio/AspectRatio.stories.tsx +414 -0
  577. package/src/components/utility/AspectRatio/AspectRatio.test.stories.tsx +859 -0
  578. package/src/components/utility/AspectRatio/AspectRatio.tsx +76 -0
  579. package/src/components/utility/AspectRatio/AspectRatio.types.ts +13 -0
  580. package/src/components/utility/AspectRatio/index.ts +2 -0
  581. package/src/components/utility/InfiniteScroll/InfiniteScroll.md +531 -0
  582. package/src/components/utility/InfiniteScroll/InfiniteScroll.stories.tsx +620 -0
  583. package/src/components/utility/InfiniteScroll/InfiniteScroll.test.stories.tsx +358 -0
  584. package/src/components/utility/InfiniteScroll/InfiniteScroll.tsx +204 -0
  585. package/src/components/utility/InfiniteScroll/InfiniteScroll.types.ts +39 -0
  586. package/src/components/utility/InfiniteScroll/index.ts +6 -0
  587. package/src/components/utility/Portal/Portal.button-container.example.tsx +99 -0
  588. package/src/components/utility/Portal/Portal.md +593 -0
  589. package/src/components/utility/Portal/Portal.stories.tsx +340 -0
  590. package/src/components/utility/Portal/Portal.test.stories.tsx +2086 -0
  591. package/src/components/utility/Portal/Portal.tsx +39 -0
  592. package/src/components/utility/Portal/Portal.types.ts +7 -0
  593. package/src/components/utility/Portal/index.ts +2 -0
  594. package/src/components/utility/Transition/Transition.md +684 -0
  595. package/src/components/utility/Transition/Transition.stories.tsx +493 -0
  596. package/src/components/utility/Transition/Transition.test.stories.tsx +854 -0
  597. package/src/components/utility/Transition/Transition.tsx +140 -0
  598. package/src/components/utility/Transition/Transition.types.ts +14 -0
  599. package/src/components/utility/Transition/index.ts +6 -0
  600. package/src/components/utility/VirtualList/VirtualList.md +520 -0
  601. package/src/components/utility/VirtualList/VirtualList.stories.tsx +1073 -0
  602. package/src/components/utility/VirtualList/VirtualList.test.stories.tsx +1074 -0
  603. package/src/components/utility/VirtualList/VirtualList.tsx +369 -0
  604. package/src/components/utility/VirtualList/VirtualList.types.ts +56 -0
  605. package/src/components/utility/VirtualList/index.ts +7 -0
  606. package/src/components/utility/WorkflowStep/WorkflowStep.md +187 -0
  607. package/src/components/utility/WorkflowStep/WorkflowStep.stories.tsx +433 -0
  608. package/src/components/utility/WorkflowStep/WorkflowStep.test.stories.tsx +364 -0
  609. package/src/components/utility/WorkflowStep/WorkflowStep.tsx +506 -0
  610. package/src/components/utility/WorkflowStep/WorkflowStep.types.ts +133 -0
  611. package/src/components/utility/WorkflowStep/index.ts +13 -0
  612. package/src/components/utility/index.ts +7 -0
  613. package/src/index.ts +17 -0
  614. package/src/mui/AppBar.ts +2 -0
  615. package/src/mui/Box.ts +2 -0
  616. package/src/mui/CssBaseline.ts +2 -0
  617. package/src/mui/FormControl.ts +2 -0
  618. package/src/mui/FormControlLabel.ts +2 -0
  619. package/src/mui/Grid2.ts +2 -0
  620. package/src/mui/InputLabel.ts +2 -0
  621. package/src/mui/List.ts +2 -0
  622. package/src/mui/ListItem.ts +2 -0
  623. package/src/mui/ListItemButton.ts +2 -0
  624. package/src/mui/ListItemText.ts +2 -0
  625. package/src/mui/Stack.ts +2 -0
  626. package/src/mui/SvgIcon.ts +2 -0
  627. package/src/mui/Toolbar.ts +2 -0
  628. package/src/mui/styles.ts +2 -0
  629. package/src/mui/useMediaQuery.ts +1 -0
  630. package/src/social-login-button.tsx +199 -0
  631. package/src/user-avatar.tsx +102 -0
  632. package/src/utils/cn.ts +9 -0
  633. package/src/utils/index.ts +1 -0
@@ -0,0 +1,2086 @@
1
+ import { Alert, Box, Button, Paper, TextField,Typography } from '@mui/material';
2
+ import type { Meta, StoryObj } from '@storybook/react-vite';
3
+ import { useState } from 'react';
4
+ import { expect, userEvent, waitFor,within } from 'storybook/test';
5
+
6
+ import { Portal } from './Portal';
7
+
8
+ const meta: Meta<typeof Portal> = {
9
+ title: 'Utility/Portal/Tests',
10
+ component: Portal,
11
+ parameters: {
12
+ layout: 'centered',
13
+ chromatic: { disableSnapshot: false },
14
+ },
15
+ tags: ['autodocs', 'test', 'component:Portal'],
16
+ };
17
+
18
+ export default meta;
19
+ export type Story = StoryObj<typeof meta>;
20
+
21
+ // ================================
22
+ // INTERACTION TESTS
23
+ // ================================
24
+
25
+ export const BasicInteraction: Story = {
26
+ render: () => {
27
+ const BasicInteractionComponent = () => {
28
+ const [showPortal, setShowPortal] = useState(false);
29
+
30
+ return (
31
+ <Box data-testid="portal-container">
32
+ <Button data-testid="toggle-portal" onClick={() => setShowPortal(!showPortal)}>
33
+ {showPortal ? 'Hide' : 'Show'} Portal
34
+ </Button>
35
+
36
+ {showPortal && (
37
+ <Portal>
38
+ <Box
39
+ data-testid="portal-content"
40
+ sx={{
41
+ position: 'fixed',
42
+ top: 20,
43
+ right: 20,
44
+ p: 2,
45
+ bgcolor: 'primary.main',
46
+ color: 'white',
47
+ borderRadius: 1,
48
+ zIndex: 9999,
49
+ }}
50
+ >
51
+ Portal Content Rendered
52
+ </Box>
53
+ </Portal>
54
+ )}
55
+ </Box>
56
+ );
57
+ };
58
+
59
+ return <BasicInteractionComponent />;
60
+ },
61
+ play: async ({ canvasElement }) => {
62
+ const canvas = within(canvasElement);
63
+ const toggleButton = canvas.getByTestId('toggle-portal');
64
+
65
+ // Initially, portal content should not exist
66
+ expect(canvas.queryByTestId('portal-content')).not.toBeInTheDocument();
67
+
68
+ // Click to show portal
69
+ await userEvent.click(toggleButton);
70
+
71
+ // Portal content should now be visible
72
+ await waitFor(() => {
73
+ expect(document.querySelector('[data-testid="portal-content"]')).toBeInTheDocument();
74
+ });
75
+
76
+ // Verify portal content is rendered outside the component tree
77
+ const portalContent = document.querySelector('[data-testid="portal-content"]');
78
+ expect(portalContent?.parentElement).not.toBe(canvasElement);
79
+
80
+ // Click to hide portal
81
+ await userEvent.click(toggleButton);
82
+
83
+ // Portal content should be removed
84
+ await waitFor(() => {
85
+ expect(document.querySelector('[data-testid="portal-content"]')).not.toBeInTheDocument();
86
+ });
87
+ },
88
+ };
89
+
90
+ export const FormInteraction: Story = {
91
+ render: () => {
92
+ const SimpleInteractionComponent = () => {
93
+ const [showPortal, setShowPortal] = useState(false);
94
+ const [actionTaken, setActionTaken] = useState(false);
95
+
96
+ const handleAction = () => {
97
+ setActionTaken(true);
98
+ setShowPortal(false);
99
+ };
100
+
101
+ return (
102
+ <Box>
103
+ <Button data-testid="open-portal" onClick={() => setShowPortal(true)} variant="contained">
104
+ Open Portal
105
+ </Button>
106
+
107
+ {actionTaken && (
108
+ <Alert data-testid="action-success" severity="success" sx={{ mt: 2 }}>
109
+ Portal interaction completed successfully
110
+ </Alert>
111
+ )}
112
+
113
+ {showPortal && (
114
+ <Portal>
115
+ <Box
116
+ data-testid="portal-content"
117
+ sx={{
118
+ position: 'fixed',
119
+ top: '50%',
120
+ left: '50%',
121
+ transform: 'translate(-50%, -50%)',
122
+ bgcolor: 'background.paper',
123
+ p: 3,
124
+ borderRadius: 2,
125
+ boxShadow: 24,
126
+ minWidth: 300,
127
+ zIndex: 9999,
128
+ }}
129
+ >
130
+ <Typography variant="h6" gutterBottom>
131
+ Portal Content
132
+ </Typography>
133
+ <Typography variant="body1" sx={{ mb: 2 }}>
134
+ This content is rendered in a portal
135
+ </Typography>
136
+ <Box sx={{ display: 'flex', gap: 1, justifyContent: 'flex-end' }}>
137
+ <Button data-testid="cancel-portal" onClick={() => setShowPortal(false)}>
138
+ Cancel
139
+ </Button>
140
+ <Button data-testid="confirm-portal" onClick={handleAction} variant="contained">
141
+ Confirm
142
+ </Button>
143
+ </Box>
144
+ </Box>
145
+ </Portal>
146
+ )}
147
+ </Box>
148
+ );
149
+ };
150
+
151
+ return <SimpleInteractionComponent />;
152
+ },
153
+ play: async ({ canvasElement }) => {
154
+ const canvas = within(canvasElement);
155
+
156
+ // Open the portal
157
+ const openButton = canvas.getByTestId('open-portal');
158
+ await userEvent.click(openButton);
159
+
160
+ // Wait for portal content to appear
161
+ await waitFor(() => {
162
+ expect(document.querySelector('[data-testid="portal-content"]')).toBeInTheDocument();
163
+ });
164
+
165
+ // Interact with the portal content
166
+ const confirmButton = document.querySelector(
167
+ '[data-testid="confirm-portal"]',
168
+ ) as HTMLButtonElement;
169
+ expect(confirmButton).toBeInTheDocument();
170
+
171
+ await userEvent.click(confirmButton);
172
+
173
+ // Wait for portal to close and success message to appear
174
+ await waitFor(
175
+ () => {
176
+ // Check that the portal is closed
177
+ expect(document.querySelector('[data-testid="portal-content"]')).not.toBeInTheDocument();
178
+ // Check that success message appears
179
+ const successElement = canvas.getByTestId('action-success');
180
+ expect(successElement).toBeInTheDocument();
181
+ expect(successElement).toHaveTextContent('Portal interaction completed successfully');
182
+ },
183
+ { timeout: 3000 },
184
+ );
185
+ },
186
+ };
187
+
188
+ export const StateChange: Story = {
189
+ render: () => {
190
+ const StateChangeComponent = () => {
191
+ const [portalMode, setPortalMode] = useState<'disabled' | 'body' | 'custom'>('body');
192
+ const [customContainer, setCustomContainer] = useState<HTMLElement | null>(null);
193
+ const [showContent, setShowContent] = useState(true);
194
+
195
+ return (
196
+ <Box sx={{ display: 'flex', gap: 2, flexDirection: 'column' }}>
197
+ <Box sx={{ display: 'flex', gap: 1, flexWrap: 'wrap' }}>
198
+ <Button
199
+ data-testid="mode-disabled"
200
+ variant={portalMode === 'disabled' ? 'contained' : 'outlined'}
201
+ onClick={() => setPortalMode('disabled')}
202
+ size="small"
203
+ >
204
+ Disabled
205
+ </Button>
206
+ <Button
207
+ data-testid="mode-body"
208
+ variant={portalMode === 'body' ? 'contained' : 'outlined'}
209
+ onClick={() => setPortalMode('body')}
210
+ size="small"
211
+ >
212
+ Body Portal
213
+ </Button>
214
+ <Button
215
+ data-testid="mode-custom"
216
+ variant={portalMode === 'custom' ? 'contained' : 'outlined'}
217
+ onClick={() => setPortalMode('custom')}
218
+ size="small"
219
+ >
220
+ Custom Container
221
+ </Button>
222
+ <Button
223
+ data-testid="toggle-content"
224
+ onClick={() => setShowContent(!showContent)}
225
+ color="secondary"
226
+ size="small"
227
+ >
228
+ {showContent ? 'Hide' : 'Show'} Content
229
+ </Button>
230
+ </Box>
231
+
232
+ <Box sx={{ display: 'flex', gap: 2 }}>
233
+ <Paper sx={{ p: 2, flex: 1 }}>
234
+ <Typography variant="h6">Source Component</Typography>
235
+ <Typography>Portal mode: {portalMode}</Typography>
236
+ </Paper>
237
+
238
+ <Paper
239
+ ref={(el) => setCustomContainer(el)}
240
+ data-testid="custom-container"
241
+ sx={{ p: 2, flex: 1, bgcolor: 'grey.100', position: 'relative' }}
242
+ >
243
+ <Typography variant="h6">Custom Container</Typography>
244
+ <Typography variant="body2" color="text.secondary">
245
+ Target for custom portal
246
+ </Typography>
247
+ </Paper>
248
+ </Box>
249
+
250
+ {showContent && (
251
+ <Portal
252
+ disablePortal={portalMode === 'disabled'}
253
+ container={portalMode === 'custom' ? customContainer : undefined}
254
+ >
255
+ <Box
256
+ data-testid="portal-state-content"
257
+ sx={{
258
+ position: portalMode === 'disabled' ? 'static' : 'absolute',
259
+ top: portalMode === 'disabled' ? 'auto' : 10,
260
+ right: portalMode === 'disabled' ? 'auto' : 10,
261
+ bgcolor: 'primary.main',
262
+ color: 'white',
263
+ p: 1,
264
+ borderRadius: 1,
265
+ fontSize: '0.875rem',
266
+ zIndex: portalMode === 'disabled' ? 'auto' : 1000,
267
+ }}
268
+ >
269
+ Mode: {portalMode}
270
+ </Box>
271
+ </Portal>
272
+ )}
273
+ </Box>
274
+ );
275
+ };
276
+
277
+ return <StateChangeComponent />;
278
+ },
279
+ play: async ({ canvasElement }) => {
280
+ const canvas = within(canvasElement);
281
+
282
+ // Test body portal mode (default)
283
+ let content = document.querySelector('[data-testid="portal-state-content"]');
284
+ expect(content).toBeInTheDocument();
285
+ expect(content?.textContent).toBe('Mode: body');
286
+
287
+ // Switch to disabled mode
288
+ await userEvent.click(canvas.getByTestId('mode-disabled'));
289
+ await waitFor(() => {
290
+ content = canvas.queryByTestId('portal-state-content');
291
+ expect(content).toBeInTheDocument();
292
+ expect(content?.textContent).toBe('Mode: disabled');
293
+ });
294
+
295
+ // Switch to custom container mode
296
+ await userEvent.click(canvas.getByTestId('mode-custom'));
297
+ await waitFor(() => {
298
+ content = document.querySelector('[data-testid="portal-state-content"]');
299
+ expect(content?.textContent).toBe('Mode: custom');
300
+
301
+ // Verify it's in the custom container
302
+ const customContainer = canvas.getByTestId('custom-container');
303
+ expect(customContainer).toContainElement(content);
304
+ });
305
+
306
+ // Test content toggle
307
+ await userEvent.click(canvas.getByTestId('toggle-content'));
308
+ await waitFor(() => {
309
+ expect(
310
+ document.querySelector('[data-testid="portal-state-content"]'),
311
+ ).not.toBeInTheDocument();
312
+ });
313
+
314
+ // Show content again
315
+ await userEvent.click(canvas.getByTestId('toggle-content'));
316
+ await waitFor(() => {
317
+ expect(document.querySelector('[data-testid="portal-state-content"]')).toBeInTheDocument();
318
+ });
319
+ },
320
+ };
321
+
322
+ // ================================
323
+ // ACCESSIBILITY TESTS
324
+ // ================================
325
+
326
+ export const KeyboardNavigation: Story = {
327
+ render: () => {
328
+ const KeyboardNavigationComponent = () => {
329
+ const [showPortal, setShowPortal] = useState(false);
330
+
331
+ const handleKeyDown = (e: React.KeyboardEvent) => {
332
+ if (e.key === 'Escape') {
333
+ setShowPortal(false);
334
+ }
335
+ };
336
+
337
+ return (
338
+ <Box>
339
+ <Button
340
+ data-testid="keyboard-trigger"
341
+ onClick={() => setShowPortal(true)}
342
+ onKeyDown={(e) => e.key === 'Enter' && setShowPortal(true)}
343
+ >
344
+ Open Portal (Enter or Click)
345
+ </Button>
346
+
347
+ {showPortal && (
348
+ <Portal>
349
+ <Box
350
+ data-testid="keyboard-portal"
351
+ onKeyDown={handleKeyDown}
352
+ tabIndex={-1}
353
+ sx={{
354
+ position: 'fixed',
355
+ top: '50%',
356
+ left: '50%',
357
+ transform: 'translate(-50%, -50%)',
358
+ bgcolor: 'background.paper',
359
+ p: 3,
360
+ borderRadius: 2,
361
+ boxShadow: 24,
362
+ minWidth: 300,
363
+ zIndex: 9999,
364
+ outline: 'none',
365
+ }}
366
+ >
367
+ <Typography variant="h6" gutterBottom>
368
+ Keyboard Accessible Portal
369
+ </Typography>
370
+
371
+ <Box sx={{ mb: 2 }}>
372
+ <Button data-testid="first-button" variant="outlined" sx={{ mr: 1 }}>
373
+ First Button
374
+ </Button>
375
+ <Button data-testid="second-button" variant="outlined">
376
+ Second Button
377
+ </Button>
378
+ </Box>
379
+
380
+ <Typography variant="body2" color="text.secondary">
381
+ Press Escape to close, Tab to navigate
382
+ </Typography>
383
+
384
+ <Button
385
+ data-testid="close-button"
386
+ onClick={() => setShowPortal(false)}
387
+ sx={{ mt: 2 }}
388
+ variant="contained"
389
+ size="small"
390
+ >
391
+ Close
392
+ </Button>
393
+ </Box>
394
+ </Portal>
395
+ )}
396
+ </Box>
397
+ );
398
+ };
399
+
400
+ return <KeyboardNavigationComponent />;
401
+ },
402
+ play: async ({ canvasElement }) => {
403
+ const canvas = within(canvasElement);
404
+ const triggerButton = canvas.getByTestId('keyboard-trigger');
405
+
406
+ // Focus and press Enter to open portal
407
+ triggerButton.focus();
408
+ await userEvent.keyboard('{Enter}');
409
+
410
+ // Wait for portal to appear
411
+ await waitFor(() => {
412
+ expect(document.querySelector('[data-testid="keyboard-portal"]')).toBeInTheDocument();
413
+ });
414
+
415
+ // Test Tab navigation within portal
416
+ await userEvent.keyboard('{Tab}');
417
+ const firstButton = document.querySelector('[data-testid="first-button"]') as HTMLButtonElement;
418
+ expect(firstButton).toHaveFocus();
419
+
420
+ await userEvent.keyboard('{Tab}');
421
+ const secondButton = document.querySelector('[data-testid="second-button"]') as HTMLButtonElement;
422
+ expect(secondButton).toHaveFocus();
423
+
424
+ await userEvent.keyboard('{Tab}');
425
+ const closeButton = document.querySelector('[data-testid="close-button"]') as HTMLButtonElement;
426
+ expect(closeButton).toHaveFocus();
427
+
428
+ // Test Escape key to close
429
+ await userEvent.keyboard('{Escape}');
430
+
431
+ await waitFor(() => {
432
+ expect(document.querySelector('[data-testid="keyboard-portal"]')).not.toBeInTheDocument();
433
+ });
434
+ },
435
+ };
436
+
437
+ export const ScreenReader: Story = {
438
+ render: () => {
439
+ const ScreenReaderComponent = () => {
440
+ const [announcements, setAnnouncements] = useState<string[]>([]);
441
+ const [showPortal, setShowPortal] = useState(false);
442
+
443
+ const announce = (message: string) => {
444
+ setAnnouncements((prev) => [...prev, message]);
445
+ };
446
+
447
+ return (
448
+ <Box>
449
+ <Box sx={{ mb: 2 }}>
450
+ <Typography variant="h6" id="portal-demo-title">
451
+ Screen Reader Portal Demo
452
+ </Typography>
453
+ <Typography variant="body2" color="text.secondary">
454
+ This demo shows proper ARIA attributes and announcements for portals
455
+ </Typography>
456
+ </Box>
457
+
458
+ <Button
459
+ data-testid="sr-trigger"
460
+ onClick={() => {
461
+ setShowPortal(true);
462
+ announce('Portal dialog opened');
463
+ }}
464
+ aria-describedby="portal-demo-title"
465
+ >
466
+ Open Accessible Portal
467
+ </Button>
468
+
469
+ {/* Screen reader announcements */}
470
+ <Box
471
+ data-testid="announcements"
472
+ role="log"
473
+ aria-live="polite"
474
+ aria-label="Screen reader announcements"
475
+ sx={{ position: 'absolute', left: '-10000px' }}
476
+ >
477
+ {announcements.map((msg, index) => (
478
+ <div key={index}>{msg}</div>
479
+ ))}
480
+ </Box>
481
+
482
+ {showPortal && (
483
+ <Portal>
484
+ <Box
485
+ data-testid="sr-portal"
486
+ role="dialog"
487
+ aria-modal="true"
488
+ aria-labelledby="portal-title"
489
+ aria-describedby="portal-description"
490
+ sx={{
491
+ position: 'fixed',
492
+ top: '50%',
493
+ left: '50%',
494
+ transform: 'translate(-50%, -50%)',
495
+ bgcolor: 'background.paper',
496
+ p: 3,
497
+ borderRadius: 2,
498
+ boxShadow: 24,
499
+ minWidth: 400,
500
+ zIndex: 9999,
501
+ }}
502
+ >
503
+ <Typography id="portal-title" variant="h6" component="h2" gutterBottom>
504
+ Accessible Portal Dialog
505
+ </Typography>
506
+
507
+ <Typography id="portal-description" variant="body1" sx={{ mb: 2 }}>
508
+ This portal has proper ARIA attributes including role, aria-modal,
509
+ aria-labelledby, and aria-describedby for screen reader accessibility.
510
+ </Typography>
511
+
512
+ <Box sx={{ mb: 2 }}>
513
+ <TextField
514
+ data-testid="portal-input"
515
+ label="Accessible Input"
516
+ fullWidth
517
+ size="small"
518
+ InputProps={{
519
+ 'aria-describedby': 'input-help',
520
+ }}
521
+ />
522
+ <Typography
523
+ id="input-help"
524
+ variant="caption"
525
+ color="text.secondary"
526
+ sx={{ display: 'block', mt: 0.5 }}
527
+ >
528
+ This input is properly labeled and described
529
+ </Typography>
530
+ </Box>
531
+
532
+ <Box sx={{ display: 'flex', gap: 1, justifyContent: 'flex-end' }}>
533
+ <Button
534
+ data-testid="sr-cancel"
535
+ onClick={() => {
536
+ setShowPortal(false);
537
+ announce('Portal dialog cancelled and closed');
538
+ }}
539
+ >
540
+ Cancel
541
+ </Button>
542
+ <Button
543
+ data-testid="sr-confirm"
544
+ variant="contained"
545
+ onClick={() => {
546
+ setShowPortal(false);
547
+ announce('Portal dialog confirmed and closed');
548
+ }}
549
+ >
550
+ OK
551
+ </Button>
552
+ </Box>
553
+ </Box>
554
+ </Portal>
555
+ )}
556
+ </Box>
557
+ );
558
+ };
559
+
560
+ return <ScreenReaderComponent />;
561
+ },
562
+ play: async ({ canvasElement }) => {
563
+ const canvas = within(canvasElement);
564
+
565
+ // Open the portal
566
+ const triggerButton = canvas.getByTestId('sr-trigger');
567
+ await userEvent.click(triggerButton);
568
+
569
+ // Wait for portal to appear
570
+ await waitFor(() => {
571
+ expect(document.querySelector('[data-testid="sr-portal"]')).toBeInTheDocument();
572
+ });
573
+
574
+ // Verify ARIA attributes
575
+ const portalDialog = document.querySelector('[data-testid="sr-portal"]');
576
+ expect(portalDialog).toHaveAttribute('role', 'dialog');
577
+ expect(portalDialog).toHaveAttribute('aria-modal', 'true');
578
+ expect(portalDialog).toHaveAttribute('aria-labelledby', 'portal-title');
579
+ expect(portalDialog).toHaveAttribute('aria-describedby', 'portal-description');
580
+
581
+ // Verify title and description elements exist
582
+ expect(document.getElementById('portal-title')).toBeInTheDocument();
583
+ expect(document.getElementById('portal-description')).toBeInTheDocument();
584
+
585
+ // Test input accessibility
586
+ const input = document.querySelector('[data-testid="portal-input"] input');
587
+ expect(input).toHaveAttribute('aria-describedby', 'input-help');
588
+
589
+ // Verify announcements area
590
+ const announcements = canvas.getByTestId('announcements');
591
+ expect(announcements).toHaveAttribute('role', 'log');
592
+ expect(announcements).toHaveAttribute('aria-live', 'polite');
593
+
594
+ // Test closing with proper announcement
595
+ const confirmButton = document.querySelector('[data-testid="sr-confirm"]') as HTMLButtonElement;
596
+ await userEvent.click(confirmButton);
597
+
598
+ await waitFor(() => {
599
+ expect(document.querySelector('[data-testid="sr-portal"]')).not.toBeInTheDocument();
600
+ });
601
+
602
+ // Verify announcement was made
603
+ expect(announcements).toHaveTextContent('Portal dialog confirmed and closed');
604
+ },
605
+ };
606
+
607
+ export const FocusManagement: Story = {
608
+ render: () => {
609
+ const FocusManagementComponent = () => {
610
+ const [showPortal, setShowPortal] = useState(false);
611
+ const [previousFocus, setPreviousFocus] = useState<HTMLElement | null>(null);
612
+
613
+ const openPortal = () => {
614
+ setPreviousFocus(document.activeElement as HTMLElement);
615
+ setShowPortal(true);
616
+ };
617
+
618
+ const closePortal = () => {
619
+ setShowPortal(false);
620
+ // Restore focus after a brief delay to allow DOM updates
621
+ window.setTimeout(() => {
622
+ previousFocus?.focus();
623
+ }, 10);
624
+ };
625
+
626
+ return (
627
+ <Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
628
+ <TextField data-testid="before-portal" label="Input Before Portal" size="small" />
629
+
630
+ <Button data-testid="focus-trigger" onClick={openPortal} variant="contained">
631
+ Open Focus Managed Portal
632
+ </Button>
633
+
634
+ <TextField data-testid="after-portal" label="Input After Portal" size="small" />
635
+
636
+ {showPortal && (
637
+ <Portal>
638
+ <Box
639
+ data-testid="focus-portal"
640
+ sx={{
641
+ position: 'fixed',
642
+ top: '50%',
643
+ left: '50%',
644
+ transform: 'translate(-50%, -50%)',
645
+ bgcolor: 'background.paper',
646
+ p: 3,
647
+ borderRadius: 2,
648
+ boxShadow: 24,
649
+ minWidth: 400,
650
+ zIndex: 9999,
651
+ }}
652
+ >
653
+ <Typography variant="h6" gutterBottom>
654
+ Focus Management Portal
655
+ </Typography>
656
+
657
+ <Box sx={{ mb: 2 }}>
658
+ <TextField
659
+ data-testid="first-portal-input"
660
+ label="First Input"
661
+ fullWidth
662
+ size="small"
663
+ sx={{ mb: 1 }}
664
+ autoFocus
665
+ />
666
+ <TextField
667
+ data-testid="second-portal-input"
668
+ label="Second Input"
669
+ fullWidth
670
+ size="small"
671
+ />
672
+ </Box>
673
+
674
+ <Box sx={{ display: 'flex', gap: 1, justifyContent: 'flex-end' }}>
675
+ <Button data-testid="focus-cancel" onClick={closePortal}>
676
+ Cancel
677
+ </Button>
678
+ <Button data-testid="focus-save" variant="contained" onClick={closePortal}>
679
+ Save
680
+ </Button>
681
+ </Box>
682
+ </Box>
683
+ </Portal>
684
+ )}
685
+ </Box>
686
+ );
687
+ };
688
+
689
+ return <FocusManagementComponent />;
690
+ },
691
+ play: async ({ canvasElement }) => {
692
+ const canvas = within(canvasElement);
693
+
694
+ // Focus on the trigger button
695
+ const triggerButton = canvas.getByTestId('focus-trigger');
696
+ triggerButton.focus();
697
+ expect(triggerButton).toHaveFocus();
698
+
699
+ // Open portal
700
+ await userEvent.click(triggerButton);
701
+
702
+ // Wait for portal to appear
703
+ await waitFor(() => {
704
+ expect(document.querySelector('[data-testid="focus-portal"]')).toBeInTheDocument();
705
+ });
706
+
707
+ // Verify focus moved to first input in portal
708
+ await waitFor(() => {
709
+ const firstInput = document.querySelector(
710
+ '[data-testid="first-portal-input"] input',
711
+ ) as HTMLInputElement;
712
+ expect(firstInput).toHaveFocus();
713
+ });
714
+
715
+ // Test tab navigation within portal
716
+ await userEvent.keyboard('{Tab}');
717
+ const secondInput = document.querySelector(
718
+ '[data-testid="second-portal-input"] input',
719
+ ) as HTMLInputElement;
720
+ expect(secondInput).toHaveFocus();
721
+
722
+ await userEvent.keyboard('{Tab}');
723
+ const cancelButton = document.querySelector(
724
+ '[data-testid="focus-cancel"]',
725
+ ) as HTMLButtonElement;
726
+ expect(cancelButton).toHaveFocus();
727
+
728
+ await userEvent.keyboard('{Tab}');
729
+ const saveButton = document.querySelector('[data-testid="focus-save"]') as HTMLButtonElement;
730
+ expect(saveButton).toHaveFocus();
731
+
732
+ // Close portal and verify focus returns to trigger
733
+ await userEvent.click(saveButton);
734
+
735
+ await waitFor(() => {
736
+ expect(document.querySelector('[data-testid="focus-portal"]')).not.toBeInTheDocument();
737
+ });
738
+
739
+ // Verify focus returned to trigger button
740
+ await waitFor(
741
+ () => {
742
+ expect(triggerButton).toHaveFocus();
743
+ },
744
+ { timeout: 200 },
745
+ );
746
+ },
747
+ };
748
+
749
+ // ================================
750
+ // VISUAL TESTS
751
+ // ================================
752
+
753
+ export const ResponsiveDesign: Story = {
754
+ render: () => {
755
+ const ResponsiveComponent = () => {
756
+ const [showPortal, setShowPortal] = useState(true);
757
+ const [viewport, setViewport] = useState('desktop');
758
+
759
+ return (
760
+ <Box>
761
+ <Box sx={{ mb: 2, display: 'flex', gap: 1, flexWrap: 'wrap' }}>
762
+ {['mobile', 'tablet', 'desktop'].map((size) => (
763
+ <Button
764
+ key={size}
765
+ data-testid={`viewport-${size}`}
766
+ variant={viewport === size ? 'contained' : 'outlined'}
767
+ onClick={() => setViewport(size)}
768
+ size="small"
769
+ >
770
+ {size}
771
+ </Button>
772
+ ))}
773
+ <Button
774
+ data-testid="toggle-responsive-portal"
775
+ onClick={() => setShowPortal(!showPortal)}
776
+ color="secondary"
777
+ size="small"
778
+ >
779
+ {showPortal ? 'Hide' : 'Show'} Portal
780
+ </Button>
781
+ </Box>
782
+
783
+ {showPortal && (
784
+ <Portal>
785
+ <Box
786
+ data-testid="responsive-portal"
787
+ sx={{
788
+ position: 'fixed',
789
+ top: '50%',
790
+ left: '50%',
791
+ transform: 'translate(-50%, -50%)',
792
+ bgcolor: 'background.paper',
793
+ p: viewport === 'mobile' ? 1 : viewport === 'tablet' ? 2 : 3,
794
+ borderRadius: 1,
795
+ boxShadow: 24,
796
+ width: viewport === 'mobile' ? '90vw' : viewport === 'tablet' ? '60vw' : '40vw',
797
+ maxWidth: viewport === 'mobile' ? 320 : viewport === 'tablet' ? 600 : 800,
798
+ maxHeight: '90vh',
799
+ overflow: 'auto',
800
+ zIndex: 9999,
801
+ }}
802
+ >
803
+ <Typography variant={viewport === 'mobile' ? 'subtitle1' : 'h6'} gutterBottom>
804
+ Responsive Portal Content ({viewport})
805
+ </Typography>
806
+
807
+ <Typography
808
+ variant="body2"
809
+ sx={{
810
+ mb: 2,
811
+ fontSize: viewport === 'mobile' ? '0.75rem' : '0.875rem',
812
+ }}
813
+ >
814
+ This portal adapts its size and padding based on viewport. Content scales
815
+ appropriately for different screen sizes.
816
+ </Typography>
817
+
818
+ <Box
819
+ sx={{
820
+ display: 'grid',
821
+ gridTemplateColumns: viewport === 'mobile' ? '1fr' : 'repeat(2, 1fr)',
822
+ gap: 1,
823
+ mb: 2,
824
+ }}
825
+ >
826
+ <TextField
827
+ label="Field 1"
828
+ size={viewport === 'mobile' ? 'small' : 'medium'}
829
+ fullWidth
830
+ />
831
+ <TextField
832
+ label="Field 2"
833
+ size={viewport === 'mobile' ? 'small' : 'medium'}
834
+ fullWidth
835
+ />
836
+ </Box>
837
+
838
+ <Box
839
+ sx={{
840
+ display: 'flex',
841
+ gap: 1,
842
+ justifyContent: 'flex-end',
843
+ flexDirection: viewport === 'mobile' ? 'column' : 'row',
844
+ }}
845
+ >
846
+ <Button size={viewport === 'mobile' ? 'small' : 'medium'}>Cancel</Button>
847
+ <Button variant="contained" size={viewport === 'mobile' ? 'small' : 'medium'}>
848
+ Confirm
849
+ </Button>
850
+ </Box>
851
+ </Box>
852
+ </Portal>
853
+ )}
854
+ </Box>
855
+ );
856
+ };
857
+
858
+ return <ResponsiveComponent />;
859
+ },
860
+ play: async ({ canvasElement }) => {
861
+ const canvas = within(canvasElement);
862
+
863
+ // Test different viewport configurations
864
+ for (const viewport of ['mobile', 'tablet', 'desktop']) {
865
+ await userEvent.click(canvas.getByTestId(`viewport-${viewport}`));
866
+
867
+ await waitFor(() => {
868
+ const portal = document.querySelector('[data-testid="responsive-portal"]');
869
+ expect(portal).toBeInTheDocument();
870
+
871
+ // Verify content adapts to viewport
872
+ expect(portal).toHaveTextContent(`Responsive Portal Content (${viewport})`);
873
+ });
874
+ }
875
+
876
+ // Test portal visibility toggle
877
+ await userEvent.click(canvas.getByTestId('toggle-responsive-portal'));
878
+ await waitFor(() => {
879
+ expect(document.querySelector('[data-testid="responsive-portal"]')).not.toBeInTheDocument();
880
+ });
881
+
882
+ await userEvent.click(canvas.getByTestId('toggle-responsive-portal'));
883
+ await waitFor(() => {
884
+ expect(document.querySelector('[data-testid="responsive-portal"]')).toBeInTheDocument();
885
+ });
886
+ },
887
+ };
888
+
889
+ export const ThemeVariations: Story = {
890
+ render: () => {
891
+ const ThemeComponent = () => {
892
+ const [theme, setTheme] = useState('light');
893
+ const [showPortal, setShowPortal] = useState(true);
894
+
895
+ return (
896
+ <Box>
897
+ <Box sx={{ mb: 2, display: 'flex', gap: 1 }}>
898
+ {['light', 'dark', 'high-contrast'].map((themeMode) => (
899
+ <Button
900
+ key={themeMode}
901
+ data-testid={`theme-${themeMode}`}
902
+ variant={theme === themeMode ? 'contained' : 'outlined'}
903
+ onClick={() => setTheme(themeMode)}
904
+ size="small"
905
+ >
906
+ {themeMode.charAt(0).toUpperCase() + themeMode.slice(1)}
907
+ </Button>
908
+ ))}
909
+ <Button
910
+ data-testid="toggle-theme-portal"
911
+ onClick={() => setShowPortal(!showPortal)}
912
+ color="secondary"
913
+ size="small"
914
+ >
915
+ {showPortal ? 'Hide' : 'Show'} Portal
916
+ </Button>
917
+ </Box>
918
+
919
+ {showPortal && (
920
+ <Portal>
921
+ <Box
922
+ data-testid="theme-portal"
923
+ sx={{
924
+ position: 'fixed',
925
+ top: '50%',
926
+ left: '50%',
927
+ transform: 'translate(-50%, -50%)',
928
+ bgcolor:
929
+ theme === 'dark'
930
+ ? 'grey.900'
931
+ : theme === 'high-contrast'
932
+ ? '#000'
933
+ : 'background.paper',
934
+ color:
935
+ theme === 'dark'
936
+ ? 'grey.100'
937
+ : theme === 'high-contrast'
938
+ ? '#fff'
939
+ : 'text.primary',
940
+ p: 3,
941
+ borderRadius: theme === 'high-contrast' ? 0 : 2,
942
+ boxShadow: theme === 'high-contrast' ? 'none' : 24,
943
+ border: theme === 'high-contrast' ? '2px solid #fff' : 'none',
944
+ minWidth: 350,
945
+ zIndex: 9999,
946
+ }}
947
+ >
948
+ <Typography variant="h6" gutterBottom>
949
+ {theme.charAt(0).toUpperCase() + theme.slice(1)} Theme Portal
950
+ </Typography>
951
+
952
+ <Typography variant="body2" sx={{ mb: 2 }}>
953
+ This portal adapts to different theme modes including colors, contrast levels, and
954
+ visual styling.
955
+ </Typography>
956
+
957
+ <Box
958
+ sx={{
959
+ mb: 2,
960
+ p: 1.5,
961
+ bgcolor:
962
+ theme === 'dark'
963
+ ? 'grey.800'
964
+ : theme === 'high-contrast'
965
+ ? '#333'
966
+ : 'grey.50',
967
+ borderRadius: theme === 'high-contrast' ? 0 : 1,
968
+ border: theme === 'high-contrast' ? '1px solid #fff' : 'none',
969
+ }}
970
+ >
971
+ <Typography variant="body2" sx={{ fontWeight: 'bold' }}>
972
+ Current Theme: {theme}
973
+ </Typography>
974
+ <Typography variant="caption">
975
+ Background and text colors adapt automatically
976
+ </Typography>
977
+ </Box>
978
+
979
+ <Box sx={{ display: 'flex', gap: 1, justifyContent: 'flex-end' }}>
980
+ <Button
981
+ sx={{
982
+ color: theme === 'high-contrast' ? '#fff' : 'inherit',
983
+ borderColor: theme === 'high-contrast' ? '#fff' : 'inherit',
984
+ '&:hover': {
985
+ bgcolor: theme === 'high-contrast' ? '#333' : 'inherit',
986
+ },
987
+ }}
988
+ >
989
+ Cancel
990
+ </Button>
991
+ <Button
992
+ variant="contained"
993
+ sx={{
994
+ bgcolor: theme === 'high-contrast' ? '#fff' : 'primary.main',
995
+ color: theme === 'high-contrast' ? '#000' : 'primary.contrastText',
996
+ '&:hover': {
997
+ bgcolor: theme === 'high-contrast' ? '#ccc' : 'primary.dark',
998
+ },
999
+ }}
1000
+ >
1001
+ Apply
1002
+ </Button>
1003
+ </Box>
1004
+ </Box>
1005
+ </Portal>
1006
+ )}
1007
+ </Box>
1008
+ );
1009
+ };
1010
+
1011
+ return <ThemeComponent />;
1012
+ },
1013
+ play: async ({ canvasElement }) => {
1014
+ const canvas = within(canvasElement);
1015
+
1016
+ // Test different theme variations
1017
+ for (const theme of ['light', 'dark', 'high-contrast']) {
1018
+ await userEvent.click(canvas.getByTestId(`theme-${theme}`));
1019
+
1020
+ await waitFor(() => {
1021
+ const portal = document.querySelector('[data-testid="theme-portal"]');
1022
+ expect(portal).toBeInTheDocument();
1023
+ expect(portal).toHaveTextContent(
1024
+ `${theme.charAt(0).toUpperCase() + theme.slice(1)} Theme Portal`,
1025
+ );
1026
+ });
1027
+ }
1028
+
1029
+ // Test portal toggle
1030
+ await userEvent.click(canvas.getByTestId('toggle-theme-portal'));
1031
+ await waitFor(() => {
1032
+ expect(document.querySelector('[data-testid="theme-portal"]')).not.toBeInTheDocument();
1033
+ });
1034
+
1035
+ await userEvent.click(canvas.getByTestId('toggle-theme-portal'));
1036
+ await waitFor(() => {
1037
+ expect(document.querySelector('[data-testid="theme-portal"]')).toBeInTheDocument();
1038
+ });
1039
+ },
1040
+ };
1041
+
1042
+ export const VisualStates: Story = {
1043
+ render: () => {
1044
+ const VisualStatesComponent = () => {
1045
+ const [state, setState] = useState<'default' | 'loading' | 'error' | 'success'>('default');
1046
+ const [showPortal, setShowPortal] = useState(true);
1047
+
1048
+ const stateConfig = {
1049
+ default: { color: 'primary', icon: '📄' },
1050
+ loading: { color: 'info', icon: '⏳' },
1051
+ error: { color: 'error', icon: '❌' },
1052
+ success: { color: 'success', icon: '✅' },
1053
+ };
1054
+
1055
+ return (
1056
+ <Box>
1057
+ <Box sx={{ mb: 2, display: 'flex', gap: 1, flexWrap: 'wrap' }}>
1058
+ {Object.keys(stateConfig).map((stateName) => (
1059
+ <Button
1060
+ key={stateName}
1061
+ data-testid={`state-${stateName}`}
1062
+ variant={state === stateName ? 'contained' : 'outlined'}
1063
+ onClick={() => setState(stateName as 'default' | 'loading' | 'error' | 'success')}
1064
+ size="small"
1065
+ >
1066
+ {stateName}
1067
+ </Button>
1068
+ ))}
1069
+ <Button
1070
+ data-testid="toggle-visual-portal"
1071
+ onClick={() => setShowPortal(!showPortal)}
1072
+ color="secondary"
1073
+ size="small"
1074
+ >
1075
+ {showPortal ? 'Hide' : 'Show'} Portal
1076
+ </Button>
1077
+ </Box>
1078
+
1079
+ {showPortal && (
1080
+ <Portal>
1081
+ <Box
1082
+ data-testid="visual-states-portal"
1083
+ sx={{
1084
+ position: 'fixed',
1085
+ top: '50%',
1086
+ left: '50%',
1087
+ transform: 'translate(-50%, -50%)',
1088
+ bgcolor: 'background.paper',
1089
+ p: 3,
1090
+ borderRadius: 2,
1091
+ boxShadow: 24,
1092
+ minWidth: 300,
1093
+ zIndex: 9999,
1094
+ border: state === 'error' ? 2 : 1,
1095
+ borderColor: state === 'error' ? 'error.main' : 'divider',
1096
+ }}
1097
+ >
1098
+ <Box sx={{ display: 'flex', alignItems: 'center', mb: 2 }}>
1099
+ <Typography variant="h4" sx={{ mr: 1 }}>
1100
+ {stateConfig[state].icon}
1101
+ </Typography>
1102
+ <Typography variant="h6">
1103
+ Portal State: {state.charAt(0).toUpperCase() + state.slice(1)}
1104
+ </Typography>
1105
+ </Box>
1106
+
1107
+ {state === 'loading' && (
1108
+ <Box sx={{ mb: 2 }}>
1109
+ <Typography variant="body2" color="text.secondary">
1110
+ Content is loading, please wait...
1111
+ </Typography>
1112
+ <Box sx={{ mt: 1, display: 'flex', alignItems: 'center' }}>
1113
+ <Box
1114
+ sx={{
1115
+ width: 20,
1116
+ height: 20,
1117
+ border: '2px solid',
1118
+ borderColor: 'primary.light',
1119
+ borderTopColor: 'primary.main',
1120
+ borderRadius: '50%',
1121
+ animation: 'spin 1s linear infinite',
1122
+ mr: 1,
1123
+ '@keyframes spin': {
1124
+ '0%': { transform: 'rotate(0deg)' },
1125
+ '100%': { transform: 'rotate(360deg)' },
1126
+ },
1127
+ }}
1128
+ />
1129
+ <Typography variant="caption">Loading...</Typography>
1130
+ </Box>
1131
+ </Box>
1132
+ )}
1133
+
1134
+ {state === 'error' && (
1135
+ <Alert severity="error" sx={{ mb: 2 }}>
1136
+ <Typography variant="body2">
1137
+ An error occurred while processing your request.
1138
+ </Typography>
1139
+ </Alert>
1140
+ )}
1141
+
1142
+ {state === 'success' && (
1143
+ <Alert severity="success" sx={{ mb: 2 }}>
1144
+ <Typography variant="body2">Operation completed successfully!</Typography>
1145
+ </Alert>
1146
+ )}
1147
+
1148
+ {state === 'default' && (
1149
+ <Typography variant="body2" sx={{ mb: 2 }}>
1150
+ This is the default state of the portal with standard styling and no special
1151
+ indicators.
1152
+ </Typography>
1153
+ )}
1154
+
1155
+ <Box sx={{ display: 'flex', gap: 1, justifyContent: 'flex-end' }}>
1156
+ {state !== 'loading' && (
1157
+ <Button
1158
+ color={
1159
+ stateConfig[state].color as
1160
+ | 'primary'
1161
+ | 'secondary'
1162
+ | 'error'
1163
+ | 'warning'
1164
+ | 'info'
1165
+ | 'success'
1166
+ }
1167
+ variant={state === 'error' ? 'outlined' : 'contained'}
1168
+ >
1169
+ {state === 'error' ? 'Retry' : state === 'success' ? 'Done' : 'OK'}
1170
+ </Button>
1171
+ )}
1172
+ </Box>
1173
+ </Box>
1174
+ </Portal>
1175
+ )}
1176
+ </Box>
1177
+ );
1178
+ };
1179
+
1180
+ return <VisualStatesComponent />;
1181
+ },
1182
+ play: async ({ canvasElement }) => {
1183
+ const canvas = within(canvasElement);
1184
+
1185
+ // Test each visual state
1186
+ const states = ['default', 'loading', 'error', 'success'];
1187
+
1188
+ for (const state of states) {
1189
+ await userEvent.click(canvas.getByTestId(`state-${state}`));
1190
+
1191
+ await waitFor(() => {
1192
+ const portal = document.querySelector('[data-testid="visual-states-portal"]');
1193
+ expect(portal).toBeInTheDocument();
1194
+ expect(portal).toHaveTextContent(
1195
+ `Portal State: ${state.charAt(0).toUpperCase() + state.slice(1)}`,
1196
+ );
1197
+
1198
+ // Verify state-specific content
1199
+ if (state === 'loading') {
1200
+ expect(portal).toHaveTextContent('Content is loading, please wait...');
1201
+ } else if (state === 'error') {
1202
+ expect(portal?.querySelector('[role="alert"]')).toBeInTheDocument();
1203
+ } else if (state === 'success') {
1204
+ expect(portal?.querySelector('[role="alert"]')).toBeInTheDocument();
1205
+ expect(portal).toHaveTextContent('Operation completed successfully!');
1206
+ }
1207
+ });
1208
+ }
1209
+
1210
+ // Test portal toggle
1211
+ await userEvent.click(canvas.getByTestId('toggle-visual-portal'));
1212
+ await waitFor(() => {
1213
+ expect(
1214
+ document.querySelector('[data-testid="visual-states-portal"]'),
1215
+ ).not.toBeInTheDocument();
1216
+ });
1217
+
1218
+ await userEvent.click(canvas.getByTestId('toggle-visual-portal'));
1219
+ await waitFor(() => {
1220
+ expect(document.querySelector('[data-testid="visual-states-portal"]')).toBeInTheDocument();
1221
+ });
1222
+ },
1223
+ };
1224
+
1225
+ // ================================
1226
+ // PERFORMANCE TESTS
1227
+ // ================================
1228
+
1229
+ export const Performance: Story = {
1230
+ render: () => {
1231
+ const PerformanceComponent = () => {
1232
+ const [portalCount, setPortalCount] = useState(1);
1233
+ const [showPortals, setShowPortals] = useState(false);
1234
+ const [renderTime, setRenderTime] = useState<number | null>(null);
1235
+
1236
+ const handleTogglePortals = async () => {
1237
+ const startTime = window.performance.now();
1238
+ setShowPortals(!showPortals);
1239
+
1240
+ // Use requestAnimationFrame to measure after render
1241
+ window.requestAnimationFrame(() => {
1242
+ const endTime = window.performance.now();
1243
+ setRenderTime(endTime - startTime);
1244
+ });
1245
+ };
1246
+
1247
+ const portals = Array.from({ length: portalCount }, (_, i) => i);
1248
+
1249
+ return (
1250
+ <Box>
1251
+ <Box sx={{ mb: 2, display: 'flex', gap: 2, alignItems: 'center', flexWrap: 'wrap' }}>
1252
+ <Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
1253
+ <Typography>Portal Count:</Typography>
1254
+ <Button
1255
+ data-testid="decrease-count"
1256
+ size="small"
1257
+ onClick={() => setPortalCount(Math.max(1, portalCount - 1))}
1258
+ disabled={portalCount <= 1}
1259
+ >
1260
+ -
1261
+ </Button>
1262
+ <Typography data-testid="portal-count" sx={{ minWidth: 20, textAlign: 'center' }}>
1263
+ {portalCount}
1264
+ </Typography>
1265
+ <Button
1266
+ data-testid="increase-count"
1267
+ size="small"
1268
+ onClick={() => setPortalCount(Math.min(50, portalCount + 1))}
1269
+ disabled={portalCount >= 50}
1270
+ >
1271
+ +
1272
+ </Button>
1273
+ </Box>
1274
+
1275
+ <Button
1276
+ data-testid="toggle-performance-portals"
1277
+ variant="contained"
1278
+ onClick={handleTogglePortals}
1279
+ >
1280
+ {showPortals ? 'Hide' : 'Show'} {portalCount} Portal{portalCount > 1 ? 's' : ''}
1281
+ </Button>
1282
+
1283
+ {renderTime !== null && (
1284
+ <Typography data-testid="render-time" variant="caption" color="text.secondary">
1285
+ Render time: {renderTime.toFixed(2)}ms
1286
+ </Typography>
1287
+ )}
1288
+ </Box>
1289
+
1290
+ {showPortals &&
1291
+ portals.map((index) => (
1292
+ <Portal key={index}>
1293
+ <Box
1294
+ data-testid={`performance-portal-${index}`}
1295
+ sx={{
1296
+ position: 'fixed',
1297
+ top: 50 + (index % 10) * 60,
1298
+ left: 50 + Math.floor(index / 10) * 250,
1299
+ bgcolor: `hsl(${(index * 360) / portalCount}, 70%, 95%)`,
1300
+ border: `2px solid hsl(${(index * 360) / portalCount}, 70%, 70%)`,
1301
+ p: 1,
1302
+ borderRadius: 1,
1303
+ minWidth: 200,
1304
+ zIndex: 9999 + index,
1305
+ }}
1306
+ >
1307
+ <Typography variant="caption" fontWeight="bold">
1308
+ Portal #{index + 1}
1309
+ </Typography>
1310
+ <Typography variant="body2" sx={{ fontSize: '0.75rem' }}>
1311
+ Performance test portal with unique styling and positioning. Z-index:{' '}
1312
+ {9999 + index}
1313
+ </Typography>
1314
+ </Box>
1315
+ </Portal>
1316
+ ))}
1317
+ </Box>
1318
+ );
1319
+ };
1320
+
1321
+ return <PerformanceComponent />;
1322
+ },
1323
+ play: async ({ canvasElement }) => {
1324
+ const canvas = within(canvasElement);
1325
+
1326
+ // Test single portal performance
1327
+ const toggleButton = canvas.getByTestId('toggle-performance-portals');
1328
+ await userEvent.click(toggleButton);
1329
+
1330
+ await waitFor(() => {
1331
+ expect(document.querySelector('[data-testid="performance-portal-0"]')).toBeInTheDocument();
1332
+ });
1333
+
1334
+ // Verify render time is recorded
1335
+ await waitFor(() => {
1336
+ const renderTimeEl = canvas.queryByTestId('render-time');
1337
+ if (renderTimeEl) {
1338
+ expect(renderTimeEl).toHaveTextContent(/Render time: \d+\.\d+ms/);
1339
+ }
1340
+ });
1341
+
1342
+ // Test multiple portals
1343
+ const increaseButton = canvas.getByTestId('increase-count');
1344
+
1345
+ // Increase to 5 portals
1346
+ for (let i = 0; i < 4; i++) {
1347
+ await userEvent.click(increaseButton);
1348
+ }
1349
+
1350
+ // Verify count updated
1351
+ expect(canvas.getByTestId('portal-count')).toHaveTextContent('5');
1352
+
1353
+ // Toggle portals with increased count
1354
+ await userEvent.click(toggleButton);
1355
+
1356
+ await waitFor(() => {
1357
+ expect(
1358
+ document.querySelector('[data-testid="performance-portal-0"]'),
1359
+ ).not.toBeInTheDocument();
1360
+ });
1361
+
1362
+ await userEvent.click(toggleButton);
1363
+
1364
+ await waitFor(() => {
1365
+ for (let i = 0; i < 5; i++) {
1366
+ expect(
1367
+ document.querySelector(`[data-testid="performance-portal-${i}"]`),
1368
+ ).toBeInTheDocument();
1369
+ }
1370
+ });
1371
+
1372
+ // Test performance with many portals
1373
+ for (let i = 0; i < 5; i++) {
1374
+ await userEvent.click(increaseButton);
1375
+ }
1376
+
1377
+ expect(canvas.getByTestId('portal-count')).toHaveTextContent('10');
1378
+
1379
+ // Toggle with 10 portals and verify performance
1380
+ await userEvent.click(toggleButton);
1381
+ await userEvent.click(toggleButton);
1382
+
1383
+ await waitFor(() => {
1384
+ for (let i = 0; i < 10; i++) {
1385
+ expect(
1386
+ document.querySelector(`[data-testid="performance-portal-${i}"]`),
1387
+ ).toBeInTheDocument();
1388
+ }
1389
+ });
1390
+
1391
+ // Verify all portals have unique positioning and styling
1392
+ const portal0 = document.querySelector('[data-testid="performance-portal-0"]');
1393
+ const portal5 = document.querySelector('[data-testid="performance-portal-5"]');
1394
+
1395
+ expect(portal0).toHaveStyle('top: 50px');
1396
+ expect(portal5).toHaveStyle('top: 350px'); // 50 + (5 % 10) * 60
1397
+ },
1398
+ };
1399
+
1400
+ // ================================
1401
+ // EDGE CASES TESTS
1402
+ // ================================
1403
+
1404
+ export const EdgeCases: Story = {
1405
+ render: () => {
1406
+ const EdgeCasesComponent = () => {
1407
+ const [testCase, setTestCase] = useState<string>('null-container');
1408
+ const [customContainer, setCustomContainer] = useState<HTMLElement | null>(null);
1409
+ const [showPortal, setShowPortal] = useState(false);
1410
+ const [showContainer, setShowContainer] = useState(true);
1411
+ const [,] = useState<React.ReactNode>('Default content');
1412
+
1413
+ const testCases = {
1414
+ 'null-container': {
1415
+ title: 'Null Container',
1416
+ description: 'Portal with explicitly null container should fall back to document.body',
1417
+ },
1418
+ 'undefined-container': {
1419
+ title: 'Undefined Container',
1420
+ description: 'Portal with undefined container should create default container',
1421
+ },
1422
+ 'invalid-container': {
1423
+ title: 'Invalid Container',
1424
+ description: 'Portal with container that gets removed from DOM',
1425
+ },
1426
+ 'empty-children': {
1427
+ title: 'Empty Children',
1428
+ description: 'Portal with no children or null children',
1429
+ },
1430
+ 'complex-children': {
1431
+ title: 'Complex Children',
1432
+ description: 'Portal with complex nested React elements',
1433
+ },
1434
+ 'rapid-toggle': {
1435
+ title: 'Rapid Toggle',
1436
+ description: 'Rapidly showing and hiding portal to test cleanup',
1437
+ },
1438
+ };
1439
+
1440
+ const renderPortalForTestCase = () => {
1441
+ switch (testCase) {
1442
+ case 'null-container':
1443
+ return (
1444
+ <Portal container={null}>
1445
+ <Box
1446
+ data-testid="edge-portal-content"
1447
+ sx={{
1448
+ position: 'fixed',
1449
+ top: 20,
1450
+ right: 20,
1451
+ p: 2,
1452
+ bgcolor: 'primary.main',
1453
+ color: 'white',
1454
+ borderRadius: 1,
1455
+ }}
1456
+ >
1457
+ Null container test
1458
+ </Box>
1459
+ </Portal>
1460
+ );
1461
+
1462
+ case 'undefined-container':
1463
+ return (
1464
+ <Portal container={undefined}>
1465
+ <Box
1466
+ data-testid="edge-portal-content"
1467
+ sx={{
1468
+ position: 'fixed',
1469
+ top: 20,
1470
+ right: 20,
1471
+ p: 2,
1472
+ bgcolor: 'secondary.main',
1473
+ color: 'white',
1474
+ borderRadius: 1,
1475
+ }}
1476
+ >
1477
+ Undefined container test
1478
+ </Box>
1479
+ </Portal>
1480
+ );
1481
+
1482
+ case 'invalid-container':
1483
+ return (
1484
+ <Portal container={customContainer}>
1485
+ <Box
1486
+ data-testid="edge-portal-content"
1487
+ sx={{
1488
+ position: 'absolute',
1489
+ top: 10,
1490
+ right: 10,
1491
+ p: 2,
1492
+ bgcolor: 'warning.main',
1493
+ color: 'white',
1494
+ borderRadius: 1,
1495
+ }}
1496
+ >
1497
+ Invalid container test
1498
+ </Box>
1499
+ </Portal>
1500
+ );
1501
+
1502
+ case 'empty-children':
1503
+ return <Portal>{null}</Portal>;
1504
+
1505
+ case 'complex-children':
1506
+ return (
1507
+ <Portal>
1508
+ <Box
1509
+ data-testid="edge-portal-content"
1510
+ sx={{
1511
+ position: 'fixed',
1512
+ top: '50%',
1513
+ left: '50%',
1514
+ transform: 'translate(-50%, -50%)',
1515
+ p: 2,
1516
+ bgcolor: 'background.paper',
1517
+ boxShadow: 24,
1518
+ borderRadius: 2,
1519
+ }}
1520
+ >
1521
+ <Typography variant="h6">Complex Portal Content</Typography>
1522
+ <Box sx={{ display: 'flex', gap: 1, mt: 1 }}>
1523
+ <Button size="small" variant="outlined">
1524
+ Button 1
1525
+ </Button>
1526
+ <Button size="small" variant="contained">
1527
+ Button 2
1528
+ </Button>
1529
+ </Box>
1530
+ <TextField label="Input Field" size="small" fullWidth sx={{ mt: 1 }} />
1531
+ <Alert severity="info" sx={{ mt: 1 }}>
1532
+ This is a complex nested structure
1533
+ </Alert>
1534
+ </Box>
1535
+ </Portal>
1536
+ );
1537
+
1538
+ case 'rapid-toggle':
1539
+ return (
1540
+ <Portal>
1541
+ <Box
1542
+ data-testid="edge-portal-content"
1543
+ sx={{
1544
+ position: 'fixed',
1545
+ top: 20,
1546
+ right: 20,
1547
+ p: 2,
1548
+ bgcolor: 'success.main',
1549
+ color: 'white',
1550
+ borderRadius: 1,
1551
+ }}
1552
+ >
1553
+ Rapid toggle test - {Date.now()}
1554
+ </Box>
1555
+ </Portal>
1556
+ );
1557
+
1558
+ default:
1559
+ return null;
1560
+ }
1561
+ };
1562
+
1563
+ return (
1564
+ <Box>
1565
+ <Box sx={{ mb: 2 }}>
1566
+ <Typography variant="h6" gutterBottom>
1567
+ Edge Cases Test
1568
+ </Typography>
1569
+ <Typography variant="body2" color="text.secondary">
1570
+ {testCases[testCase as keyof typeof testCases]?.description}
1571
+ </Typography>
1572
+ </Box>
1573
+
1574
+ <Box sx={{ mb: 2, display: 'flex', gap: 1, flexWrap: 'wrap' }}>
1575
+ {Object.entries(testCases).map(([key, config]) => (
1576
+ <Button
1577
+ key={key}
1578
+ data-testid={`test-case-${key}`}
1579
+ variant={testCase === key ? 'contained' : 'outlined'}
1580
+ onClick={() => {
1581
+ setTestCase(key);
1582
+ setShowPortal(false); // Reset portal state when changing test cases
1583
+ setShowContainer(true); // Reset container state when changing test cases
1584
+ }}
1585
+ size="small"
1586
+ >
1587
+ {config.title}
1588
+ </Button>
1589
+ ))}
1590
+ </Box>
1591
+
1592
+ <Box sx={{ mb: 2, display: 'flex', gap: 1 }}>
1593
+ <Button
1594
+ data-testid="toggle-edge-portal"
1595
+ variant="contained"
1596
+ onClick={() => setShowPortal(!showPortal)}
1597
+ >
1598
+ {showPortal ? 'Hide' : 'Show'} Portal
1599
+ </Button>
1600
+
1601
+ {testCase === 'invalid-container' && (
1602
+ <Button
1603
+ data-testid="remove-container"
1604
+ color="warning"
1605
+ onClick={() => {
1606
+ setShowContainer(false);
1607
+ setCustomContainer(null);
1608
+ }}
1609
+ >
1610
+ Remove Container
1611
+ </Button>
1612
+ )}
1613
+
1614
+ {testCase === 'rapid-toggle' && (
1615
+ <Button
1616
+ data-testid="rapid-toggle"
1617
+ color="secondary"
1618
+ onClick={async () => {
1619
+ // Rapidly toggle portal 10 times
1620
+ for (let i = 0; i < 10; i++) {
1621
+ setShowPortal((prev) => !prev);
1622
+ await new Promise((resolve) => window.setTimeout(resolve, 50));
1623
+ }
1624
+ }}
1625
+ >
1626
+ Rapid Toggle (10x)
1627
+ </Button>
1628
+ )}
1629
+ </Box>
1630
+
1631
+ {/* Container for invalid-container test */}
1632
+ {testCase === 'invalid-container' && showContainer && (
1633
+ <Paper
1634
+ ref={(el) => setCustomContainer(el)}
1635
+ data-testid="removable-container"
1636
+ sx={{ p: 2, mb: 2, bgcolor: 'grey.100', position: 'relative' }}
1637
+ >
1638
+ <Typography variant="subtitle2">Removable Container</Typography>
1639
+ <Typography variant="body2" color="text.secondary">
1640
+ This container can be removed to test invalid container handling
1641
+ </Typography>
1642
+ </Paper>
1643
+ )}
1644
+
1645
+ {showPortal && renderPortalForTestCase()}
1646
+ </Box>
1647
+ );
1648
+ };
1649
+
1650
+ return <EdgeCasesComponent />;
1651
+ },
1652
+ play: async ({ canvasElement }) => {
1653
+ const canvas = within(canvasElement);
1654
+
1655
+ // Test null container case
1656
+ await userEvent.click(canvas.getByTestId('test-case-null-container'));
1657
+ await userEvent.click(canvas.getByTestId('toggle-edge-portal'));
1658
+
1659
+ await waitFor(() => {
1660
+ const portalContent = document.querySelector('[data-testid="edge-portal-content"]');
1661
+ expect(portalContent).toBeInTheDocument();
1662
+ // Should be appended to document.body since container is null
1663
+ expect(portalContent?.parentElement).toBe(document.body);
1664
+ });
1665
+
1666
+ await userEvent.click(canvas.getByTestId('toggle-edge-portal')); // Hide
1667
+
1668
+ // Test undefined container case
1669
+ await userEvent.click(canvas.getByTestId('test-case-undefined-container'));
1670
+ await userEvent.click(canvas.getByTestId('toggle-edge-portal'));
1671
+
1672
+ await waitFor(() => {
1673
+ const portalContent = document.querySelector('[data-testid="edge-portal-content"]');
1674
+ expect(portalContent).toBeInTheDocument();
1675
+ });
1676
+
1677
+ await userEvent.click(canvas.getByTestId('toggle-edge-portal')); // Hide
1678
+
1679
+ // Test invalid container case
1680
+ await userEvent.click(canvas.getByTestId('test-case-invalid-container'));
1681
+ await userEvent.click(canvas.getByTestId('toggle-edge-portal'));
1682
+
1683
+ await waitFor(() => {
1684
+ const portalContent = document.querySelector('[data-testid="edge-portal-content"]');
1685
+ expect(portalContent).toBeInTheDocument();
1686
+
1687
+ // Should be in the custom container initially
1688
+ const customContainer = canvas.getByTestId('removable-container');
1689
+ expect(customContainer).toContainElement(portalContent);
1690
+ });
1691
+
1692
+ // Remove the container and verify portal handles it gracefully
1693
+ await userEvent.click(canvas.getByTestId('remove-container'));
1694
+
1695
+ // Portal should still exist and handle the missing container
1696
+ await waitFor(() => {
1697
+ // The portal content might still exist or be moved to body
1698
+ const _portalContent = document.querySelector('[data-testid="edge-portal-content"]');
1699
+ // We don't expect it to crash, content may or may not still be visible
1700
+ void _portalContent; // Acknowledge the unused variable
1701
+ });
1702
+
1703
+ await userEvent.click(canvas.getByTestId('toggle-edge-portal')); // Hide
1704
+
1705
+ // Test empty children case
1706
+ await userEvent.click(canvas.getByTestId('test-case-empty-children'));
1707
+ await userEvent.click(canvas.getByTestId('toggle-edge-portal'));
1708
+
1709
+ // Should not crash with empty children
1710
+ await waitFor(() => {
1711
+ // No portal content should be visible
1712
+ expect(document.querySelector('[data-testid="edge-portal-content"]')).not.toBeInTheDocument();
1713
+ });
1714
+
1715
+ await userEvent.click(canvas.getByTestId('toggle-edge-portal')); // Hide
1716
+
1717
+ // Test complex children case
1718
+ await userEvent.click(canvas.getByTestId('test-case-complex-children'));
1719
+ await userEvent.click(canvas.getByTestId('toggle-edge-portal'));
1720
+
1721
+ await waitFor(() => {
1722
+ const portalContent = document.querySelector('[data-testid="edge-portal-content"]');
1723
+ expect(portalContent).toBeInTheDocument();
1724
+ expect(portalContent).toHaveTextContent('Complex Portal Content');
1725
+
1726
+ // Verify nested elements exist
1727
+ expect(portalContent?.querySelector('button')).toBeInTheDocument();
1728
+ expect(portalContent?.querySelector('input')).toBeInTheDocument();
1729
+ expect(portalContent?.querySelector('[role="alert"]')).toBeInTheDocument();
1730
+ });
1731
+
1732
+ await userEvent.click(canvas.getByTestId('toggle-edge-portal')); // Hide
1733
+
1734
+ // Test rapid toggle case
1735
+ await userEvent.click(canvas.getByTestId('test-case-rapid-toggle'));
1736
+
1737
+ // Test single toggle first
1738
+ await userEvent.click(canvas.getByTestId('toggle-edge-portal'));
1739
+ await waitFor(() => {
1740
+ expect(document.querySelector('[data-testid="edge-portal-content"]')).toBeInTheDocument();
1741
+ });
1742
+
1743
+ // Test rapid toggle functionality
1744
+ await userEvent.click(canvas.getByTestId('rapid-toggle'));
1745
+
1746
+ // Give some time for rapid toggles to complete
1747
+ await new Promise((resolve) => window.setTimeout(resolve, 600));
1748
+
1749
+ // Should not crash and final state should be deterministic
1750
+ // (The exact final state depends on the implementation but shouldn't crash)
1751
+ },
1752
+ };
1753
+
1754
+ // ================================
1755
+ // INTEGRATION TESTS
1756
+ // ================================
1757
+
1758
+ export const Integration: Story = {
1759
+ render: () => {
1760
+ const IntegrationComponent = () => {
1761
+ const [showModal, setShowModal] = useState(false);
1762
+ const [showTooltip, setShowTooltip] = useState(false);
1763
+ const [showDropdown, setShowDropdown] = useState(false);
1764
+ const [notifications, setNotifications] = useState<string[]>([]);
1765
+
1766
+ const addNotification = (message: string) => {
1767
+ setNotifications((prev) => [...prev, message]);
1768
+ window.setTimeout(() => {
1769
+ setNotifications((prev) => prev.slice(1));
1770
+ }, 3000);
1771
+ };
1772
+
1773
+ return (
1774
+ <Box>
1775
+ <Typography variant="h6" gutterBottom>
1776
+ Multiple Portal Integration
1777
+ </Typography>
1778
+ <Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}>
1779
+ Test multiple portals working together with proper z-index stacking and interaction.
1780
+ </Typography>
1781
+
1782
+ <Box sx={{ display: 'flex', gap: 1, mb: 2, flexWrap: 'wrap' }}>
1783
+ <Button
1784
+ data-testid="show-modal"
1785
+ variant="contained"
1786
+ onClick={() => {
1787
+ setShowModal(true);
1788
+ addNotification('Modal opened');
1789
+ }}
1790
+ >
1791
+ Show Modal
1792
+ </Button>
1793
+
1794
+ <Button
1795
+ data-testid="show-tooltip"
1796
+ variant="outlined"
1797
+ onMouseEnter={() => setShowTooltip(true)}
1798
+ onMouseLeave={() => setShowTooltip(false)}
1799
+ >
1800
+ Hover for Tooltip
1801
+ </Button>
1802
+
1803
+ <Button
1804
+ data-testid="show-dropdown"
1805
+ variant="outlined"
1806
+ onClick={() => setShowDropdown(!showDropdown)}
1807
+ >
1808
+ Toggle Dropdown
1809
+ </Button>
1810
+
1811
+ <Button
1812
+ data-testid="add-notification"
1813
+ variant="text"
1814
+ onClick={() => addNotification(`Notification ${Date.now()}`)}
1815
+ >
1816
+ Add Notification
1817
+ </Button>
1818
+ </Box>
1819
+
1820
+ {/* Modal Portal */}
1821
+ {showModal && (
1822
+ <Portal>
1823
+ {/* Backdrop */}
1824
+ <Box
1825
+ data-testid="modal-backdrop"
1826
+ onClick={() => setShowModal(false)}
1827
+ sx={{
1828
+ position: 'fixed',
1829
+ top: 0,
1830
+ left: 0,
1831
+ right: 0,
1832
+ bottom: 0,
1833
+ bgcolor: 'rgba(0, 0, 0, 0.5)',
1834
+ zIndex: 1000,
1835
+ }}
1836
+ />
1837
+
1838
+ {/* Modal Content */}
1839
+ <Box
1840
+ data-testid="modal-content"
1841
+ sx={{
1842
+ position: 'fixed',
1843
+ top: '50%',
1844
+ left: '50%',
1845
+ transform: 'translate(-50%, -50%)',
1846
+ bgcolor: 'background.paper',
1847
+ p: 3,
1848
+ borderRadius: 2,
1849
+ boxShadow: 24,
1850
+ minWidth: 400,
1851
+ zIndex: 1001,
1852
+ }}
1853
+ >
1854
+ <Typography variant="h6" gutterBottom>
1855
+ Modal Dialog
1856
+ </Typography>
1857
+ <Typography variant="body2" sx={{ mb: 2 }}>
1858
+ This modal is rendered in a portal with high z-index to appear above other
1859
+ content.
1860
+ </Typography>
1861
+
1862
+ <Box sx={{ mb: 2 }}>
1863
+ <TextField label="Input in Modal" fullWidth size="small" />
1864
+ </Box>
1865
+
1866
+ <Box sx={{ display: 'flex', gap: 1, justifyContent: 'flex-end' }}>
1867
+ <Button data-testid="modal-cancel" onClick={() => setShowModal(false)}>
1868
+ Cancel
1869
+ </Button>
1870
+ <Button
1871
+ data-testid="modal-confirm"
1872
+ variant="contained"
1873
+ onClick={() => {
1874
+ setShowModal(false);
1875
+ addNotification('Modal confirmed');
1876
+ }}
1877
+ >
1878
+ Confirm
1879
+ </Button>
1880
+ </Box>
1881
+ </Box>
1882
+ </Portal>
1883
+ )}
1884
+
1885
+ {/* Tooltip Portal */}
1886
+ {showTooltip && (
1887
+ <Portal>
1888
+ <Box
1889
+ data-testid="tooltip-content"
1890
+ sx={{
1891
+ position: 'absolute',
1892
+ top: '100%',
1893
+ left: '50%',
1894
+ transform: 'translateX(-50%)',
1895
+ bgcolor: 'grey.800',
1896
+ color: 'white',
1897
+ p: 1,
1898
+ borderRadius: 1,
1899
+ fontSize: '0.75rem',
1900
+ zIndex: 1100,
1901
+ mt: 0.5,
1902
+ '&::before': {
1903
+ content: '""',
1904
+ position: 'absolute',
1905
+ top: -4,
1906
+ left: '50%',
1907
+ transform: 'translateX(-50%)',
1908
+ width: 0,
1909
+ height: 0,
1910
+ borderLeft: '4px solid transparent',
1911
+ borderRight: '4px solid transparent',
1912
+ borderBottom: '4px solid',
1913
+ borderBottomColor: 'grey.800',
1914
+ },
1915
+ }}
1916
+ >
1917
+ This is a tooltip rendered in a portal
1918
+ </Box>
1919
+ </Portal>
1920
+ )}
1921
+
1922
+ {/* Dropdown Portal */}
1923
+ {showDropdown && (
1924
+ <Portal>
1925
+ <Box
1926
+ data-testid="dropdown-content"
1927
+ sx={{
1928
+ position: 'fixed',
1929
+ top: 200,
1930
+ left: 300,
1931
+ bgcolor: 'background.paper',
1932
+ border: 1,
1933
+ borderColor: 'divider',
1934
+ borderRadius: 1,
1935
+ boxShadow: 3,
1936
+ py: 1,
1937
+ minWidth: 150,
1938
+ zIndex: 1050,
1939
+ }}
1940
+ >
1941
+ {['Option 1', 'Option 2', 'Option 3'].map((option) => (
1942
+ <Box
1943
+ key={option}
1944
+ data-testid={`dropdown-option-${option.toLowerCase().replace(' ', '-')}`}
1945
+ sx={{
1946
+ px: 2,
1947
+ py: 1,
1948
+ cursor: 'pointer',
1949
+ '&:hover': {
1950
+ bgcolor: 'action.hover',
1951
+ },
1952
+ }}
1953
+ onClick={() => {
1954
+ setShowDropdown(false);
1955
+ addNotification(`Selected: ${option}`);
1956
+ }}
1957
+ >
1958
+ <Typography variant="body2">{option}</Typography>
1959
+ </Box>
1960
+ ))}
1961
+ </Box>
1962
+ </Portal>
1963
+ )}
1964
+
1965
+ {/* Notifications Portal */}
1966
+ {notifications.map((notification, index) => (
1967
+ <Portal key={`${notification}-${index}`}>
1968
+ <Box
1969
+ data-testid={`notification-${index}`}
1970
+ sx={{
1971
+ position: 'fixed',
1972
+ top: 20 + index * 60,
1973
+ right: 20,
1974
+ bgcolor: 'success.main',
1975
+ color: 'white',
1976
+ p: 2,
1977
+ borderRadius: 1,
1978
+ boxShadow: 3,
1979
+ maxWidth: 300,
1980
+ zIndex: 1200,
1981
+ animation: 'slideIn 0.3s ease-out',
1982
+ '@keyframes slideIn': {
1983
+ from: {
1984
+ transform: 'translateX(100%)',
1985
+ opacity: 0,
1986
+ },
1987
+ to: {
1988
+ transform: 'translateX(0)',
1989
+ opacity: 1,
1990
+ },
1991
+ },
1992
+ }}
1993
+ >
1994
+ <Typography variant="body2">{notification}</Typography>
1995
+ </Box>
1996
+ </Portal>
1997
+ ))}
1998
+ </Box>
1999
+ );
2000
+ };
2001
+
2002
+ return <IntegrationComponent />;
2003
+ },
2004
+ play: async ({ canvasElement }) => {
2005
+ const canvas = within(canvasElement);
2006
+
2007
+ // Test notification system
2008
+ const addNotificationBtn = canvas.getByTestId('add-notification');
2009
+ await userEvent.click(addNotificationBtn);
2010
+
2011
+ await waitFor(() => {
2012
+ expect(document.querySelector('[data-testid="notification-0"]')).toBeInTheDocument();
2013
+ });
2014
+
2015
+ // Test dropdown
2016
+ const dropdownBtn = canvas.getByTestId('show-dropdown');
2017
+ await userEvent.click(dropdownBtn);
2018
+
2019
+ await waitFor(() => {
2020
+ expect(document.querySelector('[data-testid="dropdown-content"]')).toBeInTheDocument();
2021
+ });
2022
+
2023
+ // Select an option from dropdown
2024
+ const option1 = document.querySelector(
2025
+ '[data-testid="dropdown-option-option-1"]',
2026
+ ) as HTMLElement;
2027
+ await userEvent.click(option1);
2028
+
2029
+ await waitFor(() => {
2030
+ expect(document.querySelector('[data-testid="dropdown-content"]')).not.toBeInTheDocument();
2031
+ // Should trigger a new notification
2032
+ expect(document.querySelector('[data-testid="notification-1"]')).toBeInTheDocument();
2033
+ });
2034
+
2035
+ // Test modal (highest z-index)
2036
+ const modalBtn = canvas.getByTestId('show-modal');
2037
+ await userEvent.click(modalBtn);
2038
+
2039
+ await waitFor(() => {
2040
+ expect(document.querySelector('[data-testid="modal-backdrop"]')).toBeInTheDocument();
2041
+ expect(document.querySelector('[data-testid="modal-content"]')).toBeInTheDocument();
2042
+ });
2043
+
2044
+ // Test modal interaction
2045
+ const modalInput = document.querySelector(
2046
+ '[data-testid="modal-content"] input',
2047
+ ) as HTMLInputElement;
2048
+ await userEvent.type(modalInput, 'Test input in modal');
2049
+
2050
+ expect(modalInput.value).toBe('Test input in modal');
2051
+
2052
+ // Confirm modal
2053
+ const confirmBtn = document.querySelector('[data-testid="modal-confirm"]') as HTMLButtonElement;
2054
+ await userEvent.click(confirmBtn);
2055
+
2056
+ await waitFor(() => {
2057
+ expect(document.querySelector('[data-testid="modal-content"]')).not.toBeInTheDocument();
2058
+ // Should trigger another notification
2059
+ expect(document.querySelector('[data-testid="notification-2"]')).toBeInTheDocument();
2060
+ });
2061
+
2062
+ // Test tooltip (requires hover simulation)
2063
+ const tooltipBtn = canvas.getByTestId('show-tooltip');
2064
+ await userEvent.hover(tooltipBtn);
2065
+
2066
+ await waitFor(() => {
2067
+ expect(document.querySelector('[data-testid="tooltip-content"]')).toBeInTheDocument();
2068
+ });
2069
+
2070
+ await userEvent.unhover(tooltipBtn);
2071
+
2072
+ await waitFor(() => {
2073
+ expect(document.querySelector('[data-testid="tooltip-content"]')).not.toBeInTheDocument();
2074
+ });
2075
+
2076
+ // Verify multiple notifications stack properly
2077
+ await userEvent.click(addNotificationBtn);
2078
+ await userEvent.click(addNotificationBtn);
2079
+
2080
+ await waitFor(() => {
2081
+ // Should have multiple notifications stacked
2082
+ const notifications = document.querySelectorAll('[data-testid^="notification-"]');
2083
+ expect(notifications.length).toBeGreaterThan(3);
2084
+ });
2085
+ },
2086
+ };