From 06688790557d3e038e66593825005bdefe61c5f7 Mon Sep 17 00:00:00 2001 From: Paul Gschwendtner Date: Sun, 26 Feb 2017 21:50:59 +0100 Subject: [PATCH 1/4] feat(ripple): add option for persistent ripples * Adds an option to the ripple service that allows persistent ripples (useful for focus indicators) * Manually launched ripples now return a `RippleRef` instance that can be used to to fade-out the ripples. * Adds a method to the component that developers to fade-out all currently active ripple elements. 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 | 26 ++++++++++ src/lib/core/ripple/ripple-renderer.ts | 68 +++++++++++++++---------- src/lib/core/ripple/ripple.spec.ts | 35 ++++++++++++- src/lib/core/ripple/ripple.ts | 39 +++++--------- 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, 160 insertions(+), 65 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(); + /** Currently active ripples. */ + activeRipples: RippleRef[] = []; + /** 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,15 +94,24 @@ 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.push(rippleRef); + } else { + rippleRef.fadeOut(); + } }, duration); + + return rippleRef; } /** Fades out a ripple element. */ @@ -151,8 +153,11 @@ 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 + .filter(ripple => !ripple.config.persistent) + .forEach(ripple => ripple.fadeOut()); } /** Listener being called on mouseleave event. */ @@ -167,13 +172,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..83f67ee34e7e 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,18 @@ 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() { + // Iterate in reverse, to avoid issues with the `fadeOut` method that will immediately remove + // items from the array. + let i = this._rippleRenderer.activeRipples.length; + while (i--) { + this._rippleRenderer.activeRipples[i].fadeOut(); + } } /** Ripple configuration from the directive's input values. */ @@ -100,22 +107,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 0603b311c281..8f71b43c05ee 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'; /** From a4f2dbedbb280e77b79b0cf8adc15d0465f9df99 Mon Sep 17 00:00:00 2001 From: Paul Gschwendtner Date: Sun, 26 Feb 2017 22:24:36 +0100 Subject: [PATCH 2/4] Address feedback --- src/lib/core/ripple/ripple-ref.ts | 12 ++---------- src/lib/core/ripple/ripple-renderer.ts | 18 +++++++++++++----- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/src/lib/core/ripple/ripple-ref.ts b/src/lib/core/ripple/ripple-ref.ts index f98a160846e2..78a293802be1 100644 --- a/src/lib/core/ripple/ripple-ref.ts +++ b/src/lib/core/ripple/ripple-ref.ts @@ -6,21 +6,13 @@ import {RippleConfig, RippleRenderer} from './ripple-renderer'; export class RippleRef { constructor( - private renderer: RippleRenderer, + private _renderer: RippleRenderer, public element: HTMLElement, public config: RippleConfig) { } /** Fades out the ripple element. */ fadeOut() { - let rippleIndex = this.renderer.activeRipples.indexOf(this); - - // Remove the ripple reference if added to the list of active ripples. - if (rippleIndex !== -1) { - this.renderer.activeRipples.splice(rippleIndex, 1); - } - - // Regardless of being added to the list, fade-out the ripple element. - this.renderer.fadeOutRipple(this.element); + this._renderer.fadeOutRipple(this); } } diff --git a/src/lib/core/ripple/ripple-renderer.ts b/src/lib/core/ripple/ripple-renderer.ts index d0f30e3501ac..be2e8a46df18 100644 --- a/src/lib/core/ripple/ripple-renderer.ts +++ b/src/lib/core/ripple/ripple-renderer.ts @@ -114,14 +114,22 @@ export class RippleRenderer { 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; + let rippleIndex = this.activeRipples.indexOf(ripple); + + // Remove the ripple reference if added to the list of active ripples. + if (rippleIndex !== -1) { + this.activeRipples.splice(rippleIndex, 1); + } + + 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); } From 766027e5685f10c066df0dbcb2b427021d284f3e Mon Sep 17 00:00:00 2001 From: Paul Gschwendtner Date: Sun, 26 Feb 2017 23:13:18 +0100 Subject: [PATCH 3/4] Move fadeOutAll logic into renderer --- src/lib/core/ripple/ripple-renderer.ts | 22 ++++++++++++++++------ src/lib/core/ripple/ripple.ts | 7 +------ 2 files changed, 17 insertions(+), 12 deletions(-) diff --git a/src/lib/core/ripple/ripple-renderer.ts b/src/lib/core/ripple/ripple-renderer.ts index be2e8a46df18..e79d5b98690e 100644 --- a/src/lib/core/ripple/ripple-renderer.ts +++ b/src/lib/core/ripple/ripple-renderer.ts @@ -37,8 +37,8 @@ export class RippleRenderer { /** Events to be registered on the trigger element. */ private _triggerEvents = new Map(); - /** Currently active ripples. */ - activeRipples: RippleRef[] = []; + /** Currently active ripple references. */ + private _activeRipples: RippleRef[] = []; /** Ripple config for all ripples created by events. */ rippleConfig: RippleConfig = {}; @@ -105,7 +105,7 @@ export class RippleRenderer { // Once it's faded in, the ripple can be hidden immediately if the mouse is released. this.runTimeoutOutsideZone(() => { if (config.persistent || this._isMousedown) { - this.activeRipples.push(rippleRef); + this._activeRipples.push(rippleRef); } else { rippleRef.fadeOut(); } @@ -117,11 +117,11 @@ export class RippleRenderer { /** Fades out a ripple reference. */ fadeOutRipple(ripple: RippleRef) { let rippleEl = ripple.element; - let rippleIndex = this.activeRipples.indexOf(ripple); + let rippleIndex = this._activeRipples.indexOf(ripple); // Remove the ripple reference if added to the list of active ripples. if (rippleIndex !== -1) { - this.activeRipples.splice(rippleIndex, 1); + this._activeRipples.splice(rippleIndex, 1); } rippleEl.style.transitionDuration = `${RIPPLE_FADE_OUT_DURATION}ms`; @@ -133,6 +133,16 @@ export class RippleRenderer { }, RIPPLE_FADE_OUT_DURATION); } + /** Fades out all currently active ripples. */ + fadeOutAll() { + // Iterate in reverse, to avoid issues with the `fadeOut` method that will immediately remove + // items from the array. + let i = this._activeRipples.length; + while (i--) { + this._activeRipples[i].fadeOut(); + } + } + /** Sets the trigger element and registers the mouse events. */ setTriggerElement(element: HTMLElement) { // Remove all previously register event listeners from the trigger element. @@ -163,7 +173,7 @@ export class RippleRenderer { this._isMousedown = false; // On mouseup, fade-out all ripples that are active and not persistent. - this.activeRipples + this._activeRipples .filter(ripple => !ripple.config.persistent) .forEach(ripple => ripple.fadeOut()); } diff --git a/src/lib/core/ripple/ripple.ts b/src/lib/core/ripple/ripple.ts index 83f67ee34e7e..5218e7e75e94 100644 --- a/src/lib/core/ripple/ripple.ts +++ b/src/lib/core/ripple/ripple.ts @@ -90,12 +90,7 @@ export class MdRipple implements OnChanges, OnDestroy { /** Fades out all currently showing ripple elements. */ fadeOutAll() { - // Iterate in reverse, to avoid issues with the `fadeOut` method that will immediately remove - // items from the array. - let i = this._rippleRenderer.activeRipples.length; - while (i--) { - this._rippleRenderer.activeRipples[i].fadeOut(); - } + this._rippleRenderer.fadeOutAll(); } /** Ripple configuration from the directive's input values. */ From c4f033b9ba50897fdd463876864163a174271d6f Mon Sep 17 00:00:00 2001 From: Paul Gschwendtner Date: Mon, 27 Feb 2017 20:06:27 +0100 Subject: [PATCH 4/4] Use Set instead of Field --- src/lib/core/ripple/ripple-renderer.ts | 27 ++++++++++---------------- 1 file changed, 10 insertions(+), 17 deletions(-) diff --git a/src/lib/core/ripple/ripple-renderer.ts b/src/lib/core/ripple/ripple-renderer.ts index e79d5b98690e..6b31dbd2473d 100644 --- a/src/lib/core/ripple/ripple-renderer.ts +++ b/src/lib/core/ripple/ripple-renderer.ts @@ -37,8 +37,8 @@ export class RippleRenderer { /** Events to be registered on the trigger element. */ private _triggerEvents = new Map(); - /** Currently active ripple references. */ - private _activeRipples: RippleRef[] = []; + /** Set of currently active ripple references. */ + private _activeRipples = new Set(); /** Ripple config for all ripples created by events. */ rippleConfig: RippleConfig = {}; @@ -105,7 +105,7 @@ export class RippleRenderer { // Once it's faded in, the ripple can be hidden immediately if the mouse is released. this.runTimeoutOutsideZone(() => { if (config.persistent || this._isMousedown) { - this._activeRipples.push(rippleRef); + this._activeRipples.add(rippleRef); } else { rippleRef.fadeOut(); } @@ -117,12 +117,8 @@ export class RippleRenderer { /** Fades out a ripple reference. */ fadeOutRipple(ripple: RippleRef) { let rippleEl = ripple.element; - let rippleIndex = this._activeRipples.indexOf(ripple); - // Remove the ripple reference if added to the list of active ripples. - if (rippleIndex !== -1) { - this._activeRipples.splice(rippleIndex, 1); - } + this._activeRipples.delete(ripple); rippleEl.style.transitionDuration = `${RIPPLE_FADE_OUT_DURATION}ms`; rippleEl.style.opacity = '0'; @@ -135,12 +131,7 @@ export class RippleRenderer { /** Fades out all currently active ripples. */ fadeOutAll() { - // Iterate in reverse, to avoid issues with the `fadeOut` method that will immediately remove - // items from the array. - let i = this._activeRipples.length; - while (i--) { - this._activeRipples[i].fadeOut(); - } + this._activeRipples.forEach(ripple => ripple.fadeOut()); } /** Sets the trigger element and registers the mouse events. */ @@ -173,9 +164,11 @@ export class RippleRenderer { this._isMousedown = false; // On mouseup, fade-out all ripples that are active and not persistent. - this._activeRipples - .filter(ripple => !ripple.config.persistent) - .forEach(ripple => ripple.fadeOut()); + this._activeRipples.forEach(ripple => { + if (!ripple.config.persistent) { + ripple.fadeOut(); + } + }); } /** Listener being called on mouseleave event. */