diff --git a/src/lib/core/ripple/ripple-renderer.ts b/src/lib/core/ripple/ripple-renderer.ts index f5691f982a5a..9a809774e66a 100644 --- a/src/lib/core/ripple/ripple-renderer.ts +++ b/src/lib/core/ripple/ripple-renderer.ts @@ -8,9 +8,9 @@ import {ElementRef, NgZone} from '@angular/core'; import {Platform} 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; @@ -58,13 +58,11 @@ export class RippleRenderer { /** Time in milliseconds when the last touchstart event happened. */ private _lastTouchStartEvent: number; - /** 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; @@ -76,9 +74,6 @@ export class RippleRenderer { this._triggerEvents.set('touchstart', this.onTouchStart.bind(this)); this._triggerEvents.set('touchend', this.onPointerUp.bind(this)); - - // By default use the host element as trigger element. - this.setTriggerElement(this._containerElement); } } @@ -170,6 +165,10 @@ export class RippleRenderer { /** Sets the trigger element and registers the mouse events. */ setTriggerElement(element: HTMLElement | null) { + if (element === this._triggerElement) { + return; + } + // Remove all previously register event listeners from the trigger element. if (this._triggerElement) { this._triggerEvents.forEach((fn, type) => { @@ -192,22 +191,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); } } @@ -231,11 +231,9 @@ export class RippleRenderer { private runTimeoutOutsideZone(fn: Function, delay = 0) { this._ngZone.runOutsideAngular(() => setTimeout(fn, delay)); } - } /** 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. diff --git a/src/lib/core/ripple/ripple.ts b/src/lib/core/ripple/ripple.ts index 93ef4b6dc0a0..f69cfa6cdf50 100644 --- a/src/lib/core/ripple/ripple.ts +++ b/src/lib/core/ripple/ripple.ts @@ -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 { @@ -50,15 +49,13 @@ 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 @@ -66,12 +63,6 @@ export class MatRipple implements OnChanges, OnDestroy { */ @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 @@ -86,11 +77,39 @@ export class MatRipple implements OnChanges, OnDestroy { */ @Input('matRippleSpeedFactor') speedFactor: number = 1; - /** Custom color for ripples. */ - @Input('matRippleColor') color: string; + /** + * 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; } + set trigger(trigger: HTMLElement | null) { + this._trigger = trigger; + + // The events for a trigger element shouldn't be registered if ripples are disabled. + // As soon as the ripples are enabled again, the events for the current trigger will be added. + if (!this.disabled && this._isInitialized) { + this._rippleRenderer.setTriggerElement(trigger); + } + } + private _trigger: HTMLElement | null; - /** Whether foreground ripples should be visible outside the component's bounds. */ - @Input('matRippleUnbounded') unbounded: boolean; + /** + * 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; + + // The events for the trigger element haven't been added if ripples were disabled. This means + // that the events for the trigger element need to be registered if ripples are enabled again. + if (!this.disabled && this._isInitialized) { + this._rippleRenderer.setTriggerElement(this._trigger); + } + } + private _disabled: boolean = false; /** Renderer for the ripple DOM manipulations. */ private _rippleRenderer: RippleRenderer; @@ -98,24 +117,31 @@ export class MatRipple implements OnChanges, OnDestroy { /** 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 : {}; + /** Reference to the directive's host HTMLElement. */ + private _hostElement: HTMLElement; + + /** Whether ripple directive is initialized and the input bindings are set. */ + private _isInitialized: boolean = false; - this._updateRippleRenderer(); + constructor(elementRef: ElementRef, + ngZone: NgZone, + platform: Platform, + @Optional() @Inject(MAT_RIPPLE_GLOBAL_OPTIONS) globalOptions: RippleGlobalOptions) { + + this._globalOptions = globalOptions || {}; + this._hostElement = elementRef.nativeElement; + this._rippleRenderer = new RippleRenderer(this, ngZone, elementRef, platform); } - ngOnChanges(changes: SimpleChanges) { - if (changes['trigger'] && this.trigger) { - this._rippleRenderer.setTriggerElement(this.trigger); - } + ngOnInit() { + this._isInitialized = true; - this._updateRippleRenderer(); + // The trigger element might have been set before the `ngOnInit` lifecycle hook, but the + // events haven't been registered yet. This has been deferred to the `ngOnInit` lifecycle hook, + // because the events should be only registered if the ripples aren't disabled initially. + // Also if there is no trigger element, that has been specified explicitly through the input, + // the default trigger element will be the directive's host element. + this.trigger = this._trigger === undefined ? this._hostElement : this.trigger; } ngOnDestroy() { @@ -124,7 +150,7 @@ export class MatRipple implements OnChanges, OnDestroy { } /** 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); } @@ -142,10 +168,4 @@ export class MatRipple implements OnChanges, OnDestroy { color: this.color }; } - - /** Updates the ripple renderer with the latest ripple configuration. */ - _updateRippleRenderer() { - this._rippleRenderer.rippleDisabled = this._globalOptions.disabled || this.disabled; - this._rippleRenderer.rippleConfig = this.rippleConfig; - } } diff --git a/src/lib/tabs/tab-nav-bar/tab-nav-bar.ts b/src/lib/tabs/tab-nav-bar/tab-nav-bar.ts index da9c40358dcc..175b942cd3f2 100644 --- a/src/lib/tabs/tab-nav-bar/tab-nav-bar.ts +++ b/src/lib/tabs/tab-nav-bar/tab-nav-bar.ts @@ -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, @@ -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; }