UNPKG

@zag-js/tooltip

Version:

Core logic for the tooltip widget implemented as a state machine

452 lines (450 loc) • 15.4 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/tooltip.machine.ts var tooltip_machine_exports = {}; __export(tooltip_machine_exports, { machine: () => machine }); module.exports = __toCommonJS(tooltip_machine_exports); var import_core = require("@zag-js/core"); var import_dom_query = require("@zag-js/dom-query"); var import_focus_visible = require("@zag-js/focus-visible"); var import_popper = require("@zag-js/popper"); var import_utils = require("@zag-js/utils"); var dom = __toESM(require("./tooltip.dom.js")); var import_tooltip = require("./tooltip.store.js"); var { and, not } = (0, import_core.createGuards)(); var machine = (0, import_core.createMachine)({ initialState: ({ prop }) => { const open = prop("open") || prop("defaultOpen"); return open ? "open" : "closed"; }, props({ props }) { (0, import_utils.ensureProps)(props, ["id"]); const closeOnClick = props.closeOnClick ?? true; const closeOnPointerDown = props.closeOnPointerDown ?? closeOnClick; return { openDelay: 400, closeDelay: 150, closeOnEscape: true, interactive: false, closeOnScroll: true, disabled: false, ...props, closeOnPointerDown, closeOnClick, positioning: { placement: "bottom", ...props.positioning } }; }, effects: ["trackFocusVisible", "trackStore"], context: ({ bindable, prop, scope }) => ({ currentPlacement: bindable(() => ({ defaultValue: void 0 })), hasPointerMoveOpened: bindable(() => ({ defaultValue: null })), triggerValue: bindable(() => ({ defaultValue: prop("defaultTriggerValue") ?? null, value: prop("triggerValue"), onChange(value) { const onTriggerValueChange = prop("onTriggerValueChange"); if (!onTriggerValueChange) return; const triggerElement = dom.getActiveTriggerEl(scope, value); onTriggerValueChange({ value, triggerElement }); } })) }), watch({ track, action, prop }) { track([() => prop("disabled")], () => { action(["closeIfDisabled"]); }); track([() => prop("open")], () => { action(["toggleVisibility"]); }); track([() => prop("triggerValue")], () => { action(["repositionImmediate"]); }); }, on: { "triggerValue.set": { actions: ["setTriggerValue", "repositionImmediate"] } }, states: { closed: { entry: ["clearGlobalId"], on: { "controlled.open": { target: "open" }, open: [ { guard: "isOpenControlled", actions: ["setTriggerValue", "invokeOnOpen"] }, { target: "open", actions: ["setTriggerValue", "invokeOnOpen"] } ], "pointer.leave": { actions: ["clearPointerMoveOpened"] }, "pointer.move": [ { guard: and("noVisibleTooltip", not("hasPointerMoveOpened")), target: "opening", actions: ["setTriggerValue"] }, { guard: not("hasPointerMoveOpened"), target: "open", actions: ["setPointerMoveOpened", "invokeOnOpen", "setTriggerValue"] } ] } }, opening: { effects: ["trackScroll", "trackPointerlockChange", "waitForOpenDelay"], on: { "after.openDelay": [ { guard: "isOpenControlled", actions: ["setPointerMoveOpened", "invokeOnOpen"] }, { target: "open", actions: ["setPointerMoveOpened", "invokeOnOpen"] } ], "controlled.open": { target: "open" }, "controlled.close": { target: "closed" }, open: [ { guard: "isOpenControlled", actions: ["setTriggerValue", "invokeOnOpen"] }, { target: "open", actions: ["setTriggerValue", "invokeOnOpen"] } ], "pointer.leave": [ { guard: "isOpenControlled", // We trigger toggleVisibility manually since the `ctx.open` has not changed yet (at this point) actions: ["clearPointerMoveOpened", "invokeOnClose", "toggleVisibility"] }, { target: "closed", actions: ["clearPointerMoveOpened", "invokeOnClose"] } ], close: [ { guard: "isOpenControlled", // We trigger toggleVisibility manually since the `ctx.open` has not changed yet (at this point) actions: ["invokeOnClose", "toggleVisibility"] }, { target: "closed", actions: ["invokeOnClose"] } ] } }, open: { effects: ["trackEscapeKey", "trackScroll", "trackPointerlockChange", "trackPositioning"], entry: ["setGlobalId"], on: { "controlled.close": { target: "closed" }, close: [ { guard: "isOpenControlled", actions: ["invokeOnClose"] }, { target: "closed", actions: ["invokeOnClose"] } ], "pointer.leave": [ { guard: "isVisible", target: "closing", actions: ["clearPointerMoveOpened"] }, // == group == { guard: "isOpenControlled", actions: ["clearPointerMoveOpened", "invokeOnClose"] }, { target: "closed", actions: ["clearPointerMoveOpened", "invokeOnClose"] } ], "content.pointer.leave": { guard: "isInteractive", target: "closing" }, "positioning.set": { actions: ["reposition"] }, "triggerValue.set": { // Transition to closing (which cleans up trackPositioning) then immediately back to open // This re-creates the positioning effect with the new trigger target: "closing", actions: ["setTriggerValue", "immediateReopen"] } } }, closing: { effects: ["trackPositioning", "waitForCloseDelay"], on: { "after.closeDelay": [ { guard: "isOpenControlled", actions: ["invokeOnClose"] }, { target: "closed", actions: ["invokeOnClose"] } ], "controlled.close": { target: "closed" }, "controlled.open": { target: "open" }, close: [ { guard: "isOpenControlled", actions: ["invokeOnClose"] }, { target: "closed", actions: ["invokeOnClose"] } ], "pointer.move": [ { guard: "isOpenControlled", // We trigger toggleVisibility manually since the `ctx.open` has not changed yet (at this point) actions: ["setPointerMoveOpened", "setTriggerValue", "invokeOnOpen", "toggleVisibility"] }, { target: "open", actions: ["setPointerMoveOpened", "setTriggerValue", "invokeOnOpen"] } ], "triggerValue.set": { target: "open", actions: ["setTriggerValue", "repositionImmediate"] }, reopen: { target: "open" }, "content.pointer.move": { guard: "isInteractive", target: "open" }, "positioning.set": { actions: ["reposition"] } } } }, implementations: { guards: { noVisibleTooltip: () => import_tooltip.store.get("id") === null, isVisible: ({ prop }) => prop("id") === import_tooltip.store.get("id"), isInteractive: ({ prop }) => !!prop("interactive"), hasPointerMoveOpened: ({ context }) => !!context.get("hasPointerMoveOpened"), isOpenControlled: ({ prop }) => prop("open") !== void 0 }, actions: { setGlobalId: ({ prop }) => { const prevId = import_tooltip.store.get("id"); const isInstant = prevId !== null && prevId !== prop("id"); import_tooltip.store.update({ id: prop("id"), prevId: isInstant ? prevId : null, instant: isInstant }); }, clearGlobalId: ({ prop }) => { if (prop("id") === import_tooltip.store.get("id")) { import_tooltip.store.update({ id: null, prevId: null, instant: false }); } }, invokeOnOpen: ({ prop }) => { prop("onOpenChange")?.({ open: true }); }, invokeOnClose: ({ prop }) => { prop("onOpenChange")?.({ open: false }); }, closeIfDisabled: ({ prop, send }) => { if (!prop("disabled")) return; send({ type: "close", src: "disabled.change" }); }, reposition: ({ context, event, prop, scope }) => { if (event.type !== "positioning.set") return; const getPositionerEl2 = () => dom.getPositionerEl(scope); const getTriggerEl = () => dom.getActiveTriggerEl(scope, context.get("triggerValue")); (0, import_popper.getPlacement)(getTriggerEl, getPositionerEl2, { ...prop("positioning"), ...event.options, listeners: false, onComplete(data) { context.set("currentPlacement", data.placement); } }); }, repositionImmediate: ({ context, event, prop, scope }) => { const triggerValue = event.value ?? context.get("triggerValue"); const getPositionerEl2 = () => dom.getPositionerEl(scope); const getTriggerEl = () => dom.getActiveTriggerEl(scope, triggerValue); return (0, import_popper.getPlacement)(getTriggerEl, getPositionerEl2, { ...prop("positioning"), onComplete(data) { context.set("currentPlacement", data.placement); } }); }, toggleVisibility: ({ prop, event, send }) => { queueMicrotask(() => { send({ type: prop("open") ? "controlled.open" : "controlled.close", previousEvent: event }); }); }, setPointerMoveOpened: ({ context, event }) => { const triggerId = event.triggerId ?? event.previousEvent?.triggerId; context.set("hasPointerMoveOpened", triggerId ?? null); }, clearPointerMoveOpened: ({ context }) => { context.set("hasPointerMoveOpened", null); }, setTriggerValue: ({ context, event }) => { if (event.value === void 0) return; context.set("triggerValue", event.value); }, immediateReopen: ({ send }) => { queueMicrotask(() => { send({ type: "reopen" }); }); } }, effects: { trackFocusVisible: ({ scope }) => { return (0, import_focus_visible.trackFocusVisible)({ root: scope.getRootNode?.() }); }, trackPositioning: ({ context, prop, scope }) => { if (!context.get("currentPlacement")) { context.set("currentPlacement", prop("positioning").placement); } const getPositionerEl2 = () => dom.getPositionerEl(scope); const getTriggerEl = () => dom.getActiveTriggerEl(scope, context.get("triggerValue")); return (0, import_popper.getPlacement)(getTriggerEl, getPositionerEl2, { ...prop("positioning"), defer: true, onComplete(data) { context.set("currentPlacement", data.placement); } }); }, trackPointerlockChange: ({ send, scope }) => { const doc = scope.getDoc(); const onChange = () => send({ type: "close", src: "pointerlock:change" }); return (0, import_dom_query.addDomEvent)(doc, "pointerlockchange", onChange, false); }, trackScroll: ({ send, prop, scope, context }) => { if (!prop("closeOnScroll")) return; const triggerValue = context.get("triggerValue"); const triggerEl = dom.getActiveTriggerEl(scope, triggerValue); if (!triggerEl) return; const overflowParents = (0, import_dom_query.getOverflowAncestors)(triggerEl); const cleanups = overflowParents.map((overflowParent) => { const onScroll = () => { send({ type: "close", src: "scroll" }); }; return (0, import_dom_query.addDomEvent)(overflowParent, "scroll", onScroll, { passive: true, capture: true }); }); return () => { cleanups.forEach((fn) => fn?.()); }; }, trackStore: ({ prop, send }) => { let cleanup; queueMicrotask(() => { cleanup = import_tooltip.store.subscribe(() => { if (import_tooltip.store.get("id") !== prop("id")) { send({ type: "close", src: "id.change" }); } }); }); return () => cleanup?.(); }, trackEscapeKey: ({ send, prop }) => { if (!prop("closeOnEscape")) return; const onKeyDown = (event) => { if ((0, import_dom_query.isComposingEvent)(event)) return; if (event.key !== "Escape") return; event.stopPropagation(); send({ type: "close", src: "keydown.escape" }); }; return (0, import_dom_query.addDomEvent)(document, "keydown", onKeyDown, true); }, waitForOpenDelay: ({ send, prop, event }) => { const id = setTimeout(() => { send({ type: "after.openDelay", previousEvent: event }); }, prop("openDelay")); return () => clearTimeout(id); }, waitForCloseDelay: ({ send, prop, event }) => { const id = setTimeout(() => { send({ type: "after.closeDelay", previousEvent: event }); }, prop("closeDelay")); return () => clearTimeout(id); } } } }); // Annotate the CommonJS export names for ESM import in node: 0 && (module.exports = { machine });