From e3ba1e1c267ce3ab4ceb9ed8d1e760c23439ee1f Mon Sep 17 00:00:00 2001 From: Paul Gschwendtner Date: Wed, 1 Mar 2017 19:49:12 +0100 Subject: [PATCH] feat(ripple): add option for persistent ripples (#3315) Closes #3169 --- src/demo-app/ripple/ripple-demo.html | 4 +- src/demo-app/ripple/ripple-demo.ts | 10 ++- src/lib/core/core.ts | 4 +- src/lib/core/option/option.ts | 2 +- src/lib/core/ripple/index.ts | 25 +++++++ src/lib/core/ripple/ripple-ref.ts | 18 +++++ src/lib/core/ripple/ripple-renderer.ts | 89 ++++++++++++++++--------- src/lib/core/ripple/ripple.spec.ts | 35 +++++++++- src/lib/core/ripple/ripple.ts | 34 +++------- src/lib/menu/menu.ts | 2 +- src/lib/slide-toggle/slide-toggle.ts | 2 +- src/lib/tabs/tab-body.spec.ts | 2 +- src/lib/tabs/tab-group.ts | 2 +- src/lib/tabs/tab-header.spec.ts | 2 +- src/lib/tabs/tab-nav-bar/tab-nav-bar.ts | 2 +- 15 files changed, 163 insertions(+), 70 deletions(-) create mode 100644 src/lib/core/ripple/index.ts create mode 100644 src/lib/core/ripple/ripple-ref.ts diff --git a/src/demo-app/ripple/ripple-demo.html b/src/demo-app/ripple/ripple-demo.html index 0cec0791fbe3..14f867a0aabc 100644 --- a/src/demo-app/ripple/ripple-demo.html +++ b/src/demo-app/ripple/ripple-demo.html @@ -35,7 +35,9 @@
- + + +
{ - const distX = Math.max(Math.abs(x - rect.left), Math.abs(x - rect.right)); - const distY = Math.max(Math.abs(y - rect.top), Math.abs(y - rect.bottom)); - return Math.sqrt(distX * distX + distY * distY); -}; - export type RippleConfig = { color?: string; centered?: boolean; radius?: number; speedFactor?: number; + persistent?: boolean; }; /** @@ -41,12 +34,12 @@ export class RippleRenderer { /** Whether the mouse is currently down or not. */ private _isMousedown: boolean = false; - /** Currently active ripples that will be closed on mouseup. */ - private _activeRipples: HTMLElement[] = []; - /** Events to be registered on the trigger element. */ private _triggerEvents = new Map(); + /** Set of currently active ripple references. */ + private _activeRipples = new Set(); + /** Ripple config for all ripples created by events. */ rippleConfig: RippleConfig = {}; @@ -66,7 +59,7 @@ export class RippleRenderer { } /** Fades in a ripple at the given coordinates. */ - fadeInRipple(pageX: number, pageY: number, config: RippleConfig = {}) { + fadeInRipple(pageX: number, pageY: number, config: RippleConfig = {}): RippleRef { let containerRect = this._containerElement.getBoundingClientRect(); if (config.centered) { @@ -101,28 +94,46 @@ export class RippleRenderer { // By default the browser does not recalculate the styles of dynamically created // ripple elements. This is critical because then the `scale` would not animate properly. - this._enforceStyleRecalculation(ripple); + enforceStyleRecalculation(ripple); ripple.style.transform = 'scale(1)'; - // Wait for the ripple to be faded in. Once it's faded in, the ripple can be hidden immediately - // if the mouse is released. + // Exposed reference to the ripple that will be returned. + let rippleRef = new RippleRef(this, ripple, config); + + // Wait for the ripple element to be completely faded in. + // Once it's faded in, the ripple can be hidden immediately if the mouse is released. this.runTimeoutOutsideZone(() => { - this._isMousedown ? this._activeRipples.push(ripple) : this.fadeOutRipple(ripple); + if (config.persistent || this._isMousedown) { + this._activeRipples.add(rippleRef); + } else { + rippleRef.fadeOut(); + } }, duration); + + return rippleRef; } - /** Fades out a ripple element. */ - fadeOutRipple(ripple: HTMLElement) { - ripple.style.transitionDuration = `${RIPPLE_FADE_OUT_DURATION}ms`; - ripple.style.opacity = '0'; + /** Fades out a ripple reference. */ + fadeOutRipple(ripple: RippleRef) { + let rippleEl = ripple.element; + + this._activeRipples.delete(ripple); + + rippleEl.style.transitionDuration = `${RIPPLE_FADE_OUT_DURATION}ms`; + rippleEl.style.opacity = '0'; // Once the ripple faded out, the ripple can be safely removed from the DOM. this.runTimeoutOutsideZone(() => { - ripple.parentNode.removeChild(ripple); + rippleEl.parentNode.removeChild(rippleEl); }, RIPPLE_FADE_OUT_DURATION); } + /** Fades out all currently active ripples. */ + fadeOutAll() { + this._activeRipples.forEach(ripple => ripple.fadeOut()); + } + /** Sets the trigger element and registers the mouse events. */ setTriggerElement(element: HTMLElement) { // Remove all previously register event listeners from the trigger element. @@ -151,8 +162,13 @@ export class RippleRenderer { /** Listener being called on mouseup event. */ private onMouseup() { this._isMousedown = false; - this._activeRipples.forEach(ripple => this.fadeOutRipple(ripple)); - this._activeRipples = []; + + // On mouseup, fade-out all ripples that are active and not persistent. + this._activeRipples.forEach(ripple => { + if (!ripple.config.persistent) { + ripple.fadeOut(); + } + }); } /** Listener being called on mouseleave event. */ @@ -167,13 +183,22 @@ export class RippleRenderer { 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. - private _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. - // See: https://gist.github.com/paulirish/5d52fb081b3570c81e3a - window.getComputedStyle(element).getPropertyValue('opacity'); - } +} + +/** 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. + // See: https://gist.github.com/paulirish/5d52fb081b3570c81e3a + window.getComputedStyle(element).getPropertyValue('opacity'); +} +/** + * Returns the distance from the point (x, y) to the furthest corner of a rectangle. + */ +function distanceToFurthestCorner(x: number, y: number, rect: ClientRect) { + const distX = Math.max(Math.abs(x - rect.left), Math.abs(x - rect.right)); + const distY = Math.max(Math.abs(y - rect.top), Math.abs(y - rect.bottom)); + return Math.sqrt(distX * distX + distY * distY); } diff --git a/src/lib/core/ripple/ripple.spec.ts b/src/lib/core/ripple/ripple.spec.ts index 68e2c62238f4..71531a9abbff 100644 --- a/src/lib/core/ripple/ripple.spec.ts +++ b/src/lib/core/ripple/ripple.spec.ts @@ -1,6 +1,6 @@ import {TestBed, ComponentFixture, fakeAsync, tick, inject} from '@angular/core/testing'; import {Component, ViewChild} from '@angular/core'; -import {MdRipple, MdRippleModule} from './ripple'; +import {MdRipple, MdRippleModule} from './index'; import {ViewportRuler} from '../overlay/position/viewport-ruler'; import {RIPPLE_FADE_OUT_DURATION, RIPPLE_FADE_IN_DURATION} from './ripple-renderer'; import {dispatchMouseEvent} from '../testing/dispatch-events'; @@ -239,6 +239,39 @@ describe('MdRipple', () => { }); + describe('manual ripples', () => { + let rippleDirective: MdRipple; + + beforeEach(() => { + fixture = TestBed.createComponent(BasicRippleContainer); + fixture.detectChanges(); + + rippleTarget = fixture.nativeElement.querySelector('[mat-ripple]'); + rippleDirective = fixture.componentInstance.ripple; + }); + + it('should allow persistent ripple elements', fakeAsync(() => { + expect(rippleTarget.querySelectorAll('.mat-ripple-element').length).toBe(0); + + let rippleRef = rippleDirective.launch(0, 0, { persistent: true }); + + expect(rippleTarget.querySelectorAll('.mat-ripple-element').length).toBe(1); + + // Calculates the duration for fading-in and fading-out the ripple. Also adds some + // extra time to demonstrate that the ripples are persistent. + tick(RIPPLE_FADE_IN_DURATION + RIPPLE_FADE_OUT_DURATION + 5000); + + expect(rippleTarget.querySelectorAll('.mat-ripple-element').length).toBe(1); + + rippleRef.fadeOut(); + + tick(RIPPLE_FADE_OUT_DURATION); + + expect(rippleTarget.querySelectorAll('.mat-ripple-element').length).toBe(0); + })); + + }); + describe('configuring behavior', () => { let controller: RippleContainerWithInputBindings; let rippleComponent: MdRipple; diff --git a/src/lib/core/ripple/ripple.ts b/src/lib/core/ripple/ripple.ts index d2bd9b6cae2c..5218e7e75e94 100644 --- a/src/lib/core/ripple/ripple.ts +++ b/src/lib/core/ripple/ripple.ts @@ -1,6 +1,4 @@ import { - NgModule, - ModuleWithProviders, Directive, ElementRef, Input, @@ -10,9 +8,8 @@ import { OnDestroy, } from '@angular/core'; import {RippleConfig, RippleRenderer} from './ripple-renderer'; -import {CompatibilityModule} from '../compatibility/compatibility'; -import {ViewportRuler, VIEWPORT_RULER_PROVIDER} from '../overlay/position/viewport-ruler'; -import {SCROLL_DISPATCHER_PROVIDER} from '../overlay/scroll/scroll-dispatcher'; +import {ViewportRuler} from '../overlay/position/viewport-ruler'; +import {RippleRef} from './ripple-ref'; @Directive({ @@ -87,8 +84,13 @@ export class MdRipple implements OnChanges, OnDestroy { } /** Launches a manual ripple at the specified position. */ - launch(pageX: number, pageY: number, config = this.rippleConfig) { - this._rippleRenderer.fadeInRipple(pageX, pageY, config); + launch(pageX: number, pageY: number, config = this.rippleConfig): RippleRef { + return this._rippleRenderer.fadeInRipple(pageX, pageY, config); + } + + /** Fades out all currently showing ripple elements. */ + fadeOutAll() { + this._rippleRenderer.fadeOutAll(); } /** Ripple configuration from the directive's input values. */ @@ -100,22 +102,4 @@ export class MdRipple implements OnChanges, OnDestroy { color: this.color }; } - -} - - -@NgModule({ - imports: [CompatibilityModule], - exports: [MdRipple, CompatibilityModule], - declarations: [MdRipple], - providers: [VIEWPORT_RULER_PROVIDER, SCROLL_DISPATCHER_PROVIDER], -}) -export class MdRippleModule { - /** @deprecated */ - static forRoot(): ModuleWithProviders { - return { - ngModule: MdRippleModule, - providers: [] - }; - } } diff --git a/src/lib/menu/menu.ts b/src/lib/menu/menu.ts index 27d551aca46f..262ca5e7623e 100644 --- a/src/lib/menu/menu.ts +++ b/src/lib/menu/menu.ts @@ -4,7 +4,7 @@ import {OverlayModule, CompatibilityModule} from '../core'; import {MdMenu} from './menu-directive'; import {MdMenuItem} from './menu-item'; import {MdMenuTrigger} from './menu-trigger'; -import {MdRippleModule} from '../core/ripple/ripple'; +import {MdRippleModule} from '../core/ripple/index'; export {MdMenu} from './menu-directive'; export {MdMenuItem} from './menu-item'; export {MdMenuTrigger} from './menu-trigger'; diff --git a/src/lib/slide-toggle/slide-toggle.ts b/src/lib/slide-toggle/slide-toggle.ts index fd2a2bd9b175..2cc0edde5064 100644 --- a/src/lib/slide-toggle/slide-toggle.ts +++ b/src/lib/slide-toggle/slide-toggle.ts @@ -23,7 +23,7 @@ import { CompatibilityModule, } from '../core'; import {Observable} from 'rxjs/Observable'; -import {MdRippleModule} from '../core/ripple/ripple'; +import {MdRippleModule} from '../core/ripple/index'; export const MD_SLIDE_TOGGLE_VALUE_ACCESSOR: any = { diff --git a/src/lib/tabs/tab-body.spec.ts b/src/lib/tabs/tab-body.spec.ts index f0bae7c6a757..4ff41baf642a 100644 --- a/src/lib/tabs/tab-body.spec.ts +++ b/src/lib/tabs/tab-body.spec.ts @@ -3,7 +3,7 @@ import {Component, ViewChild, TemplateRef, ViewContainerRef} from '@angular/core import {LayoutDirection, Dir} from '../core/rtl/dir'; import {TemplatePortal} from '../core/portal/portal'; import {MdTabBody} from './tab-body'; -import {MdRippleModule} from '../core/ripple/ripple'; +import {MdRippleModule} from '../core/ripple/index'; import {CommonModule} from '@angular/common'; import {PortalModule} from '../core'; diff --git a/src/lib/tabs/tab-group.ts b/src/lib/tabs/tab-group.ts index c40f9fa5a28f..3b7b8d62d639 100644 --- a/src/lib/tabs/tab-group.ts +++ b/src/lib/tabs/tab-group.ts @@ -22,7 +22,7 @@ import {MdTabNavBar, MdTabLink, MdTabLinkRipple} from './tab-nav-bar/tab-nav-bar import {MdInkBar} from './ink-bar'; import {Observable} from 'rxjs/Observable'; import 'rxjs/add/operator/map'; -import {MdRippleModule} from '../core/ripple/ripple'; +import {MdRippleModule} from '../core/ripple/index'; import {ObserveContentModule} from '../core/observe-content/observe-content'; import {MdTab} from './tab'; import {MdTabBody} from './tab-body'; diff --git a/src/lib/tabs/tab-header.spec.ts b/src/lib/tabs/tab-header.spec.ts index ef7bac7e39c4..3c3e068bd06e 100644 --- a/src/lib/tabs/tab-header.spec.ts +++ b/src/lib/tabs/tab-header.spec.ts @@ -2,7 +2,7 @@ import {async, ComponentFixture, TestBed} from '@angular/core/testing'; import {Component, ViewChild, ViewContainerRef} from '@angular/core'; import {LayoutDirection, Dir} from '../core/rtl/dir'; import {MdTabHeader} from './tab-header'; -import {MdRippleModule} from '../core/ripple/ripple'; +import {MdRippleModule} from '../core/ripple/index'; import {CommonModule} from '@angular/common'; import {PortalModule} from '../core'; import {MdInkBar} from './ink-bar'; 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 995261e91b53..930f8a9a2f1e 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,7 @@ import { NgZone, } from '@angular/core'; import {MdInkBar} from '../ink-bar'; -import {MdRipple} from '../../core/ripple/ripple'; +import {MdRipple} from '../../core/ripple/index'; import {ViewportRuler} from '../../core/overlay/position/viewport-ruler'; /**