@@ -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/test-element' ; 
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 ( [ 'Apple' ,  'Apricot' ] ) ; 
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 ( [ 'Apple' ,  'Apricot' ,  'Banana' ] ) ; 
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 ( [ 'Apple' ,  'Apricot' ,  'Banana' ,  'Blackberry' ] ) ; 
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