UNPKG

@zag-js/toast

Version:

Core logic for the toast widget implemented as a state machine

297 lines (295 loc) • 9.58 kB
"use strict"; var __create = Object.create; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __getProtoOf = Object.getPrototypeOf; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( // If the importer is in node compatibility mode or this is not an ESM // file that has been converted to a CommonJS file using a Babel- // compatible transform (i.e. "__esModule" has not been set), then set // "default" to the CommonJS "module.exports" for node compatibility. isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, mod )); var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // src/toast.machine.ts var toast_machine_exports = {}; __export(toast_machine_exports, { machine: () => machine }); module.exports = __toCommonJS(toast_machine_exports); var import_core = require("@zag-js/core"); var import_dom_query = require("@zag-js/dom-query"); var import_utils = require("@zag-js/utils"); var dom = __toESM(require("./toast.dom.js")); var import_toast = require("./toast.utils.js"); var { not } = (0, import_core.createGuards)(); var machine = (0, import_core.createMachine)({ props({ props }) { (0, import_utils.ensureProps)(props, ["id", "type", "parent", "removeDelay"], "toast"); return { closable: true, ...props, translations: { closeTriggerLabel: "Dismiss notification", ...props.translations }, duration: (0, import_toast.getToastDuration)(props.duration, props.type) }; }, initialState({ prop }) { const persist = prop("type") === "loading" || prop("duration") === Infinity; return persist ? "visible:persist" : "visible"; }, context({ prop, bindable }) { return { remainingTime: bindable(() => ({ defaultValue: (0, import_toast.getToastDuration)(prop("duration"), prop("type")) })), createdAt: bindable(() => ({ defaultValue: Date.now() })), mounted: bindable(() => ({ defaultValue: false })), initialHeight: bindable(() => ({ defaultValue: 0 })) }; }, refs() { return { closeTimerStartTime: Date.now(), lastCloseStartTimerStartTime: 0 }; }, computed: { zIndex: ({ prop }) => { const toasts = prop("parent").context.get("toasts"); const index = toasts.findIndex((toast) => toast.id === prop("id")); return toasts.length - index; }, height: ({ prop }) => { const heights = prop("parent").context.get("heights"); const height = heights.find((height2) => height2.id === prop("id")); return height?.height ?? 0; }, heightIndex: ({ prop }) => { const heights = prop("parent").context.get("heights"); return heights.findIndex((height) => height.id === prop("id")); }, frontmost: ({ prop }) => prop("index") === 0, heightBefore: ({ prop }) => { const heights = prop("parent").context.get("heights"); const heightIndex = heights.findIndex((height) => height.id === prop("id")); return heights.reduce((prev, curr, reducerIndex) => { if (reducerIndex >= heightIndex) return prev; return prev + curr.height; }, 0); }, shouldPersist: ({ prop }) => prop("type") === "loading" || prop("duration") === Infinity }, watch({ track, prop, send }) { track([() => prop("message")], () => { const message = prop("message"); if (message) send({ type: message, src: "programmatic" }); }); track([() => prop("type"), () => prop("duration")], () => { send({ type: "UPDATE" }); }); }, on: { UPDATE: [ { guard: "shouldPersist", target: "visible:persist", actions: ["resetCloseTimer"] }, { target: "visible:updating", actions: ["resetCloseTimer"] } ], MEASURE: { actions: ["measureHeight"] } }, entry: ["setMounted", "measureHeight", "invokeOnVisible"], effects: ["trackHeight"], states: { "visible:updating": { tags: ["visible", "updating"], effects: ["waitForNextTick"], on: { SHOW: { target: "visible" } } }, "visible:persist": { tags: ["visible", "paused"], on: { RESUME: { guard: not("isLoadingType"), target: "visible", actions: ["setCloseTimer"] }, DISMISS: { target: "dismissing" } } }, visible: { tags: ["visible"], effects: ["waitForDuration"], on: { DISMISS: { target: "dismissing" }, PAUSE: { target: "visible:persist", actions: ["syncRemainingTime"] } } }, dismissing: { entry: ["invokeOnDismiss"], effects: ["waitForRemoveDelay"], on: { REMOVE: { target: "unmounted", actions: ["notifyParentToRemove"] } } }, unmounted: { entry: ["invokeOnUnmount"] } }, implementations: { effects: { waitForRemoveDelay({ prop, send }) { return (0, import_utils.setRafTimeout)(() => { send({ type: "REMOVE", src: "timer" }); }, prop("removeDelay")); }, waitForDuration({ send, context, computed }) { if (computed("shouldPersist")) return; return (0, import_utils.setRafTimeout)(() => { send({ type: "DISMISS", src: "timer" }); }, context.get("remainingTime")); }, waitForNextTick({ send }) { return (0, import_utils.setRafTimeout)(() => { send({ type: "SHOW", src: "timer" }); }, 0); }, trackHeight({ scope, prop }) { let cleanup; (0, import_dom_query.raf)(() => { const rootEl = dom.getRootEl(scope); if (!rootEl) return; const syncHeight = () => { const height = measureLayoutHeight(rootEl); const item = { id: prop("id"), height }; setHeight(prop("parent"), item); }; const win = scope.getWin(); const observer = new win.MutationObserver(syncHeight); observer.observe(rootEl, { childList: true, subtree: true, characterData: true }); cleanup = () => observer.disconnect(); }); return () => cleanup?.(); } }, guards: { isLoadingType: ({ prop }) => prop("type") === "loading", shouldPersist: ({ computed }) => computed("shouldPersist") }, actions: { setMounted({ context }) { (0, import_dom_query.raf)(() => { context.set("mounted", true); }); }, measureHeight({ scope, prop, context }) { queueMicrotask(() => { const rootEl = dom.getRootEl(scope); if (!rootEl) return; const height = measureLayoutHeight(rootEl); context.set("initialHeight", height); const item = { id: prop("id"), height }; setHeight(prop("parent"), item); }); }, setCloseTimer({ refs }) { refs.set("closeTimerStartTime", Date.now()); }, resetCloseTimer({ context, refs, prop }) { refs.set("closeTimerStartTime", Date.now()); context.set("remainingTime", (0, import_toast.getToastDuration)(prop("duration"), prop("type"))); }, syncRemainingTime({ context, refs }) { context.set("remainingTime", (prev) => { const closeTimerStartTime = refs.get("closeTimerStartTime"); const elapsedTime = Date.now() - closeTimerStartTime; refs.set("lastCloseStartTimerStartTime", Date.now()); return prev - elapsedTime; }); }, notifyParentToRemove({ prop }) { const parent = prop("parent"); parent.send({ type: "TOAST.REMOVE", id: prop("id") }); }, invokeOnDismiss({ prop, event }) { prop("onStatusChange")?.({ status: "dismissing", src: event.src }); }, invokeOnUnmount({ prop }) { prop("onStatusChange")?.({ status: "unmounted" }); }, invokeOnVisible({ prop }) { prop("onStatusChange")?.({ status: "visible" }); } } } }); function measureLayoutHeight(el) { const prevHeight = el.style.height; el.style.height = "auto"; const height = el.offsetHeight; el.style.height = prevHeight; return height; } function setHeight(parent, item) { const { id, height } = item; parent.context.set("heights", (prev) => { const alreadyExists = prev.find((i) => i.id === id); if (!alreadyExists) { return [{ id, height }, ...prev]; } else { return prev.map((i) => i.id === id ? { ...i, height } : i); } }); } // Annotate the CommonJS export names for ESM import in node: 0 && (module.exports = { machine });