Skip to content

Commit 5edee00

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 a620fca commit 5edee00

File tree

3 files changed

+201
-155
lines changed

3 files changed

+201
-155
lines changed

src/lib/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. */
@@ -242,6 +242,39 @@ export class RippleRenderer {
242242
this._triggerElement = element;
243243
}
244244

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

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

0 commit comments

Comments
 (0)