@wordpress/components 24.0.0 → 25.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (166) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/CONTRIBUTING.md +10 -0
  3. package/build/color-picker/styles.js +8 -8
  4. package/build/color-picker/styles.js.map +1 -1
  5. package/build/date-time/date-time/index.js +3 -84
  6. package/build/date-time/date-time/index.js.map +1 -1
  7. package/build/date-time/date-time/styles.js +4 -19
  8. package/build/date-time/date-time/styles.js.map +1 -1
  9. package/build/dropdown-menu/index.js +87 -11
  10. package/build/dropdown-menu/index.js.map +1 -1
  11. package/build/dropdown-menu/types.js +6 -0
  12. package/build/dropdown-menu/types.js.map +1 -0
  13. package/build/dropdown-menu-v2/index.js +195 -0
  14. package/build/dropdown-menu-v2/index.js.map +1 -0
  15. package/build/dropdown-menu-v2/styles.js +176 -0
  16. package/build/dropdown-menu-v2/styles.js.map +1 -0
  17. package/build/dropdown-menu-v2/types.js +6 -0
  18. package/build/dropdown-menu-v2/types.js.map +1 -0
  19. package/build/index.native.js +0 -9
  20. package/build/index.native.js.map +1 -1
  21. package/build/input-control/styles/input-control-styles.js +30 -23
  22. package/build/input-control/styles/input-control-styles.js.map +1 -1
  23. package/build/mobile/bottom-sheet/cell.native.js +16 -8
  24. package/build/mobile/bottom-sheet/cell.native.js.map +1 -1
  25. package/build/mobile/bottom-sheet/range-cell.native.js +3 -2
  26. package/build/mobile/bottom-sheet/range-cell.native.js.map +1 -1
  27. package/build/mobile/bottom-sheet/stepper-cell/index.native.js +4 -2
  28. package/build/mobile/bottom-sheet/stepper-cell/index.native.js.map +1 -1
  29. package/build/mobile/bottom-sheet/switch-cell.native.js +8 -2
  30. package/build/mobile/bottom-sheet/switch-cell.native.js.map +1 -1
  31. package/build/mobile/bottom-sheet-select-control/index.native.js +4 -2
  32. package/build/mobile/bottom-sheet-select-control/index.native.js.map +1 -1
  33. package/build/mobile/bottom-sheet-text-control/index.native.js +4 -2
  34. package/build/mobile/bottom-sheet-text-control/index.native.js.map +1 -1
  35. package/build/modal/index.js +1 -2
  36. package/build/modal/index.js.map +1 -1
  37. package/build/private-apis.js +13 -1
  38. package/build/private-apis.js.map +1 -1
  39. package/build/range-control/index.native.js +5 -2
  40. package/build/range-control/index.native.js.map +1 -1
  41. package/build/snackbar/list.js +0 -2
  42. package/build/snackbar/list.js.map +1 -1
  43. package/build/toggle-group-control/toggle-group-control/styles.js +7 -7
  44. package/build/toggle-group-control/toggle-group-control/styles.js.map +1 -1
  45. package/build-module/color-picker/styles.js +8 -8
  46. package/build-module/color-picker/styles.js.map +1 -1
  47. package/build-module/date-time/date-time/index.js +6 -81
  48. package/build-module/date-time/date-time/index.js.map +1 -1
  49. package/build-module/date-time/date-time/styles.js +3 -17
  50. package/build-module/date-time/date-time/styles.js.map +1 -1
  51. package/build-module/dropdown-menu/index.js +87 -10
  52. package/build-module/dropdown-menu/index.js.map +1 -1
  53. package/build-module/dropdown-menu/types.js +2 -0
  54. package/build-module/dropdown-menu/types.js.map +1 -0
  55. package/build-module/dropdown-menu-v2/index.js +149 -0
  56. package/build-module/dropdown-menu-v2/index.js.map +1 -0
  57. package/build-module/dropdown-menu-v2/styles.js +153 -0
  58. package/build-module/dropdown-menu-v2/styles.js.map +1 -0
  59. package/build-module/dropdown-menu-v2/types.js +2 -0
  60. package/build-module/dropdown-menu-v2/types.js.map +1 -0
  61. package/build-module/index.native.js +0 -1
  62. package/build-module/index.native.js.map +1 -1
  63. package/build-module/input-control/styles/input-control-styles.js +30 -23
  64. package/build-module/input-control/styles/input-control-styles.js.map +1 -1
  65. package/build-module/mobile/bottom-sheet/cell.native.js +16 -8
  66. package/build-module/mobile/bottom-sheet/cell.native.js.map +1 -1
  67. package/build-module/mobile/bottom-sheet/range-cell.native.js +3 -2
  68. package/build-module/mobile/bottom-sheet/range-cell.native.js.map +1 -1
  69. package/build-module/mobile/bottom-sheet/stepper-cell/index.native.js +4 -2
  70. package/build-module/mobile/bottom-sheet/stepper-cell/index.native.js.map +1 -1
  71. package/build-module/mobile/bottom-sheet/switch-cell.native.js +7 -2
  72. package/build-module/mobile/bottom-sheet/switch-cell.native.js.map +1 -1
  73. package/build-module/mobile/bottom-sheet-select-control/index.native.js +4 -2
  74. package/build-module/mobile/bottom-sheet-select-control/index.native.js.map +1 -1
  75. package/build-module/mobile/bottom-sheet-text-control/index.native.js +4 -2
  76. package/build-module/mobile/bottom-sheet-text-control/index.native.js.map +1 -1
  77. package/build-module/modal/index.js +1 -2
  78. package/build-module/modal/index.js.map +1 -1
  79. package/build-module/private-apis.js +12 -1
  80. package/build-module/private-apis.js.map +1 -1
  81. package/build-module/range-control/index.native.js +5 -2
  82. package/build-module/range-control/index.native.js.map +1 -1
  83. package/build-module/snackbar/list.js +0 -2
  84. package/build-module/snackbar/list.js.map +1 -1
  85. package/build-module/toggle-group-control/toggle-group-control/styles.js +7 -7
  86. package/build-module/toggle-group-control/toggle-group-control/styles.js.map +1 -1
  87. package/build-style/style-rtl.css +11 -14
  88. package/build-style/style.css +11 -14
  89. package/build-types/color-picker/styles.d.ts.map +1 -1
  90. package/build-types/date-time/date-time/index.d.ts +3 -4
  91. package/build-types/date-time/date-time/index.d.ts.map +1 -1
  92. package/build-types/date-time/date-time/styles.d.ts +0 -4
  93. package/build-types/date-time/date-time/styles.d.ts.map +1 -1
  94. package/build-types/date-time/stories/date-time.d.ts.map +1 -1
  95. package/build-types/date-time/types.d.ts +0 -14
  96. package/build-types/date-time/types.d.ts.map +1 -1
  97. package/build-types/dropdown-menu/index.d.ts +83 -1
  98. package/build-types/dropdown-menu/index.d.ts.map +1 -1
  99. package/build-types/dropdown-menu/stories/index.d.ts +13 -0
  100. package/build-types/dropdown-menu/stories/index.d.ts.map +1 -0
  101. package/build-types/dropdown-menu/test/index.d.ts +2 -0
  102. package/build-types/dropdown-menu/test/index.d.ts.map +1 -0
  103. package/build-types/dropdown-menu/types.d.ts +134 -0
  104. package/build-types/dropdown-menu/types.d.ts.map +1 -0
  105. package/build-types/dropdown-menu-v2/index.d.ts +17 -0
  106. package/build-types/dropdown-menu-v2/index.d.ts.map +1 -0
  107. package/build-types/dropdown-menu-v2/stories/index.d.ts +13 -0
  108. package/build-types/dropdown-menu-v2/stories/index.d.ts.map +1 -0
  109. package/build-types/dropdown-menu-v2/styles.d.ts +41 -0
  110. package/build-types/dropdown-menu-v2/styles.d.ts.map +1 -0
  111. package/build-types/dropdown-menu-v2/test/index.d.ts +2 -0
  112. package/build-types/dropdown-menu-v2/test/index.d.ts.map +1 -0
  113. package/build-types/dropdown-menu-v2/types.d.ts +242 -0
  114. package/build-types/dropdown-menu-v2/types.d.ts.map +1 -0
  115. package/build-types/input-control/styles/input-control-styles.d.ts.map +1 -1
  116. package/build-types/modal/index.d.ts.map +1 -1
  117. package/build-types/private-apis.d.ts.map +1 -1
  118. package/build-types/snackbar/list.d.ts.map +1 -1
  119. package/build-types/toggle-group-control/toggle-group-control/styles.d.ts.map +1 -1
  120. package/build-types/toolbar/stories/index.d.ts.map +1 -1
  121. package/build-types/ui/context/get-styled-class-name-from-key.d.ts +1 -10
  122. package/build-types/ui/context/get-styled-class-name-from-key.d.ts.map +1 -1
  123. package/package.json +21 -20
  124. package/src/button/style.scss +5 -12
  125. package/src/color-picker/styles.ts +7 -2
  126. package/src/date-time/README.md +0 -16
  127. package/src/date-time/date-time/index.tsx +17 -155
  128. package/src/date-time/date-time/styles.ts +0 -4
  129. package/src/date-time/stories/date-time.tsx +0 -4
  130. package/src/date-time/types.ts +0 -16
  131. package/src/dropdown-menu/README.md +12 -22
  132. package/src/dropdown-menu/{index.js → index.tsx} +111 -25
  133. package/src/dropdown-menu/stories/{index.js → index.tsx} +14 -22
  134. package/src/dropdown-menu/test/{index.js → index.tsx} +6 -5
  135. package/src/dropdown-menu/types.ts +143 -0
  136. package/src/dropdown-menu-v2/README.md +392 -0
  137. package/src/dropdown-menu-v2/index.tsx +241 -0
  138. package/src/dropdown-menu-v2/stories/index.tsx +193 -0
  139. package/src/dropdown-menu-v2/styles.ts +263 -0
  140. package/src/dropdown-menu-v2/test/index.tsx +816 -0
  141. package/src/dropdown-menu-v2/types.ts +250 -0
  142. package/src/index.native.js +0 -1
  143. package/src/input-control/styles/input-control-styles.tsx +7 -0
  144. package/src/mobile/bottom-sheet/cell.native.js +26 -5
  145. package/src/mobile/bottom-sheet/range-cell.native.js +2 -1
  146. package/src/mobile/bottom-sheet/stepper-cell/index.native.js +2 -0
  147. package/src/mobile/bottom-sheet/styles.native.scss +13 -1
  148. package/src/mobile/bottom-sheet/switch-cell.native.js +10 -2
  149. package/src/mobile/bottom-sheet-select-control/index.native.js +2 -0
  150. package/src/mobile/bottom-sheet-text-control/index.native.js +2 -0
  151. package/src/modal/index.tsx +1 -6
  152. package/src/private-apis.ts +22 -0
  153. package/src/range-control/index.native.js +3 -0
  154. package/src/search-control/style.scss +2 -0
  155. package/src/snackbar/list.tsx +0 -1
  156. package/src/toggle-group-control/test/__snapshots__/index.tsx.snap +6 -2
  157. package/src/toggle-group-control/toggle-group-control/styles.ts +6 -1
  158. package/src/toolbar/stories/index.tsx +25 -28
  159. package/src/tooltip/style.scss +2 -2
  160. package/tsconfig.tsbuildinfo +1 -1
  161. package/build/mobile/readable-content-view/index.native.js +0 -97
  162. package/build/mobile/readable-content-view/index.native.js.map +0 -1
  163. package/build-module/mobile/readable-content-view/index.native.js +0 -81
  164. package/build-module/mobile/readable-content-view/index.native.js.map +0 -1
  165. package/src/mobile/readable-content-view/index.native.js +0 -85
  166. package/src/mobile/readable-content-view/style.native.scss +0 -30
@@ -0,0 +1,816 @@
1
+ /**
2
+ * External dependencies
3
+ */
4
+ import { render, screen, waitFor } from '@testing-library/react';
5
+ import {
6
+ default as userEvent,
7
+ PointerEventsCheckLevel,
8
+ } from '@testing-library/user-event';
9
+
10
+ /**
11
+ * WordPress dependencies
12
+ */
13
+ import { useState } from '@wordpress/element';
14
+
15
+ /**
16
+ * Internal dependencies
17
+ */
18
+ import {
19
+ DropdownMenu,
20
+ DropdownMenuCheckboxItem,
21
+ DropdownMenuItem,
22
+ DropdownMenuLabel,
23
+ DropdownMenuRadioGroup,
24
+ DropdownMenuRadioItem,
25
+ DropdownMenuSeparator,
26
+ DropdownSubMenu,
27
+ DropdownSubMenuTrigger,
28
+ } from '..';
29
+
30
+ const delay = ( delayInMs: number ) => {
31
+ return new Promise( ( resolve ) => setTimeout( resolve, delayInMs ) );
32
+ };
33
+
34
+ describe( 'DropdownMenu', () => {
35
+ // See https://www.w3.org/WAI/ARIA/apg/patterns/menu-button/
36
+ it( 'should follow the WAI-ARIA spec', async () => {
37
+ // Radio and Checkbox items'
38
+ const user = userEvent.setup();
39
+
40
+ render(
41
+ <DropdownMenu trigger={ <button>Open dropdown</button> }>
42
+ <DropdownMenuItem>Dropdown menu item</DropdownMenuItem>
43
+ <DropdownMenuSeparator />
44
+ <DropdownSubMenu
45
+ trigger={
46
+ <DropdownSubMenuTrigger>
47
+ Dropdown submenu
48
+ </DropdownSubMenuTrigger>
49
+ }
50
+ >
51
+ <DropdownMenuItem>Dropdown submenu item 1</DropdownMenuItem>
52
+ <DropdownMenuItem>Dropdown submenu item 2</DropdownMenuItem>
53
+ </DropdownSubMenu>
54
+ </DropdownMenu>
55
+ );
56
+
57
+ const toggleButton = screen.getByRole( 'button', {
58
+ name: 'Open dropdown',
59
+ } );
60
+
61
+ expect( toggleButton ).toHaveAttribute( 'aria-haspopup', 'menu' );
62
+ expect( toggleButton ).toHaveAttribute( 'aria-expanded', 'false' );
63
+
64
+ await user.click( toggleButton );
65
+
66
+ expect( toggleButton ).toHaveAttribute( 'aria-expanded', 'true' );
67
+
68
+ expect( screen.getByRole( 'menu' ) ).toHaveFocus();
69
+ expect( screen.getByRole( 'separator' ) ).toHaveAttribute(
70
+ 'aria-orientation',
71
+ 'horizontal'
72
+ );
73
+ expect( screen.getAllByRole( 'menuitem' ) ).toHaveLength( 2 );
74
+
75
+ const submenuTrigger = screen.getByRole( 'menuitem', {
76
+ name: 'Dropdown submenu',
77
+ } );
78
+ expect( submenuTrigger ).toHaveAttribute( 'aria-haspopup', 'menu' );
79
+ expect( submenuTrigger ).toHaveAttribute( 'aria-expanded', 'false' );
80
+
81
+ await user.hover( submenuTrigger );
82
+
83
+ // Wait for the open animation after hovering
84
+ await waitFor( () =>
85
+ expect( screen.getAllByRole( 'menu' ) ).toHaveLength( 2 )
86
+ );
87
+
88
+ expect( submenuTrigger ).toHaveAttribute( 'aria-expanded', 'true' );
89
+ expect( submenuTrigger ).toHaveAttribute(
90
+ 'aria-controls',
91
+ screen.getAllByRole( 'menu' )[ 1 ].id
92
+ );
93
+ } );
94
+
95
+ describe( 'pointer and keyboard interactions', () => {
96
+ it( 'should open when clicking the trigger', async () => {
97
+ const user = userEvent.setup();
98
+
99
+ render(
100
+ <DropdownMenu trigger={ <button>Open dropdown</button> }>
101
+ <DropdownMenuItem>Dropdown menu item</DropdownMenuItem>
102
+ </DropdownMenu>
103
+ );
104
+
105
+ const toggleButton = screen.getByRole( 'button', {
106
+ name: 'Open dropdown',
107
+ } );
108
+
109
+ // DropdownMenu closed, the content is not displayed
110
+ expect( screen.queryByRole( 'menu' ) ).not.toBeInTheDocument();
111
+ expect( screen.queryByRole( 'menuitem' ) ).not.toBeInTheDocument();
112
+
113
+ // Click to open the menu
114
+ await user.click( toggleButton );
115
+
116
+ // DropdownMenu open, the content is displayed
117
+ expect( screen.getByRole( 'menu' ) ).toBeInTheDocument();
118
+ expect( screen.getByRole( 'menuitem' ) ).toBeInTheDocument();
119
+ } );
120
+
121
+ it( 'should open when pressing the arrow down key on the trigger', async () => {
122
+ const user = userEvent.setup();
123
+
124
+ render(
125
+ <DropdownMenu trigger={ <button>Open dropdown</button> }>
126
+ <DropdownMenuItem>Dropdown menu item</DropdownMenuItem>
127
+ </DropdownMenu>
128
+ );
129
+
130
+ const toggleButton = screen.getByRole( 'button', {
131
+ name: 'Open dropdown',
132
+ } );
133
+
134
+ // Move focus on the toggle
135
+ await user.keyboard( '{Tab}' );
136
+
137
+ expect( toggleButton ).toHaveFocus();
138
+
139
+ // DropdownMenu closed, the content is not displayed
140
+ expect( screen.queryByRole( 'menuitem' ) ).not.toBeInTheDocument();
141
+
142
+ await user.keyboard( '{ArrowDown}' );
143
+
144
+ // DropdownMenu open, the content is displayed
145
+ expect( screen.getByRole( 'menuitem' ) ).toBeInTheDocument();
146
+ } );
147
+
148
+ it( 'should close when pressing the escape key', async () => {
149
+ const user = userEvent.setup();
150
+
151
+ render(
152
+ <DropdownMenu
153
+ defaultOpen
154
+ trigger={ <button>Open dropdown</button> }
155
+ >
156
+ <DropdownMenuItem>Dropdown menu item</DropdownMenuItem>
157
+ </DropdownMenu>
158
+ );
159
+
160
+ // The menu is focused automatically when `defaultOpen` is set.
161
+ expect( screen.getByRole( 'menu' ) ).toHaveFocus();
162
+
163
+ // Pressing esc will close the menu and move focus to the toggle
164
+ await user.keyboard( '{Escape}' );
165
+
166
+ expect( screen.queryByRole( 'menu' ) ).not.toBeInTheDocument();
167
+ expect(
168
+ screen.getByRole( 'button', { name: 'Open dropdown' } )
169
+ ).toHaveFocus();
170
+ } );
171
+
172
+ it( 'should close when clicking outside of the content', async () => {
173
+ const user = userEvent.setup( {
174
+ // Disabling this check otherwise testing-library would complain
175
+ // when clicking on document.body to close the dropdown menu.
176
+ pointerEventsCheck: PointerEventsCheckLevel.Never,
177
+ } );
178
+
179
+ render(
180
+ <DropdownMenu
181
+ defaultOpen
182
+ trigger={ <button>Open dropdown</button> }
183
+ >
184
+ <DropdownMenuItem>Dropdown menu item</DropdownMenuItem>
185
+ </DropdownMenu>
186
+ );
187
+
188
+ expect( screen.getByRole( 'menu' ) ).toBeInTheDocument();
189
+
190
+ // Click on the body (ie. outside of the dropdown menu)
191
+ await user.click( document.body );
192
+
193
+ expect( screen.queryByRole( 'menu' ) ).not.toBeInTheDocument();
194
+ } );
195
+
196
+ it( 'should close when clicking on a menu item', async () => {
197
+ const user = userEvent.setup();
198
+
199
+ render(
200
+ <DropdownMenu
201
+ defaultOpen
202
+ trigger={ <button>Open dropdown</button> }
203
+ >
204
+ <DropdownMenuItem>Dropdown menu item</DropdownMenuItem>
205
+ </DropdownMenu>
206
+ );
207
+
208
+ expect( screen.getByRole( 'menu' ) ).toBeInTheDocument();
209
+
210
+ // Clicking a menu item will close the menu
211
+ await user.click( screen.getByRole( 'menuitem' ) );
212
+
213
+ expect( screen.queryByRole( 'menu' ) ).not.toBeInTheDocument();
214
+ } );
215
+
216
+ it( 'should not close when clicking on a disabled menu item', async () => {
217
+ const user = userEvent.setup( {
218
+ // Disabling this check otherwise testing-library would complain
219
+ // when clicking on a disabled element with pointer-events: none
220
+ pointerEventsCheck: PointerEventsCheckLevel.Never,
221
+ } );
222
+
223
+ render(
224
+ <DropdownMenu
225
+ defaultOpen
226
+ trigger={ <button>Open dropdown</button> }
227
+ >
228
+ <DropdownMenuItem disabled>
229
+ Dropdown menu item
230
+ </DropdownMenuItem>
231
+ </DropdownMenu>
232
+ );
233
+
234
+ expect( screen.getByRole( 'menu' ) ).toBeInTheDocument();
235
+
236
+ // Clicking a disabled menu item won't close the menu
237
+ await user.click( screen.getByRole( 'menuitem' ) );
238
+
239
+ expect( screen.getByRole( 'menu' ) ).toBeInTheDocument();
240
+ } );
241
+
242
+ it( 'should reveal submenu content when hovering over the submenu trigger', async () => {
243
+ const user = userEvent.setup();
244
+
245
+ render(
246
+ <DropdownMenu
247
+ defaultOpen
248
+ trigger={ <button>Open dropdown</button> }
249
+ >
250
+ <DropdownMenuItem>Dropdown menu item 1</DropdownMenuItem>
251
+ <DropdownMenuItem>Dropdown menu item 2</DropdownMenuItem>
252
+ <DropdownSubMenu
253
+ trigger={
254
+ <DropdownSubMenuTrigger>
255
+ Dropdown submenu
256
+ </DropdownSubMenuTrigger>
257
+ }
258
+ >
259
+ <DropdownMenuItem>
260
+ Dropdown submenu item 1
261
+ </DropdownMenuItem>
262
+ <DropdownMenuItem>
263
+ Dropdown submenu item 2
264
+ </DropdownMenuItem>
265
+ </DropdownSubMenu>
266
+ <DropdownMenuItem>Dropdown menu item 3</DropdownMenuItem>
267
+ </DropdownMenu>
268
+ );
269
+
270
+ // Before hover, submenu items are not rendered
271
+ expect(
272
+ screen.queryByRole( 'menuitem', {
273
+ name: 'Dropdown submenu item 1',
274
+ } )
275
+ ).not.toBeInTheDocument();
276
+
277
+ await user.hover(
278
+ screen.getByRole( 'menuitem', { name: 'Dropdown submenu' } )
279
+ );
280
+
281
+ // After hover, submenu items are rendered
282
+ // Reason for `findByRole`: due to the animation, we've got to wait
283
+ // a short amount of time for the submenu to appear
284
+ await screen.findByRole( 'menuitem', {
285
+ name: 'Dropdown submenu item 1',
286
+ } );
287
+ } );
288
+
289
+ it( 'should navigate menu items and subitems using the arrow, spacebar and enter keys', async () => {
290
+ const user = userEvent.setup();
291
+
292
+ render(
293
+ <DropdownMenu
294
+ defaultOpen
295
+ trigger={ <button>Open dropdown</button> }
296
+ >
297
+ <DropdownMenuItem>Dropdown menu item 1</DropdownMenuItem>
298
+ <DropdownMenuItem>Dropdown menu item 2</DropdownMenuItem>
299
+ <DropdownSubMenu
300
+ trigger={
301
+ <DropdownSubMenuTrigger>
302
+ Dropdown submenu
303
+ </DropdownSubMenuTrigger>
304
+ }
305
+ >
306
+ <DropdownMenuItem>
307
+ Dropdown submenu item 1
308
+ </DropdownMenuItem>
309
+ <DropdownMenuItem>
310
+ Dropdown submenu item 2
311
+ </DropdownMenuItem>
312
+ </DropdownSubMenu>
313
+ <DropdownMenuItem>Dropdown menu item 3</DropdownMenuItem>
314
+ </DropdownMenu>
315
+ );
316
+
317
+ // The menu is focused automatically when `defaultOpen` is set.
318
+ expect( screen.getByRole( 'menu' ) ).toHaveFocus();
319
+
320
+ // Arrow up/down selects menu items
321
+ // The selection wraps around from last to first and viceversa
322
+ await user.keyboard( '{ArrowDown}' );
323
+ expect(
324
+ screen.getByRole( 'menuitem', { name: 'Dropdown menu item 1' } )
325
+ ).toHaveFocus();
326
+
327
+ await user.keyboard( '{ArrowDown}' );
328
+ expect(
329
+ screen.getByRole( 'menuitem', { name: 'Dropdown menu item 2' } )
330
+ ).toHaveFocus();
331
+
332
+ await user.keyboard( '{ArrowDown}' );
333
+ expect(
334
+ screen.getByRole( 'menuitem', { name: 'Dropdown submenu' } )
335
+ ).toHaveFocus();
336
+
337
+ await user.keyboard( '{ArrowDown}' );
338
+ expect(
339
+ screen.getByRole( 'menuitem', { name: 'Dropdown menu item 3' } )
340
+ ).toHaveFocus();
341
+
342
+ await user.keyboard( '{ArrowDown}' );
343
+ expect(
344
+ screen.getByRole( 'menuitem', { name: 'Dropdown menu item 1' } )
345
+ ).toHaveFocus();
346
+
347
+ await user.keyboard( '{ArrowUp}' );
348
+ expect(
349
+ screen.getByRole( 'menuitem', { name: 'Dropdown menu item 3' } )
350
+ ).toHaveFocus();
351
+
352
+ await user.keyboard( '{ArrowUp}' );
353
+ expect(
354
+ screen.getByRole( 'menuitem', { name: 'Dropdown submenu' } )
355
+ ).toHaveFocus();
356
+
357
+ // Arrow right/left can be used to enter/leave submenus
358
+ await user.keyboard( '{ArrowRight}' );
359
+ expect(
360
+ screen.getByRole( 'menuitem', {
361
+ name: 'Dropdown submenu item 1',
362
+ } )
363
+ ).toHaveFocus();
364
+
365
+ await user.keyboard( '{ArrowDown}' );
366
+ expect(
367
+ screen.getByRole( 'menuitem', {
368
+ name: 'Dropdown submenu item 2',
369
+ } )
370
+ ).toHaveFocus();
371
+
372
+ await user.keyboard( '{ArrowLeft}' );
373
+ expect(
374
+ screen.getByRole( 'menuitem', {
375
+ name: 'Dropdown submenu',
376
+ } )
377
+ ).toHaveFocus();
378
+
379
+ // Spacebar or enter key can also be used to enter a submenu
380
+ await user.keyboard( '{Enter}' );
381
+ expect(
382
+ screen.getByRole( 'menuitem', {
383
+ name: 'Dropdown submenu item 1',
384
+ } )
385
+ ).toHaveFocus();
386
+
387
+ await user.keyboard( '{ArrowLeft}' );
388
+ expect(
389
+ screen.getByRole( 'menuitem', {
390
+ name: 'Dropdown submenu',
391
+ } )
392
+ ).toHaveFocus();
393
+
394
+ await user.keyboard( '{Spacebar}' );
395
+ expect(
396
+ screen.getByRole( 'menuitem', {
397
+ name: 'Dropdown submenu item 1',
398
+ } )
399
+ ).toHaveFocus();
400
+
401
+ await user.keyboard( '{ArrowLeft}' );
402
+ expect(
403
+ screen.getByRole( 'menuitem', {
404
+ name: 'Dropdown submenu',
405
+ } )
406
+ ).toHaveFocus();
407
+ } );
408
+
409
+ it( 'should check menu radio items', async () => {
410
+ const user = userEvent.setup();
411
+
412
+ const onRadioValueChangeSpy = jest.fn();
413
+
414
+ const ControlledRadioGroup = () => {
415
+ const [ radioValue, setRadioValue ] = useState< string >();
416
+ return (
417
+ <DropdownMenu trigger={ <button>Open dropdown</button> }>
418
+ <DropdownMenuRadioGroup
419
+ value={ radioValue }
420
+ onValueChange={ ( value ) => {
421
+ onRadioValueChangeSpy( value );
422
+ setRadioValue( value );
423
+ } }
424
+ >
425
+ <DropdownMenuLabel>
426
+ Radio group label
427
+ </DropdownMenuLabel>
428
+ <DropdownMenuRadioItem value="radio-one">
429
+ Radio item one
430
+ </DropdownMenuRadioItem>
431
+ <DropdownMenuRadioItem value="radio-two">
432
+ Radio item two
433
+ </DropdownMenuRadioItem>
434
+ </DropdownMenuRadioGroup>
435
+ </DropdownMenu>
436
+ );
437
+ };
438
+
439
+ render( <ControlledRadioGroup /> );
440
+
441
+ // Open dropdown
442
+ await user.click(
443
+ screen.getByRole( 'button', { name: 'Open dropdown' } )
444
+ );
445
+
446
+ // No radios should be checked at this point
447
+ expect( screen.getAllByRole( 'menuitemradio' ) ).toHaveLength( 2 );
448
+ expect(
449
+ screen.getByRole( 'menuitemradio', { name: 'Radio item one' } )
450
+ ).not.toBeChecked();
451
+ expect(
452
+ screen.getByRole( 'menuitemradio', { name: 'Radio item two' } )
453
+ ).not.toBeChecked();
454
+
455
+ // Click first radio item, make sure that the callback fires
456
+ await user.click(
457
+ screen.getByRole( 'menuitemradio', { name: 'Radio item one' } )
458
+ );
459
+ expect( onRadioValueChangeSpy ).toHaveBeenCalledTimes( 1 );
460
+ expect( onRadioValueChangeSpy ).toHaveBeenLastCalledWith(
461
+ 'radio-one'
462
+ );
463
+
464
+ // Open dropdown
465
+ await user.click(
466
+ screen.getByRole( 'button', { name: 'Open dropdown' } )
467
+ );
468
+
469
+ // Make sure that first radio is checked
470
+ expect(
471
+ screen.getByRole( 'menuitemradio', { name: 'Radio item one' } )
472
+ ).toBeChecked();
473
+ expect(
474
+ screen.getByRole( 'menuitemradio', { name: 'Radio item two' } )
475
+ ).not.toBeChecked();
476
+
477
+ // Click second radio item, make sure that the callback fires
478
+ await user.click(
479
+ screen.getByRole( 'menuitemradio', { name: 'Radio item two' } )
480
+ );
481
+ expect( onRadioValueChangeSpy ).toHaveBeenCalledTimes( 2 );
482
+ expect( onRadioValueChangeSpy ).toHaveBeenLastCalledWith(
483
+ 'radio-two'
484
+ );
485
+
486
+ // Open dropdown
487
+ await user.click(
488
+ screen.getByRole( 'button', { name: 'Open dropdown' } )
489
+ );
490
+
491
+ // Make sure that second radio is selected
492
+ expect(
493
+ screen.getByRole( 'menuitemradio', { name: 'Radio item one' } )
494
+ ).not.toBeChecked();
495
+ expect(
496
+ screen.getByRole( 'menuitemradio', { name: 'Radio item two' } )
497
+ ).toBeChecked();
498
+ } );
499
+
500
+ it( 'should check menu checkbox items', async () => {
501
+ const user = userEvent.setup();
502
+
503
+ const onCheckboxValueChangeSpy = jest.fn();
504
+
505
+ const ControlledRadioGroup = () => {
506
+ const [ itemOneChecked, setItemOneChecked ] =
507
+ useState< boolean >();
508
+ const [ itemTwoChecked, setItemTwoChecked ] =
509
+ useState< boolean >();
510
+ return (
511
+ <DropdownMenu trigger={ <button>Open dropdown</button> }>
512
+ <DropdownMenuLabel>
513
+ Checkbox group label
514
+ </DropdownMenuLabel>
515
+ <DropdownMenuCheckboxItem
516
+ checked={ itemOneChecked }
517
+ onCheckedChange={ ( checked ) => {
518
+ setItemOneChecked( checked );
519
+ onCheckboxValueChangeSpy( 'item-one', checked );
520
+ } }
521
+ >
522
+ Checkbox item one
523
+ </DropdownMenuCheckboxItem>
524
+
525
+ <DropdownMenuCheckboxItem
526
+ checked={ itemTwoChecked }
527
+ onCheckedChange={ ( checked ) => {
528
+ setItemTwoChecked( checked );
529
+ onCheckboxValueChangeSpy( 'item-two', checked );
530
+ } }
531
+ >
532
+ Checkbox item two
533
+ </DropdownMenuCheckboxItem>
534
+ </DropdownMenu>
535
+ );
536
+ };
537
+
538
+ render( <ControlledRadioGroup /> );
539
+
540
+ // Open dropdown
541
+ await user.click(
542
+ screen.getByRole( 'button', { name: 'Open dropdown' } )
543
+ );
544
+
545
+ // No checkboxes should be checked at this point
546
+ expect( screen.getAllByRole( 'menuitemcheckbox' ) ).toHaveLength(
547
+ 2
548
+ );
549
+ expect(
550
+ screen.getByRole( 'menuitemcheckbox', {
551
+ name: 'Checkbox item one',
552
+ } )
553
+ ).not.toBeChecked();
554
+ expect(
555
+ screen.getByRole( 'menuitemcheckbox', {
556
+ name: 'Checkbox item two',
557
+ } )
558
+ ).not.toBeChecked();
559
+
560
+ // Click first checkbox item, make sure that the callback fires
561
+ await user.click(
562
+ screen.getByRole( 'menuitemcheckbox', {
563
+ name: 'Checkbox item one',
564
+ } )
565
+ );
566
+ expect( onCheckboxValueChangeSpy ).toHaveBeenCalledTimes( 1 );
567
+ expect( onCheckboxValueChangeSpy ).toHaveBeenLastCalledWith(
568
+ 'item-one',
569
+ true
570
+ );
571
+
572
+ // Open dropdown
573
+ await user.click(
574
+ screen.getByRole( 'button', { name: 'Open dropdown' } )
575
+ );
576
+
577
+ // Make sure that first checkbox is checked
578
+ expect(
579
+ screen.getByRole( 'menuitemcheckbox', {
580
+ name: 'Checkbox item one',
581
+ } )
582
+ ).toBeChecked();
583
+
584
+ // Click second checkbox item, make sure that the callback fires
585
+ await user.click(
586
+ screen.getByRole( 'menuitemcheckbox', {
587
+ name: 'Checkbox item two',
588
+ } )
589
+ );
590
+ expect( onCheckboxValueChangeSpy ).toHaveBeenCalledTimes( 2 );
591
+ expect( onCheckboxValueChangeSpy ).toHaveBeenLastCalledWith(
592
+ 'item-two',
593
+ true
594
+ );
595
+
596
+ // Open dropdown
597
+ await user.click(
598
+ screen.getByRole( 'button', { name: 'Open dropdown' } )
599
+ );
600
+
601
+ // Make sure that second checkbox is selected
602
+ expect(
603
+ screen.getByRole( 'menuitemcheckbox', {
604
+ name: 'Checkbox item two',
605
+ } )
606
+ ).toBeChecked();
607
+
608
+ // Click second checkbox item, make sure that the callback fires
609
+ await user.click(
610
+ screen.getByRole( 'menuitemcheckbox', {
611
+ name: 'Checkbox item two',
612
+ } )
613
+ );
614
+ expect( onCheckboxValueChangeSpy ).toHaveBeenCalledTimes( 3 );
615
+ expect( onCheckboxValueChangeSpy ).toHaveBeenLastCalledWith(
616
+ 'item-two',
617
+ false
618
+ );
619
+
620
+ // Open dropdown
621
+ await user.click(
622
+ screen.getByRole( 'button', { name: 'Open dropdown' } )
623
+ );
624
+
625
+ // Make sure that second checkbox is unselected
626
+ expect(
627
+ screen.getByRole( 'menuitemcheckbox', {
628
+ name: 'Checkbox item two',
629
+ } )
630
+ ).not.toBeChecked();
631
+ } );
632
+ } );
633
+
634
+ describe( 'items prefix and suffix', () => {
635
+ it( 'should display a prefix on regular items', async () => {
636
+ const user = userEvent.setup();
637
+
638
+ render(
639
+ <DropdownMenu trigger={ <button>Open dropdown</button> }>
640
+ <DropdownMenuItem prefix={ <>Item prefix</> }>
641
+ Dropdown menu item
642
+ </DropdownMenuItem>
643
+ </DropdownMenu>
644
+ );
645
+
646
+ // Click to open the menu
647
+ await user.click(
648
+ screen.getByRole( 'button', {
649
+ name: 'Open dropdown',
650
+ } )
651
+ );
652
+
653
+ // The contents of the prefix are rendered before the item's children
654
+ expect(
655
+ screen.getByRole( 'menuitem', {
656
+ name: 'Item prefix Dropdown menu item',
657
+ } )
658
+ ).toBeInTheDocument();
659
+ } );
660
+
661
+ it( 'should display a suffix on regular items', async () => {
662
+ const user = userEvent.setup();
663
+
664
+ render(
665
+ <DropdownMenu trigger={ <button>Open dropdown</button> }>
666
+ <DropdownMenuItem suffix={ <>Item suffix</> }>
667
+ Dropdown menu item
668
+ </DropdownMenuItem>
669
+ </DropdownMenu>
670
+ );
671
+
672
+ // Click to open the menu
673
+ await user.click(
674
+ screen.getByRole( 'button', {
675
+ name: 'Open dropdown',
676
+ } )
677
+ );
678
+
679
+ // The contents of the suffix are rendered after the item's children
680
+ expect(
681
+ screen.getByRole( 'menuitem', {
682
+ name: 'Dropdown menu item Item suffix',
683
+ } )
684
+ ).toBeInTheDocument();
685
+ } );
686
+
687
+ it( 'should display a suffix on radio items', async () => {
688
+ const user = userEvent.setup();
689
+
690
+ render(
691
+ <DropdownMenu trigger={ <button>Open dropdown</button> }>
692
+ <DropdownMenuRadioGroup>
693
+ <DropdownMenuRadioItem
694
+ value="radio-one"
695
+ suffix="Radio suffix"
696
+ >
697
+ Radio item one
698
+ </DropdownMenuRadioItem>
699
+ </DropdownMenuRadioGroup>
700
+ </DropdownMenu>
701
+ );
702
+
703
+ // Click to open the menu
704
+ await user.click(
705
+ screen.getByRole( 'button', {
706
+ name: 'Open dropdown',
707
+ } )
708
+ );
709
+
710
+ // The contents of the suffix are rendered after the item's children
711
+ expect(
712
+ screen.getByRole( 'menuitemradio', {
713
+ name: 'Radio item one Radio suffix',
714
+ } )
715
+ ).toBeInTheDocument();
716
+ } );
717
+
718
+ it( 'should display a suffix on checkbox items', async () => {
719
+ const user = userEvent.setup();
720
+
721
+ render(
722
+ <DropdownMenu trigger={ <button>Open dropdown</button> }>
723
+ <DropdownMenuCheckboxItem suffix={ 'Checkbox suffix' }>
724
+ Checkbox item one
725
+ </DropdownMenuCheckboxItem>
726
+ </DropdownMenu>
727
+ );
728
+
729
+ // Click to open the menu
730
+ await user.click(
731
+ screen.getByRole( 'button', {
732
+ name: 'Open dropdown',
733
+ } )
734
+ );
735
+
736
+ // The contents of the suffix are rendered after the item's children
737
+ expect(
738
+ screen.getByRole( 'menuitemcheckbox', {
739
+ name: 'Checkbox item one Checkbox suffix',
740
+ } )
741
+ ).toBeInTheDocument();
742
+ } );
743
+ } );
744
+
745
+ describe( 'typeahead', () => {
746
+ it( 'should highlight matching item', async () => {
747
+ const user = userEvent.setup();
748
+
749
+ render(
750
+ <DropdownMenu trigger={ <button>Open dropdown</button> }>
751
+ <DropdownMenuItem>One</DropdownMenuItem>
752
+ <DropdownMenuItem>Two</DropdownMenuItem>
753
+ </DropdownMenu>
754
+ );
755
+
756
+ // Click to open the menu
757
+ await user.click(
758
+ screen.getByRole( 'button', {
759
+ name: 'Open dropdown',
760
+ } )
761
+ );
762
+ expect( screen.getByRole( 'menu' ) ).toBeInTheDocument();
763
+
764
+ // Type "tw", it should match and focus the item with content "Two"
765
+ await user.keyboard( 'tw' );
766
+ expect(
767
+ screen.getByRole( 'menuitem', { name: 'Two' } )
768
+ ).toHaveFocus();
769
+
770
+ // Wait for the typeahead timer to reset and interpret
771
+ // the next keystrokes as a new search
772
+ await delay( 1000 );
773
+
774
+ // Type "on", it should match and focus the item with content "One"
775
+ await user.keyboard( 'on' );
776
+ expect(
777
+ screen.getByRole( 'menuitem', { name: 'One' } )
778
+ ).toHaveFocus();
779
+ } );
780
+
781
+ it( 'should use the textValue prop if specificied', async () => {
782
+ const user = userEvent.setup();
783
+
784
+ render(
785
+ <DropdownMenu trigger={ <button>Open dropdown</button> }>
786
+ <DropdownMenuItem>One</DropdownMenuItem>
787
+ <DropdownMenuItem textValue="Four">Two</DropdownMenuItem>
788
+ </DropdownMenu>
789
+ );
790
+
791
+ // Click to open the menu
792
+ await user.click(
793
+ screen.getByRole( 'button', {
794
+ name: 'Open dropdown',
795
+ } )
796
+ );
797
+ expect( screen.getByRole( 'menu' ) ).toBeInTheDocument();
798
+
799
+ // Type "tw", it should not match the item with content "Two" because it
800
+ // that item specifies the "textValue" prop. Therefore, the menu container
801
+ // retains focus.
802
+ await user.keyboard( 'tw' );
803
+ expect( screen.getByRole( 'menu' ) ).toHaveFocus();
804
+
805
+ // Wait for the typeahead timer to reset and interpret
806
+ // the next keystrokes as a new search
807
+ await delay( 1000 );
808
+
809
+ // Type "fo", it should match and focus the item with textValue "Four"
810
+ await user.keyboard( 'fo' );
811
+ expect(
812
+ screen.getByRole( 'menuitem', { name: 'Two' } )
813
+ ).toHaveFocus();
814
+ } );
815
+ } );
816
+ } );