@@ -10,11 +10,21 @@ import {signal} from '@angular/core';
1010import { ListboxInputs , ListboxPattern } from './listbox' ;
1111import { OptionPattern } from './option' ;
1212import { createKeyboardEvent } from '@angular/cdk/testing/private' ;
13+ import { ModifierKeys } from '@angular/cdk/testing' ;
1314
1415type TestInputs = ListboxInputs < string > ;
1516type TestOption = OptionPattern < string > ;
1617type TestListbox = ListboxPattern < string > ;
1718
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+
1828describe ( 'Listbox Pattern' , ( ) => {
1929 function getListbox ( inputs : Partial < TestInputs > & Pick < TestInputs , 'items' > ) {
2030 return new ListboxPattern ( {
@@ -70,85 +80,287 @@ describe('Listbox Pattern', () => {
7080 ) ;
7181 }
7282
73- describe ( 'Navigation' , ( ) => {
83+ describe ( 'Keyboard Navigation' , ( ) => {
7484 it ( 'should navigate next on ArrowDown' , ( ) => {
7585 const { listbox} = getDefaultPatterns ( ) ;
76- const event = createKeyboardEvent ( 'keydown' , 40 , 'ArrowDown' ) ;
7786 expect ( listbox . inputs . activeIndex ( ) ) . toBe ( 0 ) ;
78- listbox . onKeydown ( event ) ;
87+ listbox . onKeydown ( down ( ) ) ;
7988 expect ( listbox . inputs . activeIndex ( ) ) . toBe ( 1 ) ;
8089 } ) ;
8190
8291 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 ) } ) ;
8793 expect ( listbox . inputs . activeIndex ( ) ) . toBe ( 1 ) ;
88- listbox . onKeydown ( event ) ;
94+ listbox . onKeydown ( up ( ) ) ;
8995 expect ( listbox . inputs . activeIndex ( ) ) . toBe ( 0 ) ;
9096 } ) ;
9197
9298 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' ) } ) ;
97100 expect ( listbox . inputs . activeIndex ( ) ) . toBe ( 0 ) ;
98- listbox . onKeydown ( event ) ;
101+ listbox . onKeydown ( right ( ) ) ;
99102 expect ( listbox . inputs . activeIndex ( ) ) . toBe ( 1 ) ;
100103 } ) ;
101104
102105 it ( 'should navigate prev on ArrowLeft (horizontal)' , ( ) => {
103- const event = createKeyboardEvent ( 'keydown' , 37 , 'ArrowLeft' ) ;
104106 const { listbox} = getDefaultPatterns ( {
105107 activeIndex : signal ( 1 ) ,
106108 orientation : signal ( 'horizontal' ) ,
107109 } ) ;
108110 expect ( listbox . inputs . activeIndex ( ) ) . toBe ( 1 ) ;
109- listbox . onKeydown ( event ) ;
111+ listbox . onKeydown ( left ( ) ) ;
110112 expect ( listbox . inputs . activeIndex ( ) ) . toBe ( 0 ) ;
111113 } ) ;
112114
113115 it ( 'should navigate next on ArrowLeft (horizontal & rtl)' , ( ) => {
114- const event = createKeyboardEvent ( 'keydown' , 38 , 'ArrowLeft' ) ;
115116 const { listbox} = getDefaultPatterns ( {
116117 textDirection : signal ( 'rtl' ) ,
117118 orientation : signal ( 'horizontal' ) ,
118119 } ) ;
119120 expect ( listbox . inputs . activeIndex ( ) ) . toBe ( 0 ) ;
120- listbox . onKeydown ( event ) ;
121+ listbox . onKeydown ( left ( ) ) ;
121122 expect ( listbox . inputs . activeIndex ( ) ) . toBe ( 1 ) ;
122123 } ) ;
123124
124125 it ( 'should navigate prev on ArrowRight (horizontal & rtl)' , ( ) => {
125- const event = createKeyboardEvent ( 'keydown' , 39 , 'ArrowRight' ) ;
126126 const { listbox} = getDefaultPatterns ( {
127127 activeIndex : signal ( 1 ) ,
128128 textDirection : signal ( 'rtl' ) ,
129129 orientation : signal ( 'horizontal' ) ,
130130 } ) ;
131131 expect ( listbox . inputs . activeIndex ( ) ) . toBe ( 1 ) ;
132- listbox . onKeydown ( event ) ;
132+ listbox . onKeydown ( right ( ) ) ;
133133 expect ( listbox . inputs . activeIndex ( ) ) . toBe ( 0 ) ;
134134 } ) ;
135135
136136 it ( 'should navigate to the first option on Home' , ( ) => {
137- const event = createKeyboardEvent ( 'keydown' , 36 , 'Home' ) ;
138137 const { listbox} = getDefaultPatterns ( {
139138 activeIndex : signal ( 8 ) ,
140139 } ) ;
141140 expect ( listbox . inputs . activeIndex ( ) ) . toBe ( 8 ) ;
142- listbox . onKeydown ( event ) ;
141+ listbox . onKeydown ( home ( ) ) ;
143142 expect ( listbox . inputs . activeIndex ( ) ) . toBe ( 0 ) ;
144143 } ) ;
145144
146145 it ( 'should navigate to the last option on End' , ( ) => {
147- const event = createKeyboardEvent ( 'keydown' , 35 , 'End' ) ;
148146 const { listbox} = getDefaultPatterns ( ) ;
149147 expect ( listbox . inputs . activeIndex ( ) ) . toBe ( 0 ) ;
150- listbox . onKeydown ( event ) ;
148+ listbox . onKeydown ( end ( ) ) ;
151149 expect ( listbox . inputs . activeIndex ( ) ) . toBe ( 8 ) ;
152150 } ) ;
153151 } ) ;
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+ } ) ;
154366} ) ;
0 commit comments