Skip to content

Commit 1bb8429

Browse files
crisbetommalerba
authored andcommitted
fix(material-experimental/mdc-chips): decouple removal from animation (#21586)
Currently the MDC chips automatically trigger an animation that collapses the chip to 0x0 dimensions, but it still keeps it in the DOM unless the consumer updates their data source. This is misleading and there's no way for the consumer to see it. These changes bring the chip in line with our current behavior where we only dispatch the remove event and then it's up to the consumer to remove the chip. Making these changes also revealed some tests that were passing by accident and weren't actually testing the things they were supposed to, because the `animationend` event wasn't being dispatched. I've tweaked them in order to get them to work as expected. Fixes #21561. (cherry picked from commit 2d66ecb)
1 parent f76369a commit 1bb8429

File tree

9 files changed

+29
-117
lines changed

9 files changed

+29
-117
lines changed

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

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

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

603602
describe('with chip remove', () => {
604603
let chipGrid: MatChipGrid;
605-
let chipElements: DebugElement[];
606604
let chipRemoveDebugElements: DebugElement[];
607605

608606
beforeEach(() => {
609607
fixture = createComponent(ChipGridWithRemove);
610608
fixture.detectChanges();
611609

612610
chipGrid = fixture.debugElement.query(By.directive(MatChipGrid))!.componentInstance;
613-
chipElements = fixture.debugElement.queryAll(By.directive(MatChipRow));
614611
chipRemoveDebugElements = fixture.debugElement.queryAll(By.directive(MatChipRemove));
615612
chips = chipGrid._chips;
616613
});
@@ -623,12 +620,6 @@ describe('MDC-based MatChipGrid', () => {
623620
dispatchMouseEvent(chipRemoveDebugElements[2].nativeElement, 'click');
624621
fixture.detectChanges();
625622

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

772763
dispatchFakeEvent(nativeChips[0], 'focusout');
773764
fixture.detectChanges();
765+
tick();
766+
fixture.detectChanges();
774767
zone.simulateZoneExit();
775768
fixture.detectChanges();
776-
tick();
777769
expect(formField.classList).not.toContain('mat-focused');
778770
}));
779771

@@ -787,10 +779,6 @@ describe('MDC-based MatChipGrid', () => {
787779
chip.focus();
788780
dispatchKeyboardEvent(chip, 'keydown', BACKSPACE);
789781
fixture.detectChanges();
790-
const fakeEvent = createFakeEvent('transitionend');
791-
(fakeEvent as any).propertyName = 'width';
792-
chip.dispatchEvent(fakeEvent);
793-
fixture.detectChanges();
794782
tick();
795783
});
796784

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: this._keyManager.activeColumnIndex
508+
column: Math.max(this._keyManager.activeColumnIndex, 0)
509509
});
510510
} else {
511511
this.focus();

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

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

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-
7057
it('should emit (removed) event when exit animation is complete', () => {
7158
let buttonElement = chipNativeElement.querySelector('button')!;
7259

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

80-
const fakeEvent = createFakeEvent('transitionend');
81-
(fakeEvent as any).propertyName = 'width';
82-
chipNativeElement.dispatchEvent(fakeEvent);
83-
8467
expect(testChip.didRemove).toHaveBeenCalled();
8568
});
8669

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

167-
const fakeEvent = createFakeEvent('transitionend');
168-
(fakeEvent as any).propertyName = 'width';
169-
chipNativeElement.dispatchEvent(fakeEvent);
170-
171150
expect(testChip.didRemove).not.toHaveBeenCalled();
172151
});
173152

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

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

100-
const fakeEvent = createFakeEvent('transitionend');
101-
(fakeEvent as any).propertyName = 'width';
102-
chipNativeElement.dispatchEvent(fakeEvent);
103-
10499
expect(testComponent.chipRemove).toHaveBeenCalledWith({chip: chipInstance});
105100
});
106101

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

130-
const fakeEvent = createFakeEvent('transitionend');
131-
(fakeEvent as any).propertyName = 'width';
132-
chipNativeElement.dispatchEvent(fakeEvent);
133-
134125
expect(testComponent.chipRemove).toHaveBeenCalled();
135126
});
136127

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

145-
const fakeEvent = createFakeEvent('transitionend');
146-
(fakeEvent as any).propertyName = 'width';
147-
chipNativeElement.dispatchEvent(fakeEvent);
148-
149136
expect(testComponent.chipRemove).toHaveBeenCalled();
150137
});
151138

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

160-
const fakeEvent = createFakeEvent('transitionend');
161-
(fakeEvent as any).propertyName = 'width';
162-
chipNativeElement.dispatchEvent(fakeEvent);
163-
164147
expect(testComponent.chipRemove).not.toHaveBeenCalled();
165148
});
166149
});
@@ -179,10 +162,6 @@ describe('MDC-based Row Chips', () => {
179162
chipInstance._keydown(DELETE_EVENT);
180163
fixture.detectChanges();
181164

182-
const fakeEvent = createFakeEvent('transitionend');
183-
(fakeEvent as any).propertyName = 'width';
184-
chipNativeElement.dispatchEvent(fakeEvent);
185-
186165
expect(testComponent.chipRemove).not.toHaveBeenCalled();
187166
});
188167

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

198-
const fakeEvent = createFakeEvent('transitionend');
199-
(fakeEvent as any).propertyName = 'width';
200-
chipNativeElement.dispatchEvent(fakeEvent);
201-
202177
expect(testComponent.chipRemove).not.toHaveBeenCalled();
203178
});
204179
});

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

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,12 @@ 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+
102108
constructor(
103109
@Inject(DOCUMENT) private readonly _document: any,
104110
changeDetectorRef: ChangeDetectorRef,
@@ -153,19 +159,25 @@ export class MatChipRow extends MatChip implements AfterContentInit, AfterViewIn
153159
* to the other gridcell.
154160
*/
155161
_focusout(event: FocusEvent) {
156-
this._hasFocusInternal = false;
162+
if (this._focusoutTimeout) {
163+
clearTimeout(this._focusoutTimeout);
164+
}
165+
157166
// Wait to see if focus moves to the other gridcell
158-
setTimeout(() => {
159-
if (this._hasFocus()) {
160-
return;
161-
}
167+
this._focusoutTimeout = setTimeout(() => {
168+
this._hasFocusInternal = false;
162169
this._onBlur.next({chip: this});
163170
this._handleInteraction(event);
164171
});
165172
}
166173

167174
/** Records that the chip has focus when one of the gridcells is focused. */
168175
_focusin(event: FocusEvent) {
176+
if (this._focusoutTimeout) {
177+
clearTimeout(this._focusoutTimeout);
178+
this._focusoutTimeout = null;
179+
}
180+
169181
this._hasFocusInternal = true;
170182
this._handleInteraction(event);
171183
}

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

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

138-
const fakeEvent = createFakeEvent('transitionend');
139-
(fakeEvent as any).propertyName = 'width';
140-
chipNativeElement.dispatchEvent(fakeEvent);
141-
142137
expect(testComponent.chipRemove).toHaveBeenCalledWith({chip: chipInstance});
143138
});
144139

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-
156140
it('should be able to disable ripples with the `[rippleDisabled]` input', () => {
157141
expect(chipRippleInstance.disabled).toBe(false, 'Expected chip ripples to be enabled.');
158142

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

Lines changed: 7 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -231,9 +231,6 @@ 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-
237234
/** The chip's leading icon. */
238235
@ContentChild(MAT_CHIP_AVATAR) leadingIcon: MatChipAvatar;
239236

@@ -278,15 +275,7 @@ export class MatChip extends _MatChipMixinBase implements AfterContentInit, Afte
278275
notifyNavigation: () => this._notifyNavigation(),
279276
notifyTrailingIconInteraction: () =>
280277
this.removeIconInteraction.emit(this.id),
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-
},
278+
notifyRemoval: () => this.remove(),
290279
notifyEditStart:
291280
() => {
292281
this._onEditStart();
@@ -375,24 +364,17 @@ export class MatChip extends _MatChipMixinBase implements AfterContentInit, Afte
375364

376365
ngOnDestroy() {
377366
this.destroyed.emit({chip: this});
378-
this._destroyed.next();
379-
this._destroyed.complete();
380367
this._chipFoundation.destroy();
381368
}
382369

383370
/** Sets up the remove icon chip foundation, and subscribes to remove icon events. */
384-
_initRemoveIcon() {
371+
private _initRemoveIcon() {
385372
if (this.removeIcon) {
386373
this._chipFoundation.setShouldRemoveOnTrailingIconClick(true);
387-
this._listenToRemoveIconInteraction();
388374
this.removeIcon.disabled = this.disabled;
389-
}
390-
}
391375

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

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

410392
if (isKeyboardEvent && !hasModifierKey(event as KeyboardEvent)) {
411393
const keyCode = (event as KeyboardEvent).keyCode;
@@ -416,6 +398,7 @@ export class MatChip extends _MatChipMixinBase implements AfterContentInit, Afte
416398
}
417399
}
418400
});
401+
}
419402
}
420403

421404
/**
@@ -425,7 +408,7 @@ export class MatChip extends _MatChipMixinBase implements AfterContentInit, Afte
425408
*/
426409
remove(): void {
427410
if (this.removable) {
428-
this._chipFoundation.beginExit();
411+
this.removed.emit({chip: this});
429412
}
430413
}
431414

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

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,8 @@
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.
2114
animation: none;
15+
transition: none;
2216

2317
.mdc-chip__checkmark-svg {
2418
transition: none;

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

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -43,9 +43,6 @@ 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'});
4946
}
5047

5148
/**

0 commit comments

Comments
 (0)