diff --git a/src/lib/core/ripple/index.ts b/src/lib/core/ripple/index.ts index 94da47051775..d76f94314e58 100644 --- a/src/lib/core/ripple/index.ts +++ b/src/lib/core/ripple/index.ts @@ -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], diff --git a/src/lib/core/ripple/ripple-renderer.ts b/src/lib/core/ripple/ripple-renderer.ts index 0f8342589c22..a01c808e754e 100644 --- a/src/lib/core/ripple/ripple-renderer.ts +++ b/src/lib/core/ripple/ripple-renderer.ts @@ -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; @@ -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 @@ -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; @@ -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); } } @@ -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; } @@ -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); } } @@ -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. diff --git a/src/lib/core/ripple/ripple.ts b/src/lib/core/ripple/ripple.ts index 93ef4b6dc0a0..5a26091d10b7 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, RippleTarget} 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, 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 @@ -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,29 @@ 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; @@ -98,33 +107,29 @@ 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 : {}; + /** 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); } @@ -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); + } } } + 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..b4ea13503cd4 100644 --- a/src/lib/tabs/tab-nav-bar/tab-nav-bar.ts +++ b/src/lib/tabs/tab-nav-bar/tab-nav-bar.ts @@ -8,7 +8,6 @@ import {Directionality} from '@angular/cdk/bidi'; import {coerceBooleanProperty} from '@angular/cdk/coercion'; import {Platform} from '@angular/cdk/platform'; -import {takeUntil} from 'rxjs/operators/takeUntil'; import {ViewportRuler} from '@angular/cdk/scrolling'; import { AfterContentInit, @@ -35,16 +34,18 @@ import { CanDisableRipple, HasTabIndex, MAT_RIPPLE_GLOBAL_OPTIONS, - MatRipple, mixinColor, mixinDisabled, mixinDisableRipple, - mixinTabIndex, + mixinTabIndex, RippleConfig, RippleGlobalOptions, + RippleRenderer, + RippleTarget, ThemePalette, } from '@angular/material/core'; import {merge} from 'rxjs/observable/merge'; import {of as observableOf} from 'rxjs/observable/of'; +import {takeUntil} from 'rxjs/operators/takeUntil'; import {Subject} from 'rxjs/Subject'; import {MatInkBar} from '../ink-bar'; @@ -171,7 +172,8 @@ export class MatTabNav extends _MatTabNavMixinBase implements AfterContentInit, // Boilerplate for applying mixins to MatTabLink. export class MatTabLinkBase {} -export const _MatTabLinkMixinBase = mixinTabIndex(mixinDisabled(MatTabLinkBase)); +export const _MatTabLinkMixinBase = + mixinTabIndex(mixinDisableRipple(mixinDisabled(MatTabLinkBase))); /** * Link inside of a `mat-tab-nav-bar`. @@ -179,7 +181,7 @@ export const _MatTabLinkMixinBase = mixinTabIndex(mixinDisabled(MatTabLinkBase)) @Directive({ selector: '[mat-tab-link], [matTabLink]', exportAs: 'matTabLink', - inputs: ['disabled', 'tabIndex'], + inputs: ['disabled', 'disableRipple', 'tabIndex'], host: { 'class': 'mat-tab-link', '[attr.aria-disabled]': 'disabled.toString()', @@ -189,16 +191,13 @@ export const _MatTabLinkMixinBase = mixinTabIndex(mixinDisabled(MatTabLinkBase)) } }) export class MatTabLink extends _MatTabLinkMixinBase - implements OnDestroy, CanDisable, HasTabIndex { + implements OnDestroy, CanDisable, CanDisableRipple, HasTabIndex, RippleTarget { /** Whether the tab link is active or not. */ private _isActive: boolean = false; - /** Whether the ripples for this tab should be disabled or not. */ - private _disableRipple: boolean = false; - - /** Reference to the instance of the ripple for the tab link. */ - private _tabLinkRipple: MatRipple; + /** Reference to the RippleRenderer for the tab-link. */ + private _tabLinkRipple: RippleRenderer; /** Whether the link is active. */ @Input() @@ -210,12 +209,18 @@ export class MatTabLink extends _MatTabLinkMixinBase } } - /** Whether ripples should be disabled or not. */ - get disableRipple(): boolean { return this.disabled || this._disableRipple; } - set disableRipple(value: boolean) { - this._disableRipple = value; - this._tabLinkRipple.disabled = this.disableRipple; - this._tabLinkRipple._updateRippleRenderer(); + /** + * Ripple configuration for ripples that are launched on pointer down. + * @docs-private + */ + rippleConfig: RippleConfig = {}; + + /** + * Whether ripples are disabled on interaction + * @docs-private + */ + get rippleDisabled(): boolean { + return this.disabled || this.disableRipple; } constructor(private _tabNavBar: MatTabNav, @@ -226,16 +231,17 @@ export class MatTabLink extends _MatTabLinkMixinBase @Attribute('tabindex') tabIndex: string) { super(); - // 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 = new RippleRenderer(this, ngZone, _elementRef, platform); + this._tabLinkRipple.setupTriggerEvents(_elementRef.nativeElement); this.tabIndex = parseInt(tabIndex) || 0; + + if (globalOptions) { + this.rippleConfig = {speedFactor: globalOptions.baseSpeedFactor}; + } } ngOnDestroy() { - // Manually call the ngOnDestroy lifecycle hook of the ripple instance because it won't be - // called automatically since its instance is not created by Angular. - this._tabLinkRipple.ngOnDestroy(); + this._tabLinkRipple._removeTriggerEvents(); } }