Skip to content

Commit 3bc4cd3

Browse files
devversionjelbourn
authored andcommitted
feat(ripple): support animation duration overwrites (#9253)
* Adds a new option to the ripples that allows developers to have a better control of the animation (all ripples, or even individual ripples). * Deprecates the `matRippleSpeedFactor` in favor of the `matRippleAnimation` binding that accepts a `RippleAnimationConfig`. The configuration is more explicit, clean and not confusing as the `speedFactor`. * To provide a more user-friendly `launch()` method API, the passed ripple config will extend the default ripple config from the `MatRipple` instance (removes unnecessary bloat; requested in #4179 (comment)) * Disables ripples for most of the demo buttons in the ripple demo (allows better debugging; when pressing the buttons)
1 parent d5a7cce commit 3bc4cd3

16 files changed

+184
-84
lines changed

src/demo-app/ripple/ripple-demo.html

+4-3
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,10 @@
3535
</mat-form-field>
3636
</section>
3737
<section>
38-
<button mat-raised-button (click)="launchRipple()">Launch Ripple</button>
39-
<button mat-raised-button (click)="launchRipple(true)">Launch Ripple (Persistent)</button>
40-
<button mat-raised-button (click)="fadeOutAll()">Fade Out All</button>
38+
<button mat-raised-button (click)="launchRipple()" disableRipple>Launch Ripple</button>
39+
<button mat-raised-button (click)="launchRipple(true)" disableRipple>Launch Ripple (Persistent)</button>
40+
<button mat-raised-button (click)="launchRipple(true, true)" disableRipple>Launch Ripple (No Animation)</button>
41+
<button mat-raised-button (click)="fadeOutAll()" disableRipple>Fade Out All</button>
4142
</section>
4243
<section>
4344
<div class="demo-ripple-container"

src/demo-app/ripple/ripple-demo.ts

+11-3
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,18 @@ export class RippleDemo {
2929

3030
disableButtonRipples = false;
3131

32-
launchRipple(persistent = false) {
33-
if (this.ripple) {
34-
this.ripple.launch(0, 0, { centered: true, persistent });
32+
launchRipple(persistent = false, disableAnimation = false) {
33+
if (!this.ripple) {
34+
return;
3535
}
36+
37+
const rippleConfig = {
38+
centered: true,
39+
persistent: persistent,
40+
animation: disableAnimation ? {enterDuration: 0, exitDuration: 0} : undefined
41+
};
42+
43+
this.ripple.launch(0, 0, rippleConfig);
3644
}
3745

3846
fadeOutAll() {

src/lib/checkbox/checkbox.html

+3-3
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,9 @@
1919
<div matRipple class="mat-checkbox-ripple"
2020
[matRippleTrigger]="label"
2121
[matRippleDisabled]="_isRippleDisabled()"
22-
[matRippleRadius]="_rippleConfig.radius"
23-
[matRippleSpeedFactor]="_rippleConfig.speedFactor"
24-
[matRippleCentered]="_rippleConfig.centered">
22+
[matRippleRadius]="25"
23+
[matRippleCentered]="true"
24+
[matRippleAnimation]="{enterDuration: 300}">
2525
</div>
2626
<div class="mat-checkbox-frame"></div>
2727
<div class="mat-checkbox-background">

src/lib/checkbox/checkbox.spec.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import {Component, DebugElement} from '@angular/core';
44
import {By} from '@angular/platform-browser';
55
import {dispatchFakeEvent} from '@angular/cdk/testing';
66
import {MatCheckbox, MatCheckboxChange, MatCheckboxModule} from './index';
7-
import {RIPPLE_FADE_IN_DURATION, RIPPLE_FADE_OUT_DURATION} from '@angular/material/core';
7+
import {defaultRippleAnimationConfig} from '@angular/material/core';
88
import {MAT_CHECKBOX_CLICK_ACTION} from './checkbox-config';
99
import {MutationObserverFactory} from '@angular/cdk/observers';
1010

@@ -390,13 +390,13 @@ describe('MatCheckbox', () => {
390390
dispatchFakeEvent(inputElement, 'keydown');
391391
dispatchFakeEvent(inputElement, 'focus');
392392

393-
tick(RIPPLE_FADE_IN_DURATION);
393+
tick(defaultRippleAnimationConfig.enterDuration);
394394

395395
expect(fixture.nativeElement.querySelectorAll('.mat-ripple-element').length)
396396
.toBe(1, 'Expected ripple after element is focused.');
397397

398398
dispatchFakeEvent(checkboxInstance._inputElement.nativeElement, 'blur');
399-
tick(RIPPLE_FADE_OUT_DURATION);
399+
tick(defaultRippleAnimationConfig.exitDuration);
400400

401401
expect(fixture.nativeElement.querySelectorAll('.mat-ripple-element').length)
402402
.toBe(0, 'Expected no ripple after element is blurred.');

src/lib/checkbox/checkbox.ts

+1-5
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,6 @@ import {
3636
mixinDisabled,
3737
mixinDisableRipple,
3838
mixinTabIndex,
39-
RippleConfig,
4039
RippleRef,
4140
} from '@angular/material/core';
4241
import {MAT_CHECKBOX_CLICK_ACTION, MatCheckboxClickAction} from './checkbox-config';
@@ -180,9 +179,6 @@ export class MatCheckbox extends _MatCheckboxMixinBase implements ControlValueAc
180179
/** Reference to the ripple instance of the checkbox. */
181180
@ViewChild(MatRipple) ripple: MatRipple;
182181

183-
/** Ripple configuration for the mouse ripples and focus indicators. */
184-
_rippleConfig: RippleConfig = {centered: true, radius: 25, speedFactor: 1.5};
185-
186182
/**
187183
* Called when the checkbox is blurred. Needed to properly implement ControlValueAccessor.
188184
* @docs-private
@@ -341,7 +337,7 @@ export class MatCheckbox extends _MatCheckboxMixinBase implements ControlValueAc
341337
/** Function is called whenever the focus changes for the input element. */
342338
private _onInputFocusChange(focusOrigin: FocusOrigin) {
343339
if (!this._focusRipple && focusOrigin === 'keyboard') {
344-
this._focusRipple = this.ripple.launch(0, 0, {persistent: true, ...this._rippleConfig});
340+
this._focusRipple = this.ripple.launch(0, 0, {persistent: true});
345341
} else if (!focusOrigin) {
346342
this._removeFocusRipple();
347343
this.onTouched();

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

+35-18
Original file line numberDiff line numberDiff line change
@@ -9,26 +9,27 @@ import {ElementRef, NgZone} from '@angular/core';
99
import {Platform, supportsPassiveEventListeners} from '@angular/cdk/platform';
1010
import {RippleRef, RippleState} from './ripple-ref';
1111

12-
/** Fade-in duration for the ripples. Can be modified with the speedFactor option. */
13-
export const RIPPLE_FADE_IN_DURATION = 450;
14-
15-
/** Fade-out duration for the ripples in milliseconds. This can't be modified by the speedFactor. */
16-
export const RIPPLE_FADE_OUT_DURATION = 400;
17-
18-
/**
19-
* Timeout for ignoring mouse events. Mouse events will be temporary ignored after touch
20-
* events to avoid synthetic mouse events.
21-
*/
22-
const IGNORE_MOUSE_EVENTS_TIMEOUT = 800;
23-
2412
export type RippleConfig = {
2513
color?: string;
2614
centered?: boolean;
2715
radius?: number;
28-
speedFactor?: number;
2916
persistent?: boolean;
17+
animation?: RippleAnimationConfig;
18+
/** @deprecated Use the animation property instead. */
19+
speedFactor?: number;
3020
};
3121

22+
/**
23+
* Interface that describes the configuration for the animation of a ripple.
24+
* There are two animation phases with different durations for the ripples.
25+
*/
26+
export interface RippleAnimationConfig {
27+
/** Duration in milliseconds for the enter animation (expansion from point of contact). */
28+
enterDuration?: number;
29+
/** Duration in milliseconds for the exit animation (fade-out). */
30+
exitDuration?: number;
31+
}
32+
3233
/**
3334
* Interface that describes the target for launching ripples.
3435
* It defines the ripple configuration and disabled state for interaction ripples.
@@ -37,11 +38,25 @@ export type RippleConfig = {
3738
export interface RippleTarget {
3839
/** Configuration for ripples that are launched on pointer down. */
3940
rippleConfig: RippleConfig;
40-
4141
/** Whether ripples on pointer down should be disabled. */
4242
rippleDisabled: boolean;
4343
}
4444

45+
/**
46+
* Default ripple animation configuration for ripples without an explicit
47+
* animation config specified.
48+
*/
49+
export const defaultRippleAnimationConfig = {
50+
enterDuration: 450,
51+
exitDuration: 400
52+
};
53+
54+
/**
55+
* Timeout for ignoring mouse events. Mouse events will be temporary ignored after touch
56+
* events to avoid synthetic mouse events.
57+
*/
58+
const ignoreMouseEventsTimeout = 800;
59+
4560
/**
4661
* Helper service that performs DOM manipulations. Not intended to be used outside this module.
4762
* The constructor takes a reference to the ripple directive's host element and a map of DOM
@@ -99,16 +114,17 @@ export class RippleRenderer {
99114
*/
100115
fadeInRipple(x: number, y: number, config: RippleConfig = {}): RippleRef {
101116
const containerRect = this._containerElement.getBoundingClientRect();
117+
const animationConfig = {...defaultRippleAnimationConfig, ...config.animation};
102118

103119
if (config.centered) {
104120
x = containerRect.left + containerRect.width / 2;
105121
y = containerRect.top + containerRect.height / 2;
106122
}
107123

108124
const radius = config.radius || distanceToFurthestCorner(x, y, containerRect);
109-
const duration = RIPPLE_FADE_IN_DURATION / (config.speedFactor || 1);
110125
const offsetX = x - containerRect.left;
111126
const offsetY = y - containerRect.top;
127+
const duration = animationConfig.enterDuration / (config.speedFactor || 1);
112128

113129
const ripple = document.createElement('div');
114130
ripple.classList.add('mat-ripple-element');
@@ -159,8 +175,9 @@ export class RippleRenderer {
159175
}
160176

161177
const rippleEl = rippleRef.element;
178+
const animationConfig = {...defaultRippleAnimationConfig, ...rippleRef.config.animation};
162179

163-
rippleEl.style.transitionDuration = `${RIPPLE_FADE_OUT_DURATION}ms`;
180+
rippleEl.style.transitionDuration = `${animationConfig.exitDuration}ms`;
164181
rippleEl.style.opacity = '0';
165182

166183
rippleRef.state = RippleState.FADING_OUT;
@@ -169,7 +186,7 @@ export class RippleRenderer {
169186
this.runTimeoutOutsideZone(() => {
170187
rippleRef.state = RippleState.HIDDEN;
171188
rippleEl.parentNode!.removeChild(rippleEl);
172-
}, RIPPLE_FADE_OUT_DURATION);
189+
}, animationConfig.exitDuration);
173190
}
174191

175192
/** Fades out all currently active ripples. */
@@ -197,7 +214,7 @@ export class RippleRenderer {
197214
/** Function being called whenever the trigger is being pressed using mouse. */
198215
private onMousedown = (event: MouseEvent) => {
199216
const isSyntheticEvent = this._lastTouchStartEvent &&
200-
Date.now() < this._lastTouchStartEvent + IGNORE_MOUSE_EVENTS_TIMEOUT;
217+
Date.now() < this._lastTouchStartEvent + ignoreMouseEventsTimeout;
201218

202219
if (!this._target.rippleDisabled && !isSyntheticEvent) {
203220
this._isPointerDown = true;

src/lib/core/ripple/ripple.md

+25-2
Original file line numberDiff line numberDiff line change
@@ -75,8 +75,11 @@ Global ripple options can be specified by setting the `MAT_RIPPLE_GLOBAL_OPTIONS
7575
```ts
7676
const globalRippleConfig: RippleGlobalOptions = {
7777
disabled: true,
78-
baseSpeedFactor: 1.5 // Ripples will animate 50% faster than before.
79-
}
78+
animation: {
79+
enterDuration: 300,
80+
exitDuration: 0
81+
}
82+
};
8083

8184
@NgModule({
8285
providers: [
@@ -86,3 +89,23 @@ const globalRippleConfig: RippleGlobalOptions = {
8689
```
8790

8891
All available global options can be seen in the `RippleGlobalOptions` interface.
92+
93+
### Disabling animation
94+
95+
The animation of ripples can be disabled by using the `animation` global option. If the
96+
`enterDuration` and `exitDuration` is being set to `0`, ripples will just appear without any
97+
animation.
98+
99+
This is specifically useful in combination with the `disabled` global option, because globally
100+
disabling ripples won't affect the focus indicator ripples. If someone still wants to disable
101+
those ripples for performance reasons, the duration can be set to `0`, to remove the ripple feel.
102+
103+
```ts
104+
const globalRippleConfig: RippleGlobalOptions = {
105+
disabled: true,
106+
animation: {
107+
enterDuration: 0,
108+
exitDuration: 0
109+
}
110+
};
111+
```

0 commit comments

Comments
 (0)