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 all 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
3 changes: 3 additions & 0 deletions src/core/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ export {
// Gestures
export {MdGestureConfig} from './gestures/MdGestureConfig';

// Ripple
export {MD_RIPPLE_DIRECTIVES, MdRipple} from './ripple/ripple';

// a11y
export {
AriaLivePoliteness,
Expand Down
27 changes: 27 additions & 0 deletions src/core/ripple/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# md-ripple

`md-ripple` defines an area in which a ripple animates, usually in response to user action. It is used as an attribute directive, for example `<div md-ripple [md-ripple-color]="rippleColor">...</div>`.

By default, a ripple is activated when the host element of the `md-ripple` directive receives mouse or touch events. On a mousedown or touch start, the ripple background fades in. When the click event completes, a circular foreground ripple fades in and expands from the event location to cover the host element bounds.

Ripples can also be triggered programmatically by getting a reference to the MdRipple directive and calling its `start` and `end` methods.


### Upcoming work

Ripples will be added to the `md-button`, `md-radio-button`, `md-checkbox`, and `md-nav-list` components.

### API Summary

Properties:

| Name | Type | Description |
| --- | --- | --- |
| `md-ripple-trigger` | Element | The DOM element that triggers the ripple when clicked. Defaults to the parent of the `md-ripple`.
| `md-ripple-color` | string | Custom color for foreground ripples
| `md-ripple-background-color` | string | Custom color for the ripple background
| `md-ripple-centered` | boolean | If true, the ripple animation originates from the center of the `md-ripple` bounds rather than from the location of the click event.
| `md-ripple-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.
| `md-ripple-unbounded` | boolean | If true, foreground ripples will be visible outside the component's bounds.
| `md-ripple-focused` | boolean | If true, the background ripple is shown using the current theme's accent color to indicate focus.
| `md-ripple-disabled` | 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.
175 changes: 175 additions & 0 deletions src/core/ripple/ripple-renderer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
import {
ElementRef,
} from '@angular/core';

/** TODO: internal */
export enum ForegroundRippleState {
NEW,
EXPANDING,
FADING_OUT,
}

/**
* Wrapper for a foreground ripple DOM element and its animation state.
* TODO: 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;

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

/**
* 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
* event handlers to be installed on the element that triggers ripple animations.
* This will eventually become a custom renderer once Angular support exists.
* TODO: internal
*/
export class RippleRenderer {
private _backgroundDiv: HTMLElement;
private _rippleElement: HTMLElement;
private _triggerElement: HTMLElement;

constructor(_elementRef: ElementRef, private _eventHandlers: Map<string, (e: Event) => void>) {
this._rippleElement = _elementRef.nativeElement;
// It might be nice to delay creating the background until it's needed, but doing this in
// fadeInRippleBackground causes the first click event to not be handled reliably.
this._backgroundDiv = document.createElement('div');
this._backgroundDiv.classList.add('md-ripple-background');
this._rippleElement.appendChild(this._backgroundDiv);
}

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

/**
* Installs event handlers on the host element of the md-ripple directive.
*/
setTriggerElementToHost() {
this.setTriggerElement(this._rippleElement);
}

/**
* 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-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`;
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 ripple background.
*/
fadeInRippleBackground(color: string) {
this._backgroundDiv.classList.add('md-ripple-active');
// If color is not set, this will default to the background color defined in CSS.
this._backgroundDiv.style.backgroundColor = color;
}

/**
* Fades out the ripple background.
*/
fadeOutRippleBackground() {
if (this._backgroundDiv) {
this._backgroundDiv.classList.remove('md-ripple-active');
}
}
}
Loading