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 } from '@angular/material/core' ;
37
39
@@ -106,8 +108,9 @@ export class MatButtonToggleChange {
106
108
{ provide : MAT_BUTTON_TOGGLE_GROUP , useExisting : MatButtonToggleGroup } ,
107
109
] ,
108
110
host : {
109
- 'role' : 'group' ,
110
111
'class' : 'mat-button-toggle-group' ,
112
+ '(keydown)' : '_keydown($event)' ,
113
+ '[role]' : "multiple ? 'group' : 'radiogroup'" ,
111
114
'[attr.aria-disabled]' : 'disabled' ,
112
115
'[class.mat-button-toggle-vertical]' : 'vertical' ,
113
116
'[class.mat-button-toggle-group-appearance-standard]' : 'appearance === "standard"' ,
@@ -211,6 +214,11 @@ export class MatButtonToggleGroup implements ControlValueAccessor, OnInit, After
211
214
this . _markButtonsForCheck ( ) ;
212
215
}
213
216
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
+
214
222
/** Event emitted when the group's value changes. */
215
223
@Output ( ) readonly change : EventEmitter < MatButtonToggleChange > =
216
224
new EventEmitter < MatButtonToggleChange > ( ) ;
@@ -220,6 +228,7 @@ export class MatButtonToggleGroup implements ControlValueAccessor, OnInit, After
220
228
@Optional ( )
221
229
@Inject ( MAT_BUTTON_TOGGLE_DEFAULT_OPTIONS )
222
230
defaultOptions ?: MatButtonToggleDefaultOptions ,
231
+ @Optional ( ) private _dir ?: Directionality ,
223
232
) {
224
233
this . appearance =
225
234
defaultOptions && defaultOptions . appearance ? defaultOptions . appearance : 'standard' ;
@@ -231,6 +240,9 @@ export class MatButtonToggleGroup implements ControlValueAccessor, OnInit, After
231
240
232
241
ngAfterContentInit ( ) {
233
242
this . _selectionModel . select ( ...this . _buttonToggles . filter ( toggle => toggle . checked ) ) ;
243
+ if ( ! this . multiple ) {
244
+ this . _initializeTabIndex ( ) ;
245
+ }
234
246
}
235
247
236
248
/**
@@ -257,6 +269,49 @@ export class MatButtonToggleGroup implements ControlValueAccessor, OnInit, After
257
269
this . disabled = isDisabled ;
258
270
}
259
271
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
+ nextButton = this . _buttonToggles . get ( this . _getNextIndex ( index , - 1 ) ) ;
292
+ break ;
293
+ case LEFT_ARROW :
294
+ nextButton = this . _buttonToggles . get (
295
+ this . _getNextIndex ( index , this . dir === 'ltr' ? - 1 : 1 ) ,
296
+ ) ;
297
+ break ;
298
+ case DOWN_ARROW :
299
+ nextButton = this . _buttonToggles . get ( this . _getNextIndex ( index , 1 ) ) ;
300
+ break ;
301
+ case RIGHT_ARROW :
302
+ nextButton = this . _buttonToggles . get (
303
+ this . _getNextIndex ( index , this . dir === 'ltr' ? 1 : - 1 ) ,
304
+ ) ;
305
+ break ;
306
+ default :
307
+ return ;
308
+ }
309
+
310
+ event . preventDefault ( ) ;
311
+ nextButton ?. _onButtonClick ( ) ;
312
+ nextButton ?. focus ( ) ;
313
+ }
314
+
260
315
/** Dispatch change event with current selection and group value. */
261
316
_emitChangeEvent ( toggle : MatButtonToggle ) : void {
262
317
const event = new MatButtonToggleChange ( toggle , this . value ) ;
@@ -322,6 +377,31 @@ export class MatButtonToggleGroup implements ControlValueAccessor, OnInit, After
322
377
return toggle . value === this . _rawValue ;
323
378
}
324
379
380
+ /** Initializes the tabindex attribute using the radio pattern. */
381
+ private _initializeTabIndex ( ) {
382
+ this . _buttonToggles . forEach ( toggle => {
383
+ toggle . tabIndex = - 1 ;
384
+ } ) ;
385
+ if ( this . selected ) {
386
+ ( this . selected as MatButtonToggle ) . tabIndex = 0 ;
387
+ } else if ( this . _buttonToggles . length > 0 ) {
388
+ this . _buttonToggles . get ( 0 ) ! . tabIndex = 0 ;
389
+ }
390
+ this . _markButtonsForCheck ( ) ;
391
+ }
392
+
393
+ /** Obtain the subsequent index to which the focus shifts. */
394
+ private _getNextIndex ( index : number , offset : number ) : number {
395
+ let nextIndex = index + offset ;
396
+ if ( nextIndex === this . _buttonToggles . length ) {
397
+ nextIndex = 0 ;
398
+ }
399
+ if ( nextIndex === - 1 ) {
400
+ nextIndex = this . _buttonToggles . length - 1 ;
401
+ }
402
+ return nextIndex ;
403
+ }
404
+
325
405
/** Updates the selection state of the toggles in the group based on a value. */
326
406
private _setSelectionByValue ( value : any | any [ ] ) {
327
407
this . _rawValue = value ;
@@ -346,7 +426,13 @@ export class MatButtonToggleGroup implements ControlValueAccessor, OnInit, After
346
426
/** Clears the selected toggles. */
347
427
private _clearSelection ( ) {
348
428
this . _selectionModel . clear ( ) ;
349
- this . _buttonToggles . forEach ( toggle => ( toggle . checked = false ) ) ;
429
+ this . _buttonToggles . forEach ( toggle => {
430
+ toggle . checked = false ;
431
+ // If the button toggle is in single select mode, initialize the tabIndex.
432
+ if ( ! this . multiple ) {
433
+ toggle . tabIndex = - 1 ;
434
+ }
435
+ } ) ;
350
436
}
351
437
352
438
/** Selects a value if there's a toggle that corresponds to it. */
@@ -358,6 +444,10 @@ export class MatButtonToggleGroup implements ControlValueAccessor, OnInit, After
358
444
if ( correspondingOption ) {
359
445
correspondingOption . checked = true ;
360
446
this . _selectionModel . select ( correspondingOption ) ;
447
+ if ( ! this . multiple ) {
448
+ // If the button toggle is in single select mode, reset the tabIndex.
449
+ correspondingOption . tabIndex = 0 ;
450
+ }
361
451
}
362
452
}
363
453
@@ -437,8 +527,16 @@ export class MatButtonToggle implements OnInit, AfterViewInit, OnDestroy {
437
527
/** MatButtonToggleGroup reads this to assign its own value. */
438
528
@Input ( ) value : any ;
439
529
440
- /** Tabindex for the toggle. */
441
- @Input ( ) tabIndex : number | null ;
530
+ /** Tabindex of the toggle. */
531
+ @Input ( )
532
+ get tabIndex ( ) : number | null {
533
+ return this . _tabIndex ;
534
+ }
535
+ set tabIndex ( value : number | null ) {
536
+ this . _tabIndex = value ;
537
+ this . _markForCheck ( ) ;
538
+ }
539
+ private _tabIndex : number | null ;
442
540
443
541
/** Whether ripples are disabled on the button toggle. */
444
542
@Input ( { transform : booleanAttribute } ) disableRipple : boolean ;
@@ -541,7 +639,7 @@ export class MatButtonToggle implements OnInit, AfterViewInit, OnDestroy {
541
639
542
640
/** Checks the button toggle due to an interaction with the underlying native button. */
543
641
_onButtonClick ( ) {
544
- const newChecked = this . _isSingleSelector ( ) ? true : ! this . _checked ;
642
+ const newChecked = this . isSingleSelector ( ) ? true : ! this . _checked ;
545
643
546
644
if ( newChecked !== this . _checked ) {
547
645
this . _checked = newChecked ;
@@ -550,6 +648,19 @@ export class MatButtonToggle implements OnInit, AfterViewInit, OnDestroy {
550
648
this . buttonToggleGroup . _onTouched ( ) ;
551
649
}
552
650
}
651
+
652
+ if ( this . isSingleSelector ( ) ) {
653
+ const focusable = this . buttonToggleGroup . _buttonToggles . find ( toggle => {
654
+ return toggle . tabIndex === 0 ;
655
+ } ) ;
656
+ // Modify the tabindex attribute of the last focusable button toggle to -1.
657
+ if ( focusable ) {
658
+ focusable . tabIndex = - 1 ;
659
+ }
660
+ // Modify the tabindex attribute of the presently selected button toggle to 0.
661
+ this . tabIndex = 0 ;
662
+ }
663
+
553
664
// Emit a change event when it's the single selector
554
665
this . change . emit ( new MatButtonToggleChange ( this , this . value ) ) ;
555
666
}
@@ -567,14 +678,14 @@ export class MatButtonToggle implements OnInit, AfterViewInit, OnDestroy {
567
678
568
679
/** Gets the name that should be assigned to the inner DOM node. */
569
680
_getButtonName ( ) : string | null {
570
- if ( this . _isSingleSelector ( ) ) {
681
+ if ( this . isSingleSelector ( ) ) {
571
682
return this . buttonToggleGroup . name ;
572
683
}
573
684
return this . name || null ;
574
685
}
575
686
576
687
/** Whether the toggle is in single selection mode. */
577
- private _isSingleSelector ( ) : boolean {
688
+ isSingleSelector ( ) : boolean {
578
689
return this . buttonToggleGroup && ! this . buttonToggleGroup . multiple ;
579
690
}
580
691
}
0 commit comments