/** * normalizeProps for Marko 6 native tags. * * - `className`→`class`, `htmlFor`→`for`, `onChange`→`onInput`, * `onDoubleClick`→`onDblClick` (Marko lowercases the event name), * `onFocus`→`onFocusin`, `onBlur`→`onFocusout` (bubbling parity — see * propMap comment). * - style objects: hyphenate camelCase keys (Marko writes keys verbatim). * - `event.currentTarget` is unavailable in Marko's delegated events; every * handler is wrapped to shadow it with the element Marko passes as the * handler's 2nd argument. * - SSR: handlers are stripped — functions from @zag-js modules are not * Marko-serializable, and server HTML doesn't need them. They re-appear on * the first client recompute (service.start() schedules it). * - boolean `aria-*` values are stringified ("true"/"false"); Marko's * boolean-attribute rendering would emit an empty attribute instead. */ import { createNormalizer } from "@zag-js/types"; import { isFunction } from "@zag-js/utils"; import type { PropTypes } from "./prop-types.ts"; type Dict = Record; const propMap: Dict = { className: "class", htmlFor: "for", defaultValue: "value", defaultChecked: "checked", onChange: "onInput", onDoubleClick: "onDblClick", // Zag's onFocus/onBlur assume React's synthetic-focus semantics, where a // parent's handler fires when a descendant gains focus. Marko delegates // every event at the document but only walks ancestors when `ev.bubbles`, // and focus/blur don't bubble — a delegated onFocus would fire for the // exact target only, never the parent. focusin/focusout are the bubbling // twins, so this mapping restores the semantics machines were written for // (every non-React official adapter does the same). onFocus: "onFocusin", onBlur: "onFocusout", // Zag emits the React-style `tabIndex`. Marko writes attribute keys verbatim, // so the camelCase spelling is treated as a *different* attribute from the // `tabindex` already on the element: each update removes the old one and adds // the new. Removing `tabindex` from the focused element blurs it in Chromium // (setting it in place does not), which made every roving-focus widget go // keyboard-dead after one keypress — the slider lost focus to after a // single arrow press. Mapping to the canonical lowercase name keeps it a // single in-place attribute write. tabIndex: "tabindex", }; const uppercasePattern = /[A-Z]/g; /** camelCase → kebab-case for style keys; `--custom-props` pass through. */ function hyphenate(name: string) { if (name.startsWith("--")) return name; return name.replace(uppercasePattern, (m) => "-" + m.toLowerCase()); } /** Normalizes a React-style style object to Marko's hyphenated shape. */ function cssify(style: Dict) { const css: Dict = {}; for (const property in style) { const value = style[property]; if (typeof value !== "string" && typeof value !== "number") continue; css[hyphenate(property)] = value; } return css; } /** * Wraps a Zag handler so `event.currentTarget` resolves to the element Marko * passes as the handler's second argument (delegated events lack it). */ function wrapHandler(fn: (event: Event) => void) { return function (event: Event, el?: Element) { if (el) { Object.defineProperty(event, "currentTarget", { get: () => el, configurable: true, }); } return fn(event); }; } const isServer = typeof document === "undefined"; /** * Zag `normalizeProps` implementation for Marko 6 native tags — maps Zag's * React-flavored prop objects onto Marko DOM attributes. Pass it as the * second argument of a machine module's `connect()`. * * What it translates: * - `className`→`class`, `htmlFor`→`for`, `onChange`→`onInput`, * `onDoubleClick`→`onDblClick` (Marko lowercases the event name), * `onFocus`→`onFocusin` / `onBlur`→`onFocusout` (focus/blur don't bubble, * so Marko's document-level delegation would never notify a parent — the * focusin/focusout twins restore the React-like semantics Zag machines * assume), and crucially `tabIndex`→`tabindex` (see remarks). * - style objects: camelCase keys hyphenated (Marko writes keys verbatim). * - `event.currentTarget` is unavailable in Marko's delegated events; every * handler is wrapped to shadow it with the element Marko passes as the * handler's 2nd argument. * - SSR: handlers are stripped — functions from `@zag-js/*` modules are not * Marko-serializable, and server HTML doesn't need them. They re-appear on * the first client recompute (`service.start()` schedules it). * - boolean `aria-*` values are stringified (`"true"`/`"false"`); Marko's * boolean-attribute rendering would emit an empty attribute instead. * * @example * `` applies this normalizer by default: * * ```marko * switchMachine from=input/> *