-
Notifications
You must be signed in to change notification settings - Fork 6.8k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(ripple): initial mdInkRipple implementation (#681)
* Initial mdInkRipple implementation. * Add missing files. * Remove unused code. * Fix stylelint errors. * In-progress updates for PR comments. * More PR comments. * Fix tests, use @internal. * Restore original body margin after tests. * Add "unbounded" and "max-radius" bindings. * Tweaking ripple color and speed. * Fix ripple scaling. * In-progress updates for PR comments. * PR comments * Fix maxRadius binding in tests. * Simplify ripple demo @ViewChild. * Switch to attribute directive (<div md-ink-ripple> instead of <md-ink-ripple>) and move to core. * Change MdInkRipple identifiers to MdRipple, remove duplicate CSS file.
- Loading branch information
Showing
13 changed files
with
920 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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'); | ||
} | ||
} | ||
} |
Oops, something went wrong.