Skip to content

Commit e3ba1e1

Browse files
devversionkara
authored andcommitted
feat(ripple): add option for persistent ripples (#3315)
Closes #3169
1 parent beb0edf commit e3ba1e1

File tree

15 files changed

+163
-70
lines changed

15 files changed

+163
-70
lines changed

src/demo-app/ripple/ripple-demo.html

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,9 @@
3535
</md-input-container>
3636
</section>
3737
<section>
38-
<button md-raised-button (click)="doManualRipple()">Manual ripple</button>
38+
<button md-raised-button (click)="launchRipple()">Launch Ripple</button>
39+
<button md-raised-button (click)="launchRipple(true)">Launch Ripple (Persistent)</button>
40+
<button md-raised-button (click)="fadeOutAll()">Fade Out All</button>
3941
</section>
4042
<section>
4143
<div class="demo-ripple-container"

src/demo-app/ripple/ripple-demo.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,15 @@ export class RippleDemo {
2121

2222
disableButtonRipples = false;
2323

24-
doManualRipple() {
24+
launchRipple(persistent = false) {
2525
if (this.ripple) {
26-
this.ripple.launch(0, 0, { centered: true });
26+
this.ripple.launch(0, 0, { centered: true, persistent });
27+
}
28+
}
29+
30+
fadeOutAll() {
31+
if (this.ripple) {
32+
this.ripple.fadeOutAll();
2733
}
2834
}
2935

src/lib/core/core.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,11 @@ import {MdLineModule} from './line/line';
33
import {RtlModule} from './rtl/dir';
44
import {ObserveContentModule} from './observe-content/observe-content';
55
import {MdOptionModule} from './option/option';
6-
import {MdRippleModule} from './ripple/ripple';
76
import {PortalModule} from './portal/portal-directives';
87
import {OverlayModule} from './overlay/overlay-directives';
98
import {A11yModule} from './a11y/index';
109
import {MdSelectionModule} from './selection/index';
10+
import {MdRippleModule} from './ripple/index';
1111

1212

1313
// RTL
@@ -64,7 +64,7 @@ export {GestureConfig} from './gestures/gesture-config';
6464
export {HammerInput, HammerManager} from './gestures/gesture-annotations';
6565

6666
// Ripple
67-
export {MdRipple, MdRippleModule} from './ripple/ripple';
67+
export * from './ripple/index';
6868

6969
// a11y
7070
export {

src/lib/core/option/option.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import {
1212
import {CommonModule} from '@angular/common';
1313
import {ENTER, SPACE} from '../keyboard/keycodes';
1414
import {coerceBooleanProperty} from '../coercion/boolean-property';
15-
import {MdRippleModule} from '../ripple/ripple';
15+
import {MdRippleModule} from '../ripple/index';
1616

1717
/**
1818
* Option IDs need to be unique across components, so this counter exists outside of

src/lib/core/ripple/index.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import {ModuleWithProviders, NgModule} from '@angular/core';
2+
import {MdRipple} from './ripple';
3+
import {CompatibilityModule} from '../compatibility/compatibility';
4+
import {VIEWPORT_RULER_PROVIDER} from '../overlay/position/viewport-ruler';
5+
import {SCROLL_DISPATCHER_PROVIDER} from '../overlay/scroll/scroll-dispatcher';
6+
7+
export {MdRipple} from './ripple';
8+
export {RippleRef} from './ripple-ref';
9+
export {RippleConfig} from './ripple-renderer';
10+
11+
@NgModule({
12+
imports: [CompatibilityModule],
13+
exports: [MdRipple, CompatibilityModule],
14+
declarations: [MdRipple],
15+
providers: [VIEWPORT_RULER_PROVIDER, SCROLL_DISPATCHER_PROVIDER],
16+
})
17+
export class MdRippleModule {
18+
/** @deprecated */
19+
static forRoot(): ModuleWithProviders {
20+
return {
21+
ngModule: MdRippleModule,
22+
providers: []
23+
};
24+
}
25+
}

src/lib/core/ripple/ripple-ref.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import {RippleConfig, RippleRenderer} from './ripple-renderer';
2+
3+
/**
4+
* Reference to a previously launched ripple element.
5+
*/
6+
export class RippleRef {
7+
8+
constructor(
9+
private _renderer: RippleRenderer,
10+
public element: HTMLElement,
11+
public config: RippleConfig) {
12+
}
13+
14+
/** Fades out the ripple element. */
15+
fadeOut() {
16+
this._renderer.fadeOutRipple(this);
17+
}
18+
}

src/lib/core/ripple/ripple-renderer.ts

Lines changed: 57 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,19 @@
11
import {ElementRef, NgZone} from '@angular/core';
22
import {ViewportRuler} from '../overlay/position/viewport-ruler';
3+
import {RippleRef} from './ripple-ref';
34

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

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

10-
/**
11-
* Returns the distance from the point (x, y) to the furthest corner of a rectangle.
12-
*/
13-
const distanceToFurthestCorner = (x: number, y: number, rect: ClientRect) => {
14-
const distX = Math.max(Math.abs(x - rect.left), Math.abs(x - rect.right));
15-
const distY = Math.max(Math.abs(y - rect.top), Math.abs(y - rect.bottom));
16-
return Math.sqrt(distX * distX + distY * distY);
17-
};
18-
1911
export type RippleConfig = {
2012
color?: string;
2113
centered?: boolean;
2214
radius?: number;
2315
speedFactor?: number;
16+
persistent?: boolean;
2417
};
2518

2619
/**
@@ -41,12 +34,12 @@ export class RippleRenderer {
4134
/** Whether the mouse is currently down or not. */
4235
private _isMousedown: boolean = false;
4336

44-
/** Currently active ripples that will be closed on mouseup. */
45-
private _activeRipples: HTMLElement[] = [];
46-
4737
/** Events to be registered on the trigger element. */
4838
private _triggerEvents = new Map<string, any>();
4939

40+
/** Set of currently active ripple references. */
41+
private _activeRipples = new Set<RippleRef>();
42+
5043
/** Ripple config for all ripples created by events. */
5144
rippleConfig: RippleConfig = {};
5245

@@ -66,7 +59,7 @@ export class RippleRenderer {
6659
}
6760

6861
/** Fades in a ripple at the given coordinates. */
69-
fadeInRipple(pageX: number, pageY: number, config: RippleConfig = {}) {
62+
fadeInRipple(pageX: number, pageY: number, config: RippleConfig = {}): RippleRef {
7063
let containerRect = this._containerElement.getBoundingClientRect();
7164

7265
if (config.centered) {
@@ -101,28 +94,46 @@ export class RippleRenderer {
10194

10295
// By default the browser does not recalculate the styles of dynamically created
10396
// ripple elements. This is critical because then the `scale` would not animate properly.
104-
this._enforceStyleRecalculation(ripple);
97+
enforceStyleRecalculation(ripple);
10598

10699
ripple.style.transform = 'scale(1)';
107100

108-
// Wait for the ripple to be faded in. Once it's faded in, the ripple can be hidden immediately
109-
// if the mouse is released.
101+
// Exposed reference to the ripple that will be returned.
102+
let rippleRef = new RippleRef(this, ripple, config);
103+
104+
// Wait for the ripple element to be completely faded in.
105+
// Once it's faded in, the ripple can be hidden immediately if the mouse is released.
110106
this.runTimeoutOutsideZone(() => {
111-
this._isMousedown ? this._activeRipples.push(ripple) : this.fadeOutRipple(ripple);
107+
if (config.persistent || this._isMousedown) {
108+
this._activeRipples.add(rippleRef);
109+
} else {
110+
rippleRef.fadeOut();
111+
}
112112
}, duration);
113+
114+
return rippleRef;
113115
}
114116

115-
/** Fades out a ripple element. */
116-
fadeOutRipple(ripple: HTMLElement) {
117-
ripple.style.transitionDuration = `${RIPPLE_FADE_OUT_DURATION}ms`;
118-
ripple.style.opacity = '0';
117+
/** Fades out a ripple reference. */
118+
fadeOutRipple(ripple: RippleRef) {
119+
let rippleEl = ripple.element;
120+
121+
this._activeRipples.delete(ripple);
122+
123+
rippleEl.style.transitionDuration = `${RIPPLE_FADE_OUT_DURATION}ms`;
124+
rippleEl.style.opacity = '0';
119125

120126
// Once the ripple faded out, the ripple can be safely removed from the DOM.
121127
this.runTimeoutOutsideZone(() => {
122-
ripple.parentNode.removeChild(ripple);
128+
rippleEl.parentNode.removeChild(rippleEl);
123129
}, RIPPLE_FADE_OUT_DURATION);
124130
}
125131

132+
/** Fades out all currently active ripples. */
133+
fadeOutAll() {
134+
this._activeRipples.forEach(ripple => ripple.fadeOut());
135+
}
136+
126137
/** Sets the trigger element and registers the mouse events. */
127138
setTriggerElement(element: HTMLElement) {
128139
// Remove all previously register event listeners from the trigger element.
@@ -151,8 +162,13 @@ export class RippleRenderer {
151162
/** Listener being called on mouseup event. */
152163
private onMouseup() {
153164
this._isMousedown = false;
154-
this._activeRipples.forEach(ripple => this.fadeOutRipple(ripple));
155-
this._activeRipples = [];
165+
166+
// On mouseup, fade-out all ripples that are active and not persistent.
167+
this._activeRipples.forEach(ripple => {
168+
if (!ripple.config.persistent) {
169+
ripple.fadeOut();
170+
}
171+
});
156172
}
157173

158174
/** Listener being called on mouseleave event. */
@@ -167,13 +183,22 @@ export class RippleRenderer {
167183
this._ngZone.runOutsideAngular(() => setTimeout(fn, delay));
168184
}
169185

170-
/** Enforces a style recalculation of a DOM element by computing its styles. */
171-
// TODO(devversion): Move into global utility function.
172-
private _enforceStyleRecalculation(element: HTMLElement) {
173-
// Enforce a style recalculation by calling `getComputedStyle` and accessing any property.
174-
// Calling `getPropertyValue` is important to let optimizers know that this is not a noop.
175-
// See: https://gist.github.com/paulirish/5d52fb081b3570c81e3a
176-
window.getComputedStyle(element).getPropertyValue('opacity');
177-
}
186+
}
187+
188+
/** Enforces a style recalculation of a DOM element by computing its styles. */
189+
// TODO(devversion): Move into global utility function.
190+
function enforceStyleRecalculation(element: HTMLElement) {
191+
// Enforce a style recalculation by calling `getComputedStyle` and accessing any property.
192+
// Calling `getPropertyValue` is important to let optimizers know that this is not a noop.
193+
// See: https://gist.github.com/paulirish/5d52fb081b3570c81e3a
194+
window.getComputedStyle(element).getPropertyValue('opacity');
195+
}
178196

197+
/**
198+
* Returns the distance from the point (x, y) to the furthest corner of a rectangle.
199+
*/
200+
function distanceToFurthestCorner(x: number, y: number, rect: ClientRect) {
201+
const distX = Math.max(Math.abs(x - rect.left), Math.abs(x - rect.right));
202+
const distY = Math.max(Math.abs(y - rect.top), Math.abs(y - rect.bottom));
203+
return Math.sqrt(distX * distX + distY * distY);
179204
}

src/lib/core/ripple/ripple.spec.ts

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import {TestBed, ComponentFixture, fakeAsync, tick, inject} from '@angular/core/testing';
22
import {Component, ViewChild} from '@angular/core';
3-
import {MdRipple, MdRippleModule} from './ripple';
3+
import {MdRipple, MdRippleModule} from './index';
44
import {ViewportRuler} from '../overlay/position/viewport-ruler';
55
import {RIPPLE_FADE_OUT_DURATION, RIPPLE_FADE_IN_DURATION} from './ripple-renderer';
66
import {dispatchMouseEvent} from '../testing/dispatch-events';
@@ -239,6 +239,39 @@ describe('MdRipple', () => {
239239

240240
});
241241

242+
describe('manual ripples', () => {
243+
let rippleDirective: MdRipple;
244+
245+
beforeEach(() => {
246+
fixture = TestBed.createComponent(BasicRippleContainer);
247+
fixture.detectChanges();
248+
249+
rippleTarget = fixture.nativeElement.querySelector('[mat-ripple]');
250+
rippleDirective = fixture.componentInstance.ripple;
251+
});
252+
253+
it('should allow persistent ripple elements', fakeAsync(() => {
254+
expect(rippleTarget.querySelectorAll('.mat-ripple-element').length).toBe(0);
255+
256+
let rippleRef = rippleDirective.launch(0, 0, { persistent: true });
257+
258+
expect(rippleTarget.querySelectorAll('.mat-ripple-element').length).toBe(1);
259+
260+
// Calculates the duration for fading-in and fading-out the ripple. Also adds some
261+
// extra time to demonstrate that the ripples are persistent.
262+
tick(RIPPLE_FADE_IN_DURATION + RIPPLE_FADE_OUT_DURATION + 5000);
263+
264+
expect(rippleTarget.querySelectorAll('.mat-ripple-element').length).toBe(1);
265+
266+
rippleRef.fadeOut();
267+
268+
tick(RIPPLE_FADE_OUT_DURATION);
269+
270+
expect(rippleTarget.querySelectorAll('.mat-ripple-element').length).toBe(0);
271+
}));
272+
273+
});
274+
242275
describe('configuring behavior', () => {
243276
let controller: RippleContainerWithInputBindings;
244277
let rippleComponent: MdRipple;

src/lib/core/ripple/ripple.ts

Lines changed: 9 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
11
import {
2-
NgModule,
3-
ModuleWithProviders,
42
Directive,
53
ElementRef,
64
Input,
@@ -10,9 +8,8 @@ import {
108
OnDestroy,
119
} from '@angular/core';
1210
import {RippleConfig, RippleRenderer} from './ripple-renderer';
13-
import {CompatibilityModule} from '../compatibility/compatibility';
14-
import {ViewportRuler, VIEWPORT_RULER_PROVIDER} from '../overlay/position/viewport-ruler';
15-
import {SCROLL_DISPATCHER_PROVIDER} from '../overlay/scroll/scroll-dispatcher';
11+
import {ViewportRuler} from '../overlay/position/viewport-ruler';
12+
import {RippleRef} from './ripple-ref';
1613

1714

1815
@Directive({
@@ -87,8 +84,13 @@ export class MdRipple implements OnChanges, OnDestroy {
8784
}
8885

8986
/** Launches a manual ripple at the specified position. */
90-
launch(pageX: number, pageY: number, config = this.rippleConfig) {
91-
this._rippleRenderer.fadeInRipple(pageX, pageY, config);
87+
launch(pageX: number, pageY: number, config = this.rippleConfig): RippleRef {
88+
return this._rippleRenderer.fadeInRipple(pageX, pageY, config);
89+
}
90+
91+
/** Fades out all currently showing ripple elements. */
92+
fadeOutAll() {
93+
this._rippleRenderer.fadeOutAll();
9294
}
9395

9496
/** Ripple configuration from the directive's input values. */
@@ -100,22 +102,4 @@ export class MdRipple implements OnChanges, OnDestroy {
100102
color: this.color
101103
};
102104
}
103-
104-
}
105-
106-
107-
@NgModule({
108-
imports: [CompatibilityModule],
109-
exports: [MdRipple, CompatibilityModule],
110-
declarations: [MdRipple],
111-
providers: [VIEWPORT_RULER_PROVIDER, SCROLL_DISPATCHER_PROVIDER],
112-
})
113-
export class MdRippleModule {
114-
/** @deprecated */
115-
static forRoot(): ModuleWithProviders {
116-
return {
117-
ngModule: MdRippleModule,
118-
providers: []
119-
};
120-
}
121105
}

src/lib/menu/menu.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import {OverlayModule, CompatibilityModule} from '../core';
44
import {MdMenu} from './menu-directive';
55
import {MdMenuItem} from './menu-item';
66
import {MdMenuTrigger} from './menu-trigger';
7-
import {MdRippleModule} from '../core/ripple/ripple';
7+
import {MdRippleModule} from '../core/ripple/index';
88
export {MdMenu} from './menu-directive';
99
export {MdMenuItem} from './menu-item';
1010
export {MdMenuTrigger} from './menu-trigger';

src/lib/slide-toggle/slide-toggle.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import {
2323
CompatibilityModule,
2424
} from '../core';
2525
import {Observable} from 'rxjs/Observable';
26-
import {MdRippleModule} from '../core/ripple/ripple';
26+
import {MdRippleModule} from '../core/ripple/index';
2727

2828

2929
export const MD_SLIDE_TOGGLE_VALUE_ACCESSOR: any = {

src/lib/tabs/tab-body.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import {Component, ViewChild, TemplateRef, ViewContainerRef} from '@angular/core
33
import {LayoutDirection, Dir} from '../core/rtl/dir';
44
import {TemplatePortal} from '../core/portal/portal';
55
import {MdTabBody} from './tab-body';
6-
import {MdRippleModule} from '../core/ripple/ripple';
6+
import {MdRippleModule} from '../core/ripple/index';
77
import {CommonModule} from '@angular/common';
88
import {PortalModule} from '../core';
99

0 commit comments

Comments
 (0)