@@ -28,6 +28,7 @@ export interface RippleTarget {
28
28
interface RippleEventListeners {
29
29
onTransitionEnd : EventListener ;
30
30
onTransitionCancel : EventListener ;
31
+ fallbackTimer : ReturnType < typeof setTimeout > | null ;
31
32
}
32
33
33
34
/**
@@ -193,14 +194,31 @@ export class RippleRenderer implements EventListenerObject {
193
194
// are set to zero. The events won't fire anyway and we can save resources here.
194
195
if ( ! animationForciblyDisabledThroughCss && ( enterDuration || animationConfig . exitDuration ) ) {
195
196
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
+ } ;
197
205
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
+
198
216
ripple . addEventListener ( 'transitionend' , onTransitionEnd ) ;
199
217
// If the transition is cancelled (e.g. due to DOM removal), we destroy the ripple
200
218
// directly as otherwise we would keep it part of the ripple container forever.
201
219
// https://www.w3.org/TR/css-transitions-1/#:~:text=no%20longer%20in%20the%20document.
202
220
ripple . addEventListener ( 'transitioncancel' , onTransitionCancel ) ;
203
- eventListeners = { onTransitionEnd, onTransitionCancel} ;
221
+ eventListeners = { onTransitionEnd, onTransitionCancel, fallbackTimer } ;
204
222
} ) ;
205
223
}
206
224
@@ -352,6 +370,9 @@ export class RippleRenderer implements EventListenerObject {
352
370
if ( eventListeners !== null ) {
353
371
rippleRef . element . removeEventListener ( 'transitionend' , eventListeners . onTransitionEnd ) ;
354
372
rippleRef . element . removeEventListener ( 'transitioncancel' , eventListeners . onTransitionCancel ) ;
373
+ if ( eventListeners . fallbackTimer !== null ) {
374
+ clearTimeout ( eventListeners . fallbackTimer ) ;
375
+ }
355
376
}
356
377
rippleRef . element . remove ( ) ;
357
378
}
0 commit comments