Skip to content

feat(ripple): add option for persistent ripples #3315

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Mar 1, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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: []
};
}
}
18 changes: 18 additions & 0 deletions src/lib/core/ripple/ripple-ref.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import {RippleConfig, RippleRenderer} from './ripple-renderer';

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

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

/** Fades out the ripple element. */
fadeOut() {
this._renderer.fadeOutRipple(this);
}
}
89 changes: 57 additions & 32 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>();

/** Set of currently active ripple references. */
private _activeRipples = new Set<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,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.
Expand Down Expand Up @@ -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. */
Expand All @@ -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);
}
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';
import {dispatchMouseEvent} from '../testing/dispatch-events';
Expand Down Expand Up @@ -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;
Expand Down
34 changes: 9 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,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. */
Expand All @@ -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: []
};
}
}
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
Loading