8
8
9
9
import { FocusMonitor } from '@angular/cdk/a11y' ;
10
10
import { SelectionModel } from '@angular/cdk/collections' ;
11
+ import { DOWN_ARROW , LEFT_ARROW , RIGHT_ARROW , UP_ARROW , SPACE , ENTER } from '@angular/cdk/keycodes' ;
11
12
import {
12
13
AfterContentInit ,
13
14
Attribute ,
@@ -32,6 +33,7 @@ import {
32
33
AfterViewInit ,
33
34
booleanAttribute ,
34
35
} from '@angular/core' ;
36
+ import { Direction , Directionality } from '@angular/cdk/bidi' ;
35
37
import { ControlValueAccessor , NG_VALUE_ACCESSOR } from '@angular/forms' ;
36
38
import { MatRipple , MatPseudoCheckbox } from '@angular/material/core' ;
37
39
@@ -121,8 +123,9 @@ export class MatButtonToggleChange {
121
123
{ provide : MAT_BUTTON_TOGGLE_GROUP , useExisting : MatButtonToggleGroup } ,
122
124
] ,
123
125
host : {
124
- 'role' : 'group' ,
125
126
'class' : 'mat-button-toggle-group' ,
127
+ '(keydown)' : '_keydown($event)' ,
128
+ '[attr.role]' : "multiple ? 'group' : 'radiogroup'" ,
126
129
'[attr.aria-disabled]' : 'disabled' ,
127
130
'[class.mat-button-toggle-vertical]' : 'vertical' ,
128
131
'[class.mat-button-toggle-group-appearance-standard]' : 'appearance === "standard"' ,
@@ -226,6 +229,11 @@ export class MatButtonToggleGroup implements ControlValueAccessor, OnInit, After
226
229
this . _markButtonsForCheck ( ) ;
227
230
}
228
231
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
+
229
237
/** Event emitted when the group's value changes. */
230
238
@Output ( ) readonly change : EventEmitter < MatButtonToggleChange > =
231
239
new EventEmitter < MatButtonToggleChange > ( ) ;
@@ -257,6 +265,7 @@ export class MatButtonToggleGroup implements ControlValueAccessor, OnInit, After
257
265
@Optional ( )
258
266
@Inject ( MAT_BUTTON_TOGGLE_DEFAULT_OPTIONS )
259
267
defaultOptions ?: MatButtonToggleDefaultOptions ,
268
+ @Optional ( ) private _dir ?: Directionality ,
260
269
) {
261
270
this . appearance =
262
271
defaultOptions && defaultOptions . appearance ? defaultOptions . appearance : 'standard' ;
@@ -270,6 +279,9 @@ export class MatButtonToggleGroup implements ControlValueAccessor, OnInit, After
270
279
271
280
ngAfterContentInit ( ) {
272
281
this . _selectionModel . select ( ...this . _buttonToggles . filter ( toggle => toggle . checked ) ) ;
282
+ if ( ! this . multiple ) {
283
+ this . _initializeTabIndex ( ) ;
284
+ }
273
285
}
274
286
275
287
/**
@@ -296,6 +308,49 @@ export class MatButtonToggleGroup implements ControlValueAccessor, OnInit, After
296
308
this . disabled = isDisabled ;
297
309
}
298
310
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
+
299
354
/** Dispatch change event with current selection and group value. */
300
355
_emitChangeEvent ( toggle : MatButtonToggle ) : void {
301
356
const event = new MatButtonToggleChange ( toggle , this . value ) ;
@@ -361,6 +416,31 @@ export class MatButtonToggleGroup implements ControlValueAccessor, OnInit, After
361
416
return toggle . value === this . _rawValue ;
362
417
}
363
418
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
+
364
444
/** Updates the selection state of the toggles in the group based on a value. */
365
445
private _setSelectionByValue ( value : any | any [ ] ) {
366
446
this . _rawValue = value ;
@@ -385,7 +465,13 @@ export class MatButtonToggleGroup implements ControlValueAccessor, OnInit, After
385
465
/** Clears the selected toggles. */
386
466
private _clearSelection ( ) {
387
467
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
+ } ) ;
389
475
}
390
476
391
477
/** Selects a value if there's a toggle that corresponds to it. */
@@ -397,6 +483,10 @@ export class MatButtonToggleGroup implements ControlValueAccessor, OnInit, After
397
483
if ( correspondingOption ) {
398
484
correspondingOption . checked = true ;
399
485
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
+ }
400
490
}
401
491
}
402
492
@@ -476,8 +566,16 @@ export class MatButtonToggle implements OnInit, AfterViewInit, OnDestroy {
476
566
/** MatButtonToggleGroup reads this to assign its own value. */
477
567
@Input ( ) value : any ;
478
568
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 ;
481
579
482
580
/** Whether ripples are disabled on the button toggle. */
483
581
@Input ( { transform : booleanAttribute } ) disableRipple : boolean ;
@@ -580,7 +678,7 @@ export class MatButtonToggle implements OnInit, AfterViewInit, OnDestroy {
580
678
581
679
/** Checks the button toggle due to an interaction with the underlying native button. */
582
680
_onButtonClick ( ) {
583
- const newChecked = this . _isSingleSelector ( ) ? true : ! this . _checked ;
681
+ const newChecked = this . isSingleSelector ( ) ? true : ! this . _checked ;
584
682
585
683
if ( newChecked !== this . _checked ) {
586
684
this . _checked = newChecked ;
@@ -589,6 +687,19 @@ export class MatButtonToggle implements OnInit, AfterViewInit, OnDestroy {
589
687
this . buttonToggleGroup . _onTouched ( ) ;
590
688
}
591
689
}
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
+
592
703
// Emit a change event when it's the single selector
593
704
this . change . emit ( new MatButtonToggleChange ( this , this . value ) ) ;
594
705
}
@@ -606,14 +717,14 @@ export class MatButtonToggle implements OnInit, AfterViewInit, OnDestroy {
606
717
607
718
/** Gets the name that should be assigned to the inner DOM node. */
608
719
_getButtonName ( ) : string | null {
609
- if ( this . _isSingleSelector ( ) ) {
720
+ if ( this . isSingleSelector ( ) ) {
610
721
return this . buttonToggleGroup . name ;
611
722
}
612
723
return this . name || null ;
613
724
}
614
725
615
726
/** Whether the toggle is in single selection mode. */
616
- private _isSingleSelector ( ) : boolean {
727
+ isSingleSelector ( ) : boolean {
617
728
return this . buttonToggleGroup && ! this . buttonToggleGroup . multiple ;
618
729
}
619
730
}
0 commit comments