Skip to content

Commit

Permalink
feat(ripple): add option for persistent ripples
Browse files Browse the repository at this point in the history
* 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 angular#3169
  • Loading branch information
devversion committed Feb 26, 2017
1 parent c203589 commit fbf2119
Show file tree
Hide file tree
Showing 15 changed files with 159 additions and 65 deletions.
4 changes: 3 additions & 1 deletion src/demo-app/ripple/ripple-demo.html
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,9 @@
</md-input-container>
</section>
<section>
<button md-raised-button (click)="doManualRipple()">Manual ripple</button>
<button md-raised-button (click)="launchRipple()">Launch Ripple</button>
<button md-raised-button (click)="launchRipple(true)">Launch Ripple (Persistent)</button>
<button md-raised-button (click)="fadeOutAll()">Fade Out All</button>
</section>
<section>
<div class="demo-ripple-container"
Expand Down
10 changes: 8 additions & 2 deletions src/demo-app/ripple/ripple-demo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,15 @@ export class RippleDemo {

disableButtonRipples = false;

doManualRipple() {
launchRipple(persistent = false) {
if (this.ripple) {
this.ripple.launch(0, 0, { centered: true });
this.ripple.launch(0, 0, { centered: true, persistent });
}
}

fadeOutAll() {
if (this.ripple) {
this.ripple.fadeOutAll();
}
}

Expand Down
4 changes: 2 additions & 2 deletions src/lib/core/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@ import {MdLineModule} from './line/line';
import {RtlModule} from './rtl/dir';
import {ObserveContentModule} from './observe-content/observe-content';
import {MdOptionModule} from './option/option';
import {MdRippleModule} from './ripple/ripple';
import {PortalModule} from './portal/portal-directives';
import {OverlayModule} from './overlay/overlay-directives';
import {A11yModule} from './a11y/index';
import {MdSelectionModule} from './selection/index';
import {MdRippleModule} from './ripple/index';


// RTL
Expand Down Expand Up @@ -64,7 +64,7 @@ export {GestureConfig} from './gestures/gesture-config';
export {HammerInput, HammerManager} from './gestures/gesture-annotations';

// Ripple
export {MdRipple, MdRippleModule} from './ripple/ripple';
export * from './ripple/index';

// a11y
export {
Expand Down
2 changes: 1 addition & 1 deletion src/lib/core/option/option.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {
import {CommonModule} from '@angular/common';
import {ENTER, SPACE} from '../keyboard/keycodes';
import {coerceBooleanProperty} from '../coercion/boolean-property';
import {MdRippleModule} from '../ripple/ripple';
import {MdRippleModule} from '../ripple/index';

/**
* Option IDs need to be unique across components, so this counter exists outside of
Expand Down
25 changes: 25 additions & 0 deletions src/lib/core/ripple/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import {ModuleWithProviders, NgModule} from '@angular/core';
import {MdRipple} from './ripple';
import {CompatibilityModule} from '../compatibility/compatibility';
import {VIEWPORT_RULER_PROVIDER} from '../overlay/position/viewport-ruler';
import {SCROLL_DISPATCHER_PROVIDER} from '../overlay/scroll/scroll-dispatcher';

export {MdRipple} from './ripple';
export {RippleRef} from './ripple-ref';
export {RippleConfig} from './ripple-renderer';

@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: []
};
}
}
25 changes: 25 additions & 0 deletions src/lib/core/ripple/ripple-ref.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import {RippleConfig, RippleRenderer} from './ripple-renderer';

/**
* Exposed reference to a previously launched ripple element.
*/
export class RippleRef {

constructor(
private renderer: RippleRenderer,
public element: HTMLElement,
public config: RippleConfig) {
}

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);
}
}
68 changes: 41 additions & 27 deletions src/lib/core/ripple/ripple-renderer.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,19 @@
import {ElementRef, NgZone} from '@angular/core';
import {ViewportRuler} from '../overlay/position/viewport-ruler';
import {RippleRef} from './ripple-ref';

/** Fade-in duration for the ripples. Can be modified with the speedFactor option. */
export const RIPPLE_FADE_IN_DURATION = 450;

/** Fade-out duration for the ripples in milliseconds. This can't be modified by the speedFactor. */
export const RIPPLE_FADE_OUT_DURATION = 400;

/**
* Returns the distance from the point (x, y) to the furthest corner of a rectangle.
*/
const 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);
};

export type RippleConfig = {
color?: string;
centered?: boolean;
radius?: number;
speedFactor?: number;
persistent?: boolean;
};

/**
Expand All @@ -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<string, any>();

/** Currently active ripples. */
activeRipples: RippleRef[] = [];

/** Ripple config for all ripples created by events. */
rippleConfig: RippleConfig = {};

Expand All @@ -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) {
Expand Down Expand Up @@ -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. */
Expand Down Expand Up @@ -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. */
Expand All @@ -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);
}
35 changes: 34 additions & 1 deletion src/lib/core/ripple/ripple.spec.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand Down Expand Up @@ -279,6 +279,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;
Expand Down
39 changes: 14 additions & 25 deletions src/lib/core/ripple/ripple.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
import {
NgModule,
ModuleWithProviders,
Directive,
ElementRef,
Input,
Expand All @@ -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({
Expand Down Expand Up @@ -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 active ripples. */
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. */
Expand All @@ -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: []
};
}
}
2 changes: 1 addition & 1 deletion src/lib/menu/menu.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
2 changes: 1 addition & 1 deletion src/lib/slide-toggle/slide-toggle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down
2 changes: 1 addition & 1 deletion src/lib/tabs/tab-body.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down
2 changes: 1 addition & 1 deletion src/lib/tabs/tab-group.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
2 changes: 1 addition & 1 deletion src/lib/tabs/tab-header.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
Loading

0 comments on commit fbf2119

Please sign in to comment.