Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

perf(ripple): do not register events if ripples are disabled initially #8882

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
70 changes: 41 additions & 29 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 @@ -60,13 +72,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 _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 +88,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 +177,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._removeTriggerEvents();

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

this._triggerElement = element;
}
Expand All @@ -195,22 +199,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 +240,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