From d65327d21b6e7f930eb45a9a40f177a6e2de2da2 Mon Sep 17 00:00:00 2001 From: Elizabeth Mitchell Date: Thu, 25 May 2023 15:33:26 -0700 Subject: [PATCH] feat(ripple): add semantic and imperative attaching PiperOrigin-RevId: 535409389 --- ripple/demo/demo.ts | 3 +- ripple/demo/stories.ts | 18 ++++------ ripple/directive.ts | 27 ++------------- ripple/lib/ripple.ts | 79 +++++++++++++++++++++++++++++++++++++++++- 4 files changed, 87 insertions(+), 40 deletions(-) diff --git a/ripple/demo/demo.ts b/ripple/demo/demo.ts index 8bf57b4d25..572604a557 100644 --- a/ripple/demo/demo.ts +++ b/ripple/demo/demo.ts @@ -8,7 +8,7 @@ import './index.js'; import './material-collection.js'; import {KnobTypesToKnobs, MaterialCollection, materialInitsToStoryInits, setUpDemo} from './material-collection.js'; -import {boolInput, colorPicker, cssCustomProperty, Knob, numberInput} from './index.js'; +import {colorPicker, cssCustomProperty, Knob, numberInput} from './index.js'; import {stories, StoryKnobs} from './stories.js'; @@ -24,7 +24,6 @@ function cssCustomPropertyAsNumber( const collection = new MaterialCollection>('Ripple', [ - new Knob('disabled', {ui: boolInput(), defaultValue: false}), new Knob( '--md-ripple-pressed-color', {ui: colorPicker(), wiring: cssCustomProperty}), diff --git a/ripple/demo/stories.ts b/ripple/demo/stories.ts index 6a9175bb96..6c769c26bc 100644 --- a/ripple/demo/stories.ts +++ b/ripple/demo/stories.ts @@ -7,14 +7,10 @@ import '@material/web/ripple/ripple.js'; import {MaterialStoryInit} from './material-collection.js'; -import {ripple} from '@material/web/ripple/directive.js'; -import {MdRipple} from '@material/web/ripple/ripple.js'; import {css, html} from 'lit'; -import {createRef, ref} from 'lit/directives/ref.js'; /** Knob types for ripple stories. */ export interface StoryKnobs { - disabled: boolean; '--md-ripple-pressed-color': string; '--md-ripple-pressed-opacity': number; '--md-ripple-hover-color': string; @@ -32,11 +28,10 @@ const bounded: MaterialStoryInit = { width: 64px; } `, - render({disabled}) { - const rippleRef = createRef(); + render() { return html` -
rippleRef.value || null)}> - +
+
`; } @@ -76,12 +71,11 @@ const unbounded: MaterialStoryInit = { width: 40px; } `, - render({disabled}) { - const rippleRef = createRef(); + render() { return html` -
rippleRef.value || null)}> +
- +
diff --git a/ripple/directive.ts b/ripple/directive.ts index a170c14958..30a7c7d479 100644 --- a/ripple/directive.ts +++ b/ripple/directive.ts @@ -38,31 +38,8 @@ class RippleDirective extends Directive { if (!ripple) { return; } - switch (event.type) { - case 'click': - ripple.handleClick(); - break; - case 'contextmenu': - ripple.handleContextmenu(); - break; - case 'pointercancel': - ripple.handlePointercancel(event as PointerEvent); - break; - case 'pointerdown': - await ripple.handlePointerdown(event as PointerEvent); - break; - case 'pointerenter': - ripple.handlePointerenter(event as PointerEvent); - break; - case 'pointerleave': - ripple.handlePointerleave(event as PointerEvent); - break; - case 'pointerup': - ripple.handlePointerup(event as PointerEvent); - break; - default: - break; - } + + await ripple.handleEvent(event); } override update(part: ElementPart, [ripple]: DirectiveParameters) { diff --git a/ripple/lib/ripple.ts b/ripple/lib/ripple.ts index c3e8e0c79a..6f6e33ae39 100644 --- a/ripple/lib/ripple.ts +++ b/ripple/lib/ripple.ts @@ -8,6 +8,7 @@ import {html, LitElement, PropertyValues} from 'lit'; import {property, query, state} from 'lit/decorators.js'; import {classMap} from 'lit/directives/class-map.js'; +import {Attachable, AttachableController} from '../../controller/attachable-controller.js'; import {EASING} from '../../motion/animation.js'; const PRESS_GROW_MS = 450; @@ -64,6 +65,14 @@ enum State { WAITING_FOR_CLICK } +/** + * Events that the ripple listens to. + */ +const EVENTS = [ + 'click', 'contextmenu', 'pointercancel', 'pointerdown', 'pointerenter', + 'pointerleave', 'pointerup' +]; + /** * Delay reacting to touch so that we do not show the ripple for a swipe or * scroll interaction. @@ -73,12 +82,24 @@ const TOUCH_DELAY_MS = 150; /** * A ripple component. */ -export class Ripple extends LitElement { +export class Ripple extends LitElement implements Attachable { /** * Disables the ripple. */ @property({type: Boolean, reflect: true}) disabled = false; + get htmlFor() { + return this.attachableController.htmlFor; + } + + set htmlFor(htmlFor: string|null) { + this.attachableController.htmlFor = htmlFor; + } + + get control() { + return this.attachableController.control; + } + @state() private hovered = false; @state() private pressed = false; @@ -90,6 +111,21 @@ export class Ripple extends LitElement { private state = State.INACTIVE; private rippleStartEvent?: PointerEvent; private checkBoundsAfterContextMenu = false; + private readonly attachableController = + new AttachableController(this, this.onControlChange.bind(this)); + + // TODO(b/265337232): Remove once ripple directive is removed. This is used to + // prevent two animations while migrating ripples from the directive to the + // new attachment syntax. + private lastHandledEvent?: Event; + + attach(control: HTMLElement) { + this.attachableController.attach(control); + } + + detach() { + this.attachableController.detach(); + } handlePointerenter(event: PointerEvent) { if (!this.shouldReactToEvent(event)) { @@ -367,4 +403,45 @@ export class Ripple extends LitElement { private isTouch({pointerType}: PointerEvent) { return pointerType === 'touch'; } + + /** @private */ + async handleEvent(event: Event) { + if (this.lastHandledEvent === event) { + return; + } + + this.lastHandledEvent = event; + switch (event.type) { + case 'click': + this.handleClick(); + break; + case 'contextmenu': + this.handleContextmenu(); + break; + case 'pointercancel': + this.handlePointercancel(event as PointerEvent); + break; + case 'pointerdown': + await this.handlePointerdown(event as PointerEvent); + break; + case 'pointerenter': + this.handlePointerenter(event as PointerEvent); + break; + case 'pointerleave': + this.handlePointerleave(event as PointerEvent); + break; + case 'pointerup': + this.handlePointerup(event as PointerEvent); + break; + default: + break; + } + } + + private onControlChange(prev: HTMLElement|null, next: HTMLElement|null) { + for (const event of EVENTS) { + prev?.removeEventListener(event, this); + next?.addEventListener(event, this); + } + } }