@@ -51,7 +51,7 @@ import {
51
51
ThemePalette ,
52
52
} from '@angular/material/core' ;
53
53
import { Subject } from 'rxjs' ;
54
- import { takeUntil } from 'rxjs/operators' ;
54
+ import { startWith , takeUntil } from 'rxjs/operators' ;
55
55
56
56
import { MatListAvatarCssMatStyler , MatListIconCssMatStyler } from './list' ;
57
57
@@ -97,7 +97,6 @@ export class MatSelectionListChange {
97
97
'(focus)' : '_handleFocus()' ,
98
98
'(blur)' : '_handleBlur()' ,
99
99
'(click)' : '_handleClick()' ,
100
- 'tabindex' : '-1' ,
101
100
'[class.mat-list-item-disabled]' : 'disabled' ,
102
101
'[class.mat-list-item-with-avatar]' : '_avatar || _icon' ,
103
102
// Manually set the "primary" or "warn" class if the color has been explicitly
@@ -110,6 +109,7 @@ export class MatSelectionListChange {
110
109
'[class.mat-warn]' : 'color === "warn"' ,
111
110
'[attr.aria-selected]' : 'selected' ,
112
111
'[attr.aria-disabled]' : 'disabled' ,
112
+ '[attr.tabindex]' : '-1' ,
113
113
} ,
114
114
templateUrl : 'list-option.html' ,
115
115
encapsulation : ViewEncapsulation . None ,
@@ -320,12 +320,13 @@ export class MatListOption extends _MatListOptionMixinBase implements AfterConte
320
320
inputs : [ 'disableRipple' ] ,
321
321
host : {
322
322
'role' : 'listbox' ,
323
- '[tabIndex]' : 'tabIndex' ,
324
323
'class' : 'mat-selection-list mat-list-base' ,
324
+ '(focus)' : '_onFocus()' ,
325
325
'(blur)' : '_onTouched()' ,
326
326
'(keydown)' : '_keydown($event)' ,
327
327
'aria-multiselectable' : 'true' ,
328
328
'[attr.aria-disabled]' : 'disabled.toString()' ,
329
+ '[attr.tabindex]' : '_tabIndex' ,
329
330
} ,
330
331
template : '<ng-content></ng-content>' ,
331
332
styleUrls : [ 'list.css' ] ,
@@ -346,7 +347,10 @@ export class MatSelectionList extends _MatSelectionListMixinBase implements CanD
346
347
@Output ( ) readonly selectionChange : EventEmitter < MatSelectionListChange > =
347
348
new EventEmitter < MatSelectionListChange > ( ) ;
348
349
349
- /** Tabindex of the selection list. */
350
+ /**
351
+ * Tabindex of the selection list.
352
+ * @breaking -change 11.0.0 Remove `tabIndex` input.
353
+ */
350
354
@Input ( ) tabIndex : number = 0 ;
351
355
352
356
/** Theme color of the selection list. This sets the checkbox color for all list options. */
@@ -376,6 +380,9 @@ export class MatSelectionList extends _MatSelectionListMixinBase implements CanD
376
380
/** The currently selected options. */
377
381
selectedOptions : SelectionModel < MatListOption > = new SelectionModel < MatListOption > ( true ) ;
378
382
383
+ /** The tabindex of the selection list. */
384
+ _tabIndex = - 1 ;
385
+
379
386
/** View to model callback that should be called whenever the selected options change. */
380
387
private _onChange : ( value : any ) => void = ( _ : any ) => { } ;
381
388
@@ -391,9 +398,11 @@ export class MatSelectionList extends _MatSelectionListMixinBase implements CanD
391
398
/** Whether the list has been destroyed. */
392
399
private _isDestroyed : boolean ;
393
400
394
- constructor ( private _element : ElementRef < HTMLElement > , @Attribute ( 'tabindex' ) tabIndex : string ) {
401
+ constructor ( private _element : ElementRef < HTMLElement > ,
402
+ // @breaking -change 11.0.0 Remove `tabIndex` parameter.
403
+ @Attribute ( 'tabindex' ) tabIndex : string ,
404
+ private _changeDetector : ChangeDetectorRef ) {
395
405
super ( ) ;
396
- this . tabIndex = parseInt ( tabIndex ) || 0 ;
397
406
}
398
407
399
408
ngAfterContentInit ( ) : void {
@@ -409,6 +418,16 @@ export class MatSelectionList extends _MatSelectionListMixinBase implements CanD
409
418
this . _setOptionsFromValues ( this . _value ) ;
410
419
}
411
420
421
+ // If the user attempts to tab out of the selection list, allow focus to escape.
422
+ this . _keyManager . tabOut . pipe ( takeUntil ( this . _destroyed ) ) . subscribe ( ( ) => {
423
+ this . _allowFocusEscape ( ) ;
424
+ } ) ;
425
+
426
+ // When the number of options change, update the tabindex of the selection list.
427
+ this . options . changes . pipe ( startWith ( null ) , takeUntil ( this . _destroyed ) ) . subscribe ( ( ) => {
428
+ this . _updateTabIndex ( ) ;
429
+ } ) ;
430
+
412
431
// Sync external changes to the model back to the options.
413
432
this . selectedOptions . changed . pipe ( takeUntil ( this . _destroyed ) ) . subscribe ( event => {
414
433
if ( event . added ) {
@@ -536,6 +555,22 @@ export class MatSelectionList extends _MatSelectionListMixinBase implements CanD
536
555
this . selectionChange . emit ( new MatSelectionListChange ( this , option ) ) ;
537
556
}
538
557
558
+ /**
559
+ * When the selection list is focused, we want to move focus to an option within the list. Do this
560
+ * by setting the appropriate option to be active.
561
+ */
562
+ _onFocus ( ) : void {
563
+ const activeIndex = this . _keyManager . activeItemIndex ;
564
+
565
+ if ( ! activeIndex || ( activeIndex === - 1 ) ) {
566
+ // If there is no active index, set focus to the first option.
567
+ this . _keyManager . setFirstItemActive ( ) ;
568
+ } else {
569
+ // Otherwise, set focus to the active option.
570
+ this . _keyManager . setActiveItem ( activeIndex ) ;
571
+ }
572
+ }
573
+
539
574
/** Implemented as part of ControlValueAccessor. */
540
575
writeValue ( values : string [ ] ) : void {
541
576
this . _value = values ;
@@ -640,6 +675,25 @@ export class MatSelectionList extends _MatSelectionListMixinBase implements CanD
640
675
}
641
676
}
642
677
678
+ /**
679
+ * Removes the tabindex from the selection list and resets it back afterwards, allowing the user
680
+ * to tab out of it. This prevents the list from capturing focus and redirecting it back within
681
+ * the list, creating a focus trap if it user tries to tab away.
682
+ */
683
+ private _allowFocusEscape ( ) {
684
+ this . _tabIndex = - 1 ;
685
+
686
+ setTimeout ( ( ) => {
687
+ this . _tabIndex = 0 ;
688
+ this . _changeDetector . markForCheck ( ) ;
689
+ } ) ;
690
+ }
691
+
692
+ /** Updates the tabindex based upon if the selection list is empty. */
693
+ private _updateTabIndex ( ) : void {
694
+ this . _tabIndex = ( this . options . length === 0 ) ? - 1 : 0 ;
695
+ }
696
+
643
697
static ngAcceptInputType_disabled : BooleanInput ;
644
698
static ngAcceptInputType_disableRipple : BooleanInput ;
645
699
}
0 commit comments