Skip to content

Commit 0c7325c

Browse files
committed
fix(ripple): not fading out on touch devices
* Makes the ripple animations no longer dependent on `setTimeout` that does not always fire properly / or within the specified duration. (related chrome issue: https://bugs.chromium.org/p/chromium/issues/detail?id=567800) * Fix indentation of a few ripple tests * Fixes that the speed factor tests are basically not checking anything (even though they will be removed in the future; they need to pass right now) Fixes #12470
1 parent 8424209 commit 0c7325c

File tree

9 files changed

+268
-207
lines changed

9 files changed

+268
-207
lines changed

src/material-experimental/mdc-list/list.spec.ts

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,10 @@
1-
import {waitForAsync, TestBed, fakeAsync, tick} from '@angular/core/testing';
1+
import {fakeAsync, TestBed, waitForAsync} from '@angular/core/testing';
2+
import {dispatchFakeEvent, dispatchMouseEvent} from '@angular/cdk/testing/private';
23
import {Component, QueryList, ViewChildren} from '@angular/core';
3-
import {defaultRippleAnimationConfig} from '@angular/material-experimental/mdc-core';
4-
import {dispatchMouseEvent} from '../../cdk/testing/private';
54
import {By} from '@angular/platform-browser';
65
import {MatListItem, MatListModule} from './index';
76

87
describe('MDC-based MatList', () => {
9-
// Default ripple durations used for testing.
10-
const {enterDuration, exitDuration} = defaultRippleAnimationConfig;
11-
128
beforeEach(waitForAsync(() => {
139
TestBed.configureTestingModule({
1410
imports: [MatListModule],
@@ -245,11 +241,15 @@ describe('MDC-based MatList', () => {
245241
dispatchMouseEvent(rippleTarget, 'mousedown');
246242
dispatchMouseEvent(rippleTarget, 'mouseup');
247243

244+
// Flush the ripple enter animation.
245+
dispatchFakeEvent(rippleTarget.querySelector('.mat-ripple-element')!, 'transitionend');
246+
248247
expect(rippleTarget.querySelectorAll('.mat-ripple-element').length)
249248
.withContext('Expected ripples to be enabled by default.').toBe(1);
250249

251-
// Wait for the ripples to go away.
252-
tick(enterDuration + exitDuration);
250+
// Flush the ripple exit animation.
251+
dispatchFakeEvent(rippleTarget.querySelector('.mat-ripple-element')!, 'transitionend');
252+
253253
expect(rippleTarget.querySelectorAll('.mat-ripple-element').length)
254254
.withContext('Expected ripples to go away.').toBe(0);
255255

@@ -273,11 +273,15 @@ describe('MDC-based MatList', () => {
273273
dispatchMouseEvent(rippleTarget, 'mousedown');
274274
dispatchMouseEvent(rippleTarget, 'mouseup');
275275

276+
// Flush the ripple enter animation.
277+
dispatchFakeEvent(rippleTarget.querySelector('.mat-ripple-element')!, 'transitionend');
278+
276279
expect(rippleTarget.querySelectorAll('.mat-ripple-element').length)
277280
.withContext('Expected ripples to be enabled by default.').toBe(1);
278281

279-
// Wait for the ripples to go away.
280-
tick(enterDuration + exitDuration);
282+
// Flush the ripple exit animation.
283+
dispatchFakeEvent(rippleTarget.querySelector('.mat-ripple-element')!, 'transitionend');
284+
281285
expect(rippleTarget.querySelectorAll('.mat-ripple-element').length)
282286
.withContext('Expected ripples to go away.').toBe(0);
283287

src/material-experimental/mdc-list/selection-list.spec.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import {
2222
waitForAsync,
2323
} from '@angular/core/testing';
2424
import {FormControl, FormsModule, NgModel, ReactiveFormsModule} from '@angular/forms';
25-
import {defaultRippleAnimationConfig, ThemePalette} from '@angular/material-experimental/mdc-core';
25+
import {ThemePalette} from '@angular/material-experimental/mdc-core';
2626
import {By} from '@angular/platform-browser';
2727
import {numbers} from '@material/list';
2828
import {
@@ -523,16 +523,18 @@ describe('MDC-based MatSelectionList without forms', () => {
523523
fakeAsync(() => {
524524
const rippleTarget = fixture.nativeElement
525525
.querySelector('.mat-mdc-list-option:not(.mdc-list-item--disabled)');
526-
const {enterDuration, exitDuration} = defaultRippleAnimationConfig;
527-
528526
dispatchMouseEvent(rippleTarget, 'mousedown');
529527
dispatchMouseEvent(rippleTarget, 'mouseup');
530528

529+
// Flush the ripple enter animation.
530+
dispatchFakeEvent(rippleTarget.querySelector('.mat-ripple-element')!, 'transitionend');
531+
531532
expect(rippleTarget.querySelectorAll('.mat-ripple-element').length)
532533
.withContext('Expected ripples to be enabled by default.').toBe(1);
533534

534-
// Wait for the ripples to go away.
535-
tick(enterDuration + exitDuration);
535+
// Flush the ripple exit animation.
536+
dispatchFakeEvent(rippleTarget.querySelector('.mat-ripple-element')!, 'transitionend');
537+
536538
expect(rippleTarget.querySelectorAll('.mat-ripple-element').length)
537539
.withContext('Expected ripples to go away.').toBe(0);
538540

src/material-experimental/mdc-slider/slider.spec.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import {BidiModule, Directionality} from '@angular/cdk/bidi';
1010
import {Platform} from '@angular/cdk/platform';
1111
import {
12+
dispatchFakeEvent,
1213
dispatchMouseEvent,
1314
dispatchPointerEvent,
1415
dispatchTouchEvent,
@@ -289,8 +290,14 @@ describe('MDC-based MatSlider' , () => {
289290
}));
290291

291292
function isRippleVisible(selector: string) {
292-
tick(500);
293-
return !!document.querySelector(`.mat-mdc-slider-${selector}-ripple`);
293+
flushRippleTransitions();
294+
return thumbElement.querySelector(`.mat-mdc-slider-${selector}-ripple`) !== null;
295+
}
296+
297+
function flushRippleTransitions() {
298+
thumbElement.querySelectorAll('.mat-ripple-element').forEach(el => {
299+
dispatchFakeEvent(el, 'transitionend');
300+
});
294301
}
295302

296303
function blur() {

src/material-experimental/mdc-tabs/tab-group.spec.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -192,7 +192,6 @@ describe('MDC-based MatTabGroup', () => {
192192
.withContext('Expected no ripples to show up initially.').toBe(0);
193193

194194
dispatchFakeEvent(tabLabel.nativeElement, 'mousedown');
195-
dispatchFakeEvent(tabLabel.nativeElement, 'mouseup');
196195

197196
expect(testElement.querySelectorAll('.mat-ripple-element').length)
198197
.withContext('Expected one ripple to show up on label mousedown.').toBe(1);

src/material/core/ripple/ripple-renderer.ts

Lines changed: 54 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -152,21 +152,19 @@ export class RippleRenderer implements EventListenerObject {
152152
this._mostRecentTransientRipple = rippleRef;
153153
}
154154

155-
// Wait for the ripple element to be completely faded in.
156-
// Once it's faded in, the ripple can be hidden immediately if the mouse is released.
157-
this._runTimeoutOutsideZone(() => {
158-
const isMostRecentTransientRipple = rippleRef === this._mostRecentTransientRipple;
159-
160-
rippleRef.state = RippleState.VISIBLE;
161-
162-
// When the timer runs out while the user has kept their pointer down, we want to
163-
// keep only the persistent ripples and the latest transient ripple. We do this,
164-
// because we don't want stacked transient ripples to appear after their enter
165-
// animation has finished.
166-
if (!config.persistent && (!isMostRecentTransientRipple || !this._isPointerDown)) {
167-
rippleRef.fadeOut();
168-
}
169-
}, duration);
155+
// Do not register the `transition` event listener if fade-in and fade-out duration
156+
// are set to zero. The events won't fire anyway and we can save resources here.
157+
if (duration || animationConfig.exitDuration) {
158+
this._ngZone.runOutsideAngular(() => {
159+
ripple.addEventListener('transitionend', () => this._finishRippleTransition(rippleRef));
160+
});
161+
}
162+
163+
// In case there is no fade-in transition duration, we need to manually call the transition
164+
// end listener because `transitionend` doesn't fire if there is no transition.
165+
if (!duration) {
166+
this._finishRippleTransition(rippleRef);
167+
}
170168

171169
return rippleRef;
172170
}
@@ -192,15 +190,17 @@ export class RippleRenderer implements EventListenerObject {
192190
const rippleEl = rippleRef.element;
193191
const animationConfig = {...defaultRippleAnimationConfig, ...rippleRef.config.animation};
194192

193+
// This starts the fade-out transition and will fire the transition end listener that
194+
// removes the ripple element from the DOM.
195195
rippleEl.style.transitionDuration = `${animationConfig.exitDuration}ms`;
196196
rippleEl.style.opacity = '0';
197197
rippleRef.state = RippleState.FADING_OUT;
198198

199-
// Once the ripple faded out, the ripple can be safely removed from the DOM.
200-
this._runTimeoutOutsideZone(() => {
201-
rippleRef.state = RippleState.HIDDEN;
202-
rippleEl.parentNode!.removeChild(rippleEl);
203-
}, animationConfig.exitDuration);
199+
// In case there is no fade-out transition duration, we need to manually call the
200+
// transition end listener because `transitionend` doesn't fire if there is no transition.
201+
if (!animationConfig.exitDuration) {
202+
this._finishRippleTransition(rippleRef);
203+
}
204204
}
205205

206206
/** Fades out all currently active ripples. */
@@ -254,6 +254,40 @@ export class RippleRenderer implements EventListenerObject {
254254
}
255255
}
256256

257+
/** Method that will be called if the fade-in or fade-in transition completed. */
258+
private _finishRippleTransition(rippleRef: RippleRef) {
259+
if (rippleRef.state === RippleState.FADING_IN) {
260+
this._startFadeOutTransition(rippleRef);
261+
} else if (rippleRef.state === RippleState.FADING_OUT) {
262+
this._destroyRipple(rippleRef);
263+
}
264+
}
265+
266+
/**
267+
* Starts the fade-out transition of the given ripple if it's not persistent and the pointer
268+
* is not held down anymore.
269+
*/
270+
private _startFadeOutTransition(rippleRef: RippleRef) {
271+
const isMostRecentTransientRipple = rippleRef === this._mostRecentTransientRipple;
272+
const {persistent} = rippleRef.config;
273+
274+
rippleRef.state = RippleState.VISIBLE;
275+
276+
// When the timer runs out while the user has kept their pointer down, we want to
277+
// keep only the persistent ripples and the latest transient ripple. We do this,
278+
// because we don't want stacked transient ripples to appear after their enter
279+
// animation has finished.
280+
if (!persistent && (!isMostRecentTransientRipple || !this._isPointerDown)) {
281+
rippleRef.fadeOut();
282+
}
283+
}
284+
285+
/** Destroys the given ripple by removing it from the DOM and updating its state. */
286+
private _destroyRipple(rippleRef: RippleRef) {
287+
rippleRef.state = RippleState.HIDDEN;
288+
rippleRef.element.parentNode!.removeChild(rippleRef.element);
289+
}
290+
257291
/** Function being called whenever the trigger is being pressed using mouse. */
258292
private _onMousedown(event: MouseEvent) {
259293
// Screen readers will fire fake mouse events for space/enter. Skip launching a
@@ -308,11 +342,6 @@ export class RippleRenderer implements EventListenerObject {
308342
});
309343
}
310344

311-
/** Runs a timeout outside of the Angular zone to avoid triggering the change detection. */
312-
private _runTimeoutOutsideZone(fn: Function, delay = 0) {
313-
this._ngZone.runOutsideAngular(() => setTimeout(fn, delay));
314-
}
315-
316345
/** Registers event listeners for a given list of events. */
317346
private _registerEvents(eventTypes: string[]) {
318347
this._ngZone.runOutsideAngular(() => {

0 commit comments

Comments
 (0)