UNPKG

@zag-js/tour

Version:

Core logic for the tour widget implemented as a state machine

983 lines (975 loc) 31 kB
import { createAnatomy } from '@zag-js/anatomy'; import { createGuards, createMachine, mergeProps } from '@zag-js/core'; import { isHTMLElement, contains, raf, getComputedStyle as getComputedStyle$1, getWindow, dataAttr, getDocument } from '@zag-js/dom-query'; import { getPlacement, getPlacementStyles } from '@zag-js/popper'; import { trackDismissableBranch } from '@zag-js/dismissable'; import { trapFocus } from '@zag-js/focus-trap'; import { trackInteractOutside } from '@zag-js/interact-outside'; import { prevIndex, nextIndex, isString, isEqual, createSplitProps } from '@zag-js/utils'; import { createProps } from '@zag-js/types'; // src/tour.anatomy.ts var anatomy = createAnatomy("tour").parts( "content", "actionTrigger", "closeTrigger", "progressText", "title", "description", "positioner", "arrow", "arrowTip", "backdrop", "spotlight" ); var parts = anatomy.build(); // src/tour.dom.ts var getPositionerId = (ctx) => ctx.ids?.positioner ?? `tour-positioner-${ctx.id}`; var getContentId = (ctx) => ctx.ids?.content ?? `tour-content-${ctx.id}`; var getTitleId = (ctx) => ctx.ids?.title ?? `tour-title-${ctx.id}`; var getDescriptionId = (ctx) => ctx.ids?.description ?? `tour-desc-${ctx.id}`; var getArrowId = (ctx) => ctx.ids?.arrow ?? `tour-arrow-${ctx.id}`; var getBackdropId = (ctx) => ctx.ids?.backdrop ?? `tour-backdrop-${ctx.id}`; var getContentEl = (ctx) => ctx.getById(getContentId(ctx)); var getPositionerEl = (ctx) => ctx.getById(getPositionerId(ctx)); var getBackdropEl = (ctx) => ctx.getById(getBackdropId(ctx)); // src/utils/clip-path.ts function getClipPath(options) { const { radius = 0, rootSize: { width: w, height: h }, rect: { width, height, x, y }, enabled = true } = options; if (!enabled) return ""; const { topLeft = 0, topRight = 0, bottomRight = 0, bottomLeft = 0 } = typeof radius === "number" ? { topLeft: radius, topRight: radius, bottomRight: radius, bottomLeft: radius } : radius; return `M${w},${h} H0 V0 H${w} V${h} Z M${x + topLeft},${y} a${topLeft},${topLeft},0,0,0-${topLeft},${topLeft} V${height + y - bottomLeft} a${bottomLeft},${bottomLeft},0,0,0,${bottomLeft},${bottomLeft} H${width + x - bottomRight} a${bottomRight},${bottomRight},0,0,0,${bottomRight}-${bottomRight} V${y + topRight} a${topRight},${topRight},0,0,0-${topRight}-${topRight} Z`; } // src/utils/step.ts var isTooltipStep = (step) => { return step?.type === "tooltip"; }; var isDialogStep = (step) => { return step?.type === "dialog"; }; var isTooltipPlacement = (placement) => { return placement != null && placement != "center"; }; var normalizeStep = (step) => { if (step.type === "floating") { return { backdrop: false, arrow: false, placement: "bottom-end", ...step }; } if (step.target == null || step.type === "dialog") { return { type: "dialog", placement: "center", backdrop: true, ...step }; } if (!step.type || step.type === "tooltip") { return { type: "tooltip", arrow: true, backdrop: true, ...step }; } return step; }; var findStep = (steps, id) => { const res = id != null ? steps.find((step) => step.id === id) : null; return res ? normalizeStep(res) : null; }; var findStepIndex = (steps, id) => { return id != null ? steps.findIndex((step) => step.id === id) : -1; }; // src/tour.connect.ts function connect(service, normalize) { const { state, context, computed, send, prop, scope } = service; const open = state.hasTag("open"); const steps = Array.from(context.get("steps")); const stepIndex = computed("stepIndex"); const step = computed("step"); const hasTarget = typeof step?.target?.() !== "undefined"; const hasNextStep = computed("hasNextStep"); const hasPrevStep = computed("hasPrevStep"); const firstStep = computed("isFirstStep"); const lastStep = computed("isLastStep"); const placement = context.get("currentPlacement"); const targetRect = context.get("targetRect"); const popperStyles = getPlacementStyles({ strategy: "absolute", placement: isTooltipPlacement(placement) ? placement : void 0 }); const clipPath = getClipPath({ enabled: isTooltipStep(step), rect: targetRect, rootSize: context.get("boundarySize"), radius: prop("spotlightRadius") }); const actionMap = { next() { send({ type: "STEP.NEXT", src: "actionTrigger" }); }, prev() { send({ type: "STEP.PREV", src: "actionTrigger" }); }, dismiss() { send({ type: "DISMISS", src: "actionTrigger" }); }, goto(id) { send({ type: "STEP.SET", value: id, src: "actionTrigger" }); } }; return { open, totalSteps: steps.length, stepIndex, step, hasNextStep, hasPrevStep, firstStep, lastStep, addStep(step2) { const next = steps.concat(step2); send({ type: "STEPS.SET", value: next, src: "addStep" }); }, removeStep(id) { const next = steps.filter((step2) => step2.id !== id); send({ type: "STEPS.SET", value: next, src: "removeStep" }); }, updateStep(id, stepOverrides) { const next = steps.map((step2) => step2.id === id ? mergeProps(step2, stepOverrides) : step2); send({ type: "STEPS.SET", value: next, src: "updateStep" }); }, setSteps(steps2) { send({ type: "STEPS.SET", value: steps2, src: "setSteps" }); }, setStep(id) { send({ type: "STEP.SET", value: id }); }, start(id) { send({ type: "START", id }); }, isValidStep(id) { return steps.some((step2) => step2.id === id); }, isCurrentStep(id) { return Boolean(step?.id === id); }, next() { send({ type: "STEP.NEXT" }); }, prev() { send({ type: "STEP.PREV" }); }, getProgressPercent() { return stepIndex / steps.length * 100; }, getProgressText() { const effectiveSteps = steps.filter((step2) => step2.type !== "wait"); const index = findStepIndex(effectiveSteps, step?.id); const details = { current: index, total: effectiveSteps.length }; return prop("translations").progressText?.(details) ?? ""; }, getBackdropProps() { return normalize.element({ ...parts.backdrop.attrs, id: getBackdropId(scope), dir: prop("dir"), hidden: !open, "data-state": open ? "open" : "closed", "data-type": step?.type, style: { "--tour-layer": 0, clipPath: isTooltipStep(step) ? `path("${clipPath}")` : void 0, position: "absolute", inset: "0", willChange: "clip-path" } }); }, getSpotlightProps() { return normalize.element({ ...parts.spotlight.attrs, hidden: !open || !step?.target?.(), style: { "--tour-layer": 1, position: "absolute", width: `${targetRect.width}px`, height: `${targetRect.height}px`, left: `${targetRect.x}px`, top: `${targetRect.y}px`, borderRadius: `${prop("spotlightRadius")}px`, pointerEvents: "none" } }); }, getProgressTextProps() { return normalize.element({ ...parts.progressText.attrs }); }, getPositionerProps() { return normalize.element({ ...parts.positioner.attrs, dir: prop("dir"), id: getPositionerId(scope), "data-type": step?.type, "data-placement": placement, style: { "--tour-layer": 2, ...step?.type === "tooltip" && popperStyles.floating } }); }, getArrowProps() { return normalize.element({ id: getArrowId(scope), ...parts.arrow.attrs, dir: prop("dir"), hidden: step?.type !== "tooltip", style: step?.type === "tooltip" ? popperStyles.arrow : void 0, opacity: hasTarget ? void 0 : 0 }); }, getArrowTipProps() { return normalize.element({ ...parts.arrowTip.attrs, dir: prop("dir"), style: popperStyles.arrowTip }); }, getContentProps() { return normalize.element({ ...parts.content.attrs, id: getContentId(scope), dir: prop("dir"), role: "alertdialog", "aria-modal": "true", "aria-live": "polite", "aria-atomic": "true", hidden: !open, "data-state": open ? "open" : "closed", "data-type": step?.type, "data-placement": placement, "data-step": step?.id, "aria-labelledby": getTitleId(scope), "aria-describedby": getDescriptionId(scope), tabIndex: -1, onKeyDown(event) { if (event.defaultPrevented) return; if (!prop("keyboardNavigation")) return; const isRtl = prop("dir") === "rtl"; switch (event.key) { case "ArrowRight": if (!hasNextStep) return; send({ type: isRtl ? "STEP.PREV" : "STEP.NEXT", src: "keydown" }); break; case "ArrowLeft": if (!hasPrevStep) return; send({ type: isRtl ? "STEP.NEXT" : "STEP.PREV", src: "keydown" }); break; } } }); }, getTitleProps() { return normalize.element({ ...parts.title.attrs, id: getTitleId(scope), "data-placement": hasTarget ? placement : "center" }); }, getDescriptionProps() { return normalize.element({ ...parts.description.attrs, id: getDescriptionId(scope), "data-placement": hasTarget ? placement : "center" }); }, getCloseTriggerProps() { return normalize.element({ ...parts.closeTrigger.attrs, "data-type": step?.type, "aria-label": prop("translations").close, onClick: actionMap.dismiss }); }, getActionTriggerProps(props2) { const { action, attrs } = props2.action; let actionProps = {}; switch (action) { case "next": actionProps = { "data-type": "next", disabled: !hasNextStep, "data-disabled": dataAttr(!hasNextStep), "aria-label": prop("translations").nextStep, onClick: actionMap.next }; break; case "prev": actionProps = { "data-type": "prev", disabled: !hasPrevStep, "data-disabled": dataAttr(!hasPrevStep), "aria-label": prop("translations").prevStep, onClick: actionMap.prev }; break; case "dismiss": actionProps = { "data-type": "close", "aria-label": prop("translations").close, onClick: actionMap.dismiss }; break; default: actionProps = { "data-type": "custom", onClick() { if (typeof action === "function") { action(actionMap); } } }; break; } return normalize.button({ ...parts.actionTrigger.attrs, type: "button", ...attrs, ...actionProps }); } }; } function getFrameElement(win) { return win.parent && Object.getPrototypeOf(win.parent) ? win.frameElement : null; } var normalizeEventPoint = (event) => { let clientX = event.clientX; let clientY = event.clientY; let win = event.view || window; let frame = getFrameElement(win); while (frame) { const iframeRect = frame.getBoundingClientRect(); const css = getComputedStyle(frame); const left = iframeRect.left + (frame.clientLeft + parseFloat(css.paddingLeft)); const top = iframeRect.top + (frame.clientTop + parseFloat(css.paddingTop)); clientX += left; clientY += top; win = getWindow(frame); frame = getFrameElement(win); } return { clientX, clientY }; }; function isEventInRect(rect, event) { const { clientX, clientY } = normalizeEventPoint(event); return rect.y <= clientY && clientY <= rect.y + rect.height && rect.x <= clientX && clientX <= rect.x + rect.width; } function offset(r, i) { const dx = i.x || 0; const dy = i.y || 0; return { x: r.x - dx, y: r.y - dy, width: r.width + dx + dx, height: r.height + dy + dy }; } // src/tour.machine.ts var { and } = createGuards(); var getEffectiveSteps = (steps) => steps.filter((step) => step.type !== "wait"); var getProgress = (steps, stepIndex) => { const effectiveLength = getEffectiveSteps(steps).length; return (stepIndex + 1) / effectiveLength; }; var machine = createMachine({ props({ props: props2 }) { return { preventInteraction: false, closeOnInteractOutside: true, closeOnEscape: true, keyboardNavigation: true, spotlightOffset: { x: 10, y: 10 }, spotlightRadius: 4, ...props2, translations: { nextStep: "next step", prevStep: "previous step", close: "close tour", progressText: ({ current, total }) => `${current + 1} of ${total}`, skip: "skip tour", ...props2.translations } }; }, initialState() { return "tour.inactive"; }, context({ prop, bindable, getContext }) { return { steps: bindable(() => ({ defaultValue: prop("steps") ?? [], onChange(value) { prop("onStepsChange")?.({ steps: value }); } })), stepId: bindable(() => ({ defaultValue: prop("stepId"), sync: true, onChange(value) { const context = getContext(); const steps = context.get("steps"); const stepIndex = findStepIndex(steps, value); const progress = getProgress(steps, stepIndex); const complete = stepIndex == steps.length - 1; prop("onStepChange")?.({ stepId: value, stepIndex, totalSteps: steps.length, complete, progress }); } })), resolvedTarget: bindable(() => ({ sync: true, defaultValue: null })), targetRect: bindable(() => ({ defaultValue: { width: 0, height: 0, x: 0, y: 0 } })), boundarySize: bindable(() => ({ defaultValue: { width: 0, height: 0 } })), currentPlacement: bindable(() => ({ defaultValue: void 0 })) }; }, computed: { stepIndex: ({ context }) => findStepIndex(context.get("steps"), context.get("stepId")), step: ({ context }) => findStep(context.get("steps"), context.get("stepId")), hasNextStep: ({ context, computed }) => computed("stepIndex") < context.get("steps").length - 1, hasPrevStep: ({ computed }) => computed("stepIndex") > 0, isFirstStep: ({ computed }) => computed("stepIndex") === 0, isLastStep: ({ context, computed }) => computed("stepIndex") === context.get("steps").length - 1, progress: ({ context, computed }) => { const effectiveLength = getEffectiveSteps(context.get("steps")).length; return (computed("stepIndex") + 1) / effectiveLength; } }, watch({ track, context, action }) { track([() => context.get("stepId")], () => { queueMicrotask(() => { action(["setResolvedTarget", "raiseStepChange", "syncTargetAttrs"]); }); }); }, effects: ["trackBoundarySize"], exit: ["cleanupRefs"], on: { "STEPS.SET": { actions: ["setSteps"] }, "STEP.SET": { actions: ["setStep"] }, "STEP.NEXT": { actions: ["setNextStep"] }, "STEP.PREV": { actions: ["setPrevStep"] }, "STEP.CHANGED": [ { guard: and("isValidStep", "hasResolvedTarget"), target: "target.scrolling", actions: ["cleanupRefs"] }, { guard: and("isValidStep", "hasTarget"), target: "target.resolving", actions: ["cleanupRefs"] }, { guard: and("isValidStep", "isWaitingStep"), target: "step.waiting", actions: ["cleanupRefs"] }, { guard: "isValidStep", target: "tour.active", actions: ["cleanupRefs"] } ], DISMISS: [ { guard: "isLastStep", target: "tour.inactive", actions: ["invokeOnDismiss", "invokeOnComplete", "clearStep"] }, { target: "tour.inactive", actions: ["invokeOnDismiss", "clearStep"] } ] }, states: { "tour.inactive": { tags: ["closed"], on: { START: { actions: ["setInitialStep", "invokeOnStart"] } } }, "target.resolving": { tags: ["closed"], effects: ["waitForTarget", "waitForTargetTimeout"], on: { "TARGET.NOT_FOUND": { target: "tour.inactive", actions: ["invokeOnNotFound", "clearStep"] }, "TARGET.RESOLVED": { target: "target.scrolling", actions: ["setResolvedTarget"] } } }, "target.scrolling": { tags: ["open"], entry: ["scrollToTarget"], effects: [ "waitForScrollEnd", "trapFocus", "trackPlacement", "trackDismissableBranch", "trackInteractOutside", "trackEscapeKeydown" ], on: { "SCROLL.END": { target: "tour.active" } } }, "step.waiting": { tags: ["closed"] }, "tour.active": { tags: ["open"], effects: ["trapFocus", "trackPlacement", "trackDismissableBranch", "trackInteractOutside", "trackEscapeKeydown"] } }, implementations: { guards: { isLastStep: ({ computed, context }) => computed("stepIndex") === context.get("steps").length - 1, isValidStep: ({ context }) => context.get("stepId") != null, hasTarget: ({ computed }) => computed("step")?.target != null, hasResolvedTarget: ({ context }) => context.get("resolvedTarget") != null, isWaitingStep: ({ computed }) => computed("step")?.type === "wait" }, actions: { scrollToTarget({ context }) { const node = context.get("resolvedTarget"); node?.scrollIntoView({ behavior: "instant", block: "center", inline: "center" }); }, setStep(params) { const { event } = params; setStep(params, event.value); }, clearStep(params) { const { context } = params; context.set("targetRect", { width: 0, height: 0, x: 0, y: 0 }); setStep(params, -1); }, setInitialStep(params) { const { context, event } = params; const steps = context.get("steps"); if (steps.length === 0) return; if (isString(event.value)) { const idx = findStepIndex(steps, event.value); setStep(params, idx); return; } setStep(params, 0); }, setNextStep(params) { const { context, computed } = params; const steps = context.get("steps"); const idx = nextIndex(steps, computed("stepIndex")); setStep(params, idx); }, setPrevStep(params) { const { context, computed } = params; const steps = context.get("steps"); const idx = prevIndex(steps, computed("stepIndex")); setStep(params, idx); }, invokeOnStart({ prop, context, computed }) { prop("onStatusChange")?.({ status: "started", stepId: context.get("stepId"), stepIndex: computed("stepIndex") }); }, invokeOnDismiss({ prop, context, computed }) { prop("onStatusChange")?.({ status: "dismissed", stepId: context.get("stepId"), stepIndex: computed("stepIndex") }); }, invokeOnComplete({ prop, context, computed }) { prop("onStatusChange")?.({ status: "completed", stepId: context.get("stepId"), stepIndex: computed("stepIndex") }); }, invokeOnSkip({ prop, context, computed }) { prop("onStatusChange")?.({ status: "skipped", stepId: context.get("stepId"), stepIndex: computed("stepIndex") }); }, invokeOnNotFound({ prop, context, computed }) { prop("onStatusChange")?.({ status: "not-found", stepId: context.get("stepId"), stepIndex: computed("stepIndex") }); }, raiseStepChange({ send }) { send({ type: "STEP.CHANGED" }); }, setResolvedTarget({ context, event, computed }) { const node = event.node ?? computed("step")?.target?.(); context.set("resolvedTarget", node ?? null); }, syncTargetAttrs({ context, refs, prop }) { refs.get("_targetCleanup")?.(); refs.set("_targetCleanup", void 0); const targetEl = context.get("resolvedTarget"); if (!targetEl) return; if (prop("preventInteraction")) targetEl.inert = true; targetEl.setAttribute("data-tour-highlighted", ""); refs.set("_targetCleanup", () => { if (prop("preventInteraction")) targetEl.inert = false; targetEl.removeAttribute("data-tour-highlighted"); }); }, cleanupRefs({ refs }) { refs.get("_targetCleanup")?.(); refs.set("_targetCleanup", void 0); refs.get("_effectCleanup")?.(); refs.set("_effectCleanup", void 0); }, validateSteps({ context }) { const ids = /* @__PURE__ */ new Set(); context.get("steps").forEach((step) => { if (ids.has(step.id)) { throw new Error(`[zag-js/tour] Duplicate step id: ${step.id}`); } if (step.target == null && step.type == null) { throw new Error(`[zag-js/tour] Step ${step.id} has no target or type. At least one of those is required.`); } ids.add(step.id); }); } }, effects: { waitForScrollEnd({ send }) { const id = setTimeout(() => { send({ type: "SCROLL.END" }); }, 100); return () => clearTimeout(id); }, waitForTargetTimeout({ send }) { const id = setTimeout(() => { send({ type: "TARGET.NOT_FOUND" }); }, 3e3); return () => clearTimeout(id); }, waitForTarget({ scope, computed, send }) { const step = computed("step"); if (!step) return; const targetEl = step.target; const win = scope.getWin(); const rootNode = scope.getRootNode(); const observer = new win.MutationObserver(() => { const node = targetEl?.(); if (node) { send({ type: "TARGET.RESOLVED", node }); observer.disconnect(); } }); observer.observe(rootNode, { childList: true, subtree: true, characterData: true }); return () => { observer.disconnect(); }; }, trackBoundarySize({ context, scope }) { const win = scope.getWin(); const doc = scope.getDoc(); const onResize = () => { const width = visualViewport?.width ?? win.innerWidth; const height = doc.documentElement.scrollHeight; context.set("boundarySize", { width, height }); }; onResize(); const viewport = win.visualViewport ?? win; viewport.addEventListener("resize", onResize); return () => viewport.removeEventListener("resize", onResize); }, trackEscapeKeydown({ scope, send, prop }) { if (!prop("closeOnEscape")) return; const doc = scope.getDoc(); const onKeyDown = (event) => { if (event.key === "Escape") { event.preventDefault(); event.stopPropagation(); send({ type: "DISMISS", src: "esc" }); } }; doc.addEventListener("keydown", onKeyDown, true); return () => { doc.removeEventListener("keydown", onKeyDown, true); }; }, trackInteractOutside({ context, computed, scope, send, prop }) { const step = computed("step"); if (step == null) return; const contentEl = () => getContentEl(scope); return trackInteractOutside(contentEl, { defer: true, exclude(target) { return contains(step.target?.(), target); }, onFocusOutside(event) { prop("onFocusOutside")?.(event); if (!prop("closeOnInteractOutside")) { event.preventDefault(); } }, onPointerDownOutside(event) { prop("onPointerDownOutside")?.(event); const isWithin = isEventInRect(context.get("targetRect"), event.detail.originalEvent); if (isWithin) { event.preventDefault(); return; } if (!prop("closeOnInteractOutside")) { event.preventDefault(); } }, onInteractOutside(event) { prop("onInteractOutside")?.(event); if (event.defaultPrevented) return; send({ type: "DISMISS", src: "interact-outside" }); } }); }, trackDismissableBranch({ computed, scope }) { const step = computed("step"); if (step == null) return; const contentEl = () => getContentEl(scope); return trackDismissableBranch(contentEl, { defer: !contentEl() }); }, trapFocus({ computed, scope }) { const step = computed("step"); if (step == null) return; const contentEl = () => getContentEl(scope); return trapFocus(contentEl, { escapeDeactivates: false, allowOutsideClick: true, preventScroll: true, returnFocusOnDeactivate: false }); }, trackPlacement({ context, computed, scope, prop }) { const step = computed("step"); if (step == null) return; context.set("currentPlacement", step.placement ?? "bottom"); if (isDialogStep(step)) { return syncZIndex(scope); } if (!isTooltipStep(step)) { return; } const positionerEl = () => getPositionerEl(scope); return getPlacement(context.get("resolvedTarget"), positionerEl, { defer: true, placement: step.placement ?? "bottom", strategy: "absolute", gutter: 10, offset: step.offset, getAnchorRect(el) { if (!isHTMLElement(el)) return null; const rect = el.getBoundingClientRect(); return offset(rect, prop("spotlightOffset")); }, onComplete(data) { const { rects } = data.middlewareData; context.set("currentPlacement", data.placement); context.set("targetRect", rects.reference); } }); } } } }); function syncZIndex(scope) { return raf(() => { const contentEl = getContentEl(scope); if (!contentEl) return; const styles = getComputedStyle$1(contentEl); const positionerEl = getPositionerEl(scope); const backdropEl = getBackdropEl(scope); if (positionerEl) { positionerEl.style.setProperty("--z-index", styles.zIndex); positionerEl.style.setProperty("z-index", "var(--z-index)"); } if (backdropEl) { backdropEl.style.setProperty("--z-index", styles.zIndex); } }); } function setStep(params, idx) { const { context, refs, computed, prop } = params; const steps = context.get("steps"); const step = steps[idx]; if (!step) { context.set("stepId", null); return; } if (isEqual(context.get("stepId"), step.id)) return; const update = (data) => { context.set("steps", (prev) => prev.map((s, i) => i === idx ? { ...s, ...data } : s)); }; const next = () => { const idx2 = nextIndex(steps, computed("stepIndex")); context.set("stepId", steps[idx2].id); }; const goto = (id) => { const step2 = findStep(steps, id); if (!step2) return; context.set("stepId", step2.id); }; const dismiss = () => { context.set("stepId", null); prop("onStatusChange")?.({ status: "dismissed", stepId: null, stepIndex: -1 }); }; const show = () => { context.set("stepId", step.id); }; if (!step.effect) { show(); return; } const cleanup = step.effect({ show, next, update, target: step.target, dismiss, goto }); refs.set("_effectCleanup", cleanup); } var props = createProps()([ "closeOnEscape", "closeOnInteractOutside", "dir", "getRootNode", "id", "ids", "keyboardNavigation", "onFocusOutside", "onInteractOutside", "onPointerDownOutside", "onStatusChange", "onStepChange", "onStepsChange", "preventInteraction", "spotlightOffset", "spotlightRadius", "stepId", "steps", "translations" ]); var splitProps = createSplitProps(props); function waitForPromise(promise, controller, timeout) { const { signal } = controller; const wrappedPromise = new Promise((resolve, reject) => { const timeoutId = setTimeout(() => { reject(new Error(`Timeout of ${timeout}ms exceeded`)); }, timeout); signal.addEventListener("abort", () => { clearTimeout(timeoutId); reject(new Error("Promise aborted")); }); promise.then((result) => { if (!signal.aborted) { clearTimeout(timeoutId); resolve(result); } }).catch((error) => { if (!signal.aborted) { clearTimeout(timeoutId); reject(error); } }); }); const abort = () => controller.abort(); return [wrappedPromise, abort]; } function waitForElement(target, options) { const { timeout, rootNode } = options; const win = getWindow(rootNode); const doc = getDocument(rootNode); const controller = new win.AbortController(); return waitForPromise( new Promise((resolve) => { const el = target(); if (el) { resolve(el); return; } const observer = new win.MutationObserver(() => { const el2 = target(); if (el2) { observer.disconnect(); resolve(el2); } }); observer.observe(doc.body, { childList: true, subtree: true }); }), controller, timeout ); } function waitForElementValue(target, value, options) { const { timeout, rootNode } = options; const win = getWindow(rootNode); const controller = new win.AbortController(); return waitForPromise( new Promise((resolve) => { const el = target(); if (!el) return; const checkValue = () => { if (el.value === value) { resolve(); el.removeEventListener("input", checkValue); } }; checkValue(); el.addEventListener("input", checkValue, { signal: controller.signal }); }), controller, timeout ); } export { anatomy, connect, machine, props, splitProps, waitForElement, waitForElementValue };