@@ -308,21 +308,271 @@ describe('CdkOption', () => {
308308 expect ( listboxInstance . _listKeyManager . activeItem ) . toEqual ( optionInstances [ 2 ] ) ;
309309 expect ( listboxInstance . _listKeyManager . activeItemIndex ) . toBe ( 2 ) ;
310310 } ) ;
311+
312+ it ( 'should update selected option on click event' , ( ) => {
313+ let selectedOptions = optionInstances . filter ( option => option . selected ) ;
314+
315+ expect ( selectedOptions . length ) . toBe ( 0 ) ;
316+ expect ( optionElements [ 0 ] . hasAttribute ( 'aria-selected' ) ) . toBeFalse ( ) ;
317+ expect ( optionInstances [ 0 ] . selected ) . toBeFalse ( ) ;
318+ expect ( fixture . componentInstance . changedOption ) . toBeUndefined ( ) ;
319+
320+ dispatchMouseEvent ( optionElements [ 0 ] , 'click' ) ;
321+ fixture . detectChanges ( ) ;
322+
323+ selectedOptions = optionInstances . filter ( option => option . selected ) ;
324+ expect ( selectedOptions . length ) . toBe ( 1 ) ;
325+ expect ( optionElements [ 0 ] . getAttribute ( 'aria-selected' ) ) . toBe ( 'true' ) ;
326+ expect ( optionInstances [ 0 ] . selected ) . toBeTrue ( ) ;
327+ expect ( fixture . componentInstance . changedOption ) . toBeDefined ( ) ;
328+ expect ( fixture . componentInstance . changedOption . id ) . toBe ( optionInstances [ 0 ] . id ) ;
329+ } ) ;
330+ } ) ;
331+
332+ describe ( 'with multiple selection' , ( ) => {
333+ let fixture : ComponentFixture < ListboxMultiselect > ;
334+
335+ let testComponent : ListboxMultiselect ;
336+
337+ let listbox : DebugElement ;
338+ let listboxInstance : CdkListbox ;
339+
340+ let options : DebugElement [ ] ;
341+ let optionInstances : CdkOption [ ] ;
342+ let optionElements : HTMLElement [ ] ;
343+
344+ beforeEach ( async ( ( ) => {
345+ TestBed . configureTestingModule ( {
346+ imports : [ CdkListboxModule ] ,
347+ declarations : [ ListboxMultiselect ] ,
348+ } ) . compileComponents ( ) ;
349+ } ) ) ;
350+
351+ beforeEach ( async ( ( ) => {
352+ fixture = TestBed . createComponent ( ListboxMultiselect ) ;
353+ fixture . detectChanges ( ) ;
354+
355+ testComponent = fixture . debugElement . componentInstance ;
356+
357+ listbox = fixture . debugElement . query ( By . directive ( CdkListbox ) ) ;
358+ listboxInstance = listbox . injector . get < CdkListbox > ( CdkListbox ) ;
359+
360+ options = fixture . debugElement . queryAll ( By . directive ( CdkOption ) ) ;
361+ optionInstances = options . map ( o => o . injector . get < CdkOption > ( CdkOption ) ) ;
362+ optionElements = options . map ( o => o . nativeElement ) ;
363+ } ) ) ;
364+
365+ it ( 'should select all options using the select all method' , ( ) => {
366+ let selectedOptions = optionInstances . filter ( option => option . selected ) ;
367+ testComponent . isMultiselectable = true ;
368+ fixture . detectChanges ( ) ;
369+
370+ expect ( selectedOptions . length ) . toBe ( 0 ) ;
371+ expect ( optionElements [ 0 ] . hasAttribute ( 'aria-selected' ) ) . toBeFalse ( ) ;
372+ expect ( optionInstances [ 0 ] . selected ) . toBeFalse ( ) ;
373+ expect ( fixture . componentInstance . changedOption ) . toBeUndefined ( ) ;
374+
375+ listboxInstance . setAllSelected ( true ) ;
376+ fixture . detectChanges ( ) ;
377+
378+ selectedOptions = optionInstances . filter ( option => option . selected ) ;
379+ expect ( selectedOptions . length ) . toBe ( 4 ) ;
380+
381+ for ( const option of optionElements ) {
382+ expect ( option . getAttribute ( 'aria-selected' ) ) . toBe ( 'true' ) ;
383+ }
384+
385+ expect ( fixture . componentInstance . changedOption ) . toBeDefined ( ) ;
386+ expect ( fixture . componentInstance . changedOption . id ) . toBe ( optionInstances [ 3 ] . id ) ;
387+ } ) ;
388+
389+ it ( 'should deselect previously selected when multiple is false' , ( ) => {
390+ let selectedOptions = optionInstances . filter ( option => option . selected ) ;
391+
392+ expect ( selectedOptions . length ) . toBe ( 0 ) ;
393+ expect ( optionElements [ 0 ] . hasAttribute ( 'aria-selected' ) ) . toBeFalse ( ) ;
394+ expect ( optionInstances [ 0 ] . selected ) . toBeFalse ( ) ;
395+ expect ( fixture . componentInstance . changedOption ) . toBeUndefined ( ) ;
396+
397+ dispatchMouseEvent ( optionElements [ 0 ] , 'click' ) ;
398+ fixture . detectChanges ( ) ;
399+
400+ selectedOptions = optionInstances . filter ( option => option . selected ) ;
401+ expect ( selectedOptions . length ) . toBe ( 1 ) ;
402+ expect ( optionElements [ 0 ] . getAttribute ( 'aria-selected' ) ) . toBe ( 'true' ) ;
403+ expect ( optionInstances [ 0 ] . selected ) . toBeTrue ( ) ;
404+ expect ( fixture . componentInstance . changedOption . id ) . toBe ( optionInstances [ 0 ] . id ) ;
405+
406+ dispatchMouseEvent ( optionElements [ 2 ] , 'click' ) ;
407+ fixture . detectChanges ( ) ;
408+
409+ selectedOptions = optionInstances . filter ( option => option . selected ) ;
410+ expect ( selectedOptions . length ) . toBe ( 1 ) ;
411+ expect ( optionElements [ 0 ] . hasAttribute ( 'aria-selected' ) ) . toBeFalse ( ) ;
412+ expect ( optionInstances [ 0 ] . selected ) . toBeFalse ( ) ;
413+ expect ( optionElements [ 2 ] . getAttribute ( 'aria-selected' ) ) . toBe ( 'true' ) ;
414+ expect ( optionInstances [ 2 ] . selected ) . toBeTrue ( ) ;
415+
416+ /** Expect first option to be most recently changed because it was deselected. */
417+ expect ( fixture . componentInstance . changedOption . id ) . toBe ( optionInstances [ 0 ] . id ) ;
418+ } ) ;
419+
420+ it ( 'should allow multiple selection when multiple is true' , ( ) => {
421+ let selectedOptions = optionInstances . filter ( option => option . selected ) ;
422+ testComponent . isMultiselectable = true ;
423+
424+ expect ( selectedOptions . length ) . toBe ( 0 ) ;
425+ expect ( fixture . componentInstance . changedOption ) . toBeUndefined ( ) ;
426+
427+ dispatchMouseEvent ( optionElements [ 0 ] , 'click' ) ;
428+ fixture . detectChanges ( ) ;
429+
430+ selectedOptions = optionInstances . filter ( option => option . selected ) ;
431+ expect ( selectedOptions . length ) . toBe ( 1 ) ;
432+ expect ( optionElements [ 0 ] . getAttribute ( 'aria-selected' ) ) . toBe ( 'true' ) ;
433+ expect ( optionInstances [ 0 ] . selected ) . toBeTrue ( ) ;
434+ expect ( fixture . componentInstance . changedOption . id ) . toBe ( optionInstances [ 0 ] . id ) ;
435+
436+ dispatchMouseEvent ( optionElements [ 2 ] , 'click' ) ;
437+ fixture . detectChanges ( ) ;
438+
439+ selectedOptions = optionInstances . filter ( option => option . selected ) ;
440+ expect ( selectedOptions . length ) . toBe ( 2 ) ;
441+ expect ( optionElements [ 0 ] . getAttribute ( 'aria-selected' ) ) . toBe ( 'true' ) ;
442+ expect ( optionInstances [ 0 ] . selected ) . toBeTrue ( ) ;
443+ expect ( optionElements [ 2 ] . getAttribute ( 'aria-selected' ) ) . toBe ( 'true' ) ;
444+ expect ( optionInstances [ 2 ] . selected ) . toBeTrue ( ) ;
445+ expect ( fixture . componentInstance . changedOption . id ) . toBe ( optionInstances [ 2 ] . id ) ;
446+ } ) ;
447+
448+ it ( 'should deselect all options when multiple switches to false' , ( ) => {
449+ let selectedOptions = optionInstances . filter ( option => option . selected ) ;
450+ testComponent . isMultiselectable = true ;
451+
452+ expect ( selectedOptions . length ) . toBe ( 0 ) ;
453+ expect ( fixture . componentInstance . changedOption ) . toBeUndefined ( ) ;
454+
455+ dispatchMouseEvent ( optionElements [ 0 ] , 'click' ) ;
456+ fixture . detectChanges ( ) ;
457+
458+ selectedOptions = optionInstances . filter ( option => option . selected ) ;
459+ expect ( selectedOptions . length ) . toBe ( 1 ) ;
460+ expect ( optionElements [ 0 ] . getAttribute ( 'aria-selected' ) ) . toBe ( 'true' ) ;
461+ expect ( optionInstances [ 0 ] . selected ) . toBeTrue ( ) ;
462+ expect ( fixture . componentInstance . changedOption . id ) . toBe ( optionInstances [ 0 ] . id ) ;
463+
464+ testComponent . isMultiselectable = false ;
465+ fixture . detectChanges ( ) ;
466+
467+ selectedOptions = optionInstances . filter ( option => option . selected ) ;
468+ expect ( selectedOptions . length ) . toBe ( 0 ) ;
469+ expect ( optionElements [ 0 ] . hasAttribute ( 'aria-selected' ) ) . toBeFalse ( ) ;
470+ expect ( optionInstances [ 0 ] . selected ) . toBeFalse ( ) ;
471+ expect ( fixture . componentInstance . changedOption . id ) . toBe ( optionInstances [ 0 ] . id ) ;
472+ } ) ;
311473 } ) ;
312474
475+ describe ( 'with aria active descendant' , ( ) => {
476+ let fixture : ComponentFixture < ListboxActiveDescendant > ;
477+
478+ let testComponent : ListboxActiveDescendant ;
479+
480+ let listbox : DebugElement ;
481+ let listboxInstance : CdkListbox ;
482+ let listboxElement : HTMLElement ;
483+
484+ let options : DebugElement [ ] ;
485+ let optionInstances : CdkOption [ ] ;
486+ let optionElements : HTMLElement [ ] ;
487+
488+ beforeEach ( async ( ( ) => {
489+ TestBed . configureTestingModule ( {
490+ imports : [ CdkListboxModule ] ,
491+ declarations : [ ListboxActiveDescendant ] ,
492+ } ) . compileComponents ( ) ;
493+ } ) ) ;
494+
495+ beforeEach ( async ( ( ) => {
496+ fixture = TestBed . createComponent ( ListboxActiveDescendant ) ;
497+ fixture . detectChanges ( ) ;
498+
499+ testComponent = fixture . debugElement . componentInstance ;
500+
501+ listbox = fixture . debugElement . query ( By . directive ( CdkListbox ) ) ;
502+ listboxInstance = listbox . injector . get < CdkListbox > ( CdkListbox ) ;
503+ listboxElement = listbox . nativeElement ;
504+
505+ options = fixture . debugElement . queryAll ( By . directive ( CdkOption ) ) ;
506+ optionInstances = options . map ( o => o . injector . get < CdkOption > ( CdkOption ) ) ;
507+ optionElements = options . map ( o => o . nativeElement ) ;
508+ } ) ) ;
509+
510+ it ( 'should update aria active descendant when enabled' , ( ) => {
511+ expect ( listboxElement . hasAttribute ( 'aria-activedescendant' ) ) . toBeFalse ( ) ;
512+
513+ listboxInstance . setActiveOption ( optionInstances [ 0 ] ) ;
514+ fixture . detectChanges ( ) ;
515+
516+ expect ( listboxElement . hasAttribute ( 'aria-activedescendant' ) ) . toBeTrue ( ) ;
517+ expect ( listboxElement . getAttribute ( 'aria-activedescendant' ) ) . toBe ( optionElements [ 0 ] . id ) ;
518+
519+ listboxInstance . setActiveOption ( optionInstances [ 2 ] ) ;
520+ fixture . detectChanges ( ) ;
521+
522+ expect ( listboxElement . hasAttribute ( 'aria-activedescendant' ) ) . toBeTrue ( ) ;
523+ expect ( listboxElement . getAttribute ( 'aria-activedescendant' ) ) . toBe ( optionElements [ 2 ] . id ) ;
524+ } ) ;
525+
526+ it ( 'should update aria active descendant via arrow keys' , ( ) => {
527+ expect ( listboxElement . hasAttribute ( 'aria-activedescendant' ) ) . toBeFalse ( ) ;
528+
529+ dispatchKeyboardEvent ( listboxElement , 'keydown' , DOWN_ARROW ) ;
530+ fixture . detectChanges ( ) ;
531+
532+ expect ( listboxElement . hasAttribute ( 'aria-activedescendant' ) ) . toBeTrue ( ) ;
533+ expect ( listboxElement . getAttribute ( 'aria-activedescendant' ) ) . toBe ( optionElements [ 0 ] . id ) ;
534+
535+ dispatchKeyboardEvent ( listboxElement , 'keydown' , DOWN_ARROW ) ;
536+ fixture . detectChanges ( ) ;
537+
538+ expect ( listboxElement . hasAttribute ( 'aria-activedescendant' ) ) . toBeTrue ( ) ;
539+ expect ( listboxElement . getAttribute ( 'aria-activedescendant' ) ) . toBe ( optionElements [ 1 ] . id ) ;
540+ } ) ;
541+
542+ it ( 'should place focus on options and not set active descendant' , ( ) => {
543+ testComponent . isActiveDescendant = false ;
544+ fixture . detectChanges ( ) ;
545+
546+ expect ( listboxElement . hasAttribute ( 'aria-activedescendant' ) ) . toBeFalse ( ) ;
547+
548+ dispatchKeyboardEvent ( listboxElement , 'keydown' , DOWN_ARROW ) ;
549+ fixture . detectChanges ( ) ;
550+
551+ expect ( listboxElement . hasAttribute ( 'aria-activedescendant' ) ) . toBeFalse ( ) ;
552+ expect ( document . activeElement ) . toEqual ( optionElements [ 0 ] ) ;
553+ dispatchKeyboardEvent ( listboxElement , 'keydown' , DOWN_ARROW ) ;
554+ fixture . detectChanges ( ) ;
555+
556+ expect ( listboxElement . hasAttribute ( 'aria-activedescendant' ) ) . toBeFalse ( ) ;
557+ expect ( document . activeElement ) . toEqual ( optionElements [ 1 ] ) ;
558+
559+ } ) ;
560+ } ) ;
313561} ) ;
314562
315563@Component ( {
316564 template : `
317565 <div cdkListbox
318- [disabled]="isListboxDisabled"
319- (selectionChange)="onSelectionChange($event)">
566+ [disabled]="isListboxDisabled"
567+ (selectionChange)="onSelectionChange($event)">
320568 <div cdkOption
321569 [disabled]="isPurpleDisabled">
322- Purple</div>
570+ Purple
571+ </div>
323572 <div cdkOption
324573 [disabled]="isSolarDisabled">
325- Solar</div>
574+ Solar
575+ </div>
326576 <div cdkOption>Arc</div>
327577 <div cdkOption>Stasis</div>
328578 </div>`
@@ -337,3 +587,47 @@ class ListboxWithOptions {
337587 this . changedOption = event . option ;
338588 }
339589}
590+
591+ @Component ( {
592+ template : `
593+ <div cdkListbox
594+ [multiple]="isMultiselectable"
595+ (selectionChange)="onSelectionChange($event)">
596+ <div cdkOption>Purple</div>
597+ <div cdkOption>Solar</div>
598+ <div cdkOption>Arc</div>
599+ <div cdkOption>Stasis</div>
600+ </div>`
601+ } )
602+ class ListboxMultiselect {
603+ changedOption : CdkOption ;
604+ isMultiselectable : boolean = false ;
605+
606+ onSelectionChange ( event : ListboxSelectionChangeEvent ) {
607+ this . changedOption = event . option ;
608+ }
609+ }
610+
611+ @Component ( {
612+ template : `
613+ <div cdkListbox
614+ [useActiveDescendant]="isActiveDescendant">
615+ <div cdkOption>Purple</div>
616+ <div cdkOption>Solar</div>
617+ <div cdkOption>Arc</div>
618+ <div cdkOption>Stasis</div>
619+ </div>`
620+ } )
621+ class ListboxActiveDescendant {
622+ changedOption : CdkOption ;
623+ isActiveDescendant : boolean = true ;
624+ focusedOption : string ;
625+
626+ onSelectionChange ( event : ListboxSelectionChangeEvent ) {
627+ this . changedOption = event . option ;
628+ }
629+
630+ onFocus ( option : string ) {
631+ this . focusedOption = option ;
632+ }
633+ }
0 commit comments