UNPKG

@zag-js/react

Version:

The react wrapper for zag

346 lines (340 loc) • 10.1 kB
"use client"; import { createScope, MachineStatus, INIT_STATE } from '@zag-js/core'; export { mergeProps } from '@zag-js/core'; import { compact, ensure, isFunction, warn, toArray, isString, identity } from '@zag-js/utils'; import * as React from 'react'; import { useMemo, useRef, useLayoutEffect, useEffect, useState } from 'react'; import { flushSync, createPortal } from 'react-dom'; import { createNormalizer } from '@zag-js/types'; import { jsx } from 'react/jsx-runtime'; // src/index.ts var useSafeLayoutEffect = typeof globalThis.document !== "undefined" ? useLayoutEffect : useEffect; // src/bindable.ts function useBindable(props) { const initial = props().value ?? props().defaultValue; const eq = props().isEqual ?? Object.is; const [initialValue] = useState(initial); const [value, setValue] = useState(initialValue); const controlled = props().value !== void 0; const valueRef = useRef(value); valueRef.current = controlled ? props().value : value; const prevValue = useRef(valueRef.current); useSafeLayoutEffect(() => { prevValue.current = valueRef.current; }, [value, props().value]); const setFn = (value2) => { const prev = prevValue.current; const next = isFunction(value2) ? value2(prev) : value2; if (props().debug) { console.log(`[bindable > ${props().debug}] setValue`, { next, prev }); } if (!controlled) setValue(next); if (!eq(next, prev)) { props().onChange?.(next, prev); } }; function get() { return controlled ? props().value : value; } return { initial: initialValue, ref: valueRef, get, set(value2) { const exec = props().sync ? flushSync : identity; exec(() => setFn(value2)); }, invoke(nextValue, prevValue2) { props().onChange?.(nextValue, prevValue2); }, hash(value2) { return props().hash?.(value2) ?? String(value2); } }; } useBindable.cleanup = (fn) => { useEffect(() => fn, []); }; useBindable.ref = (defaultValue) => { const value = useRef(defaultValue); return { get: () => value.current, set: (next) => { value.current = next; } }; }; function useRefs(refs) { const ref = useRef(refs); return { get(key) { return ref.current[key]; }, set(key, value) { ref.current[key] = value; } }; } var useTrack = (deps, effect) => { const render = useRef(false); const called = useRef(false); useEffect(() => { const mounted = render.current; const run = mounted && called.current; if (run) return effect(); called.current = true; }, [...(deps ?? []).map((d) => typeof d === "function" ? d() : d)]); useEffect(() => { render.current = true; return () => { render.current = false; }; }, []); }; // src/machine.ts function useMachine(machine, userProps = {}) { const scope = useMemo(() => { const { id, ids, getRootNode } = userProps; return createScope({ id, ids, getRootNode }); }, [userProps]); const debug = (...args) => { if (machine.debug) console.log(...args); }; const props = machine.props?.({ props: compact(userProps), scope }) ?? userProps; const prop = useProp(props); const context = machine.context?.({ prop, bindable: useBindable, scope, flush, getContext() { return ctx; }, getComputed() { return computed; }, getRefs() { return refs; } }); const contextRef = useLiveRef(context); const ctx = { get(key) { return contextRef.current?.[key].ref.current; }, set(key, value) { contextRef.current?.[key].set(value); }, initial(key) { return contextRef.current?.[key].initial; }, hash(key) { const current = contextRef.current?.[key].get(); return contextRef.current?.[key].hash(current); } }; const effects = useRef(/* @__PURE__ */ new Map()); const transitionRef = useRef(null); const previousEventRef = useRef(null); const eventRef = useRef({ type: "" }); const getEvent = () => ({ ...eventRef.current, current() { return eventRef.current; }, previous() { return previousEventRef.current; } }); const getState = () => ({ ...state, matches(...values) { return values.includes(state.ref.current); }, hasTag(tag) { return !!machine.states[state.ref.current]?.tags?.includes(tag); } }); const refs = useRefs(machine.refs?.({ prop, context: ctx }) ?? {}); const getParams = () => ({ state: getState(), context: ctx, event: getEvent(), prop, send, action, guard, track: useTrack, refs, computed, flush, scope, choose }); const action = (keys) => { const strs = isFunction(keys) ? keys(getParams()) : keys; if (!strs) return; const fns = strs.map((s) => { const fn = machine.implementations?.actions?.[s]; if (!fn) warn(`[zag-js] No implementation found for action "${JSON.stringify(s)}"`); return fn; }); for (const fn of fns) { fn?.(getParams()); } }; const guard = (str) => { if (isFunction(str)) return str(getParams()); return machine.implementations?.guards?.[str](getParams()); }; const effect = (keys) => { const strs = isFunction(keys) ? keys(getParams()) : keys; if (!strs) return; const fns = strs.map((s) => { const fn = machine.implementations?.effects?.[s]; if (!fn) warn(`[zag-js] No implementation found for effect "${JSON.stringify(s)}"`); return fn; }); const cleanups = []; for (const fn of fns) { const cleanup = fn?.(getParams()); if (cleanup) cleanups.push(cleanup); } return () => cleanups.forEach((fn) => fn?.()); }; const choose = (transitions) => { return toArray(transitions).find((t) => { let result = !t.guard; if (isString(t.guard)) result = !!guard(t.guard); else if (isFunction(t.guard)) result = t.guard(getParams()); return result; }); }; const computed = (key) => { ensure(machine.computed, () => `[zag-js] No computed object found on machine`); const fn = machine.computed[key]; return fn({ context: ctx, event: getEvent(), prop, refs, scope, computed }); }; const state = useBindable(() => ({ defaultValue: machine.initialState({ prop }), onChange(nextState, prevState) { if (prevState) { const exitEffects = effects.current.get(prevState); exitEffects?.(); effects.current.delete(prevState); } if (prevState) { action(machine.states[prevState]?.exit); } action(transitionRef.current?.actions); const cleanup = effect(machine.states[nextState]?.effects); if (cleanup) effects.current.set(nextState, cleanup); if (prevState === INIT_STATE) { action(machine.entry); const cleanup2 = effect(machine.effects); if (cleanup2) effects.current.set(INIT_STATE, cleanup2); } action(machine.states[nextState]?.entry); } })); const hydratedStateRef = useRef(void 0); const statusRef = useRef(MachineStatus.NotStarted); useSafeLayoutEffect(() => { queueMicrotask(() => { const started = statusRef.current === MachineStatus.Started; statusRef.current = MachineStatus.Started; debug(started ? "rehydrating..." : "initializing..."); const initialState = hydratedStateRef.current ?? state.initial; state.invoke(initialState, started ? state.get() : INIT_STATE); }); const fns = effects.current; const currentState = state.ref.current; return () => { debug("unmounting..."); hydratedStateRef.current = currentState; statusRef.current = MachineStatus.Stopped; fns.forEach((fn) => fn?.()); effects.current = /* @__PURE__ */ new Map(); transitionRef.current = null; queueMicrotask(() => { action(machine.exit); }); }; }, []); const getCurrentState = () => { if ("ref" in state) return state.ref.current; return state.get(); }; const send = (event) => { queueMicrotask(() => { if (statusRef.current !== MachineStatus.Started) return; previousEventRef.current = eventRef.current; eventRef.current = event; debug("send", event); let currentState = getCurrentState(); const transitions = ( // @ts-ignore machine.states[currentState].on?.[event.type] ?? // @ts-ignore machine.on?.[event.type] ); const transition = choose(transitions); if (!transition) return; transitionRef.current = transition; const target = transition.target ?? currentState; debug("transition", transition); const changed = target !== currentState; if (changed) { flushSync(() => state.set(target)); } else if (transition.reenter && !changed) { state.invoke(currentState, currentState); } else { action(transition.actions ?? []); } }); }; machine.watch?.(getParams()); return { state: getState(), send, context: ctx, prop, scope, refs, computed, event: getEvent(), getStatus: () => statusRef.current }; } function useLiveRef(value) { const ref = useRef(value); ref.current = value; return ref; } function useProp(value) { const ref = useLiveRef(value); return function get(key) { return ref.current[key]; }; } function flush(fn) { queueMicrotask(() => { flushSync(() => fn()); }); } var normalizeProps = createNormalizer((v) => v); var Portal = (props) => { const { children, container, disabled, getRootNode } = props; const isServer = typeof window === "undefined"; if (isServer || disabled) return /* @__PURE__ */ jsx(React.Fragment, { children }); const doc = getRootNode?.().ownerDocument ?? document; const mountNode = container?.current ?? doc.body; return /* @__PURE__ */ jsx(React.Fragment, { children: React.Children.map(children, (child) => createPortal(child, mountNode)) }); }; export { Portal, normalizeProps, useMachine };