UNPKG

@zag-js/presence

Version:

Core logic for the presence widget implemented as a state machine

183 lines (179 loc) • 5.54 kB
'use strict'; 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;