Skip to content

Commit

Permalink
feat(ripple): support animation duration overwrites
Browse files Browse the repository at this point in the history
* 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)
  • Loading branch information
devversion committed Jan 24, 2018
1 parent d5a7cce commit ea98b5d
Show file tree
Hide file tree
Showing 16 changed files with 184 additions and 84 deletions.
7 changes: 4 additions & 3 deletions src/demo-app/ripple/ripple-demo.html
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,10 @@
</mat-form-field>
</section>
<section>
<button mat-raised-button (click)="launchRipple()">Launch Ripple</button>
<button mat-raised-button (click)="launchRipple(true)">Launch Ripple (Persistent)</button>
<button mat-raised-button (click)="fadeOutAll()">Fade Out All</button>
<button mat-raised-button (click)="launchRipple()" disableRipple>Launch Ripple</button>
<button mat-raised-button (click)="launchRipple(true)" disableRipple>Launch Ripple (Persistent)</button>
<button mat-raised-button (click)="launchRipple(true, true)" disableRipple>Launch Ripple (No Animation)</button>
<button mat-raised-button (click)="fadeOutAll()" disableRipple>Fade Out All</button>
</section>
<section>
<div class="demo-ripple-container"
Expand Down
14 changes: 11 additions & 3 deletions src/demo-app/ripple/ripple-demo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,18 @@ export class RippleDemo {

disableButtonRipples = false;

launchRipple(persistent = false) {
if (this.ripple) {
this.ripple.launch(0, 0, { centered: true, persistent });
launchRipple(persistent = false, disableAnimation = false) {
if (!this.ripple) {
return;
}

const rippleConfig = {
centered: true,
persistent: persistent,
animation: disableAnimation ? {enterDuration: 0, exitDuration: 0} : undefined
};

this.ripple.launch(0, 0, rippleConfig);
}

fadeOutAll() {
Expand Down
6 changes: 3 additions & 3 deletions src/lib/checkbox/checkbox.html
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,9 @@
<div matRipple class="mat-checkbox-ripple"
[matRippleTrigger]="label"
[matRippleDisabled]="_isRippleDisabled()"
[matRippleRadius]="_rippleConfig.radius"
[matRippleSpeedFactor]="_rippleConfig.speedFactor"
[matRippleCentered]="_rippleConfig.centered">
[matRippleRadius]="25"
[matRippleCentered]="true"
[matRippleAnimation]="{enterDuration: 300}">
</div>
<div class="mat-checkbox-frame"></div>
<div class="mat-checkbox-background">
Expand Down
6 changes: 3 additions & 3 deletions src/lib/checkbox/checkbox.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import {Component, DebugElement} from '@angular/core';
import {By} from '@angular/platform-browser';
import {dispatchFakeEvent} from '@angular/cdk/testing';
import {MatCheckbox, MatCheckboxChange, MatCheckboxModule} from './index';
import {RIPPLE_FADE_IN_DURATION, RIPPLE_FADE_OUT_DURATION} from '@angular/material/core';
import {defaultRippleAnimationConfig} from '@angular/material/core';
import {MAT_CHECKBOX_CLICK_ACTION} from './checkbox-config';
import {MutationObserverFactory} from '@angular/cdk/observers';

Expand Down Expand Up @@ -390,13 +390,13 @@ describe('MatCheckbox', () => {
dispatchFakeEvent(inputElement, 'keydown');
dispatchFakeEvent(inputElement, 'focus');

tick(RIPPLE_FADE_IN_DURATION);
tick(defaultRippleAnimationConfig.enterDuration);

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

dispatchFakeEvent(checkboxInstance._inputElement.nativeElement, 'blur');
tick(RIPPLE_FADE_OUT_DURATION);
tick(defaultRippleAnimationConfig.exitDuration);

expect(fixture.nativeElement.querySelectorAll('.mat-ripple-element').length)
.toBe(0, 'Expected no ripple after element is blurred.');
Expand Down
6 changes: 1 addition & 5 deletions src/lib/checkbox/checkbox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,6 @@ import {
mixinDisabled,
mixinDisableRipple,
mixinTabIndex,
RippleConfig,
RippleRef,
} from '@angular/material/core';
import {MAT_CHECKBOX_CLICK_ACTION, MatCheckboxClickAction} from './checkbox-config';
Expand Down Expand Up @@ -180,9 +179,6 @@ export class MatCheckbox extends _MatCheckboxMixinBase implements ControlValueAc
/** Reference to the ripple instance of the checkbox. */
@ViewChild(MatRipple) ripple: MatRipple;

/** Ripple configuration for the mouse ripples and focus indicators. */
_rippleConfig: RippleConfig = {centered: true, radius: 25, speedFactor: 1.5};

/**
* Called when the checkbox is blurred. Needed to properly implement ControlValueAccessor.
* @docs-private
Expand Down Expand Up @@ -341,7 +337,7 @@ export class MatCheckbox extends _MatCheckboxMixinBase implements ControlValueAc
/** Function is called whenever the focus changes for the input element. */
private _onInputFocusChange(focusOrigin: FocusOrigin) {
if (!this._focusRipple && focusOrigin === 'keyboard') {
this._focusRipple = this.ripple.launch(0, 0, {persistent: true, ...this._rippleConfig});
this._focusRipple = this.ripple.launch(0, 0, {persistent: true});
} else if (!focusOrigin) {
this._removeFocusRipple();
this.onTouched();
Expand Down
53 changes: 35 additions & 18 deletions src/lib/core/ripple/ripple-renderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,26 +9,27 @@ import {ElementRef, NgZone} from '@angular/core';
import {Platform, supportsPassiveEventListeners} from '@angular/cdk/platform';
import {RippleRef, RippleState} from './ripple-ref';

/** Fade-in duration for the ripples. Can be modified with the speedFactor option. */
export const RIPPLE_FADE_IN_DURATION = 450;

/** Fade-out duration for the ripples in milliseconds. This can't be modified by the speedFactor. */
export const RIPPLE_FADE_OUT_DURATION = 400;

/**
* Timeout for ignoring mouse events. Mouse events will be temporary ignored after touch
* events to avoid synthetic mouse events.
*/
const IGNORE_MOUSE_EVENTS_TIMEOUT = 800;

export type RippleConfig = {
color?: string;
centered?: boolean;
radius?: number;
speedFactor?: number;
persistent?: boolean;
animation?: RippleAnimationConfig;
/** @deprecated Use the animation property instead. */
speedFactor?: number;
};

/**
* Interface that describes the configuration for the animation of a ripple.
* There are two animation phases with different durations for the ripples.
*/
export interface RippleAnimationConfig {
/** Duration in milliseconds for the enter animation (expansion from point of contact). */
enterDuration?: number;
/** Duration in milliseconds for the exit animation (fade-out). */
exitDuration?: number;
}

/**
* Interface that describes the target for launching ripples.
* It defines the ripple configuration and disabled state for interaction ripples.
Expand All @@ -37,11 +38,25 @@ export type RippleConfig = {
export interface RippleTarget {
/** Configuration for ripples that are launched on pointer down. */
rippleConfig: RippleConfig;

/** Whether ripples on pointer down should be disabled. */
rippleDisabled: boolean;
}

/**
* Default ripple animation configuration for ripples without an explicit
* animation config specified.
*/
export const defaultRippleAnimationConfig = {
enterDuration: 450,
exitDuration: 400
};

/**
* Timeout for ignoring mouse events. Mouse events will be temporary ignored after touch
* events to avoid synthetic mouse events.
*/
const ignoreMouseEventsTimeout = 800;

/**
* Helper service that performs DOM manipulations. Not intended to be used outside this module.
* The constructor takes a reference to the ripple directive's host element and a map of DOM
Expand Down Expand Up @@ -99,16 +114,17 @@ export class RippleRenderer {
*/
fadeInRipple(x: number, y: number, config: RippleConfig = {}): RippleRef {
const containerRect = this._containerElement.getBoundingClientRect();
const animationConfig = {...defaultRippleAnimationConfig, ...config.animation};

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

const radius = config.radius || distanceToFurthestCorner(x, y, containerRect);
const duration = RIPPLE_FADE_IN_DURATION / (config.speedFactor || 1);
const offsetX = x - containerRect.left;
const offsetY = y - containerRect.top;
const duration = animationConfig.enterDuration / (config.speedFactor || 1);

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

const rippleEl = rippleRef.element;
const animationConfig = {...defaultRippleAnimationConfig, ...rippleRef.config.animation};

rippleEl.style.transitionDuration = `${RIPPLE_FADE_OUT_DURATION}ms`;
rippleEl.style.transitionDuration = `${animationConfig.exitDuration}ms`;
rippleEl.style.opacity = '0';

rippleRef.state = RippleState.FADING_OUT;
Expand All @@ -169,7 +186,7 @@ export class RippleRenderer {
this.runTimeoutOutsideZone(() => {
rippleRef.state = RippleState.HIDDEN;
rippleEl.parentNode!.removeChild(rippleEl);
}, RIPPLE_FADE_OUT_DURATION);
}, animationConfig.exitDuration);
}

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

if (!this._target.rippleDisabled && !isSyntheticEvent) {
this._isPointerDown = true;
Expand Down
27 changes: 25 additions & 2 deletions src/lib/core/ripple/ripple.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,8 +75,11 @@ Global ripple options can be specified by setting the `MAT_RIPPLE_GLOBAL_OPTIONS
```ts
const globalRippleConfig: RippleGlobalOptions = {
disabled: true,
baseSpeedFactor: 1.5 // Ripples will animate 50% faster than before.
}
animation: {
enterDuration: 300,
exitDuration: 0
}
};

@NgModule({
providers: [
Expand All @@ -86,3 +89,23 @@ const globalRippleConfig: RippleGlobalOptions = {
```

All available global options can be seen in the `RippleGlobalOptions` interface.

### Disabling animation

The animation of ripples can be disabled by using the `animation` global option. If the
`enterDuration` and `exitDuration` is being set to `0`, ripples will just appear without any
animation.

This is specifically useful in combination with the `disabled` global option, because globally
disabling ripples won't affect the focus indicator ripples. If someone still wants to disable
those ripples for performance reasons, the duration can be set to `0`, to remove the ripple feel.

```ts
const globalRippleConfig: RippleGlobalOptions = {
disabled: true,
animation: {
enterDuration: 0,
exitDuration: 0
}
};
```
Loading

0 comments on commit ea98b5d

Please sign in to comment.