-
Notifications
You must be signed in to change notification settings - Fork 6.8k
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
Changes from 14 commits
a7ba9d0
551a999
b2f45b4
dad7a44
7f720cc
4f2230a
cd3d435
fed661b
12053e3
7698999
f23fadf
0ff965e
7dc5b67
b862c5f
9e0f879
6dcbe61
ff53c5c
64cbaf2
ebddf73
0fbc26c
923a7ac
b46a0aa
e2208d8
39b850b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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. | ||
|
||
Ripples can also be triggered programatically by getting a reference to the MdInkRipple component and calling its `start` and `end` methods. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. programatically -> programmatically There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What about There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. programatically -> programmatically There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done |
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", | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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" | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,174 @@ | ||
import { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we rename this class to There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done. |
||
ElementRef, | ||
} from '@angular/core'; | ||
|
||
/** @internal */ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We discovered last week that we need to omit There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Removed |
||
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); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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); There was a problem hiding this comment. Choose a reason for hiding this commentThe 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'; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. optional: could use template strings, e.g. `${2 * maxRadius}px`; etc. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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'); | ||
} | ||
} |
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; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
complets --> completes
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done