diff --git a/src/lib/core/ripple/ripple-renderer.ts b/src/lib/core/ripple/ripple-renderer.ts index 8e379bb0a663..9ebd7f582907 100644 --- a/src/lib/core/ripple/ripple-renderer.ts +++ b/src/lib/core/ripple/ripple-renderer.ts @@ -1,5 +1,6 @@ import { ElementRef, + NgZone, } from '@angular/core'; /** TODO: internal */ @@ -44,7 +45,9 @@ export class RippleRenderer { private _triggerElement: HTMLElement; _opacity: string; - constructor(_elementRef: ElementRef, private _eventHandlers: Map void>) { + constructor(_elementRef: ElementRef, + private _eventHandlers: Map void>, + private _ngZone: NgZone) { this._rippleElement = _elementRef.nativeElement; // The background div is created in createBackgroundIfNeeded when the ripple becomes enabled. // This avoids creating unneeded divs when the ripple is always disabled. @@ -143,6 +146,16 @@ export class RippleRenderer { rippleDiv.addEventListener('transitionend', (event: TransitionEvent) => transitionEndCallback(ripple, event)); + // Ensure that ripples are always removed, even when transitionend doesn't fire. + // Run this outside the Angular zone because there's nothing that Angular cares about. + // If it were to run inside the Angular zone, every test that used ripples would have to be + // either async or fakeAsync. + this._ngZone.runOutsideAngular(() => { + // The ripple lasts a time equal to the sum of fade-in, transform, + // and fade-out (3 * fade-in time). + let rippleDuration = fadeInSeconds * 3 * 1000; + setTimeout(() => this.removeRippleFromDom(ripple.rippleElement), rippleDuration); + }); } /** Fades out a foreground ripple after it has fully expanded and faded in. */ @@ -153,7 +166,9 @@ export class RippleRenderer { /** Removes a foreground ripple from the DOM after it has faded out. */ removeRippleFromDom(ripple: Element) { - ripple.parentElement.removeChild(ripple); + if (ripple && ripple.parentElement) { + ripple.parentElement.removeChild(ripple); + } } /** Fades in the ripple background. */ diff --git a/src/lib/core/ripple/ripple.spec.ts b/src/lib/core/ripple/ripple.spec.ts index 9c045860fd17..ac30c0fed082 100644 --- a/src/lib/core/ripple/ripple.spec.ts +++ b/src/lib/core/ripple/ripple.spec.ts @@ -1,4 +1,4 @@ -import {TestBed, ComponentFixture} from '@angular/core/testing'; +import {TestBed, ComponentFixture, async} from '@angular/core/testing'; import {Component, ViewChild} from '@angular/core'; import {MdRipple, MdRippleModule} from './ripple'; @@ -132,6 +132,16 @@ describe('MdRipple', () => { expect(rippleElement.querySelectorAll('.md-ripple-foreground').length).toBe(0); }); + it('removes foreground ripples after timeout', async(() => { + rippleElement.click(); + expect(rippleElement.querySelectorAll('.md-ripple-foreground').length).toBe(1); + + // Use a real timeout because the ripple's timeout runs outside of the angular zone. + setTimeout(() => { + expect(rippleElement.querySelectorAll('.md-ripple-foreground').length).toBe(0); + }, 1600); + })); + it('creates ripples when manually triggered', () => { const rippleComponent = fixture.debugElement.componentInstance.ripple; // start() should show the background, but no foreground ripple yet. diff --git a/src/lib/core/ripple/ripple.ts b/src/lib/core/ripple/ripple.ts index 3849a67b18df..ab3b95e29109 100644 --- a/src/lib/core/ripple/ripple.ts +++ b/src/lib/core/ripple/ripple.ts @@ -5,6 +5,7 @@ import { ElementRef, HostBinding, Input, + NgZone, OnChanges, OnDestroy, OnInit, @@ -62,13 +63,13 @@ export class MdRipple implements OnInit, OnDestroy, OnChanges { private _rippleRenderer: RippleRenderer; - constructor(_elementRef: ElementRef) { + constructor(_elementRef: ElementRef, _ngZone: NgZone) { // These event handlers are attached to the element that triggers the ripple animations. const eventHandlers = new Map void>(); eventHandlers.set('mousedown', (event: MouseEvent) => this._mouseDown(event)); eventHandlers.set('click', (event: MouseEvent) => this._click(event)); eventHandlers.set('mouseleave', (event: MouseEvent) => this._mouseLeave(event)); - this._rippleRenderer = new RippleRenderer(_elementRef, eventHandlers); + this._rippleRenderer = new RippleRenderer(_elementRef, eventHandlers, _ngZone); } /** TODO: internal */ 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 9fcc8393c9c1..92284074b8b2 100644 --- a/src/lib/tabs/tab-nav-bar/tab-nav-bar.ts +++ b/src/lib/tabs/tab-nav-bar/tab-nav-bar.ts @@ -5,9 +5,9 @@ import { ElementRef, ViewEncapsulation, Directive, + NgZone, OnDestroy, } from '@angular/core'; - import {MdInkBar} from '../ink-bar'; import {MdRipple} from '../../core/ripple/ripple'; @@ -60,8 +60,8 @@ export class MdTabLink { selector: '[md-tab-link], [mat-tab-link]', }) export class MdTabLinkRipple extends MdRipple implements OnDestroy { - constructor(private _element: ElementRef) { - super(_element); + constructor(private _element: ElementRef, private _ngZone: NgZone) { + super(_element, _ngZone); } // In certain cases the parent destroy handler