6
6
* found in the LICENSE file at https://angular.io/license
7
7
*/
8
8
9
+ import { Directionality } from '@angular/cdk/bidi' ;
9
10
import { coerceBooleanProperty } from '@angular/cdk/coercion' ;
10
- import { BACKSPACE } from '@angular/cdk/keycodes' ;
11
+ import { BACKSPACE , TAB } from '@angular/cdk/keycodes' ;
11
12
import {
12
13
AfterContentInit ,
13
14
AfterViewInit ,
14
15
ChangeDetectionStrategy ,
15
16
ChangeDetectorRef ,
16
17
Component ,
18
+ ContentChildren ,
17
19
DoCheck ,
18
20
ElementRef ,
19
21
EventEmitter ,
20
22
Input ,
21
23
OnDestroy ,
22
24
Optional ,
23
25
Output ,
26
+ QueryList ,
24
27
Self ,
25
28
ViewEncapsulation
26
29
} from '@angular/core' ;
@@ -35,9 +38,10 @@ import {MatFormFieldControl} from '@angular/material/form-field';
35
38
import { MatChipTextControl } from './chip-text-control' ;
36
39
import { merge , Observable , Subscription } from 'rxjs' ;
37
40
import { startWith , takeUntil } from 'rxjs/operators' ;
38
-
39
41
import { MatChipEvent } from './chip' ;
42
+ import { MatChipRow } from './chip-row' ;
40
43
import { MatChipSet } from './chip-set' ;
44
+ import { GridFocusKeyManager } from './grid-focus-key-manager' ;
41
45
42
46
43
47
/** Change event object that is emitted when the chip grid value has changed. */
@@ -76,10 +80,11 @@ const _MatChipGridMixinBase: CanUpdateErrorStateCtor & typeof MatChipGridBase =
76
80
selector : 'mat-chip-grid' ,
77
81
template : '<ng-content></ng-content>' ,
78
82
styleUrls : [ 'chips.css' ] ,
83
+ inputs : [ 'tabIndex' ] ,
79
84
host : {
80
85
'class' : 'mat-mdc-chip-set mat-mdc-chip-grid mdc-chip-set' ,
81
86
'role' : 'grid' ,
82
- '[tabIndex]' : 'empty ? -1 : 0 ' ,
87
+ '[tabIndex]' : 'tabIndex ' ,
83
88
// TODO: replace this binding with use of AriaDescriber
84
89
'[attr.aria-describedby]' : '_ariaDescribedby || null' ,
85
90
'[attr.aria-required]' : 'required.toString()' ,
@@ -88,6 +93,9 @@ const _MatChipGridMixinBase: CanUpdateErrorStateCtor & typeof MatChipGridBase =
88
93
'[class.mat-mdc-chip-list-disabled]' : 'disabled' ,
89
94
'[class.mat-mdc-chip-list-invalid]' : 'errorState' ,
90
95
'[class.mat-mdc-chip-list-required]' : 'required' ,
96
+ '(focus)' : 'focus()' ,
97
+ '(blur)' : '_blur()' ,
98
+ '(keydown)' : '_keydown($event)' ,
91
99
'[id]' : '_uid' ,
92
100
} ,
93
101
providers : [ { provide : MatFormFieldControl , useExisting : MatChipGrid } ] ,
@@ -105,6 +113,9 @@ export class MatChipGrid extends _MatChipGridMixinBase implements AfterContentIn
105
113
/** Subscription to blur changes in the chips. */
106
114
private _chipBlurSubscription : Subscription | null ;
107
115
116
+ /** Subscription to focus changes in the chips. */
117
+ private _chipFocusSubscription : Subscription | null ;
118
+
108
119
/** The chip input to add more chips */
109
120
protected _chipInput : MatChipTextControl ;
110
121
@@ -120,6 +131,9 @@ export class MatChipGrid extends _MatChipGridMixinBase implements AfterContentIn
120
131
*/
121
132
_onChange : ( value : any ) => void = ( ) => { } ;
122
133
134
+ /** The GridFocusKeyManager which handles focus. */
135
+ _keyManager : GridFocusKeyManager ;
136
+
123
137
/**
124
138
* Implemented as part of MatFormFieldControl.
125
139
* @docs -private
@@ -187,6 +201,11 @@ export class MatChipGrid extends _MatChipGridMixinBase implements AfterContentIn
187
201
return merge ( ...this . _chips . map ( chip => chip . _onBlur ) ) ;
188
202
}
189
203
204
+ /** Combined stream of all of the child chips' focus events. */
205
+ get chipFocusChanges ( ) : Observable < MatChipEvent > {
206
+ return merge ( ...this . _chips . map ( chip => chip . _onFocus ) ) ;
207
+ }
208
+
190
209
/** Emits when the chip grid value has been changed by the user. */
191
210
@Output ( ) readonly change : EventEmitter < MatChipGridChange > =
192
211
new EventEmitter < MatChipGridChange > ( ) ;
@@ -198,8 +217,16 @@ export class MatChipGrid extends _MatChipGridMixinBase implements AfterContentIn
198
217
*/
199
218
@Output ( ) readonly valueChange : EventEmitter < any > = new EventEmitter < any > ( ) ;
200
219
220
+ @ContentChildren ( MatChipRow , {
221
+ // We need to use `descendants: true`, because Ivy will no longer match
222
+ // indirect descendants if it's left as false.
223
+ descendants : true
224
+ } )
225
+ _rowChips : QueryList < MatChipRow > ;
226
+
201
227
constructor ( _elementRef : ElementRef ,
202
228
_changeDetectorRef : ChangeDetectorRef ,
229
+ @Optional ( ) private _dir : Directionality ,
203
230
@Optional ( ) _parentForm : NgForm ,
204
231
@Optional ( ) _parentFormGroup : FormGroupDirective ,
205
232
_defaultErrorStateMatcher : ErrorStateMatcher ,
@@ -214,7 +241,11 @@ export class MatChipGrid extends _MatChipGridMixinBase implements AfterContentIn
214
241
215
242
ngAfterContentInit ( ) {
216
243
super . ngAfterContentInit ( ) ;
244
+ this . _initKeyManager ( ) ;
245
+
217
246
this . _chips . changes . pipe ( startWith ( null ) , takeUntil ( this . _destroyed ) ) . subscribe ( ( ) => {
247
+ this . _updateTabIndex ( ) ;
248
+
218
249
// Check to see if we have a destroyed chip and need to refocus
219
250
this . _updateFocusForDestroyedChips ( ) ;
220
251
@@ -269,7 +300,7 @@ export class MatChipGrid extends _MatChipGridMixinBase implements AfterContentIn
269
300
}
270
301
271
302
if ( this . _chips . length > 0 ) {
272
- this . _chips . toArray ( ) [ 0 ] . focus ( ) ;
303
+ this . _keyManager . setFirstCellActive ( ) ;
273
304
} else {
274
305
this . _focusInput ( ) ;
275
306
}
@@ -320,6 +351,7 @@ export class MatChipGrid extends _MatChipGridMixinBase implements AfterContentIn
320
351
// Timeout is needed to wait for the focus() event trigger on chip input.
321
352
setTimeout ( ( ) => {
322
353
if ( ! this . focused ) {
354
+ this . _keyManager . setActiveCell ( { row : - 1 , column : - 1 } ) ;
323
355
this . _propagateChanges ( ) ;
324
356
this . _markAsTouched ( ) ;
325
357
}
@@ -332,7 +364,18 @@ export class MatChipGrid extends _MatChipGridMixinBase implements AfterContentIn
332
364
* it back to the first chip, creating a focus trap, if it user tries to tab away.
333
365
*/
334
366
_allowFocusEscape ( ) {
335
- // TODO
367
+ if ( this . _chipInput . focused ) {
368
+ return ;
369
+ }
370
+
371
+ if ( this . tabIndex !== - 1 ) {
372
+ this . tabIndex = - 1 ;
373
+
374
+ setTimeout ( ( ) => {
375
+ this . tabIndex = 0 ;
376
+ this . _changeDetectorRef . markForCheck ( ) ;
377
+ } ) ;
378
+ }
336
379
}
337
380
338
381
/** Handles custom keyboard events. */
@@ -342,11 +385,15 @@ export class MatChipGrid extends _MatChipGridMixinBase implements AfterContentIn
342
385
// If they are on an empty input and hit backspace, focus the last chip
343
386
if ( event . keyCode === BACKSPACE && this . _isEmptyInput ( target ) ) {
344
387
if ( this . _chips . length ) {
345
- this . _chips . toArray ( ) [ this . _chips . length - 1 ] . focus ( ) ;
388
+ this . _keyManager . setLastCellActive ( ) ;
346
389
}
347
390
event . preventDefault ( ) ;
391
+ } else if ( event . keyCode === TAB ) {
392
+ this . _allowFocusEscape ( ) ;
393
+ } else {
394
+ this . _keyManager . onKeydown ( event ) ;
348
395
}
349
- this . stateChanges . next ( ) ;
396
+ this . stateChanges . next ( ) ;
350
397
}
351
398
352
399
/** Unsubscribes from all chip events. */
@@ -356,14 +403,43 @@ export class MatChipGrid extends _MatChipGridMixinBase implements AfterContentIn
356
403
this . _chipBlurSubscription . unsubscribe ( ) ;
357
404
this . _chipBlurSubscription = null ;
358
405
}
406
+
407
+ if ( this . _chipFocusSubscription ) {
408
+ this . _chipFocusSubscription . unsubscribe ( ) ;
409
+ this . _chipFocusSubscription = null ;
410
+ }
359
411
}
360
412
361
413
/** Subscribes to events on the child chips. */
362
414
protected _subscribeToChipEvents ( ) {
363
415
super . _subscribeToChipEvents ( ) ;
416
+ this . _listenToChipsFocus ( ) ;
364
417
this . _listenToChipsBlur ( ) ;
365
418
}
366
419
420
+ /** Initializes the key manager to manage focus. */
421
+ private _initKeyManager ( ) {
422
+ this . _keyManager = new GridFocusKeyManager ( this . _rowChips )
423
+ . withDirectionality ( this . _dir ? this . _dir . value : 'ltr' ) ;
424
+
425
+ if ( this . _dir ) {
426
+ this . _dir . change
427
+ . pipe ( takeUntil ( this . _destroyed ) )
428
+ . subscribe ( dir => this . _keyManager . withDirectionality ( dir ) ) ;
429
+ }
430
+ }
431
+
432
+ /** Subscribes to chip focus events. */
433
+ private _listenToChipsFocus ( ) : void {
434
+ this . _chipFocusSubscription = this . chipFocusChanges . subscribe ( ( event : MatChipEvent ) => {
435
+ let chipIndex : number = this . _chips . toArray ( ) . indexOf ( event . chip ) ;
436
+
437
+ if ( this . _isValidIndex ( chipIndex ) ) {
438
+ this . _keyManager . updateActiveCell ( { row : chipIndex , column : 0 } ) ;
439
+ }
440
+ } ) ;
441
+ }
442
+
367
443
/** Subscribes to chip blur events. */
368
444
private _listenToChipsBlur ( ) : void {
369
445
this . _chipBlurSubscription = this . chipBlurChanges . subscribe ( ( ) => {
@@ -409,17 +485,23 @@ export class MatChipGrid extends _MatChipGridMixinBase implements AfterContentIn
409
485
* If the amount of chips changed, we need to focus the next closest chip.
410
486
*/
411
487
private _updateFocusForDestroyedChips ( ) {
488
+ // Wait for chips to be updated in keyManager
489
+ setTimeout ( ( ) => {
412
490
// Move focus to the closest chip. If no other chips remain, focus the chip-grid itself.
413
491
if ( this . _lastDestroyedChipIndex != null ) {
414
492
if ( this . _chips . length ) {
415
493
const newChipIndex = Math . min ( this . _lastDestroyedChipIndex , this . _chips . length - 1 ) ;
416
- this . _chips . toArray ( ) [ newChipIndex ] . focus ( ) ;
494
+ this . _keyManager . setActiveCell ( {
495
+ row : newChipIndex ,
496
+ column : this . _keyManager . activeColumnIndex
497
+ } ) ;
417
498
} else {
418
499
this . focus ( ) ;
419
500
}
420
501
}
421
502
422
503
this . _lastDestroyedChipIndex = null ;
504
+ } ) ;
423
505
}
424
506
425
507
/** Focus input element. */
@@ -436,4 +518,12 @@ export class MatChipGrid extends _MatChipGridMixinBase implements AfterContentIn
436
518
437
519
return false ;
438
520
}
521
+
522
+ /**
523
+ * Check the tab index as you should not be allowed to focus an empty grid.
524
+ */
525
+ protected _updateTabIndex ( ) : void {
526
+ // If we have 0 chips, we should not allow keyboard focus
527
+ this . tabIndex = this . _chips . length === 0 ? - 1 : 0 ;
528
+ }
439
529
}
0 commit comments