Skip to content

Commit 73f2fa3

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 73f2fa3

File tree

2 files changed

+25
-4
lines changed

2 files changed

+25
-4
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
}

src/material/slider/slider.spec.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -691,8 +691,8 @@ describe('MDC-based MatSlider', () => {
691691
it('should show the active ripple on pointerdown', fakeAsync(() => {
692692
expect(isRippleVisible('active')).toBeFalse();
693693
pointerdown();
694-
flush();
695694
expect(isRippleVisible('active')).toBeTrue();
695+
flush();
696696
}));
697697

698698
it('should hide the active ripple on pointerup', fakeAsync(() => {
@@ -1831,7 +1831,7 @@ function setValueByClick(
18311831
input.focus();
18321832
dispatchPointerEvent(inputElement, 'pointerup', x, y);
18331833
dispatchEvent(input._hostElement, new Event('change'));
1834-
tick();
1834+
flush();
18351835
}
18361836

18371837
/** Slides the MatSlider's thumb to the given value. */

0 commit comments

Comments
 (0)