88
99import { FocusMonitor } from '@angular/cdk/a11y' ;
1010import { SelectionModel } from '@angular/cdk/collections' ;
11+ import { DOWN_ARROW , LEFT_ARROW , RIGHT_ARROW , UP_ARROW , SPACE , ENTER } from '@angular/cdk/keycodes' ;
1112import {
1213 AfterContentInit ,
1314 Attribute ,
@@ -32,6 +33,7 @@ import {
3233 AfterViewInit ,
3334 booleanAttribute ,
3435} from '@angular/core' ;
36+ import { Direction , Directionality } from '@angular/cdk/bidi' ;
3537import { ControlValueAccessor , NG_VALUE_ACCESSOR } from '@angular/forms' ;
3638import { MatRipple , MatPseudoCheckbox } from '@angular/material/core' ;
3739
@@ -121,8 +123,9 @@ export class MatButtonToggleChange {
121123 { provide : MAT_BUTTON_TOGGLE_GROUP , useExisting : MatButtonToggleGroup } ,
122124 ] ,
123125 host : {
124- 'role' : 'group' ,
125126 'class' : 'mat-button-toggle-group' ,
127+ '(keydown)' : '_keydown($event)' ,
128+ '[role]' : "multiple ? 'group' : 'radiogroup'" ,
126129 '[attr.aria-disabled]' : 'disabled' ,
127130 '[class.mat-button-toggle-vertical]' : 'vertical' ,
128131 '[class.mat-button-toggle-group-appearance-standard]' : 'appearance === "standard"' ,
@@ -226,6 +229,11 @@ export class MatButtonToggleGroup implements ControlValueAccessor, OnInit, After
226229 this . _markButtonsForCheck ( ) ;
227230 }
228231
232+ /** The layout direction of the toggle button group. */
233+ get dir ( ) : Direction {
234+ return this . _dir && this . _dir . value === 'rtl' ? 'rtl' : 'ltr' ;
235+ }
236+
229237 /** Event emitted when the group's value changes. */
230238 @Output ( ) readonly change : EventEmitter < MatButtonToggleChange > =
231239 new EventEmitter < MatButtonToggleChange > ( ) ;
@@ -257,6 +265,7 @@ export class MatButtonToggleGroup implements ControlValueAccessor, OnInit, After
257265 @Optional ( )
258266 @Inject ( MAT_BUTTON_TOGGLE_DEFAULT_OPTIONS )
259267 defaultOptions ?: MatButtonToggleDefaultOptions ,
268+ @Optional ( ) private _dir ?: Directionality ,
260269 ) {
261270 this . appearance =
262271 defaultOptions && defaultOptions . appearance ? defaultOptions . appearance : 'standard' ;
@@ -270,6 +279,9 @@ export class MatButtonToggleGroup implements ControlValueAccessor, OnInit, After
270279
271280 ngAfterContentInit ( ) {
272281 this . _selectionModel . select ( ...this . _buttonToggles . filter ( toggle => toggle . checked ) ) ;
282+ if ( ! this . multiple ) {
283+ this . _initializeTabIndex ( ) ;
284+ }
273285 }
274286
275287 /**
@@ -296,6 +308,49 @@ export class MatButtonToggleGroup implements ControlValueAccessor, OnInit, After
296308 this . disabled = isDisabled ;
297309 }
298310
311+ /** Handle keydown event calling to single-select button toggle. */
312+ protected _keydown ( event : KeyboardEvent ) {
313+ if ( this . multiple || this . disabled ) {
314+ return ;
315+ }
316+
317+ const target = event . target as HTMLButtonElement ;
318+ const buttonId = target . id ;
319+ const index = this . _buttonToggles . toArray ( ) . findIndex ( toggle => {
320+ return toggle . buttonId === buttonId ;
321+ } ) ;
322+
323+ let nextButton ;
324+ switch ( event . keyCode ) {
325+ case SPACE :
326+ case ENTER :
327+ nextButton = this . _buttonToggles . get ( index ) ;
328+ break ;
329+ case UP_ARROW :
330+ nextButton = this . _buttonToggles . get ( this . _getNextIndex ( index , - 1 ) ) ;
331+ break ;
332+ case LEFT_ARROW :
333+ nextButton = this . _buttonToggles . get (
334+ this . _getNextIndex ( index , this . dir === 'ltr' ? - 1 : 1 ) ,
335+ ) ;
336+ break ;
337+ case DOWN_ARROW :
338+ nextButton = this . _buttonToggles . get ( this . _getNextIndex ( index , 1 ) ) ;
339+ break ;
340+ case RIGHT_ARROW :
341+ nextButton = this . _buttonToggles . get (
342+ this . _getNextIndex ( index , this . dir === 'ltr' ? 1 : - 1 ) ,
343+ ) ;
344+ break ;
345+ default :
346+ return ;
347+ }
348+
349+ event . preventDefault ( ) ;
350+ nextButton ?. _onButtonClick ( ) ;
351+ nextButton ?. focus ( ) ;
352+ }
353+
299354 /** Dispatch change event with current selection and group value. */
300355 _emitChangeEvent ( toggle : MatButtonToggle ) : void {
301356 const event = new MatButtonToggleChange ( toggle , this . value ) ;
@@ -361,6 +416,31 @@ export class MatButtonToggleGroup implements ControlValueAccessor, OnInit, After
361416 return toggle . value === this . _rawValue ;
362417 }
363418
419+ /** Initializes the tabindex attribute using the radio pattern. */
420+ private _initializeTabIndex ( ) {
421+ this . _buttonToggles . forEach ( toggle => {
422+ toggle . tabIndex = - 1 ;
423+ } ) ;
424+ if ( this . selected ) {
425+ ( this . selected as MatButtonToggle ) . tabIndex = 0 ;
426+ } else if ( this . _buttonToggles . length > 0 ) {
427+ this . _buttonToggles . get ( 0 ) ! . tabIndex = 0 ;
428+ }
429+ this . _markButtonsForCheck ( ) ;
430+ }
431+
432+ /** Obtain the subsequent index to which the focus shifts. */
433+ private _getNextIndex ( index : number , offset : number ) : number {
434+ let nextIndex = index + offset ;
435+ if ( nextIndex === this . _buttonToggles . length ) {
436+ nextIndex = 0 ;
437+ }
438+ if ( nextIndex === - 1 ) {
439+ nextIndex = this . _buttonToggles . length - 1 ;
440+ }
441+ return nextIndex ;
442+ }
443+
364444 /** Updates the selection state of the toggles in the group based on a value. */
365445 private _setSelectionByValue ( value : any | any [ ] ) {
366446 this . _rawValue = value ;
@@ -385,7 +465,13 @@ export class MatButtonToggleGroup implements ControlValueAccessor, OnInit, After
385465 /** Clears the selected toggles. */
386466 private _clearSelection ( ) {
387467 this . _selectionModel . clear ( ) ;
388- this . _buttonToggles . forEach ( toggle => ( toggle . checked = false ) ) ;
468+ this . _buttonToggles . forEach ( toggle => {
469+ toggle . checked = false ;
470+ // If the button toggle is in single select mode, initialize the tabIndex.
471+ if ( ! this . multiple ) {
472+ toggle . tabIndex = - 1 ;
473+ }
474+ } ) ;
389475 }
390476
391477 /** Selects a value if there's a toggle that corresponds to it. */
@@ -397,6 +483,10 @@ export class MatButtonToggleGroup implements ControlValueAccessor, OnInit, After
397483 if ( correspondingOption ) {
398484 correspondingOption . checked = true ;
399485 this . _selectionModel . select ( correspondingOption ) ;
486+ if ( ! this . multiple ) {
487+ // If the button toggle is in single select mode, reset the tabIndex.
488+ correspondingOption . tabIndex = 0 ;
489+ }
400490 }
401491 }
402492
@@ -476,8 +566,16 @@ export class MatButtonToggle implements OnInit, AfterViewInit, OnDestroy {
476566 /** MatButtonToggleGroup reads this to assign its own value. */
477567 @Input ( ) value : any ;
478568
479- /** Tabindex for the toggle. */
480- @Input ( ) tabIndex : number | null ;
569+ /** Tabindex of the toggle. */
570+ @Input ( )
571+ get tabIndex ( ) : number | null {
572+ return this . _tabIndex ;
573+ }
574+ set tabIndex ( value : number | null ) {
575+ this . _tabIndex = value ;
576+ this . _markForCheck ( ) ;
577+ }
578+ private _tabIndex : number | null ;
481579
482580 /** Whether ripples are disabled on the button toggle. */
483581 @Input ( { transform : booleanAttribute } ) disableRipple : boolean ;
@@ -580,7 +678,7 @@ export class MatButtonToggle implements OnInit, AfterViewInit, OnDestroy {
580678
581679 /** Checks the button toggle due to an interaction with the underlying native button. */
582680 _onButtonClick ( ) {
583- const newChecked = this . _isSingleSelector ( ) ? true : ! this . _checked ;
681+ const newChecked = this . isSingleSelector ( ) ? true : ! this . _checked ;
584682
585683 if ( newChecked !== this . _checked ) {
586684 this . _checked = newChecked ;
@@ -589,6 +687,19 @@ export class MatButtonToggle implements OnInit, AfterViewInit, OnDestroy {
589687 this . buttonToggleGroup . _onTouched ( ) ;
590688 }
591689 }
690+
691+ if ( this . isSingleSelector ( ) ) {
692+ const focusable = this . buttonToggleGroup . _buttonToggles . find ( toggle => {
693+ return toggle . tabIndex === 0 ;
694+ } ) ;
695+ // Modify the tabindex attribute of the last focusable button toggle to -1.
696+ if ( focusable ) {
697+ focusable . tabIndex = - 1 ;
698+ }
699+ // Modify the tabindex attribute of the presently selected button toggle to 0.
700+ this . tabIndex = 0 ;
701+ }
702+
592703 // Emit a change event when it's the single selector
593704 this . change . emit ( new MatButtonToggleChange ( this , this . value ) ) ;
594705 }
@@ -606,14 +717,14 @@ export class MatButtonToggle implements OnInit, AfterViewInit, OnDestroy {
606717
607718 /** Gets the name that should be assigned to the inner DOM node. */
608719 _getButtonName ( ) : string | null {
609- if ( this . _isSingleSelector ( ) ) {
720+ if ( this . isSingleSelector ( ) ) {
610721 return this . buttonToggleGroup . name ;
611722 }
612723 return this . name || null ;
613724 }
614725
615726 /** Whether the toggle is in single selection mode. */
616- private _isSingleSelector ( ) : boolean {
727+ isSingleSelector ( ) : boolean {
617728 return this . buttonToggleGroup && ! this . buttonToggleGroup . multiple ;
618729 }
619730}
0 commit comments