@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
|
@@ -1,94 +0,0 @@
|
|
|
1
|
-
import type { VNode } from '@pyreon/core'
|
|
2
|
-
import { h } from '@pyreon/core'
|
|
3
|
-
import type { CSSProperties, TransitionCallbacks } from '../types'
|
|
4
|
-
import { cloneVNode, resolveChildren } from '../utils'
|
|
5
|
-
import TransitionItem from './TransitionItem'
|
|
6
|
-
import type { KineticConfig } from './types'
|
|
7
|
-
|
|
8
|
-
type StaggerRendererProps = {
|
|
9
|
-
config: KineticConfig
|
|
10
|
-
htmlProps: Record<string, unknown>
|
|
11
|
-
show: () => boolean
|
|
12
|
-
appear?: boolean | undefined
|
|
13
|
-
timeout?: number | undefined
|
|
14
|
-
interval?: number | undefined
|
|
15
|
-
reverseLeave?: boolean | undefined
|
|
16
|
-
callbacks: Partial<TransitionCallbacks>
|
|
17
|
-
children: VNode[]
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
const isVNode = (child: unknown): child is VNode =>
|
|
21
|
-
child != null && typeof child === 'object' && 'type' in (child as object)
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* Renders children with staggered enter/exit animation.
|
|
25
|
-
* config.tag wraps the staggered children as a container element.
|
|
26
|
-
* Each child is individually animated via TransitionItem.
|
|
27
|
-
*/
|
|
28
|
-
const StaggerRenderer = ({
|
|
29
|
-
config,
|
|
30
|
-
htmlProps,
|
|
31
|
-
show,
|
|
32
|
-
appear,
|
|
33
|
-
timeout,
|
|
34
|
-
interval,
|
|
35
|
-
reverseLeave,
|
|
36
|
-
callbacks,
|
|
37
|
-
children,
|
|
38
|
-
}: StaggerRendererProps): VNode | null => {
|
|
39
|
-
const effectiveAppear = appear ?? config.appear ?? false
|
|
40
|
-
const effectiveTimeout = timeout ?? config.timeout ?? 5000
|
|
41
|
-
const effectiveInterval = interval ?? config.interval ?? 50
|
|
42
|
-
const effectiveReverseLeave = reverseLeave ?? config.reverseLeave ?? false
|
|
43
|
-
|
|
44
|
-
// Unwrap compiler-emitted accessor wrap — see `resolveChildren` jsdoc.
|
|
45
|
-
const resolved = resolveChildren(children)
|
|
46
|
-
const childArray = (Array.isArray(resolved) ? resolved : [resolved]).filter(isVNode)
|
|
47
|
-
const count = childArray.length
|
|
48
|
-
|
|
49
|
-
const staggeredChildren = childArray.map((child, index) => {
|
|
50
|
-
const staggerIndex = !show() && effectiveReverseLeave ? count - 1 - index : index
|
|
51
|
-
const delay = staggerIndex * effectiveInterval
|
|
52
|
-
|
|
53
|
-
return (
|
|
54
|
-
<TransitionItem
|
|
55
|
-
key={(child as VNode & { key?: string | number }).key ?? index}
|
|
56
|
-
show={show}
|
|
57
|
-
appear={effectiveAppear}
|
|
58
|
-
timeout={effectiveTimeout + delay}
|
|
59
|
-
enterStyle={config.enterStyle}
|
|
60
|
-
enterToStyle={config.enterToStyle}
|
|
61
|
-
enterTransition={config.enterTransition}
|
|
62
|
-
leaveStyle={config.leaveStyle}
|
|
63
|
-
leaveToStyle={config.leaveToStyle}
|
|
64
|
-
leaveTransition={config.leaveTransition}
|
|
65
|
-
enter={config.enter}
|
|
66
|
-
enterFrom={config.enterFrom}
|
|
67
|
-
enterTo={config.enterTo}
|
|
68
|
-
leave={config.leave}
|
|
69
|
-
leaveFrom={config.leaveFrom}
|
|
70
|
-
leaveTo={config.leaveTo}
|
|
71
|
-
onAfterLeave={
|
|
72
|
-
index === (effectiveReverseLeave ? 0 : count - 1) ? callbacks.onAfterLeave : undefined
|
|
73
|
-
}
|
|
74
|
-
>
|
|
75
|
-
{cloneVNode(child, {
|
|
76
|
-
style: {
|
|
77
|
-
...((child.props as Record<string, unknown>)?.style as CSSProperties | undefined),
|
|
78
|
-
'--stagger-index': staggerIndex,
|
|
79
|
-
'--stagger-interval': `${effectiveInterval}ms`,
|
|
80
|
-
transitionDelay: `${delay}ms`,
|
|
81
|
-
} as CSSProperties,
|
|
82
|
-
})}
|
|
83
|
-
</TransitionItem>
|
|
84
|
-
)
|
|
85
|
-
})
|
|
86
|
-
|
|
87
|
-
// Pass htmlProps by reference — `{ ...htmlProps }` value-copies, firing
|
|
88
|
-
// any reactive getter the kinetic split preserved (frozen attr forever).
|
|
89
|
-
// runtime-dom's applyProps detects the getter descriptor on the live
|
|
90
|
-
// object and wraps it in renderEffect.
|
|
91
|
-
return h(config.tag, htmlProps, ...staggeredChildren)
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
export default StaggerRenderer
|
|
@@ -1,250 +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, TransitionCallbacks } 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
|
-
type TransitionItemProps = ClassTransitionProps &
|
|
19
|
-
StyleTransitionProps &
|
|
20
|
-
TransitionCallbacks & {
|
|
21
|
-
show: () => boolean
|
|
22
|
-
appear?: boolean | undefined
|
|
23
|
-
unmount?: boolean | undefined
|
|
24
|
-
timeout?: number | undefined
|
|
25
|
-
delay?: number | undefined
|
|
26
|
-
children: VNode
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
const applyEnter = (el: HTMLElement, config: ClassTransitionProps & StyleTransitionProps) => {
|
|
30
|
-
// Symmetric to applyLeave: clear residual leave-cycle classes — including
|
|
31
|
-
// the `leaveTo`/`enterFrom` class the SSR/initially-hidden render path
|
|
32
|
-
// inlines (see the `wasInitiallyShown` branch below). Without this, the
|
|
33
|
-
// SSR-baked hidden class would compete with `enterTo`'s CSS rules.
|
|
34
|
-
removeClasses(el, config.leave)
|
|
35
|
-
removeClasses(el, config.leaveFrom)
|
|
36
|
-
removeClasses(el, config.leaveTo)
|
|
37
|
-
|
|
38
|
-
addClasses(el, config.enter)
|
|
39
|
-
addClasses(el, config.enterFrom)
|
|
40
|
-
if (config.enterStyle) Object.assign(el.style, config.enterStyle)
|
|
41
|
-
if (config.enterTransition) el.style.transition = config.enterTransition
|
|
42
|
-
|
|
43
|
-
return nextFrame(() => {
|
|
44
|
-
removeClasses(el, config.enterFrom)
|
|
45
|
-
addClasses(el, config.enterTo)
|
|
46
|
-
if (config.enterToStyle) Object.assign(el.style, config.enterToStyle)
|
|
47
|
-
})
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
const applyLeave = (el: HTMLElement, config: ClassTransitionProps & StyleTransitionProps) => {
|
|
51
|
-
removeClasses(el, config.enter)
|
|
52
|
-
removeClasses(el, config.enterTo)
|
|
53
|
-
|
|
54
|
-
addClasses(el, config.leave)
|
|
55
|
-
addClasses(el, config.leaveFrom)
|
|
56
|
-
if (config.leaveStyle) Object.assign(el.style, config.leaveStyle)
|
|
57
|
-
if (config.leaveTransition) el.style.transition = config.leaveTransition
|
|
58
|
-
|
|
59
|
-
return nextFrame(() => {
|
|
60
|
-
removeClasses(el, config.leaveFrom)
|
|
61
|
-
addClasses(el, config.leaveTo)
|
|
62
|
-
if (config.leaveToStyle) Object.assign(el.style, config.leaveToStyle)
|
|
63
|
-
})
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
const applyReducedMotion = (
|
|
67
|
-
stage: string,
|
|
68
|
-
callbacks: Partial<TransitionCallbacks>,
|
|
69
|
-
complete: () => void,
|
|
70
|
-
) => {
|
|
71
|
-
if (stage === 'entering') {
|
|
72
|
-
callbacks.onEnter?.()
|
|
73
|
-
callbacks.onAfterEnter?.()
|
|
74
|
-
complete()
|
|
75
|
-
} else if (stage === 'leaving') {
|
|
76
|
-
callbacks.onLeave?.()
|
|
77
|
-
callbacks.onAfterLeave?.()
|
|
78
|
-
complete()
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
/**
|
|
83
|
-
* Internal per-child transition component. Used by StaggerRenderer and
|
|
84
|
-
* GroupRenderer to give each child its own animation state.
|
|
85
|
-
*
|
|
86
|
-
* Uses cloneVNode to inject ref onto the child — the child must accept ref.
|
|
87
|
-
*/
|
|
88
|
-
const TransitionItem = (props: TransitionItemProps): VNode | null => {
|
|
89
|
-
// The Pyreon compiler wraps `{cloneVNode(child, {...})}` JSX child
|
|
90
|
-
// expressions in StaggerRenderer/GroupRenderer as `() => cloneVNode(...)`
|
|
91
|
-
// (prop-inlining for reactivity — see `resolveChildren` jsdoc). At this
|
|
92
|
-
// boundary `props.children` therefore arrives as a FUNCTION instead of a
|
|
93
|
-
// VNode. cloneVNode-on-a-function silently produces `{type: undefined,
|
|
94
|
-
// props: {ref: ...}}` (spreading a function yields no own properties
|
|
95
|
-
// because functions have none enumerable), which mountElement renders
|
|
96
|
-
// as a literal `<undefined>` tag in the DOM — the SSG'd `<h1>Hello</h1>`
|
|
97
|
-
// becomes an empty `<undefined></undefined>` post-hydrate (reproducer:
|
|
98
|
-
// bokisch.com Intro section). Resolve eagerly so the entire body below
|
|
99
|
-
// can treat `child` as a static VNode.
|
|
100
|
-
const child = resolveChildren(props.children) as VNode
|
|
101
|
-
const appear = props.appear ?? false
|
|
102
|
-
const unmount = props.unmount ?? true
|
|
103
|
-
const timeout = props.timeout ?? 5000
|
|
104
|
-
const reducedMotion = useReducedMotion()
|
|
105
|
-
const { stage, ref: stateRef, shouldMount, complete } = useTransitionState({
|
|
106
|
-
show: props.show,
|
|
107
|
-
appear,
|
|
108
|
-
})
|
|
109
|
-
|
|
110
|
-
const elementRef = createRef<HTMLElement>()
|
|
111
|
-
const mergedRef = mergeRefs(
|
|
112
|
-
elementRef,
|
|
113
|
-
stateRef,
|
|
114
|
-
(child?.props as Record<string, unknown>)?.ref as
|
|
115
|
-
| ((el: HTMLElement | null) => void)
|
|
116
|
-
| undefined,
|
|
117
|
-
)
|
|
118
|
-
|
|
119
|
-
const callbacks = {
|
|
120
|
-
onEnter: props.onEnter,
|
|
121
|
-
onAfterEnter: props.onAfterEnter,
|
|
122
|
-
onLeave: props.onLeave,
|
|
123
|
-
onAfterLeave: props.onAfterLeave,
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
const transitionConfig = {
|
|
127
|
-
enter: props.enter,
|
|
128
|
-
enterFrom: props.enterFrom,
|
|
129
|
-
enterTo: props.enterTo,
|
|
130
|
-
leave: props.leave,
|
|
131
|
-
leaveFrom: props.leaveFrom,
|
|
132
|
-
leaveTo: props.leaveTo,
|
|
133
|
-
enterStyle: props.enterStyle,
|
|
134
|
-
enterToStyle: props.enterToStyle,
|
|
135
|
-
enterTransition: props.enterTransition,
|
|
136
|
-
leaveStyle: props.leaveStyle,
|
|
137
|
-
leaveToStyle: props.leaveToStyle,
|
|
138
|
-
leaveTransition: props.leaveTransition,
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
useAnimationEnd({
|
|
142
|
-
ref: elementRef,
|
|
143
|
-
active: () => (stage() === 'entering' || stage() === 'leaving') && !reducedMotion(),
|
|
144
|
-
timeout,
|
|
145
|
-
onEnd: () => {
|
|
146
|
-
if (stage() === 'entering') {
|
|
147
|
-
callbacks.onAfterEnter?.()
|
|
148
|
-
} else if (stage() === 'leaving') {
|
|
149
|
-
callbacks.onAfterLeave?.()
|
|
150
|
-
}
|
|
151
|
-
complete()
|
|
152
|
-
},
|
|
153
|
-
})
|
|
154
|
-
|
|
155
|
-
watch(
|
|
156
|
-
() => stage(),
|
|
157
|
-
(currentStage) => {
|
|
158
|
-
const el = elementRef.current
|
|
159
|
-
if (!el) return
|
|
160
|
-
|
|
161
|
-
if (reducedMotion()) {
|
|
162
|
-
applyReducedMotion(currentStage, callbacks, complete)
|
|
163
|
-
return
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
if (currentStage === 'entering') {
|
|
167
|
-
callbacks.onEnter?.()
|
|
168
|
-
const frameId = applyEnter(el, transitionConfig)
|
|
169
|
-
return () => cancelAnimationFrame(frameId)
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
if (currentStage === 'leaving') {
|
|
173
|
-
callbacks.onLeave?.()
|
|
174
|
-
const frameId = applyLeave(el, transitionConfig)
|
|
175
|
-
return () => cancelAnimationFrame(frameId)
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
if (currentStage === 'entered') {
|
|
179
|
-
removeClasses(el, props.enter)
|
|
180
|
-
el.style.transition = ''
|
|
181
|
-
}
|
|
182
|
-
},
|
|
183
|
-
{ immediate: true },
|
|
184
|
-
)
|
|
185
|
-
|
|
186
|
-
// Initially-visible items keep the original Show-gated mount, preserving
|
|
187
|
-
// the documented runtime-unmount semantic for visible→hidden. The SSR
|
|
188
|
-
// bug (children dropped from prerendered HTML) only fires for the
|
|
189
|
-
// initially-HIDDEN case below, where `<Show when={false}>` renders `null`
|
|
190
|
-
// on the server. For Stagger/Group usage at SSR (when the parent's
|
|
191
|
-
// `show: () => false`), each per-item TransitionItem hit this and
|
|
192
|
-
// dropped its child — full list missing from prerendered HTML.
|
|
193
|
-
//
|
|
194
|
-
// Mirrors the fix in `<Transition>` (PR #717) and `TransitionRenderer`
|
|
195
|
-
// (same PR as this). Ecosystem norm: content is structural, animation
|
|
196
|
-
// is visual.
|
|
197
|
-
const wasInitiallyShown = props.show()
|
|
198
|
-
if (wasInitiallyShown) {
|
|
199
|
-
return (
|
|
200
|
-
<Show
|
|
201
|
-
when={shouldMount}
|
|
202
|
-
fallback={
|
|
203
|
-
unmount
|
|
204
|
-
? null
|
|
205
|
-
: cloneVNode(child, {
|
|
206
|
-
ref: mergedRef,
|
|
207
|
-
style: mergeStyles(
|
|
208
|
-
(child?.props as Record<string, unknown>)?.style as
|
|
209
|
-
| Record<string, string | number | undefined>
|
|
210
|
-
| undefined,
|
|
211
|
-
{ display: 'none' },
|
|
212
|
-
),
|
|
213
|
-
})
|
|
214
|
-
}
|
|
215
|
-
>
|
|
216
|
-
{cloneVNode(child, { ref: mergedRef })}
|
|
217
|
-
</Show>
|
|
218
|
-
)
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
// Initially-hidden path — always emit the child with hidden-state class +
|
|
222
|
-
// style inlined. `leaveTo`/`leaveToStyle` (explicit hidden-end state)
|
|
223
|
-
// wins; falls back to `enterFrom`/`enterStyle` (pre-enter state — covers
|
|
224
|
-
// both class-based scroll-reveal AND the preset path, where
|
|
225
|
-
// `@pyreon/kinetic-presets` factories populate `enterStyle` as the
|
|
226
|
-
// hidden state but may not set `leaveToStyle`).
|
|
227
|
-
//
|
|
228
|
-
// Trade-off: for an initially-hidden item, `unmount: true` no longer
|
|
229
|
-
// triggers a true DOM removal after a later leave animation completes.
|
|
230
|
-
// Initially-visible items keep the unmount semantic.
|
|
231
|
-
const hiddenClass = props.leaveTo ?? props.enterFrom
|
|
232
|
-
const hiddenStyle = props.leaveToStyle ?? props.enterStyle
|
|
233
|
-
const childProps = (child?.props ?? {}) as Record<string, unknown>
|
|
234
|
-
const childClass = childProps.class
|
|
235
|
-
const mergedClass = hiddenClass
|
|
236
|
-
? cx([childClass as Parameters<typeof cx>[0], hiddenClass])
|
|
237
|
-
: undefined
|
|
238
|
-
const mergedStyle = mergeStyles(
|
|
239
|
-
childProps.style as Record<string, string | number | undefined> | undefined,
|
|
240
|
-
hiddenStyle,
|
|
241
|
-
)
|
|
242
|
-
|
|
243
|
-
const extra: Record<string, unknown> = { ref: mergedRef }
|
|
244
|
-
if (mergedClass !== undefined) extra.class = mergedClass
|
|
245
|
-
if (mergedStyle !== undefined) extra.style = mergedStyle
|
|
246
|
-
|
|
247
|
-
return cloneVNode(child, extra)
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
export default TransitionItem
|
|
@@ -1,230 +0,0 @@
|
|
|
1
|
-
import type { VNode } from '@pyreon/core'
|
|
2
|
-
import { createRef, cx, h, mergeProps, Show } from '@pyreon/core'
|
|
3
|
-
import { watch } from '@pyreon/reactivity'
|
|
4
|
-
import type { CSSProperties, TransitionCallbacks } from '../types'
|
|
5
|
-
import useAnimationEnd from '../useAnimationEnd'
|
|
6
|
-
import { useReducedMotion } from '../useReducedMotion'
|
|
7
|
-
import useTransitionState from '../useTransitionState'
|
|
8
|
-
import { addClasses, mergeRefs, nextFrame, removeClasses } from '../utils'
|
|
9
|
-
import type { KineticConfig } from './types'
|
|
10
|
-
|
|
11
|
-
type TransitionRendererProps = {
|
|
12
|
-
config: KineticConfig
|
|
13
|
-
htmlProps: Record<string, unknown>
|
|
14
|
-
show: () => boolean
|
|
15
|
-
appear?: boolean | undefined
|
|
16
|
-
unmount?: boolean | undefined
|
|
17
|
-
timeout?: number | undefined
|
|
18
|
-
callbacks: Partial<TransitionCallbacks>
|
|
19
|
-
children: VNode | VNode[]
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
const applyEnter = (el: HTMLElement, config: KineticConfig) => {
|
|
23
|
-
// Symmetric to applyLeave's `removeClasses(enter)` / `removeClasses(enterTo)`:
|
|
24
|
-
// clear residual leave-cycle classes — including the `leaveTo` / `enterFrom`
|
|
25
|
-
// class the SSR / initially-hidden render path inlines for structural
|
|
26
|
-
// content (see the `wasInitiallyShown` branch below). Without this, the
|
|
27
|
-
// SSR-baked hidden-state class would compete with `enterTo`'s CSS rules.
|
|
28
|
-
removeClasses(el, config.leave)
|
|
29
|
-
removeClasses(el, config.leaveFrom)
|
|
30
|
-
removeClasses(el, config.leaveTo)
|
|
31
|
-
|
|
32
|
-
addClasses(el, config.enter)
|
|
33
|
-
addClasses(el, config.enterFrom)
|
|
34
|
-
if (config.enterStyle) Object.assign(el.style, config.enterStyle)
|
|
35
|
-
if (config.enterTransition) el.style.transition = config.enterTransition
|
|
36
|
-
|
|
37
|
-
return nextFrame(() => {
|
|
38
|
-
removeClasses(el, config.enterFrom)
|
|
39
|
-
addClasses(el, config.enterTo)
|
|
40
|
-
if (config.enterToStyle) Object.assign(el.style, config.enterToStyle)
|
|
41
|
-
})
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
const applyLeave = (el: HTMLElement, config: KineticConfig) => {
|
|
45
|
-
removeClasses(el, config.enter)
|
|
46
|
-
removeClasses(el, config.enterTo)
|
|
47
|
-
|
|
48
|
-
addClasses(el, config.leave)
|
|
49
|
-
addClasses(el, config.leaveFrom)
|
|
50
|
-
if (config.leaveStyle) Object.assign(el.style, config.leaveStyle)
|
|
51
|
-
if (config.leaveTransition) el.style.transition = config.leaveTransition
|
|
52
|
-
|
|
53
|
-
return nextFrame(() => {
|
|
54
|
-
removeClasses(el, config.leaveFrom)
|
|
55
|
-
addClasses(el, config.leaveTo)
|
|
56
|
-
if (config.leaveToStyle) Object.assign(el.style, config.leaveToStyle)
|
|
57
|
-
})
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
const applyReducedMotion = (
|
|
61
|
-
stage: string,
|
|
62
|
-
cbs: Partial<TransitionCallbacks>,
|
|
63
|
-
complete: () => void,
|
|
64
|
-
) => {
|
|
65
|
-
if (stage === 'entering') {
|
|
66
|
-
cbs.onEnter?.()
|
|
67
|
-
cbs.onAfterEnter?.()
|
|
68
|
-
complete()
|
|
69
|
-
} else if (stage === 'leaving') {
|
|
70
|
-
cbs.onLeave?.()
|
|
71
|
-
cbs.onAfterLeave?.()
|
|
72
|
-
complete()
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
/**
|
|
77
|
-
* Renders a single element with CSS transition enter/exit animation.
|
|
78
|
-
* Uses h(config.tag) — no cloneElement needed.
|
|
79
|
-
*/
|
|
80
|
-
const TransitionRenderer = (props: TransitionRendererProps): VNode | null => {
|
|
81
|
-
const reducedMotion = useReducedMotion()
|
|
82
|
-
const {
|
|
83
|
-
stage,
|
|
84
|
-
ref: stateRef,
|
|
85
|
-
shouldMount,
|
|
86
|
-
complete,
|
|
87
|
-
} = useTransitionState({
|
|
88
|
-
show: props.show,
|
|
89
|
-
appear: props.appear ?? props.config.appear ?? false,
|
|
90
|
-
})
|
|
91
|
-
|
|
92
|
-
const elementRef = createRef<HTMLElement>()
|
|
93
|
-
const mergedRef = mergeRefs(elementRef, stateRef)
|
|
94
|
-
|
|
95
|
-
const effectiveUnmount = props.unmount ?? props.config.unmount ?? true
|
|
96
|
-
const effectiveTimeout = props.timeout ?? props.config.timeout ?? 5000
|
|
97
|
-
|
|
98
|
-
useAnimationEnd({
|
|
99
|
-
ref: elementRef,
|
|
100
|
-
active: () => (stage() === 'entering' || stage() === 'leaving') && !reducedMotion(),
|
|
101
|
-
timeout: effectiveTimeout,
|
|
102
|
-
onEnd: () => {
|
|
103
|
-
if (stage() === 'entering') {
|
|
104
|
-
props.callbacks.onAfterEnter?.()
|
|
105
|
-
} else if (stage() === 'leaving') {
|
|
106
|
-
props.callbacks.onAfterLeave?.()
|
|
107
|
-
}
|
|
108
|
-
complete()
|
|
109
|
-
},
|
|
110
|
-
})
|
|
111
|
-
|
|
112
|
-
watch(
|
|
113
|
-
() => stage(),
|
|
114
|
-
(currentStage) => {
|
|
115
|
-
const el = elementRef.current
|
|
116
|
-
if (!el) return
|
|
117
|
-
|
|
118
|
-
if (reducedMotion()) {
|
|
119
|
-
applyReducedMotion(currentStage, props.callbacks, complete)
|
|
120
|
-
return
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
if (currentStage === 'entering') {
|
|
124
|
-
props.callbacks.onEnter?.()
|
|
125
|
-
const frameId = applyEnter(el, props.config)
|
|
126
|
-
return () => cancelAnimationFrame(frameId)
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
if (currentStage === 'leaving') {
|
|
130
|
-
props.callbacks.onLeave?.()
|
|
131
|
-
const frameId = applyLeave(el, props.config)
|
|
132
|
-
return () => cancelAnimationFrame(frameId)
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
if (currentStage === 'entered') {
|
|
136
|
-
removeClasses(el, props.config.enter)
|
|
137
|
-
el.style.transition = ''
|
|
138
|
-
}
|
|
139
|
-
},
|
|
140
|
-
{ immediate: true },
|
|
141
|
-
)
|
|
142
|
-
|
|
143
|
-
// Initially-visible kinetic-mode Transitions keep the original Show-gated
|
|
144
|
-
// mount, preserving the documented runtime-unmount semantic for the
|
|
145
|
-
// visible→hidden transition. The SSR bug (children dropped from prerendered
|
|
146
|
-
// HTML) only fires for the initially-HIDDEN case below, where
|
|
147
|
-
// `<Show when={false}>` renders `null` on the server — leaving SSG sites
|
|
148
|
-
// using kinetic-mode transitions (e.g. `kinetic('div').preset(fadeUp)` with
|
|
149
|
-
// `show: () => false` at SSR, the scroll-reveal pattern via
|
|
150
|
-
// `useIntersection`) without structural content for SEO / social scrapers
|
|
151
|
-
// / accessibility tools / no-JS users.
|
|
152
|
-
//
|
|
153
|
-
// Mirrors the same fix shape applied to the top-level `<Transition>` in
|
|
154
|
-
// PR #717. Ecosystem norm (Framer Motion / react-transition-group / react-
|
|
155
|
-
// spring): content is structural, animation is visual.
|
|
156
|
-
const wasInitiallyShown = props.show()
|
|
157
|
-
if (wasInitiallyShown) {
|
|
158
|
-
return (
|
|
159
|
-
<Show
|
|
160
|
-
when={shouldMount}
|
|
161
|
-
fallback={
|
|
162
|
-
effectiveUnmount
|
|
163
|
-
? null
|
|
164
|
-
: h(
|
|
165
|
-
props.config.tag,
|
|
166
|
-
// mergeProps keeps every reactive HTML-attr getter; ref + the
|
|
167
|
-
// hidden-state `display:none` style come last and win. The
|
|
168
|
-
// one-time `props.htmlProps.style` read seeds the hidden
|
|
169
|
-
// style — display:none must compose over the user's style.
|
|
170
|
-
mergeProps(props.htmlProps, {
|
|
171
|
-
ref: mergedRef,
|
|
172
|
-
style: {
|
|
173
|
-
...((props.htmlProps.style as CSSProperties) ?? {}),
|
|
174
|
-
display: 'none',
|
|
175
|
-
},
|
|
176
|
-
}),
|
|
177
|
-
props.children,
|
|
178
|
-
)
|
|
179
|
-
}
|
|
180
|
-
>
|
|
181
|
-
{h(
|
|
182
|
-
props.config.tag,
|
|
183
|
-
// Descriptor-preserving merge — reactive HTML attrs keep their
|
|
184
|
-
// getters; ref wins last. `{ ...props.htmlProps }` would freeze them.
|
|
185
|
-
mergeProps(props.htmlProps, { ref: mergedRef }),
|
|
186
|
-
props.children,
|
|
187
|
-
)}
|
|
188
|
-
</Show>
|
|
189
|
-
)
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
// Initially-hidden path — ecosystem-correct: always emit children with
|
|
193
|
-
// hidden-state class/style inlined so SSG / SEO / social scrapers / no-JS
|
|
194
|
-
// users see structural content. `leaveTo` (explicit hidden-end state)
|
|
195
|
-
// wins; falls back to `enterFrom` (pre-enter state) for scroll-reveal
|
|
196
|
-
// patterns that only configure the enter side. The existing
|
|
197
|
-
// `watch(stage)` effect drives the enter animation when `show` flips
|
|
198
|
-
// true; the symmetric `applyEnter` above clears these residual classes.
|
|
199
|
-
//
|
|
200
|
-
// Trade-off: for initially-hidden kinetic-mode Transitions, `unmount: true`
|
|
201
|
-
// no longer triggers a true DOM removal after a later leave animation
|
|
202
|
-
// completes — element stays in DOM with the leave-to class applied.
|
|
203
|
-
// Initially-visible Transitions (the branch above) keep the unmount
|
|
204
|
-
// semantic. Matches Framer Motion / react-transition-group conventions
|
|
205
|
-
// and is the price of SSR correctness.
|
|
206
|
-
// Mirrors the class picker: prefer `leaveTo`/`leaveToStyle` (explicit
|
|
207
|
-
// leave-end / hidden state) and fall back to `enterFrom`/`enterStyle`
|
|
208
|
-
// (pre-enter state). The fallback covers the preset path —
|
|
209
|
-
// `@pyreon/kinetic-presets` factories (fadeUp, slideLeft, blurInUp, …)
|
|
210
|
-
// populate `enterStyle` as the hidden state and may not set
|
|
211
|
-
// `leaveToStyle` at all; without this fallback, presets would SSR-render
|
|
212
|
-
// VISIBLE → flash-on-hydration.
|
|
213
|
-
const hiddenClass = props.config.leaveTo ?? props.config.enterFrom
|
|
214
|
-
const hiddenStyle = props.config.leaveToStyle ?? props.config.enterStyle
|
|
215
|
-
const childClass = props.htmlProps.class
|
|
216
|
-
const mergedClass = hiddenClass
|
|
217
|
-
? cx([childClass as Parameters<typeof cx>[0], hiddenClass])
|
|
218
|
-
: undefined
|
|
219
|
-
const mergedStyle = hiddenStyle
|
|
220
|
-
? { ...((props.htmlProps.style as CSSProperties) ?? {}), ...hiddenStyle }
|
|
221
|
-
: undefined
|
|
222
|
-
|
|
223
|
-
const extra: Record<string, unknown> = { ref: mergedRef }
|
|
224
|
-
if (mergedClass !== undefined) extra.class = mergedClass
|
|
225
|
-
if (mergedStyle !== undefined) extra.style = mergedStyle
|
|
226
|
-
|
|
227
|
-
return h(props.config.tag, mergeProps(props.htmlProps, extra), props.children)
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
export default TransitionRenderer
|