@pyreon/runtime-dom 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +65 -0
- package/lib/analysis/index.js.html +5406 -0
- package/lib/index.js +1909 -0
- package/lib/index.js.map +1 -0
- package/lib/types/index.d.ts +1845 -0
- package/lib/types/index.d.ts.map +1 -0
- package/lib/types/index2.d.ts +355 -0
- package/lib/types/index2.d.ts.map +1 -0
- package/package.json +48 -0
- package/src/devtools.ts +304 -0
- package/src/hydrate.ts +385 -0
- package/src/hydration-debug.ts +39 -0
- package/src/index.ts +43 -0
- package/src/keep-alive.ts +71 -0
- package/src/mount.ts +367 -0
- package/src/nodes.ts +741 -0
- package/src/props.ts +328 -0
- package/src/template.ts +81 -0
- package/src/tests/coverage-gaps.test.ts +2488 -0
- package/src/tests/coverage.test.ts +1123 -0
- package/src/tests/mount.test.ts +3098 -0
- package/src/tests/setup.ts +3 -0
- package/src/transition-group.ts +264 -0
- package/src/transition.ts +184 -0
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
import type { Props, VNode, VNodeChild } from "@pyreon/core"
|
|
2
|
+
import { createRef, h, onMount, onUnmount } from "@pyreon/core"
|
|
3
|
+
import { effect, runUntracked, signal } from "@pyreon/reactivity"
|
|
4
|
+
import { mountChild } from "./mount"
|
|
5
|
+
|
|
6
|
+
export interface TransitionGroupProps<T = unknown> {
|
|
7
|
+
/** Wrapper element tag. Default: "div" */
|
|
8
|
+
tag?: string
|
|
9
|
+
/** CSS class prefix. Default: "pyreon" */
|
|
10
|
+
name?: string
|
|
11
|
+
/** Animate items on initial mount. Default: false */
|
|
12
|
+
appear?: boolean
|
|
13
|
+
// CSS class overrides
|
|
14
|
+
enterFrom?: string
|
|
15
|
+
enterActive?: string
|
|
16
|
+
enterTo?: string
|
|
17
|
+
leaveFrom?: string
|
|
18
|
+
leaveActive?: string
|
|
19
|
+
leaveTo?: string
|
|
20
|
+
/** Class applied during FLIP move animation. Default: "{name}-move" */
|
|
21
|
+
moveClass?: string
|
|
22
|
+
/** Reactive list source */
|
|
23
|
+
items: () => T[]
|
|
24
|
+
/** Stable key extractor */
|
|
25
|
+
keyFn: (item: T, index: number) => string | number
|
|
26
|
+
/**
|
|
27
|
+
* Render a single DOM-element VNode for each item.
|
|
28
|
+
* Must return a VNode whose `type` is a string (e.g. "div", "li") so
|
|
29
|
+
* the component can inject a ref and read the underlying DOM node.
|
|
30
|
+
*/
|
|
31
|
+
render: (item: T, index: number) => VNode
|
|
32
|
+
// Lifecycle callbacks
|
|
33
|
+
onBeforeEnter?: (el: HTMLElement) => void
|
|
34
|
+
onAfterEnter?: (el: HTMLElement) => void
|
|
35
|
+
onBeforeLeave?: (el: HTMLElement) => void
|
|
36
|
+
onAfterLeave?: (el: HTMLElement) => void
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
type ItemEntry = {
|
|
40
|
+
key: string | number
|
|
41
|
+
ref: ReturnType<typeof createRef<HTMLElement>>
|
|
42
|
+
cleanup: () => void
|
|
43
|
+
leaving: boolean
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* TransitionGroup — animates a keyed reactive list with CSS enter/leave and
|
|
48
|
+
* FLIP move animations.
|
|
49
|
+
*
|
|
50
|
+
* Class lifecycle:
|
|
51
|
+
* Enter: {name}-enter-from → {name}-enter-active + {name}-enter-to → cleanup
|
|
52
|
+
* Leave: {name}-leave-from → {name}-leave-active + {name}-leave-to → item removed
|
|
53
|
+
* Move: {name}-move (applied when an item shifts position)
|
|
54
|
+
*
|
|
55
|
+
* @example
|
|
56
|
+
* const items = signal([{ id: 1 }, { id: 2 }])
|
|
57
|
+
*
|
|
58
|
+
* h(TransitionGroup, {
|
|
59
|
+
* tag: "ul",
|
|
60
|
+
* name: "list",
|
|
61
|
+
* items,
|
|
62
|
+
* keyFn: (item) => item.id,
|
|
63
|
+
* render: (item) => h("li", { class: "item" }, item.id),
|
|
64
|
+
* })
|
|
65
|
+
*
|
|
66
|
+
* // CSS:
|
|
67
|
+
* // .list-enter-from, .list-leave-to { opacity: 0; transform: translateY(-10px); }
|
|
68
|
+
* // .list-enter-active, .list-leave-active { transition: all 300ms ease; }
|
|
69
|
+
* // .list-move { transition: transform 300ms ease; }
|
|
70
|
+
*/
|
|
71
|
+
export function TransitionGroup<T = unknown>(props: TransitionGroupProps<T>): VNodeChild {
|
|
72
|
+
const tag = props.tag ?? "div"
|
|
73
|
+
const n = props.name ?? "pyreon"
|
|
74
|
+
const cls = {
|
|
75
|
+
ef: props.enterFrom ?? `${n}-enter-from`,
|
|
76
|
+
ea: props.enterActive ?? `${n}-enter-active`,
|
|
77
|
+
et: props.enterTo ?? `${n}-enter-to`,
|
|
78
|
+
lf: props.leaveFrom ?? `${n}-leave-from`,
|
|
79
|
+
la: props.leaveActive ?? `${n}-leave-active`,
|
|
80
|
+
lt: props.leaveTo ?? `${n}-leave-to`,
|
|
81
|
+
mv: props.moveClass ?? `${n}-move`,
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const containerRef = createRef<HTMLElement>()
|
|
85
|
+
const entries = new Map<string | number, ItemEntry>()
|
|
86
|
+
// Gates the effect until the container element is in the DOM
|
|
87
|
+
const ready = signal(false)
|
|
88
|
+
let firstRun = true
|
|
89
|
+
|
|
90
|
+
const applyEnter = (el: HTMLElement) => {
|
|
91
|
+
props.onBeforeEnter?.(el)
|
|
92
|
+
el.classList.remove(cls.lf, cls.la, cls.lt)
|
|
93
|
+
el.classList.add(cls.ef, cls.ea)
|
|
94
|
+
requestAnimationFrame(() => {
|
|
95
|
+
el.classList.remove(cls.ef)
|
|
96
|
+
el.classList.add(cls.et)
|
|
97
|
+
const done = () => {
|
|
98
|
+
el.removeEventListener("transitionend", done)
|
|
99
|
+
el.removeEventListener("animationend", done)
|
|
100
|
+
el.classList.remove(cls.ea, cls.et)
|
|
101
|
+
props.onAfterEnter?.(el)
|
|
102
|
+
}
|
|
103
|
+
el.addEventListener("transitionend", done, { once: true })
|
|
104
|
+
el.addEventListener("animationend", done, { once: true })
|
|
105
|
+
})
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const applyLeave = (el: HTMLElement, onDone: () => void) => {
|
|
109
|
+
props.onBeforeLeave?.(el)
|
|
110
|
+
el.classList.remove(cls.ef, cls.ea, cls.et)
|
|
111
|
+
el.classList.add(cls.lf, cls.la)
|
|
112
|
+
requestAnimationFrame(() => {
|
|
113
|
+
el.classList.remove(cls.lf)
|
|
114
|
+
el.classList.add(cls.lt)
|
|
115
|
+
const done = () => {
|
|
116
|
+
el.removeEventListener("transitionend", done)
|
|
117
|
+
el.removeEventListener("animationend", done)
|
|
118
|
+
el.classList.remove(cls.la, cls.lt)
|
|
119
|
+
props.onAfterLeave?.(el)
|
|
120
|
+
onDone()
|
|
121
|
+
}
|
|
122
|
+
el.addEventListener("transitionend", done, { once: true })
|
|
123
|
+
el.addEventListener("animationend", done, { once: true })
|
|
124
|
+
})
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** Start leave animation for removed items. */
|
|
128
|
+
const processLeaves = (newKeys: Set<string | number>) => {
|
|
129
|
+
for (const [key, entry] of entries) {
|
|
130
|
+
if (newKeys.has(key) || entry.leaving) continue
|
|
131
|
+
entry.leaving = true
|
|
132
|
+
const el = entry.ref.current
|
|
133
|
+
if (el) {
|
|
134
|
+
applyLeave(el, () => {
|
|
135
|
+
entry.cleanup()
|
|
136
|
+
entries.delete(key)
|
|
137
|
+
})
|
|
138
|
+
} else {
|
|
139
|
+
entry.cleanup()
|
|
140
|
+
entries.delete(key)
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** Mount new items and return the list of newly created entries. */
|
|
146
|
+
const mountNewItems = (items: T[], container: HTMLElement): ItemEntry[] => {
|
|
147
|
+
const newEntries: ItemEntry[] = []
|
|
148
|
+
for (let i = 0; i < items.length; i++) {
|
|
149
|
+
const item = items[i] as T
|
|
150
|
+
const key = props.keyFn(item, i)
|
|
151
|
+
if (entries.has(key)) continue
|
|
152
|
+
const itemRef = createRef<HTMLElement>()
|
|
153
|
+
const rawVNode = runUntracked(() => props.render(item, i))
|
|
154
|
+
const vnode: VNode =
|
|
155
|
+
typeof rawVNode.type === "string"
|
|
156
|
+
? { ...rawVNode, props: { ...rawVNode.props, ref: itemRef } as Props }
|
|
157
|
+
: rawVNode
|
|
158
|
+
const cleanup = mountChild(vnode, container, null)
|
|
159
|
+
const entry: ItemEntry = { key, ref: itemRef, cleanup, leaving: false }
|
|
160
|
+
entries.set(key, entry)
|
|
161
|
+
newEntries.push(entry)
|
|
162
|
+
}
|
|
163
|
+
return newEntries
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const startMoveAnimation = (el: HTMLElement) => {
|
|
167
|
+
requestAnimationFrame(() => {
|
|
168
|
+
el.classList.add(cls.mv)
|
|
169
|
+
el.style.transform = ""
|
|
170
|
+
el.style.transition = ""
|
|
171
|
+
const done = () => {
|
|
172
|
+
el.removeEventListener("transitionend", done)
|
|
173
|
+
el.removeEventListener("animationend", done)
|
|
174
|
+
el.classList.remove(cls.mv)
|
|
175
|
+
}
|
|
176
|
+
el.addEventListener("transitionend", done, { once: true })
|
|
177
|
+
el.addEventListener("animationend", done, { once: true })
|
|
178
|
+
})
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const flipEntry = (entry: ItemEntry, oldPos: DOMRect) => {
|
|
182
|
+
if (!entry.ref.current) return
|
|
183
|
+
const newPos = entry.ref.current.getBoundingClientRect()
|
|
184
|
+
const dx = oldPos.left - newPos.left
|
|
185
|
+
const dy = oldPos.top - newPos.top
|
|
186
|
+
if (Math.abs(dx) < 1 && Math.abs(dy) < 1) return
|
|
187
|
+
const el = entry.ref.current
|
|
188
|
+
el.style.transform = `translate(${dx}px, ${dy}px)`
|
|
189
|
+
el.style.transition = "none"
|
|
190
|
+
startMoveAnimation(el)
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/** Apply FLIP move animations for items that shifted position. */
|
|
194
|
+
const applyFlipMoves = (oldPositions: Map<string | number, DOMRect>) => {
|
|
195
|
+
requestAnimationFrame(() => {
|
|
196
|
+
for (const [key, entry] of entries) {
|
|
197
|
+
if (entry.leaving) continue
|
|
198
|
+
const oldPos = oldPositions.get(key)
|
|
199
|
+
if (!oldPos) continue
|
|
200
|
+
flipEntry(entry, oldPos)
|
|
201
|
+
}
|
|
202
|
+
})
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const recordOldPositions = (): Map<string | number, DOMRect> => {
|
|
206
|
+
const oldPositions = new Map<string | number, DOMRect>()
|
|
207
|
+
for (const [key, entry] of entries) {
|
|
208
|
+
if (!entry.leaving && entry.ref.current) {
|
|
209
|
+
oldPositions.set(key, entry.ref.current.getBoundingClientRect())
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
return oldPositions
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const reorderEntries = (items: T[], container: HTMLElement) => {
|
|
216
|
+
for (let i = 0; i < items.length; i++) {
|
|
217
|
+
const key = props.keyFn(items[i] as T, i)
|
|
218
|
+
const entry = entries.get(key)
|
|
219
|
+
if (!entry || entry.leaving || !entry.ref.current) continue
|
|
220
|
+
container.appendChild(entry.ref.current)
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const animateNewEntries = (newEntries: ItemEntry[]) => {
|
|
225
|
+
for (const entry of newEntries) {
|
|
226
|
+
queueMicrotask(() => {
|
|
227
|
+
if (entry.ref.current) applyEnter(entry.ref.current)
|
|
228
|
+
})
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const e = effect(() => {
|
|
233
|
+
if (!ready()) return
|
|
234
|
+
const container = containerRef.current
|
|
235
|
+
if (!container) return
|
|
236
|
+
|
|
237
|
+
const items = props.items()
|
|
238
|
+
const newKeys = new Set(items.map((item, i) => props.keyFn(item, i)))
|
|
239
|
+
const isFirst = firstRun
|
|
240
|
+
firstRun = false
|
|
241
|
+
|
|
242
|
+
const oldPositions = recordOldPositions()
|
|
243
|
+
processLeaves(newKeys)
|
|
244
|
+
const newEntries = mountNewItems(items, container)
|
|
245
|
+
reorderEntries(items, container)
|
|
246
|
+
|
|
247
|
+
if (!isFirst || props.appear) animateNewEntries(newEntries)
|
|
248
|
+
if (!isFirst && oldPositions.size > 0) applyFlipMoves(oldPositions)
|
|
249
|
+
})
|
|
250
|
+
|
|
251
|
+
// Fire the effect once the container is in the DOM
|
|
252
|
+
onMount(() => {
|
|
253
|
+
ready.set(true)
|
|
254
|
+
return undefined
|
|
255
|
+
})
|
|
256
|
+
|
|
257
|
+
onUnmount(() => {
|
|
258
|
+
e.dispose()
|
|
259
|
+
for (const entry of entries.values()) entry.cleanup()
|
|
260
|
+
entries.clear()
|
|
261
|
+
})
|
|
262
|
+
|
|
263
|
+
return h(tag, { ref: containerRef })
|
|
264
|
+
}
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import type { Props, VNode, VNodeChild } from "@pyreon/core"
|
|
2
|
+
import { createRef, Fragment, h, onUnmount } from "@pyreon/core"
|
|
3
|
+
import { effect, runUntracked, signal } from "@pyreon/reactivity"
|
|
4
|
+
|
|
5
|
+
const __DEV__ = typeof process !== "undefined" && process.env.NODE_ENV !== "production"
|
|
6
|
+
|
|
7
|
+
export interface TransitionProps {
|
|
8
|
+
/**
|
|
9
|
+
* CSS class name prefix.
|
|
10
|
+
* "fade" → fade-enter-from, fade-enter-active, fade-enter-to, fade-leave-from, …
|
|
11
|
+
* Default: "pyreon"
|
|
12
|
+
*/
|
|
13
|
+
name?: string
|
|
14
|
+
/** Reactive boolean controlling whether the child is shown. */
|
|
15
|
+
show: () => boolean
|
|
16
|
+
/**
|
|
17
|
+
* If true, runs the enter transition on the initial mount (instead of
|
|
18
|
+
* appearing immediately). Default: false.
|
|
19
|
+
*/
|
|
20
|
+
appear?: boolean
|
|
21
|
+
// Individual class name overrides (override the prefix-based defaults)
|
|
22
|
+
enterFrom?: string
|
|
23
|
+
enterActive?: string
|
|
24
|
+
enterTo?: string
|
|
25
|
+
leaveFrom?: string
|
|
26
|
+
leaveActive?: string
|
|
27
|
+
leaveTo?: string
|
|
28
|
+
// Lifecycle callbacks
|
|
29
|
+
onBeforeEnter?: (el: HTMLElement) => void
|
|
30
|
+
onAfterEnter?: (el: HTMLElement) => void
|
|
31
|
+
onBeforeLeave?: (el: HTMLElement) => void
|
|
32
|
+
onAfterLeave?: (el: HTMLElement) => void
|
|
33
|
+
/**
|
|
34
|
+
* The single child element to animate.
|
|
35
|
+
* Must be a direct DOM element VNode (not a component) for class injection to work.
|
|
36
|
+
*/
|
|
37
|
+
children?: VNodeChild
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Transition — adds CSS enter/leave animation classes to a single child element,
|
|
42
|
+
* controlled by the reactive `show` prop.
|
|
43
|
+
*
|
|
44
|
+
* Class lifecycle:
|
|
45
|
+
* Enter: {name}-enter-from → (next frame) → {name}-enter-active + {name}-enter-to → cleanup
|
|
46
|
+
* Leave: {name}-leave-from → (next frame) → {name}-leave-active + {name}-leave-to → unmount
|
|
47
|
+
*
|
|
48
|
+
* The child element stays in the DOM during the leave animation and is removed only
|
|
49
|
+
* after the CSS transition / animation completes.
|
|
50
|
+
*
|
|
51
|
+
* @example
|
|
52
|
+
* const visible = signal(false)
|
|
53
|
+
*
|
|
54
|
+
* h(Transition, { name: "fade", show: () => visible() },
|
|
55
|
+
* h("div", { class: "modal" }, "content")
|
|
56
|
+
* )
|
|
57
|
+
*
|
|
58
|
+
* // CSS:
|
|
59
|
+
* // .fade-enter-from, .fade-leave-to { opacity: 0; }
|
|
60
|
+
* // .fade-enter-active, .fade-leave-active { transition: opacity 300ms ease; }
|
|
61
|
+
*/
|
|
62
|
+
export function Transition(props: TransitionProps): VNodeChild {
|
|
63
|
+
const n = props.name ?? "pyreon"
|
|
64
|
+
const cls = {
|
|
65
|
+
ef: props.enterFrom ?? `${n}-enter-from`,
|
|
66
|
+
ea: props.enterActive ?? `${n}-enter-active`,
|
|
67
|
+
et: props.enterTo ?? `${n}-enter-to`,
|
|
68
|
+
lf: props.leaveFrom ?? `${n}-leave-from`,
|
|
69
|
+
la: props.leaveActive ?? `${n}-leave-active`,
|
|
70
|
+
lt: props.leaveTo ?? `${n}-leave-to`,
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Ref injected into the child element so we can apply/remove classes
|
|
74
|
+
const ref = createRef<HTMLElement>()
|
|
75
|
+
const isMounted = signal(runUntracked<boolean>(props.show))
|
|
76
|
+
|
|
77
|
+
// Cancel an in-progress leave when re-entering before the animation ends
|
|
78
|
+
let pendingLeaveCancel: (() => void) | null = null
|
|
79
|
+
let initialized = false
|
|
80
|
+
|
|
81
|
+
const applyEnter = (el: HTMLElement) => {
|
|
82
|
+
pendingLeaveCancel?.()
|
|
83
|
+
pendingLeaveCancel = null
|
|
84
|
+
props.onBeforeEnter?.(el)
|
|
85
|
+
el.classList.remove(cls.lf, cls.la, cls.lt)
|
|
86
|
+
el.classList.add(cls.ef, cls.ea)
|
|
87
|
+
requestAnimationFrame(() => {
|
|
88
|
+
el.classList.remove(cls.ef)
|
|
89
|
+
el.classList.add(cls.et)
|
|
90
|
+
const done = () => {
|
|
91
|
+
// Remove both listeners — only one fires, so clean up the other
|
|
92
|
+
el.removeEventListener("transitionend", done)
|
|
93
|
+
el.removeEventListener("animationend", done)
|
|
94
|
+
el.classList.remove(cls.ea, cls.et)
|
|
95
|
+
props.onAfterEnter?.(el)
|
|
96
|
+
}
|
|
97
|
+
el.addEventListener("transitionend", done, { once: true })
|
|
98
|
+
el.addEventListener("animationend", done, { once: true })
|
|
99
|
+
})
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const applyLeave = (el: HTMLElement) => {
|
|
103
|
+
props.onBeforeLeave?.(el)
|
|
104
|
+
el.classList.remove(cls.ef, cls.ea, cls.et)
|
|
105
|
+
el.classList.add(cls.lf, cls.la)
|
|
106
|
+
requestAnimationFrame(() => {
|
|
107
|
+
el.classList.remove(cls.lf)
|
|
108
|
+
el.classList.add(cls.lt)
|
|
109
|
+
const done = () => {
|
|
110
|
+
// Remove both listeners — only one fires, so clean up the other
|
|
111
|
+
el.removeEventListener("transitionend", done)
|
|
112
|
+
el.removeEventListener("animationend", done)
|
|
113
|
+
el.classList.remove(cls.la, cls.lt)
|
|
114
|
+
pendingLeaveCancel = null
|
|
115
|
+
isMounted.set(false)
|
|
116
|
+
props.onAfterLeave?.(el)
|
|
117
|
+
}
|
|
118
|
+
pendingLeaveCancel = () => {
|
|
119
|
+
el.removeEventListener("transitionend", done)
|
|
120
|
+
el.removeEventListener("animationend", done)
|
|
121
|
+
el.classList.remove(cls.lf, cls.la, cls.lt)
|
|
122
|
+
}
|
|
123
|
+
el.addEventListener("transitionend", done, { once: true })
|
|
124
|
+
el.addEventListener("animationend", done, { once: true })
|
|
125
|
+
})
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const handleVisibilityChange = (visible: boolean) => {
|
|
129
|
+
if (visible) {
|
|
130
|
+
if (!isMounted.peek()) isMounted.set(true)
|
|
131
|
+
queueMicrotask(() => applyEnter(ref.current as HTMLElement))
|
|
132
|
+
return
|
|
133
|
+
}
|
|
134
|
+
if (!isMounted.peek()) return
|
|
135
|
+
const el = ref.current
|
|
136
|
+
if (!el) {
|
|
137
|
+
isMounted.set(false)
|
|
138
|
+
return
|
|
139
|
+
}
|
|
140
|
+
applyLeave(el)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
effect(() => {
|
|
144
|
+
const visible = props.show()
|
|
145
|
+
if (!initialized) {
|
|
146
|
+
initialized = true
|
|
147
|
+
if (visible && props.appear) {
|
|
148
|
+
queueMicrotask(() => applyEnter(ref.current as HTMLElement))
|
|
149
|
+
}
|
|
150
|
+
return
|
|
151
|
+
}
|
|
152
|
+
handleVisibilityChange(visible)
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
onUnmount(() => {
|
|
156
|
+
pendingLeaveCancel?.()
|
|
157
|
+
pendingLeaveCancel = null
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
// Return a reactive getter. Each call clones the child VNode with our injected ref
|
|
161
|
+
// so we can read / write classes on the underlying DOM element.
|
|
162
|
+
const rawChild = props.children
|
|
163
|
+
// Return an empty Fragment (not null) when unmounted so mountChild uses
|
|
164
|
+
// mountReactive instead of the null/primitive text-node fast-path, which
|
|
165
|
+
// cannot later be swapped for a VNode when the element enters.
|
|
166
|
+
const emptyFragment = h(Fragment, null)
|
|
167
|
+
return (() => {
|
|
168
|
+
if (!isMounted()) return emptyFragment
|
|
169
|
+
if (!rawChild || typeof rawChild !== "object" || Array.isArray(rawChild)) {
|
|
170
|
+
return rawChild ?? null
|
|
171
|
+
}
|
|
172
|
+
const vnode = rawChild as VNode
|
|
173
|
+
// Only inject ref into DOM element children — component children need ref forwarding
|
|
174
|
+
if (typeof vnode.type !== "string") {
|
|
175
|
+
if (__DEV__) {
|
|
176
|
+
console.warn(
|
|
177
|
+
"[Pyreon] Transition child is a component. Wrap it in a DOM element for enter/leave animations to work.",
|
|
178
|
+
)
|
|
179
|
+
}
|
|
180
|
+
return vnode
|
|
181
|
+
}
|
|
182
|
+
return { ...vnode, props: { ...vnode.props, ref } as Props }
|
|
183
|
+
}) as unknown as VNode
|
|
184
|
+
}
|