66 * found in the LICENSE file at https://angular.io/license
77 */
88
9+ import { Directionality } from '@angular/cdk/bidi' ;
910import { coerceBooleanProperty } from '@angular/cdk/coercion' ;
10- import { BACKSPACE } from '@angular/cdk/keycodes' ;
11+ import { BACKSPACE , TAB } from '@angular/cdk/keycodes' ;
1112import {
1213 AfterContentInit ,
1314 AfterViewInit ,
1415 ChangeDetectionStrategy ,
1516 ChangeDetectorRef ,
1617 Component ,
18+ ContentChildren ,
1719 DoCheck ,
1820 ElementRef ,
1921 EventEmitter ,
2022 Input ,
2123 OnDestroy ,
2224 Optional ,
2325 Output ,
26+ QueryList ,
2427 Self ,
2528 ViewEncapsulation
2629} from '@angular/core' ;
@@ -35,9 +38,10 @@ import {MatFormFieldControl} from '@angular/material/form-field';
3538import { MatChipTextControl } from './chip-text-control' ;
3639import { merge , Observable , Subscription } from 'rxjs' ;
3740import { startWith , takeUntil } from 'rxjs/operators' ;
38-
3941import { MatChipEvent } from './chip' ;
42+ import { MatChipRow } from './chip-row' ;
4043import { MatChipSet } from './chip-set' ;
44+ import { GridFocusKeyManager } from './grid-focus-key-manager' ;
4145
4246
4347/** Change event object that is emitted when the chip grid value has changed. */
@@ -76,10 +80,11 @@ const _MatChipGridMixinBase: CanUpdateErrorStateCtor & typeof MatChipGridBase =
7680 selector : 'mat-chip-grid' ,
7781 template : '<ng-content></ng-content>' ,
7882 styleUrls : [ 'chips.css' ] ,
83+ inputs : [ 'tabIndex' ] ,
7984 host : {
8085 'class' : 'mat-mdc-chip-set mat-mdc-chip-grid mdc-chip-set' ,
8186 'role' : 'grid' ,
82- '[tabIndex]' : 'empty ? -1 : 0 ' ,
87+ '[tabIndex]' : 'tabIndex ' ,
8388 // TODO: replace this binding with use of AriaDescriber
8489 '[attr.aria-describedby]' : '_ariaDescribedby || null' ,
8590 '[attr.aria-required]' : 'required.toString()' ,
@@ -88,6 +93,9 @@ const _MatChipGridMixinBase: CanUpdateErrorStateCtor & typeof MatChipGridBase =
8893 '[class.mat-mdc-chip-list-disabled]' : 'disabled' ,
8994 '[class.mat-mdc-chip-list-invalid]' : 'errorState' ,
9095 '[class.mat-mdc-chip-list-required]' : 'required' ,
96+ '(focus)' : 'focus()' ,
97+ '(blur)' : '_blur()' ,
98+ '(keydown)' : '_keydown($event)' ,
9199 '[id]' : '_uid' ,
92100 } ,
93101 providers : [ { provide : MatFormFieldControl , useExisting : MatChipGrid } ] ,
@@ -105,6 +113,9 @@ export class MatChipGrid extends _MatChipGridMixinBase implements AfterContentIn
105113 /** Subscription to blur changes in the chips. */
106114 private _chipBlurSubscription : Subscription | null ;
107115
116+ /** Subscription to focus changes in the chips. */
117+ private _chipFocusSubscription : Subscription | null ;
118+
108119 /** The chip input to add more chips */
109120 protected _chipInput : MatChipTextControl ;
110121
@@ -120,6 +131,9 @@ export class MatChipGrid extends _MatChipGridMixinBase implements AfterContentIn
120131 */
121132 _onChange : ( value : any ) => void = ( ) => { } ;
122133
134+ /** The GridFocusKeyManager which handles focus. */
135+ _keyManager : GridFocusKeyManager ;
136+
123137 /**
124138 * Implemented as part of MatFormFieldControl.
125139 * @docs -private
@@ -187,6 +201,11 @@ export class MatChipGrid extends _MatChipGridMixinBase implements AfterContentIn
187201 return merge ( ...this . _chips . map ( chip => chip . _onBlur ) ) ;
188202 }
189203
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+
190209 /** Emits when the chip grid value has been changed by the user. */
191210 @Output ( ) readonly change : EventEmitter < MatChipGridChange > =
192211 new EventEmitter < MatChipGridChange > ( ) ;
@@ -198,8 +217,16 @@ export class MatChipGrid extends _MatChipGridMixinBase implements AfterContentIn
198217 */
199218 @Output ( ) readonly valueChange : EventEmitter < any > = new EventEmitter < any > ( ) ;
200219
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+
201227 constructor ( _elementRef : ElementRef ,
202228 _changeDetectorRef : ChangeDetectorRef ,
229+ @Optional ( ) private _dir : Directionality ,
203230 @Optional ( ) _parentForm : NgForm ,
204231 @Optional ( ) _parentFormGroup : FormGroupDirective ,
205232 _defaultErrorStateMatcher : ErrorStateMatcher ,
@@ -214,7 +241,11 @@ export class MatChipGrid extends _MatChipGridMixinBase implements AfterContentIn
214241
215242 ngAfterContentInit ( ) {
216243 super . ngAfterContentInit ( ) ;
244+ this . _initKeyManager ( ) ;
245+
217246 this . _chips . changes . pipe ( startWith ( null ) , takeUntil ( this . _destroyed ) ) . subscribe ( ( ) => {
247+ this . _updateTabIndex ( ) ;
248+
218249 // Check to see if we have a destroyed chip and need to refocus
219250 this . _updateFocusForDestroyedChips ( ) ;
220251
@@ -269,7 +300,7 @@ export class MatChipGrid extends _MatChipGridMixinBase implements AfterContentIn
269300 }
270301
271302 if ( this . _chips . length > 0 ) {
272- this . _chips . toArray ( ) [ 0 ] . focus ( ) ;
303+ this . _keyManager . setFirstCellActive ( ) ;
273304 } else {
274305 this . _focusInput ( ) ;
275306 }
@@ -320,6 +351,7 @@ export class MatChipGrid extends _MatChipGridMixinBase implements AfterContentIn
320351 // Timeout is needed to wait for the focus() event trigger on chip input.
321352 setTimeout ( ( ) => {
322353 if ( ! this . focused ) {
354+ this . _keyManager . setActiveCell ( { row : - 1 , column : - 1 } ) ;
323355 this . _propagateChanges ( ) ;
324356 this . _markAsTouched ( ) ;
325357 }
@@ -332,7 +364,18 @@ export class MatChipGrid extends _MatChipGridMixinBase implements AfterContentIn
332364 * it back to the first chip, creating a focus trap, if it user tries to tab away.
333365 */
334366 _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+ }
336379 }
337380
338381 /** Handles custom keyboard events. */
@@ -342,11 +385,15 @@ export class MatChipGrid extends _MatChipGridMixinBase implements AfterContentIn
342385 // If they are on an empty input and hit backspace, focus the last chip
343386 if ( event . keyCode === BACKSPACE && this . _isEmptyInput ( target ) ) {
344387 if ( this . _chips . length ) {
345- this . _chips . toArray ( ) [ this . _chips . length - 1 ] . focus ( ) ;
388+ this . _keyManager . setLastCellActive ( ) ;
346389 }
347390 event . preventDefault ( ) ;
391+ } else if ( event . keyCode === TAB ) {
392+ this . _allowFocusEscape ( ) ;
393+ } else {
394+ this . _keyManager . onKeydown ( event ) ;
348395 }
349- this . stateChanges . next ( ) ;
396+ this . stateChanges . next ( ) ;
350397 }
351398
352399 /** Unsubscribes from all chip events. */
@@ -356,14 +403,43 @@ export class MatChipGrid extends _MatChipGridMixinBase implements AfterContentIn
356403 this . _chipBlurSubscription . unsubscribe ( ) ;
357404 this . _chipBlurSubscription = null ;
358405 }
406+
407+ if ( this . _chipFocusSubscription ) {
408+ this . _chipFocusSubscription . unsubscribe ( ) ;
409+ this . _chipFocusSubscription = null ;
410+ }
359411 }
360412
361413 /** Subscribes to events on the child chips. */
362414 protected _subscribeToChipEvents ( ) {
363415 super . _subscribeToChipEvents ( ) ;
416+ this . _listenToChipsFocus ( ) ;
364417 this . _listenToChipsBlur ( ) ;
365418 }
366419
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+
367443 /** Subscribes to chip blur events. */
368444 private _listenToChipsBlur ( ) : void {
369445 this . _chipBlurSubscription = this . chipBlurChanges . subscribe ( ( ) => {
@@ -409,17 +485,23 @@ export class MatChipGrid extends _MatChipGridMixinBase implements AfterContentIn
409485 * If the amount of chips changed, we need to focus the next closest chip.
410486 */
411487 private _updateFocusForDestroyedChips ( ) {
488+ // Wait for chips to be updated in keyManager
489+ setTimeout ( ( ) => {
412490 // Move focus to the closest chip. If no other chips remain, focus the chip-grid itself.
413491 if ( this . _lastDestroyedChipIndex != null ) {
414492 if ( this . _chips . length ) {
415493 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+ } ) ;
417498 } else {
418499 this . focus ( ) ;
419500 }
420501 }
421502
422503 this . _lastDestroyedChipIndex = null ;
504+ } ) ;
423505 }
424506
425507 /** Focus input element. */
@@ -436,4 +518,12 @@ export class MatChipGrid extends _MatChipGridMixinBase implements AfterContentIn
436518
437519 return false ;
438520 }
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+ }
439529}
0 commit comments