From e5c5f151cc3a5293f629bfa84bcddb0b391cf268 Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Thu, 27 Jun 2024 15:57:06 +0000 Subject: [PATCH] fix(material/core): add fallback if ripples get stuck (#29323) 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. (cherry picked from commit 674538b778e75d229ada06d19c362752db3a18cc) --- src/material/core/ripple/ripple-renderer.ts | 25 +++++++++++++++++++-- src/material/slider/slider.spec.ts | 4 ++-- 2 files changed, 25 insertions(+), 4 deletions(-) diff --git a/src/material/core/ripple/ripple-renderer.ts b/src/material/core/ripple/ripple-renderer.ts index dc0296ce1276..89309484a8a7 100644 --- a/src/material/core/ripple/ripple-renderer.ts +++ b/src/material/core/ripple/ripple-renderer.ts @@ -28,6 +28,7 @@ export interface RippleTarget { interface RippleEventListeners { onTransitionEnd: EventListener; onTransitionCancel: EventListener; + fallbackTimer: ReturnType | null; } /** @@ -193,14 +194,31 @@ export class RippleRenderer implements EventListenerObject { // are set to zero. The events won't fire anyway and we can save resources here. if (!animationForciblyDisabledThroughCss && (enterDuration || animationConfig.exitDuration)) { this._ngZone.runOutsideAngular(() => { - const onTransitionEnd = () => this._finishRippleTransition(rippleRef); + const onTransitionEnd = () => { + // Clear the fallback timer since the transition fired correctly. + if (eventListeners) { + eventListeners.fallbackTimer = null; + } + clearTimeout(fallbackTimer); + this._finishRippleTransition(rippleRef); + }; const onTransitionCancel = () => this._destroyRipple(rippleRef); + + // In some cases where there's a higher load on the browser, it can choose not to dispatch + // neither `transitionend` nor `transitioncancel` (see b/227356674). This timer serves as a + // fallback for such cases so that the ripple doesn't become stuck. We add a 100ms buffer + // because timers aren't precise. Note that another approach can be to transition the ripple + // to the `VISIBLE` state immediately above and to `FADING_IN` afterwards inside + // `transitionstart`. We go with the timer because it's one less event listener and + // it's less likely to break existing tests. + const fallbackTimer = setTimeout(onTransitionCancel, enterDuration + 100); + ripple.addEventListener('transitionend', onTransitionEnd); // If the transition is cancelled (e.g. due to DOM removal), we destroy the ripple // directly as otherwise we would keep it part of the ripple container forever. // https://www.w3.org/TR/css-transitions-1/#:~:text=no%20longer%20in%20the%20document. ripple.addEventListener('transitioncancel', onTransitionCancel); - eventListeners = {onTransitionEnd, onTransitionCancel}; + eventListeners = {onTransitionEnd, onTransitionCancel, fallbackTimer}; }); } @@ -352,6 +370,9 @@ export class RippleRenderer implements EventListenerObject { if (eventListeners !== null) { rippleRef.element.removeEventListener('transitionend', eventListeners.onTransitionEnd); rippleRef.element.removeEventListener('transitioncancel', eventListeners.onTransitionCancel); + if (eventListeners.fallbackTimer !== null) { + clearTimeout(eventListeners.fallbackTimer); + } } rippleRef.element.remove(); } diff --git a/src/material/slider/slider.spec.ts b/src/material/slider/slider.spec.ts index a722dd24f30d..6da54806fdf8 100644 --- a/src/material/slider/slider.spec.ts +++ b/src/material/slider/slider.spec.ts @@ -691,8 +691,8 @@ describe('MDC-based MatSlider', () => { it('should show the active ripple on pointerdown', fakeAsync(() => { expect(isRippleVisible('active')).toBeFalse(); pointerdown(); - flush(); expect(isRippleVisible('active')).toBeTrue(); + flush(); })); it('should hide the active ripple on pointerup', fakeAsync(() => { @@ -1831,7 +1831,7 @@ function setValueByClick( input.focus(); dispatchPointerEvent(inputElement, 'pointerup', x, y); dispatchEvent(input._hostElement, new Event('change')); - tick(); + flush(); } /** Slides the MatSlider's thumb to the given value. */