chromium/ui/file_manager/file_manager/foreground/elements/files_toast.ts

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

import 'chrome://resources/ash/common/cr_elements/cr_shared_style.css.js';

import type {CrButtonElement} from 'chrome://resources/ash/common/cr_elements/cr_button/cr_button.js';
import type {CrToastElement} from 'chrome://resources/ash/common/cr_elements/cr_toast/cr_toast.js';
import {PolymerElement} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';

import {getTemplate} from './files_toast.html.js';

interface FilesToastAction {
  text: string;
  callback?: () => void;
}

interface FilesToastData {
  text: string;
  action?: FilesToastAction;
}

export interface FilesToast {
  $: {
    container: CrToastElement,
    text: HTMLDivElement,
    action: CrButtonElement,
  };
}

/**
 * Files Toast.
 *
 * The toast is shown at the bottom-right in LTR, bottom-left in RTL. Usage:
 *
 * toast.show('Toast without action.');
 * toast.show('Toast with action', {text: 'Action', callback:function(){}});
 * toast.hide();
 */
export class FilesToast extends PolymerElement {
  static get is() {
    return 'files-toast';
  }

  static get template() {
    return getTemplate();
  }

  static get properties() {
    return {
      visible: {
        type: Boolean,
        value: false,
      },

    };
  }

  private action_: FilesToastAction|null = null;
  private queue_: FilesToastData[] = [];
  visible: boolean = false;

  override connectedCallback() {
    super.connectedCallback();
    this.$.container.ontransitionend = this.onTransitionEnd_.bind(this);
  }

  /**
   * Shows toast. If a toast is already shown, add the toast to the pending
   * queue. It will shown later when other toasts have completed.
   *
   * @param text Text of toast.
   * @param action Action. The |Action.callback| is called if the user taps or
   *     clicks the action button.
   */
  show(text: string, action?: FilesToastAction) {
    if (this.visible) {
      this.queue_.push({text: text, action: action});
      return;
    }

    this.visible = true;

    this.$.text.innerText = text;
    this.action_ = action || null;
    if (this.action_) {
      this.$.text.setAttribute('style', 'margin-inline-end: 0');
      this.$.action.innerText = this.action_.text;
      this.$.action.hidden = false;
    } else {
      this.$.text.removeAttribute('style');
      this.$.action.innerText = '';
      this.$.action.hidden = true;
    }

    this.$.container.show();
  }

  /** Handles action button tap/click. */
  protected onActionClicked_() {
    if (this.action_ && this.action_.callback) {
      this.action_.callback();
      this.hide();
    }
  }

  /**
   * Handles the <cr-toast> transitionend event. On a hide transition, show
   * the next queued toast if any.
   */
  private onTransitionEnd_() {
    const hide = !this.$.container.open;

    if (hide && this.visible) {
      this.visible = false;

      if (this.queue_.length > 0) {
        const next = this.queue_.shift()!;
        setTimeout(this.show.bind(this), 0, next.text, next.action);
      }
    }
  }

  /**
   * Hides toast if visible.
   */
  hide() {
    if (this.visible) {
      this.$.container.hide();
    }
  }
}

declare global {
  interface HTMLElementTagNameMap {
    'files-toast': FilesToast;
  }
}

customElements.define(FilesToast.is, FilesToast);