Skip to content

Commit

Permalink
perf(ripple): do not register events if ripples are disabled
Browse files Browse the repository at this point in the history
* No longer registers trigger event listeners if the ripples are disabled initially.

Fixes angular#8854.
  • Loading branch information
devversion committed Dec 31, 2017
1 parent c3d7cd9 commit dbb00f9
Show file tree
Hide file tree
Showing 3 changed files with 83 additions and 76 deletions.
58 changes: 29 additions & 29 deletions src/lib/core/ripple/ripple-renderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@
*/
import {ElementRef, NgZone} from '@angular/core';
import {Platform, supportsPassiveEventListeners} from '@angular/cdk/platform';
import {MatRipple} from './ripple';
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 Down Expand Up @@ -60,13 +60,11 @@ export class RippleRenderer {
/** Options that apply to all the event listeners that are bound by the renderer. */
private _eventOptions = supportsPassiveEventListeners() ? ({passive: true} as any) : false;

/** Ripple config for all ripples created by events. */
rippleConfig: RippleConfig = {};

/** Whether mouse ripples should be created or not. */
rippleDisabled: boolean = false;
constructor(private _ripple: MatRipple,
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 +76,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 +165,19 @@ 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(() => {
this._triggerEvents.forEach((fn, type) =>
element.addEventListener(type, fn, this._eventOptions));
});
}
// Remove all previously registered event listeners from the trigger element.
this._removeTriggerListeners();

this._ngZone.runOutsideAngular(() => {
this._triggerEvents.forEach((fn, type) =>
element.addEventListener(type, fn, this._eventOptions));
});

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

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

/** Function being called whenever the trigger is being pressed using touch. */
private onTouchStart = (event: TouchEvent) => {
if (!this.rippleDisabled) {
if (!this._ripple.disabled) {
// 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._ripple.rippleConfig);
}
}

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

/** Removes previously registered event listeners from the trigger element. */
_removeTriggerListeners() {
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
99 changes: 53 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} 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 {

/**
* 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 || !!this._globalOptions.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._removeTriggerListeners();
}

/** 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,11 @@ 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;
/** Sets up the the trigger event listeners if ripples are enabled. */
private _setupTriggerEventsIfEnabled() {
if (!this.disabled && this._isInitialized) {
this._rippleRenderer.setupTriggerEvents(this.trigger);
}
}
}

2 changes: 1 addition & 1 deletion src/lib/tabs/tab-nav-bar/tab-nav-bar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -215,7 +215,6 @@ export class MatTabLink extends _MatTabLinkMixinBase
set disableRipple(value: boolean) {
this._disableRipple = value;
this._tabLinkRipple.disabled = this.disableRipple;
this._tabLinkRipple._updateRippleRenderer();
}

constructor(private _tabNavBar: MatTabNav,
Expand All @@ -229,6 +228,7 @@ export class MatTabLink extends _MatTabLinkMixinBase
// Manually create a ripple instance that uses the tab link element as trigger element.
// Notice that the lifecycle hooks for the ripple config won't be called anymore.
this._tabLinkRipple = new MatRipple(_elementRef, ngZone, platform, globalOptions);
this._tabLinkRipple.ngOnInit();

this.tabIndex = parseInt(tabIndex) || 0;
}
Expand Down

0 comments on commit dbb00f9

Please sign in to comment.