Skip to content

Commit

Permalink
chore(ripple): add docs and clean up implementation
Browse files Browse the repository at this point in the history
PiperOrigin-RevId: 509273426
  • Loading branch information
asyncLiz authored and copybara-github committed Feb 13, 2023
1 parent 77b4864 commit 56dc57b
Show file tree
Hide file tree
Showing 4 changed files with 90 additions and 108 deletions.
16 changes: 6 additions & 10 deletions ripple/lib/_ripple.scss
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,6 @@
// SPDX-License-Identifier: Apache-2.0
//

// stylelint-disable selector-class-pattern --
// Selector '.md3-*' should only be used in this project.

@use 'sass:map';
@use '../../sass/theme';
@use '../../tokens';
Expand Down Expand Up @@ -37,14 +34,14 @@
}

:host,
.md3-ripple-surface {
.surface {
position: absolute;
inset: 0;
pointer-events: none;
overflow: hidden;
}

.md3-ripple-surface {
.surface {
// TODO(https://bugs.webkit.org/show_bug.cgi?id=247546)
// Remove Safari workaround for incorrect ripple overflow when addressed.
// This addresses `hover` and `pressed` state oveflow.
Expand Down Expand Up @@ -79,24 +76,24 @@
}
}

.md3-ripple--hovered::before {
.hovered::before {
background-color: var(--_hover-state-layer-color);
opacity: var(--_hover-state-layer-opacity);
}

.md3-ripple--focused::before {
.focused::before {
background-color: var(--_focus-state-layer-color);
opacity: var(--_focus-state-layer-opacity);
transition-duration: 75ms;
}

.md3-ripple--pressed::after {
.pressed::after {
// press ripple fade-in
opacity: var(--_pressed-state-layer-opacity);
transition-duration: 105ms;
}

.md3-ripple--unbounded {
.unbounded {
$unbounded: (
state-layer-shape: map.get(tokens.md-sys-shape-values(), 'corner-full'),
);
Expand All @@ -105,7 +102,6 @@
--_state-layer-shape: #{map.get($unbounded, 'state-layer-shape')};
}

// TODO(b/230630968): create a forced-colors-mode mixin
@media screen and (forced-colors: active) {
:host {
display: none;
Expand Down
162 changes: 70 additions & 92 deletions ripple/lib/ripple.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@
* SPDX-License-Identifier: Apache-2.0
*/

import {html, LitElement, PropertyValues, TemplateResult} from 'lit';
import {html, LitElement, PropertyValues} from 'lit';
import {property, query, state} from 'lit/decorators.js';
import {ClassInfo, classMap} from 'lit/directives/class-map.js';
import {classMap} from 'lit/directives/class-map.js';

import {createAnimationSignal, EASING} from '../../motion/animation.js';

Expand All @@ -19,41 +19,79 @@ const SOFT_EDGE_CONTAINER_RATIO = 0.35;
const PRESS_PSEUDO = '::after';
const ANIMATION_FILL = 'forwards';

/** @soyCompatible */
/**
* A ripple component.
*/
export class Ripple extends LitElement {
@query('.md3-ripple-surface') mdRoot!: HTMLElement;

// TODO(https://bugs.webkit.org/show_bug.cgi?id=247546)
// Remove Safari workaround that requires reflecting `unbounded` so
// it can be styled against.
/**
* Sets the ripple to be an unbounded circle.
*/
@property({type: Boolean, reflect: true}) unbounded = false;

/**
* Disables the ripple.
*/
@property({type: Boolean, reflect: true}) disabled = false;

@state() protected hovered = false;
@state() protected focused = false;
@state() protected pressed = false;

protected rippleSize = '';
protected rippleScale = '';
protected initialSize = 0;
protected pressAnimationSignal = createAnimationSignal();
protected growAnimation: Animation|null = null;
protected delayedEndPressHandle: number|null = null;

/** @soyTemplate */
protected override render(): TemplateResult {
return html`<div class="md3-ripple-surface ${
classMap(this.getRenderRippleClasses())}"></div>`;
@state() private hovered = false;
@state() private focused = false;
@state() private pressed = false;

@query('.surface') private readonly mdRoot!: HTMLElement;
private rippleSize = '';
private rippleScale = '';
private initialSize = 0;
private readonly pressAnimationSignal = createAnimationSignal();
private growAnimation: Animation|null = null;
private delayedEndPressHandle?: number;

beginHover(hoverEvent?: Event) {
if ((hoverEvent as PointerEvent)?.pointerType !== 'touch') {
this.hovered = true;
}
}

endHover() {
this.hovered = false;
}

/** @soyTemplate */
protected getRenderRippleClasses(): ClassInfo {
return {
'md3-ripple--hovered': this.hovered,
'md3-ripple--focused': this.focused,
'md3-ripple--pressed': this.pressed,
'md3-ripple--unbounded': this.unbounded,
beginFocus() {
this.focused = true;
}

endFocus() {
this.focused = false;
}

beginPress(positionEvent?: Event|null) {
this.pressed = true;
clearTimeout(this.delayedEndPressHandle);
this.startPressAnimation(positionEvent);
}

endPress() {
const pressAnimationPlayState = this.growAnimation?.currentTime ?? Infinity;
if (pressAnimationPlayState >= MINIMUM_PRESS_MS) {
this.pressed = false;
} else {
this.delayedEndPressHandle = setTimeout(() => {
this.pressed = false;
}, MINIMUM_PRESS_MS - pressAnimationPlayState);
}
}

protected override render() {
const classes = {
'hovered': this.hovered,
'focused': this.focused,
'pressed': this.pressed,
'unbounded': this.unbounded,
};

return html`<div class="surface ${classMap(classes)}"></div>`;
}

protected override update(changedProps: PropertyValues<this>) {
Expand All @@ -65,11 +103,11 @@ export class Ripple extends LitElement {
super.update(changedProps);
}

protected getDimensions() {
private getDimensions() {
return (this.parentElement ?? this).getBoundingClientRect();
}

protected determineRippleSize() {
private determineRippleSize() {
const {height, width} = this.getDimensions();
const maxDim = Math.max(height, width);
const softEdgeSize =
Expand All @@ -92,7 +130,7 @@ export class Ripple extends LitElement {
this.rippleSize = `${this.initialSize}px`;
}

protected getNormalizedPointerEventCoords(pointerEvent: PointerEvent):
private getNormalizedPointerEventCoords(pointerEvent: PointerEvent):
{x: number, y: number} {
const {scrollX, scrollY} = window;
const {left, top} = this.getDimensions();
Expand All @@ -102,7 +140,7 @@ export class Ripple extends LitElement {
return {x: pageX - documentX, y: pageY - documentY};
}

protected getTranslationCoordinates(positionEvent?: Event|null) {
private getTranslationCoordinates(positionEvent?: Event|null) {
const {height, width} = this.getDimensions();
// end in the center
const endPoint = {
Expand All @@ -129,7 +167,7 @@ export class Ripple extends LitElement {
return {startPoint, endPoint};
}

protected startPressAnimation(positionEvent?: Event|null) {
private startPressAnimation(positionEvent?: Event|null) {
this.determineRippleSize();
const {startPoint, endPoint} =
this.getTranslationCoordinates(positionEvent);
Expand Down Expand Up @@ -168,64 +206,4 @@ export class Ripple extends LitElement {

this.growAnimation = growAnimation;
}

/**
* @deprecated Use beginHover
*/
startHover(hoverEvent?: Event) {
this.beginHover(hoverEvent);
}

beginHover(hoverEvent?: Event) {
if ((hoverEvent as PointerEvent)?.pointerType !== 'touch') {
this.hovered = true;
}
}

endHover() {
this.hovered = false;
}

/**
* @deprecated Use beginFocus
*/
startFocus() {
this.beginFocus();
}

beginFocus() {
this.focused = true;
}

endFocus() {
this.focused = false;
}

/**
* @deprecated Use beginPress
*/
startPress(positionEvent?: Event|null) {
this.beginPress(positionEvent);
}

beginPress(positionEvent?: Event|null) {
this.pressed = true;
if (this.delayedEndPressHandle !== null) {
clearTimeout(this.delayedEndPressHandle);
this.delayedEndPressHandle = null;
}
this.startPressAnimation(positionEvent);
}

endPress() {
const pressAnimationPlayState = this.growAnimation?.currentTime ?? Infinity;
if (pressAnimationPlayState >= MINIMUM_PRESS_MS) {
this.pressed = false;
} else {
this.delayedEndPressHandle = setTimeout(() => {
this.pressed = false;
this.delayedEndPressHandle = null;
}, MINIMUM_PRESS_MS - pressAnimationPlayState);
}
}
}
8 changes: 4 additions & 4 deletions ripple/lib/ripple_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@ import {customElement} from 'lit/decorators.js';
import {Ripple} from './ripple.js';

enum RippleStateClasses {
HOVERED = 'md3-ripple--hovered',
FOCUSED = 'md3-ripple--focused',
PRESSED = 'md3-ripple--pressed',
HOVERED = 'hovered',
FOCUSED = 'focused',
PRESSED = 'pressed',
}

declare global {
Expand All @@ -38,7 +38,7 @@ describe('ripple', () => {
container.appendChild(element);
await element.updateComplete;

surface = element.renderRoot.querySelector('.md3-ripple-surface')!;
surface = element.renderRoot.querySelector('.surface')!;
});

afterEach(() => {
Expand Down
12 changes: 10 additions & 2 deletions ripple/ripple.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,19 @@ declare global {
}

/**
* @soyCompatible
* @summary Ripples, also known as state layers, are visual indicators used to
* communicate the status of a component or interactive element.
*
* @description A state layer is a semi-transparent covering on an element that
* indicates its state. State layers provide a systematic approach to
* visualizing states by using opacity. A layer can be applied to an entire
* element or in a circular shape and only one state layer can be applied at a
* given time.
*
* @final
* @suppress {visibility}
*/
@customElement('md-ripple')
export class MdRipple extends Ripple {
static override styles = [styles];
}
}

0 comments on commit 56dc57b

Please sign in to comment.