Skip to content

Commit 0b3cf77

Browse files
committed
fixup! fix(material/core): ripples not fading out on touch devices when scrolling
Backwards compatibility change for g3 tests using just transition: none without the noopanimations module
1 parent ac1280c commit 0b3cf77

File tree

3 files changed

+47
-20
lines changed

3 files changed

+47
-20
lines changed

src/material/core/ripple/ripple-ref.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,8 @@ export class RippleRef {
4747
public element: HTMLElement,
4848
/** Ripple configuration used for the ripple. */
4949
public config: RippleConfig,
50+
/* Whether animations are forcibly disabled for ripples through CSS. */
51+
public _animationForciblyDisabledThroughCss = false,
5052
) {}
5153

5254
/** Fades out the ripple element. */

src/material/core/ripple/ripple-renderer.ts

Lines changed: 21 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -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,20 +130,30 @@ 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 transitionProperty = computedStyles.transitionProperty;
140143

141-
// We use a 3d transform here in order to avoid an issue in Safari where
142-
// the ripples aren't clipped when inside the shadow DOM (see #24028).
143-
ripple.style.transform = 'scale3d(1, 1, 1)';
144+
// Note: We detect whether animation is forcibly disabled through CSS by the use
145+
// of `transition: none`. This is technically unexpected since animations are
146+
// controlled through the animation config, but this exists for backwards compatibility
147+
const animationForciblyDisabledThroughCss = transitionProperty === 'none';
144148

145149
// Exposed reference to the ripple that will be returned.
146-
const rippleRef = new RippleRef(this, ripple, config);
150+
const rippleRef = new RippleRef(this, ripple, config, animationForciblyDisabledThroughCss);
151+
152+
// Start the enter animation by setting the transform/scale to 100%. The animation will
153+
// execute as part of this statement because we forced a style recalculation before.
154+
// Note: We use a 3d transform here in order to avoid an issue in Safari where
155+
// the ripples aren't clipped when inside the shadow DOM (see #24028).
156+
ripple.style.transform = 'scale3d(1, 1, 1)';
147157

148158
rippleRef.state = RippleState.FADING_IN;
149159

@@ -156,15 +166,15 @@ export class RippleRenderer implements EventListenerObject {
156166

157167
// Do not register the `transition` event listener if fade-in and fade-out duration
158168
// are set to zero. The events won't fire anyway and we can save resources here.
159-
if (duration || animationConfig.exitDuration) {
169+
if (!animationForciblyDisabledThroughCss && (enterDuration || animationConfig.exitDuration)) {
160170
this._ngZone.runOutsideAngular(() => {
161171
ripple.addEventListener('transitionend', () => this._finishRippleTransition(rippleRef));
162172
});
163173
}
164174

165175
// In case there is no fade-in transition duration, we need to manually call the transition
166176
// end listener because `transitionend` doesn't fire if there is no transition.
167-
if (!duration) {
177+
if (animationForciblyDisabledThroughCss || !enterDuration) {
168178
this._finishRippleTransition(rippleRef);
169179
}
170180

@@ -200,7 +210,7 @@ export class RippleRenderer implements EventListenerObject {
200210

201211
// In case there is no fade-out transition duration, we need to manually call the
202212
// transition end listener because `transitionend` doesn't fire if there is no transition.
203-
if (!animationConfig.exitDuration) {
213+
if (rippleRef._animationForciblyDisabledThroughCss || !animationConfig.exitDuration) {
204214
this._finishRippleTransition(rippleRef);
205215
}
206216
}
@@ -371,14 +381,6 @@ export class RippleRenderer implements EventListenerObject {
371381
}
372382
}
373383

374-
/** Enforces a style recalculation of a DOM element by computing its styles. */
375-
function enforceStyleRecalculation(element: HTMLElement) {
376-
// Enforce a style recalculation by calling `getComputedStyle` and accessing any property.
377-
// Calling `getPropertyValue` is important to let optimizers know that this is not a noop.
378-
// See: https://gist.github.com/paulirish/5d52fb081b3570c81e3a
379-
window.getComputedStyle(element).getPropertyValue('opacity');
380-
}
381-
382384
/**
383385
* Returns the distance from the point (x, y) to the furthest corner of a rectangle.
384386
*/

src/material/core/ripple/ripple.spec.ts

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import {
77
dispatchMouseEvent,
88
dispatchTouchEvent,
99
} from '@angular/cdk/testing/private';
10-
import {Component, ViewChild} from '@angular/core';
10+
import {Component, ViewChild, ViewEncapsulation} from '@angular/core';
1111
import {ComponentFixture, inject, TestBed} from '@angular/core/testing';
1212
import {NoopAnimationsModule} from '@angular/platform-browser/animations';
1313
import {
@@ -43,6 +43,7 @@ describe('MatRipple', () => {
4343
RippleContainerWithInputBindings,
4444
RippleContainerWithoutBindings,
4545
RippleContainerWithNgIf,
46+
RippleCssTransitionNone,
4647
],
4748
});
4849
});
@@ -773,6 +774,21 @@ describe('MatRipple', () => {
773774
expect(rippleTarget.querySelectorAll('.mat-ripple-element').length).toBe(0);
774775
});
775776
});
777+
778+
describe('edge cases', () => {
779+
it('should handle forcibly disabled animations through CSS `transition: none`', async () => {
780+
fixture = TestBed.createComponent(RippleCssTransitionNone);
781+
fixture.detectChanges();
782+
783+
rippleTarget = fixture.nativeElement.querySelector('.mat-ripple');
784+
785+
dispatchMouseEvent(rippleTarget, 'mousedown');
786+
expect(rippleTarget.querySelectorAll('.mat-ripple-element').length).toBe(1);
787+
788+
dispatchMouseEvent(rippleTarget, 'mouseup');
789+
expect(rippleTarget.querySelectorAll('.mat-ripple-element').length).toBe(0);
790+
});
791+
});
776792
});
777793

778794
@Component({
@@ -822,3 +838,10 @@ class RippleContainerWithNgIf {
822838
@ViewChild(MatRipple) ripple: MatRipple;
823839
isDestroyed = false;
824840
}
841+
842+
@Component({
843+
styles: [`* { transition: none !important; }`],
844+
template: `<div id="container" matRipple></div>`,
845+
encapsulation: ViewEncapsulation.None,
846+
})
847+
class RippleCssTransitionNone {}

0 commit comments

Comments
 (0)