@12-apps/ui 1.5.0 → 1.5.2

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 (229) hide show
  1. package/package.json +17 -9
  2. package/src/button/index.ts +1 -0
  3. package/src/social-login-button/index.ts +1 -0
  4. package/src/{social-login-button.tsx → social-login-button/social-login-button.tsx} +5 -5
  5. package/src/user-avatar/index.ts +1 -0
  6. package/src/{user-avatar.tsx → user-avatar/user-avatar.tsx} +4 -4
  7. package/eslint.config.js +0 -89
  8. package/src/__tests__/data-grid.test.tsx +0 -125
  9. package/src/__tests__/package-wiring.test.ts +0 -41
  10. package/src/__tests__/setup.ts +0 -1
  11. package/src/__tests__/social-login-button.test.tsx +0 -186
  12. package/src/__tests__/user-avatar.test.tsx +0 -195
  13. package/src/__tests__/viewport.test.ts +0 -50
  14. package/src/components/command/__tests__/command.test.tsx +0 -112
  15. package/src/components/data-display/Alert/Alert.stories.tsx +0 -559
  16. package/src/components/data-display/Alert/Alert.test.stories.tsx +0 -707
  17. package/src/components/data-display/AlertDialog/AlertDialog.stories.tsx +0 -362
  18. package/src/components/data-display/AlertDialog/AlertDialog.test.stories.tsx +0 -763
  19. package/src/components/data-display/AsyncStateContainer/AsyncStateContainer.stories.tsx +0 -230
  20. package/src/components/data-display/AsyncStateContainer/AsyncStateContainer.test.stories.tsx +0 -270
  21. package/src/components/data-display/Avatar/Avatar.stories.tsx +0 -721
  22. package/src/components/data-display/Avatar/Avatar.test.stories.tsx +0 -822
  23. package/src/components/data-display/Badge/Badge.stories.tsx +0 -1134
  24. package/src/components/data-display/Badge/Badge.test.stories.tsx +0 -1194
  25. package/src/components/data-display/Banner/Banner.stories.tsx +0 -528
  26. package/src/components/data-display/Banner/Banner.test.stories.tsx +0 -511
  27. package/src/components/data-display/Carousel/Carousel.stories.tsx +0 -607
  28. package/src/components/data-display/Carousel/Carousel.test.stories.tsx +0 -701
  29. package/src/components/data-display/Chart/Chart.stories.tsx +0 -568
  30. package/src/components/data-display/Chart/Chart.test.stories.tsx +0 -174
  31. package/src/components/data-display/Chart/SpecChart.stories.tsx +0 -150
  32. package/src/components/data-display/Chart/SpecChart.test.stories.tsx +0 -56
  33. package/src/components/data-display/Chart/__tests__/SpecChart.test.tsx +0 -60
  34. package/src/components/data-display/Chart/__tests__/chart-format.test.ts +0 -43
  35. package/src/components/data-display/Chart/__tests__/chart-spec.test.ts +0 -119
  36. package/src/components/data-display/Chip/Chip.stories.tsx +0 -252
  37. package/src/components/data-display/Chip/Chip.test.stories.tsx +0 -266
  38. package/src/components/data-display/DataGrid/DataGrid.stories.tsx +0 -515
  39. package/src/components/data-display/DataGrid/DataGrid.test.stories.tsx +0 -653
  40. package/src/components/data-display/DataViews/__tests__/DataViewsGrid.test.tsx +0 -271
  41. package/src/components/data-display/DataViews/__tests__/DataViewsTableBase.test.tsx +0 -134
  42. package/src/components/data-display/DataViews/__tests__/SaveViewModal.test.tsx +0 -83
  43. package/src/components/data-display/DataViews/__tests__/data-views-filter.test.ts +0 -115
  44. package/src/components/data-display/DataViews/__tests__/data-views-preview.test.ts +0 -53
  45. package/src/components/data-display/DataViews/__tests__/range-pill.test.tsx +0 -114
  46. package/src/components/data-display/DataViews/base-card.stories.tsx +0 -136
  47. package/src/components/data-display/DescriptionItem/DescriptionItem.stories.tsx +0 -254
  48. package/src/components/data-display/EmptyState/EmptyState.stories.tsx +0 -282
  49. package/src/components/data-display/EmptyState/EmptyState.test.stories.tsx +0 -441
  50. package/src/components/data-display/ErrorState/ErrorState.stories.tsx +0 -110
  51. package/src/components/data-display/ErrorState/ErrorState.test.stories.tsx +0 -133
  52. package/src/components/data-display/HoverCard/HoverCard.stories.tsx +0 -650
  53. package/src/components/data-display/HoverCard/HoverCard.test.stories.tsx +0 -929
  54. package/src/components/data-display/InteractiveTooltip/InteractiveTooltip.stories.tsx +0 -411
  55. package/src/components/data-display/LazyImage/LazyImage.stories.tsx +0 -387
  56. package/src/components/data-display/Lightbox/Lightbox.stories.tsx +0 -315
  57. package/src/components/data-display/Lightbox/Lightbox.test.stories.tsx +0 -598
  58. package/src/components/data-display/LoadingState/LoadingState.stories.tsx +0 -124
  59. package/src/components/data-display/LoadingState/LoadingState.test.stories.tsx +0 -99
  60. package/src/components/data-display/MapPreview/MapPreview.stories.tsx +0 -458
  61. package/src/components/data-display/MapPreview/MapPreview.test.stories.tsx +0 -394
  62. package/src/components/data-display/Popover/Popover.stories.tsx +0 -295
  63. package/src/components/data-display/Popover/Popover.test.stories.tsx +0 -1162
  64. package/src/components/data-display/Progress/Progress.stories.tsx +0 -481
  65. package/src/components/data-display/Progress/Progress.test.stories.tsx +0 -2070
  66. package/src/components/data-display/SectionOnboarding/SectionOnboarding.stories.tsx +0 -160
  67. package/src/components/data-display/SectionOnboarding/SectionOnboarding.test.stories.tsx +0 -190
  68. package/src/components/data-display/Sheet/Sheet.stories.tsx +0 -883
  69. package/src/components/data-display/Sheet/Sheet.test.stories.tsx +0 -1105
  70. package/src/components/data-display/StatCard/StatCard.stories.tsx +0 -167
  71. package/src/components/data-display/StatCard/StatCard.test.stories.tsx +0 -240
  72. package/src/components/data-display/Stepper/Stepper.stories.tsx +0 -540
  73. package/src/components/data-display/Stepper/Stepper.test.stories.tsx +0 -600
  74. package/src/components/data-display/Table/Table.stories.tsx +0 -803
  75. package/src/components/data-display/Table/Table.test.stories.tsx +0 -369
  76. package/src/components/data-display/Timeline/Timeline.stories.tsx +0 -442
  77. package/src/components/data-display/Timeline/Timeline.test.stories.tsx +0 -930
  78. package/src/components/data-display/TimingDiagram/TimingDiagram.stories.tsx +0 -475
  79. package/src/components/data-display/TimingDiagram/TimingDiagram.test.stories.tsx +0 -734
  80. package/src/components/data-display/Tooltip/Tooltip.stories.tsx +0 -468
  81. package/src/components/data-display/Tooltip/Tooltip.test.stories.tsx +0 -1154
  82. package/src/components/feedback/ConfirmAction/ConfirmAction.stories.tsx +0 -146
  83. package/src/components/feedback/ConfirmAction/ConfirmAction.test.stories.tsx +0 -114
  84. package/src/components/feedback/ConfirmAction/__tests__/confirm-action.test.tsx +0 -288
  85. package/src/components/feedback/Dialog/Dialog.stories.tsx +0 -833
  86. package/src/components/feedback/Dialog/Dialog.test.stories.tsx +0 -964
  87. package/src/components/feedback/Dialog/__tests__/dialog-content-spacing.test.tsx +0 -66
  88. package/src/components/feedback/Modal/Modal.stories.tsx +0 -765
  89. package/src/components/feedback/Modal/Modal.test.stories.tsx +0 -251
  90. package/src/components/feedback/Sonner/Sonner.stories.tsx +0 -698
  91. package/src/components/feedback/Sonner/Sonner.test.stories.tsx +0 -896
  92. package/src/components/feedback/StackedModal/StackedModal.stories.tsx +0 -1783
  93. package/src/components/feedback/StackedModal/StackedModal.test.stories.tsx +0 -973
  94. package/src/components/feedback/StackedModal/__tests__/stacked-modal-viewport.test.tsx +0 -104
  95. package/src/components/feedback/Toast/Toast.stories.tsx +0 -548
  96. package/src/components/feedback/Toast/Toast.test.stories.tsx +0 -834
  97. package/src/components/feedback/TutorialOverlay/TutorialOverlay.stories.tsx +0 -691
  98. package/src/components/feedback/TutorialOverlay/TutorialOverlay.test.stories.tsx +0 -634
  99. package/src/components/form/AddressAutocomplete/AddressAutocomplete.stories.tsx +0 -665
  100. package/src/components/form/AddressAutocomplete/AddressAutocomplete.test.stories.tsx +0 -727
  101. package/src/components/form/Autocomplete/Autocomplete.stories.tsx +0 -715
  102. package/src/components/form/Autocomplete/Autocomplete.test.stories.tsx +0 -825
  103. package/src/components/form/Button/Button.stories.tsx +0 -365
  104. package/src/components/form/Button/Button.test.stories.tsx +0 -607
  105. package/src/components/form/Calendar/Calendar.stories.tsx +0 -349
  106. package/src/components/form/Calendar/Calendar.test.stories.tsx +0 -464
  107. package/src/components/form/CepField/CepField.stories.tsx +0 -138
  108. package/src/components/form/CepField/CepField.test.stories.tsx +0 -281
  109. package/src/components/form/Checkbox/Checkbox.stories.tsx +0 -265
  110. package/src/components/form/Checkbox/Checkbox.test.stories.tsx +0 -181
  111. package/src/components/form/CodeEditor/CodeEditor.stories.tsx +0 -871
  112. package/src/components/form/CodeEditor/CodeEditor.test.stories.tsx +0 -574
  113. package/src/components/form/Command/Command.stories.tsx +0 -580
  114. package/src/components/form/Command/Command.test.stories.tsx +0 -215
  115. package/src/components/form/CreatableSelect/CreatableSelect.stories.tsx +0 -112
  116. package/src/components/form/CreatableSelect/CreatableSelect.test.stories.tsx +0 -134
  117. package/src/components/form/Form/Form.stories.tsx +0 -687
  118. package/src/components/form/Form/Form.test.stories.tsx +0 -601
  119. package/src/components/form/Input/Input.stories.tsx +0 -260
  120. package/src/components/form/Input/Input.test.stories.tsx +0 -1191
  121. package/src/components/form/InputOTP/InputOTP.stories.tsx +0 -774
  122. package/src/components/form/InputOTP/InputOTP.test.stories.tsx +0 -588
  123. package/src/components/form/Label/Label.stories.tsx +0 -473
  124. package/src/components/form/Label/Label.test.stories.tsx +0 -915
  125. package/src/components/form/Menubar/Menubar.stories.tsx +0 -472
  126. package/src/components/form/Menubar/Menubar.test.stories.tsx +0 -584
  127. package/src/components/form/PasswordStrength/PasswordStrength.stories.tsx +0 -562
  128. package/src/components/form/PasswordStrength/PasswordStrength.test.stories.tsx +0 -327
  129. package/src/components/form/PhoneInput/PhoneInput.stories.tsx +0 -495
  130. package/src/components/form/PhoneInput/PhoneInput.test.stories.tsx +0 -666
  131. package/src/components/form/RadioGroup/RadioGroup.stories.tsx +0 -518
  132. package/src/components/form/RadioGroup/RadioGroup.test.stories.tsx +0 -685
  133. package/src/components/form/RichTextEditor/RichTextEditor.stories.tsx +0 -261
  134. package/src/components/form/RichTextEditor/RichTextEditor.test.stories.tsx +0 -491
  135. package/src/components/form/SearchPalette/SearchPalette.stories.tsx +0 -135
  136. package/src/components/form/Select/Select.stories.tsx +0 -364
  137. package/src/components/form/Select/Select.test.stories.tsx +0 -985
  138. package/src/components/form/Slider/Slider.stories.tsx +0 -418
  139. package/src/components/form/Slider/Slider.test.stories.tsx +0 -1042
  140. package/src/components/form/Switch/Switch.stories.tsx +0 -697
  141. package/src/components/form/Switch/Switch.test.stories.tsx +0 -597
  142. package/src/components/form/Textarea/Textarea.stories.tsx +0 -473
  143. package/src/components/form/Textarea/Textarea.test.stories.tsx +0 -702
  144. package/src/components/form/Toggle/Toggle.stories.tsx +0 -638
  145. package/src/components/form/Toggle/Toggle.test.stories.tsx +0 -1159
  146. package/src/components/form/ToggleGroup/ToggleGroup.stories.tsx +0 -617
  147. package/src/components/form/ToggleGroup/ToggleGroup.test.stories.tsx +0 -915
  148. package/src/components/form/UploadButton/UploadButton.stories.tsx +0 -367
  149. package/src/components/form/UploadButton/UploadButton.test.stories.tsx +0 -573
  150. package/src/components/form/total-form/total-form.test.tsx +0 -214
  151. package/src/components/layout/Accordion/Accordion.stories.tsx +0 -694
  152. package/src/components/layout/Accordion/Accordion.test.stories.tsx +0 -1344
  153. package/src/components/layout/Card/Card.stories.tsx +0 -591
  154. package/src/components/layout/Card/Card.test.stories.tsx +0 -1134
  155. package/src/components/layout/CardGrid/__tests__/CardGrid.test.tsx +0 -92
  156. package/src/components/layout/Collapsible/Collapsible.stories.tsx +0 -180
  157. package/src/components/layout/Collapsible/Collapsible.test.stories.tsx +0 -308
  158. package/src/components/layout/Container/Container.stories.tsx +0 -557
  159. package/src/components/layout/Container/Container.test.stories.tsx +0 -745
  160. package/src/components/layout/ContentToolbar/ContentToolbar.stories.tsx +0 -129
  161. package/src/components/layout/ContentToolbar/ContentToolbar.test.stories.tsx +0 -251
  162. package/src/components/layout/ContentToolbar/__tests__/ColumnsMenu.test.tsx +0 -39
  163. package/src/components/layout/ContentToolbar/__tests__/MultiSelectDropdown.test.tsx +0 -131
  164. package/src/components/layout/ContentToolbar/__tests__/SavedViewsMenu.test.tsx +0 -99
  165. package/src/components/layout/Dashboard/Dashboard.stories.tsx +0 -104
  166. package/src/components/layout/Dashboard/Dashboard.test.stories.tsx +0 -190
  167. package/src/components/layout/Drawer/Drawer.stories.tsx +0 -658
  168. package/src/components/layout/Drawer/Drawer.test.stories.tsx +0 -1343
  169. package/src/components/layout/Resizable/Resizable.stories.tsx +0 -1039
  170. package/src/components/layout/Resizable/Resizable.test.stories.tsx +0 -754
  171. package/src/components/layout/ScrollArea/ScrollArea.stories.tsx +0 -651
  172. package/src/components/layout/ScrollArea/ScrollArea.test.stories.tsx +0 -619
  173. package/src/components/layout/Separator/Separator.stories.tsx +0 -563
  174. package/src/components/layout/Separator/Separator.test.stories.tsx +0 -518
  175. package/src/components/layout/SettingsLayout/SettingsLayout.stories.tsx +0 -192
  176. package/src/components/layout/SettingsLayout/SettingsLayout.test.stories.tsx +0 -175
  177. package/src/components/layout/Sidebar/Sidebar.stories.tsx +0 -609
  178. package/src/components/layout/Sidebar/Sidebar.test.stories.tsx +0 -1324
  179. package/src/components/layout/Skeleton/Skeleton.stories.tsx +0 -689
  180. package/src/components/layout/Skeleton/Skeleton.test.stories.tsx +0 -521
  181. package/src/components/layout/Spacer/Spacer.stories.tsx +0 -502
  182. package/src/components/layout/Spacer/Spacer.test.stories.tsx +0 -474
  183. package/src/components/layout/TableFilter/TableFilter.stories.tsx +0 -82
  184. package/src/components/layout/TableFilter/TableFilter.test.stories.tsx +0 -87
  185. package/src/components/navigation/Breadcrumbs/Breadcrumbs.stories.tsx +0 -638
  186. package/src/components/navigation/Breadcrumbs/Breadcrumbs.test.stories.tsx +0 -1008
  187. package/src/components/navigation/CommandPalette/CommandPalette.stories.tsx +0 -386
  188. package/src/components/navigation/CommandPalette/CommandPalette.test.stories.tsx +0 -435
  189. package/src/components/navigation/ContextMenu/ContextMenu.stories.tsx +0 -614
  190. package/src/components/navigation/ContextMenu/ContextMenu.test.stories.tsx +0 -845
  191. package/src/components/navigation/DropdownMenu/DropdownMenu.stories.tsx +0 -578
  192. package/src/components/navigation/DropdownMenu/DropdownMenu.test.stories.tsx +0 -660
  193. package/src/components/navigation/NavigationMenu/stories/NavigationMenu.stories.tsx +0 -516
  194. package/src/components/navigation/NavigationMenu/stories/NavigationMenu.test.stories.tsx +0 -935
  195. package/src/components/navigation/NavigationMenu/stories/NavigationMenuHorizontal.stories.tsx +0 -501
  196. package/src/components/navigation/NavigationMenu/stories/NavigationMenuVertical.stories.tsx +0 -453
  197. package/src/components/navigation/Pagination/Pagination.stories.tsx +0 -602
  198. package/src/components/navigation/Pagination/Pagination.test.stories.tsx +0 -520
  199. package/src/components/navigation/Tabs/Tabs.stories.tsx +0 -1004
  200. package/src/components/navigation/Tabs/Tabs.test.stories.tsx +0 -1225
  201. package/src/components/typography/Blockquote/Blockquote.stories.tsx +0 -359
  202. package/src/components/typography/Blockquote/Blockquote.test.stories.tsx +0 -432
  203. package/src/components/typography/Code/Code.stories.tsx +0 -686
  204. package/src/components/typography/Code/Code.test.stories.tsx +0 -703
  205. package/src/components/typography/Heading/Heading.stories.tsx +0 -451
  206. package/src/components/typography/Heading/Heading.test.stories.tsx +0 -830
  207. package/src/components/typography/Paragraph/Paragraph.stories.tsx +0 -280
  208. package/src/components/typography/Paragraph/Paragraph.test.stories.tsx +0 -231
  209. package/src/components/typography/Text/Text.stories.tsx +0 -359
  210. package/src/components/typography/Text/Text.test.stories.tsx +0 -759
  211. package/src/components/utility/AnimatedIcon/AnimatedIcon.stories.tsx +0 -945
  212. package/src/components/utility/AnimatedIcon/AnimatedIcon.test.stories.tsx +0 -603
  213. package/src/components/utility/AspectRatio/AspectRatio.stories.tsx +0 -414
  214. package/src/components/utility/AspectRatio/AspectRatio.test.stories.tsx +0 -859
  215. package/src/components/utility/InfiniteScroll/InfiniteScroll.stories.tsx +0 -620
  216. package/src/components/utility/InfiniteScroll/InfiniteScroll.test.stories.tsx +0 -361
  217. package/src/components/utility/InstallPrompt/InstallPrompt.stories.tsx +0 -118
  218. package/src/components/utility/InstallPrompt/InstallPrompt.test.stories.tsx +0 -83
  219. package/src/components/utility/InstallPrompt/__tests__/InstallPrompt.ios.test.tsx +0 -65
  220. package/src/components/utility/InstallPrompt/__tests__/usePwaInstall.test.tsx +0 -271
  221. package/src/components/utility/Portal/Portal.stories.tsx +0 -340
  222. package/src/components/utility/Portal/Portal.test.stories.tsx +0 -2086
  223. package/src/components/utility/Transition/Transition.stories.tsx +0 -493
  224. package/src/components/utility/Transition/Transition.test.stories.tsx +0 -854
  225. package/src/components/utility/VirtualList/VirtualList.stories.tsx +0 -1073
  226. package/src/components/utility/VirtualList/VirtualList.test.stories.tsx +0 -1085
  227. package/src/components/utility/WorkflowStep/WorkflowStep.stories.tsx +0 -431
  228. package/src/components/utility/WorkflowStep/WorkflowStep.test.stories.tsx +0 -364
  229. /package/src/{button.tsx → button/button.tsx} +0 -0
@@ -1,2086 +0,0 @@
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
- };