@@ -10,11 +10,21 @@ import {signal} from '@angular/core';
10
10
import { ListboxInputs , ListboxPattern } from './listbox' ;
11
11
import { OptionPattern } from './option' ;
12
12
import { createKeyboardEvent } from '@angular/cdk/testing/private' ;
13
+ import { ModifierKeys } from '@angular/cdk/testing' ;
13
14
14
15
type TestInputs = ListboxInputs < string > ;
15
16
type TestOption = OptionPattern < string > ;
16
17
type TestListbox = ListboxPattern < string > ;
17
18
19
+ const up = ( mods ?: ModifierKeys ) => createKeyboardEvent ( 'keydown' , 38 , 'ArrowUp' , mods ) ;
20
+ const down = ( mods ?: ModifierKeys ) => createKeyboardEvent ( 'keydown' , 40 , 'ArrowDown' , mods ) ;
21
+ const left = ( mods ?: ModifierKeys ) => createKeyboardEvent ( 'keydown' , 37 , 'ArrowLeft' , mods ) ;
22
+ const right = ( mods ?: ModifierKeys ) => createKeyboardEvent ( 'keydown' , 39 , 'ArrowRight' , mods ) ;
23
+ const home = ( mods ?: ModifierKeys ) => createKeyboardEvent ( 'keydown' , 36 , 'Home' , mods ) ;
24
+ const end = ( mods ?: ModifierKeys ) => createKeyboardEvent ( 'keydown' , 35 , 'End' , mods ) ;
25
+ const space = ( mods ?: ModifierKeys ) => createKeyboardEvent ( 'keydown' , 32 , ' ' , mods ) ;
26
+ const enter = ( mods ?: ModifierKeys ) => createKeyboardEvent ( 'keydown' , 13 , 'Enter' , mods ) ;
27
+
18
28
describe ( 'Listbox Pattern' , ( ) => {
19
29
function getListbox ( inputs : Partial < TestInputs > & Pick < TestInputs , 'items' > ) {
20
30
return new ListboxPattern ( {
@@ -70,85 +80,287 @@ describe('Listbox Pattern', () => {
70
80
) ;
71
81
}
72
82
73
- describe ( 'Navigation' , ( ) => {
83
+ describe ( 'Keyboard Navigation' , ( ) => {
74
84
it ( 'should navigate next on ArrowDown' , ( ) => {
75
85
const { listbox} = getDefaultPatterns ( ) ;
76
- const event = createKeyboardEvent ( 'keydown' , 40 , 'ArrowDown' ) ;
77
86
expect ( listbox . inputs . activeIndex ( ) ) . toBe ( 0 ) ;
78
- listbox . onKeydown ( event ) ;
87
+ listbox . onKeydown ( down ( ) ) ;
79
88
expect ( listbox . inputs . activeIndex ( ) ) . toBe ( 1 ) ;
80
89
} ) ;
81
90
82
91
it ( 'should navigate prev on ArrowUp' , ( ) => {
83
- const event = createKeyboardEvent ( 'keydown' , 38 , 'ArrowUp' ) ;
84
- const { listbox} = getDefaultPatterns ( {
85
- activeIndex : signal ( 1 ) ,
86
- } ) ;
92
+ const { listbox} = getDefaultPatterns ( { activeIndex : signal ( 1 ) } ) ;
87
93
expect ( listbox . inputs . activeIndex ( ) ) . toBe ( 1 ) ;
88
- listbox . onKeydown ( event ) ;
94
+ listbox . onKeydown ( up ( ) ) ;
89
95
expect ( listbox . inputs . activeIndex ( ) ) . toBe ( 0 ) ;
90
96
} ) ;
91
97
92
98
it ( 'should navigate next on ArrowRight (horizontal)' , ( ) => {
93
- const event = createKeyboardEvent ( 'keydown' , 39 , 'ArrowRight' ) ;
94
- const { listbox} = getDefaultPatterns ( {
95
- orientation : signal ( 'horizontal' ) ,
96
- } ) ;
99
+ const { listbox} = getDefaultPatterns ( { orientation : signal ( 'horizontal' ) } ) ;
97
100
expect ( listbox . inputs . activeIndex ( ) ) . toBe ( 0 ) ;
98
- listbox . onKeydown ( event ) ;
101
+ listbox . onKeydown ( right ( ) ) ;
99
102
expect ( listbox . inputs . activeIndex ( ) ) . toBe ( 1 ) ;
100
103
} ) ;
101
104
102
105
it ( 'should navigate prev on ArrowLeft (horizontal)' , ( ) => {
103
- const event = createKeyboardEvent ( 'keydown' , 37 , 'ArrowLeft' ) ;
104
106
const { listbox} = getDefaultPatterns ( {
105
107
activeIndex : signal ( 1 ) ,
106
108
orientation : signal ( 'horizontal' ) ,
107
109
} ) ;
108
110
expect ( listbox . inputs . activeIndex ( ) ) . toBe ( 1 ) ;
109
- listbox . onKeydown ( event ) ;
111
+ listbox . onKeydown ( left ( ) ) ;
110
112
expect ( listbox . inputs . activeIndex ( ) ) . toBe ( 0 ) ;
111
113
} ) ;
112
114
113
115
it ( 'should navigate next on ArrowLeft (horizontal & rtl)' , ( ) => {
114
- const event = createKeyboardEvent ( 'keydown' , 38 , 'ArrowLeft' ) ;
115
116
const { listbox} = getDefaultPatterns ( {
116
117
textDirection : signal ( 'rtl' ) ,
117
118
orientation : signal ( 'horizontal' ) ,
118
119
} ) ;
119
120
expect ( listbox . inputs . activeIndex ( ) ) . toBe ( 0 ) ;
120
- listbox . onKeydown ( event ) ;
121
+ listbox . onKeydown ( left ( ) ) ;
121
122
expect ( listbox . inputs . activeIndex ( ) ) . toBe ( 1 ) ;
122
123
} ) ;
123
124
124
125
it ( 'should navigate prev on ArrowRight (horizontal & rtl)' , ( ) => {
125
- const event = createKeyboardEvent ( 'keydown' , 39 , 'ArrowRight' ) ;
126
126
const { listbox} = getDefaultPatterns ( {
127
127
activeIndex : signal ( 1 ) ,
128
128
textDirection : signal ( 'rtl' ) ,
129
129
orientation : signal ( 'horizontal' ) ,
130
130
} ) ;
131
131
expect ( listbox . inputs . activeIndex ( ) ) . toBe ( 1 ) ;
132
- listbox . onKeydown ( event ) ;
132
+ listbox . onKeydown ( right ( ) ) ;
133
133
expect ( listbox . inputs . activeIndex ( ) ) . toBe ( 0 ) ;
134
134
} ) ;
135
135
136
136
it ( 'should navigate to the first option on Home' , ( ) => {
137
- const event = createKeyboardEvent ( 'keydown' , 36 , 'Home' ) ;
138
137
const { listbox} = getDefaultPatterns ( {
139
138
activeIndex : signal ( 8 ) ,
140
139
} ) ;
141
140
expect ( listbox . inputs . activeIndex ( ) ) . toBe ( 8 ) ;
142
- listbox . onKeydown ( event ) ;
141
+ listbox . onKeydown ( home ( ) ) ;
143
142
expect ( listbox . inputs . activeIndex ( ) ) . toBe ( 0 ) ;
144
143
} ) ;
145
144
146
145
it ( 'should navigate to the last option on End' , ( ) => {
147
- const event = createKeyboardEvent ( 'keydown' , 35 , 'End' ) ;
148
146
const { listbox} = getDefaultPatterns ( ) ;
149
147
expect ( listbox . inputs . activeIndex ( ) ) . toBe ( 0 ) ;
150
- listbox . onKeydown ( event ) ;
148
+ listbox . onKeydown ( end ( ) ) ;
151
149
expect ( listbox . inputs . activeIndex ( ) ) . toBe ( 8 ) ;
152
150
} ) ;
153
151
} ) ;
152
+
153
+ describe ( 'Keyboard Selection' , ( ) => {
154
+ describe ( 'follows focus & single select' , ( ) => {
155
+ it ( 'should select an option on navigation' , ( ) => {
156
+ const { listbox} = getDefaultPatterns ( {
157
+ value : signal ( [ 'Apple' ] ) ,
158
+ multiselectable : signal ( false ) ,
159
+ selectionMode : signal ( 'follow' ) ,
160
+ } ) ;
161
+
162
+ expect ( listbox . inputs . activeIndex ( ) ) . toBe ( 0 ) ;
163
+ expect ( listbox . inputs . value ( ) ) . toEqual ( [ 'Apple' ] ) ;
164
+
165
+ listbox . onKeydown ( down ( ) ) ;
166
+ expect ( listbox . inputs . activeIndex ( ) ) . toBe ( 1 ) ;
167
+ expect ( listbox . inputs . value ( ) ) . toEqual ( [ 'Apricot' ] ) ;
168
+
169
+ listbox . onKeydown ( up ( ) ) ;
170
+ expect ( listbox . inputs . activeIndex ( ) ) . toBe ( 0 ) ;
171
+ expect ( listbox . inputs . value ( ) ) . toEqual ( [ 'Apple' ] ) ;
172
+
173
+ listbox . onKeydown ( end ( ) ) ;
174
+ expect ( listbox . inputs . activeIndex ( ) ) . toBe ( 8 ) ;
175
+ expect ( listbox . inputs . value ( ) ) . toEqual ( [ 'Cranberry' ] ) ;
176
+
177
+ listbox . onKeydown ( home ( ) ) ;
178
+ expect ( listbox . inputs . activeIndex ( ) ) . toBe ( 0 ) ;
179
+ expect ( listbox . inputs . value ( ) ) . toEqual ( [ 'Apple' ] ) ;
180
+ } ) ;
181
+ } ) ;
182
+
183
+ describe ( 'explicit focus & single select' , ( ) => {
184
+ let listbox : TestListbox ;
185
+
186
+ beforeEach ( ( ) => {
187
+ listbox = getDefaultPatterns ( {
188
+ value : signal ( [ ] ) ,
189
+ selectionMode : signal ( 'explicit' ) ,
190
+ multiselectable : signal ( false ) ,
191
+ } ) . listbox ;
192
+ } ) ;
193
+
194
+ it ( 'should select an option on Space' , ( ) => {
195
+ listbox . onKeydown ( space ( ) ) ;
196
+ expect ( listbox . inputs . value ( ) ) . toEqual ( [ 'Apple' ] ) ;
197
+ } ) ;
198
+
199
+ it ( 'should select an option on Enter' , ( ) => {
200
+ listbox . onKeydown ( enter ( ) ) ;
201
+ expect ( listbox . inputs . value ( ) ) . toEqual ( [ 'Apple' ] ) ;
202
+ } ) ;
203
+
204
+ it ( 'should only allow one selected option' , ( ) => {
205
+ listbox . onKeydown ( enter ( ) ) ;
206
+ listbox . onKeydown ( down ( ) ) ;
207
+ listbox . onKeydown ( enter ( ) ) ;
208
+ expect ( listbox . inputs . value ( ) ) . toEqual ( [ 'Apricot' ] ) ;
209
+ } ) ;
210
+ } ) ;
211
+
212
+ describe ( 'explicit focus & multi select' , ( ) => {
213
+ let listbox : TestListbox ;
214
+
215
+ beforeEach ( ( ) => {
216
+ listbox = getDefaultPatterns ( {
217
+ value : signal ( [ ] ) ,
218
+ selectionMode : signal ( 'explicit' ) ,
219
+ multiselectable : signal ( true ) ,
220
+ } ) . listbox ;
221
+ } ) ;
222
+
223
+ it ( 'should select an option on Space' , ( ) => {
224
+ listbox . onKeydown ( space ( ) ) ;
225
+ expect ( listbox . inputs . value ( ) ) . toEqual ( [ 'Apple' ] ) ;
226
+ } ) ;
227
+
228
+ it ( 'should select an option on Enter' , ( ) => {
229
+ listbox . onKeydown ( enter ( ) ) ;
230
+ expect ( listbox . inputs . value ( ) ) . toEqual ( [ 'Apple' ] ) ;
231
+ } ) ;
232
+
233
+ it ( 'should allow multiple selected options' , ( ) => {
234
+ listbox . onKeydown ( enter ( ) ) ;
235
+ listbox . onKeydown ( down ( ) ) ;
236
+ listbox . onKeydown ( enter ( ) ) ;
237
+ expect ( listbox . inputs . value ( ) ) . toEqual ( [ 'Apple' , 'Apricot' ] ) ;
238
+ } ) ;
239
+
240
+ it ( 'should toggle the selected state of the next option on Shift + ArrowDown' , ( ) => {
241
+ listbox . onKeydown ( down ( { shift : true } ) ) ;
242
+ listbox . onKeydown ( down ( { shift : true } ) ) ;
243
+ expect ( listbox . inputs . value ( ) ) . toEqual ( [ 'Apricot' , 'Banana' ] ) ;
244
+ } ) ;
245
+
246
+ it ( 'should toggle the selected state of the next option on Shift + ArrowUp' , ( ) => {
247
+ listbox . onKeydown ( down ( ) ) ;
248
+ listbox . onKeydown ( down ( ) ) ;
249
+ listbox . onKeydown ( up ( { shift : true } ) ) ;
250
+ listbox . onKeydown ( up ( { shift : true } ) ) ;
251
+ expect ( listbox . inputs . value ( ) ) . toEqual ( [ 'Apricot' , 'Apple' ] ) ;
252
+ } ) ;
253
+
254
+ it ( 'should select contiguous items from the most recently selected item to the focused item on Shift + Space (or Enter)' , ( ) => {
255
+ listbox . onKeydown ( down ( ) ) ;
256
+ listbox . onKeydown ( space ( ) ) ; // Apricot
257
+ listbox . onKeydown ( down ( ) ) ;
258
+ listbox . onKeydown ( down ( ) ) ;
259
+ listbox . onKeydown ( space ( { shift : true } ) ) ;
260
+ expect ( listbox . inputs . value ( ) ) . toEqual ( [ 'Apricot' , 'Banana' , 'Blackberry' ] ) ;
261
+ } ) ;
262
+
263
+ it ( 'should select the focused option and all options up to the first option on Ctrl + Shift + Home' , ( ) => {
264
+ listbox . onKeydown ( down ( ) ) ;
265
+ listbox . onKeydown ( down ( ) ) ;
266
+ listbox . onKeydown ( down ( ) ) ;
267
+ listbox . onKeydown ( home ( { control : true , shift : true } ) ) ;
268
+ expect ( listbox . inputs . value ( ) ) . toEqual ( [ 'Apple' , 'Apricot' , 'Banana' , 'Blackberry' ] ) ;
269
+ } ) ;
270
+
271
+ it ( 'should select the focused option and all options down to the last option on Ctrl + Shift + End' , ( ) => {
272
+ listbox . onKeydown ( down ( ) ) ;
273
+ listbox . onKeydown ( down ( ) ) ;
274
+ listbox . onKeydown ( down ( ) ) ;
275
+ listbox . onKeydown ( down ( ) ) ;
276
+ listbox . onKeydown ( down ( ) ) ;
277
+ listbox . onKeydown ( end ( { control : true , shift : true } ) ) ;
278
+ expect ( listbox . inputs . value ( ) ) . toEqual ( [ 'Cantaloupe' , 'Cherry' , 'Clementine' , 'Cranberry' ] ) ;
279
+ } ) ;
280
+ } ) ;
281
+
282
+ describe ( 'follows focus & multi select' , ( ) => {
283
+ let listbox : TestListbox ;
284
+
285
+ beforeEach ( ( ) => {
286
+ listbox = getDefaultPatterns ( {
287
+ value : signal ( [ 'Apple' ] ) ,
288
+ multiselectable : signal ( true ) ,
289
+ selectionMode : signal ( 'follow' ) ,
290
+ } ) . listbox ;
291
+ } ) ;
292
+
293
+ it ( 'should select an option on navigation' , ( ) => {
294
+ expect ( listbox . inputs . value ( ) ) . toEqual ( [ 'Apple' ] ) ;
295
+ listbox . onKeydown ( down ( ) ) ;
296
+ expect ( listbox . inputs . value ( ) ) . toEqual ( [ 'Apricot' ] ) ;
297
+ listbox . onKeydown ( up ( ) ) ;
298
+ expect ( listbox . inputs . value ( ) ) . toEqual ( [ 'Apple' ] ) ;
299
+ listbox . onKeydown ( end ( ) ) ;
300
+ expect ( listbox . inputs . value ( ) ) . toEqual ( [ 'Cranberry' ] ) ;
301
+ listbox . onKeydown ( home ( ) ) ;
302
+ expect ( listbox . inputs . value ( ) ) . toEqual ( [ 'Apple' ] ) ;
303
+ } ) ;
304
+
305
+ it ( 'should navigate without selecting an option if the Ctrl key is pressed' , ( ) => {
306
+ expect ( listbox . inputs . value ( ) ) . toEqual ( [ 'Apple' ] ) ;
307
+ listbox . onKeydown ( down ( { control : true } ) ) ;
308
+ expect ( listbox . inputs . value ( ) ) . toEqual ( [ 'Apple' ] ) ;
309
+ listbox . onKeydown ( up ( { control : true } ) ) ;
310
+ expect ( listbox . inputs . value ( ) ) . toEqual ( [ 'Apple' ] ) ;
311
+ listbox . onKeydown ( end ( { control : true } ) ) ;
312
+ expect ( listbox . inputs . value ( ) ) . toEqual ( [ 'Apple' ] ) ;
313
+ listbox . onKeydown ( home ( { control : true } ) ) ;
314
+ } ) ;
315
+
316
+ it ( 'should toggle an options selection state on Ctrl + Space' , ( ) => {
317
+ listbox . onKeydown ( down ( { control : true } ) ) ;
318
+ listbox . onKeydown ( down ( { control : true } ) ) ;
319
+ listbox . onKeydown ( space ( { control : true } ) ) ;
320
+ expect ( listbox . inputs . value ( ) ) . toEqual ( [ 'Apple' , 'Banana' ] ) ;
321
+ } ) ;
322
+
323
+ it ( 'should toggle the selected state of the next option on Shift + ArrowDown' , ( ) => {
324
+ listbox . onKeydown ( down ( { shift : true } ) ) ;
325
+ listbox . onKeydown ( down ( { shift : true } ) ) ;
326
+ expect ( listbox . inputs . value ( ) ) . toEqual ( [ 'Apple' , 'Apricot' , 'Banana' ] ) ;
327
+ } ) ;
328
+
329
+ it ( 'should toggle the selected state of the next option on Shift + ArrowUp' , ( ) => {
330
+ listbox . onKeydown ( down ( ) ) ;
331
+ listbox . onKeydown ( down ( ) ) ;
332
+ listbox . onKeydown ( up ( { shift : true } ) ) ;
333
+ listbox . onKeydown ( up ( { shift : true } ) ) ;
334
+ expect ( listbox . inputs . value ( ) ) . toEqual ( [ 'Banana' , 'Apricot' , 'Apple' ] ) ;
335
+ } ) ;
336
+
337
+ it ( 'should select contiguous items from the most recently selected item to the focused item on Shift + Space (or Enter)' , ( ) => {
338
+ listbox . onKeydown ( down ( { control : true } ) ) ;
339
+ listbox . onKeydown ( down ( { control : true } ) ) ;
340
+ listbox . onKeydown ( down ( ) ) ; // Blackberry
341
+ listbox . onKeydown ( down ( { control : true } ) ) ;
342
+ listbox . onKeydown ( down ( { control : true } ) ) ;
343
+ listbox . onKeydown ( space ( { shift : true } ) ) ;
344
+ expect ( listbox . inputs . value ( ) ) . toEqual ( [ 'Blackberry' , 'Blueberry' , 'Cantaloupe' ] ) ;
345
+ } ) ;
346
+
347
+ it ( 'should select the focused option and all options up to the first option on Ctrl + Shift + Home' , ( ) => {
348
+ listbox . onKeydown ( down ( { control : true } ) ) ;
349
+ listbox . onKeydown ( down ( { control : true } ) ) ;
350
+ listbox . onKeydown ( down ( ) ) ;
351
+ listbox . onKeydown ( home ( { control : true , shift : true } ) ) ;
352
+ expect ( listbox . inputs . value ( ) ) . toEqual ( [ 'Blackberry' , 'Apple' , 'Apricot' , 'Banana' ] ) ;
353
+ } ) ;
354
+
355
+ it ( 'should select the focused option and all options down to the last option on Ctrl + Shift + End' , ( ) => {
356
+ listbox . onKeydown ( down ( { control : true } ) ) ;
357
+ listbox . onKeydown ( down ( { control : true } ) ) ;
358
+ listbox . onKeydown ( down ( { control : true } ) ) ;
359
+ listbox . onKeydown ( down ( { control : true } ) ) ;
360
+ listbox . onKeydown ( down ( ) ) ;
361
+ listbox . onKeydown ( end ( { control : true , shift : true } ) ) ;
362
+ expect ( listbox . inputs . value ( ) ) . toEqual ( [ 'Cantaloupe' , 'Cherry' , 'Clementine' , 'Cranberry' ] ) ;
363
+ } ) ;
364
+ } ) ;
365
+ } ) ;
154
366
} ) ;
0 commit comments