Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

feat(ripple): initial mdInkRipple implementation #681

Merged
merged 24 commits into from
Jul 25, 2016
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
a7ba9d0
Initial mdInkRipple implementation.
dozingcat Jun 14, 2016
551a999
Add missing files.
dozingcat Jun 14, 2016
b2f45b4
Remove unused code.
dozingcat Jun 14, 2016
dad7a44
Fix stylelint errors.
dozingcat Jun 14, 2016
7f720cc
In-progress updates for PR comments.
Jun 15, 2016
4f2230a
More PR comments.
Jun 16, 2016
cd3d435
Fix tests, use @internal.
dozingcat Jun 17, 2016
fed661b
Merge remote-tracking branch 'upstream/master' into mdInkRipple_Compo…
dozingcat Jun 17, 2016
12053e3
Restore original body margin after tests.
dozingcat Jun 17, 2016
7698999
Merge remote-tracking branch 'upstream/master' into mdInkRipple_Compo…
dozingcat Jun 18, 2016
f23fadf
Merge remote-tracking branch 'upstream/master' into mdInkRipple_Compo…
dozingcat Jun 23, 2016
0ff965e
Add "unbounded" and "max-radius" bindings.
dozingcat Jun 23, 2016
7dc5b67
Tweaking ripple color and speed.
Jun 23, 2016
b862c5f
Fix ripple scaling.
Jun 23, 2016
9e0f879
Merge remote-tracking branch 'upstream/master' into mdInkRipple_Compo…
dozingcat Jul 8, 2016
6dcbe61
Merge branch 'mdInkRipple_ComponentOnly' of https://github.com/dozing…
dozingcat Jul 8, 2016
ff53c5c
In-progress updates for PR comments.
dozingcat Jul 8, 2016
64cbaf2
PR comments
dozingcat Jul 8, 2016
ebddf73
Fix maxRadius binding in tests.
dozingcat Jul 8, 2016
0fbc26c
Simplify ripple demo @ViewChild.
dozingcat Jul 10, 2016
923a7ac
Merge remote-tracking branch 'upstream/master' into mdInkRipple_Compo…
dozingcat Jul 14, 2016
b46a0aa
Merge remote-tracking branch 'upstream/master' into mdInkRipple_Compo…
dozingcat Jul 21, 2016
e2208d8
Switch to attribute directive (<div md-ink-ripple> instead of <md-ink…
dozingcat Jul 22, 2016
39b850b
Change MdInkRipple identifiers to MdRipple, remove duplicate CSS file.
dozingcat Jul 22, 2016
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions src/components/ripple/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# md-ink-ripple

`md-ink-ripple` defines an area in which a ripple animates, usually in response to user action.

By default, an `md-ink-ripple` component is activated when its parent element receives mouse or touch events. On a mousedown or touch start, the ripple background fades in. When the click event complets, a circular foreground ripple fades in and expands from the event location to cover the component bounds.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

complets --> completes

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done


Ripples can also be triggered programatically by getting a reference to the MdInkRipple component and calling its `start` and `end` methods.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

programatically -> programmatically

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done



### Upcoming work

Ripples will be added to `md-button`, `md-radio-button`, and `md-checkbox` components.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about md-nav-list?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done


### API Summary

Properties:

| Name | Type | Description |
| --- | --- | --- |
| `trigger` | Element | The DOM element that triggers the ripple when clicked. Defaults to the parent of the `md-ink-ripple`.
| `color` | string | Custom color for foreground ripples
| `backgroundColor` | string | Custom color for the ripple background
| `centered` | boolean | If true, the ripple animation originates from the center of the `md-ink-ripple` bounds rather than from the location of the click event.
| `max-radius` | 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.
| `unbounded` | boolean | If true, foreground ripples will be visible outside the component's bounds.
| `focused` | boolean | If true, the background ripple is shown using the current theme's accent color to indicate focus.
| `disabled` | boolean | If true, click events on the trigger element will not activate ripples. The `start` and `end` methods can still be called to programatically create ripples.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

programatically -> programmatically

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

26 changes: 26 additions & 0 deletions src/components/ripple/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
{
"name": "@angular2-material/ripple",
"version": "2.0.0-alpha.5-2",
Copy link
Contributor

@kara kara Jul 6, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2.0.0-alpha.5-2 --> 2.0.0-alpha.6. Same on line 24.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

"description": "Angular 2 Material ripple",
"main": "./ripple.js",
"typings": "./ripple.d.ts",
"repository": {
"type": "git",
"url": "https://github.com/angular/material2.git"
},
"keywords": [
"angular",
"material",
"material design",
"components",
"ripple"
],
"license": "MIT",
"bugs": {
"url": "https://github.com/angular/material2/issues"
},
"homepage": "https://github.com/angular/material2#readme",
"peerDependencies": {
"@angular2-material/core": "2.0.0-alpha.5-2"
}
}
174 changes: 174 additions & 0 deletions src/components/ripple/ripple-manager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
import {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we rename this class to RippleRenderer (w/ corresponding file rename)?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

ElementRef,
} from '@angular/core';

/** @internal */
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We discovered last week that we need to omit @internal (almost) everywhere in order for the offline compiler to work, so we're just forgoing it completely now. For things that shouldn't be part of the public API, just put "TODO: internal" and eventually we'll mark these things in our API docs. For properties/methods that are "internal", you can prefix them with an underscore but leave off the private modifier.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed @internal. Do I need to remove any private modifiers?

export enum ForegroundRippleState {
NEW,
EXPANDING,
FADING_OUT,
}

/**
* Wrapper for a foreground ripple DOM element and its animation state.
* @internal
*/
export class ForegroundRipple {
state = ForegroundRippleState.NEW;
constructor(public rippleElement: Element) {}
}

const RIPPLE_SPEED_PX_PER_SECOND = 1000;
const MIN_RIPPLE_FILL_TIME_SECONDS = 0.1;
const MAX_RIPPLE_FILL_TIME_SECONDS = 0.3;

const sqr = (x: number) => x * x;

/**
* Returns the distance from the point (x, y) to the furthest corner of a rectangle.
*/
const distanceToFurthestCorner = (x: number, y: number, rect: ClientRect) => {
const maxSquaredDistance = Math.max(
sqr(x - rect.left) + sqr(y - rect.top),
sqr(rect.right - x) + sqr(y - rect.top),
sqr(x - rect.left) + sqr(rect.bottom - y),
sqr(rect.right - x) + sqr(rect.bottom - y));
return Math.sqrt(maxSquaredDistance);
Copy link
Contributor

@robertmesserle robertmesserle Jul 6, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A bit nitpicky, but it seems easier to do something like:

const maxX = Math.max(sqr(x - rect.left), sqr(x - rect.right));
const maxY = Math.max(sqr(y - rect.top), sqr(y - rect.bottom));
return Math.sqrt(maxX + maxY);

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That is better, and we can get rid of the custom sqr function using Math.abs. Also O(N) instead of O(2^N), which will be nice if we ever want to render 26-dimensional ripples to support superstrings.

};

/**
* Helper service that performs DOM manipulations. Not intended to be used outside this module.
* The constructor takes a reference to the <md-ink-ripple> element and a map of DOM event handlers
* to be installed on the element that triggers ripple animations.
* This will eventually become a custom renderer once Angular support exists.
* @internal
*/
export class MdInkRippleManager {
private _rippleElement: Element;
private _triggerElement: Element;

constructor(_elementRef: ElementRef, private _eventHandlers: Map<string, (e: Event) => void>) {
this._rippleElement = _elementRef.nativeElement;
}

/**
* Installs event handlers on the given trigger element, and removes event handlers from the
* previous trigger if needed.
*/
setTriggerElement(newTrigger: Element) {
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);
});
}
}
}

/**
* Installs event handlers on the parent of the <md-ink-ripple> element.
*/
setTriggerElementToParent() {
this.setTriggerElement(this._rippleElement.parentElement);
}

/**
* Removes event handlers from the current trigger element if needed.
*/
clearTriggerElement() {
this.setTriggerElement(null);
}

/**
* 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 <md-ink-ripple>
* bounding rect if centered is true).
*/
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('md-ripple-foreground');
rippleDiv.style.left = (offsetX - maxRadius) + 'px';
rippleDiv.style.top = (offsetY - maxRadius) + 'px';
rippleDiv.style.width = (2 * maxRadius) + 'px';
Copy link
Member

@jelbourn jelbourn Jul 6, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

optional: could use template strings, e.g.

`${2 * maxRadius}px`;

etc.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

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/
window.getComputedStyle(rippleDiv).opacity;

rippleDiv.classList.add('md-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));
}

/**
* Fades out a foreground ripple after it has fully expanded and faded in.
*/
fadeOutForegroundRipple(ripple: Element) {
ripple.classList.remove('md-ripple-fade-in');
ripple.classList.add('md-ripple-fade-out');
}

/**
* Removes a foreground ripple from the DOM after it has faded out.
*/
removeRippleFromDom(ripple: Element) {
ripple.parentElement.removeChild(ripple);
}

/**
* Fades in the <md-ink-ripple> background.
*/
fadeInRippleBackground(color: string) {
const background = <HTMLElement>this._rippleElement.querySelector('.md-ripple-background');
background.classList.add('md-ripple-active');
// If color is not set, this will default to the background color defined in CSS.
background.style.backgroundColor = color;
}

/**
* Fades out the <md-ink-ripple> background.
*/
fadeOutRippleBackground() {
const background = <HTMLElement>this._rippleElement.querySelector('.md-ripple-background');
background.classList.remove('md-ripple-active');
}
}
79 changes: 79 additions & 0 deletions src/components/ripple/ripple.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
@import 'default-theme';
@import 'theme-functions';

$md-ink-ripple-focused-opacity: 0.1;
$md-ink-ripple-background-fade-duration: 300ms;
$md-ink-ripple-background-default-color: rgba(0, 0, 0, 0.0588);
$md-ink-ripple-foreground-initial-opacity: 0.25;
$md-ink-ripple-foreground-default-color: rgba(0, 0, 0, 0.0588);

/**
* An <md-ink-ripple> component should always have a position of "absolute" or "relative",
* so that the ripple divs it creates inside itself are correctly positioned.
*/
md-ink-ripple {
overflow: hidden;
// The default trigger for the ripple is the parent element, but we need to
// disable mouse events here to prevent the event from bubbling up to the
// parent. The <md-ink-ripple> can have larger bounds than its parent, and
// we don't want a click outside the parent to trigger the ripple.
pointer-events: none;
}

md-ink-ripple.md-ripple-unbounded {
overflow: visible;
}

/**
* Style that can be used by parent elements to specify that the <md-ink-ripple>
* should have the same bounds as its parent.
*/
md-ink-ripple.md-ripple-fit-parent {
position: absolute;
left: 0;
top: 0;
right: 0;
bottom: 0;
}

.md-ripple-background {
background-color: $md-ink-ripple-background-default-color;
opacity: 0;
transition: opacity $md-ink-ripple-background-fade-duration linear;
position: absolute;
left: 0;
top: 0;
right: 0;
bottom: 0;
}

md-ink-ripple.md-ripple-unbounded .md-ripple-background {
display: none;
}

.md-ripple-background.md-ripple-active {
opacity: 1;
}

md-ink-ripple.md-ripple-focused .md-ripple-background {
background-color: md-color($md-accent, $md-ink-ripple-focused-opacity);
opacity: 1;
}

.md-ripple-foreground {
background-color: $md-ink-ripple-foreground-default-color;
border-radius: 50%;
pointer-events: none;
opacity: $md-ink-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);
}

.md-ripple-foreground.md-ripple-fade-in {
opacity: 1;
}

.md-ripple-foreground.md-ripple-fade-out {
opacity: 0;
}
Loading