@zag-js/toast
Version:
Core logic for the toast widget implemented as a state machine
297 lines (295 loc) • 9.58 kB
JavaScript
;
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
});