@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.
- package/CHANGELOG.md +26 -0
- package/CONTRIBUTING.md +10 -0
- package/build/color-picker/styles.js +8 -8
- package/build/color-picker/styles.js.map +1 -1
- package/build/date-time/date-time/index.js +3 -84
- package/build/date-time/date-time/index.js.map +1 -1
- package/build/date-time/date-time/styles.js +4 -19
- package/build/date-time/date-time/styles.js.map +1 -1
- package/build/dropdown-menu/index.js +87 -11
- package/build/dropdown-menu/index.js.map +1 -1
- package/build/dropdown-menu/types.js +6 -0
- package/build/dropdown-menu/types.js.map +1 -0
- package/build/dropdown-menu-v2/index.js +195 -0
- package/build/dropdown-menu-v2/index.js.map +1 -0
- package/build/dropdown-menu-v2/styles.js +176 -0
- package/build/dropdown-menu-v2/styles.js.map +1 -0
- package/build/dropdown-menu-v2/types.js +6 -0
- package/build/dropdown-menu-v2/types.js.map +1 -0
- package/build/index.native.js +0 -9
- package/build/index.native.js.map +1 -1
- package/build/input-control/styles/input-control-styles.js +30 -23
- package/build/input-control/styles/input-control-styles.js.map +1 -1
- package/build/mobile/bottom-sheet/cell.native.js +16 -8
- package/build/mobile/bottom-sheet/cell.native.js.map +1 -1
- package/build/mobile/bottom-sheet/range-cell.native.js +3 -2
- package/build/mobile/bottom-sheet/range-cell.native.js.map +1 -1
- package/build/mobile/bottom-sheet/stepper-cell/index.native.js +4 -2
- package/build/mobile/bottom-sheet/stepper-cell/index.native.js.map +1 -1
- package/build/mobile/bottom-sheet/switch-cell.native.js +8 -2
- package/build/mobile/bottom-sheet/switch-cell.native.js.map +1 -1
- package/build/mobile/bottom-sheet-select-control/index.native.js +4 -2
- package/build/mobile/bottom-sheet-select-control/index.native.js.map +1 -1
- package/build/mobile/bottom-sheet-text-control/index.native.js +4 -2
- package/build/mobile/bottom-sheet-text-control/index.native.js.map +1 -1
- package/build/modal/index.js +1 -2
- package/build/modal/index.js.map +1 -1
- package/build/private-apis.js +13 -1
- package/build/private-apis.js.map +1 -1
- package/build/range-control/index.native.js +5 -2
- package/build/range-control/index.native.js.map +1 -1
- package/build/snackbar/list.js +0 -2
- package/build/snackbar/list.js.map +1 -1
- package/build/toggle-group-control/toggle-group-control/styles.js +7 -7
- package/build/toggle-group-control/toggle-group-control/styles.js.map +1 -1
- package/build-module/color-picker/styles.js +8 -8
- package/build-module/color-picker/styles.js.map +1 -1
- package/build-module/date-time/date-time/index.js +6 -81
- package/build-module/date-time/date-time/index.js.map +1 -1
- package/build-module/date-time/date-time/styles.js +3 -17
- package/build-module/date-time/date-time/styles.js.map +1 -1
- package/build-module/dropdown-menu/index.js +87 -10
- package/build-module/dropdown-menu/index.js.map +1 -1
- package/build-module/dropdown-menu/types.js +2 -0
- package/build-module/dropdown-menu/types.js.map +1 -0
- package/build-module/dropdown-menu-v2/index.js +149 -0
- package/build-module/dropdown-menu-v2/index.js.map +1 -0
- package/build-module/dropdown-menu-v2/styles.js +153 -0
- package/build-module/dropdown-menu-v2/styles.js.map +1 -0
- package/build-module/dropdown-menu-v2/types.js +2 -0
- package/build-module/dropdown-menu-v2/types.js.map +1 -0
- package/build-module/index.native.js +0 -1
- package/build-module/index.native.js.map +1 -1
- package/build-module/input-control/styles/input-control-styles.js +30 -23
- package/build-module/input-control/styles/input-control-styles.js.map +1 -1
- package/build-module/mobile/bottom-sheet/cell.native.js +16 -8
- package/build-module/mobile/bottom-sheet/cell.native.js.map +1 -1
- package/build-module/mobile/bottom-sheet/range-cell.native.js +3 -2
- package/build-module/mobile/bottom-sheet/range-cell.native.js.map +1 -1
- package/build-module/mobile/bottom-sheet/stepper-cell/index.native.js +4 -2
- package/build-module/mobile/bottom-sheet/stepper-cell/index.native.js.map +1 -1
- package/build-module/mobile/bottom-sheet/switch-cell.native.js +7 -2
- package/build-module/mobile/bottom-sheet/switch-cell.native.js.map +1 -1
- package/build-module/mobile/bottom-sheet-select-control/index.native.js +4 -2
- package/build-module/mobile/bottom-sheet-select-control/index.native.js.map +1 -1
- package/build-module/mobile/bottom-sheet-text-control/index.native.js +4 -2
- package/build-module/mobile/bottom-sheet-text-control/index.native.js.map +1 -1
- package/build-module/modal/index.js +1 -2
- package/build-module/modal/index.js.map +1 -1
- package/build-module/private-apis.js +12 -1
- package/build-module/private-apis.js.map +1 -1
- package/build-module/range-control/index.native.js +5 -2
- package/build-module/range-control/index.native.js.map +1 -1
- package/build-module/snackbar/list.js +0 -2
- package/build-module/snackbar/list.js.map +1 -1
- package/build-module/toggle-group-control/toggle-group-control/styles.js +7 -7
- package/build-module/toggle-group-control/toggle-group-control/styles.js.map +1 -1
- package/build-style/style-rtl.css +11 -14
- package/build-style/style.css +11 -14
- package/build-types/color-picker/styles.d.ts.map +1 -1
- package/build-types/date-time/date-time/index.d.ts +3 -4
- package/build-types/date-time/date-time/index.d.ts.map +1 -1
- package/build-types/date-time/date-time/styles.d.ts +0 -4
- package/build-types/date-time/date-time/styles.d.ts.map +1 -1
- package/build-types/date-time/stories/date-time.d.ts.map +1 -1
- package/build-types/date-time/types.d.ts +0 -14
- package/build-types/date-time/types.d.ts.map +1 -1
- package/build-types/dropdown-menu/index.d.ts +83 -1
- package/build-types/dropdown-menu/index.d.ts.map +1 -1
- package/build-types/dropdown-menu/stories/index.d.ts +13 -0
- package/build-types/dropdown-menu/stories/index.d.ts.map +1 -0
- package/build-types/dropdown-menu/test/index.d.ts +2 -0
- package/build-types/dropdown-menu/test/index.d.ts.map +1 -0
- package/build-types/dropdown-menu/types.d.ts +134 -0
- package/build-types/dropdown-menu/types.d.ts.map +1 -0
- package/build-types/dropdown-menu-v2/index.d.ts +17 -0
- package/build-types/dropdown-menu-v2/index.d.ts.map +1 -0
- package/build-types/dropdown-menu-v2/stories/index.d.ts +13 -0
- package/build-types/dropdown-menu-v2/stories/index.d.ts.map +1 -0
- package/build-types/dropdown-menu-v2/styles.d.ts +41 -0
- package/build-types/dropdown-menu-v2/styles.d.ts.map +1 -0
- package/build-types/dropdown-menu-v2/test/index.d.ts +2 -0
- package/build-types/dropdown-menu-v2/test/index.d.ts.map +1 -0
- package/build-types/dropdown-menu-v2/types.d.ts +242 -0
- package/build-types/dropdown-menu-v2/types.d.ts.map +1 -0
- package/build-types/input-control/styles/input-control-styles.d.ts.map +1 -1
- package/build-types/modal/index.d.ts.map +1 -1
- package/build-types/private-apis.d.ts.map +1 -1
- package/build-types/snackbar/list.d.ts.map +1 -1
- package/build-types/toggle-group-control/toggle-group-control/styles.d.ts.map +1 -1
- package/build-types/toolbar/stories/index.d.ts.map +1 -1
- package/build-types/ui/context/get-styled-class-name-from-key.d.ts +1 -10
- package/build-types/ui/context/get-styled-class-name-from-key.d.ts.map +1 -1
- package/package.json +21 -20
- package/src/button/style.scss +5 -12
- package/src/color-picker/styles.ts +7 -2
- package/src/date-time/README.md +0 -16
- package/src/date-time/date-time/index.tsx +17 -155
- package/src/date-time/date-time/styles.ts +0 -4
- package/src/date-time/stories/date-time.tsx +0 -4
- package/src/date-time/types.ts +0 -16
- package/src/dropdown-menu/README.md +12 -22
- package/src/dropdown-menu/{index.js → index.tsx} +111 -25
- package/src/dropdown-menu/stories/{index.js → index.tsx} +14 -22
- package/src/dropdown-menu/test/{index.js → index.tsx} +6 -5
- package/src/dropdown-menu/types.ts +143 -0
- package/src/dropdown-menu-v2/README.md +392 -0
- package/src/dropdown-menu-v2/index.tsx +241 -0
- package/src/dropdown-menu-v2/stories/index.tsx +193 -0
- package/src/dropdown-menu-v2/styles.ts +263 -0
- package/src/dropdown-menu-v2/test/index.tsx +816 -0
- package/src/dropdown-menu-v2/types.ts +250 -0
- package/src/index.native.js +0 -1
- package/src/input-control/styles/input-control-styles.tsx +7 -0
- package/src/mobile/bottom-sheet/cell.native.js +26 -5
- package/src/mobile/bottom-sheet/range-cell.native.js +2 -1
- package/src/mobile/bottom-sheet/stepper-cell/index.native.js +2 -0
- package/src/mobile/bottom-sheet/styles.native.scss +13 -1
- package/src/mobile/bottom-sheet/switch-cell.native.js +10 -2
- package/src/mobile/bottom-sheet-select-control/index.native.js +2 -0
- package/src/mobile/bottom-sheet-text-control/index.native.js +2 -0
- package/src/modal/index.tsx +1 -6
- package/src/private-apis.ts +22 -0
- package/src/range-control/index.native.js +3 -0
- package/src/search-control/style.scss +2 -0
- package/src/snackbar/list.tsx +0 -1
- package/src/toggle-group-control/test/__snapshots__/index.tsx.snap +6 -2
- package/src/toggle-group-control/toggle-group-control/styles.ts +6 -1
- package/src/toolbar/stories/index.tsx +25 -28
- package/src/tooltip/style.scss +2 -2
- package/tsconfig.tsbuildinfo +1 -1
- package/build/mobile/readable-content-view/index.native.js +0 -97
- package/build/mobile/readable-content-view/index.native.js.map +0 -1
- package/build-module/mobile/readable-content-view/index.native.js +0 -81
- package/build-module/mobile/readable-content-view/index.native.js.map +0 -1
- package/src/mobile/readable-content-view/index.native.js +0 -85
- 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
|
+
} );
|