diff --git a/src/lib/core/ripple/ripple-renderer.ts b/src/lib/core/ripple/ripple-renderer.ts index 9a42d14f1e56..1a70627f748a 100644 --- a/src/lib/core/ripple/ripple-renderer.ts +++ b/src/lib/core/ripple/ripple-renderer.ts @@ -15,6 +15,7 @@ export type RippleConfig = { radius?: number; persistent?: boolean; animation?: RippleAnimationConfig; + terminateOnPointerUp?: boolean; /** @deprecated Use the animation property instead. */ speedFactor?: number; }; @@ -244,9 +245,14 @@ export class RippleRenderer { this._isPointerDown = false; - // Fade-out all ripples that are completely visible and not persistent. + // Fade-out all ripples that are visible and not persistent. this._activeRipples.forEach(ripple => { - if (!ripple.config.persistent && ripple.state === RippleState.VISIBLE) { + // By default, only ripples that are completely visible will fade out on pointer release. + // If the `terminateOnPointerUp` option is set, ripples that still fade in will also fade out. + const isVisible = ripple.state === RippleState.VISIBLE || + ripple.config.terminateOnPointerUp && ripple.state === RippleState.FADING_IN; + + if (!ripple.config.persistent && isVisible) { ripple.fadeOut(); } }); diff --git a/src/lib/core/ripple/ripple.spec.ts b/src/lib/core/ripple/ripple.spec.ts index 5f2734da3661..618796bd8745 100644 --- a/src/lib/core/ripple/ripple.spec.ts +++ b/src/lib/core/ripple/ripple.spec.ts @@ -500,6 +500,27 @@ describe('MatRipple', () => { expect(rippleTarget.querySelectorAll('.mat-ripple-element').length).toBe(0); })); + + it('should allow ripples to fade out immediately on pointer up', fakeAsync(() => { + createTestComponent({ + terminateOnPointerUp: true + }); + + dispatchMouseEvent(rippleTarget, 'mousedown'); + dispatchMouseEvent(rippleTarget, 'mouseup'); + + expect(rippleTarget.querySelectorAll('.mat-ripple-element').length).toBe(1); + + // Ignore the enter duration, because we immediately fired the mouseup after the mousedown. + // This means that the ripple should just fade out, and there shouldn't be an enter animation. + tick(exitDuration); + + expect(rippleTarget.querySelectorAll('.mat-ripple-element').length).toBe(0); + + // Since the enter duration is bigger than the exit duration, the enter duration timer + // will still exist. To properly finish all timers, we just wait the remaining time. + tick(enterDuration - exitDuration); + })); }); describe('configuring behavior', () => { diff --git a/src/lib/core/ripple/ripple.ts b/src/lib/core/ripple/ripple.ts index 52c2d693f0fa..f229d53e4835 100644 --- a/src/lib/core/ripple/ripple.ts +++ b/src/lib/core/ripple/ripple.ts @@ -42,6 +42,12 @@ export interface RippleGlobalOptions { * @deprecated Use the `animation` global option instead. */ baseSpeedFactor?: number; + + /** + * Whether ripples should start fading out immediately after the mouse our touch is released. By + * default, ripples will wait for the enter animation to complete and for mouse or touch release. + */ + terminateOnPointerUp?: boolean; } /** Injection token that can be used to specify the global ripple options. */ @@ -159,6 +165,7 @@ export class MatRipple implements OnInit, OnDestroy, RippleTarget { radius: this.radius, color: this.color, animation: {...this._globalOptions.animation, ...this.animation}, + terminateOnPointerUp: this._globalOptions.terminateOnPointerUp, speedFactor: this.speedFactor * (this._globalOptions.baseSpeedFactor || 1), }; } diff --git a/src/lib/tabs/tab-nav-bar/tab-nav-bar.ts b/src/lib/tabs/tab-nav-bar/tab-nav-bar.ts index 534a5a3f1ad5..4838eb9e5b31 100644 --- a/src/lib/tabs/tab-nav-bar/tab-nav-bar.ts +++ b/src/lib/tabs/tab-nav-bar/tab-nav-bar.ts @@ -240,6 +240,7 @@ export class MatTabLink extends _MatTabLinkMixinBase if (globalOptions) { this.rippleConfig = { + terminateOnPointerUp: globalOptions.terminateOnPointerUp, speedFactor: globalOptions.baseSpeedFactor, animation: globalOptions.animation, };