-
-
Notifications
You must be signed in to change notification settings - Fork 35
/
primitive.ts
202 lines (182 loc) · 5.57 KB
/
primitive.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
// same inspiration and resource https://github.com/chakra-ui/ark/blob/main/packages/vue/src/factory.tsx
import type {
ComponentPropsOptions,
ComponentPublicInstance,
DefineComponent,
FunctionalComponent,
HTMLAttributes,
IntrinsicElementAttributes,
} from 'vue'
import {
cloneVNode,
defineComponent,
getCurrentInstance,
h,
mergeProps,
onMounted,
} from 'vue'
import { isValidVNodeElement, renderSlotFragments } from './utils'
const NODES = [
'a',
'button',
'div',
'form',
'h2',
'h3',
'img',
'input',
'label',
'li',
'nav',
'ol',
'p',
'span',
'svg',
'ul',
] as const
type ElementConstructor<P> =
| (new () => { $props: P })
| ((props: P, ...args: any) => FunctionalComponent<any, any>)
// extends keyof JSX.IntrinsicElements | ElementConstructor<any>
type ComponentProps<
T extends keyof JSX.IntrinsicElements | ElementConstructor<any>,
> = T extends ElementConstructor<infer P>
? P
: T extends keyof JSX.IntrinsicElements
? JSX.IntrinsicElements[T]
: {}
type RefElement<T extends abstract new (...args: any) => any> = Omit<
InstanceType<T>,
keyof ComponentPublicInstance | 'class' | 'style'
>
type MergeProps<T, U> = U & T
interface PrimitiveProps {
asChild?: boolean
}
type PrimitivePropsWithRef<E extends keyof HTMLElementTagNameMap> =
HTMLAttributes &
ComponentPropsOptions & {
asChild?: boolean
}
type PropsWithoutRef<P> = P extends any
? 'ref' extends keyof P
? Pick<P, Exclude<keyof P, 'ref'>>
: P
: P
type ComponentPropsWithoutRef<
T extends keyof HTMLElementTagNameMap | DefineComponent<any>,
> = PropsWithoutRef<ComponentPropsOptions<T>>
type Primitives = {
[E in (typeof NODES)[number]]: DefineComponent<{
asChild?: boolean
}>;
}
type ElementType<T extends keyof IntrinsicElementAttributes> = Partial<
IntrinsicElementAttributes[T]
>
const Primitive = NODES.reduce((primitive, node) => {
const Node = defineComponent({
name: `Primitive${node}`,
inheritAttrs: false,
props: {
asChild: Boolean,
},
setup(props, { attrs, slots }) {
const instance = getCurrentInstance()
onMounted(() => {
(window as any)[Symbol.for('oku-ui')] = true
})
const Tag: any = props.asChild ? 'slot' : node
if (!props.asChild) {
return () =>
h(
Tag,
{ ...attrs },
{
default: () => slots.default && slots.default(),
},
)
}
else {
return () => {
let children = slots.default?.()
children = renderSlotFragments(children || [])
if (Object.keys(attrs).length > 0) {
const [firstChild, ...otherChildren] = children
if (!isValidVNodeElement(firstChild) || otherChildren.length > 0) {
const componentName = instance?.parent?.type.name
? `<${instance.parent.type.name} />`
: 'component'
throw new Error(
[
`Detected an invalid children for \`${componentName}\` with \`asChild\` prop.`,
'',
'Note: All components accepting `asChild` expect only one direct child of valid VNode type.',
'You can apply a few solutions:',
[
'Provide a single child element so that we can forward the props onto that element.',
'Ensure the first child is an actual element instead of a raw text node or comment node.',
]
.map(line => ` - ${line}`)
.join('\n'),
].join('\n'),
)
}
const mergedProps = mergeProps(firstChild.props ?? {}, attrs)
const cloned = cloneVNode(firstChild, mergedProps)
// Explicitly override props starting with `on`.
// It seems cloneVNode from Vue doesn't like overriding `onXXX` props. So
// we have to do it manually.
for (const prop in mergedProps) {
if (prop.startsWith('on')) {
cloned.props ||= {}
cloned.props[prop] = mergedProps[prop]
}
}
return cloned
}
else if (Array.isArray(children)) {
if (children.length === 1) {
return children[0]
}
else {
const componentName = instance?.parent?.type.name
? `<${instance.parent.type.name} />`
: 'component'
throw new Error(
[
`Detected an invalid children for \`${componentName}\` with \`asChild\` prop.`,
'',
'Note: All components accepting `asChild` expect only one direct child of valid VNode type.',
'You can apply a few solutions:',
[
'Provide a single child element so that we can forward the props onto that element.',
'Ensure the first child is an actual element instead of a raw text node or comment node.',
]
.map(line => ` - ${line}`)
.join('\n'),
].join('\n'),
)
}
}
else {
// No children.
return null
}
}
}
},
})
return { ...primitive, [node]: Node }
}, {} as Primitives)
const OkuPrimitive = Primitive
export { OkuPrimitive, Primitive }
export type {
ComponentProps,
MergeProps,
PrimitiveProps,
RefElement,
ElementType,
PrimitivePropsWithRef,
ComponentPropsWithoutRef,
}