Skip to content

Commit e488152

Browse files
crisbetojelbourn
authored andcommitted
fix(chips): unable to tab out of chip list with input on firefox (#15060)
Fixes user focus being wrapped back to the first chip, when trying to tab out of a chip list that has an input, on Firefox. Fixes #15011.
1 parent 5fb6125 commit e488152

File tree

4 files changed

+69
-15
lines changed

4 files changed

+69
-15
lines changed

src/material/chips/chip-input.spec.ts

Lines changed: 44 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import {Directionality} from '@angular/cdk/bidi';
2-
import {ENTER, COMMA} from '@angular/cdk/keycodes';
2+
import {ENTER, COMMA, TAB} from '@angular/cdk/keycodes';
33
import {PlatformModule} from '@angular/cdk/platform';
4-
import {createKeyboardEvent} from '@angular/cdk/testing';
4+
import {createKeyboardEvent, dispatchKeyboardEvent, dispatchEvent} from '@angular/cdk/testing';
55
import {Component, DebugElement, ViewChild} from '@angular/core';
6-
import {async, ComponentFixture, TestBed} from '@angular/core/testing';
6+
import {async, ComponentFixture, TestBed, fakeAsync, tick} from '@angular/core/testing';
77
import {By} from '@angular/platform-browser';
88
import {NoopAnimationsModule} from '@angular/platform-browser/animations';
99
import {MatFormFieldModule} from '@angular/material/form-field';
@@ -98,6 +98,42 @@ describe('MatChipInput', () => {
9898
expect(chipInputDirective.disabled).toBe(true);
9999
});
100100

101+
it('should allow focus to escape when tabbing forwards', fakeAsync(() => {
102+
const listElement: HTMLElement = fixture.nativeElement.querySelector('.mat-chip-list');
103+
104+
expect(listElement.getAttribute('tabindex')).toBe('0');
105+
106+
dispatchKeyboardEvent(inputNativeElement, 'keydown', TAB);
107+
fixture.detectChanges();
108+
109+
expect(listElement.getAttribute('tabindex'))
110+
.toBe('-1', 'Expected tabIndex to be set to -1 temporarily.');
111+
112+
tick();
113+
fixture.detectChanges();
114+
115+
expect(listElement.getAttribute('tabindex'))
116+
.toBe('0', 'Expected tabIndex to be reset back to 0');
117+
}));
118+
119+
it('should not allow focus to escape when tabbing backwards', fakeAsync(() => {
120+
const listElement: HTMLElement = fixture.nativeElement.querySelector('.mat-chip-list');
121+
const event = createKeyboardEvent('keydown', TAB);
122+
Object.defineProperty(event, 'shiftKey', {get: () => true});
123+
124+
expect(listElement.getAttribute('tabindex')).toBe('0');
125+
126+
dispatchEvent(inputNativeElement, event);
127+
fixture.detectChanges();
128+
129+
expect(listElement.getAttribute('tabindex')).toBe('0', 'Expected tabindex to remain 0');
130+
131+
tick();
132+
fixture.detectChanges();
133+
134+
expect(listElement.getAttribute('tabindex')).toBe('0', 'Expected tabindex to remain 0');
135+
}));
136+
101137
});
102138

103139
describe('[addOnBlur]', () => {
@@ -205,11 +241,12 @@ describe('MatChipInput', () => {
205241
template: `
206242
<mat-form-field>
207243
<mat-chip-list #chipList>
244+
<mat-chip>Hello</mat-chip>
245+
<input matInput [matChipInputFor]="chipList"
246+
[matChipInputAddOnBlur]="addOnBlur"
247+
(matChipInputTokenEnd)="add($event)"
248+
[placeholder]="placeholder" />
208249
</mat-chip-list>
209-
<input matInput [matChipInputFor]="chipList"
210-
[matChipInputAddOnBlur]="addOnBlur"
211-
(matChipInputTokenEnd)="add($event)"
212-
[placeholder]="placeholder" />
213250
</mat-form-field>
214251
`
215252
})

src/material/chips/chip-input.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
import {coerceBooleanProperty} from '@angular/cdk/coercion';
1010
import {Directive, ElementRef, EventEmitter, Inject, Input, OnChanges, Output} from '@angular/core';
11-
import {hasModifierKey} from '@angular/cdk/keycodes';
11+
import {hasModifierKey, TAB} from '@angular/cdk/keycodes';
1212
import {MAT_CHIPS_DEFAULT_OPTIONS, MatChipsDefaultOptions} from './chip-default-options';
1313
import {MatChipList} from './chip-list';
1414
import {MatChipTextControl} from './chip-text-control';
@@ -109,6 +109,12 @@ export class MatChipInput implements MatChipTextControl, OnChanges {
109109

110110
/** Utility method to make host definition/tests more clear. */
111111
_keydown(event?: KeyboardEvent) {
112+
// Allow the user's focus to escape when they're tabbing forward. Note that we don't
113+
// want to do this when going backwards, because focus should go back to the first chip.
114+
if (event && event.keyCode === TAB && !hasModifierKey(event, 'shiftKey')) {
115+
this._chipList._allowFocusEscape();
116+
}
117+
112118
this._emitChipEnd(event);
113119
}
114120

src/material/chips/chip-list.ts

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -356,14 +356,8 @@ export class MatChipList extends _MatChipListMixinBase implements MatFormFieldCo
356356
.subscribe(dir => this._keyManager.withHorizontalOrientation(dir));
357357
}
358358

359-
// Prevents the chip list from capturing focus and redirecting
360-
// it back to the first chip when the user tabs out.
361359
this._keyManager.tabOut.pipe(takeUntil(this._destroyed)).subscribe(() => {
362-
this._tabIndex = -1;
363-
setTimeout(() => {
364-
this._tabIndex = this._userTabIndex || 0;
365-
this._changeDetectorRef.markForCheck();
366-
});
360+
this._allowFocusEscape();
367361
});
368362

369363
// When the list changes, re-subscribe
@@ -685,6 +679,22 @@ export class MatChipList extends _MatChipListMixinBase implements MatFormFieldCo
685679
this.stateChanges.next();
686680
}
687681

682+
/**
683+
* Removes the `tabindex` from the chip list and resets it back afterwards, allowing the
684+
* user to tab out of it. This prevents the list from capturing focus and redirecting
685+
* it back to the first chip, creating a focus trap, if it user tries to tab away.
686+
*/
687+
_allowFocusEscape() {
688+
if (this._tabIndex !== -1) {
689+
this._tabIndex = -1;
690+
691+
setTimeout(() => {
692+
this._tabIndex = this._userTabIndex || 0;
693+
this._changeDetectorRef.markForCheck();
694+
});
695+
}
696+
}
697+
688698
private _resetChips() {
689699
this._dropSubscriptions();
690700
this._listenToChipsFocus();

tools/public_api_guard/material/chips.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@ export declare class MatChipList extends _MatChipListMixinBase implements MatFor
117117
readonly valueChange: EventEmitter<any>;
118118
constructor(_elementRef: ElementRef<HTMLElement>, _changeDetectorRef: ChangeDetectorRef, _dir: Directionality, _parentForm: NgForm, _parentFormGroup: FormGroupDirective, _defaultErrorStateMatcher: ErrorStateMatcher,
119119
ngControl: NgControl);
120+
_allowFocusEscape(): void;
120121
_blur(): void;
121122
_focusInput(): void;
122123
_keydown(event: KeyboardEvent): void;

0 commit comments

Comments
 (0)