@@ -23,6 +23,12 @@ export interface RippleTarget {
2323 rippleDisabled : boolean ;
2424}
2525
26+ /** Interfaces the defines ripple element transition event listeners. */
27+ interface RippleEventListeners {
28+ onTransitionEnd : EventListener ;
29+ onTransitionCancel : EventListener ;
30+ }
31+
2632// TODO: import these values from `@material/ripple` eventually.
2733/**
2834 * Default ripple animation configuration for ripples without an explicit
@@ -65,8 +71,13 @@ export class RippleRenderer implements EventListenerObject {
6571 /** Whether the pointer is currently down or not. */
6672 private _isPointerDown = false ;
6773
68- /** Set of currently active ripple references. */
69- private _activeRipples = new Set < RippleRef > ( ) ;
74+ /**
75+ * Map of currently active ripple references.
76+ * The ripple reference is mapped to its element event listeners.
77+ * The reason why `| null` is used is that event listeners are added only
78+ * when the condition is truthy (see the `_startFadeOutTransition` method).
79+ */
80+ private _activeRipples = new Map < RippleRef , RippleEventListeners | null > ( ) ;
7081
7182 /** Latest non-persistent ripple that was triggered. */
7283 private _mostRecentTransientRipple : RippleRef | null ;
@@ -164,25 +175,30 @@ export class RippleRenderer implements EventListenerObject {
164175
165176 rippleRef . state = RippleState . FADING_IN ;
166177
167- // Add the ripple reference to the list of all active ripples.
168- this . _activeRipples . add ( rippleRef ) ;
169-
170178 if ( ! config . persistent ) {
171179 this . _mostRecentTransientRipple = rippleRef ;
172180 }
173181
182+ let eventListeners : RippleEventListeners | null = null ;
183+
174184 // Do not register the `transition` event listener if fade-in and fade-out duration
175185 // are set to zero. The events won't fire anyway and we can save resources here.
176186 if ( ! animationForciblyDisabledThroughCss && ( enterDuration || animationConfig . exitDuration ) ) {
177187 this . _ngZone . runOutsideAngular ( ( ) => {
178- ripple . addEventListener ( 'transitionend' , ( ) => this . _finishRippleTransition ( rippleRef ) ) ;
188+ const onTransitionEnd = ( ) => this . _finishRippleTransition ( rippleRef ) ;
189+ const onTransitionCancel = ( ) => this . _destroyRipple ( rippleRef ) ;
190+ ripple . addEventListener ( 'transitionend' , onTransitionEnd ) ;
179191 // If the transition is cancelled (e.g. due to DOM removal), we destroy the ripple
180192 // directly as otherwise we would keep it part of the ripple container forever.
181193 // https://www.w3.org/TR/css-transitions-1/#:~:text=no%20longer%20in%20the%20document.
182- ripple . addEventListener ( 'transitioncancel' , ( ) => this . _destroyRipple ( rippleRef ) ) ;
194+ ripple . addEventListener ( 'transitioncancel' , onTransitionCancel ) ;
195+ eventListeners = { onTransitionEnd, onTransitionCancel} ;
183196 } ) ;
184197 }
185198
199+ // Add the ripple reference to the list of all active ripples.
200+ this . _activeRipples . set ( rippleRef , eventListeners ) ;
201+
186202 // In case there is no fade-in transition duration, we need to manually call the transition
187203 // end listener because `transitionend` doesn't fire if there is no transition.
188204 if ( animationForciblyDisabledThroughCss || ! enterDuration ) {
@@ -217,12 +233,12 @@ export class RippleRenderer implements EventListenerObject {
217233
218234 /** Fades out all currently active ripples. */
219235 fadeOutAll ( ) {
220- this . _activeRipples . forEach ( ripple => ripple . fadeOut ( ) ) ;
236+ this . _getActiveRipples ( ) . forEach ( ripple => ripple . fadeOut ( ) ) ;
221237 }
222238
223239 /** Fades out all currently active non-persistent ripples. */
224240 fadeOutAllNonPersistent ( ) {
225- this . _activeRipples . forEach ( ripple => {
241+ this . _getActiveRipples ( ) . forEach ( ripple => {
226242 if ( ! ripple . config . persistent ) {
227243 ripple . fadeOut ( ) ;
228244 }
@@ -296,6 +312,7 @@ export class RippleRenderer implements EventListenerObject {
296312
297313 /** Destroys the given ripple by removing it from the DOM and updating its state. */
298314 private _destroyRipple ( rippleRef : RippleRef ) {
315+ const eventListeners = this . _activeRipples . get ( rippleRef ) ?? null ;
299316 this . _activeRipples . delete ( rippleRef ) ;
300317
301318 // Clear out the cached bounding rect if we have no more ripples.
@@ -310,6 +327,10 @@ export class RippleRenderer implements EventListenerObject {
310327 }
311328
312329 rippleRef . state = RippleState . HIDDEN ;
330+ if ( eventListeners !== null ) {
331+ rippleRef . element . removeEventListener ( 'transitionend' , eventListeners . onTransitionEnd ) ;
332+ rippleRef . element . removeEventListener ( 'transitioncancel' , eventListeners . onTransitionCancel ) ;
333+ }
313334 rippleRef . element . remove ( ) ;
314335 }
315336
@@ -356,7 +377,7 @@ export class RippleRenderer implements EventListenerObject {
356377 this . _isPointerDown = false ;
357378
358379 // Fade-out all ripples that are visible and not persistent.
359- this . _activeRipples . forEach ( ripple => {
380+ this . _getActiveRipples ( ) . forEach ( ripple => {
360381 // By default, only ripples that are completely visible will fade out on pointer release.
361382 // If the `terminateOnPointerUp` option is set, ripples that still fade in will also fade out.
362383 const isVisible =
@@ -378,6 +399,10 @@ export class RippleRenderer implements EventListenerObject {
378399 } ) ;
379400 }
380401
402+ private _getActiveRipples ( ) : RippleRef [ ] {
403+ return Array . from ( this . _activeRipples . keys ( ) ) ;
404+ }
405+
381406 /** Removes previously registered event listeners from the trigger element. */
382407 _removeTriggerEvents ( ) {
383408 if ( this . _triggerElement ) {
0 commit comments