@@ -114,7 +114,7 @@ export class RippleRenderer implements EventListenerObject {
114
114
const radius = config . radius || distanceToFurthestCorner ( x , y , containerRect ) ;
115
115
const offsetX = x - containerRect . left ;
116
116
const offsetY = y - containerRect . top ;
117
- const duration = animationConfig . enterDuration ;
117
+ const enterDuration = animationConfig . enterDuration ;
118
118
119
119
const ripple = document . createElement ( 'div' ) ;
120
120
ripple . classList . add ( 'mat-ripple-element' ) ;
@@ -130,21 +130,38 @@ export class RippleRenderer implements EventListenerObject {
130
130
ripple . style . backgroundColor = config . color ;
131
131
}
132
132
133
- ripple . style . transitionDuration = `${ duration } ms` ;
133
+ ripple . style . transitionDuration = `${ enterDuration } ms` ;
134
134
135
135
this . _containerElement . appendChild ( ripple ) ;
136
136
137
137
// 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' ;
140
155
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
142
162
// the ripples aren't clipped when inside the shadow DOM (see #24028).
143
163
ripple . style . transform = 'scale3d(1, 1, 1)' ;
144
164
145
- // Exposed reference to the ripple that will be returned.
146
- const rippleRef = new RippleRef ( this , ripple , config ) ;
147
-
148
165
rippleRef . state = RippleState . FADING_IN ;
149
166
150
167
// Add the ripple reference to the list of all active ripples.
@@ -154,21 +171,19 @@ export class RippleRenderer implements EventListenerObject {
154
171
this . _mostRecentTransientRipple = rippleRef ;
155
172
}
156
173
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
+ }
163
181
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
+ }
172
187
173
188
return rippleRef ;
174
189
}
@@ -194,15 +209,17 @@ export class RippleRenderer implements EventListenerObject {
194
209
const rippleEl = rippleRef . element ;
195
210
const animationConfig = { ...defaultRippleAnimationConfig , ...rippleRef . config . animation } ;
196
211
212
+ // This starts the fade-out transition and will fire the transition end listener that
213
+ // removes the ripple element from the DOM.
197
214
rippleEl . style . transitionDuration = `${ animationConfig . exitDuration } ms` ;
198
215
rippleEl . style . opacity = '0' ;
199
216
rippleRef . state = RippleState . FADING_OUT ;
200
217
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
+ }
206
223
}
207
224
208
225
/** Fades out all currently active ripples. */
@@ -256,6 +273,40 @@ export class RippleRenderer implements EventListenerObject {
256
273
}
257
274
}
258
275
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
+
259
310
/** Function being called whenever the trigger is being pressed using mouse. */
260
311
private _onMousedown ( event : MouseEvent ) {
261
312
// Screen readers will fire fake mouse events for space/enter. Skip launching a
@@ -312,11 +363,6 @@ export class RippleRenderer implements EventListenerObject {
312
363
} ) ;
313
364
}
314
365
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
-
320
366
/** Registers event listeners for a given list of events. */
321
367
private _registerEvents ( eventTypes : string [ ] ) {
322
368
this . _ngZone . runOutsideAngular ( ( ) => {
@@ -342,14 +388,6 @@ export class RippleRenderer implements EventListenerObject {
342
388
}
343
389
}
344
390
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
-
353
391
/**
354
392
* Returns the distance from the point (x, y) to the furthest corner of a rectangle.
355
393
*/
0 commit comments