Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 45 additions & 31 deletions packages/core/src/merge-props.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { callAll, isString } from "@zag-js/utils"

interface Props {
export interface Props {
[key: string | symbol]: any
}

Expand Down Expand Up @@ -38,45 +38,59 @@ type TupleTypes<T extends any[]> = T[number]

type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends (k: infer I) => void ? I : never

export function mergeProps<T extends Props>(...args: Array<T | undefined>): UnionToIntersection<TupleTypes<T[]>> {
let result: Props = {}
type ClassMergeFn = (existing: any, incoming: any) => any
type StyleMergeFn = (existing: any, incoming: any) => any

for (let props of args) {
if (!props) continue
interface MergeOptions {
classMerge?: ClassMergeFn
styleMerge?: StyleMergeFn
}

// Handle string keys
for (let key in result) {
if (key.startsWith("on") && typeof result[key] === "function" && typeof props[key] === "function") {
result[key] = callAll(props[key], result[key])
continue
}
export function createMergeProps(options: MergeOptions = {}) {
const { classMerge = clsx, styleMerge = css } = options

if (key === "className" || key === "class") {
result[key] = clsx(result[key], props[key])
continue
}
return function mergeProps<T extends Props>(...args: Array<T | undefined>): UnionToIntersection<TupleTypes<T[]>> {
let result: Props = {}

for (let props of args) {
if (!props) continue

// Handle string keys
for (let key in result) {
if (key.startsWith("on") && typeof result[key] === "function" && typeof props[key] === "function") {
result[key] = callAll(props[key], result[key])
continue
}

if (key === "className" || key === "class") {
result[key] = classMerge(result[key], props[key])
continue
}

if (key === "style") {
result[key] = css(result[key], props[key])
continue
if (key === "style") {
result[key] = styleMerge(result[key], props[key])
continue
}

result[key] = props[key] !== undefined ? props[key] : result[key]
}

result[key] = props[key] !== undefined ? props[key] : result[key]
}
// Add props from b that are not in a
for (let key in props) {
if (result[key] === undefined) {
result[key] = props[key]
}
}

// Add props from b that are not in a
for (let key in props) {
if (result[key] === undefined) {
result[key] = props[key]
// Handle symbol keys (for Svelte attachments)
const symbols = Object.getOwnPropertySymbols(props)
for (let symbol of symbols) {
result[symbol] = props[symbol]
}
}

// Handle symbol keys (for Svelte attachments)
const symbols = Object.getOwnPropertySymbols(props)
for (let symbol of symbols) {
result[symbol] = props[symbol]
}
return result as any
}

return result as any
}

export const mergeProps = createMergeProps()
78 changes: 56 additions & 22 deletions packages/frameworks/svelte/src/merge-props.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { mergeProps as zagMergeProps } from "@zag-js/core"
import { createMergeProps } from "@zag-js/core"
import { toStyleString } from "./normalize-props"

const CSS_REGEX = /((?:--)?(?:\w+-?)+)\s*:\s*([^;]*)/g
Expand All @@ -14,35 +14,69 @@ const serialize = (style: string): CSSObject => {
return res
}

export function mergeProps(...args: Record<string | symbol, any>[]) {
// Collect all class values (as-is, without conversion)
// Svelte 5.15+ supports ClassValue types (string | array | object) and handles
// the conversion internally using clsx. We collect them into an array and let
// Svelte's native class handling resolve them at render time.
// @see https://github.com/sveltejs/svelte/pull/14714
// Custom class merge function for Svelte
// Collect all class values (as-is, without conversion)
// Svelte 5.15+ supports ClassValue types (string | array | object) and handles
// the conversion internally using clsx. We collect them into an array and let
// Svelte's native class handling resolve them at render time.
// @see https://github.com/sveltejs/svelte/pull/14714
const svelteClassMerge = (existing: any, incoming: any) => {
const classNames: any[] = []

for (const props of args) {
if (!props) continue
if ("class" in props && props.class != null) {
classNames.push(props.class)
}
}

const merged = zagMergeProps(...args)
if (existing != null) classNames.push(existing)
if (incoming != null) classNames.push(incoming)

// Override class with our collected values
// If only one value, return as-is; if multiple, return as array
if (classNames.length > 0) {
merged.class = classNames.length === 1 ? classNames[0] : classNames
return classNames.length === 1 ? classNames[0] : classNames
}

// Custom style merge function for Svelte
const svelteStyleMerge = (existing: any, incoming: any) => {
// First handle the basic merging (string to object conversion if needed)
let merged = existing
if (existing && incoming) {
if (typeof existing === "string" && typeof incoming === "string") {
merged = `${existing};${incoming}`
} else {
if (typeof existing === "string") existing = serialize(existing)
if (typeof incoming === "string") incoming = serialize(incoming)
merged = Object.assign({}, existing ?? {}, incoming ?? {})
}
} else {
merged = incoming ?? existing
}

if ("style" in merged) {
if (typeof merged.style === "string") {
merged.style = serialize(merged.style)
// Convert final result to Svelte's expected format
if (merged) {
if (typeof merged === "string") {
merged = serialize(merged)
}
merged.style = toStyleString(merged.style)
merged = toStyleString(merged)
}

return merged
}

type SvelteClassValue =
| string
| (string | Record<string, boolean> | null | undefined | boolean | (string | Record<string, boolean>)[])[]
| Record<string, boolean>
| null
| undefined

type SvelteProps = Record<string | symbol, any> & {
class?: SvelteClassValue
style?: string | Record<string, string>
}

export function mergeProps<T extends SvelteProps[]>(
...args: T
): Record<string | symbol, any> & {
class?: SvelteClassValue
style?: string
} {
return createMergeProps({
classMerge: svelteClassMerge,
styleMerge: svelteStyleMerge,
})(...args) as any
}
Loading