@hyphen/hyphen-components 7.3.2 → 7.3.4

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 (221) hide show
  1. package/dist/css/utilities.css +1 -1
  2. package/dist/css/variables.css +18 -28
  3. package/dist/hyphen-components.cjs.development.js +5873 -5019
  4. package/dist/hyphen-components.cjs.development.js.map +1 -1
  5. package/dist/hyphen-components.cjs.production.min.js +18 -2
  6. package/dist/hyphen-components.cjs.production.min.js.map +1 -1
  7. package/dist/hyphen-components.esm.js +5731 -4844
  8. package/dist/hyphen-components.esm.js.map +1 -1
  9. package/dist/index.d.ts +2799 -57
  10. package/dist/index.js +0 -1
  11. package/package.json +18 -19
  12. package/src/components/Badge/Badge.module.scss +6 -0
  13. package/src/components/Badge/Badge.stories.tsx +1 -0
  14. package/src/components/Badge/Badge.test.tsx +3 -2
  15. package/src/components/Badge/Badge.tsx +5 -3
  16. package/src/components/Box/Box.tsx +5 -2
  17. package/src/components/Button/Button.module.scss +1 -1
  18. package/src/components/Button/Button.test.tsx +2 -2
  19. package/src/components/Calendar/Calendar.test.tsx +262 -0
  20. package/src/components/Card/Card.tsx +2 -0
  21. package/src/components/CheckboxInput/components/Checkbox.module.scss +1 -1
  22. package/src/components/CheckboxInput/components/Checkbox.tsx +2 -0
  23. package/src/components/Details/Details.module.scss +2 -2
  24. package/src/components/Details/Details.tsx +2 -0
  25. package/src/components/Drawer/Drawer.stories.tsx +1 -1
  26. package/src/components/Drawer/Drawer.test.tsx +494 -56
  27. package/src/components/Drawer/Drawer.tsx +7 -1
  28. package/src/components/DropdownMenu/DropdownMenu.test.tsx +532 -12
  29. package/src/components/FormControl/FormControl.tsx +2 -0
  30. package/src/components/Formik/Formik.stories.tsx +30 -7
  31. package/src/components/Formik/FormikSelectInput/FormikSelectInput.tsx +6 -5
  32. package/src/components/Formik/FormikToggleGroup/FormikToggleGroup.tsx +1 -1
  33. package/src/components/HelpText/HelpText.tsx +2 -0
  34. package/src/components/Icon/Icon.stories.tsx +1 -1
  35. package/src/components/Icon/Icon.tsx +2 -0
  36. package/src/components/Modal/Modal.test.tsx +630 -81
  37. package/src/components/Modal/Modal.tsx +2 -0
  38. package/src/components/Modal/components/ModalFooter/ModalFooter.test.tsx +2 -2
  39. package/src/components/Popover/Popover.tsx +2 -0
  40. package/src/components/RadioGroup/RadioInput/RadioInput.tsx +2 -0
  41. package/src/components/SelectInput/SelectInput.stories.tsx +22 -22
  42. package/src/components/SelectInput/SelectInput.tsx +13 -9
  43. package/src/components/SelectInputInset/SelectInputInset.tsx +2 -0
  44. package/src/components/Sidebar/Sidebar.module.scss +4 -0
  45. package/src/components/Sidebar/Sidebar.stories.tsx +8 -4
  46. package/src/components/Sidebar/Sidebar.test.tsx +7 -4
  47. package/src/components/Sidebar/Sidebar.tsx +7 -4
  48. package/src/components/Table/Table.stories.tsx +102 -52
  49. package/src/components/TextInput/TextInput.tsx +2 -0
  50. package/src/components/TextInputInset/TextInputInset.tsx +2 -0
  51. package/src/components/TextareaInputInset/TextareaInputInset.tsx +2 -0
  52. package/src/components/TimePickerNative/TimePickerNative.stories.tsx +0 -1
  53. package/src/components/Toast/Toast.store.ts +1 -1
  54. package/src/components/Toast/Toast.stories.tsx +3 -2
  55. package/src/components/Toast/Toast.test.tsx +8 -6
  56. package/src/components/Toggle/Toggle.tsx +2 -0
  57. package/src/components/ToggleGroup/ToggleGroup.tsx +2 -0
  58. package/src/docs/Colors.mdx +0 -13
  59. package/src/index.ts +2 -0
  60. package/src/lib/getColumnKeys.ts +3 -3
  61. package/src/lib/mergeRefs.ts +1 -1
  62. package/src/lib/tokens.ts +4 -4
  63. package/dist/components/Alert/Alert.constants.d.ts +0 -8
  64. package/dist/components/Alert/Alert.d.ts +0 -42
  65. package/dist/components/Alert/Alert.stories.d.ts +0 -12
  66. package/dist/components/Alert/Alert.types.d.ts +0 -7
  67. package/dist/components/AspectRatio/AspectRatio.d.ts +0 -3
  68. package/dist/components/AspectRatio/AspectRatio.stories.d.ts +0 -6
  69. package/dist/components/Badge/Badge.d.ts +0 -24
  70. package/dist/components/Badge/Badge.stories.d.ts +0 -8
  71. package/dist/components/Box/Box.d.ts +0 -247
  72. package/dist/components/Box/Box.stories.d.ts +0 -46
  73. package/dist/components/Button/Button.constants.d.ts +0 -3
  74. package/dist/components/Button/Button.d.ts +0 -53
  75. package/dist/components/Button/Button.stories.d.ts +0 -16
  76. package/dist/components/Calendar/Calendar.d.ts +0 -7
  77. package/dist/components/Calendar/Calendar.stories.d.ts +0 -12
  78. package/dist/components/Card/Card.d.ts +0 -17
  79. package/dist/components/Card/Card.stories.d.ts +0 -8
  80. package/dist/components/Card/components/CardFooter/CardFooter.d.ts +0 -13
  81. package/dist/components/Card/components/CardHeader/CardHeader.d.ts +0 -13
  82. package/dist/components/Card/components/CardSection/CardSection.d.ts +0 -46
  83. package/dist/components/Card/components/index.d.ts +0 -3
  84. package/dist/components/CheckboxInput/CheckboxInput.d.ts +0 -72
  85. package/dist/components/CheckboxInput/CheckboxInput.stories.d.ts +0 -18
  86. package/dist/components/CheckboxInput/components/Checkbox.d.ts +0 -71
  87. package/dist/components/CheckboxInput/components/CheckboxIcon.d.ts +0 -27
  88. package/dist/components/Collapsible/Collapsible.d.ts +0 -5
  89. package/dist/components/Collapsible/Collapsible.stories.d.ts +0 -9
  90. package/dist/components/Details/Details.d.ts +0 -15
  91. package/dist/components/Details/Details.stories.d.ts +0 -6
  92. package/dist/components/Details/DetailsSummary.d.ts +0 -7
  93. package/dist/components/Drawer/Drawer.d.ts +0 -105
  94. package/dist/components/Drawer/Drawer.stories.d.ts +0 -62
  95. package/dist/components/DropdownMenu/DropdownMenu.d.ts +0 -25
  96. package/dist/components/DropdownMenu/DropdownMenu.stories.d.ts +0 -9
  97. package/dist/components/FormControl/FormControl.d.ts +0 -38
  98. package/dist/components/FormLabel/FormLabel.d.ts +0 -41
  99. package/dist/components/FormLabel/FormLabel.stories.d.ts +0 -6
  100. package/dist/components/Formik/Formik.stories.d.ts +0 -18
  101. package/dist/components/Formik/FormikCheckboxInput/FormikCheckboxInput.d.ts +0 -12
  102. package/dist/components/Formik/FormikRadioGroup/FormikRadioGroup.d.ts +0 -12
  103. package/dist/components/Formik/FormikSelectInput/FormikSelectInput.d.ts +0 -13
  104. package/dist/components/Formik/FormikSelectInputInset/FormikSelectInputInset.d.ts +0 -12
  105. package/dist/components/Formik/FormikSelectInputNative/FormikSelectInputNative.d.ts +0 -12
  106. package/dist/components/Formik/FormikSwitch/FormikSwitch.d.ts +0 -12
  107. package/dist/components/Formik/FormikTextInput/FormikTextInput.d.ts +0 -12
  108. package/dist/components/Formik/FormikTextInputInset/FormikTextInputInset.d.ts +0 -12
  109. package/dist/components/Formik/FormikTextareaInput/FormikTextareaInput.d.ts +0 -12
  110. package/dist/components/Formik/FormikTextareaInputInset/FormikTextareaInputInset.d.ts +0 -12
  111. package/dist/components/Formik/FormikTimePicker/FormikTimePicker.d.ts +0 -12
  112. package/dist/components/Formik/FormikTimePickerNative/FormikTimePickerNative.d.ts +0 -12
  113. package/dist/components/Formik/FormikToggleGroup/FormikToggleGroup.d.ts +0 -20
  114. package/dist/components/Formik/FormikToggleGroupMulti/FormikToggleGroupMulti.d.ts +0 -18
  115. package/dist/components/Heading/Heading.constants.d.ts +0 -10
  116. package/dist/components/Heading/Heading.d.ts +0 -35
  117. package/dist/components/Heading/Heading.stories.d.ts +0 -9
  118. package/dist/components/HelpText/HelpText.d.ts +0 -12
  119. package/dist/components/Icon/Icon.d.ts +0 -22
  120. package/dist/components/Icon/Icon.stories.d.ts +0 -10
  121. package/dist/components/InputValidationMessage/InputValidationMessage.d.ts +0 -9
  122. package/dist/components/Modal/Modal.d.ts +0 -83
  123. package/dist/components/Modal/Modal.stories.d.ts +0 -13
  124. package/dist/components/Modal/components/ModalBody/ModalBody.d.ts +0 -4
  125. package/dist/components/Modal/components/ModalFooter/ModalFooter.d.ts +0 -4
  126. package/dist/components/Modal/components/ModalHeader/ModalHeader.d.ts +0 -21
  127. package/dist/components/Modal/components/index.d.ts +0 -4
  128. package/dist/components/Pagination/Pagination.d.ts +0 -51
  129. package/dist/components/Pagination/Pagination.stories.d.ts +0 -8
  130. package/dist/components/Pagination/Pagination.utilities.d.ts +0 -10
  131. package/dist/components/Popover/Popover.d.ts +0 -8
  132. package/dist/components/Popover/Popover.stories.d.ts +0 -7
  133. package/dist/components/RadioGroup/RadioGroup.d.ts +0 -75
  134. package/dist/components/RadioGroup/RadioGroup.stories.d.ts +0 -16
  135. package/dist/components/RadioGroup/RadioInput/RadioInput.d.ts +0 -57
  136. package/dist/components/RadioGroup/RadioInput/RadioInputIcon.d.ts +0 -27
  137. package/dist/components/RangeInput/RangeInput.d.ts +0 -29
  138. package/dist/components/RangeInput/RangeInput.stories.d.ts +0 -7
  139. package/dist/components/ResponsiveProvider/ResponsiveProvider.d.ts +0 -17
  140. package/dist/components/ResponsiveProvider/ResponsiveProvider.stories.d.ts +0 -7
  141. package/dist/components/SelectInput/SelectInput.d.ts +0 -148
  142. package/dist/components/SelectInput/SelectInput.stories.d.ts +0 -24
  143. package/dist/components/SelectInputInset/SelectInputInset.d.ts +0 -92
  144. package/dist/components/SelectInputInset/SelectInputInset.stories.d.ts +0 -12
  145. package/dist/components/SelectInputNative/SelectInputNative.d.ts +0 -45
  146. package/dist/components/SelectInputNative/SelectInputNative.stories.d.ts +0 -19
  147. package/dist/components/Sidebar/Sidebar.d.ts +0 -57
  148. package/dist/components/Sidebar/Sidebar.stories.d.ts +0 -9
  149. package/dist/components/Spinner/Spinner.d.ts +0 -12
  150. package/dist/components/Spinner/Spinner.stories.d.ts +0 -8
  151. package/dist/components/Switch/Switch.d.ts +0 -64
  152. package/dist/components/Switch/Switch.stories.d.ts +0 -12
  153. package/dist/components/Table/Table.d.ts +0 -86
  154. package/dist/components/Table/Table.stories.d.ts +0 -31
  155. package/dist/components/Table/TableBody/TableBody.d.ts +0 -52
  156. package/dist/components/Table/TableBody/TableBodyCell/TableBodyCell.d.ts +0 -45
  157. package/dist/components/Table/TableHead/TableHead.d.ts +0 -46
  158. package/dist/components/Table/TableHead/TableHeaderCell/TableHeaderCell.d.ts +0 -65
  159. package/dist/components/Table/common/TableRow/TableRow.d.ts +0 -67
  160. package/dist/components/TextInput/TextInput.d.ts +0 -106
  161. package/dist/components/TextInput/TextInput.stories.d.ts +0 -19
  162. package/dist/components/TextInputInset/TextInputInset.d.ts +0 -102
  163. package/dist/components/TextInputInset/TextInputInset.stories.d.ts +0 -13
  164. package/dist/components/TextareaInput/TextareaInput.d.ts +0 -97
  165. package/dist/components/TextareaInput/TextareaInput.stories.d.ts +0 -23
  166. package/dist/components/TextareaInputInset/TextareaInputInset.d.ts +0 -105
  167. package/dist/components/TextareaInputInset/TextareaInputInset.stories.d.ts +0 -12
  168. package/dist/components/ThemeProvider/ThemeProvider.d.ts +0 -15
  169. package/dist/components/ThemeProvider/ThemeProvider.stories.d.ts +0 -6
  170. package/dist/components/TimePicker/TimePicker.d.ts +0 -35
  171. package/dist/components/TimePicker/TimePicker.stories.d.ts +0 -12
  172. package/dist/components/TimePickerNative/TimePickerNative.d.ts +0 -39
  173. package/dist/components/TimePickerNative/TimePickerNative.stories.d.ts +0 -11
  174. package/dist/components/Toast/Toast.store.d.ts +0 -36
  175. package/dist/components/Toast/Toast.stories.d.ts +0 -14
  176. package/dist/components/Toast/Toast.types.d.ts +0 -75
  177. package/dist/components/Toast/ToastContainer.d.ts +0 -43
  178. package/dist/components/Toast/ToastNotification.d.ts +0 -28
  179. package/dist/components/Toast/index.d.ts +0 -4
  180. package/dist/components/Toast/toast.d.ts +0 -20
  181. package/dist/components/Toast/useToasts.d.ts +0 -14
  182. package/dist/components/Toggle/Toggle.d.ts +0 -7
  183. package/dist/components/Toggle/Toggle.stories.d.ts +0 -11
  184. package/dist/components/ToggleGroup/ToggleGroup.d.ts +0 -19
  185. package/dist/components/ToggleGroup/ToggleGroup.stories.d.ts +0 -12
  186. package/dist/components/Tooltip/Tooltip.d.ts +0 -8
  187. package/dist/components/Tooltip/Tooltip.stories.d.ts +0 -8
  188. package/dist/constants/keyCodes.d.ts +0 -2
  189. package/dist/css/index.css +0 -36
  190. package/dist/hooks/index.d.ts +0 -6
  191. package/dist/hooks/useBreakpoint/useBreakpoint.d.ts +0 -9
  192. package/dist/hooks/useBreakpoint/useBreakpoint.stories.d.ts +0 -6
  193. package/dist/hooks/useIsMobile/useIsMobile.d.ts +0 -1
  194. package/dist/hooks/useIsMobile/useIsMobile.stories.d.ts +0 -6
  195. package/dist/hooks/useIsomorphicLayoutEffect/useIsomorphicLayoutEffect.d.ts +0 -2
  196. package/dist/hooks/useOpenClose/useOpenClose.d.ts +0 -39
  197. package/dist/hooks/useOpenClose/useOpenClose.stories.d.ts +0 -6
  198. package/dist/hooks/useTheme/useTheme.d.ts +0 -5
  199. package/dist/hooks/useTheme/useTheme.stories.d.ts +0 -6
  200. package/dist/hooks/useWindowSize/useWindowSize.d.ts +0 -7
  201. package/dist/hooks/useWindowSize/useWindowSize.stories.d.ts +0 -6
  202. package/dist/lib/cssShorthandToClasses.d.ts +0 -4
  203. package/dist/lib/doesStringIncludeCssUnit.d.ts +0 -1
  204. package/dist/lib/generateResponsiveClasses.d.ts +0 -2
  205. package/dist/lib/getAutoCompleteValue.d.ts +0 -1
  206. package/dist/lib/getColumnKeys.d.ts +0 -3
  207. package/dist/lib/getDimensionCss.d.ts +0 -12
  208. package/dist/lib/getElementType.d.ts +0 -14
  209. package/dist/lib/getFlexCss.d.ts +0 -9
  210. package/dist/lib/index.d.ts +0 -15
  211. package/dist/lib/isFunction.d.ts +0 -3
  212. package/dist/lib/mergeRefs.d.ts +0 -2
  213. package/dist/lib/prefersReducedMotion.d.ts +0 -1
  214. package/dist/lib/react-children-utilities/filter.d.ts +0 -3
  215. package/dist/lib/react-children-utilities/index.d.ts +0 -1
  216. package/dist/lib/reactRouterClickHandler.d.ts +0 -12
  217. package/dist/lib/resolveValue.d.ts +0 -3
  218. package/dist/lib/tokens.d.ts +0 -22
  219. package/dist/modes.d.ts +0 -8
  220. package/dist/types/index.d.ts +0 -103
  221. package/dist/types/lib.types.d.ts +0 -3
@@ -1,88 +1,637 @@
1
1
  import React from 'react';
2
- import { render } from '@testing-library/react';
2
+ import {
3
+ render,
4
+ screen,
5
+ fireEvent,
6
+ waitFor,
7
+ act,
8
+ } from '@testing-library/react';
9
+ import userEvent from '@testing-library/user-event';
3
10
  import { Modal } from './Modal';
4
11
 
5
12
  describe('Modal', () => {
6
- test('renders its children', () => {
7
- const { getByText } = render(
8
- <Modal isOpen onDismiss={() => {}} ariaLabel="testDefault">
9
- test modal
10
- </Modal>
11
- );
12
- expect(getByText('test modal')).toBeInTheDocument();
13
- });
14
-
15
- test('it open and closes based on isOpen prop', () => {
16
- const { queryByText, getByText, rerender } = render(
17
- <Modal isOpen={false} onDismiss={() => {}} ariaLabel="testIsOpen">
18
- test modal
19
- </Modal>
20
- );
21
-
22
- expect(queryByText('test modal')).toBe(null);
23
-
24
- rerender(
25
- <Modal isOpen onDismiss={() => {}} ariaLabel="testIsOpen">
26
- test modal
27
- </Modal>
28
- );
29
-
30
- expect(getByText('test modal')).toBeInTheDocument();
31
- });
32
-
33
- test('Subcomponents', () => {
34
- const { getByText } = render(
35
- <Modal isOpen onDismiss={() => {}} ariaLabel="testSubcomponents">
36
- <Modal.Header
37
- id="titleFooterBody"
38
- title="The Modal Title"
13
+ beforeEach(() => {
14
+ jest.clearAllMocks();
15
+ });
16
+
17
+ describe('Basic rendering', () => {
18
+ test('renders its children', () => {
19
+ const { getByText } = render(
20
+ <Modal isOpen onDismiss={() => {}} ariaLabel="testDefault">
21
+ test modal
22
+ </Modal>
23
+ );
24
+ expect(getByText('test modal')).toBeInTheDocument();
25
+ });
26
+
27
+ test('does not render when isOpen is false', () => {
28
+ const { queryByText } = render(
29
+ <Modal isOpen={false} onDismiss={() => {}} ariaLabel="testClosed">
30
+ test modal
31
+ </Modal>
32
+ );
33
+ expect(queryByText('test modal')).not.toBeInTheDocument();
34
+ });
35
+
36
+ test('it open and closes based on isOpen prop', () => {
37
+ const { queryByText, getByText, rerender } = render(
38
+ <Modal isOpen={false} onDismiss={() => {}} ariaLabel="testIsOpen">
39
+ test modal
40
+ </Modal>
41
+ );
42
+
43
+ expect(queryByText('test modal')).toBe(null);
44
+
45
+ rerender(
46
+ <Modal isOpen onDismiss={() => {}} ariaLabel="testIsOpen">
47
+ test modal
48
+ </Modal>
49
+ );
50
+
51
+ expect(getByText('test modal')).toBeInTheDocument();
52
+ });
53
+
54
+ test('applies custom className', () => {
55
+ render(
56
+ <Modal
57
+ isOpen
58
+ onDismiss={() => {}}
59
+ ariaLabel="testClassName"
60
+ className="custom-modal"
61
+ >
62
+ content
63
+ </Modal>
64
+ );
65
+
66
+ const modal = screen.getByRole('dialog');
67
+ expect(modal).toHaveClass('custom-modal');
68
+ });
69
+ });
70
+
71
+ describe('Subcomponents', () => {
72
+ test('renders Modal.Header with title', () => {
73
+ const { getByText } = render(
74
+ <Modal isOpen onDismiss={() => {}} ariaLabel="testSubcomponents">
75
+ <Modal.Header id="titleFooterBody" title="The Modal Title" />
76
+ </Modal>
77
+ );
78
+
79
+ expect(getByText('The Modal Title')).toBeInTheDocument();
80
+ });
81
+
82
+ test('renders Modal.Header with children', () => {
83
+ render(
84
+ <Modal isOpen onDismiss={() => {}} ariaLabel="testHeaderChildren">
85
+ <Modal.Header id="header-id">
86
+ <span>Custom header content</span>
87
+ </Modal.Header>
88
+ </Modal>
89
+ );
90
+
91
+ expect(screen.getByText('Custom header content')).toBeInTheDocument();
92
+ });
93
+
94
+ test('renders Modal.Header with title and children', () => {
95
+ render(
96
+ <Modal isOpen onDismiss={() => {}} ariaLabel="testHeaderBoth">
97
+ <Modal.Header id="header-id" title="Main Title">
98
+ <span>Subtitle</span>
99
+ </Modal.Header>
100
+ </Modal>
101
+ );
102
+
103
+ expect(screen.getByText('Main Title')).toBeInTheDocument();
104
+ expect(screen.getByText('Subtitle')).toBeInTheDocument();
105
+ });
106
+
107
+ test('renders Modal.Header with close button when onDismiss is provided', () => {
108
+ const handleDismiss = jest.fn();
109
+ render(
110
+ <Modal isOpen onDismiss={() => {}} ariaLabel="testCloseButton">
111
+ <Modal.Header
112
+ id="header-id"
113
+ title="Title"
114
+ onDismiss={handleDismiss}
115
+ />
116
+ </Modal>
117
+ );
118
+
119
+ const closeButton = screen.getByLabelText('close');
120
+ expect(closeButton).toBeInTheDocument();
121
+ fireEvent.click(closeButton);
122
+ expect(handleDismiss).toHaveBeenCalled();
123
+ });
124
+
125
+ test('renders Modal.Body', () => {
126
+ const { getByText } = render(
127
+ <Modal isOpen onDismiss={() => {}} ariaLabel="testBody">
128
+ <Modal.Body>Modal body content</Modal.Body>
129
+ </Modal>
130
+ );
131
+
132
+ expect(getByText('Modal body content')).toBeInTheDocument();
133
+ });
134
+
135
+ test('renders Modal.Footer', () => {
136
+ const { getByText } = render(
137
+ <Modal isOpen onDismiss={() => {}} ariaLabel="testFooter">
138
+ <Modal.Footer>This is content in the modal footer</Modal.Footer>
139
+ </Modal>
140
+ );
141
+
142
+ expect(
143
+ getByText('This is content in the modal footer')
144
+ ).toBeInTheDocument();
145
+ });
146
+
147
+ test('renders all subcomponents together', () => {
148
+ const { getByText } = render(
149
+ <Modal isOpen onDismiss={() => {}} ariaLabel="testSubcomponents">
150
+ <Modal.Header
151
+ id="titleFooterBody"
152
+ title="The Modal Title"
153
+ onDismiss={() => {}}
154
+ />
155
+ <Modal.Body>Modal body content</Modal.Body>
156
+ <Modal.Footer>This is content in the modal footer</Modal.Footer>
157
+ </Modal>
158
+ );
159
+
160
+ expect(getByText('The Modal Title')).toBeInTheDocument();
161
+ expect(getByText('Modal body content')).toBeInTheDocument();
162
+ expect(
163
+ getByText('This is content in the modal footer')
164
+ ).toBeInTheDocument();
165
+ });
166
+ });
167
+
168
+ describe('Styling', () => {
169
+ test('applies maxWidth styles', () => {
170
+ const { getByLabelText } = render(
171
+ <Modal
172
+ isOpen
173
+ onDismiss={() => {}}
174
+ ariaLabel="testMaxWidth"
175
+ maxWidth="500px"
176
+ >
177
+ test modal
178
+ </Modal>
179
+ );
180
+
181
+ expect(getByLabelText('testMaxWidth').parentElement).toHaveStyle(
182
+ 'max-width: 500px'
183
+ );
184
+ });
185
+
186
+ test('applies custom style prop', () => {
187
+ const { getByLabelText } = render(
188
+ <Modal
189
+ isOpen
190
+ onDismiss={() => {}}
191
+ ariaLabel="testStyle"
192
+ style={{ backgroundColor: '#cacaca' }}
193
+ >
194
+ test modal
195
+ </Modal>
196
+ );
197
+
198
+ expect(getByLabelText('testStyle').parentElement).toHaveStyle(
199
+ 'background-color: #cacaca'
200
+ );
201
+ });
202
+
203
+ test('applies overflow prop', () => {
204
+ render(
205
+ <Modal
206
+ isOpen
207
+ onDismiss={() => {}}
208
+ ariaLabel="testOverflow"
209
+ overflow="visible"
210
+ >
211
+ test modal
212
+ </Modal>
213
+ );
214
+
215
+ const modal = screen.getByRole('dialog');
216
+ expect(modal).toHaveClass('overflow-visible');
217
+ });
218
+
219
+ test('applies default overflow hidden', () => {
220
+ render(
221
+ <Modal isOpen onDismiss={() => {}} ariaLabel="testDefaultOverflow">
222
+ test modal
223
+ </Modal>
224
+ );
225
+
226
+ const modal = screen.getByRole('dialog');
227
+ expect(modal).toHaveClass('overflow-hidden');
228
+ });
229
+
230
+ test('applies background color', () => {
231
+ render(
232
+ <Modal
233
+ isOpen
234
+ onDismiss={() => {}}
235
+ ariaLabel="testBackground"
236
+ background="secondary"
237
+ >
238
+ test modal
239
+ </Modal>
240
+ );
241
+
242
+ expect(screen.getByLabelText('testBackground')).toHaveClass(
243
+ 'background-color-secondary'
244
+ );
245
+ });
246
+ });
247
+
248
+ describe('Accessibility', () => {
249
+ test('applies aria-label attribute', () => {
250
+ render(
251
+ <Modal isOpen onDismiss={() => {}} ariaLabel="Accessible Modal">
252
+ content
253
+ </Modal>
254
+ );
255
+
256
+ expect(screen.getByLabelText('Accessible Modal')).toBeInTheDocument();
257
+ });
258
+
259
+ test('applies aria-labelledby attribute', () => {
260
+ const { getByLabelText } = render(
261
+ <Modal
262
+ isOpen
39
263
  onDismiss={() => {}}
40
- />
41
- <Modal.Body>Modal body content</Modal.Body>
42
- <Modal.Footer>This is content in the modal footer</Modal.Footer>
43
- </Modal>
44
- );
45
-
46
- expect(getByText('The Modal Title')).toBeInTheDocument();
47
- expect(getByText('Modal body content')).toBeInTheDocument();
48
- expect(
49
- getByText('This is content in the modal footer')
50
- ).toBeInTheDocument();
51
- });
52
-
53
- test('applies maxWidth styles', () => {
54
- const { getByLabelText } = render(
55
- <Modal
56
- isOpen
57
- onDismiss={() => {}}
58
- ariaLabel="testMaxWidth"
59
- maxWidth="500px"
60
- >
61
- test modal
62
- </Modal>
63
- );
64
-
65
- expect(getByLabelText('testMaxWidth').parentElement).toHaveStyle(
66
- 'max-width: 500px'
67
- );
68
- });
69
-
70
- test('applies aria-labelledby attribute', () => {
71
- const { getByLabelText } = render(
72
- <Modal
73
- isOpen
74
- onDismiss={() => {}}
75
- ariaLabelledBy="modalTitle"
76
- ariaLabel="testAriaLabelledBy"
77
- >
78
- <h1 id="modalTitle">Modal Title</h1>
79
- test modal
80
- </Modal>
81
- );
82
-
83
- expect(getByLabelText('testAriaLabelledBy')).toHaveAttribute(
84
- 'aria-labelledby',
85
- 'modalTitle'
86
- );
264
+ ariaLabelledBy="modalTitle"
265
+ ariaLabel="testAriaLabelledBy"
266
+ >
267
+ <h1 id="modalTitle">Modal Title</h1>
268
+ test modal
269
+ </Modal>
270
+ );
271
+
272
+ expect(getByLabelText('testAriaLabelledBy')).toHaveAttribute(
273
+ 'aria-labelledby',
274
+ 'modalTitle'
275
+ );
276
+ });
277
+
278
+ test('Modal.Header sets the id for aria-labelledby', () => {
279
+ render(
280
+ <Modal
281
+ isOpen
282
+ onDismiss={() => {}}
283
+ ariaLabelledBy="custom-header-id"
284
+ ariaLabel="test"
285
+ >
286
+ <Modal.Header id="custom-header-id" title="Accessible Title" />
287
+ </Modal>
288
+ );
289
+
290
+ const title = screen.getByText('Accessible Title');
291
+ expect(title).toHaveAttribute('id', 'custom-header-id');
292
+ });
293
+ });
294
+
295
+ describe('Focus management', () => {
296
+ test('focus is placed inside the modal when opened', async () => {
297
+ render(
298
+ <Modal isOpen onDismiss={() => {}} ariaLabel="Focus Trap Modal">
299
+ <Modal.Body>
300
+ <button>First button</button>
301
+ <button>Second button</button>
302
+ </Modal.Body>
303
+ </Modal>
304
+ );
305
+
306
+ const dialog = screen.getByRole('dialog');
307
+ const firstButton = screen.getByText('First button');
308
+ const secondButton = screen.getByText('Second button');
309
+
310
+ // Verify focusable elements are present
311
+ expect(firstButton).toBeInTheDocument();
312
+ expect(secondButton).toBeInTheDocument();
313
+
314
+ // Focus should be within the modal (FocusLock sets focus inside)
315
+ await waitFor(() => {
316
+ expect(dialog.contains(document.activeElement)).toBe(true);
317
+ });
318
+ });
319
+
320
+ test('tabbing cycles focus within the modal', async () => {
321
+ const user = userEvent.setup();
322
+ render(
323
+ <Modal isOpen onDismiss={() => {}} ariaLabel="Focus Trap Modal">
324
+ <Modal.Body>
325
+ <button data-testid="btn1">First button</button>
326
+ <button data-testid="btn2">Second button</button>
327
+ </Modal.Body>
328
+ </Modal>
329
+ );
330
+
331
+ const dialog = screen.getByRole('dialog');
332
+
333
+ // Wait for focus lock to activate
334
+ await waitFor(() => {
335
+ expect(dialog.contains(document.activeElement)).toBe(true);
336
+ });
337
+
338
+ // Tab multiple times - focus should remain within the modal
339
+ await user.tab();
340
+ expect(dialog.contains(document.activeElement)).toBe(true);
341
+
342
+ await user.tab();
343
+ expect(dialog.contains(document.activeElement)).toBe(true);
344
+
345
+ await user.tab();
346
+ expect(dialog.contains(document.activeElement)).toBe(true);
347
+ });
348
+
349
+ test('accepts initialFocusRef prop and focuses the referenced element', () => {
350
+ jest.useFakeTimers();
351
+
352
+ const TestComponent = () => {
353
+ const ref = React.useRef<HTMLDivElement>(null);
354
+ return (
355
+ <Modal
356
+ isOpen
357
+ onDismiss={() => {}}
358
+ ariaLabel="Initial Focus Modal"
359
+ initialFocusRef={ref}
360
+ >
361
+ <Modal.Body>
362
+ <div ref={ref} tabIndex={-1} data-testid="focus-target">
363
+ Focus target
364
+ </div>
365
+ </Modal.Body>
366
+ </Modal>
367
+ );
368
+ };
369
+
370
+ render(<TestComponent />);
371
+
372
+ const focusTarget = screen.getByTestId('focus-target');
373
+ expect(focusTarget).toBeInTheDocument();
374
+
375
+ // The Modal uses a setTimeout (100ms) to focus the initialFocusRef element
376
+ act(() => {
377
+ jest.advanceTimersByTime(100);
378
+ });
379
+
380
+ expect(focusTarget).toHaveFocus();
381
+
382
+ jest.useRealTimers();
383
+ });
384
+ });
385
+
386
+ describe('onDismiss behavior', () => {
387
+ test('calls onDismiss when Escape key is pressed', async () => {
388
+ const handleDismiss = jest.fn();
389
+ render(
390
+ <Modal isOpen onDismiss={handleDismiss} ariaLabel="Escape Modal">
391
+ content
392
+ </Modal>
393
+ );
394
+
395
+ expect(screen.getByText('content')).toBeInTheDocument();
396
+
397
+ // Simulate pressing Escape key - react-modal listens on the dialog
398
+ fireEvent.keyDown(screen.getByRole('dialog'), {
399
+ key: 'Escape',
400
+ code: 'Escape',
401
+ keyCode: 27,
402
+ });
403
+
404
+ await waitFor(() => {
405
+ expect(handleDismiss).toHaveBeenCalledTimes(1);
406
+ });
407
+ });
408
+ });
409
+
410
+ describe('fullScreenMobile', () => {
411
+ test('applies fullscreen class when fullScreenMobile is true', () => {
412
+ const { baseElement } = render(
413
+ <Modal
414
+ isOpen
415
+ onDismiss={() => {}}
416
+ ariaLabel="Fullscreen Modal"
417
+ fullScreenMobile
418
+ >
419
+ content
420
+ </Modal>
421
+ );
422
+
423
+ expect(screen.getByText('content')).toBeInTheDocument();
424
+
425
+ // The overlay is rendered in a portal, so query from baseElement (document.body)
426
+ const overlay = baseElement.querySelector('.ReactModal__Overlay');
427
+ expect(overlay).toHaveClass('fullscreen');
428
+ });
429
+
430
+ test('does not apply fullscreen class when fullScreenMobile is false', () => {
431
+ const { baseElement } = render(
432
+ <Modal
433
+ isOpen
434
+ onDismiss={() => {}}
435
+ ariaLabel="Non-Fullscreen Modal"
436
+ fullScreenMobile={false}
437
+ >
438
+ content
439
+ </Modal>
440
+ );
441
+
442
+ expect(screen.getByText('content')).toBeInTheDocument();
443
+
444
+ // The overlay is rendered in a portal, so query from baseElement (document.body)
445
+ const overlay = baseElement.querySelector('.ReactModal__Overlay');
446
+ expect(overlay).not.toHaveClass('fullscreen');
447
+ });
448
+ });
449
+
450
+ describe('containerRef', () => {
451
+ test('renders in custom container when containerRef is provided', () => {
452
+ const TestComponent = () => {
453
+ const containerRef = React.useRef<HTMLDivElement>(null);
454
+ return (
455
+ <div>
456
+ <div ref={containerRef} data-testid="custom-container" />
457
+ <Modal
458
+ isOpen
459
+ onDismiss={() => {}}
460
+ ariaLabel="Custom Container Modal"
461
+ containerRef={containerRef as React.RefObject<Node>}
462
+ >
463
+ Modal content
464
+ </Modal>
465
+ </div>
466
+ );
467
+ };
468
+
469
+ render(<TestComponent />);
470
+ expect(screen.getByText('Modal content')).toBeInTheDocument();
471
+ });
472
+ });
473
+
474
+ describe('allowPinchZoom', () => {
475
+ test('accepts allowPinchZoom prop', () => {
476
+ render(
477
+ <Modal
478
+ isOpen
479
+ onDismiss={() => {}}
480
+ ariaLabel="Pinch Zoom Modal"
481
+ allowPinchZoom
482
+ >
483
+ content
484
+ </Modal>
485
+ );
486
+
487
+ expect(screen.getByText('content')).toBeInTheDocument();
488
+ });
489
+ });
490
+
491
+ describe('Ref forwarding', () => {
492
+ test('forwards ref to the modal container', () => {
493
+ const ref = React.createRef<HTMLDivElement>();
494
+ render(
495
+ <Modal isOpen onDismiss={() => {}} ariaLabel="Ref Modal" ref={ref}>
496
+ content
497
+ </Modal>
498
+ );
499
+
500
+ expect(ref.current).toBeInstanceOf(HTMLDivElement);
501
+ });
502
+ });
503
+
504
+ describe('Static properties', () => {
505
+ test('Modal has Header static property', () => {
506
+ expect(Modal.Header).toBeDefined();
507
+ });
508
+
509
+ test('Modal has Body static property', () => {
510
+ expect(Modal.Body).toBeDefined();
511
+ });
512
+
513
+ test('Modal has Footer static property', () => {
514
+ expect(Modal.Footer).toBeDefined();
515
+ });
516
+ });
517
+
518
+ describe('Modal.Header', () => {
519
+ test('renders close button only when onDismiss is provided', () => {
520
+ const { rerender } = render(
521
+ <Modal isOpen onDismiss={() => {}} ariaLabel="test">
522
+ <Modal.Header id="header" title="Without Close" />
523
+ </Modal>
524
+ );
525
+
526
+ expect(screen.queryByLabelText('close')).not.toBeInTheDocument();
527
+
528
+ rerender(
529
+ <Modal isOpen onDismiss={() => {}} ariaLabel="test">
530
+ <Modal.Header id="header" title="With Close" onDismiss={() => {}} />
531
+ </Modal>
532
+ );
533
+
534
+ expect(screen.getByLabelText('close')).toBeInTheDocument();
535
+ });
536
+
537
+ test('header has correct justify-content when no title or children', () => {
538
+ render(
539
+ <Modal isOpen onDismiss={() => {}} ariaLabel="test">
540
+ <Modal.Header id="header" onDismiss={() => {}} />
541
+ </Modal>
542
+ );
543
+
544
+ // When no title/children, close button should be at flex-end
545
+ expect(screen.getByLabelText('close')).toBeInTheDocument();
546
+ });
547
+ });
548
+
549
+ describe('Modal.Body defaults', () => {
550
+ test('applies default flex, overflow, and height', () => {
551
+ render(
552
+ <Modal isOpen onDismiss={() => {}} ariaLabel="test">
553
+ <Modal.Body data-testid="modal-body">Body content</Modal.Body>
554
+ </Modal>
555
+ );
556
+
557
+ const body = screen.getByTestId('modal-body');
558
+ expect(body).toHaveClass('flex-auto', 'overflow-auto', 'h-100');
559
+ });
560
+
561
+ test('allows overriding default props', () => {
562
+ render(
563
+ <Modal isOpen onDismiss={() => {}} ariaLabel="test">
564
+ <Modal.Body
565
+ flex="none"
566
+ overflow="hidden"
567
+ height="50"
568
+ data-testid="modal-body"
569
+ >
570
+ Body content
571
+ </Modal.Body>
572
+ </Modal>
573
+ );
574
+
575
+ const body = screen.getByTestId('modal-body');
576
+ expect(body).toHaveClass('flex-none', 'overflow-hidden', 'h-50');
577
+ });
578
+ });
579
+
580
+ describe('Modal.Footer defaults', () => {
581
+ test('applies default direction, alignItems, justifyContent, and gap', () => {
582
+ render(
583
+ <Modal isOpen onDismiss={() => {}} ariaLabel="test">
584
+ <Modal.Footer data-testid="modal-footer">Footer content</Modal.Footer>
585
+ </Modal>
586
+ );
587
+
588
+ const footer = screen.getByTestId('modal-footer');
589
+ expect(footer).toHaveClass(
590
+ 'flex-direction-row',
591
+ 'align-items-center',
592
+ 'justify-content-flex-end',
593
+ 'g-md'
594
+ );
595
+ });
596
+
597
+ test('allows overriding footer defaults', () => {
598
+ render(
599
+ <Modal isOpen onDismiss={() => {}} ariaLabel="test">
600
+ <Modal.Footer
601
+ direction="column"
602
+ justifyContent="flex-start"
603
+ gap="lg"
604
+ data-testid="modal-footer"
605
+ >
606
+ Footer content
607
+ </Modal.Footer>
608
+ </Modal>
609
+ );
610
+
611
+ const footer = screen.getByTestId('modal-footer');
612
+ expect(footer).toHaveClass(
613
+ 'flex-direction-column',
614
+ 'justify-content-flex-start',
615
+ 'g-lg'
616
+ );
617
+ });
618
+ });
619
+
620
+ describe('Spread props', () => {
621
+ test('renders modal with additional props', () => {
622
+ render(
623
+ <Modal
624
+ isOpen
625
+ onDismiss={() => {}}
626
+ ariaLabel="test"
627
+ shouldCloseOnOverlayClick={false}
628
+ >
629
+ content
630
+ </Modal>
631
+ );
632
+
633
+ // Verify the modal renders with the content
634
+ expect(screen.getByText('content')).toBeInTheDocument();
635
+ });
87
636
  });
88
637
  });