Skip to content

Commit 2f61895

Browse files
mmalerbacrisbeto
andauthored
fix(material-experimental/mdc-chips): decouple removal from animation (#21636)
* fix(material-experimental/mdc-chips): decouple removal from animation * fixup! fix(material-experimental/mdc-chips): decouple removal from animation Co-authored-by: Kristiyan Kostadinov <crisbeto@abv.bg>
1 parent 2232156 commit 2f61895

File tree

9 files changed

+30
-122
lines changed

9 files changed

+30
-122
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: 8 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -213,9 +213,6 @@ export class MatChip extends _MatChipMixinBase implements AfterContentInit, Afte
213213
}
214214
protected _highlighted: boolean = false;
215215

216-
/** Emitted when the user interacts with the remove icon. */
217-
@Output() removeIconInteraction = new EventEmitter<string>();
218-
219216
/** Emitted when the user interacts with the chip. */
220217
@Output() interaction = new EventEmitter<string>();
221218

@@ -231,9 +228,6 @@ export class MatChip extends _MatChipMixinBase implements AfterContentInit, Afte
231228
/** The unstyled chip selector for this component. */
232229
protected basicChipAttrName = 'mat-basic-chip';
233230

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

@@ -276,17 +270,8 @@ export class MatChip extends _MatChipMixinBase implements AfterContentInit, Afte
276270
// input.
277271
},
278272
notifyNavigation: () => this._notifyNavigation(),
279-
notifyTrailingIconInteraction: () =>
280-
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-
},
273+
notifyTrailingIconInteraction: () => {},
274+
notifyRemoval: () => this.remove(),
290275
notifyEditStart:
291276
() => {
292277
this._onEditStart();
@@ -375,24 +360,17 @@ export class MatChip extends _MatChipMixinBase implements AfterContentInit, Afte
375360

376361
ngOnDestroy() {
377362
this.destroyed.emit({chip: this});
378-
this._destroyed.next();
379-
this._destroyed.complete();
380363
this._chipFoundation.destroy();
381364
}
382365

383366
/** Sets up the remove icon chip foundation, and subscribes to remove icon events. */
384-
_initRemoveIcon() {
367+
private _initRemoveIcon() {
385368
if (this.removeIcon) {
386369
this._chipFoundation.setShouldRemoveOnTrailingIconClick(true);
387-
this._listenToRemoveIconInteraction();
388370
this.removeIcon.disabled = this.disabled;
389-
}
390-
}
391371

392-
/** Handles interaction with the remove icon. */
393-
_listenToRemoveIconInteraction() {
394-
this.removeIcon.interaction
395-
.pipe(takeUntil(this._destroyed))
372+
this.removeIcon.interaction
373+
.pipe(takeUntil(this.destroyed))
396374
.subscribe(event => {
397375
// The MDC chip foundation calls stopPropagation() for any trailing icon interaction
398376
// event, even ones it doesn't handle, so we want to avoid passing it keyboard events
@@ -405,7 +383,7 @@ export class MatChip extends _MatChipMixinBase implements AfterContentInit, Afte
405383
return;
406384
}
407385

408-
this._chipFoundation.handleTrailingActionInteraction();
386+
this.remove();
409387

410388
if (isKeyboardEvent && !hasModifierKey(event as KeyboardEvent)) {
411389
const keyCode = (event as KeyboardEvent).keyCode;
@@ -416,6 +394,7 @@ export class MatChip extends _MatChipMixinBase implements AfterContentInit, Afte
416394
}
417395
}
418396
});
397+
}
419398
}
420399

421400
/**
@@ -425,7 +404,7 @@ export class MatChip extends _MatChipMixinBase implements AfterContentInit, Afte
425404
*/
426405
remove(): void {
427406
if (this.removable) {
428-
this._chipFoundation.beginExit();
407+
this.removed.emit({chip: this});
429408
}
430409
}
431410

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)