@@ -114,7 +114,7 @@ export class RippleRenderer implements EventListenerObject {
114114 const radius = config . radius || distanceToFurthestCorner ( x , y , containerRect ) ;
115115 const offsetX = x - containerRect . left ;
116116 const offsetY = y - containerRect . top ;
117- const duration = animationConfig . enterDuration ;
117+ const enterDuration = animationConfig . enterDuration ;
118118
119119 const ripple = document . createElement ( 'div' ) ;
120120 ripple . classList . add ( 'mat-ripple-element' ) ;
@@ -130,21 +130,38 @@ export class RippleRenderer implements EventListenerObject {
130130 ripple . style . backgroundColor = config . color ;
131131 }
132132
133- ripple . style . transitionDuration = `${ duration } ms` ;
133+ ripple . style . transitionDuration = `${ enterDuration } ms` ;
134134
135135 this . _containerElement . appendChild ( ripple ) ;
136136
137137 // By default the browser does not recalculate the styles of dynamically created
138- // ripple elements. This is critical because then the `scale` would not animate properly.
139- enforceStyleRecalculation ( ripple ) ;
138+ // ripple elements. This is critical to ensure that the `scale` animates properly.
139+ // We enforce a style recalculation by calling `getComputedStyle` and *accessing* a property.
140+ // See: https://gist.github.com/paulirish/5d52fb081b3570c81e3a
141+ const computedStyles = window . getComputedStyle ( ripple ) ;
142+ const userTransitionProperty = computedStyles . transitionProperty ;
143+ const userTransitionDuration = computedStyles . transitionDuration ;
144+
145+ // Note: We detect whether animation is forcibly disabled through CSS by the use of
146+ // `transition: none`. This is technically unexpected since animations are controlled
147+ // through the animation config, but this exists for backwards compatibility. This logic does
148+ // not need to be super accurate since it covers some edge cases which can be easily avoided by users.
149+ const animationForciblyDisabledThroughCss =
150+ userTransitionProperty === 'none' ||
151+ // Note: The canonical unit for serialized CSS `<time>` properties is seconds. Additionally
152+ // some browsers expand the duration for every property (in our case `opacity` and `transform`).
153+ userTransitionDuration === '0s' ||
154+ userTransitionDuration === '0s, 0s' ;
140155
141- // We use a 3d transform here in order to avoid an issue in Safari where
156+ // Exposed reference to the ripple that will be returned.
157+ const rippleRef = new RippleRef ( this , ripple , config , animationForciblyDisabledThroughCss ) ;
158+
159+ // Start the enter animation by setting the transform/scale to 100%. The animation will
160+ // execute as part of this statement because we forced a style recalculation before.
161+ // Note: We use a 3d transform here in order to avoid an issue in Safari where
142162 // the ripples aren't clipped when inside the shadow DOM (see #24028).
143163 ripple . style . transform = 'scale3d(1, 1, 1)' ;
144164
145- // Exposed reference to the ripple that will be returned.
146- const rippleRef = new RippleRef ( this , ripple , config ) ;
147-
148165 rippleRef . state = RippleState . FADING_IN ;
149166
150167 // Add the ripple reference to the list of all active ripples.
@@ -154,21 +171,19 @@ export class RippleRenderer implements EventListenerObject {
154171 this . _mostRecentTransientRipple = rippleRef ;
155172 }
156173
157- // Wait for the ripple element to be completely faded in.
158- // Once it's faded in, the ripple can be hidden immediately if the mouse is released.
159- this . _runTimeoutOutsideZone ( ( ) => {
160- const isMostRecentTransientRipple = rippleRef === this . _mostRecentTransientRipple ;
161-
162- rippleRef . state = RippleState . VISIBLE ;
174+ // Do not register the `transition` event listener if fade-in and fade-out duration
175+ // are set to zero. The events won't fire anyway and we can save resources here.
176+ if ( ! animationForciblyDisabledThroughCss && ( enterDuration || animationConfig . exitDuration ) ) {
177+ this . _ngZone . runOutsideAngular ( ( ) => {
178+ ripple . addEventListener ( 'transitionend' , ( ) => this . _finishRippleTransition ( rippleRef ) ) ;
179+ } ) ;
180+ }
163181
164- // When the timer runs out while the user has kept their pointer down, we want to
165- // keep only the persistent ripples and the latest transient ripple. We do this,
166- // because we don't want stacked transient ripples to appear after their enter
167- // animation has finished.
168- if ( ! config . persistent && ( ! isMostRecentTransientRipple || ! this . _isPointerDown ) ) {
169- rippleRef . fadeOut ( ) ;
170- }
171- } , duration ) ;
182+ // In case there is no fade-in transition duration, we need to manually call the transition
183+ // end listener because `transitionend` doesn't fire if there is no transition.
184+ if ( animationForciblyDisabledThroughCss || ! enterDuration ) {
185+ this . _finishRippleTransition ( rippleRef ) ;
186+ }
172187
173188 return rippleRef ;
174189 }
@@ -194,15 +209,17 @@ export class RippleRenderer implements EventListenerObject {
194209 const rippleEl = rippleRef . element ;
195210 const animationConfig = { ...defaultRippleAnimationConfig , ...rippleRef . config . animation } ;
196211
212+ // This starts the fade-out transition and will fire the transition end listener that
213+ // removes the ripple element from the DOM.
197214 rippleEl . style . transitionDuration = `${ animationConfig . exitDuration } ms` ;
198215 rippleEl . style . opacity = '0' ;
199216 rippleRef . state = RippleState . FADING_OUT ;
200217
201- // Once the ripple faded out, the ripple can be safely removed from the DOM.
202- this . _runTimeoutOutsideZone ( ( ) => {
203- rippleRef . state = RippleState . HIDDEN ;
204- rippleEl . remove ( ) ;
205- } , animationConfig . exitDuration ) ;
218+ // In case there is no fade- out transition duration, we need to manually call the
219+ // transition end listener because `transitionend` doesn't fire if there is no transition.
220+ if ( rippleRef . _animationForciblyDisabledThroughCss || ! animationConfig . exitDuration ) {
221+ this . _finishRippleTransition ( rippleRef ) ;
222+ }
206223 }
207224
208225 /** Fades out all currently active ripples. */
@@ -256,6 +273,40 @@ export class RippleRenderer implements EventListenerObject {
256273 }
257274 }
258275
276+ /** Method that will be called if the fade-in or fade-in transition completed. */
277+ private _finishRippleTransition ( rippleRef : RippleRef ) {
278+ if ( rippleRef . state === RippleState . FADING_IN ) {
279+ this . _startFadeOutTransition ( rippleRef ) ;
280+ } else if ( rippleRef . state === RippleState . FADING_OUT ) {
281+ this . _destroyRipple ( rippleRef ) ;
282+ }
283+ }
284+
285+ /**
286+ * Starts the fade-out transition of the given ripple if it's not persistent and the pointer
287+ * is not held down anymore.
288+ */
289+ private _startFadeOutTransition ( rippleRef : RippleRef ) {
290+ const isMostRecentTransientRipple = rippleRef === this . _mostRecentTransientRipple ;
291+ const { persistent} = rippleRef . config ;
292+
293+ rippleRef . state = RippleState . VISIBLE ;
294+
295+ // When the timer runs out while the user has kept their pointer down, we want to
296+ // keep only the persistent ripples and the latest transient ripple. We do this,
297+ // because we don't want stacked transient ripples to appear after their enter
298+ // animation has finished.
299+ if ( ! persistent && ( ! isMostRecentTransientRipple || ! this . _isPointerDown ) ) {
300+ rippleRef . fadeOut ( ) ;
301+ }
302+ }
303+
304+ /** Destroys the given ripple by removing it from the DOM and updating its state. */
305+ private _destroyRipple ( rippleRef : RippleRef ) {
306+ rippleRef . state = RippleState . HIDDEN ;
307+ rippleRef . element . remove ( ) ;
308+ }
309+
259310 /** Function being called whenever the trigger is being pressed using mouse. */
260311 private _onMousedown ( event : MouseEvent ) {
261312 // Screen readers will fire fake mouse events for space/enter. Skip launching a
@@ -312,11 +363,6 @@ export class RippleRenderer implements EventListenerObject {
312363 } ) ;
313364 }
314365
315- /** Runs a timeout outside of the Angular zone to avoid triggering the change detection. */
316- private _runTimeoutOutsideZone ( fn : Function , delay = 0 ) {
317- this . _ngZone . runOutsideAngular ( ( ) => setTimeout ( fn , delay ) ) ;
318- }
319-
320366 /** Registers event listeners for a given list of events. */
321367 private _registerEvents ( eventTypes : string [ ] ) {
322368 this . _ngZone . runOutsideAngular ( ( ) => {
@@ -342,14 +388,6 @@ export class RippleRenderer implements EventListenerObject {
342388 }
343389}
344390
345- /** Enforces a style recalculation of a DOM element by computing its styles. */
346- function enforceStyleRecalculation ( element : HTMLElement ) {
347- // Enforce a style recalculation by calling `getComputedStyle` and accessing any property.
348- // Calling `getPropertyValue` is important to let optimizers know that this is not a noop.
349- // See: https://gist.github.com/paulirish/5d52fb081b3570c81e3a
350- window . getComputedStyle ( element ) . getPropertyValue ( 'opacity' ) ;
351- }
352-
353391/**
354392 * Returns the distance from the point (x, y) to the furthest corner of a rectangle.
355393 */
0 commit comments