Skip to content

Commit 4db14f4

Browse files
committed
Revert "fix(material-experimental/mdc-chips): decouple removal from animation (#21586)" (#21634)
This reverts commit 2d66ecb. (cherry picked from commit 8172d06)
1 parent 25b140d commit 4db14f4

File tree

9 files changed

+117
-29
lines changed

9 files changed

+117
-29
lines changed

src/material-experimental/mdc-chips/chip-grid.spec.ts

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
TAB
1313
} from '@angular/cdk/keycodes';
1414
import {
15+
createFakeEvent,
1516
createKeyboardEvent,
1617
dispatchEvent,
1718
dispatchFakeEvent,
@@ -215,7 +216,7 @@ describe('MDC-based MatChipGrid', () => {
215216
expect(chipGridInstance.focus).toHaveBeenCalled();
216217
});
217218

218-
it('should move focus to the last chip when the focused chip was deleted inside a ' +
219+
it('should move focus to the last chip when the focused chip was deleted inside a' +
219220
'component with animations', fakeAsync(() => {
220221
fixture.destroy();
221222
TestBed.resetTestingModule();
@@ -601,13 +602,15 @@ describe('MDC-based MatChipGrid', () => {
601602

602603
describe('with chip remove', () => {
603604
let chipGrid: MatChipGrid;
605+
let chipElements: DebugElement[];
604606
let chipRemoveDebugElements: DebugElement[];
605607

606608
beforeEach(() => {
607609
fixture = createComponent(ChipGridWithRemove);
608610
fixture.detectChanges();
609611

610612
chipGrid = fixture.debugElement.query(By.directive(MatChipGrid))!.componentInstance;
613+
chipElements = fixture.debugElement.queryAll(By.directive(MatChipRow));
611614
chipRemoveDebugElements = fixture.debugElement.queryAll(By.directive(MatChipRemove));
612615
chips = chipGrid._chips;
613616
});
@@ -620,6 +623,12 @@ describe('MDC-based MatChipGrid', () => {
620623
dispatchMouseEvent(chipRemoveDebugElements[2].nativeElement, 'click');
621624
fixture.detectChanges();
622625

626+
const fakeEvent = createFakeEvent('transitionend');
627+
(fakeEvent as any).propertyName = 'width';
628+
chipElements[2].nativeElement.dispatchEvent(fakeEvent);
629+
630+
fixture.detectChanges();
631+
623632
expect(chips.toArray()[2].value).not.toBe(2, 'Expected the third chip to be removed.');
624633
expect(chipGrid._keyManager.activeRowIndex).toBe(2);
625634
});
@@ -762,10 +771,9 @@ describe('MDC-based MatChipGrid', () => {
762771

763772
dispatchFakeEvent(nativeChips[0], 'focusout');
764773
fixture.detectChanges();
765-
tick();
766-
fixture.detectChanges();
767774
zone.simulateZoneExit();
768775
fixture.detectChanges();
776+
tick();
769777
expect(formField.classList).not.toContain('mat-focused');
770778
}));
771779

@@ -779,6 +787,10 @@ describe('MDC-based MatChipGrid', () => {
779787
chip.focus();
780788
dispatchKeyboardEvent(chip, 'keydown', BACKSPACE);
781789
fixture.detectChanges();
790+
const fakeEvent = createFakeEvent('transitionend');
791+
(fakeEvent as any).propertyName = 'width';
792+
chip.dispatchEvent(fakeEvent);
793+
fixture.detectChanges();
782794
tick();
783795
});
784796

src/material-experimental/mdc-chips/chip-grid.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -505,7 +505,7 @@ export class MatChipGrid extends _MatChipGridMixinBase implements AfterContentIn
505505
const newChipIndex = Math.min(this._lastDestroyedChipIndex, this._chips.length - 1);
506506
this._keyManager.setActiveCell({
507507
row: newChipIndex,
508-
column: Math.max(this._keyManager.activeColumnIndex, 0)
508+
column: this._keyManager.activeColumnIndex
509509
});
510510
} else {
511511
this.focus();

src/material-experimental/mdc-chips/chip-remove.spec.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import {
2+
createFakeEvent,
23
dispatchKeyboardEvent,
34
createKeyboardEvent,
45
dispatchEvent,
@@ -54,6 +55,18 @@ describe('MDC-based Chip Remove', () => {
5455
expect(buttonElement.hasAttribute('type')).toBe(false);
5556
});
5657

58+
it('should start MDC exit animation on click', () => {
59+
let buttonElement = chipNativeElement.querySelector('button')!;
60+
61+
testChip.removable = true;
62+
fixture.detectChanges();
63+
64+
buttonElement.click();
65+
fixture.detectChanges();
66+
67+
expect(chipNativeElement.classList.contains('mdc-chip--exit')).toBe(true);
68+
});
69+
5770
it('should emit (removed) event when exit animation is complete', () => {
5871
let buttonElement = chipNativeElement.querySelector('button')!;
5972

@@ -64,6 +77,10 @@ describe('MDC-based Chip Remove', () => {
6477
buttonElement.click();
6578
fixture.detectChanges();
6679

80+
const fakeEvent = createFakeEvent('transitionend');
81+
(fakeEvent as any).propertyName = 'width';
82+
chipNativeElement.dispatchEvent(fakeEvent);
83+
6784
expect(testChip.didRemove).toHaveBeenCalled();
6885
});
6986

@@ -147,6 +164,10 @@ describe('MDC-based Chip Remove', () => {
147164
dispatchKeyboardEvent(buttonElement, 'keydown', TAB);
148165
fixture.detectChanges();
149166

167+
const fakeEvent = createFakeEvent('transitionend');
168+
(fakeEvent as any).propertyName = 'width';
169+
chipNativeElement.dispatchEvent(fakeEvent);
170+
150171
expect(testChip.didRemove).not.toHaveBeenCalled();
151172
});
152173

src/material-experimental/mdc-chips/chip-row.spec.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import {Directionality} from '@angular/cdk/bidi';
22
import {BACKSPACE, DELETE, RIGHT_ARROW, ENTER} from '@angular/cdk/keycodes';
33
import {
44
createKeyboardEvent,
5+
createFakeEvent,
56
dispatchEvent,
67
dispatchFakeEvent,
78
} from '@angular/cdk/testing/private';
@@ -96,6 +97,10 @@ describe('MDC-based Row Chips', () => {
9697
chipInstance.remove();
9798
fixture.detectChanges();
9899

100+
const fakeEvent = createFakeEvent('transitionend');
101+
(fakeEvent as any).propertyName = 'width';
102+
chipNativeElement.dispatchEvent(fakeEvent);
103+
99104
expect(testComponent.chipRemove).toHaveBeenCalledWith({chip: chipInstance});
100105
});
101106

@@ -122,6 +127,10 @@ describe('MDC-based Row Chips', () => {
122127
chipInstance._keydown(DELETE_EVENT);
123128
fixture.detectChanges();
124129

130+
const fakeEvent = createFakeEvent('transitionend');
131+
(fakeEvent as any).propertyName = 'width';
132+
chipNativeElement.dispatchEvent(fakeEvent);
133+
125134
expect(testComponent.chipRemove).toHaveBeenCalled();
126135
});
127136

@@ -133,6 +142,10 @@ describe('MDC-based Row Chips', () => {
133142
chipInstance._keydown(BACKSPACE_EVENT);
134143
fixture.detectChanges();
135144

145+
const fakeEvent = createFakeEvent('transitionend');
146+
(fakeEvent as any).propertyName = 'width';
147+
chipNativeElement.dispatchEvent(fakeEvent);
148+
136149
expect(testComponent.chipRemove).toHaveBeenCalled();
137150
});
138151

@@ -144,6 +157,10 @@ describe('MDC-based Row Chips', () => {
144157
removeIconInstance.interaction.next(ARROW_KEY_EVENT);
145158
fixture.detectChanges();
146159

160+
const fakeEvent = createFakeEvent('transitionend');
161+
(fakeEvent as any).propertyName = 'width';
162+
chipNativeElement.dispatchEvent(fakeEvent);
163+
147164
expect(testComponent.chipRemove).not.toHaveBeenCalled();
148165
});
149166
});
@@ -162,6 +179,10 @@ describe('MDC-based Row Chips', () => {
162179
chipInstance._keydown(DELETE_EVENT);
163180
fixture.detectChanges();
164181

182+
const fakeEvent = createFakeEvent('transitionend');
183+
(fakeEvent as any).propertyName = 'width';
184+
chipNativeElement.dispatchEvent(fakeEvent);
185+
165186
expect(testComponent.chipRemove).not.toHaveBeenCalled();
166187
});
167188

@@ -174,6 +195,10 @@ describe('MDC-based Row Chips', () => {
174195
chipInstance._keydown(BACKSPACE_EVENT);
175196
fixture.detectChanges();
176197

198+
const fakeEvent = createFakeEvent('transitionend');
199+
(fakeEvent as any).propertyName = 'width';
200+
chipNativeElement.dispatchEvent(fakeEvent);
201+
177202
expect(testComponent.chipRemove).not.toHaveBeenCalled();
178203
});
179204
});

src/material-experimental/mdc-chips/chip-row.ts

Lines changed: 5 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -99,12 +99,6 @@ export class MatChipRow extends MatChip implements AfterContentInit, AfterViewIn
9999
/** The focusable grid cells for this row. Implemented as part of GridKeyManagerRow. */
100100
cells!: HTMLElement[];
101101

102-
/**
103-
* Timeout used to give some time between `focusin` and `focusout`
104-
* in order to determine whether focus has left the chip.
105-
*/
106-
private _focusoutTimeout: number | null;
107-
108102
constructor(
109103
@Inject(DOCUMENT) private readonly _document: any,
110104
changeDetectorRef: ChangeDetectorRef,
@@ -159,25 +153,19 @@ export class MatChipRow extends MatChip implements AfterContentInit, AfterViewIn
159153
* to the other gridcell.
160154
*/
161155
_focusout(event: FocusEvent) {
162-
if (this._focusoutTimeout) {
163-
clearTimeout(this._focusoutTimeout);
164-
}
165-
156+
this._hasFocusInternal = false;
166157
// Wait to see if focus moves to the other gridcell
167-
this._focusoutTimeout = setTimeout(() => {
168-
this._hasFocusInternal = false;
158+
setTimeout(() => {
159+
if (this._hasFocus()) {
160+
return;
161+
}
169162
this._onBlur.next({chip: this});
170163
this._handleInteraction(event);
171164
});
172165
}
173166

174167
/** Records that the chip has focus when one of the gridcells is focused. */
175168
_focusin(event: FocusEvent) {
176-
if (this._focusoutTimeout) {
177-
clearTimeout(this._focusoutTimeout);
178-
this._focusoutTimeout = null;
179-
}
180-
181169
this._hasFocusInternal = true;
182170
this._handleInteraction(event);
183171
}

src/material-experimental/mdc-chips/chip.spec.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import {Directionality} from '@angular/cdk/bidi';
2+
import {createFakeEvent} from '@angular/cdk/testing/private';
23
import {Component, DebugElement, ViewChild} from '@angular/core';
34
import {waitForAsync, ComponentFixture, TestBed} from '@angular/core/testing';
45
import {MatRipple} from '@angular/material-experimental/mdc-core';
@@ -134,9 +135,24 @@ describe('MDC-based MatChip', () => {
134135
chipInstance.remove();
135136
fixture.detectChanges();
136137

138+
const fakeEvent = createFakeEvent('transitionend');
139+
(fakeEvent as any).propertyName = 'width';
140+
chipNativeElement.dispatchEvent(fakeEvent);
141+
137142
expect(testComponent.chipRemove).toHaveBeenCalledWith({chip: chipInstance});
138143
});
139144

145+
it('should make the chip non-focusable when it is removed', () => {
146+
chipInstance.remove();
147+
fixture.detectChanges();
148+
149+
const fakeEvent = createFakeEvent('transitionend');
150+
(fakeEvent as any).propertyName = 'width';
151+
chipNativeElement.dispatchEvent(fakeEvent);
152+
153+
expect(chipNativeElement.style.display).toBe('none');
154+
});
155+
140156
it('should be able to disable ripples with the `[rippleDisabled]` input', () => {
141157
expect(chipRippleInstance.disabled).toBe(false, 'Expected chip ripples to be enabled.');
142158

src/material-experimental/mdc-chips/chip.ts

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,9 @@ export class MatChip extends _MatChipMixinBase implements AfterContentInit, Afte
231231
/** The unstyled chip selector for this component. */
232232
protected basicChipAttrName = 'mat-basic-chip';
233233

234+
/** Subject that emits when the component has been destroyed. */
235+
protected _destroyed = new Subject<void>();
236+
234237
/** The chip's leading icon. */
235238
@ContentChild(MAT_CHIP_AVATAR) leadingIcon: MatChipAvatar;
236239

@@ -275,7 +278,15 @@ export class MatChip extends _MatChipMixinBase implements AfterContentInit, Afte
275278
notifyNavigation: () => this._notifyNavigation(),
276279
notifyTrailingIconInteraction: () =>
277280
this.removeIconInteraction.emit(this.id),
278-
notifyRemoval: () => this.remove(),
281+
notifyRemoval:
282+
() => {
283+
this.removed.emit({chip: this});
284+
285+
// When MDC removes a chip it just transitions it to `width: 0px`
286+
// which means that it's still in the DOM and it's still focusable.
287+
// Make it `display: none` so users can't tab into it.
288+
this._elementRef.nativeElement.style.display = 'none';
289+
},
279290
notifyEditStart:
280291
() => {
281292
this._onEditStart();
@@ -364,17 +375,24 @@ export class MatChip extends _MatChipMixinBase implements AfterContentInit, Afte
364375

365376
ngOnDestroy() {
366377
this.destroyed.emit({chip: this});
378+
this._destroyed.next();
379+
this._destroyed.complete();
367380
this._chipFoundation.destroy();
368381
}
369382

370383
/** Sets up the remove icon chip foundation, and subscribes to remove icon events. */
371-
private _initRemoveIcon() {
384+
_initRemoveIcon() {
372385
if (this.removeIcon) {
373386
this._chipFoundation.setShouldRemoveOnTrailingIconClick(true);
387+
this._listenToRemoveIconInteraction();
374388
this.removeIcon.disabled = this.disabled;
389+
}
390+
}
375391

376-
this.removeIcon.interaction
377-
.pipe(takeUntil(this.destroyed))
392+
/** Handles interaction with the remove icon. */
393+
_listenToRemoveIconInteraction() {
394+
this.removeIcon.interaction
395+
.pipe(takeUntil(this._destroyed))
378396
.subscribe(event => {
379397
// The MDC chip foundation calls stopPropagation() for any trailing icon interaction
380398
// event, even ones it doesn't handle, so we want to avoid passing it keyboard events
@@ -387,7 +405,7 @@ export class MatChip extends _MatChipMixinBase implements AfterContentInit, Afte
387405
return;
388406
}
389407

390-
this.remove();
408+
this._chipFoundation.handleTrailingActionInteraction();
391409

392410
if (isKeyboardEvent && !hasModifierKey(event as KeyboardEvent)) {
393411
const keyCode = (event as KeyboardEvent).keyCode;
@@ -398,7 +416,6 @@ export class MatChip extends _MatChipMixinBase implements AfterContentInit, Afte
398416
}
399417
}
400418
});
401-
}
402419
}
403420

404421
/**
@@ -408,7 +425,7 @@ export class MatChip extends _MatChipMixinBase implements AfterContentInit, Afte
408425
*/
409426
remove(): void {
410427
if (this.removable) {
411-
this.removed.emit({chip: this});
428+
this._chipFoundation.beginExit();
412429
}
413430
}
414431

src/material-experimental/mdc-chips/chips.scss

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,14 @@
1111
cursor: default;
1212

1313
&._mat-animation-noopable {
14+
// MDC's chip removal works by toggling a class on the chip, waiting for its transitions
15+
// to finish and emitting the remove event at the end. The problem is that if our animations
16+
// were disabled via the `NoopAnimationsModule`, the element won't have a transition and
17+
// `transitionend` won't fire. We work around the issue by assigning a very short transition.
18+
transition-duration: 1ms;
19+
20+
// Disables the chip enter animation.
1421
animation: none;
15-
transition: none;
1622

1723
.mdc-chip__checkmark-svg {
1824
transition: none;

src/material-experimental/mdc-chips/testing/chip-harness.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,9 @@ export class MatChipHarness extends ComponentHarness {
4343
async remove(): Promise<void> {
4444
const hostEl = await this.host();
4545
await hostEl.sendKeys(TestKey.DELETE);
46+
47+
// @breaking-change 12.0.0 Remove non-null assertion from `dispatchEvent`.
48+
await hostEl.dispatchEvent!('transitionend', {propertyName: 'width'});
4649
}
4750

4851
/**

0 commit comments

Comments
 (0)