Skip to content

Commit 2615e48

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 a461f32 commit 2615e48

File tree

5 files changed

+228
-179
lines changed

5 files changed

+228
-179
lines changed

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

Lines changed: 53 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -169,21 +169,19 @@ export class RippleRenderer {
169169
this._mostRecentTransientRipple = rippleRef;
170170
}
171171

172-
// Wait for the ripple element to be completely faded in.
173-
// Once it's faded in, the ripple can be hidden immediately if the mouse is released.
174-
this._runTimeoutOutsideZone(() => {
175-
const isMostRecentTransientRipple = rippleRef === this._mostRecentTransientRipple;
176-
177-
rippleRef.state = RippleState.VISIBLE;
178-
179-
// When the timer runs out while the user has kept their pointer down, we want to
180-
// keep only the persistent ripples and the latest transient ripple. We do this,
181-
// because we don't want stacked transient ripples to appear after their enter
182-
// animation has finished.
183-
if (!config.persistent && (!isMostRecentTransientRipple || !this._isPointerDown)) {
184-
rippleRef.fadeOut();
185-
}
186-
}, duration);
172+
// Do not register the transition event listener if the fade-in and fade-out duration
173+
// are set to zero because the event won't be fired at all.
174+
if (duration || animationConfig.exitDuration) {
175+
this._ngZone.runOutsideAngular(() => {
176+
ripple.addEventListener('transitionend', () => this._finishRippleTransition(rippleRef));
177+
});
178+
}
179+
180+
// In case there is no fade-in transition duration, we need to manually call the
181+
// transition end listener because `transitionend` doesn't fire if there is no transition.
182+
if (!duration) {
183+
this._finishRippleTransition(rippleRef);
184+
}
187185

188186
return rippleRef;
189187
}
@@ -209,15 +207,17 @@ export class RippleRenderer {
209207
const rippleEl = rippleRef.element;
210208
const animationConfig = {...defaultRippleAnimationConfig, ...rippleRef.config.animation};
211209

210+
// This starts the fade-out transition and will fire the transition end listener that
211+
// removes the ripple element from the DOM.
212212
rippleEl.style.transitionDuration = `${animationConfig.exitDuration}ms`;
213213
rippleEl.style.opacity = '0';
214214
rippleRef.state = RippleState.FADING_OUT;
215215

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

223223
/** Fades out all currently active ripples. */
@@ -243,6 +243,39 @@ export class RippleRenderer {
243243
this._triggerElement = element;
244244
}
245245

246+
/** Method that will be called if the fade-in or fade-in transition completed. */
247+
private _finishRippleTransition(rippleRef: RippleRef) {
248+
if (rippleRef.state === RippleState.FADING_IN) {
249+
this._startFadeOutTransition(rippleRef);
250+
} else if (rippleRef.state === RippleState.FADING_OUT) {
251+
this._destroyRipple(rippleRef);
252+
}
253+
}
254+
255+
/**
256+
* Starts the fade-out transition of the given ripple if it's not persistent and the pointer
257+
* is not held down anymore.
258+
*/
259+
private _startFadeOutTransition(rippleRef: RippleRef) {
260+
const isMostRecentTransientRipple = rippleRef === this._mostRecentTransientRipple;
261+
262+
rippleRef.state = RippleState.VISIBLE;
263+
264+
// When the timer runs out while the user has kept their pointer down, we want to
265+
// keep only the persistent ripples and the latest transient ripple. We do this,
266+
// because we don't want stacked transient ripples to appear after their enter
267+
// animation has finished.
268+
if (!rippleRef.config.persistent && (!isMostRecentTransientRipple || !this._isPointerDown)) {
269+
rippleRef.fadeOut();
270+
}
271+
}
272+
273+
/** Destroys the given ripple by removing it from the DOM and updating its state. */
274+
private _destroyRipple(rippleRef: RippleRef) {
275+
rippleRef.state = RippleState.HIDDEN;
276+
rippleRef.element.parentNode!.removeChild(rippleRef.element);
277+
}
278+
246279
/** Function being called whenever the trigger is being pressed using mouse. */
247280
private _onMousedown = (event: MouseEvent) => {
248281
// Screen readers will fire fake mouse events for space/enter. Skip launching a
@@ -297,11 +330,6 @@ export class RippleRenderer {
297330
});
298331
}
299332

300-
/** Runs a timeout outside of the Angular zone to avoid triggering the change detection. */
301-
private _runTimeoutOutsideZone(fn: Function, delay = 0) {
302-
this._ngZone.runOutsideAngular(() => setTimeout(fn, delay));
303-
}
304-
305333
/** Removes previously registered event listeners from the trigger element. */
306334
_removeTriggerEvents() {
307335
if (this._triggerElement) {

0 commit comments

Comments
 (0)