@@ -3,7 +3,17 @@ import {Component, Type} from '@angular/core';
33import { By } from '@angular/platform-browser' ;
44import { CdkListbox , CdkListboxModule , CdkOption , ListboxValueChangeEvent } from './index' ;
55import { dispatchKeyboardEvent , dispatchMouseEvent } from '../../cdk/testing/private' ;
6- import { B , DOWN_ARROW , END , HOME , SPACE , UP_ARROW } from '@angular/cdk/keycodes' ;
6+ import {
7+ A ,
8+ B ,
9+ DOWN_ARROW ,
10+ END ,
11+ HOME ,
12+ LEFT_ARROW ,
13+ RIGHT_ARROW ,
14+ SPACE ,
15+ UP_ARROW ,
16+ } from '@angular/cdk/keycodes' ;
717import { FormControl , ReactiveFormsModule } from '@angular/forms' ;
818import { CommonModule } from '@angular/common' ;
919
@@ -132,6 +142,27 @@ describe('CdkOption and CdkListbox', () => {
132142 expect ( fixture . componentInstance . changedOption ?. id ) . toBe ( options [ 0 ] . id ) ;
133143 } ) ;
134144
145+ it ( 'should select and deselect range on option SHIFT + click' , async ( ) => {
146+ const { testComponent, fixture, listbox, optionEls} = await setupComponent ( ListboxWithOptions ) ;
147+ testComponent . isMultiselectable = true ;
148+ fixture . detectChanges ( ) ;
149+
150+ dispatchMouseEvent ( optionEls [ 1 ] , 'click' , undefined , undefined , undefined , { shift : true } ) ;
151+ fixture . detectChanges ( ) ;
152+
153+ expect ( listbox . value ) . toEqual ( [ 'orange' ] ) ;
154+
155+ dispatchMouseEvent ( optionEls [ 3 ] , 'click' , undefined , undefined , undefined , { shift : true } ) ;
156+ fixture . detectChanges ( ) ;
157+
158+ expect ( listbox . value ) . toEqual ( [ 'orange' , 'banana' , 'peach' ] ) ;
159+
160+ dispatchMouseEvent ( optionEls [ 2 ] , 'click' , undefined , undefined , undefined , { shift : true } ) ;
161+ fixture . detectChanges ( ) ;
162+
163+ expect ( listbox . value ) . toEqual ( [ 'orange' ] ) ;
164+ } ) ;
165+
135166 it ( 'should update on option activated via keyboard' , async ( ) => {
136167 const { fixture, listbox, listboxEl, options, optionEls} = await setupComponent (
137168 ListboxWithOptions ,
@@ -260,20 +291,19 @@ describe('CdkOption and CdkListbox', () => {
260291 expect ( options [ 1 ] . isSelected ( ) ) . toBeFalse ( ) ;
261292 } ) ;
262293
263- // TODO(mmalerba): Fix this case.
264- // Currently banana gets booted because the option isn't loaded yet,
265- // but then when the option loads the value is already lost.
266- // it('should allow binding to listbox value', async () => {
267- // const {testComponent, fixture, listbox, options} = await setupComponent(ListboxWithBoundValue);
268- // expect(listbox.value).toEqual(['banana']);
269- // expect(options[2].isSelected()).toBeTrue();
270- //
271- // testComponent.value = ['orange'];
272- // fixture.detectChanges();
273- //
274- // expect(listbox.value).toEqual(['orange']);
275- // expect(options[1].isSelected()).toBeTrue();
276- // });
294+ it ( 'should allow binding to listbox value' , async ( ) => {
295+ const { testComponent, fixture, listbox, options} = await setupComponent (
296+ ListboxWithBoundValue ,
297+ ) ;
298+ expect ( listbox . value ) . toEqual ( [ 'banana' ] ) ;
299+ expect ( options [ 2 ] . isSelected ( ) ) . toBeTrue ( ) ;
300+
301+ testComponent . value = [ 'orange' ] ;
302+ fixture . detectChanges ( ) ;
303+
304+ expect ( listbox . value ) . toEqual ( [ 'orange' ] ) ;
305+ expect ( options [ 1 ] . isSelected ( ) ) . toBeTrue ( ) ;
306+ } ) ;
277307 } ) ;
278308
279309 describe ( 'disabled state' , ( ) => {
@@ -482,7 +512,105 @@ describe('CdkOption and CdkListbox', () => {
482512 expect ( fixture . componentInstance . changedOption ?. id ) . toBe ( options [ 1 ] . id ) ;
483513 } ) ;
484514
485- // TODO(mmalerba): ensure all keys covered
515+ it ( 'should update active item on arrow key presses in horizontal mode' , async ( ) => {
516+ const { testComponent, fixture, listbox, listboxEl, options} = await setupComponent (
517+ ListboxWithOptions ,
518+ ) ;
519+ testComponent . orientation = 'horizontal' ;
520+ fixture . detectChanges ( ) ;
521+
522+ expect ( listboxEl . getAttribute ( 'aria-orientation' ) ) . toBe ( 'horizontal' ) ;
523+
524+ listbox . focus ( ) ;
525+ dispatchKeyboardEvent ( listboxEl , 'keydown' , RIGHT_ARROW ) ;
526+ fixture . detectChanges ( ) ;
527+
528+ expect ( options [ 1 ] . isActive ( ) ) . toBeTrue ( ) ;
529+
530+ dispatchKeyboardEvent ( listboxEl , 'keydown' , LEFT_ARROW ) ;
531+ fixture . detectChanges ( ) ;
532+
533+ expect ( options [ 0 ] . isActive ( ) ) . toBeTrue ( ) ;
534+ } ) ;
535+
536+ it ( 'should select and deselect all option with CONTROL + A' , async ( ) => {
537+ const { testComponent, fixture, listbox, listboxEl} = await setupComponent ( ListboxWithOptions ) ;
538+ testComponent . isMultiselectable = true ;
539+ fixture . detectChanges ( ) ;
540+
541+ listbox . focus ( ) ;
542+ dispatchKeyboardEvent ( listboxEl , 'keydown' , A , undefined , { control : true } ) ;
543+ fixture . detectChanges ( ) ;
544+
545+ expect ( listbox . value ) . toEqual ( [ 'apple' , 'orange' , 'banana' , 'peach' ] ) ;
546+
547+ dispatchKeyboardEvent ( listboxEl , 'keydown' , A , undefined , { control : true } ) ;
548+ fixture . detectChanges ( ) ;
549+
550+ expect ( listbox . value ) . toEqual ( [ ] ) ;
551+ } ) ;
552+
553+ it ( 'should select and deselect range with CONTROL + SPACE' , async ( ) => {
554+ const { testComponent, fixture, listbox, listboxEl} = await setupComponent ( ListboxWithOptions ) ;
555+ testComponent . isMultiselectable = true ;
556+ fixture . detectChanges ( ) ;
557+
558+ listbox . focus ( ) ;
559+ dispatchKeyboardEvent ( listboxEl , 'keydown' , DOWN_ARROW ) ;
560+ dispatchKeyboardEvent ( listboxEl , 'keydown' , SPACE , undefined , { shift : true } ) ;
561+ fixture . detectChanges ( ) ;
562+
563+ expect ( listbox . value ) . toEqual ( [ 'orange' ] ) ;
564+
565+ dispatchKeyboardEvent ( listboxEl , 'keydown' , DOWN_ARROW ) ;
566+ dispatchKeyboardEvent ( listboxEl , 'keydown' , DOWN_ARROW ) ;
567+ dispatchKeyboardEvent ( listboxEl , 'keydown' , SPACE , undefined , { shift : true } ) ;
568+ fixture . detectChanges ( ) ;
569+
570+ expect ( listbox . value ) . toEqual ( [ 'orange' , 'banana' , 'peach' ] ) ;
571+
572+ dispatchKeyboardEvent ( listboxEl , 'keydown' , UP_ARROW ) ;
573+ dispatchKeyboardEvent ( listboxEl , 'keydown' , SPACE , undefined , { shift : true } ) ;
574+
575+ expect ( listbox . value ) . toEqual ( [ 'orange' ] ) ;
576+ } ) ;
577+
578+ it ( 'should select and deselect range with CONTROL + SHIFT + HOME' , async ( ) => {
579+ const { testComponent, fixture, listbox, listboxEl} = await setupComponent ( ListboxWithOptions ) ;
580+ testComponent . isMultiselectable = true ;
581+ listbox . focus ( ) ;
582+ fixture . detectChanges ( ) ;
583+
584+ dispatchKeyboardEvent ( listboxEl , 'keydown' , DOWN_ARROW ) ;
585+ dispatchKeyboardEvent ( listboxEl , 'keydown' , DOWN_ARROW ) ;
586+ dispatchKeyboardEvent ( listboxEl , 'keydown' , HOME , undefined , { control : true , shift : true } ) ;
587+
588+ expect ( listbox . value ) . toEqual ( [ 'apple' , 'orange' , 'banana' ] ) ;
589+
590+ dispatchKeyboardEvent ( listboxEl , 'keydown' , DOWN_ARROW ) ;
591+ dispatchKeyboardEvent ( listboxEl , 'keydown' , DOWN_ARROW ) ;
592+ dispatchKeyboardEvent ( listboxEl , 'keydown' , HOME , undefined , { control : true , shift : true } ) ;
593+
594+ expect ( listbox . value ) . toEqual ( [ ] ) ;
595+ } ) ;
596+
597+ it ( 'should select and deselect range with CONTROL + SHIFT + END' , async ( ) => {
598+ const { testComponent, fixture, listbox, listboxEl} = await setupComponent ( ListboxWithOptions ) ;
599+ testComponent . isMultiselectable = true ;
600+ listbox . focus ( ) ;
601+ fixture . detectChanges ( ) ;
602+
603+ dispatchKeyboardEvent ( listboxEl , 'keydown' , DOWN_ARROW ) ;
604+ dispatchKeyboardEvent ( listboxEl , 'keydown' , END , undefined , { control : true , shift : true } ) ;
605+
606+ expect ( listbox . value ) . toEqual ( [ 'orange' , 'banana' , 'peach' ] ) ;
607+
608+ dispatchKeyboardEvent ( listboxEl , 'keydown' , UP_ARROW ) ;
609+ dispatchKeyboardEvent ( listboxEl , 'keydown' , UP_ARROW ) ;
610+ dispatchKeyboardEvent ( listboxEl , 'keydown' , END , undefined , { control : true , shift : true } ) ;
611+
612+ expect ( listbox . value ) . toEqual ( [ ] ) ;
613+ } ) ;
486614 } ) ;
487615
488616 describe ( 'with roving tabindex' , ( ) => {
@@ -639,15 +767,15 @@ describe('CdkOption and CdkListbox', () => {
639767 subscription . unsubscribe ( ) ;
640768 } ) ;
641769
642- it ( 'should have FormControl error multiple values selected in single-select listbox' , async ( ) => {
770+ it ( 'should have FormControl error when multiple values selected in single-select listbox' , async ( ) => {
643771 const { testComponent, fixture} = await setupComponent ( ListboxWithFormControl , [
644772 ReactiveFormsModule ,
645773 ] ) ;
646774 testComponent . formControl . setValue ( [ 'orange' , 'banana' ] ) ;
647775 fixture . detectChanges ( ) ;
648776
649- expect ( testComponent . formControl . hasError ( 'cdkListboxMultipleValues ' ) ) . toBeTrue ( ) ;
650- expect ( testComponent . formControl . hasError ( 'cdkListboxInvalidValues ' ) ) . toBeFalse ( ) ;
777+ expect ( testComponent . formControl . hasError ( 'cdkListboxUnexpectedMultipleValues ' ) ) . toBeTrue ( ) ;
778+ expect ( testComponent . formControl . hasError ( 'cdkListboxUnexpectedOptionValues ' ) ) . toBeFalse ( ) ;
651779 } ) ;
652780
653781 it ( 'should have FormControl error when non-option value selected' , async ( ) => {
@@ -658,9 +786,9 @@ describe('CdkOption and CdkListbox', () => {
658786 testComponent . formControl . setValue ( [ 'orange' , 'dragonfruit' , 'mango' ] ) ;
659787 fixture . detectChanges ( ) ;
660788
661- expect ( testComponent . formControl . hasError ( 'cdkListboxInvalidValues ' ) ) . toBeTrue ( ) ;
662- expect ( testComponent . formControl . hasError ( 'cdkListboxMultipleValues ' ) ) . toBeFalse ( ) ;
663- expect ( testComponent . formControl . errors ?. [ 'cdkListboxInvalidValues ' ] ) . toEqual ( {
789+ expect ( testComponent . formControl . hasError ( 'cdkListboxUnexpectedOptionValues ' ) ) . toBeTrue ( ) ;
790+ expect ( testComponent . formControl . hasError ( 'cdkListboxUnexpectedMultipleValues ' ) ) . toBeFalse ( ) ;
791+ expect ( testComponent . formControl . errors ?. [ 'cdkListboxUnexpectedOptionValues ' ] ) . toEqual ( {
664792 'values' : [ 'dragonfruit' , 'mango' ] ,
665793 } ) ;
666794 } ) ;
@@ -672,9 +800,9 @@ describe('CdkOption and CdkListbox', () => {
672800 testComponent . formControl . setValue ( [ 'dragonfruit' , 'mango' ] ) ;
673801 fixture . detectChanges ( ) ;
674802
675- expect ( testComponent . formControl . hasError ( 'cdkListboxInvalidValues ' ) ) . toBeTrue ( ) ;
676- expect ( testComponent . formControl . hasError ( 'cdkListboxMultipleValues ' ) ) . toBeTrue ( ) ;
677- expect ( testComponent . formControl . errors ?. [ 'cdkListboxInvalidValues ' ] ) . toEqual ( {
803+ expect ( testComponent . formControl . hasError ( 'cdkListboxUnexpectedOptionValues ' ) ) . toBeTrue ( ) ;
804+ expect ( testComponent . formControl . hasError ( 'cdkListboxUnexpectedMultipleValues ' ) ) . toBeTrue ( ) ;
805+ expect ( testComponent . formControl . errors ?. [ 'cdkListboxUnexpectedOptionValues ' ] ) . toEqual ( {
678806 'values' : [ 'dragonfruit' , 'mango' ] ,
679807 } ) ;
680808 } ) ;
@@ -689,6 +817,7 @@ describe('CdkOption and CdkListbox', () => {
689817 [cdkListboxMultiple]="isMultiselectable"
690818 [cdkListboxDisabled]="isListboxDisabled"
691819 [cdkListboxUseActiveDescendant]="isActiveDescendant"
820+ [cdkListboxOrientation]="orientation"
692821 (cdkListboxValueChange)="onSelectionChange($event)">
693822 <div cdkOption="apple"
694823 [cdkOptionDisabled]="isAppleDisabled"
@@ -703,7 +832,7 @@ describe('CdkOption and CdkListbox', () => {
703832 ` ,
704833} )
705834class ListboxWithOptions {
706- changedOption : CdkOption ;
835+ changedOption : CdkOption | null ;
707836 isListboxDisabled = false ;
708837 isAppleDisabled = false ;
709838 isOrangeDisabled = false ;
@@ -713,6 +842,7 @@ class ListboxWithOptions {
713842 listboxTabindex : number ;
714843 appleId : string ;
715844 appleTabindex : number ;
845+ orientation : 'horizontal' | 'vertical' = 'vertical' ;
716846
717847 onSelectionChange ( event : ListboxValueChangeEvent < unknown > ) {
718848 this . changedOption = event . option ;
@@ -755,20 +885,20 @@ class ListboxWithFormControl {
755885} )
756886class ListboxWithCustomTypeahead { }
757887
758- // @Component ({
759- // template: `
760- // <div cdkListbox
761- // [cdkListboxValue]="value">
762- // <div cdkOption="apple">Apple</div>
763- // <div cdkOption="orange">Orange</div>
764- // <div cdkOption="banana">Banana</div>
765- // <div cdkOption="peach">Peach</div>
766- // </div>
767- // `,
768- // })
769- // class ListboxWithBoundValue {
770- // value = ['banana'];
771- // }
888+ @Component ( {
889+ template : `
890+ <div cdkListbox
891+ [cdkListboxValue]="value">
892+ <div cdkOption="apple">Apple</div>
893+ <div cdkOption="orange">Orange</div>
894+ <div cdkOption="banana">Banana</div>
895+ <div cdkOption="peach">Peach</div>
896+ </div>
897+ ` ,
898+ } )
899+ class ListboxWithBoundValue {
900+ value = [ 'banana' ] ;
901+ }
772902
773903@Component ( {
774904 template : `
0 commit comments