From 6381948f88140a551c92417c1b3e68fa5763d82b Mon Sep 17 00:00:00 2001 From: Paul Gschwendtner Date: Fri, 10 Feb 2017 01:11:35 +0100 Subject: [PATCH] fix(ripple): make ripples conform with specs (#2859) * fix(ripple): make ripples conform with specs This makes the ripple service conform with the specifications and other Material reference implementations. See https://material.io/guidelines/motion/material-motion.html#material-motion-how-does-material-move This means the following: * Ripples now trigger on `mousedown` * Ripples now persists as long as the element is being hold. * No longer adds an unnecessary background ripple. * Removes the ugly `scale(0.00001)` for ripple animations Not only visually the ripple has been changed. The whole renderer has been rewritten and now has a very simple API, that can be easily used by developers. References #1434 * Fix linting and IE11 unsupported error * Ensure style recalculation * Address comments * Address feedback * Document that fade-out duration can't be modified through the speedFactor --- src/demo-app/ripple/ripple-demo.html | 15 +- src/demo-app/ripple/ripple-demo.ts | 11 +- src/lib/button/button.html | 4 +- src/lib/checkbox/_checkbox-theme.scss | 6 +- src/lib/checkbox/checkbox.html | 3 +- src/lib/core/option/option.html | 4 +- src/lib/core/ripple/README.md | 4 +- src/lib/core/ripple/_ripple.scss | 54 +-- src/lib/core/ripple/ripple-renderer.ts | 301 +++++++-------- src/lib/core/ripple/ripple.spec.ts | 384 ++++++++----------- src/lib/core/ripple/ripple.ts | 197 ++-------- src/lib/menu/menu-item.html | 3 +- src/lib/radio/_radio-theme.scss | 2 +- src/lib/radio/radio.html | 7 +- src/lib/radio/radio.ts | 4 - src/lib/tabs/tab-nav-bar/tab-nav-bar.spec.ts | 4 +- src/lib/tabs/tab-nav-bar/tab-nav-bar.ts | 13 +- 17 files changed, 357 insertions(+), 659 deletions(-) diff --git a/src/demo-app/ripple/ripple-demo.html b/src/demo-app/ripple/ripple-demo.html index 5c9cf2bd8004..71721acd91aa 100644 --- a/src/demo-app/ripple/ripple-demo.html +++ b/src/demo-app/ripple/ripple-demo.html @@ -21,24 +21,18 @@
Speed - Slow + Slow Normal - Fast + Fast
- + - - -
@@ -51,9 +45,8 @@ [mdRippleCentered]="centered" [mdRippleDisabled]="disabled" [mdRippleUnbounded]="unbounded" - [mdRippleMaxRadius]="maxRadius" + [mdRippleRadius]="radius" [mdRippleColor]="rippleColor" - [mdRippleBackgroundColor]="rippleBackgroundColor" [mdRippleSpeedFactor]="rippleSpeed"> Click me diff --git a/src/demo-app/ripple/ripple-demo.ts b/src/demo-app/ripple/ripple-demo.ts index c1fc3675b423..2f1d43bec680 100644 --- a/src/demo-app/ripple/ripple-demo.ts +++ b/src/demo-app/ripple/ripple-demo.ts @@ -9,23 +9,22 @@ import {MdRipple} from '@angular/material'; styleUrls: ['ripple-demo.css'], }) export class RippleDemo { - @ViewChild(MdRipple) manualRipple: MdRipple; + @ViewChild(MdRipple) ripple: MdRipple; centered = false; disabled = false; unbounded = false; rounded = false; - maxRadius: number = null; + radius: number = null; rippleSpeed = 1; rippleColor = ''; - rippleBackgroundColor = ''; disableButtonRipples = false; doManualRipple() { - if (this.manualRipple) { - window.setTimeout(() => this.manualRipple.start(), 10); - window.setTimeout(() => this.manualRipple.end(0, 0), 500); + if (this.ripple) { + this.ripple.launch(0, 0, { centered: true }); } } + } diff --git a/src/lib/button/button.html b/src/lib/button/button.html index f7c4e67fbaad..d13ade40bef4 100644 --- a/src/lib/button/button.html +++ b/src/lib/button/button.html @@ -1,8 +1,6 @@
+ [mdRippleTrigger]="_getHostElement()">
diff --git a/src/lib/checkbox/_checkbox-theme.scss b/src/lib/checkbox/_checkbox-theme.scss index cf8140137200..cf025e1a0b37 100644 --- a/src/lib/checkbox/_checkbox-theme.scss +++ b/src/lib/checkbox/_checkbox-theme.scss @@ -69,15 +69,15 @@ } .mat-checkbox:not(.mat-checkbox-disabled) { - &.mat-primary .mat-checkbox-ripple .mat-ripple-foreground { + &.mat-primary .mat-checkbox-ripple .mat-ripple-element { background-color: mat-color($primary, 0.26); } - &.mat-accent .mat-checkbox-ripple .mat-ripple-foreground { + &.mat-accent .mat-checkbox-ripple .mat-ripple-element { background-color: mat-color($accent, 0.26); } - &.mat-warn .mat-checkbox-ripple .mat-ripple-foreground { + &.mat-warn .mat-checkbox-ripple .mat-ripple-element { background-color: mat-color($warn, 0.26); } } diff --git a/src/lib/checkbox/checkbox.html b/src/lib/checkbox/checkbox.html index 723c3eef5e55..131bc4236544 100644 --- a/src/lib/checkbox/checkbox.html +++ b/src/lib/checkbox/checkbox.html @@ -18,8 +18,7 @@
+ [mdRippleSpeedFactor]="0.3">
-
+
+
diff --git a/src/lib/core/ripple/README.md b/src/lib/core/ripple/README.md index 1f964a9b252b..4247f3cfb074 100644 --- a/src/lib/core/ripple/README.md +++ b/src/lib/core/ripple/README.md @@ -19,9 +19,7 @@ Properties: | --- | --- | --- | | `mdRippleTrigger` | Element | The DOM element that triggers the ripple when clicked. Defaults to the parent of the `md-ripple`. | `mdRippleColor` | string | Custom color for foreground ripples -| `mdRippleBackgroundColor` | string | Custom color for the ripple background | `mdRippleCentered` | boolean | If true, the ripple animation originates from the center of the `md-ripple` bounds rather than from the location of the click event. -| `mdRippleMaxRadius` | number | Optional fixed radius of foreground ripples when fully expanded. Mainly used in conjunction with `unbounded` attribute. If not set, ripples will expand from their origin to the most distant corner of the component's bounding rectangle. +| `mdRippleRadius` | number | Optional fixed radius of foreground ripples when fully expanded. Mainly used in conjunction with `unbounded` attribute. If not set, ripples will expand from their origin to the most distant corner of the component's bounding rectangle. | `mdRippleUnbounded` | boolean | If true, foreground ripples will be visible outside the component's bounds. -| `mdRippleFocused` | boolean | If true, the background ripple is shown using the current theme's accent color to indicate focus. | `mdRippleDisabled` | boolean | If true, click events on the trigger element will not activate ripples. The `start` and `end` methods can still be called to programmatically create ripples. diff --git a/src/lib/core/ripple/_ripple.scss b/src/lib/core/ripple/_ripple.scss index 0ca3bb9103bb..cbfb5d6eb8f8 100644 --- a/src/lib/core/ripple/_ripple.scss +++ b/src/lib/core/ripple/_ripple.scss @@ -1,12 +1,6 @@ @import '../theming/theming'; - -$mat-ripple-focused-opacity: 0.1; -$mat-ripple-background-fade-duration: 300ms; -$mat-ripple-background-default-color: rgba(0, 0, 0, 0.0588); -$mat-ripple-foreground-initial-opacity: 0.25; -$mat-ripple-foreground-default-color: rgba(0, 0, 0, 0.0588); - +$mat-ripple-element-color: rgba(0, 0, 0, 0.1); @mixin mat-ripple() { // The host element of an md-ripple directive should always have a position of "absolute" or @@ -19,55 +13,19 @@ $mat-ripple-foreground-default-color: rgba(0, 0, 0, 0.0588); overflow: visible; } - .mat-ripple-background { - background-color: $mat-ripple-background-default-color; - opacity: 0; - transition: opacity $mat-ripple-background-fade-duration linear; + .mat-ripple-element { position: absolute; - left: 0; - top: 0; - right: 0; - bottom: 0; - } - - .mat-ripple-unbounded .mat-ripple-background { - display: none; - } - - .mat-ripple-background.mat-ripple-active { - opacity: 1; - } - - .mat-ripple-focused .mat-ripple-background { - opacity: 1; - } - - .mat-ripple-foreground { - background-color: $mat-ripple-foreground-default-color; border-radius: 50%; pointer-events: none; - opacity: $mat-ripple-foreground-initial-opacity; - position: absolute; - // The transition duration is manually set based on the ripple size. - transition: opacity, transform 0ms cubic-bezier(0, 0, 0.2, 1); - } - .mat-ripple-foreground.mat-ripple-fade-in { - opacity: 1; - } + background-color: $mat-ripple-element-color; - .mat-ripple-foreground.mat-ripple-fade-out { - opacity: 0; + transition: opacity, transform 0ms cubic-bezier(0, 0, 0.2, 1); + transform: scale(0); } } -@mixin mat-ripple-theme($theme) { - $accent: map-get($theme, accent); - - .mat-ripple-focused .mat-ripple-background { - background-color: mat-color($accent, $mat-ripple-focused-opacity); - } -} +@mixin mat-ripple-theme($theme) {} // A mixin, which generates temporary ink ripple on a given component. diff --git a/src/lib/core/ripple/ripple-renderer.ts b/src/lib/core/ripple/ripple-renderer.ts index 2fabdbb4d579..fede0df1efe8 100644 --- a/src/lib/core/ripple/ripple-renderer.ts +++ b/src/lib/core/ripple/ripple-renderer.ts @@ -1,27 +1,11 @@ -import { - ElementRef, - NgZone, -} from '@angular/core'; - -/** @docs-private */ -export enum ForegroundRippleState { - NEW, - EXPANDING, - FADING_OUT, -} +import {ElementRef, NgZone} from '@angular/core'; +import {ViewportRuler} from '../overlay/position/viewport-ruler'; -/** - * Wrapper for a foreground ripple DOM element and its animation state. - * @docs-private - */ -export class ForegroundRipple { - state = ForegroundRippleState.NEW; - constructor(public rippleElement: Element) {} -} +/** Fade-in speed in pixels per second. Can be modified with the speedFactor option. */ +export const RIPPLE_SPEED_PX_PER_SECOND = 170; -const RIPPLE_SPEED_PX_PER_SECOND = 1000; -const MIN_RIPPLE_FILL_TIME_SECONDS = 0.1; -const MAX_RIPPLE_FILL_TIME_SECONDS = 0.3; +/** Fade-out speed for the ripples in milliseconds. This can't be modified by the speedFactor. */ +export const RIPPLE_FADE_OUT_DURATION = 600; /** * Returns the distance from the point (x, y) to the furthest corner of a rectangle. @@ -32,6 +16,13 @@ const distanceToFurthestCorner = (x: number, y: number, rect: ClientRect) => { return Math.sqrt(distX * distX + distY * distY); }; +export type RippleConfig = { + color?: string; + centered?: boolean; + radius?: number; + speedFactor?: number; +} + /** * Helper service that performs DOM manipulations. Not intended to be used outside this module. * The constructor takes a reference to the ripple directive's host element and a map of DOM @@ -40,167 +31,149 @@ const distanceToFurthestCorner = (x: number, y: number, rect: ClientRect) => { * @docs-private */ export class RippleRenderer { - private _backgroundDiv: HTMLElement; - private _rippleElement: HTMLElement; + + /** Element where the ripples are being added to. */ + private _containerElement: HTMLElement; + + /** Element which triggers the ripple elements on mouse events. */ private _triggerElement: HTMLElement; - _opacity: string; - - 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. - this._backgroundDiv = null; - } - /** Creates the div for the ripple background, if it doesn't already exist. */ - createBackgroundIfNeeded() { - if (!this._backgroundDiv) { - this._backgroundDiv = document.createElement('div'); - this._backgroundDiv.classList.add('mat-ripple-background'); - this._rippleElement.appendChild(this._backgroundDiv); - } + /** 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(); + + /** Ripple config for all ripples created by events. */ + rippleConfig: RippleConfig = {}; + + /** Whether mouse ripples should be created or not. */ + rippleDisabled: boolean = false; + + constructor(_elementRef: ElementRef, private _ngZone: NgZone, private _ruler: ViewportRuler) { + this._containerElement = _elementRef.nativeElement; + + // Specify events which need to be registered on the trigger. + this._triggerEvents.set('mousedown', this.onMousedown.bind(this)); + this._triggerEvents.set('mouseup', this.onMouseup.bind(this)); + this._triggerEvents.set('mouseleave', this.onMouseLeave.bind(this)); + + // By default use the host element as trigger element. + this.setTriggerElement(this._containerElement); } - /** - * Installs event handlers on the given trigger element, and removes event handlers from the - * previous trigger if needed. - * - * @param newTrigger New trigger to which to attach the ripple handlers. - */ - setTriggerElement(newTrigger: HTMLElement) { - if (this._triggerElement !== newTrigger) { - if (this._triggerElement) { - this._eventHandlers.forEach((eventHandler, eventName) => { - this._triggerElement.removeEventListener(eventName, eventHandler); - }); - } - this._triggerElement = newTrigger; - if (this._triggerElement) { - this._eventHandlers.forEach((eventHandler, eventName) => { - this._triggerElement.addEventListener(eventName, eventHandler); - }); - } + /** Fades in a ripple at the given coordinates. */ + fadeInRipple(pageX: number, pageY: number, config: RippleConfig = {}) { + let containerRect = this._containerElement.getBoundingClientRect(); + + if (config.centered) { + pageX = containerRect.left + containerRect.width / 2; + pageY = containerRect.top + containerRect.height / 2; + } else { + // Subtract scroll values from the coordinates because calculations below + // are always relative to the viewport rectangle. + let scrollPosition = this._ruler.getViewportScrollPosition(); + pageX -= scrollPosition.left; + pageY -= scrollPosition.top; } + + let radius = config.radius || distanceToFurthestCorner(pageX, pageY, containerRect); + let duration = 1 / (config.speedFactor || 1) * (radius / RIPPLE_SPEED_PX_PER_SECOND); + let offsetX = pageX - containerRect.left; + let offsetY = pageY - containerRect.top; + + let ripple = document.createElement('div'); + ripple.classList.add('mat-ripple-element'); + + ripple.style.left = `${offsetX - radius}px`; + ripple.style.top = `${offsetY - radius}px`; + ripple.style.height = `${radius * 2}px`; + ripple.style.width = `${radius * 2}px`; + + // If the color is not set, the default CSS color will be used. + ripple.style.backgroundColor = config.color; + ripple.style.transitionDuration = `${duration}s`; + + this._containerElement.appendChild(ripple); + + // 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); + + 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. + this.runTimeoutOutsideZone(() => { + this._isMousedown ? this._activeRipples.push(ripple) : this.fadeOutRipple(ripple); + }, duration * 1000); } - /** Installs event handlers on the host element of the md-ripple directive. */ - setTriggerElementToHost() { - this.setTriggerElement(this._rippleElement); + /** Fades out a ripple element. */ + fadeOutRipple(ripple: HTMLElement) { + ripple.style.transitionDuration = `${RIPPLE_FADE_OUT_DURATION}ms`; + ripple.style.opacity = '0'; + + // Once the ripple faded out, the ripple can be safely removed from the DOM. + this.runTimeoutOutsideZone(() => { + ripple.parentNode.removeChild(ripple); + }, RIPPLE_FADE_OUT_DURATION); } - /** Removes event handlers from the current trigger element if needed. */ - clearTriggerElement() { - this.setTriggerElement(null); + /** Sets the trigger element and registers the mouse events. */ + setTriggerElement(element: HTMLElement) { + // Remove all previously register event listeners from the trigger element. + if (this._triggerElement) { + this._triggerEvents.forEach((fn, type) => this._triggerElement.removeEventListener(type, fn)); + } + + if (element) { + // If the element is not null, register all event listeners on the trigger element. + this._triggerEvents.forEach((fn, type) => element.addEventListener(type, fn)); + } + + this._triggerElement = element; } - /** - * Creates a foreground ripple and sets its animation to expand and fade in from the position - * given by rippleOriginLeft and rippleOriginTop (or from the center of the - * bounding rect if centered is true). - * - * @param rippleOriginLeft Left origin of the ripple. - * @param rippleOriginTop Top origin of the ripple. - * @param color Ripple color. - * @param centered Whether the ripple should be centered. - * @param radius Radius of the ripple. - * @param speedFactor Speed at which the ripple expands towards the edges. - * @param transitionEndCallback Callback to be triggered when the ripple transition is done. - */ - createForegroundRipple( - rippleOriginLeft: number, - rippleOriginTop: number, - color: string, - centered: boolean, - radius: number, - speedFactor: number, - transitionEndCallback: (r: ForegroundRipple, e: TransitionEvent) => void) { - const parentRect = this._rippleElement.getBoundingClientRect(); - // Create a foreground ripple div with the size and position of the fully expanded ripple. - // When the div is created, it's given a transform style that causes the ripple to be displayed - // small and centered on the event location (or the center of the bounding rect if the centered - // argument is true). Removing that transform causes the ripple to animate to its natural size. - const startX = centered ? (parentRect.left + parentRect.width / 2) : rippleOriginLeft; - const startY = centered ? (parentRect.top + parentRect.height / 2) : rippleOriginTop; - const offsetX = startX - parentRect.left; - const offsetY = startY - parentRect.top; - const maxRadius = radius > 0 ? radius : distanceToFurthestCorner(startX, startY, parentRect); - - const rippleDiv = document.createElement('div'); - this._rippleElement.appendChild(rippleDiv); - rippleDiv.classList.add('mat-ripple-foreground'); - rippleDiv.style.left = `${offsetX - maxRadius}px`; - rippleDiv.style.top = `${offsetY - maxRadius}px`; - rippleDiv.style.width = `${2 * maxRadius}px`; - rippleDiv.style.height = rippleDiv.style.width; - // If color input is not set, this will default to the background color defined in CSS. - rippleDiv.style.backgroundColor = color; - // Start the ripple tiny. - rippleDiv.style.transform = `scale(0.001)`; - - const fadeInSeconds = (1 / (speedFactor || 1)) * Math.max( - MIN_RIPPLE_FILL_TIME_SECONDS, - Math.min(MAX_RIPPLE_FILL_TIME_SECONDS, maxRadius / RIPPLE_SPEED_PX_PER_SECOND)); - rippleDiv.style.transitionDuration = `${fadeInSeconds}s`; - - // https://timtaubert.de/blog/2012/09/css-transitions-for-dynamically-created-dom-elements/ - // Store the opacity to prevent this line as being seen as a no-op by optimizers. - this._opacity = window.getComputedStyle(rippleDiv).opacity; - - rippleDiv.classList.add('mat-ripple-fade-in'); - // Clearing the transform property causes the ripple to animate to its full size. - rippleDiv.style.transform = ''; - const ripple = new ForegroundRipple(rippleDiv); - ripple.state = ForegroundRippleState.EXPANDING; - - 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); - }); + /** Listener being called on mousedown event. */ + private onMousedown(event: MouseEvent) { + if (this.rippleDisabled) { + return; + } + + this._isMousedown = true; + this.fadeInRipple(event.pageX, event.pageY, this.rippleConfig); } - /** - * Fades out a foreground ripple after it has fully expanded and faded in. - * @param ripple Ripple to be faded out. - */ - fadeOutForegroundRipple(ripple: Element) { - ripple.classList.remove('mat-ripple-fade-in'); - ripple.classList.add('mat-ripple-fade-out'); + /** Listener being called on mouseup event. */ + private onMouseup() { + this._isMousedown = false; + this._activeRipples.forEach(ripple => this.fadeOutRipple(ripple)); + this._activeRipples = []; } - /** - * Removes a foreground ripple from the DOM after it has faded out. - * @param ripple Ripple to be removed from the DOM. - */ - removeRippleFromDom(ripple: Element) { - if (ripple && ripple.parentElement) { - ripple.parentElement.removeChild(ripple); + /** Listener being called on mouseleave event. */ + private onMouseLeave() { + if (this._isMousedown) { + this.onMouseup(); } } - /** - * Fades in the ripple background. - * @param color New background color for the ripple. - */ - fadeInRippleBackground(color: string) { - this._backgroundDiv.classList.add('mat-ripple-active'); - // If color is not set, this will default to the background color defined in CSS. - this._backgroundDiv.style.backgroundColor = color; + /** Runs a timeout outside of the Angular zone to avoid triggering the change detection. */ + private runTimeoutOutsideZone(fn: Function, delay = 0) { + this._ngZone.runOutsideAngular(() => setTimeout(fn, delay)); } - /** Fades out the ripple background. */ - fadeOutRippleBackground() { - if (this._backgroundDiv) { - this._backgroundDiv.classList.remove('mat-ripple-active'); - } + /** 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'); } + } diff --git a/src/lib/core/ripple/ripple.spec.ts b/src/lib/core/ripple/ripple.spec.ts index 30525cf3b708..531c982c7e12 100644 --- a/src/lib/core/ripple/ripple.spec.ts +++ b/src/lib/core/ripple/ripple.spec.ts @@ -2,29 +2,9 @@ import {TestBed, ComponentFixture, fakeAsync, tick, inject} from '@angular/core/ import {Component, ViewChild} from '@angular/core'; import {MdRipple, MdRippleModule} from './ripple'; import {ViewportRuler} from '../overlay/position/viewport-ruler'; +import {RIPPLE_FADE_OUT_DURATION, RIPPLE_SPEED_PX_PER_SECOND} from './ripple-renderer'; -/** Creates a DOM event to indicate that a CSS transition for the given property ended. */ -const createTransitionEndEvent = (propertyName: string) => { - // The "new" TransitionEvent constructor isn't available in anything except Firefox: - // https://developer.mozilla.org/en-US/docs/Web/API/TransitionEvent - // So we just try to create a base event, and IE11 doesn't support that so we have to use - // the deprecated initTransitionEvent. - try { - const event = new Event('transitionend'); - (event).propertyName = propertyName; - return event; - } catch (e) { - const event = document.createEvent('TransitionEvent'); - event.initTransitionEvent('transitionend', - false, /* canBubble */ - false, /* cancelable */ - propertyName, - 0 /* elapsedTime */); - return event; - } -}; - /** Creates a DOM mouse event. */ const createMouseEvent = (eventType: string, dict: any = {}) => { // Ideally this would just be "return new MouseEvent(eventType, dict)". But IE11 doesn't support @@ -58,8 +38,7 @@ const pxStringToFloat = (s: string) => { describe('MdRipple', () => { let fixture: ComponentFixture; - let rippleElement: HTMLElement; - let rippleBackground: Element; + let rippleTarget: HTMLElement; let originalBodyMargin: string; let viewportRuler: ViewportRuler; @@ -86,112 +65,86 @@ describe('MdRipple', () => { document.body.style.margin = originalBodyMargin; }); + function dispatchMouseEvent(type: string, offsetX = 0, offsetY = 0) { + let mouseEvent = createMouseEvent(type, { + clientX: rippleTarget.clientLeft + offsetX, + clientY: rippleTarget.clientTop + offsetY + }); + + rippleTarget.dispatchEvent(mouseEvent); + } + describe('basic ripple', () => { + + const TARGET_HEIGHT = 200; + const TARGET_WIDTH = 300; + beforeEach(() => { fixture = TestBed.createComponent(BasicRippleContainer); fixture.detectChanges(); - rippleElement = fixture.debugElement.nativeElement.querySelector('[md-ripple]'); - rippleBackground = rippleElement.querySelector('.mat-ripple-background'); - expect(rippleBackground).toBeTruthy(); + rippleTarget = fixture.debugElement.nativeElement.querySelector('[mat-ripple]'); }); - it('shows background when parent receives mousedown event', () => { - expect(rippleBackground.classList).not.toContain('mat-ripple-active'); - const mouseDown = createMouseEvent('mousedown'); - // mousedown on the ripple element activates the background ripple. - rippleElement.dispatchEvent(mouseDown); - expect(rippleBackground.classList).toContain('mat-ripple-active'); - // mouseleave on the container removes the background ripple. - const mouseLeave = createMouseEvent('mouseleave'); - rippleElement.dispatchEvent(mouseLeave); - expect(rippleBackground.classList).not.toContain('mat-ripple-active'); - }); + it('creates ripple on mousedown', () => { + dispatchMouseEvent('mousedown'); + dispatchMouseEvent('mouseup'); + + expect(rippleTarget.querySelectorAll('.mat-ripple-element').length).toBe(1); - it('creates foreground ripples on click', () => { - rippleElement.click(); - expect(rippleElement.querySelectorAll('.mat-ripple-foreground').length).toBe(1); - // Second click should create another ripple. - rippleElement.click(); - const ripples = rippleElement.querySelectorAll('.mat-ripple-foreground'); - expect(ripples.length).toBe(2); - expect(ripples[0].classList).toContain('mat-ripple-fade-in'); - expect(ripples[1].classList).toContain('mat-ripple-fade-in'); - // Signal the end of the first ripple's expansion. The second ripple should be unaffected. - const opacityTransitionEnd = createTransitionEndEvent('opacity'); - ripples[0].dispatchEvent(opacityTransitionEnd); - expect(ripples[0].classList).not.toContain('mat-ripple-fade-in'); - expect(ripples[0].classList).toContain('mat-ripple-fade-out'); - expect(ripples[1].classList).toContain('mat-ripple-fade-in'); - expect(ripples[1].classList).not.toContain('mat-ripple-fade-out'); - // Signal the end of the first ripple's fade out. The ripple should be removed from the DOM. - ripples[0].dispatchEvent(opacityTransitionEnd); - expect(rippleElement.querySelectorAll('.mat-ripple-foreground').length).toBe(1); - expect(rippleElement.querySelectorAll('.mat-ripple-foreground')[0]).toBe(ripples[1]); - // Finish the second ripple. - ripples[1].dispatchEvent(opacityTransitionEnd); - expect(ripples[1].classList).not.toContain('mat-ripple-fade-in'); - expect(ripples[1].classList).toContain('mat-ripple-fade-out'); - ripples[1].dispatchEvent(opacityTransitionEnd); - expect(rippleElement.querySelectorAll('.mat-ripple-foreground').length).toBe(0); + dispatchMouseEvent('mousedown'); + dispatchMouseEvent('mouseup'); + + expect(rippleTarget.querySelectorAll('.mat-ripple-element').length).toBe(2); }); - it('removes foreground ripples after timeout', fakeAsync(() => { - rippleElement.click(); - expect(rippleElement.querySelectorAll('.mat-ripple-foreground').length).toBe(1); + it('removes ripple after timeout', fakeAsync(() => { + dispatchMouseEvent('mousedown'); + dispatchMouseEvent('mouseup'); + + expect(rippleTarget.querySelectorAll('.mat-ripple-element').length).toBe(1); + + // Determines the diagonal distance of the ripple target. + let diagonal = Math.sqrt(TARGET_HEIGHT * TARGET_HEIGHT + TARGET_WIDTH * TARGET_WIDTH); - tick(1600); + // Calculates the duration for fading in the ripple. Also adds the fade-out duration. + tick((diagonal / RIPPLE_SPEED_PX_PER_SECOND * 1000) + RIPPLE_FADE_OUT_DURATION); - expect(rippleElement.querySelectorAll('.mat-ripple-foreground').length).toBe(0); + expect(rippleTarget.querySelectorAll('.mat-ripple-element').length).toBe(0); })); it('creates ripples when manually triggered', () => { - const rippleComponent = fixture.debugElement.componentInstance.ripple; - // start() should show the background, but no foreground ripple yet. - rippleComponent.start(); - expect(rippleBackground.classList).toContain('mat-ripple-active'); - expect(rippleElement.querySelectorAll('.mat-ripple-foreground').length).toBe(0); - // end() should deactivate the background and show the foreground ripple. - rippleComponent.end(0, 0); - expect(rippleBackground.classList).not.toContain('mat-ripple-active'); - expect(rippleElement.querySelectorAll('.mat-ripple-foreground').length).toBe(1); + let rippleComponent = fixture.debugElement.componentInstance.ripple as MdRipple; + + expect(rippleTarget.querySelectorAll('.mat-ripple-element').length).toBe(0); + + rippleComponent.launch(0, 0); + + expect(rippleTarget.querySelectorAll('.mat-ripple-element').length).toBe(1); }); it('sizes ripple to cover element', () => { - // Click the ripple element 50 px to the right and 75px down from its upper left. - const elementRect = rippleElement.getBoundingClientRect(); - const clickEvent = createMouseEvent('click', - {clientX: elementRect.left + 50, clientY: elementRect.top + 75}); - rippleElement.dispatchEvent(clickEvent); + let elementRect = rippleTarget.getBoundingClientRect(); + + // Dispatch a ripple at the following relative coordinates (X: 50| Y: 75) + dispatchMouseEvent('mousedown', 50, 75); + dispatchMouseEvent('mouseup'); + + // Calculate distance from the click to farthest edge of the ripple target. + let maxDistanceX = TARGET_WIDTH - 50; + let maxDistanceY = TARGET_HEIGHT - 75; + // At this point the foreground ripple should be created with a div centered at the click // location, and large enough to reach the furthest corner, which is 250px to the right // and 125px down relative to the click position. - const expectedRadius = Math.sqrt(250 * 250 + 125 * 125); - const expectedLeft = elementRect.left + 50 - expectedRadius; - const expectedTop = elementRect.top + 75 - expectedRadius; - const ripple = rippleElement.querySelector('.mat-ripple-foreground'); - // Note: getBoundingClientRect won't work because there's a transform applied to make the - // ripple start out tiny. - expect(pxStringToFloat(ripple.style.left)).toBeCloseTo(expectedLeft, 1); - expect(pxStringToFloat(ripple.style.top)).toBeCloseTo(expectedTop, 1); - expect(pxStringToFloat(ripple.style.width)).toBeCloseTo(2 * expectedRadius, 1); - expect(pxStringToFloat(ripple.style.height)).toBeCloseTo(2 * expectedRadius, 1); - }); + let expectedRadius = Math.sqrt(maxDistanceX * maxDistanceX + maxDistanceY * maxDistanceY); + let expectedLeft = elementRect.left + 50 - expectedRadius; + let expectedTop = elementRect.top + 75 - expectedRadius; + + let ripple = rippleTarget.querySelector('.mat-ripple-element') as HTMLElement; - it('expands ripple from center on click event triggered by keyboard', () => { - const elementRect = rippleElement.getBoundingClientRect(); - // Simulate a keyboard-triggered click by setting event coordinates to 0. - const clickEvent = createMouseEvent('click', - {clientX: 0, clientY: 0, screenX: 0, screenY: 0}); - rippleElement.dispatchEvent(clickEvent); - // The foreground ripple should be centered in the middle of the bounding rect, and large - // enough to reach the corners, which are all 150px horizontally and 100px vertically away. - const expectedRadius = Math.sqrt(150 * 150 + 100 * 100); - const expectedLeft = elementRect.left + (elementRect.width / 2) - expectedRadius; - const expectedTop = elementRect.top + (elementRect.height / 2) - expectedRadius; // Note: getBoundingClientRect won't work because there's a transform applied to make the // ripple start out tiny. - const ripple = rippleElement.querySelector('.mat-ripple-foreground'); expect(pxStringToFloat(ripple.style.left)).toBeCloseTo(expectedLeft, 1); expect(pxStringToFloat(ripple.style.top)).toBeCloseTo(expectedTop, 1); expect(pxStringToFloat(ripple.style.width)).toBeCloseTo(2 * expectedRadius, 1); @@ -203,33 +156,38 @@ describe('MdRipple', () => { fixture = TestBed.createComponent(RippleContainerWithNgIf); fixture.detectChanges(); - rippleElement = fixture.debugElement.nativeElement.querySelector('[md-ripple]'); - rippleBackground = rippleElement.querySelector('.mat-ripple-background'); + rippleTarget = fixture.debugElement.nativeElement.querySelector('[mat-ripple]'); fixture.componentInstance.isDestroyed = true; fixture.detectChanges(); - rippleElement.dispatchEvent(createMouseEvent('mousedown')); - expect(rippleBackground.classList).not.toContain('mat-ripple-active'); + dispatchMouseEvent('mousedown'); + dispatchMouseEvent('mouseup'); + + expect(rippleTarget.querySelectorAll('.mat-ripple-element').length).toBe(0); }); describe('when page is scrolled', () => { const startingWindowWidth = window.innerWidth; const startingWindowHeight = window.innerHeight; - var veryLargeElement: HTMLDivElement = document.createElement('div'); - var pageScrollTop = 500; - var pageScrollLeft = 500; + + let veryLargeElement: HTMLDivElement = document.createElement('div'); + let pageScrollTop = 500; + let pageScrollLeft = 500; beforeEach(() => { // Add a very large element to make the page scroll veryLargeElement.style.width = '4000px'; veryLargeElement.style.height = '4000px'; + document.body.appendChild(veryLargeElement); document.body.scrollTop = pageScrollTop; document.body.scrollLeft = pageScrollLeft; + // Firefox document.documentElement.scrollLeft = pageScrollLeft; document.documentElement.scrollTop = pageScrollTop; + // Mobile safari window.scrollTo(pageScrollLeft, pageScrollTop); // Force an update of the cached viewport geometries because IE11 emits the @@ -241,9 +199,11 @@ describe('MdRipple', () => { document.body.removeChild(veryLargeElement); document.body.scrollTop = 0; document.body.scrollLeft = 0; + // Firefox document.documentElement.scrollLeft = 0; document.documentElement.scrollTop = 0; + // Mobile safari window.scrollTo(0, 0); // Force an update of the cached viewport geometries because IE11 emits the @@ -257,23 +217,25 @@ describe('MdRipple', () => { let left = 50; let top = 75; - rippleElement.style.left = `${elementLeft}px`; - rippleElement.style.top = `${elementTop}px`; + rippleTarget.style.left = `${elementLeft}px`; + rippleTarget.style.top = `${elementTop}px`; // Simulate a keyboard-triggered click by setting event coordinates to 0. - const clickEvent = createMouseEvent('click', { + let clickEvent = createMouseEvent('mousedown', { clientX: left + elementLeft - pageScrollLeft, clientY: top + elementTop - pageScrollTop, screenX: left + elementLeft, screenY: top + elementTop }); - rippleElement.dispatchEvent(clickEvent); - const expectedRadius = Math.sqrt(250 * 250 + 125 * 125); - const expectedLeft = left - expectedRadius; - const expectedTop = top - expectedRadius; + rippleTarget.dispatchEvent(clickEvent); + dispatchMouseEvent('mouseup'); - const ripple = rippleElement.querySelector('.mat-ripple-foreground'); + let expectedRadius = Math.sqrt(250 * 250 + 125 * 125); + let expectedLeft = left - expectedRadius; + let expectedTop = top - expectedRadius; + + let ripple = rippleTarget.querySelector('.mat-ripple-element') as HTMLElement; // In the iOS simulator (BrowserStack & SauceLabs), adding the content to the // body causes karma's iframe for the test to stretch to fit that content once we attempt to @@ -303,82 +265,81 @@ describe('MdRipple', () => { controller = fixture.debugElement.componentInstance; rippleComponent = controller.ripple; - rippleElement = fixture.debugElement.nativeElement.querySelector('[md-ripple]'); - rippleBackground = rippleElement.querySelector('.mat-ripple-background'); - expect(rippleBackground).toBeTruthy(); + rippleTarget = fixture.debugElement.nativeElement.querySelector('[mat-ripple]'); }); - it('sets ripple background color', () => { - // This depends on the exact color format that getComputedStyle returns; for example, alpha - // values are quantized to increments of 1/255, so 0.1 becomes 0.0980392. 0.2 is ok. - const color = 'rgba(22, 44, 66, 0.8)'; - controller.backgroundColor = color; - fixture.detectChanges(); - rippleComponent.start(); - expect(window.getComputedStyle(rippleBackground).backgroundColor).toBe(color); - }); + it('sets ripple color', () => { + let backgroundColor = 'rgba(12, 34, 56, 0.8)'; - it('sets ripple foreground color', () => { - const color = 'rgba(12, 34, 56, 0.8)'; - controller.color = color; + controller.color = backgroundColor; fixture.detectChanges(); - rippleElement.click(); - const ripple = rippleElement.querySelector('.mat-ripple-foreground'); - expect(window.getComputedStyle(ripple).backgroundColor).toBe(color); + + dispatchMouseEvent('mousedown'); + dispatchMouseEvent('mouseup'); + + let ripple = rippleTarget.querySelector('.mat-ripple-element'); + expect(window.getComputedStyle(ripple).backgroundColor).toBe(backgroundColor); }); it('does not respond to events when disabled input is set', () => { controller.disabled = true; fixture.detectChanges(); - const mouseDown = createMouseEvent('mousedown'); - // The background ripple should not respond to mouseDown, and no foreground ripple should be - // created on a click. - rippleElement.dispatchEvent(mouseDown); - expect(rippleBackground.classList).not.toContain('mat-ripple-active'); - rippleElement.click(); - expect(rippleElement.querySelectorAll('.mat-ripple-foreground').length).toBe(0); - // Calling start() and end() should still create a ripple. - rippleComponent.start(); - expect(rippleBackground.classList).toContain('mat-ripple-active'); - rippleComponent.end(0, 0); - expect(rippleElement.querySelectorAll('.mat-ripple-foreground').length).toBe(1); + + dispatchMouseEvent('mousedown'); + dispatchMouseEvent('mouseup'); + + expect(rippleTarget.querySelectorAll('.mat-ripple-element').length).toBe(0); + + controller.disabled = false; + fixture.detectChanges(); + + dispatchMouseEvent('mousedown'); + dispatchMouseEvent('mouseup'); + + expect(rippleTarget.querySelectorAll('.mat-ripple-element').length).toBe(1); }); it('allows specifying custom trigger element', () => { - // Events on the other div don't do anything by default. - const alternateTrigger = - fixture.debugElement.nativeElement.querySelector('.alternateTrigger'); - const mouseDown = createMouseEvent('mousedown'); - alternateTrigger.dispatchEvent(mouseDown); - expect(rippleBackground.classList).not.toContain('mat-ripple-active'); - alternateTrigger.click(); - expect(rippleElement.querySelectorAll('.mat-ripple-foreground').length).toBe(0); - - // Reassign the trigger element, and now events should create ripples. + let alternateTrigger = fixture.debugElement.nativeElement + .querySelector('.alternateTrigger') as HTMLElement; + + let mousedownEvent = createMouseEvent('mousedown'); + let mouseupEvent = createMouseEvent('mouseup'); + + alternateTrigger.dispatchEvent(mousedownEvent); + alternateTrigger.dispatchEvent(mouseupEvent); + + expect(rippleTarget.querySelectorAll('.mat-ripple-element').length).toBe(0); + + // Set the trigger element, and now events should create ripples. controller.trigger = alternateTrigger; fixture.detectChanges(); - alternateTrigger.dispatchEvent(mouseDown); - expect(rippleBackground.classList).toContain('mat-ripple-active'); - alternateTrigger.click(); - expect(rippleElement.querySelectorAll('.mat-ripple-foreground').length).toBe(1); + + alternateTrigger.dispatchEvent(mousedownEvent); + alternateTrigger.dispatchEvent(mouseupEvent); + + expect(rippleTarget.querySelectorAll('.mat-ripple-element').length).toBe(1); }); it('expands ripple from center if centered input is set', () => { controller.centered = true; fixture.detectChanges(); + + let elementRect = rippleTarget.getBoundingClientRect(); + // Click the ripple element 50 px to the right and 75px down from its upper left. - const elementRect = rippleElement.getBoundingClientRect(); - const clickEvent = createMouseEvent('click', - {clientX: elementRect.left + 50, clientY: elementRect.top + 75}); - rippleElement.dispatchEvent(clickEvent); + dispatchMouseEvent('mousedown', 50, 75); + dispatchMouseEvent('mouseup'); + // Because the centered input is true, the center of the ripple should be the midpoint of the // bounding rect. The ripple should expand to cover the rect corners, which are 150px // horizontally and 100px vertically from the midpoint. - const expectedRadius = Math.sqrt(150 * 150 + 100 * 100); - const expectedLeft = elementRect.left + (elementRect.width / 2) - expectedRadius; - const expectedTop = elementRect.top + (elementRect.height / 2) - expectedRadius; + let expectedRadius = Math.sqrt(150 * 150 + 100 * 100); + let expectedLeft = elementRect.left + (elementRect.width / 2) - expectedRadius; + let expectedTop = elementRect.top + (elementRect.height / 2) - expectedRadius; + + let ripple = rippleTarget.querySelector('.mat-ripple-element') as HTMLElement; - const ripple = rippleElement.querySelector('.mat-ripple-foreground'); expect(pxStringToFloat(ripple.style.left)).toBeCloseTo(expectedLeft, 1); expect(pxStringToFloat(ripple.style.top)).toBeCloseTo(expectedTop, 1); expect(pxStringToFloat(ripple.style.width)).toBeCloseTo(2 * expectedRadius, 1); @@ -386,68 +347,35 @@ describe('MdRipple', () => { }); it('uses custom radius if set', () => { - const customRadius = 42; - controller.maxRadius = customRadius; - fixture.detectChanges(); - // Click the ripple element 50 px to the right and 75px down from its upper left. - const elementRect = rippleElement.getBoundingClientRect(); - const clickEvent = createMouseEvent('click', - {clientX: elementRect.left + 50, clientY: elementRect.top + 75}); - rippleElement.dispatchEvent(clickEvent); - const expectedLeft = elementRect.left + 50 - customRadius; - const expectedTop = elementRect.top + 75 - customRadius; - - const ripple = rippleElement.querySelector('.mat-ripple-foreground'); - expect(pxStringToFloat(ripple.style.left)).toBeCloseTo(expectedLeft, 1); - expect(pxStringToFloat(ripple.style.top)).toBeCloseTo(expectedTop, 1); - expect(pxStringToFloat(ripple.style.width)).toBeCloseTo(2 * customRadius, 1); - expect(pxStringToFloat(ripple.style.height)).toBeCloseTo(2 * customRadius, 1); - }); - }); - - describe('initially disabled ripple', () => { - let controller: RippleContainerWithInputBindings; - let rippleComponent: MdRipple; + let customRadius = 42; - beforeEach(() => { - fixture = TestBed.createComponent(RippleContainerWithInputBindings); - controller = fixture.debugElement.componentInstance; - controller.disabled = true; + controller.radius = customRadius; fixture.detectChanges(); - rippleComponent = controller.ripple; - rippleElement = fixture.debugElement.nativeElement.querySelector('[md-ripple]'); - }); + let elementRect = rippleTarget.getBoundingClientRect(); - it('initially does not create background', () => { - rippleBackground = rippleElement.querySelector('.mat-ripple-background'); - expect(rippleBackground).toBeNull(); - }); - - it('creates background when enabled', () => { - rippleBackground = rippleElement.querySelector('.mat-ripple-background'); - expect(rippleBackground).toBeNull(); + // Click the ripple element 50 px to the right and 75px down from its upper left. + dispatchMouseEvent('mousedown', 50, 75); + dispatchMouseEvent('mouseup'); - controller.disabled = false; - fixture.detectChanges(); - rippleBackground = rippleElement.querySelector('.mat-ripple-background'); - expect(rippleBackground).toBeTruthy(); - }); + let expectedLeft = elementRect.left + 50 - customRadius; + let expectedTop = elementRect.top + 75 - customRadius; - it('creates background when manually activated', () => { - rippleBackground = rippleElement.querySelector('.mat-ripple-background'); - expect(rippleBackground).toBeNull(); + let ripple = rippleTarget.querySelector('.mat-ripple-element') as HTMLElement; - rippleComponent.start(); - rippleBackground = rippleElement.querySelector('.mat-ripple-background'); - expect(rippleBackground).toBeTruthy(); + expect(pxStringToFloat(ripple.style.left)).toBeCloseTo(expectedLeft, 1); + expect(pxStringToFloat(ripple.style.top)).toBeCloseTo(expectedTop, 1); + expect(pxStringToFloat(ripple.style.width)).toBeCloseTo(2 * customRadius, 1); + expect(pxStringToFloat(ripple.style.height)).toBeCloseTo(2 * customRadius, 1); }); }); + }); @Component({ template: ` -
+
`, }) @@ -458,13 +386,13 @@ class BasicRippleContainer { @Component({ template: `
+ [mdRippleColor]="color">
`, @@ -473,13 +401,13 @@ class RippleContainerWithInputBindings { trigger: HTMLElement = null; centered = false; disabled = false; - maxRadius = 0; + radius = 0; color = ''; - backgroundColor = ''; @ViewChild(MdRipple) ripple: MdRipple; } -@Component({ template: `
` }) +@Component({ template: `
` }) class RippleContainerWithNgIf { @ViewChild(MdRipple) ripple: MdRipple; isDestroyed = false; diff --git a/src/lib/core/ripple/ripple.ts b/src/lib/core/ripple/ripple.ts index fdfd03a9e893..549510f26c98 100644 --- a/src/lib/core/ripple/ripple.ts +++ b/src/lib/core/ripple/ripple.ts @@ -3,19 +3,13 @@ import { ModuleWithProviders, Directive, ElementRef, - HostBinding, Input, NgZone, OnChanges, + SimpleChanges, OnDestroy, - OnInit, - SimpleChange, } from '@angular/core'; -import { - RippleRenderer, - ForegroundRipple, - ForegroundRippleState, -} from './ripple-renderer'; +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'; @@ -25,216 +19,87 @@ import {SCROLL_DISPATCHER_PROVIDER} from '../overlay/scroll/scroll-dispatcher'; selector: '[md-ripple], [mat-ripple]', host: { '[class.mat-ripple]': 'true', + '[class.mat-ripple-unbounded]': 'unbounded' } }) -export class MdRipple implements OnInit, OnDestroy, OnChanges { +export class MdRipple implements OnChanges, OnDestroy { + /** * The element that triggers the ripple when click events are received. Defaults to the * directive's host element. */ // Prevent TS metadata emit from referencing HTMLElement in ripple.js - // That breaks tests running in node that load material components. + // Otherwise running this code in a Node environment (e.g Universal) will not work. @Input('mdRippleTrigger') trigger: HTMLElement|HTMLElement; - /** @deprecated */ - @Input('md-ripple-trigger') - get _triggerDeprecated() { return this.trigger; } - set _triggerDeprecated(value: HTMLElement|HTMLElement) { this.trigger = value; }; - /** * Whether the ripple always originates from the center of the host element's bounds, rather * than originating from the location of the click event. */ @Input('mdRippleCentered') centered: boolean; - /** @deprecated */ - @Input('md-ripple-centered') - get _centeredDeprecated() { return this.centered; } - set _centeredDeprecated(value: boolean) { this.centered = value; }; - /** * Whether click events will not trigger the ripple. It can still be triggered by manually - * calling start() and end(). + * calling createRipple() */ @Input('mdRippleDisabled') disabled: boolean; - /** @deprecated */ - @Input('md-ripple-disabled') - get _disabledDeprecated() { return this.disabled; } - set _disabledDeprecated(value: boolean) { this.disabled = value; }; - /** * If set, the radius in pixels of foreground ripples when fully expanded. If unset, the radius * will be the distance from the center of the ripple to the furthest corner of the host element's * bounding rectangle. */ - @Input('mdRippleMaxRadius') maxRadius: number = 0; - - /** @deprecated */ - @Input('md-ripple-max-radius') - get _maxRadiusDeprecated() { return this.maxRadius; } - set _maxRadiusDeprecated(value: number) { this.maxRadius = value; }; + @Input('mdRippleRadius') radius: number = 0; /** * If set, the normal duration of ripple animations is divided by this value. For example, * setting it to 0.5 will cause the animations to take twice as long. + * A changed speedFactor will not modify the fade-out duration of the ripples. */ @Input('mdRippleSpeedFactor') speedFactor: number = 1; - /** @deprecated */ - @Input('md-ripple-speed-factor') - get _speedFactorDeprecated() { return this.speedFactor; } - set _speedFactorDeprecated(value: number) { this.speedFactor = value; }; - /** Custom color for ripples. */ @Input('mdRippleColor') color: string; - /** @deprecated */ - @Input('md-ripple-color') - get _colorDeprecated() { return this.color; } - set _colorDeprecated(value: string) { this.color = value; }; - - /** Custom color for the ripple background. */ - @Input('mdRippleBackgroundColor') backgroundColor: string; - - /** @deprecated */ - @Input('md-ripple-background-color') - get _backgroundColorDeprecated() { return this.backgroundColor; } - set _backgroundColorDeprecated(value: string) { this.backgroundColor = value; }; - - /** Whether the ripple background will be highlighted to indicated a focused state. */ - @HostBinding('class.mat-ripple-focused') @Input('mdRippleFocused') focused: boolean; - - /** @deprecated */ - @Input('md-ripple-focused') - get _focusedDeprecated(): boolean { return this.focused; } - set _focusedDeprecated(value: boolean) { this.focused = value; }; - /** Whether foreground ripples should be visible outside the component's bounds. */ - @HostBinding('class.mat-ripple-unbounded') @Input('mdRippleUnbounded') unbounded: boolean; - - /** @deprecated */ - @Input('md-ripple-unbounded') - get _unboundedDeprecated(): boolean { return this.unbounded; } - set _unboundedDeprecated(value: boolean) { this.unbounded = value; }; + @Input('mdRippleUnbounded') unbounded: boolean; + /** Renderer for the ripple DOM manipulations. */ private _rippleRenderer: RippleRenderer; - _ruler: ViewportRuler; - - constructor(_elementRef: ElementRef, _ngZone: NgZone, _ruler: ViewportRuler) { - // 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, _ngZone); - this._ruler = _ruler; - } - ngOnInit() { - // If no trigger element was explicitly set, use the host element - if (!this.trigger) { - this._rippleRenderer.setTriggerElementToHost(); - } - if (!this.disabled) { - this._rippleRenderer.createBackgroundIfNeeded(); - } + constructor(elementRef: ElementRef, ngZone: NgZone, ruler: ViewportRuler) { + this._rippleRenderer = new RippleRenderer(elementRef, ngZone, ruler); } - ngOnDestroy() { - // Remove event listeners on the trigger element. - this._rippleRenderer.clearTriggerElement(); - } - - ngOnChanges(changes: { [propertyName: string]: SimpleChange }) { - // If the trigger element changed (or is being initially set), add event listeners to it. - const changedInputs = Object.keys(changes); - if (changedInputs.indexOf('trigger') !== -1) { + ngOnChanges(changes: SimpleChanges) { + if (changes['trigger'] && this.trigger) { this._rippleRenderer.setTriggerElement(this.trigger); } - if (!this.disabled) { - this._rippleRenderer.createBackgroundIfNeeded(); - } - } - - /** - * Responds to the start of a ripple animation trigger by fading the background in. - */ - start() { - this._rippleRenderer.createBackgroundIfNeeded(); - this._rippleRenderer.fadeInRippleBackground(this.backgroundColor); - } - - /** - * Responds to the end of a ripple animation trigger by fading the background out, and creating a - * foreground ripple that expands from the event location (or from the center of the element if - * the "centered" property is set or forceCenter is true). - */ - end(left: number, top: number, forceCenter = true) { - this._rippleRenderer.createForegroundRipple( - left, - top, - this.color, - this.centered || forceCenter, - this.maxRadius, - this.speedFactor, - (ripple: ForegroundRipple, e: TransitionEvent) => this._rippleTransitionEnded(ripple, e)); - this._rippleRenderer.fadeOutRippleBackground(); - } - private _rippleTransitionEnded(ripple: ForegroundRipple, event: TransitionEvent) { - if (event.propertyName === 'opacity') { - // If the ripple finished expanding, start fading it out. If it finished fading out, - // remove it from the DOM. - switch (ripple.state) { - case ForegroundRippleState.EXPANDING: - this._rippleRenderer.fadeOutForegroundRipple(ripple.rippleElement); - ripple.state = ForegroundRippleState.FADING_OUT; - break; - case ForegroundRippleState.FADING_OUT: - this._rippleRenderer.removeRippleFromDom(ripple.rippleElement); - break; - } - } + this._rippleRenderer.rippleDisabled = this.disabled; + this._updateRippleConfig(); } - /** - * Called when the trigger element receives a mousedown event. Starts the ripple animation by - * fading in the background. - */ - private _mouseDown(event: MouseEvent) { - if (!this.disabled && event.button === 0) { - this.start(); - } + ngOnDestroy() { + // Set the trigger element to null to cleanup all listeners. + this._rippleRenderer.setTriggerElement(null); } - /** - * Called when the trigger element receives a click event. Creates a foreground ripple and - * runs its animation. - */ - private _click(event: MouseEvent) { - if (!this.disabled && event.button === 0) { - // If screen and page positions are all 0, this was probably triggered by a keypress. - // In that case, use the center of the bounding rect as the ripple origin. - // FIXME: This fails on IE11, which still sets pageX/Y and screenX/Y on keyboard clicks. - const isKeyEvent = - (event.screenX === 0 && event.screenY === 0 && event.pageX === 0 && event.pageY === 0); - - this.end(event.pageX - this._ruler.getViewportScrollPosition().left, - event.pageY - this._ruler.getViewportScrollPosition().top, - isKeyEvent); - } + /** Launches a manual ripple at the specified position. */ + launch(pageX: number, pageY: number, config?: RippleConfig) { + this._rippleRenderer.fadeInRipple(pageX, pageY, config); } - /** - * Called when the trigger element receives a mouseleave event. Fades out the background. - */ - private _mouseLeave(event: MouseEvent) { - // We can always fade out the background here; It's a no-op if it was already inactive. - this._rippleRenderer.fadeOutRippleBackground(); + /** Updates the ripple configuration with the input values. */ + private _updateRippleConfig() { + this._rippleRenderer.rippleConfig = { + centered: this.centered, + speedFactor: this.speedFactor, + radius: this.radius, + color: this.color + }; } - // TODO: Reactivate the background div if the user drags out and back in. } diff --git a/src/lib/menu/menu-item.html b/src/lib/menu/menu-item.html index ab69914487fc..86e13ecab855 100644 --- a/src/lib/menu/menu-item.html +++ b/src/lib/menu/menu-item.html @@ -1,4 +1,3 @@ -
+
diff --git a/src/lib/radio/_radio-theme.scss b/src/lib/radio/_radio-theme.scss index a00a3b15d1e1..78787b95d6f0 100644 --- a/src/lib/radio/_radio-theme.scss +++ b/src/lib/radio/_radio-theme.scss @@ -29,7 +29,7 @@ } } - .mat-radio-ripple .mat-ripple-foreground { + .mat-radio-ripple .mat-ripple-element { background-color: mat-color($accent, 0.26); .mat-radio-disabled & { diff --git a/src/lib/radio/radio.html b/src/lib/radio/radio.html index ec4476b5648a..d278313d9dcc 100644 --- a/src/lib/radio/radio.html +++ b/src/lib/radio/radio.html @@ -1,15 +1,14 @@ -
{ fixture.detectChanges(); let link = fixture.debugElement.nativeElement.querySelector('.mat-tab-link'); - let rippleBackground = link.querySelector('.mat-ripple-background'); let mouseEvent = document.createEvent('MouseEvents'); fixture.componentInstance.isDestroyed = true; @@ -61,7 +60,8 @@ describe('MdTabNavBar', () => { link.dispatchEvent(mouseEvent); - expect(rippleBackground.classList).not.toContain('mat-ripple-active'); + expect(link.querySelector('.mat-ripple-element')) + .toBeFalsy('Expected no ripple to be created when ripple target is destroyed.'); }); }); 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 8b759588be8e..27b72a8f0f36 100644 --- a/src/lib/tabs/tab-nav-bar/tab-nav-bar.ts +++ b/src/lib/tabs/tab-nav-bar/tab-nav-bar.ts @@ -6,7 +6,6 @@ import { ViewEncapsulation, Directive, NgZone, - OnDestroy, } from '@angular/core'; import {MdInkBar} from '../ink-bar'; import {MdRipple} from '../../core/ripple/ripple'; @@ -67,15 +66,9 @@ export class MdTabLink { '[class.mat-tab-link]': 'true', }, }) -export class MdTabLinkRipple extends MdRipple implements OnDestroy { - constructor(private _element: ElementRef, private _ngZone: NgZone, _ruler: ViewportRuler) { - super(_element, _ngZone, _ruler); +export class MdTabLinkRipple extends MdRipple { + constructor(elementRef: ElementRef, ngZone: NgZone, ruler: ViewportRuler) { + super(elementRef, ngZone, ruler); } - /** - * In certain cases the parent destroy handler may not get called. See Angular issue #11606. - */ - ngOnDestroy() { - super.ngOnDestroy(); - } }