Recipe
Toast
A class-based toast with autohide, animation flags, and cancellable local + global lifecycle events.
Markup
<!-- 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
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.