Skip to content

Commit fbf2119

Browse files
committed
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
1 parent c203589 commit fbf2119

File tree

15 files changed

+159
-65
lines changed

15 files changed

+159
-65
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: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import {RippleConfig, RippleRenderer} from './ripple-renderer';
2+
3+
/**
4+
* Exposed 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+
fadeOut() {
15+
let rippleIndex = this.renderer.activeRipples.indexOf(this);
16+
17+
// Remove the ripple reference if added to the list of active ripples.
18+
if (rippleIndex !== -1) {
19+
this.renderer.activeRipples.splice(rippleIndex, 1);
20+
}
21+
22+
// Regardless of being added to the list, fade-out the ripple element.
23+
this.renderer.fadeOutRipple(this.element);
24+
}
25+
}

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

Lines changed: 41 additions & 27 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+
/** Currently active ripples. */
41+
activeRipples: 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,15 +94,24 @@ 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.push(rippleRef);
109+
} else {
110+
rippleRef.fadeOut()
111+
}
112112
}, duration);
113+
114+
return rippleRef;
113115
}
114116

115117
/** Fades out a ripple element. */
@@ -151,8 +153,11 @@ export class RippleRenderer {
151153
/** Listener being called on mouseup event. */
152154
private onMouseup() {
153155
this._isMousedown = false;
154-
this._activeRipples.forEach(ripple => this.fadeOutRipple(ripple));
155-
this._activeRipples = [];
156+
157+
// On mouseup, fade-out all ripples that are active and not persistent.
158+
this.activeRipples
159+
.filter(ripple => !ripple.config.persistent)
160+
.forEach(ripple => ripple.fadeOut());
156161
}
157162

158163
/** Listener being called on mouseleave event. */
@@ -167,13 +172,22 @@ export class RippleRenderer {
167172
this._ngZone.runOutsideAngular(() => setTimeout(fn, delay));
168173
}
169174

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-
}
175+
}
178176

177+
/** Enforces a style recalculation of a DOM element by computing its styles. */
178+
// TODO(devversion): Move into global utility function.
179+
function enforceStyleRecalculation(element: HTMLElement) {
180+
// Enforce a style recalculation by calling `getComputedStyle` and accessing any property.
181+
// Calling `getPropertyValue` is important to let optimizers know that this is not a noop.
182+
// See: https://gist.github.com/paulirish/5d52fb081b3570c81e3a
183+
window.getComputedStyle(element).getPropertyValue('opacity');
184+
}
185+
186+
/**
187+
* Returns the distance from the point (x, y) to the furthest corner of a rectangle.
188+
*/
189+
function distanceToFurthestCorner(x: number, y: number, rect: ClientRect) {
190+
const distX = Math.max(Math.abs(x - rect.left), Math.abs(x - rect.right));
191+
const distY = Math.max(Math.abs(y - rect.top), Math.abs(y - rect.bottom));
192+
return Math.sqrt(distX * distX + distY * distY);
179193
}

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

@@ -279,6 +279,39 @@ describe('MdRipple', () => {
279279

280280
});
281281

282+
describe('manual ripples', () => {
283+
let rippleDirective: MdRipple;
284+
285+
beforeEach(() => {
286+
fixture = TestBed.createComponent(BasicRippleContainer);
287+
fixture.detectChanges();
288+
289+
rippleTarget = fixture.nativeElement.querySelector('[mat-ripple]');
290+
rippleDirective = fixture.componentInstance.ripple;
291+
});
292+
293+
it('should allow persistent ripple elements', fakeAsync(() => {
294+
expect(rippleTarget.querySelectorAll('.mat-ripple-element').length).toBe(0);
295+
296+
let rippleRef = rippleDirective.launch(0, 0, { persistent: true });
297+
298+
expect(rippleTarget.querySelectorAll('.mat-ripple-element').length).toBe(1);
299+
300+
// Calculates the duration for fading-in and fading-out the ripple. Also adds some
301+
// extra time to demonstrate that the ripples are persistent.
302+
tick(RIPPLE_FADE_IN_DURATION + RIPPLE_FADE_OUT_DURATION + 5000);
303+
304+
expect(rippleTarget.querySelectorAll('.mat-ripple-element').length).toBe(1);
305+
306+
rippleRef.fadeOut();
307+
308+
tick(RIPPLE_FADE_OUT_DURATION);
309+
310+
expect(rippleTarget.querySelectorAll('.mat-ripple-element').length).toBe(0);
311+
}));
312+
313+
});
314+
282315
describe('configuring behavior', () => {
283316
let controller: RippleContainerWithInputBindings;
284317
let rippleComponent: MdRipple;

src/lib/core/ripple/ripple.ts

Lines changed: 14 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,18 @@ 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 active ripples. */
92+
fadeOutAll() {
93+
// Iterate in reverse, to avoid issues with the `fadeOut` method that will immediately remove
94+
// items from the array.
95+
let i = this._rippleRenderer.activeRipples.length;
96+
while (i--) {
97+
this._rippleRenderer.activeRipples[i].fadeOut();
98+
}
9299
}
93100

94101
/** Ripple configuration from the directive's input values. */
@@ -100,22 +107,4 @@ export class MdRipple implements OnChanges, OnDestroy {
100107
color: this.color
101108
};
102109
}
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-
}
121110
}

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

src/lib/tabs/tab-group.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import {MdTabNavBar, MdTabLink, MdTabLinkRipple} from './tab-nav-bar/tab-nav-bar
2222
import {MdInkBar} from './ink-bar';
2323
import {Observable} from 'rxjs/Observable';
2424
import 'rxjs/add/operator/map';
25-
import {MdRippleModule} from '../core/ripple/ripple';
25+
import {MdRippleModule} from '../core/ripple/index';
2626
import {ObserveContentModule} from '../core/observe-content/observe-content';
2727
import {MdTab} from './tab';
2828
import {MdTabBody} from './tab-body';

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import {async, ComponentFixture, TestBed} from '@angular/core/testing';
22
import {Component, ViewChild, ViewContainerRef} from '@angular/core';
33
import {LayoutDirection, Dir} from '../core/rtl/dir';
44
import {MdTabHeader} from './tab-header';
5-
import {MdRippleModule} from '../core/ripple/ripple';
5+
import {MdRippleModule} from '../core/ripple/index';
66
import {CommonModule} from '@angular/common';
77
import {PortalModule} from '../core';
88
import {MdInkBar} from './ink-bar';

0 commit comments

Comments
 (0)