@zag-js/presence
Version:
Core logic for the presence widget implemented as a state machine
183 lines (179 loc) • 5.54 kB
JavaScript
;
var domQuery = require('@zag-js/dom-query');
var core = require('@zag-js/core');
var types = require('@zag-js/types');
// src/presence.connect.ts
function connect(service, _normalize) {
const { state, send, context } = service;
const present = state.matches("mounted", "unmountSuspended");
return {
skip: !context.get("initial"),
present,
setNode(node) {
if (!node) return;
send({ type: "NODE.SET", node });
},
unmount() {
send({ type: "UNMOUNT" });
}
};
}
var machine = core.createMachine({
props({ props: props2 }) {
return { ...props2, present: !!props2.present };
},
initialState({ prop }) {
return prop("present") ? "mounted" : "unmounted";
},
refs() {
return {
node: null,
styles: null
};
},
context({ bindable }) {
return {
unmountAnimationName: bindable(() => ({ defaultValue: null })),
prevAnimationName: bindable(() => ({ defaultValue: null })),
present: bindable(() => ({ defaultValue: false })),
initial: bindable(() => ({
sync: true,
defaultValue: false
}))
};
},
exit: ["clearInitial", "cleanupNode"],
watch({ track, action, prop }) {
track([() => prop("present")], () => {
action(["setInitial", "syncPresence"]);
});
},
on: {
"NODE.SET": {
actions: ["setNode", "setStyles"]
}
},
states: {
mounted: {
on: {
UNMOUNT: {
target: "unmounted",
actions: ["clearPrevAnimationName", "invokeOnExitComplete"]
},
"UNMOUNT.SUSPEND": {
target: "unmountSuspended"
}
}
},
unmountSuspended: {
effects: ["trackAnimationEvents"],
on: {
MOUNT: {
target: "mounted",
actions: ["setPrevAnimationName"]
},
UNMOUNT: {
target: "unmounted",
actions: ["clearPrevAnimationName", "invokeOnExitComplete"]
}
}
},
unmounted: {
on: {
MOUNT: {
target: "mounted",
actions: ["setPrevAnimationName"]
}
}
}
},
implementations: {
actions: {
setInitial: ({ context }) => {
if (context.get("initial")) return;
queueMicrotask(() => {
context.set("initial", true);
});
},
clearInitial: ({ context }) => {
context.set("initial", false);
},
cleanupNode: ({ refs }) => {
refs.set("node", null);
refs.set("styles", null);
},
invokeOnExitComplete: ({ prop }) => {
prop("onExitComplete")?.();
},
setNode: ({ refs, event }) => {
refs.set("node", event.node);
},
setStyles: ({ refs, event }) => {
refs.set("styles", domQuery.getComputedStyle(event.node));
},
syncPresence: ({ context, refs, send, prop }) => {
const presentProp = prop("present");
if (presentProp) {
return send({ type: "MOUNT", src: "presence.changed" });
}
const node = refs.get("node");
if (!presentProp && node?.ownerDocument.visibilityState === "hidden") {
return send({ type: "UNMOUNT", src: "visibilitychange" });
}
domQuery.raf(() => {
const animationName = getAnimationName(refs.get("styles"));
context.set("unmountAnimationName", animationName);
if (animationName === "none" || animationName === context.get("prevAnimationName") || refs.get("styles")?.display === "none" || refs.get("styles")?.animationDuration === "0s") {
send({ type: "UNMOUNT", src: "presence.changed" });
} else {
send({ type: "UNMOUNT.SUSPEND" });
}
});
},
setPrevAnimationName: ({ context, refs }) => {
domQuery.raf(() => {
context.set("prevAnimationName", getAnimationName(refs.get("styles")));
});
},
clearPrevAnimationName: ({ context }) => {
context.set("prevAnimationName", null);
}
},
effects: {
trackAnimationEvents: ({ context, refs, send }) => {
const node = refs.get("node");
if (!node) return;
const onStart = (event) => {
const target = event.composedPath?.()?.[0] ?? event.target;
if (target === node) {
context.set("prevAnimationName", getAnimationName(refs.get("styles")));
}
};
const onEnd = (event) => {
const animationName = getAnimationName(refs.get("styles"));
const target = domQuery.getEventTarget(event);
if (target === node && animationName === context.get("unmountAnimationName")) {
send({ type: "UNMOUNT", src: "animationend" });
}
};
node.addEventListener("animationstart", onStart);
node.addEventListener("animationcancel", onEnd);
node.addEventListener("animationend", onEnd);
const cleanupStyles = domQuery.setStyle(node, { animationFillMode: "forwards" });
return () => {
node.removeEventListener("animationstart", onStart);
node.removeEventListener("animationcancel", onEnd);
node.removeEventListener("animationend", onEnd);
domQuery.nextTick(() => cleanupStyles());
};
}
}
}
});
function getAnimationName(styles) {
return styles?.animationName || "none";
}
var props = types.createProps()(["onExitComplete", "present", "immediate"]);
exports.connect = connect;
exports.machine = machine;
exports.props = props;