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 } from '@angular/material/core' ;
3739
@@ -106,8 +108,9 @@ export class MatButtonToggleChange {
106108 { provide : MAT_BUTTON_TOGGLE_GROUP , useExisting : MatButtonToggleGroup } ,
107109 ] ,
108110 host : {
109- 'role' : 'group' ,
110111 'class' : 'mat-button-toggle-group' ,
112+ '(keydown)' : '_keydown($event)' ,
113+ '[role]' : "multiple ? 'group' : 'radiogroup'" ,
111114 '[attr.aria-disabled]' : 'disabled' ,
112115 '[class.mat-button-toggle-vertical]' : 'vertical' ,
113116 '[class.mat-button-toggle-group-appearance-standard]' : 'appearance === "standard"' ,
@@ -211,6 +214,11 @@ export class MatButtonToggleGroup implements ControlValueAccessor, OnInit, After
211214 this . _markButtonsForCheck ( ) ;
212215 }
213216
217+ /** The layout direction of the toggle button group. */
218+ get dir ( ) : Direction {
219+ return this . _dir && this . _dir . value === 'rtl' ? 'rtl' : 'ltr' ;
220+ }
221+
214222 /** Event emitted when the group's value changes. */
215223 @Output ( ) readonly change : EventEmitter < MatButtonToggleChange > =
216224 new EventEmitter < MatButtonToggleChange > ( ) ;
@@ -220,6 +228,7 @@ export class MatButtonToggleGroup implements ControlValueAccessor, OnInit, After
220228 @Optional ( )
221229 @Inject ( MAT_BUTTON_TOGGLE_DEFAULT_OPTIONS )
222230 defaultOptions ?: MatButtonToggleDefaultOptions ,
231+ @Optional ( ) private _dir ?: Directionality ,
223232 ) {
224233 this . appearance =
225234 defaultOptions && defaultOptions . appearance ? defaultOptions . appearance : 'standard' ;
@@ -231,6 +240,9 @@ export class MatButtonToggleGroup implements ControlValueAccessor, OnInit, After
231240
232241 ngAfterContentInit ( ) {
233242 this . _selectionModel . select ( ...this . _buttonToggles . filter ( toggle => toggle . checked ) ) ;
243+ if ( ! this . multiple ) {
244+ this . _initializeTabIndex ( ) ;
245+ }
234246 }
235247
236248 /**
@@ -257,6 +269,45 @@ export class MatButtonToggleGroup implements ControlValueAccessor, OnInit, After
257269 this . disabled = isDisabled ;
258270 }
259271
272+ /** Handle keydown event calling to single-select button toggle. */
273+ protected _keydown ( event : KeyboardEvent ) {
274+ if ( this . multiple || this . disabled ) {
275+ return ;
276+ }
277+
278+ const target = event . target as HTMLButtonElement ;
279+ const buttonId = target . id ;
280+ const index = this . _buttonToggles . toArray ( ) . findIndex ( toggle => {
281+ return toggle . buttonId === buttonId ;
282+ } ) ;
283+
284+ let nextButton ;
285+ switch ( event . keyCode ) {
286+ case SPACE :
287+ case ENTER :
288+ nextButton = this . _buttonToggles . get ( index ) ;
289+ break ;
290+ case UP_ARROW :
291+ case LEFT_ARROW :
292+ nextButton = this . _buttonToggles . get (
293+ this . _getNextIndex ( index , this . dir === 'ltr' ? - 1 : 1 ) ,
294+ ) ;
295+ break ;
296+ case DOWN_ARROW :
297+ case RIGHT_ARROW :
298+ nextButton = this . _buttonToggles . get (
299+ this . _getNextIndex ( index , this . dir === 'ltr' ? 1 : - 1 ) ,
300+ ) ;
301+ break ;
302+ default :
303+ return ;
304+ }
305+
306+ event . preventDefault ( ) ;
307+ nextButton ?. _onButtonClick ( ) ;
308+ nextButton ?. focus ( ) ;
309+ }
310+
260311 /** Dispatch change event with current selection and group value. */
261312 _emitChangeEvent ( toggle : MatButtonToggle ) : void {
262313 const event = new MatButtonToggleChange ( toggle , this . value ) ;
@@ -322,6 +373,31 @@ export class MatButtonToggleGroup implements ControlValueAccessor, OnInit, After
322373 return toggle . value === this . _rawValue ;
323374 }
324375
376+ /** Initializes the tabindex attribute using the radio pattern. */
377+ private _initializeTabIndex ( ) {
378+ this . _buttonToggles . forEach ( toggle => {
379+ toggle . tabIndex = - 1 ;
380+ } ) ;
381+ if ( this . selected ) {
382+ ( this . selected as MatButtonToggle ) . tabIndex = 0 ;
383+ } else if ( this . _buttonToggles . length > 0 ) {
384+ this . _buttonToggles . get ( 0 ) ! . tabIndex = 0 ;
385+ }
386+ this . _markButtonsForCheck ( ) ;
387+ }
388+
389+ /** Obtain the subsequent index to which the focus shifts. */
390+ private _getNextIndex ( index : number , offset : number ) : number {
391+ let nextIndex = index + offset ;
392+ if ( nextIndex === this . _buttonToggles . length ) {
393+ nextIndex = 0 ;
394+ }
395+ if ( nextIndex === - 1 ) {
396+ nextIndex = this . _buttonToggles . length - 1 ;
397+ }
398+ return nextIndex ;
399+ }
400+
325401 /** Updates the selection state of the toggles in the group based on a value. */
326402 private _setSelectionByValue ( value : any | any [ ] ) {
327403 this . _rawValue = value ;
@@ -346,7 +422,13 @@ export class MatButtonToggleGroup implements ControlValueAccessor, OnInit, After
346422 /** Clears the selected toggles. */
347423 private _clearSelection ( ) {
348424 this . _selectionModel . clear ( ) ;
349- this . _buttonToggles . forEach ( toggle => ( toggle . checked = false ) ) ;
425+ this . _buttonToggles . forEach ( toggle => {
426+ toggle . checked = false ;
427+ // If the button toggle is in single select mode, initialize the tabIndex.
428+ if ( ! this . multiple ) {
429+ toggle . tabIndex = - 1 ;
430+ }
431+ } ) ;
350432 }
351433
352434 /** Selects a value if there's a toggle that corresponds to it. */
@@ -358,6 +440,10 @@ export class MatButtonToggleGroup implements ControlValueAccessor, OnInit, After
358440 if ( correspondingOption ) {
359441 correspondingOption . checked = true ;
360442 this . _selectionModel . select ( correspondingOption ) ;
443+ if ( ! this . multiple ) {
444+ // If the button toggle is in single select mode, reset the tabIndex.
445+ correspondingOption . tabIndex = 0 ;
446+ }
361447 }
362448 }
363449
@@ -437,8 +523,16 @@ export class MatButtonToggle implements OnInit, AfterViewInit, OnDestroy {
437523 /** MatButtonToggleGroup reads this to assign its own value. */
438524 @Input ( ) value : any ;
439525
440- /** Tabindex for the toggle. */
441- @Input ( ) tabIndex : number | null ;
526+ /** Tabindex of the toggle. */
527+ @Input ( )
528+ get tabIndex ( ) : number | null {
529+ return this . _tabIndex ;
530+ }
531+ set tabIndex ( value : number | null ) {
532+ this . _tabIndex = value ;
533+ this . _markForCheck ( ) ;
534+ }
535+ private _tabIndex : number | null ;
442536
443537 /** Whether ripples are disabled on the button toggle. */
444538 @Input ( { transform : booleanAttribute } ) disableRipple : boolean ;
@@ -541,7 +635,7 @@ export class MatButtonToggle implements OnInit, AfterViewInit, OnDestroy {
541635
542636 /** Checks the button toggle due to an interaction with the underlying native button. */
543637 _onButtonClick ( ) {
544- const newChecked = this . _isSingleSelector ( ) ? true : ! this . _checked ;
638+ const newChecked = this . isSingleSelector ( ) ? true : ! this . _checked ;
545639
546640 if ( newChecked !== this . _checked ) {
547641 this . _checked = newChecked ;
@@ -550,6 +644,19 @@ export class MatButtonToggle implements OnInit, AfterViewInit, OnDestroy {
550644 this . buttonToggleGroup . _onTouched ( ) ;
551645 }
552646 }
647+
648+ if ( this . isSingleSelector ( ) ) {
649+ const focusable = this . buttonToggleGroup . _buttonToggles . find ( toggle => {
650+ return toggle . tabIndex === 0 ;
651+ } ) ;
652+ // Modify the tabindex attribute of the last focusable button toggle to -1.
653+ if ( focusable ) {
654+ focusable . tabIndex = - 1 ;
655+ }
656+ // Modify the tabindex attribute of the presently selected button toggle to 0.
657+ this . tabIndex = 0 ;
658+ }
659+
553660 // Emit a change event when it's the single selector
554661 this . change . emit ( new MatButtonToggleChange ( this , this . value ) ) ;
555662 }
@@ -567,14 +674,14 @@ export class MatButtonToggle implements OnInit, AfterViewInit, OnDestroy {
567674
568675 /** Gets the name that should be assigned to the inner DOM node. */
569676 _getButtonName ( ) : string | null {
570- if ( this . _isSingleSelector ( ) ) {
677+ if ( this . isSingleSelector ( ) ) {
571678 return this . buttonToggleGroup . name ;
572679 }
573680 return this . name || null ;
574681 }
575682
576683 /** Whether the toggle is in single selection mode. */
577- private _isSingleSelector ( ) : boolean {
684+ isSingleSelector ( ) : boolean {
578685 return this . buttonToggleGroup && ! this . buttonToggleGroup . multiple ;
579686 }
580687}
0 commit comments