@pyreon/kinetic 0.24.4 → 0.24.6
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/package.json +10 -12
- package/src/Collapse.tsx +0 -166
- package/src/Stagger.tsx +0 -63
- package/src/Transition.tsx +0 -280
- package/src/TransitionGroup.tsx +0 -139
- package/src/__tests__/Collapse.test.tsx +0 -803
- package/src/__tests__/GroupRenderer.test.tsx +0 -434
- package/src/__tests__/StaggerRenderer.test.tsx +0 -523
- package/src/__tests__/Transition.ssr.test.tsx +0 -183
- package/src/__tests__/Transition.test.tsx +0 -403
- package/src/__tests__/TransitionItem.test.tsx +0 -514
- package/src/__tests__/kinetic-modes.ssr.test.tsx +0 -214
- package/src/__tests__/kinetic.browser.test.tsx +0 -327
- package/src/__tests__/kinetic.test.tsx +0 -565
- package/src/__tests__/presets.test.ts +0 -46
- package/src/__tests__/stagger-component-children-hydration.test.tsx +0 -191
- package/src/__tests__/top-level-transition-stagger-function-children.test.tsx +0 -141
- package/src/__tests__/useAnimationEnd.test.ts +0 -194
- package/src/__tests__/useReducedMotion.test.ts +0 -160
- package/src/__tests__/useTransitionState.test.ts +0 -132
- package/src/__tests__/utils.test.ts +0 -139
- package/src/index.ts +0 -15
- package/src/jsx-augment.d.ts +0 -12
- package/src/kinetic/CollapseRenderer.tsx +0 -216
- package/src/kinetic/GroupRenderer.tsx +0 -149
- package/src/kinetic/StaggerRenderer.tsx +0 -94
- package/src/kinetic/TransitionItem.tsx +0 -250
- package/src/kinetic/TransitionRenderer.tsx +0 -230
- package/src/kinetic/createKineticComponent.tsx +0 -224
- package/src/kinetic/types.ts +0 -149
- package/src/kinetic.ts +0 -25
- package/src/presets.ts +0 -66
- package/src/types.ts +0 -118
- package/src/useAnimationEnd.ts +0 -59
- package/src/useReducedMotion.ts +0 -28
- package/src/useTransitionState.ts +0 -62
- package/src/utils.ts +0 -113
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pyreon/kinetic",
|
|
3
|
-
"version": "0.24.
|
|
3
|
+
"version": "0.24.6",
|
|
4
4
|
"description": "CSS-transition-based animation components for Pyreon",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -13,8 +13,7 @@
|
|
|
13
13
|
"!lib/**/*.map",
|
|
14
14
|
"!lib/analysis",
|
|
15
15
|
"README.md",
|
|
16
|
-
"LICENSE"
|
|
17
|
-
"src"
|
|
16
|
+
"LICENSE"
|
|
18
17
|
],
|
|
19
18
|
"type": "module",
|
|
20
19
|
"sideEffects": false,
|
|
@@ -22,7 +21,6 @@
|
|
|
22
21
|
"types": "./lib/index.d.ts",
|
|
23
22
|
"exports": {
|
|
24
23
|
".": {
|
|
25
|
-
"bun": "./src/index.ts",
|
|
26
24
|
"import": "./lib/index.js",
|
|
27
25
|
"types": "./lib/index.d.ts"
|
|
28
26
|
}
|
|
@@ -42,12 +40,12 @@
|
|
|
42
40
|
"typecheck": "tsc --noEmit"
|
|
43
41
|
},
|
|
44
42
|
"devDependencies": {
|
|
45
|
-
"@pyreon/core": "^0.24.
|
|
46
|
-
"@pyreon/reactivity": "^0.24.
|
|
47
|
-
"@pyreon/runtime-dom": "^0.24.
|
|
48
|
-
"@pyreon/runtime-server": "^0.24.
|
|
43
|
+
"@pyreon/core": "^0.24.6",
|
|
44
|
+
"@pyreon/reactivity": "^0.24.6",
|
|
45
|
+
"@pyreon/runtime-dom": "^0.24.6",
|
|
46
|
+
"@pyreon/runtime-server": "^0.24.6",
|
|
49
47
|
"@pyreon/test-utils": "^0.13.11",
|
|
50
|
-
"@pyreon/typescript": "^0.24.
|
|
48
|
+
"@pyreon/typescript": "^0.24.6",
|
|
51
49
|
"@vitest/browser-playwright": "^4.1.4",
|
|
52
50
|
"@vitus-labs/tools-rolldown": "^2.4.0"
|
|
53
51
|
},
|
|
@@ -55,8 +53,8 @@
|
|
|
55
53
|
"node": ">= 22"
|
|
56
54
|
},
|
|
57
55
|
"dependencies": {
|
|
58
|
-
"@pyreon/core": "^0.24.
|
|
59
|
-
"@pyreon/reactivity": "^0.24.
|
|
60
|
-
"@pyreon/runtime-dom": "^0.24.
|
|
56
|
+
"@pyreon/core": "^0.24.6",
|
|
57
|
+
"@pyreon/reactivity": "^0.24.6",
|
|
58
|
+
"@pyreon/runtime-dom": "^0.24.6"
|
|
61
59
|
}
|
|
62
60
|
}
|
package/src/Collapse.tsx
DELETED
|
@@ -1,166 +0,0 @@
|
|
|
1
|
-
import type { VNode } from '@pyreon/core'
|
|
2
|
-
import { createRef, Show } from '@pyreon/core'
|
|
3
|
-
import { runUntracked, signal, watch } from '@pyreon/reactivity'
|
|
4
|
-
import type { CollapseProps, TransitionStage } from './types'
|
|
5
|
-
import useAnimationEnd from './useAnimationEnd'
|
|
6
|
-
import { useReducedMotion } from './useReducedMotion'
|
|
7
|
-
|
|
8
|
-
const Collapse = (props: CollapseProps): VNode | null => {
|
|
9
|
-
const transition = props.transition ?? 'height 300ms ease'
|
|
10
|
-
const appear = props.appear ?? false
|
|
11
|
-
const timeout = props.timeout ?? 5000
|
|
12
|
-
|
|
13
|
-
const reducedMotion = useReducedMotion()
|
|
14
|
-
let wrapperRef: { current: HTMLDivElement | null } = createRef<HTMLDivElement>()
|
|
15
|
-
const contentRef = createRef<HTMLDivElement>()
|
|
16
|
-
|
|
17
|
-
const callbacks = {
|
|
18
|
-
onEnter: props.onEnter,
|
|
19
|
-
onAfterEnter: props.onAfterEnter,
|
|
20
|
-
onLeave: props.onLeave,
|
|
21
|
-
onAfterLeave: props.onAfterLeave,
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
const initialShow = props.show()
|
|
25
|
-
// When appear=true and show starts true, mount but defer animation until ref is wired
|
|
26
|
-
const needsAppear = appear && initialShow
|
|
27
|
-
const stage = signal<TransitionStage>(initialShow ? 'entered' : 'hidden')
|
|
28
|
-
let isInitialMount = true
|
|
29
|
-
let appearTriggered = false
|
|
30
|
-
|
|
31
|
-
// Intercept ref assignment to detect when element connects and trigger appear.
|
|
32
|
-
// Uses queueMicrotask so all sibling refs are wired before the animation starts.
|
|
33
|
-
if (needsAppear) {
|
|
34
|
-
const orig = wrapperRef
|
|
35
|
-
const proxy = { current: null as HTMLDivElement | null }
|
|
36
|
-
Object.defineProperty(proxy, 'current', {
|
|
37
|
-
get() {
|
|
38
|
-
return orig.current
|
|
39
|
-
},
|
|
40
|
-
set(node: HTMLDivElement | null) {
|
|
41
|
-
orig.current = node
|
|
42
|
-
if (node && !appearTriggered) {
|
|
43
|
-
appearTriggered = true
|
|
44
|
-
queueMicrotask(() => stage.set('entering'))
|
|
45
|
-
}
|
|
46
|
-
},
|
|
47
|
-
})
|
|
48
|
-
wrapperRef = proxy
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
// State machine transitions
|
|
52
|
-
watch(
|
|
53
|
-
props.show,
|
|
54
|
-
(showVal) => {
|
|
55
|
-
if (isInitialMount) {
|
|
56
|
-
isInitialMount = false
|
|
57
|
-
// appear case is handled by wrapperRefCallback above
|
|
58
|
-
return
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
const currentStage = runUntracked(() => stage())
|
|
62
|
-
if (showVal && (currentStage === 'hidden' || currentStage === 'leaving')) {
|
|
63
|
-
stage.set('entering')
|
|
64
|
-
} else if (!showVal && (currentStage === 'entered' || currentStage === 'entering')) {
|
|
65
|
-
stage.set('leaving')
|
|
66
|
-
}
|
|
67
|
-
},
|
|
68
|
-
{ immediate: true },
|
|
69
|
-
)
|
|
70
|
-
|
|
71
|
-
// Animate height
|
|
72
|
-
watch(
|
|
73
|
-
() => stage(),
|
|
74
|
-
(currentStage) => {
|
|
75
|
-
const wrapper = wrapperRef.current
|
|
76
|
-
const content = contentRef.current
|
|
77
|
-
if (!wrapper || !content) return
|
|
78
|
-
|
|
79
|
-
if (reducedMotion()) {
|
|
80
|
-
if (currentStage === 'entering') {
|
|
81
|
-
callbacks.onEnter?.()
|
|
82
|
-
wrapper.style.height = 'auto'
|
|
83
|
-
wrapper.style.overflow = ''
|
|
84
|
-
callbacks.onAfterEnter?.()
|
|
85
|
-
stage.set('entered')
|
|
86
|
-
} else if (currentStage === 'leaving') {
|
|
87
|
-
callbacks.onLeave?.()
|
|
88
|
-
wrapper.style.height = '0px'
|
|
89
|
-
wrapper.style.overflow = 'hidden'
|
|
90
|
-
callbacks.onAfterLeave?.()
|
|
91
|
-
stage.set('hidden')
|
|
92
|
-
}
|
|
93
|
-
return
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
if (currentStage === 'entering') {
|
|
97
|
-
callbacks.onEnter?.()
|
|
98
|
-
const height = content.scrollHeight
|
|
99
|
-
wrapper.style.transition = 'none'
|
|
100
|
-
wrapper.style.height = '0px'
|
|
101
|
-
wrapper.style.overflow = 'hidden'
|
|
102
|
-
// Force reflow so the browser registers height: 0
|
|
103
|
-
void wrapper.offsetHeight
|
|
104
|
-
wrapper.style.transition = transition
|
|
105
|
-
wrapper.style.height = `${height}px`
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
if (currentStage === 'leaving') {
|
|
109
|
-
callbacks.onLeave?.()
|
|
110
|
-
const height = content.scrollHeight
|
|
111
|
-
wrapper.style.transition = 'none'
|
|
112
|
-
wrapper.style.height = `${height}px`
|
|
113
|
-
wrapper.style.overflow = 'hidden'
|
|
114
|
-
// Force reflow
|
|
115
|
-
void wrapper.offsetHeight
|
|
116
|
-
wrapper.style.transition = transition
|
|
117
|
-
wrapper.style.height = '0px'
|
|
118
|
-
}
|
|
119
|
-
},
|
|
120
|
-
{ immediate: true },
|
|
121
|
-
)
|
|
122
|
-
|
|
123
|
-
// Listen for animation end
|
|
124
|
-
useAnimationEnd({
|
|
125
|
-
ref: wrapperRef,
|
|
126
|
-
active: () => (stage() === 'entering' || stage() === 'leaving') && !reducedMotion(),
|
|
127
|
-
timeout,
|
|
128
|
-
onEnd: () => {
|
|
129
|
-
const wrapper = wrapperRef.current
|
|
130
|
-
if (stage() === 'entering') {
|
|
131
|
-
if (wrapper) {
|
|
132
|
-
wrapper.style.height = 'auto'
|
|
133
|
-
wrapper.style.overflow = ''
|
|
134
|
-
wrapper.style.transition = ''
|
|
135
|
-
}
|
|
136
|
-
callbacks.onAfterEnter?.()
|
|
137
|
-
stage.set('entered')
|
|
138
|
-
} else if (stage() === 'leaving') {
|
|
139
|
-
callbacks.onAfterLeave?.()
|
|
140
|
-
stage.set('hidden')
|
|
141
|
-
}
|
|
142
|
-
},
|
|
143
|
-
})
|
|
144
|
-
|
|
145
|
-
const shouldRender = () => stage() !== 'hidden'
|
|
146
|
-
|
|
147
|
-
return (
|
|
148
|
-
<div
|
|
149
|
-
ref={wrapperRef}
|
|
150
|
-
style={{
|
|
151
|
-
...(stage() !== 'entered' ? { overflow: 'hidden' } : {}),
|
|
152
|
-
...(stage() === 'hidden'
|
|
153
|
-
? { height: '0px' }
|
|
154
|
-
: stage() === 'entered'
|
|
155
|
-
? { height: 'auto' }
|
|
156
|
-
: {}),
|
|
157
|
-
}}
|
|
158
|
-
>
|
|
159
|
-
<Show when={shouldRender}>
|
|
160
|
-
<div ref={contentRef}>{props.children}</div>
|
|
161
|
-
</Show>
|
|
162
|
-
</div>
|
|
163
|
-
)
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
export default Collapse
|
package/src/Stagger.tsx
DELETED
|
@@ -1,63 +0,0 @@
|
|
|
1
|
-
import type { VNode } from '@pyreon/core'
|
|
2
|
-
import { splitProps } from '@pyreon/core'
|
|
3
|
-
import Transition from './Transition'
|
|
4
|
-
import type { CSSProperties, StaggerProps } from './types'
|
|
5
|
-
import { cloneVNode, resolveChildren } from './utils'
|
|
6
|
-
|
|
7
|
-
const isVNode = (child: unknown): child is VNode =>
|
|
8
|
-
child != null && typeof child === 'object' && 'type' in (child as object)
|
|
9
|
-
|
|
10
|
-
const Stagger = (props: StaggerProps): VNode | null => {
|
|
11
|
-
const [own, transitionProps] = splitProps(props, [
|
|
12
|
-
'show',
|
|
13
|
-
'interval',
|
|
14
|
-
'reverseLeave',
|
|
15
|
-
'appear',
|
|
16
|
-
'timeout',
|
|
17
|
-
'children',
|
|
18
|
-
'onAfterLeave',
|
|
19
|
-
])
|
|
20
|
-
const interval = own.interval ?? 50
|
|
21
|
-
const reverseLeave = own.reverseLeave ?? false
|
|
22
|
-
const appear = own.appear ?? false
|
|
23
|
-
const timeout = own.timeout ?? 5000
|
|
24
|
-
|
|
25
|
-
// Unwrap the compiler's `() => x` accessor wrap — see `resolveChildren`
|
|
26
|
-
// jsdoc. PR #731 fixed this on `StaggerRenderer` (the internal kinetic-
|
|
27
|
-
// mode renderer); this is the parallel fix for the top-level `<Stagger>`
|
|
28
|
-
// component, which has the same iteration shape and the same bug.
|
|
29
|
-
const resolved = resolveChildren(own.children)
|
|
30
|
-
const childArray = (Array.isArray(resolved) ? resolved : [resolved]).filter(isVNode)
|
|
31
|
-
const count = childArray.length
|
|
32
|
-
|
|
33
|
-
return (
|
|
34
|
-
<>
|
|
35
|
-
{childArray.map((child, index) => {
|
|
36
|
-
const staggerIndex = !own.show() && reverseLeave ? count - 1 - index : index
|
|
37
|
-
const delay = staggerIndex * interval
|
|
38
|
-
|
|
39
|
-
return (
|
|
40
|
-
<Transition
|
|
41
|
-
key={(child as VNode & { key?: string | number }).key ?? index}
|
|
42
|
-
show={own.show}
|
|
43
|
-
appear={appear}
|
|
44
|
-
timeout={timeout + delay}
|
|
45
|
-
{...transitionProps}
|
|
46
|
-
onAfterLeave={index === (reverseLeave ? 0 : count - 1) ? own.onAfterLeave : undefined}
|
|
47
|
-
>
|
|
48
|
-
{cloneVNode(child, {
|
|
49
|
-
style: {
|
|
50
|
-
...((child.props as Record<string, unknown>)?.style as CSSProperties | undefined),
|
|
51
|
-
'--stagger-index': staggerIndex,
|
|
52
|
-
'--stagger-interval': `${interval}ms`,
|
|
53
|
-
transitionDelay: `${delay}ms`,
|
|
54
|
-
} as CSSProperties,
|
|
55
|
-
})}
|
|
56
|
-
</Transition>
|
|
57
|
-
)
|
|
58
|
-
})}
|
|
59
|
-
</>
|
|
60
|
-
)
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
export default Stagger
|
package/src/Transition.tsx
DELETED
|
@@ -1,280 +0,0 @@
|
|
|
1
|
-
import type { VNode } from '@pyreon/core'
|
|
2
|
-
import { createRef, cx, Show } from '@pyreon/core'
|
|
3
|
-
import { watch } from '@pyreon/reactivity'
|
|
4
|
-
import type { ClassTransitionProps, StyleTransitionProps, TransitionProps } from './types'
|
|
5
|
-
import useAnimationEnd from './useAnimationEnd'
|
|
6
|
-
import { useReducedMotion } from './useReducedMotion'
|
|
7
|
-
import useTransitionState from './useTransitionState'
|
|
8
|
-
import {
|
|
9
|
-
addClasses,
|
|
10
|
-
cloneVNode,
|
|
11
|
-
mergeRefs,
|
|
12
|
-
mergeStyles,
|
|
13
|
-
nextFrame,
|
|
14
|
-
removeClasses,
|
|
15
|
-
resolveChildren,
|
|
16
|
-
} from './utils'
|
|
17
|
-
|
|
18
|
-
const applyEnter = (
|
|
19
|
-
el: HTMLElement,
|
|
20
|
-
{
|
|
21
|
-
enter,
|
|
22
|
-
enterFrom,
|
|
23
|
-
enterTo,
|
|
24
|
-
enterStyle,
|
|
25
|
-
enterToStyle,
|
|
26
|
-
enterTransition,
|
|
27
|
-
leave,
|
|
28
|
-
leaveFrom,
|
|
29
|
-
leaveTo,
|
|
30
|
-
}: ClassTransitionProps & StyleTransitionProps,
|
|
31
|
-
) => {
|
|
32
|
-
// Symmetric to applyLeave's `removeClasses(enter)` / `removeClasses(enterTo)`:
|
|
33
|
-
// clear any residual leave-cycle classes — including the `leaveTo` /
|
|
34
|
-
// `enterFrom` class the SSR / initial-hidden render path inlines for
|
|
35
|
-
// ecosystem-correct structural content (see the `wasInitiallyShown`
|
|
36
|
-
// branch below). Without this, the SSR-baked hidden-state class would
|
|
37
|
-
// compete with `enterTo`'s CSS rules and the enter animation would
|
|
38
|
-
// visually fight itself.
|
|
39
|
-
removeClasses(el, leave)
|
|
40
|
-
removeClasses(el, leaveFrom)
|
|
41
|
-
removeClasses(el, leaveTo)
|
|
42
|
-
|
|
43
|
-
addClasses(el, enter)
|
|
44
|
-
addClasses(el, enterFrom)
|
|
45
|
-
if (enterStyle) Object.assign(el.style, enterStyle)
|
|
46
|
-
if (enterTransition) el.style.transition = enterTransition
|
|
47
|
-
|
|
48
|
-
return nextFrame(() => {
|
|
49
|
-
removeClasses(el, enterFrom)
|
|
50
|
-
addClasses(el, enterTo)
|
|
51
|
-
if (enterToStyle) Object.assign(el.style, enterToStyle)
|
|
52
|
-
})
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
const applyLeave = (
|
|
56
|
-
el: HTMLElement,
|
|
57
|
-
{
|
|
58
|
-
enter,
|
|
59
|
-
enterTo,
|
|
60
|
-
leave,
|
|
61
|
-
leaveFrom,
|
|
62
|
-
leaveTo,
|
|
63
|
-
leaveStyle,
|
|
64
|
-
leaveToStyle,
|
|
65
|
-
leaveTransition,
|
|
66
|
-
}: ClassTransitionProps & StyleTransitionProps,
|
|
67
|
-
) => {
|
|
68
|
-
removeClasses(el, enter)
|
|
69
|
-
removeClasses(el, enterTo)
|
|
70
|
-
|
|
71
|
-
addClasses(el, leave)
|
|
72
|
-
addClasses(el, leaveFrom)
|
|
73
|
-
if (leaveStyle) Object.assign(el.style, leaveStyle)
|
|
74
|
-
if (leaveTransition) el.style.transition = leaveTransition
|
|
75
|
-
|
|
76
|
-
return nextFrame(() => {
|
|
77
|
-
removeClasses(el, leaveFrom)
|
|
78
|
-
addClasses(el, leaveTo)
|
|
79
|
-
if (leaveToStyle) Object.assign(el.style, leaveToStyle)
|
|
80
|
-
})
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
const applyReducedMotion = (
|
|
84
|
-
stage: string,
|
|
85
|
-
callbacks: {
|
|
86
|
-
onEnter?: (() => void) | undefined
|
|
87
|
-
onAfterEnter?: (() => void) | undefined
|
|
88
|
-
onLeave?: (() => void) | undefined
|
|
89
|
-
onAfterLeave?: (() => void) | undefined
|
|
90
|
-
},
|
|
91
|
-
complete: () => void,
|
|
92
|
-
) => {
|
|
93
|
-
if (stage === 'entering') {
|
|
94
|
-
callbacks.onEnter?.()
|
|
95
|
-
callbacks.onAfterEnter?.()
|
|
96
|
-
complete()
|
|
97
|
-
} else if (stage === 'leaving') {
|
|
98
|
-
callbacks.onLeave?.()
|
|
99
|
-
callbacks.onAfterLeave?.()
|
|
100
|
-
complete()
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
const Transition = (props: TransitionProps): VNode | null => {
|
|
105
|
-
const appear = props.appear ?? false
|
|
106
|
-
const unmount = props.unmount ?? true
|
|
107
|
-
const timeout = props.timeout ?? 5000
|
|
108
|
-
|
|
109
|
-
const reducedMotion = useReducedMotion()
|
|
110
|
-
const {
|
|
111
|
-
stage,
|
|
112
|
-
ref: stateRef,
|
|
113
|
-
shouldMount,
|
|
114
|
-
complete,
|
|
115
|
-
} = useTransitionState({
|
|
116
|
-
show: props.show,
|
|
117
|
-
appear,
|
|
118
|
-
})
|
|
119
|
-
|
|
120
|
-
// Unwrap the compiler's `() => x` accessor wrap — see `resolveChildren`
|
|
121
|
-
// jsdoc. Parallel to `TransitionItem`'s fix (PR #731). Without this,
|
|
122
|
-
// `props.children.props` reads `function.props` (undefined), the merged
|
|
123
|
-
// ref is missing the child's own ref, and the downstream `cloneVNode`
|
|
124
|
-
// calls produce `{type: undefined}` → `<undefined>` DOM tags.
|
|
125
|
-
const child = resolveChildren(props.children) as VNode
|
|
126
|
-
const elementRef = createRef<HTMLElement>()
|
|
127
|
-
const childProps = (child?.props ?? {}) as Record<string, unknown>
|
|
128
|
-
const mergedRef = mergeRefs(
|
|
129
|
-
elementRef,
|
|
130
|
-
stateRef,
|
|
131
|
-
childProps.ref as ((el: HTMLElement | null) => void) | undefined,
|
|
132
|
-
)
|
|
133
|
-
|
|
134
|
-
const callbacks = {
|
|
135
|
-
onEnter: props.onEnter,
|
|
136
|
-
onAfterEnter: props.onAfterEnter,
|
|
137
|
-
onLeave: props.onLeave,
|
|
138
|
-
onAfterLeave: props.onAfterLeave,
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
const transitionConfig = {
|
|
142
|
-
enter: props.enter,
|
|
143
|
-
enterFrom: props.enterFrom,
|
|
144
|
-
enterTo: props.enterTo,
|
|
145
|
-
leave: props.leave,
|
|
146
|
-
leaveFrom: props.leaveFrom,
|
|
147
|
-
leaveTo: props.leaveTo,
|
|
148
|
-
enterStyle: props.enterStyle,
|
|
149
|
-
enterToStyle: props.enterToStyle,
|
|
150
|
-
enterTransition: props.enterTransition,
|
|
151
|
-
leaveStyle: props.leaveStyle,
|
|
152
|
-
leaveToStyle: props.leaveToStyle,
|
|
153
|
-
leaveTransition: props.leaveTransition,
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
useAnimationEnd({
|
|
157
|
-
ref: elementRef,
|
|
158
|
-
active: () => (stage() === 'entering' || stage() === 'leaving') && !reducedMotion(),
|
|
159
|
-
timeout,
|
|
160
|
-
onEnd: () => {
|
|
161
|
-
if (stage() === 'entering') {
|
|
162
|
-
callbacks.onAfterEnter?.()
|
|
163
|
-
} else if (stage() === 'leaving') {
|
|
164
|
-
callbacks.onAfterLeave?.()
|
|
165
|
-
}
|
|
166
|
-
complete()
|
|
167
|
-
},
|
|
168
|
-
})
|
|
169
|
-
|
|
170
|
-
watch(
|
|
171
|
-
() => stage(),
|
|
172
|
-
(currentStage) => {
|
|
173
|
-
const el = elementRef.current
|
|
174
|
-
if (!el) return
|
|
175
|
-
|
|
176
|
-
if (reducedMotion()) {
|
|
177
|
-
applyReducedMotion(currentStage, callbacks, complete)
|
|
178
|
-
return
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
if (currentStage === 'entering') {
|
|
182
|
-
callbacks.onEnter?.()
|
|
183
|
-
const frameId = applyEnter(el, transitionConfig)
|
|
184
|
-
return () => cancelAnimationFrame(frameId)
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
if (currentStage === 'leaving') {
|
|
188
|
-
callbacks.onLeave?.()
|
|
189
|
-
const frameId = applyLeave(el, transitionConfig)
|
|
190
|
-
return () => cancelAnimationFrame(frameId)
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
if (currentStage === 'entered') {
|
|
194
|
-
removeClasses(el, props.enter)
|
|
195
|
-
el.style.transition = ''
|
|
196
|
-
}
|
|
197
|
-
},
|
|
198
|
-
{ immediate: true },
|
|
199
|
-
)
|
|
200
|
-
|
|
201
|
-
// Initially-visible Transitions keep the original Show-gated mount,
|
|
202
|
-
// which preserves the documented runtime-unmount semantic for the
|
|
203
|
-
// visible → hidden transition (modal close, dropdown collapse, etc.).
|
|
204
|
-
// The SSR bug (children dropped from prerendered HTML) only fires for
|
|
205
|
-
// the initially-HIDDEN case below, because `<Show when={false}>`
|
|
206
|
-
// renders `null` on the server.
|
|
207
|
-
const wasInitiallyShown = props.show()
|
|
208
|
-
if (wasInitiallyShown) {
|
|
209
|
-
return (
|
|
210
|
-
<Show
|
|
211
|
-
when={shouldMount}
|
|
212
|
-
fallback={
|
|
213
|
-
unmount
|
|
214
|
-
? null
|
|
215
|
-
: cloneVNode(child, {
|
|
216
|
-
ref: mergedRef,
|
|
217
|
-
style: mergeStyles(
|
|
218
|
-
childProps.style as Record<string, string | number | undefined> | undefined,
|
|
219
|
-
{ display: 'none' },
|
|
220
|
-
),
|
|
221
|
-
})
|
|
222
|
-
}
|
|
223
|
-
>
|
|
224
|
-
{cloneVNode(child, { ref: mergedRef })}
|
|
225
|
-
</Show>
|
|
226
|
-
)
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
// Initially-hidden path — ecosystem-correct (Framer Motion / react-
|
|
230
|
-
// transition-group / react-spring all render children in SSR regardless
|
|
231
|
-
// of animation state; visual hiding is class/style only). Always emits
|
|
232
|
-
// children so SSG / SEO / social scrapers / no-JS users see the
|
|
233
|
-
// structural content. The hidden visual is supplied by `leaveTo`
|
|
234
|
-
// (explicit hidden-end state) or `enterFrom` (pre-enter state — covers
|
|
235
|
-
// the scroll-reveal pattern that only configures the enter side).
|
|
236
|
-
//
|
|
237
|
-
// Trade-off: for an initially-hidden Transition, `unmount: true` no
|
|
238
|
-
// longer triggers a true DOM removal after a later leave animation
|
|
239
|
-
// completes — the element stays in DOM with the leave-to class
|
|
240
|
-
// applied. Initially-visible Transitions keep the unmount semantic
|
|
241
|
-
// (the branch above). This matches Framer Motion / react-transition-
|
|
242
|
-
// group conventions and is the price of SSR correctness; the rare
|
|
243
|
-
// user who needs true unmount on a started-hidden element can drive
|
|
244
|
-
// mount/unmount themselves outside `<Transition>`.
|
|
245
|
-
//
|
|
246
|
-
// The `watch(stage)` effect above drives the enter animation when
|
|
247
|
-
// `show` flips true; `applyEnter` (above) clears these residual
|
|
248
|
-
// hidden-state classes so they don't fight `enterTo`.
|
|
249
|
-
// Picker mirrors what #719 introduced for the kinetic(tag).<mode>
|
|
250
|
-
// renderers (TransitionRenderer / TransitionItem / CollapseRenderer):
|
|
251
|
-
// prefer leave-end state, fall back to pre-enter state. The
|
|
252
|
-
// `enterStyle` fallback covers the preset path — `@pyreon/kinetic-presets`
|
|
253
|
-
// factories (fadeUp, blurInUp, slideLeft, …) populate `enterStyle` as
|
|
254
|
-
// the hidden state but may not set `leaveToStyle`. Without this
|
|
255
|
-
// fallback, preset users SSR-render VISIBLE → flash-on-hydration.
|
|
256
|
-
// (PR #717 shipped this branch with `leaveToStyle` alone; the class
|
|
257
|
-
// picker already had the `enterFrom` fallback. This commit aligns the
|
|
258
|
-
// style picker so both halves match.)
|
|
259
|
-
const hiddenClass = props.leaveTo ?? props.enterFrom
|
|
260
|
-
const hiddenStyle = props.leaveToStyle ?? props.enterStyle
|
|
261
|
-
const childClass = childProps.class
|
|
262
|
-
const mergedClass = hiddenClass
|
|
263
|
-
? cx([childClass as Parameters<typeof cx>[0], hiddenClass])
|
|
264
|
-
: undefined
|
|
265
|
-
const mergedStyle = mergeStyles(
|
|
266
|
-
childProps.style as Record<string, string | number | undefined> | undefined,
|
|
267
|
-
hiddenStyle,
|
|
268
|
-
)
|
|
269
|
-
|
|
270
|
-
// Build extra-props carefully — undefined values must NOT be passed to
|
|
271
|
-
// cloneVNode because `{...vnode.props, ...extraProps}` spreads them and
|
|
272
|
-
// overrides any user-set `class`/`style` on the child vnode with undefined.
|
|
273
|
-
const extra: Record<string, unknown> = { ref: mergedRef }
|
|
274
|
-
if (mergedClass !== undefined) extra.class = mergedClass
|
|
275
|
-
if (mergedStyle !== undefined) extra.style = mergedStyle
|
|
276
|
-
|
|
277
|
-
return cloneVNode(child, extra)
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
export default Transition
|
package/src/TransitionGroup.tsx
DELETED
|
@@ -1,139 +0,0 @@
|
|
|
1
|
-
import type { VNode } from '@pyreon/core'
|
|
2
|
-
import { splitProps } from '@pyreon/core'
|
|
3
|
-
import { signal } from '@pyreon/reactivity'
|
|
4
|
-
import Transition from './Transition'
|
|
5
|
-
import type { ClassTransitionProps, StyleTransitionProps, TransitionCallbacks } from './types'
|
|
6
|
-
|
|
7
|
-
export type TransitionGroupProps = ClassTransitionProps &
|
|
8
|
-
StyleTransitionProps &
|
|
9
|
-
TransitionCallbacks & {
|
|
10
|
-
appear?: boolean | undefined
|
|
11
|
-
timeout?: number | undefined
|
|
12
|
-
/**
|
|
13
|
-
* Children can be a static array OR a reactive accessor `() => VNode[]`.
|
|
14
|
-
* When passed as an accessor, TransitionGroup tracks changes and
|
|
15
|
-
* animates entering/leaving children automatically.
|
|
16
|
-
*/
|
|
17
|
-
children: VNode[] | (() => VNode[])
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
type KeyedChild = { key: string | number; element: VNode }
|
|
21
|
-
|
|
22
|
-
const isVNode = (child: unknown): child is VNode =>
|
|
23
|
-
child != null && typeof child === 'object' && 'type' in (child as object)
|
|
24
|
-
|
|
25
|
-
const getKeyedChildren = (children: VNode[]): KeyedChild[] => {
|
|
26
|
-
const result: KeyedChild[] = []
|
|
27
|
-
for (const child of children) {
|
|
28
|
-
if (isVNode(child)) {
|
|
29
|
-
const key = (child as VNode & { key?: string | number }).key
|
|
30
|
-
if (key != null) {
|
|
31
|
-
result.push({ key, element: child })
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
return result
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* Renders children with key-based enter/exit animations.
|
|
40
|
-
*
|
|
41
|
-
* In Pyreon, components run once. For TransitionGroup to detect children
|
|
42
|
-
* changes, pass children as a reactive accessor: `() => VNode[]`.
|
|
43
|
-
* The component uses a reactive accessor internally to diff previous vs
|
|
44
|
-
* current children and animate entries/exits.
|
|
45
|
-
*/
|
|
46
|
-
const TransitionGroup = (props: TransitionGroupProps): VNode | null => {
|
|
47
|
-
const [own, transitionProps] = splitProps(props, [
|
|
48
|
-
'children',
|
|
49
|
-
'appear',
|
|
50
|
-
'timeout',
|
|
51
|
-
'onAfterLeave',
|
|
52
|
-
])
|
|
53
|
-
const appear = own.appear ?? false
|
|
54
|
-
const prevMap = new Map<string | number, VNode>()
|
|
55
|
-
const leavingMap = new Map<string | number, VNode>()
|
|
56
|
-
const forceUpdate = signal(0)
|
|
57
|
-
|
|
58
|
-
// Normalize children to an accessor for uniform handling
|
|
59
|
-
const getChildren =
|
|
60
|
-
typeof own.children === 'function'
|
|
61
|
-
? (own.children as () => VNode[])
|
|
62
|
-
: () => own.children as VNode[]
|
|
63
|
-
|
|
64
|
-
// Track initial keys for appear animation logic
|
|
65
|
-
const initialKeyed = getKeyedChildren(getChildren())
|
|
66
|
-
const initialKeys = new Set(initialKeyed.map((c) => c.key))
|
|
67
|
-
for (const { key, element } of initialKeyed) {
|
|
68
|
-
prevMap.set(key, element)
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
const handleAfterLeave = (key: string | number) => {
|
|
72
|
-
leavingMap.delete(key)
|
|
73
|
-
own.onAfterLeave?.()
|
|
74
|
-
forceUpdate.update((c) => c + 1)
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
// Reactive accessor — re-evaluates when children() or forceUpdate changes.
|
|
78
|
-
// The runtime mounts this via mountReactive + effect, creating a
|
|
79
|
-
// reactive scope that tracks signal reads.
|
|
80
|
-
return (() => {
|
|
81
|
-
// Read forceUpdate to re-evaluate when leaving children finish
|
|
82
|
-
forceUpdate()
|
|
83
|
-
|
|
84
|
-
const currentChildren = getChildren()
|
|
85
|
-
const currentKeyed = getKeyedChildren(currentChildren)
|
|
86
|
-
const currentMap = new Map<string | number, VNode>()
|
|
87
|
-
for (const { key, element } of currentKeyed) {
|
|
88
|
-
currentMap.set(key, element)
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
// Detect leaving children (were in prev but not in current)
|
|
92
|
-
for (const [key, child] of prevMap) {
|
|
93
|
-
if (!currentMap.has(key) && !leavingMap.has(key)) {
|
|
94
|
-
leavingMap.set(key, child)
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
// If a leaving child reappears, cancel the leave
|
|
99
|
-
for (const key of currentMap.keys()) {
|
|
100
|
-
leavingMap.delete(key)
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
// Update prev for next diff
|
|
104
|
-
prevMap.clear()
|
|
105
|
-
for (const [key, element] of currentMap) {
|
|
106
|
-
prevMap.set(key, element)
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
// Merge current + leaving, preserving insertion order
|
|
110
|
-
const allEntries: KeyedChild[] = [...currentKeyed]
|
|
111
|
-
for (const [key, element] of leavingMap) {
|
|
112
|
-
allEntries.push({ key, element })
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
return (
|
|
116
|
-
<>
|
|
117
|
-
{allEntries.map(({ key, element }) => {
|
|
118
|
-
const isInitial = initialKeys.has(key)
|
|
119
|
-
const isShowing = currentMap.has(key)
|
|
120
|
-
|
|
121
|
-
return (
|
|
122
|
-
<Transition
|
|
123
|
-
key={key}
|
|
124
|
-
show={() => isShowing}
|
|
125
|
-
appear={isInitial ? appear : true}
|
|
126
|
-
timeout={own.timeout}
|
|
127
|
-
{...transitionProps}
|
|
128
|
-
onAfterLeave={() => handleAfterLeave(key)}
|
|
129
|
-
>
|
|
130
|
-
{element}
|
|
131
|
-
</Transition>
|
|
132
|
-
)
|
|
133
|
-
})}
|
|
134
|
-
</>
|
|
135
|
-
)
|
|
136
|
-
}) as unknown as VNode
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
export default TransitionGroup
|