Skip to content

Commit c6f90c3

Browse files
committed
fix(chips): focus not being restored correctly on chip removal when inside component with animations
Fixes the chip list losing its focus position if a chip is deleted while it's inside a component with animations. Fixes #12374.
1 parent e462f3d commit c6f90c3

File tree

3 files changed

+99
-9
lines changed

3 files changed

+99
-9
lines changed

src/demo-app/chips/chips-demo.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import {COMMA, ENTER} from '@angular/cdk/keycodes';
1010
import {Component} from '@angular/core';
1111
import {MatChipInputEvent, ThemePalette} from '@angular/material';
12+
import {trigger, transition, style, animate} from '@angular/animations';
1213

1314

1415
export interface Person {
@@ -24,7 +25,17 @@ export interface DemoColor {
2425
moduleId: module.id,
2526
selector: 'chips-demo',
2627
templateUrl: 'chips-demo.html',
27-
styleUrls: ['chips-demo.css']
28+
styleUrls: ['chips-demo.css'],
29+
animations: [
30+
trigger(
31+
'oeuoeuoeu', [
32+
transition(':leave', [
33+
style({opacity: 0}),
34+
animate('500ms', style({opacity: 1}))
35+
])
36+
]
37+
)
38+
]
2839
})
2940
export class ChipsDemo {
3041
tabIndex = 0;

src/lib/chips/chip-list.spec.ts

Lines changed: 75 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
11
import {FocusKeyManager} from '@angular/cdk/a11y';
22
import {Directionality, Direction} from '@angular/cdk/bidi';
33
import {BACKSPACE, DELETE, ENTER, LEFT_ARROW, RIGHT_ARROW, SPACE, TAB} from '@angular/cdk/keycodes';
4-
import {createKeyboardEvent, dispatchFakeEvent, dispatchKeyboardEvent} from '@angular/cdk/testing';
4+
import {
5+
createKeyboardEvent,
6+
dispatchFakeEvent,
7+
dispatchKeyboardEvent,
8+
MockNgZone,
9+
} from '@angular/cdk/testing';
510
import {
611
Component,
712
DebugElement,
@@ -10,16 +15,18 @@ import {
1015
ViewChildren,
1116
Type,
1217
Provider,
18+
NgZone,
1319
} from '@angular/core';
1420
import {ComponentFixture, fakeAsync, TestBed, tick} from '@angular/core/testing';
1521
import {FormControl, FormsModule, NgForm, ReactiveFormsModule, Validators} from '@angular/forms';
1622
import {MatFormFieldModule} from '@angular/material/form-field';
1723
import {By} from '@angular/platform-browser';
18-
import {NoopAnimationsModule} from '@angular/platform-browser/animations';
24+
import {NoopAnimationsModule, BrowserAnimationsModule} from '@angular/platform-browser/animations';
1925
import {MatInputModule} from '../input/index';
2026
import {MatChip} from './chip';
2127
import {MatChipInputEvent} from './chip-input';
2228
import {MatChipList, MatChipsModule} from './index';
29+
import {trigger, transition, style, animate} from '@angular/animations';
2330

2431

2532
describe('MatChipList', () => {
@@ -30,6 +37,7 @@ describe('MatChipList', () => {
3037
let testComponent: StandardChipList;
3138
let chips: QueryList<any>;
3239
let manager: FocusKeyManager<MatChip>;
40+
let zone: MockNgZone;
3341

3442
describe('StandardChipList', () => {
3543
describe('basic behaviors', () => {
@@ -154,6 +162,7 @@ describe('MatChipList', () => {
154162
// Focus and blur the middle item
155163
midItem.focus();
156164
midItem._blur();
165+
zone.simulateZoneExit();
157166

158167
// Destroy the middle item
159168
testComponent.remove = 2;
@@ -162,6 +171,32 @@ describe('MatChipList', () => {
162171
// Should not have focus
163172
expect(chipListInstance._keyManager.activeItemIndex).toEqual(-1);
164173
});
174+
175+
it('should move focus to the last chip when the focused chip was deleted inside a' +
176+
'component with animations', fakeAsync(() => {
177+
fixture.destroy();
178+
TestBed.resetTestingModule();
179+
fixture = createComponent(StandardChipListWithAnimations, [], BrowserAnimationsModule);
180+
fixture.detectChanges();
181+
182+
chipListDebugElement = fixture.debugElement.query(By.directive(MatChipList));
183+
chipListNativeElement = chipListDebugElement.nativeElement;
184+
chipListInstance = chipListDebugElement.componentInstance;
185+
testComponent = fixture.debugElement.componentInstance;
186+
chips = chipListInstance.chips;
187+
188+
chips.last.focus();
189+
fixture.detectChanges();
190+
191+
expect(chipListInstance._keyManager.activeItemIndex).toBe(chips.length - 1);
192+
193+
dispatchKeyboardEvent(chips.last._elementRef.nativeElement, 'keydown', BACKSPACE);
194+
fixture.detectChanges();
195+
tick(500);
196+
197+
expect(chipListInstance._keyManager.activeItemIndex).toBe(chips.length - 1);
198+
}));
199+
165200
});
166201
});
167202

@@ -1018,18 +1053,23 @@ describe('MatChipList', () => {
10181053
});
10191054
});
10201055

1021-
function createComponent<T>(component: Type<T>, providers: Provider[] = []): ComponentFixture<T> {
1056+
function createComponent<T>(component: Type<T>, providers: Provider[] = [], animationsModule:
1057+
Type<NoopAnimationsModule> | Type<BrowserAnimationsModule> = NoopAnimationsModule):
1058+
ComponentFixture<T> {
10221059
TestBed.configureTestingModule({
10231060
imports: [
10241061
FormsModule,
10251062
ReactiveFormsModule,
10261063
MatChipsModule,
10271064
MatFormFieldModule,
10281065
MatInputModule,
1029-
NoopAnimationsModule,
1066+
animationsModule,
10301067
],
10311068
declarations: [component],
1032-
providers
1069+
providers: [
1070+
{provide: NgZone, useFactory: () => zone = new MockNgZone()},
1071+
...providers
1072+
]
10331073
}).compileComponents();
10341074

10351075
return TestBed.createComponent<T>(component);
@@ -1293,3 +1333,33 @@ class ChipListWithFormErrorMessages {
12931333
@ViewChild('form') form: NgForm;
12941334
formControl = new FormControl('', Validators.required);
12951335
}
1336+
1337+
1338+
@Component({
1339+
template: `
1340+
<mat-chip-list>
1341+
<mat-chip *ngFor="let i of numbers" (removed)="remove(i)">{{i}}</mat-chip>
1342+
</mat-chip-list>`,
1343+
animations: [
1344+
// For the case we're testing this animation doesn't
1345+
// have to be used anywhere, it just has to be defined.
1346+
trigger('dummyAnimation', [
1347+
transition(':leave', [
1348+
style({opacity: 0}),
1349+
animate('500ms', style({opacity: 1}))
1350+
])
1351+
])
1352+
]
1353+
})
1354+
class StandardChipListWithAnimations {
1355+
numbers = [0, 1, 2, 3, 4];
1356+
1357+
remove(item: number): void {
1358+
const index = this.numbers.indexOf(item);
1359+
1360+
if (index > -1) {
1361+
this.numbers.splice(index, 1);
1362+
}
1363+
}
1364+
}
1365+

src/lib/chips/chip.ts

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ import {
3737
RippleTarget
3838
} from '@angular/material/core';
3939
import {Subject} from 'rxjs';
40+
import { take } from 'rxjs/operators';
4041

4142

4243
/** Represents an event fired on an individual `mat-chip`. */
@@ -218,14 +219,14 @@ export class MatChip extends _MatChipMixinBase implements FocusableOption, OnDes
218219
}
219220

220221
constructor(public _elementRef: ElementRef,
221-
ngZone: NgZone,
222+
private _ngZone: NgZone,
222223
platform: Platform,
223224
@Optional() @Inject(MAT_RIPPLE_GLOBAL_OPTIONS) globalOptions: RippleGlobalOptions) {
224225
super(_elementRef);
225226

226227
this._addHostClassName();
227228

228-
this._chipRipple = new RippleRenderer(this, ngZone, _elementRef, platform);
229+
this._chipRipple = new RippleRenderer(this, _ngZone, _elementRef, platform);
229230
this._chipRipple.setupTriggerEvents(_elementRef.nativeElement);
230231

231232
if (globalOptions) {
@@ -359,7 +360,15 @@ export class MatChip extends _MatChipMixinBase implements FocusableOption, OnDes
359360
}
360361

361362
_blur(): void {
362-
this._hasFocus = false;
363+
// When animations are enabled, Angular may end up removing the chip from the DOM a little
364+
// earlier than usual, causing it to be blurred and throwing off the logic in the chip list
365+
// that moves focus not the next item. To work around the issue, we defer marking the chip
366+
// as not focused until the next time the zone stabilizes.
367+
this._ngZone.onStable
368+
.asObservable()
369+
.pipe(take(1))
370+
.subscribe(() => this._hasFocus = false);
371+
363372
this._onBlur.next({chip: this});
364373
}
365374
}

0 commit comments

Comments
 (0)