Tiny·Engine (Core)

Recipe

Toast

A class-based toast with autohide, animation flags, and cancellable local + global lifecycle events.

Markup

toast.html
<!-- Toast -->
<section style="margin-top: 2rem;">
  <h2>Toasts</h2>
  <button custom-toast-target="#demo-toast">Show toast</button>
  <div
    id="demo-toast"
    class="toast"
    custom-toast
    custom-toast-autohide="true"
    custom-toast-delay="3000"
    hidden
  >
    <div class="toast-body">
      <strong>Saved</strong>
      <p style="margin: 8px 0;">Your changes were stored successfully.</p>
      <button custom-dismiss="toast">Dismiss</button>
    </div>
  </div>
</section>

Capsule

toast.ts
import { Capsule, CapsuleOptions, UI, getPrefix } from "tiny-engine-core";

export interface ToastOptions extends CapsuleOptions {
  open?: boolean;
  autohide?: boolean;
  delay?: number;
  animation?: boolean;
}

export class Toast extends Capsule {
  static defaults: ToastOptions = {
    open: false,
    autohide: true,
    delay: 5000,
    animation: true
  };

  declare options: ToastOptions;

  private prefix: string;
  private timer: ReturnType<typeof setTimeout> | null = null;

  constructor(el: HTMLElement, options: ToastOptions) {
    super(el, options);
    this.prefix = (options.prefix as string | undefined) || getPrefix();

    this.onPropChange("open", () => this.syncState());
    this.on(this.el, "click", this.handleClick.bind(this));

    requestAnimationFrame(() => this.syncState());
  }

  override refresh(root: ParentNode = this.el): void {
    super.refresh(root);
    this.syncState();
  }

  show(): void {
    if (this.options.open) {
      this.restartTimer();
      return;
    }

    const detail = { element: this.el, instance: this };
    const showEvent = this.emit("show", detail, { cancelable: true });
    const busEvent = UI.emit("toast:show", detail, { cancelable: true });
    if (showEvent.defaultPrevented || busEvent.defaultPrevented) {
      return;
    }

    this.props.open = true;
  }

  hide(): void {
    if (!this.options.open) {
      return;
    }

    const detail = { element: this.el, instance: this };
    const hideEvent = this.emit("hide", detail, { cancelable: true });
    const busEvent = UI.emit("toast:hide", detail, { cancelable: true });
    if (hideEvent.defaultPrevented || busEvent.defaultPrevented) {
      return;
    }

    this.props.open = false;
  }

  toggle(): void {
    this.options.open ? this.hide() : this.show();
  }

  private syncState(): void {
    const isOpen = !!this.options.open;

    this.el.setAttribute("role", "status");
    this.el.setAttribute("aria-live", "polite");
    this.el.setAttribute("aria-atomic", "true");
    this.el.hidden = !isOpen;
    this.el.classList.toggle("show", isOpen);
    this.el.classList.toggle("is-open", isOpen);
    this.el.classList.toggle("is-animated", !!this.options.animation);

    if (isOpen) {
      this.restartTimer();
      const detail = { element: this.el, instance: this };
      this.emit("shown", detail);
      UI.emit("toast:shown", detail);
      return;
    }

    this.clearTimer();
    const detail = { element: this.el, instance: this };
    this.emit("hidden", detail);
    UI.emit("toast:hidden", detail);
  }

  private restartTimer(): void {
    this.clearTimer();

    if (!this.options.open || !this.options.autohide) {
      return;
    }

    this.timer = setTimeout(() => this.hide(), Number(this.options.delay ?? 5000));
  }

  private clearTimer(): void {
    if (!this.timer) {
      return;
    }

    clearTimeout(this.timer);
    this.timer = null;
  }

  private handleClick(event: Event): void {
    const target = event.target as HTMLElement | null;
    const close = target?.closest<HTMLElement>(
      `[data-toast-close], [${this.prefix}-dismiss="toast"]`
    );

    if (!close) {
      return;
    }

    event.preventDefault();
    this.hide();
  }

  override destroy(): void {
    this.clearTimer();
    super.destroy();
  }
}

Demo

Demo mention below: trigger opens toast, auto-hide and dismiss behavior are controlled via prefixed attributes and emitted bus events.