Skip to content

Commit

Permalink
perf(ripple): do not register events if ripples are disabled initially (
Browse files Browse the repository at this point in the history
  • Loading branch information
devversion authored and jelbourn committed Jan 9, 2018
1 parent 70c75ea commit f590646
Show file tree
Hide file tree
Showing 4 changed files with 130 additions and 95 deletions.
6 changes: 3 additions & 3 deletions src/lib/core/ripple/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@ import {PlatformModule} from '@angular/cdk/platform';
import {MatCommonModule} from '../common-behaviors/common-module';
import {MatRipple} from './ripple';

export {MatRipple, RippleGlobalOptions, MAT_RIPPLE_GLOBAL_OPTIONS} from './ripple';
export {RippleRef, RippleState} from './ripple-ref';
export {RippleConfig, RIPPLE_FADE_IN_DURATION, RIPPLE_FADE_OUT_DURATION} from './ripple-renderer';
export * from './ripple';
export * from './ripple-ref';
export * from './ripple-renderer';

@NgModule({
imports: [MatCommonModule, PlatformModule],
Expand Down
63 changes: 40 additions & 23 deletions src/lib/core/ripple/ripple-renderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ 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;

Expand All @@ -30,6 +29,19 @@ export type RippleConfig = {
persistent?: boolean;
};

/**
* Interface that describes the target for launching ripples.
* It defines the ripple configuration and disabled state for interaction ripples.
* @docs-private
*/
export interface RippleTarget {
/** Configuration for ripples that are launched on pointer down. */
rippleConfig: RippleConfig;

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

/**
* 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 @@ -63,10 +75,14 @@ export class RippleRenderer {
/** Ripple config for all ripples created by events. */
rippleConfig: RippleConfig = {};

/** Whether mouse ripples should be created or not. */
rippleDisabled: boolean = false;
/** Options that apply to all the event listeners that are bound by the renderer. */
private _eventOptions = supportsPassiveEventListeners() ? ({passive: true} as any) : false;

constructor(private _target: RippleTarget,
private _ngZone: NgZone,
elementRef: ElementRef,
platform: Platform) {

constructor(elementRef: ElementRef, private _ngZone: NgZone, platform: Platform) {
// Only do anything if we're on the browser.
if (platform.isBrowser) {
this._containerElement = elementRef.nativeElement;
Expand All @@ -78,9 +94,6 @@ export class RippleRenderer {

this._triggerEvents.set('touchstart', this.onTouchStart);
this._triggerEvents.set('touchend', this.onPointerUp);

// By default use the host element as trigger element.
this.setTriggerElement(this._containerElement);
}
}

Expand Down Expand Up @@ -170,22 +183,18 @@ export class RippleRenderer {
this._activeRipples.forEach(ripple => ripple.fadeOut());
}

/** Sets the trigger element and registers the mouse events. */
setTriggerElement(element: HTMLElement | null) {
// Remove all previously register event listeners from the trigger element.
if (this._triggerElement) {
this._triggerEvents.forEach((fn, type) => {
this._triggerElement!.removeEventListener(type, fn, this._eventOptions);
});
/** Sets up the trigger event listeners */
setupTriggerEvents(element: HTMLElement) {
if (!element || element === this._triggerElement) {
return;
}

if (element) {
// If the element is not null, register all event listeners on the trigger element.
this._ngZone.runOutsideAngular(() => {
// Remove all previously registeredevent listeners from the trigger element.
this._removeTriggerEvents(); this._ngZone.runOutsideAngular(() => {
this._triggerEvents.forEach((fn, type) =>
element.addEventListener(type, fn, this._eventOptions));
});
}


this._triggerElement = element;
}
Expand All @@ -195,22 +204,23 @@ export class RippleRenderer {
const isSyntheticEvent = this._lastTouchStartEvent &&
Date.now() < this._lastTouchStartEvent + IGNORE_MOUSE_EVENTS_TIMEOUT;

if (!this.rippleDisabled && !isSyntheticEvent) {
if (!this._target.rippleDisabled && !isSyntheticEvent) {
this._isPointerDown = true;
this.fadeInRipple(event.clientX, event.clientY, this.rippleConfig);
this.fadeInRipple(event.clientX, event.clientY, this._target.rippleConfig);
}
}

/** Function being called whenever the trigger is being pressed using touch. */
private onTouchStart = (event: TouchEvent) => {
if (!this.rippleDisabled) {
if (!this._target.rippleDisabled) {
// Some browsers fire mouse events after a `touchstart` event. Those synthetic mouse
// events will launch a second ripple if we don't ignore mouse events for a specific
// time after a touchstart event.
this._lastTouchStartEvent = Date.now();
this._isPointerDown = true;

this.fadeInRipple(event.touches[0].clientX, event.touches[0].clientY, this.rippleConfig);
this.fadeInRipple(
event.touches[0].clientX, event.touches[0].clientY, this._target.rippleConfig);
}
}

Expand All @@ -235,10 +245,17 @@ export class RippleRenderer {
this._ngZone.runOutsideAngular(() => setTimeout(fn, delay));
}

/** Removes previously registered event listeners from the trigger element. */
_removeTriggerEvents() {
if (this._triggerElement) {
this._triggerEvents.forEach((fn, type) => {
this._triggerElement!.removeEventListener(type, fn, this._eventOptions);
});
}
}
}

/** Enforces a style recalculation of a DOM element by computing its styles. */
// TODO(devversion): Move into global utility function.
function enforceStyleRecalculation(element: HTMLElement) {
// Enforce a style recalculation by calling `getComputedStyle` and accessing any property.
// Calling `getPropertyValue` is important to let optimizers know that this is not a noop.
Expand Down
104 changes: 58 additions & 46 deletions src/lib/core/ripple/ripple.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,21 +6,20 @@
* found in the LICENSE file at https://angular.io/license
*/

import {Platform} from '@angular/cdk/platform';
import {
Directive,
ElementRef,
Input,
Inject,
InjectionToken,
Input,
NgZone,
OnChanges,
SimpleChanges,
OnDestroy,
InjectionToken,
OnInit,
Optional,
} from '@angular/core';
import {Platform} from '@angular/cdk/platform';
import {RippleConfig, RippleRenderer} from './ripple-renderer';
import {RippleRef} from './ripple-ref';
import {RippleConfig, RippleRenderer, RippleTarget} from './ripple-renderer';

/** Configurable options for `matRipple`. */
export interface RippleGlobalOptions {
Expand Down Expand Up @@ -50,28 +49,20 @@ export const MAT_RIPPLE_GLOBAL_OPTIONS =
'[class.mat-ripple-unbounded]': 'unbounded'
}
})
export class MatRipple implements OnChanges, OnDestroy {
export class MatRipple implements OnInit, OnDestroy, RippleTarget {

/**
* The element that triggers the ripple when click events are received. Defaults to the
* directive's host element.
*/
// Prevent TS metadata emit from referencing HTMLElement in ripple.js
// Otherwise running this code in a Node environment (e.g Universal) will not work.
@Input('matRippleTrigger') trigger: HTMLElement|HTMLElement;
/** Custom color for all ripples. */
@Input('matRippleColor') color: string;

/** Whether the ripples should be visible outside the component's bounds. */
@Input('matRippleUnbounded') unbounded: boolean;

/**
* Whether the ripple always originates from the center of the host element's bounds, rather
* than originating from the location of the click event.
*/
@Input('matRippleCentered') centered: boolean;

/**
* Whether click events will not trigger the ripple. Ripples can be still launched manually
* by using the `launch()` method.
*/
@Input('matRippleDisabled') disabled: boolean;

/**
* If set, the radius in pixels of foreground ripples when fully expanded. If unset, the radius
* will be the distance from the center of the ripple to the furthest corner of the host element's
Expand All @@ -86,45 +77,59 @@ export class MatRipple implements OnChanges, OnDestroy {
*/
@Input('matRippleSpeedFactor') speedFactor: number = 1;

/** Custom color for ripples. */
@Input('matRippleColor') color: string;
/**
* Whether click events will not trigger the ripple. Ripples can be still launched manually
* by using the `launch()` method.
*/
@Input('matRippleDisabled')
get disabled() { return this._disabled; }
set disabled(value: boolean) {
this._disabled = value;
this._setupTriggerEventsIfEnabled();
}
private _disabled: boolean = false;

/** Whether foreground ripples should be visible outside the component's bounds. */
@Input('matRippleUnbounded') unbounded: boolean;
/**
* The element that triggers the ripple when click events are received.
* Defaults to the directive's host element.
*/
@Input('matRippleTrigger')
get trigger() { return this._trigger || this._elementRef.nativeElement; }
set trigger(trigger: HTMLElement) {
this._trigger = trigger;
this._setupTriggerEventsIfEnabled();
}
private _trigger: HTMLElement;

/** Renderer for the ripple DOM manipulations. */
private _rippleRenderer: RippleRenderer;

/** Options that are set globally for all ripples. */
private _globalOptions: RippleGlobalOptions;

constructor(
elementRef: ElementRef,
ngZone: NgZone,
platform: Platform,
@Optional() @Inject(MAT_RIPPLE_GLOBAL_OPTIONS) globalOptions: RippleGlobalOptions
) {
this._rippleRenderer = new RippleRenderer(elementRef, ngZone, platform);
this._globalOptions = globalOptions ? globalOptions : {};
/** Whether ripple directive is initialized and the input bindings are set. */
private _isInitialized: boolean = false;

this._updateRippleRenderer();
}
constructor(private _elementRef: ElementRef,
ngZone: NgZone,
platform: Platform,
@Optional() @Inject(MAT_RIPPLE_GLOBAL_OPTIONS) globalOptions: RippleGlobalOptions) {

ngOnChanges(changes: SimpleChanges) {
if (changes['trigger'] && this.trigger) {
this._rippleRenderer.setTriggerElement(this.trigger);
}
this._globalOptions = globalOptions || {};
this._rippleRenderer = new RippleRenderer(this, ngZone, _elementRef, platform);
}

this._updateRippleRenderer();
ngOnInit() {
this._isInitialized = true;
this._setupTriggerEventsIfEnabled();
}

ngOnDestroy() {
// Set the trigger element to null to cleanup all listeners.
this._rippleRenderer.setTriggerElement(null);
this._rippleRenderer._removeTriggerEvents();
}

/** Launches a manual ripple at the specified position. */
launch(x: number, y: number, config: RippleConfig = this.rippleConfig): RippleRef {
launch(x: number, y: number, config: RippleConfig = this): RippleRef {
return this._rippleRenderer.fadeInRipple(x, y, config);
}

Expand All @@ -143,9 +148,16 @@ export class MatRipple implements OnChanges, OnDestroy {
};
}

/** Updates the ripple renderer with the latest ripple configuration. */
_updateRippleRenderer() {
this._rippleRenderer.rippleDisabled = this._globalOptions.disabled || this.disabled;
this._rippleRenderer.rippleConfig = this.rippleConfig;
/** Whether ripples on pointer-down are disabled or not. */
get rippleDisabled(): boolean {
return this.disabled || !!this._globalOptions.disabled;
}

/** Sets up the the trigger event listeners if ripples are enabled. */
private _setupTriggerEventsIfEnabled() {
if (!this.disabled && this._isInitialized) {
this._rippleRenderer.setupTriggerEvents(this.trigger);
}
}
}

Loading

0 comments on commit f590646

Please sign in to comment.