chromium/ui/webui/resources/cr_elements/cr_ripple/cr_ripple_mixin.ts

// Copyright 2024 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import './cr_ripple.js';

import {assert} from '//resources/js/assert.js';
import type {CrLitElement, PropertyValues} from '//resources/lit/v3_0/lit.rollup.js';

import type {CrRippleElement} from './cr_ripple.js';

/**
 * `CrRippleMixin` exposes methods to dynamically create a cr-ripple
 * when needed.
 */

type Constructor<T> = new (...args: any[]) => T;

export const CrRippleMixin =
    <T extends Constructor<CrLitElement>>(superClass: T): T&
    Constructor<CrRippleMixinInterface> => {
      class CrRippleMixin extends superClass implements
          CrRippleMixinInterface {
        static get properties() {
          return {
            /**
             * If true, the element will not produce a ripple effect when
             * interacted with via the pointer.
             */
            noink: {type: Boolean},
          };
        }

        noink: boolean = false;
        rippleContainer: HTMLElement|null = null;

        private ripple_: CrRippleElement|null = null;

        override updated(changedProperties: PropertyValues<this>) {
          super.updated(changedProperties);

          if (changedProperties.has('noink') && this.hasRipple()) {
            assert(this.ripple_);
            this.ripple_.noink = this.noink;
          }
        }

        ensureRippleOnPointerdown() {
          // 'capture: true' is necessary so that the cr-ripple is created early
          // enough so that it also receives the 'pointerdown' event. Otherwise
          // the ripple is created, but not shown on the 1st click.
          this.addEventListener(
              'pointerdown', () => this.ensureRipple(), {capture: true});
        }

        /**
         * Ensures this element contains a ripple effect. For startup efficiency
         * the ripple effect is dynamically added on demand when needed.
         */
        ensureRipple() {
          if (this.hasRipple()) {
            return;
          }

          this.ripple_ = this.createRipple();
          this.ripple_.noink = this.noink;
          const rippleContainer = this.rippleContainer || this.shadowRoot;
          assert(rippleContainer);
          rippleContainer.appendChild(this.ripple_);
        }

        /**
         * Returns the `<cr-ripple>` element used by this element to create
         * ripple effects. The element's ripple is created on demand, when
         * necessary, and calling this method will force the
         * ripple to be created.
         */
        getRipple() {
          this.ensureRipple();
          assert(this.ripple_);
          return this.ripple_;
        }

        /**
         * Returns true if this element currently contains a ripple effect.
         */
        hasRipple(): boolean {
          return Boolean(this.ripple_);
        }

        /**
         * Create the element's ripple effect via creating a `<cr-ripple
         * id="ink">` instance. Override this method to customize the ripple
         * element.
         */
        createRipple(): CrRippleElement {
          const ripple = document.createElement('cr-ripple');
          ripple.id = 'ink';
          return ripple;
        }
      }

      return CrRippleMixin;
    };

export interface CrRippleMixinInterface {
  noink: boolean;
  rippleContainer: HTMLElement|null;

  createRipple(): CrRippleElement;
  ensureRipple(): void;
  ensureRippleOnPointerdown(): void;
  getRipple(): CrRippleElement;
  hasRipple(): boolean;
}