diff --git a/ripple/lib/_ripple.scss b/ripple/lib/_ripple.scss index 3a32a119bf..11b6c56e5c 100644 --- a/ripple/lib/_ripple.scss +++ b/ripple/lib/_ripple.scss @@ -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'; @@ -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. @@ -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'), ); @@ -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; diff --git a/ripple/lib/ripple.ts b/ripple/lib/ripple.ts index 4cc83a3ac7..bb8762bd84 100644 --- a/ripple/lib/ripple.ts +++ b/ripple/lib/ripple.ts @@ -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'; @@ -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`
`; + @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`
`; } protected override update(changedProps: PropertyValues) { @@ -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 = @@ -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(); @@ -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 = { @@ -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); @@ -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); - } - } } diff --git a/ripple/lib/ripple_test.ts b/ripple/lib/ripple_test.ts index bd6d1068f3..b7fc2e6412 100644 --- a/ripple/lib/ripple_test.ts +++ b/ripple/lib/ripple_test.ts @@ -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 { @@ -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(() => { diff --git a/ripple/ripple.ts b/ripple/ripple.ts index 2873ca2890..cb7f587b1b 100644 --- a/ripple/ripple.ts +++ b/ripple/ripple.ts @@ -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]; -} \ No newline at end of file +}