@@ -53,7 +53,7 @@ import {
5353} from '@angular/material/core' ;
5454
5555import { Subject } from 'rxjs' ;
56- import { takeUntil } from 'rxjs/operators' ;
56+ import { startWith , takeUntil } from 'rxjs/operators' ;
5757
5858import { MatListAvatarCssMatStyler , MatListIconCssMatStyler } from './list' ;
5959
@@ -99,7 +99,6 @@ export class MatSelectionListChange {
9999 '(focus)' : '_handleFocus()' ,
100100 '(blur)' : '_handleBlur()' ,
101101 '(click)' : '_handleClick()' ,
102- 'tabindex' : '-1' ,
103102 '[class.mat-list-item-disabled]' : 'disabled' ,
104103 '[class.mat-list-item-with-avatar]' : '_avatar || _icon' ,
105104 // Manually set the "primary" or "warn" class if the color has been explicitly
@@ -113,6 +112,7 @@ export class MatSelectionListChange {
113112 '[class.mat-list-single-selected-option]' : 'selected && !selectionList.multiple' ,
114113 '[attr.aria-selected]' : 'selected' ,
115114 '[attr.aria-disabled]' : 'disabled' ,
115+ '[attr.tabindex]' : '-1' ,
116116 } ,
117117 templateUrl : 'list-option.html' ,
118118 encapsulation : ViewEncapsulation . None ,
@@ -323,12 +323,13 @@ export class MatListOption extends _MatListOptionMixinBase implements AfterConte
323323 inputs : [ 'disableRipple' ] ,
324324 host : {
325325 'role' : 'listbox' ,
326- '[tabIndex]' : 'tabIndex' ,
327326 'class' : 'mat-selection-list mat-list-base' ,
327+ '(focus)' : '_onFocus()' ,
328328 '(blur)' : '_onTouched()' ,
329329 '(keydown)' : '_keydown($event)' ,
330330 '[attr.aria-multiselectable]' : 'multiple' ,
331331 '[attr.aria-disabled]' : 'disabled.toString()' ,
332+ '[attr.tabindex]' : '_tabIndex' ,
332333 } ,
333334 template : '<ng-content></ng-content>' ,
334335 styleUrls : [ 'list.css' ] ,
@@ -351,7 +352,10 @@ export class MatSelectionList extends _MatSelectionListMixinBase implements CanD
351352 @Output ( ) readonly selectionChange : EventEmitter < MatSelectionListChange > =
352353 new EventEmitter < MatSelectionListChange > ( ) ;
353354
354- /** Tabindex of the selection list. */
355+ /**
356+ * Tabindex of the selection list.
357+ * @breaking -change 11.0.0 Remove `tabIndex` input.
358+ */
355359 @Input ( ) tabIndex : number = 0 ;
356360
357361 /** Theme color of the selection list. This sets the checkbox color for all list options. */
@@ -398,6 +402,9 @@ export class MatSelectionList extends _MatSelectionListMixinBase implements CanD
398402 /** The currently selected options. */
399403 selectedOptions = new SelectionModel < MatListOption > ( this . _multiple ) ;
400404
405+ /** The tabindex of the selection list. */
406+ _tabIndex = - 1 ;
407+
401408 /** View to model callback that should be called whenever the selected options change. */
402409 private _onChange : ( value : any ) => void = ( _ : any ) => { } ;
403410
@@ -413,9 +420,11 @@ export class MatSelectionList extends _MatSelectionListMixinBase implements CanD
413420 /** Whether the list has been destroyed. */
414421 private _isDestroyed : boolean ;
415422
416- constructor ( private _element : ElementRef < HTMLElement > , @Attribute ( 'tabindex' ) tabIndex : string ) {
423+ constructor ( private _element : ElementRef < HTMLElement > ,
424+ // @breaking -change 11.0.0 Remove `tabIndex` parameter.
425+ @Attribute ( 'tabindex' ) tabIndex : string ,
426+ private _changeDetector : ChangeDetectorRef ) {
417427 super ( ) ;
418- this . tabIndex = parseInt ( tabIndex ) || 0 ;
419428 }
420429
421430 ngAfterContentInit ( ) : void {
@@ -433,6 +442,16 @@ export class MatSelectionList extends _MatSelectionListMixinBase implements CanD
433442 this . _setOptionsFromValues ( this . _value ) ;
434443 }
435444
445+ // If the user attempts to tab out of the selection list, allow focus to escape.
446+ this . _keyManager . tabOut . pipe ( takeUntil ( this . _destroyed ) ) . subscribe ( ( ) => {
447+ this . _allowFocusEscape ( ) ;
448+ } ) ;
449+
450+ // When the number of options change, update the tabindex of the selection list.
451+ this . options . changes . pipe ( startWith ( null ) , takeUntil ( this . _destroyed ) ) . subscribe ( ( ) => {
452+ this . _updateTabIndex ( ) ;
453+ } ) ;
454+
436455 // Sync external changes to the model back to the options.
437456 this . selectedOptions . changed . pipe ( takeUntil ( this . _destroyed ) ) . subscribe ( event => {
438457 if ( event . added ) {
@@ -560,6 +579,22 @@ export class MatSelectionList extends _MatSelectionListMixinBase implements CanD
560579 this . selectionChange . emit ( new MatSelectionListChange ( this , option ) ) ;
561580 }
562581
582+ /**
583+ * When the selection list is focused, we want to move focus to an option within the list. Do this
584+ * by setting the appropriate option to be active.
585+ */
586+ _onFocus ( ) : void {
587+ const activeIndex = this . _keyManager . activeItemIndex ;
588+
589+ if ( ! activeIndex || ( activeIndex === - 1 ) ) {
590+ // If there is no active index, set focus to the first option.
591+ this . _keyManager . setFirstItemActive ( ) ;
592+ } else {
593+ // Otherwise, set focus to the active option.
594+ this . _keyManager . setActiveItem ( activeIndex ) ;
595+ }
596+ }
597+
563598 /** Implemented as part of ControlValueAccessor. */
564599 writeValue ( values : string [ ] ) : void {
565600 this . _value = values ;
@@ -664,6 +699,25 @@ export class MatSelectionList extends _MatSelectionListMixinBase implements CanD
664699 }
665700 }
666701
702+ /**
703+ * Removes the tabindex from the selection list and resets it back afterwards, allowing the user
704+ * to tab out of it. This prevents the list from capturing focus and redirecting it back within
705+ * the list, creating a focus trap if it user tries to tab away.
706+ */
707+ private _allowFocusEscape ( ) {
708+ this . _tabIndex = - 1 ;
709+
710+ setTimeout ( ( ) => {
711+ this . _tabIndex = 0 ;
712+ this . _changeDetector . markForCheck ( ) ;
713+ } ) ;
714+ }
715+
716+ /** Updates the tabindex based upon if the selection list is empty. */
717+ private _updateTabIndex ( ) : void {
718+ this . _tabIndex = ( this . options . length === 0 ) ? - 1 : 0 ;
719+ }
720+
667721 static ngAcceptInputType_disabled : BooleanInput ;
668722 static ngAcceptInputType_disableRipple : BooleanInput ;
669723 static ngAcceptInputType_multiple : BooleanInput ;
0 commit comments