Skip to content

Commit c09194f

Browse files
committed
fix(material/core): add fallback if ripples get stuck
Currently ripples assume that after the transition is started, either a `transitionend` or `transitioncancel` event will occur. That doesn't seem to be the case in some browser/OS combinations and when there's a high load on the browser. These changes add a fallback timer that will clear the ripples if they get stuck. Fixes #29159.
1 parent 202f058 commit c09194f

File tree

1 file changed

+23
-2
lines changed

1 file changed

+23
-2
lines changed

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

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ export interface RippleTarget {
2828
interface RippleEventListeners {
2929
onTransitionEnd: EventListener;
3030
onTransitionCancel: EventListener;
31+
fallbackTimer: ReturnType<typeof setTimeout> | null;
3132
}
3233

3334
/**
@@ -193,14 +194,31 @@ export class RippleRenderer implements EventListenerObject {
193194
// are set to zero. The events won't fire anyway and we can save resources here.
194195
if (!animationForciblyDisabledThroughCss && (enterDuration || animationConfig.exitDuration)) {
195196
this._ngZone.runOutsideAngular(() => {
196-
const onTransitionEnd = () => this._finishRippleTransition(rippleRef);
197+
const onTransitionEnd = () => {
198+
// Clear the fallback timer since the transition fired correctly.
199+
if (eventListeners) {
200+
eventListeners.fallbackTimer = null;
201+
}
202+
clearTimeout(fallbackTimer);
203+
this._finishRippleTransition(rippleRef);
204+
};
197205
const onTransitionCancel = () => this._destroyRipple(rippleRef);
206+
207+
// In some cases where there's a higher load on the browser, it can choose not to dispatch
208+
// neither `transitionend` nor `transitioncancel` (see b/227356674). This timer serves as a
209+
// fallback for such cases so that the ripple doesn't become stuck. We add a 100ms buffer
210+
// because timers aren't precise. Note that another approach can be to transition the ripple
211+
// to the `VISIBLE` state immediately above and to `FADING_IN` afterwards inside
212+
// `transitionstart`. We go with the timer because it's one less event listener and
213+
// it's less likely to break existing tests.
214+
const fallbackTimer = setTimeout(onTransitionCancel, enterDuration + 100);
215+
198216
ripple.addEventListener('transitionend', onTransitionEnd);
199217
// If the transition is cancelled (e.g. due to DOM removal), we destroy the ripple
200218
// directly as otherwise we would keep it part of the ripple container forever.
201219
// https://www.w3.org/TR/css-transitions-1/#:~:text=no%20longer%20in%20the%20document.
202220
ripple.addEventListener('transitioncancel', onTransitionCancel);
203-
eventListeners = {onTransitionEnd, onTransitionCancel};
221+
eventListeners = {onTransitionEnd, onTransitionCancel, fallbackTimer};
204222
});
205223
}
206224

@@ -352,6 +370,9 @@ export class RippleRenderer implements EventListenerObject {
352370
if (eventListeners !== null) {
353371
rippleRef.element.removeEventListener('transitionend', eventListeners.onTransitionEnd);
354372
rippleRef.element.removeEventListener('transitioncancel', eventListeners.onTransitionCancel);
373+
if (eventListeners.fallbackTimer !== null) {
374+
clearTimeout(eventListeners.fallbackTimer);
375+
}
355376
}
356377
rippleRef.element.remove();
357378
}

0 commit comments

Comments
 (0)