@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.
Files changed (37) hide show
  1. package/package.json +10 -12
  2. package/src/Collapse.tsx +0 -166
  3. package/src/Stagger.tsx +0 -63
  4. package/src/Transition.tsx +0 -280
  5. package/src/TransitionGroup.tsx +0 -139
  6. package/src/__tests__/Collapse.test.tsx +0 -803
  7. package/src/__tests__/GroupRenderer.test.tsx +0 -434
  8. package/src/__tests__/StaggerRenderer.test.tsx +0 -523
  9. package/src/__tests__/Transition.ssr.test.tsx +0 -183
  10. package/src/__tests__/Transition.test.tsx +0 -403
  11. package/src/__tests__/TransitionItem.test.tsx +0 -514
  12. package/src/__tests__/kinetic-modes.ssr.test.tsx +0 -214
  13. package/src/__tests__/kinetic.browser.test.tsx +0 -327
  14. package/src/__tests__/kinetic.test.tsx +0 -565
  15. package/src/__tests__/presets.test.ts +0 -46
  16. package/src/__tests__/stagger-component-children-hydration.test.tsx +0 -191
  17. package/src/__tests__/top-level-transition-stagger-function-children.test.tsx +0 -141
  18. package/src/__tests__/useAnimationEnd.test.ts +0 -194
  19. package/src/__tests__/useReducedMotion.test.ts +0 -160
  20. package/src/__tests__/useTransitionState.test.ts +0 -132
  21. package/src/__tests__/utils.test.ts +0 -139
  22. package/src/index.ts +0 -15
  23. package/src/jsx-augment.d.ts +0 -12
  24. package/src/kinetic/CollapseRenderer.tsx +0 -216
  25. package/src/kinetic/GroupRenderer.tsx +0 -149
  26. package/src/kinetic/StaggerRenderer.tsx +0 -94
  27. package/src/kinetic/TransitionItem.tsx +0 -250
  28. package/src/kinetic/TransitionRenderer.tsx +0 -230
  29. package/src/kinetic/createKineticComponent.tsx +0 -224
  30. package/src/kinetic/types.ts +0 -149
  31. package/src/kinetic.ts +0 -25
  32. package/src/presets.ts +0 -66
  33. package/src/types.ts +0 -118
  34. package/src/useAnimationEnd.ts +0 -59
  35. package/src/useReducedMotion.ts +0 -28
  36. package/src/useTransitionState.ts +0 -62
  37. package/src/utils.ts +0 -113
@@ -1,132 +0,0 @@
1
- import { signal } from '@pyreon/reactivity'
2
- import useTransitionState from '../useTransitionState'
3
-
4
- describe('useTransitionState', () => {
5
- it('initial state is hidden when show=false', () => {
6
- const show = signal(false)
7
- const result = useTransitionState({ show })
8
- expect(result.stage()).toBe('hidden')
9
- expect(result.shouldMount()).toBe(false)
10
- })
11
-
12
- it('initial state is entered when show=true and appear=false', () => {
13
- const show = signal(true)
14
- const result = useTransitionState({ show })
15
- expect(result.stage()).toBe('entered')
16
- expect(result.shouldMount()).toBe(true)
17
- })
18
-
19
- it('transitions to entering when show changes false->true', () => {
20
- const show = signal(false)
21
- const result = useTransitionState({ show })
22
-
23
- expect(result.stage()).toBe('hidden')
24
-
25
- show.set(true)
26
- expect(result.stage()).toBe('entering')
27
- expect(result.shouldMount()).toBe(true)
28
- })
29
-
30
- it('complete() transitions entering->entered', () => {
31
- const show = signal(false)
32
- const result = useTransitionState({ show })
33
-
34
- show.set(true)
35
- expect(result.stage()).toBe('entering')
36
-
37
- result.complete()
38
- expect(result.stage()).toBe('entered')
39
- })
40
-
41
- it('transitions to leaving when show changes true->false', () => {
42
- const show = signal(true)
43
- const result = useTransitionState({ show })
44
-
45
- expect(result.stage()).toBe('entered')
46
-
47
- show.set(false)
48
- expect(result.stage()).toBe('leaving')
49
- expect(result.shouldMount()).toBe(true)
50
- })
51
-
52
- it('complete() transitions leaving->hidden', () => {
53
- const show = signal(true)
54
- const result = useTransitionState({ show })
55
-
56
- show.set(false)
57
- expect(result.stage()).toBe('leaving')
58
-
59
- result.complete()
60
- expect(result.stage()).toBe('hidden')
61
- expect(result.shouldMount()).toBe(false)
62
- })
63
-
64
- it('appear=true enters after ref is connected', () => {
65
- const show = signal(true)
66
- const result = useTransitionState({ show, appear: true })
67
- // Before ref is wired, element should be mounted but stage is 'entered'
68
- expect(result.stage()).toBe('entered')
69
- expect(result.shouldMount()).toBe(true)
70
-
71
- // Simulate ref connection (as the renderer would do)
72
- const el = document.createElement('div')
73
- if (typeof result.ref === 'function') {
74
- result.ref(el)
75
- }
76
- // Now the appear animation should trigger
77
- expect(result.stage()).toBe('entering')
78
- })
79
-
80
- it('complete() is a no-op in entered state', () => {
81
- const show = signal(true)
82
- const result = useTransitionState({ show })
83
-
84
- expect(result.stage()).toBe('entered')
85
-
86
- result.complete()
87
- expect(result.stage()).toBe('entered')
88
- })
89
-
90
- it('complete() is a no-op in hidden state', () => {
91
- const show = signal(false)
92
- const result = useTransitionState({ show })
93
-
94
- expect(result.stage()).toBe('hidden')
95
-
96
- result.complete()
97
- expect(result.stage()).toBe('hidden')
98
- })
99
-
100
- it('handles rapid toggling true->false->true', () => {
101
- const show = signal(true)
102
- const result = useTransitionState({ show })
103
-
104
- // Start leave
105
- show.set(false)
106
- expect(result.stage()).toBe('leaving')
107
-
108
- // Interrupt with enter before leave completes
109
- show.set(true)
110
- expect(result.stage()).toBe('entering')
111
- })
112
-
113
- it('handles rapid toggling false->true->false (entering to leaving)', () => {
114
- const show = signal(false)
115
- const result = useTransitionState({ show })
116
-
117
- // Start enter
118
- show.set(true)
119
- expect(result.stage()).toBe('entering')
120
-
121
- // Interrupt with leave before enter completes
122
- show.set(false)
123
- expect(result.stage()).toBe('leaving')
124
- })
125
-
126
- it('provides a ref (callback or object)', () => {
127
- const show = signal(false)
128
- const result = useTransitionState({ show })
129
- expect(result.ref).toBeDefined()
130
- expect(typeof result.ref === 'function' || 'current' in result.ref).toBe(true)
131
- })
132
- })
@@ -1,139 +0,0 @@
1
- import { describe, expect, it } from 'vitest'
2
- import type { CSSProperties } from '../types'
3
- import { addClasses, mergeClassNames, mergeStyles, nextFrame, removeClasses } from '../utils'
4
-
5
- describe('mergeClassNames', () => {
6
- it('merges two class strings', () => {
7
- expect(mergeClassNames('a', 'b')).toBe('a b')
8
- })
9
-
10
- it('returns existing when additional is undefined', () => {
11
- expect(mergeClassNames('a', undefined)).toBe('a')
12
- })
13
-
14
- it('returns additional when existing is undefined', () => {
15
- expect(mergeClassNames(undefined, 'b')).toBe('b')
16
- })
17
-
18
- it('returns undefined when both are undefined', () => {
19
- expect(mergeClassNames(undefined, undefined)).toBeUndefined()
20
- })
21
-
22
- it('returns undefined when both are empty strings', () => {
23
- expect(mergeClassNames('', '')).toBeUndefined()
24
- })
25
-
26
- it('filters out empty strings', () => {
27
- expect(mergeClassNames('a', '')).toBe('a')
28
- })
29
- })
30
-
31
- describe('mergeStyles', () => {
32
- it('merges two style objects with b taking precedence', () => {
33
- const a = { color: 'red', fontSize: '12px' } as CSSProperties
34
- const b = { color: 'blue' } as CSSProperties
35
- expect(mergeStyles(a, b)).toEqual({ color: 'blue', fontSize: '12px' })
36
- })
37
-
38
- it('returns undefined when both are undefined', () => {
39
- expect(mergeStyles(undefined, undefined)).toBeUndefined()
40
- })
41
-
42
- it('returns b when a is undefined', () => {
43
- const b = { color: 'blue' } as CSSProperties
44
- expect(mergeStyles(undefined, b)).toBe(b)
45
- })
46
-
47
- it('returns a when b is undefined', () => {
48
- const a = { color: 'red' } as CSSProperties
49
- expect(mergeStyles(a, undefined)).toBe(a)
50
- })
51
- })
52
-
53
- describe('addClasses', () => {
54
- it('adds space-separated classes to an element', () => {
55
- const el = document.createElement('div')
56
- addClasses(el, 'foo bar')
57
- expect(el.classList.contains('foo')).toBe(true)
58
- expect(el.classList.contains('bar')).toBe(true)
59
- })
60
-
61
- it('does nothing when classes is undefined', () => {
62
- const el = document.createElement('div')
63
- addClasses(el, undefined)
64
- expect(el.classList.length).toBe(0)
65
- })
66
-
67
- it('does nothing when classes is empty string', () => {
68
- const el = document.createElement('div')
69
- addClasses(el, '')
70
- expect(el.classList.length).toBe(0)
71
- })
72
-
73
- it('does nothing when classes is whitespace-only', () => {
74
- const el = document.createElement('div')
75
- addClasses(el, ' ')
76
- expect(el.classList.length).toBe(0)
77
- })
78
- })
79
-
80
- describe('removeClasses', () => {
81
- it('removes space-separated classes from an element', () => {
82
- const el = document.createElement('div')
83
- el.classList.add('foo', 'bar', 'baz')
84
- removeClasses(el, 'foo bar')
85
- expect(el.classList.contains('foo')).toBe(false)
86
- expect(el.classList.contains('bar')).toBe(false)
87
- expect(el.classList.contains('baz')).toBe(true)
88
- })
89
-
90
- it('does nothing when classes is undefined', () => {
91
- const el = document.createElement('div')
92
- el.classList.add('foo')
93
- removeClasses(el, undefined)
94
- expect(el.classList.contains('foo')).toBe(true)
95
- })
96
-
97
- it('does nothing when classes is empty string', () => {
98
- const el = document.createElement('div')
99
- el.classList.add('foo')
100
- removeClasses(el, '')
101
- expect(el.classList.contains('foo')).toBe(true)
102
- })
103
-
104
- it('does nothing when classes is whitespace-only', () => {
105
- const el = document.createElement('div')
106
- el.classList.add('foo')
107
- removeClasses(el, ' ')
108
- expect(el.classList.contains('foo')).toBe(true)
109
- })
110
- })
111
-
112
- describe('nextFrame', () => {
113
- it('calls callback after double rAF', () => {
114
- const callbacks: (() => void)[] = []
115
- const originalRaf = globalThis.requestAnimationFrame
116
- globalThis.requestAnimationFrame = ((cb: () => void) => {
117
- callbacks.push(cb)
118
- return callbacks.length
119
- }) as typeof requestAnimationFrame
120
-
121
- const fn = vi.fn()
122
- nextFrame(fn)
123
-
124
- // First rAF queued
125
- expect(callbacks.length).toBe(1)
126
- expect(fn).not.toHaveBeenCalled()
127
-
128
- // Execute first rAF — queues second
129
- callbacks[0]?.()
130
- expect(callbacks.length).toBe(2)
131
- expect(fn).not.toHaveBeenCalled()
132
-
133
- // Execute second rAF — callback fires
134
- callbacks[1]?.()
135
- expect(fn).toHaveBeenCalledTimes(1)
136
-
137
- globalThis.requestAnimationFrame = originalRaf
138
- })
139
- })
package/src/index.ts DELETED
@@ -1,15 +0,0 @@
1
- export { default as kinetic } from './kinetic'
2
- export type { KineticComponent } from './kinetic/types'
3
- export type { Preset } from './presets'
4
- export { fade, presets, scaleIn, slideDown, slideLeft, slideRight, slideUp } from './presets'
5
- export type {
6
- ClassTransitionProps,
7
- StyleTransitionProps,
8
- TransitionCallbacks,
9
- TransitionStage,
10
- TransitionStateResult,
11
- } from './types'
12
- export type { UseAnimationEnd } from './useAnimationEnd'
13
- export { default as useAnimationEnd } from './useAnimationEnd'
14
- export type { UseTransitionState } from './useTransitionState'
15
- export { default as useTransitionState } from './useTransitionState'
@@ -1,12 +0,0 @@
1
- // Augment JSX namespace so that `key` is accepted on component elements,
2
- // not just intrinsic HTML elements. Pyreon's jsx() runtime already handles
3
- // key as the third argument — this just satisfies TypeScript.
4
- declare global {
5
- namespace JSX {
6
- interface IntrinsicAttributes {
7
- key?: string | number
8
- }
9
- }
10
- }
11
-
12
- export {}
@@ -1,216 +0,0 @@
1
- import type { VNode } from '@pyreon/core'
2
- import { createRef, h, mergeProps, Show } from '@pyreon/core'
3
- import { runUntracked, signal, watch } from '@pyreon/reactivity'
4
- import type { CSSProperties, TransitionCallbacks, TransitionStage } from '../types'
5
- import useAnimationEnd from '../useAnimationEnd'
6
- import { useReducedMotion } from '../useReducedMotion'
7
- import type { KineticConfig } from './types'
8
-
9
- type CollapseRendererProps = {
10
- config: KineticConfig
11
- htmlProps: Record<string, unknown>
12
- show: () => boolean
13
- appear?: boolean | undefined
14
- timeout?: number | undefined
15
- transition?: string | undefined
16
- callbacks: Partial<TransitionCallbacks>
17
- children: VNode | VNode[]
18
- }
19
-
20
- /**
21
- * Renders a height-animated collapse. The config.tag becomes the outer
22
- * wrapper (overflow:hidden + animated height). An inner div measures
23
- * scrollHeight for the target value.
24
- */
25
- const CollapseRenderer = ({
26
- config,
27
- htmlProps,
28
- show,
29
- appear,
30
- timeout,
31
- transition,
32
- callbacks,
33
- children,
34
- }: CollapseRendererProps): VNode | null => {
35
- const reducedMotion = useReducedMotion()
36
- let wrapperRef: { current: HTMLElement | null } = createRef<HTMLElement>()
37
- const contentRef = createRef<HTMLDivElement>()
38
-
39
- const effectiveAppear = appear ?? config.appear ?? false
40
- const effectiveTimeout = timeout ?? config.timeout ?? 5000
41
- const effectiveTransition = transition ?? config.transition ?? 'height 300ms ease'
42
-
43
- const initialShow = show()
44
- const needsAppear = effectiveAppear && initialShow
45
- const stage = signal<TransitionStage>(initialShow ? 'entered' : 'hidden')
46
- let isInitialMount = true
47
- let appearTriggered = false
48
-
49
- // Intercept ref assignment to trigger appear after all refs are wired
50
- if (needsAppear) {
51
- const orig = wrapperRef
52
- const proxy = { current: null as HTMLElement | null }
53
- Object.defineProperty(proxy, 'current', {
54
- get() {
55
- return orig.current
56
- },
57
- set(node: HTMLElement | null) {
58
- orig.current = node
59
- if (node && !appearTriggered) {
60
- appearTriggered = true
61
- queueMicrotask(() => stage.set('entering'))
62
- }
63
- },
64
- })
65
- wrapperRef = proxy
66
- }
67
-
68
- // State machine transitions
69
- watch(
70
- show,
71
- (showVal) => {
72
- if (isInitialMount) {
73
- isInitialMount = false
74
- // appear case is handled by ref proxy above
75
- return
76
- }
77
-
78
- const currentStage = runUntracked(() => stage())
79
- if (showVal && (currentStage === 'hidden' || currentStage === 'leaving')) {
80
- stage.set('entering')
81
- } else if (!showVal && (currentStage === 'entered' || currentStage === 'entering')) {
82
- stage.set('leaving')
83
- }
84
- },
85
- { immediate: true },
86
- )
87
-
88
- // Animate height
89
- watch(
90
- () => stage(),
91
- (currentStage) => {
92
- const wrapper = wrapperRef.current
93
- const content = contentRef.current
94
- if (!wrapper || !content) return
95
-
96
- if (reducedMotion()) {
97
- if (currentStage === 'entering') {
98
- callbacks.onEnter?.()
99
- wrapper.style.height = 'auto'
100
- wrapper.style.overflow = ''
101
- callbacks.onAfterEnter?.()
102
- stage.set('entered')
103
- } else if (currentStage === 'leaving') {
104
- callbacks.onLeave?.()
105
- wrapper.style.height = '0px'
106
- wrapper.style.overflow = 'hidden'
107
- callbacks.onAfterLeave?.()
108
- stage.set('hidden')
109
- }
110
- return
111
- }
112
-
113
- if (currentStage === 'entering') {
114
- callbacks.onEnter?.()
115
- const height = content.scrollHeight
116
- wrapper.style.transition = 'none'
117
- wrapper.style.height = '0px'
118
- wrapper.style.overflow = 'hidden'
119
- // Force reflow
120
- void wrapper.offsetHeight
121
- wrapper.style.transition = effectiveTransition
122
- wrapper.style.height = `${height}px`
123
- }
124
-
125
- if (currentStage === 'leaving') {
126
- callbacks.onLeave?.()
127
- const height = content.scrollHeight
128
- wrapper.style.transition = 'none'
129
- wrapper.style.height = `${height}px`
130
- wrapper.style.overflow = 'hidden'
131
- // Force reflow
132
- void wrapper.offsetHeight
133
- wrapper.style.transition = effectiveTransition
134
- wrapper.style.height = '0px'
135
- }
136
- },
137
- { immediate: true },
138
- )
139
-
140
- useAnimationEnd({
141
- ref: wrapperRef,
142
- active: () => (stage() === 'entering' || stage() === 'leaving') && !reducedMotion(),
143
- timeout: effectiveTimeout,
144
- onEnd: () => {
145
- const wrapper = wrapperRef.current
146
- if (stage() === 'entering') {
147
- if (wrapper) {
148
- wrapper.style.height = 'auto'
149
- wrapper.style.overflow = ''
150
- wrapper.style.transition = ''
151
- }
152
- callbacks.onAfterEnter?.()
153
- stage.set('entered')
154
- } else if (stage() === 'leaving') {
155
- callbacks.onAfterLeave?.()
156
- stage.set('hidden')
157
- }
158
- },
159
- })
160
-
161
- const shouldRender = () => stage() !== 'hidden'
162
-
163
- const wrapperStyle: CSSProperties = {
164
- ...((htmlProps.style as CSSProperties) ?? {}),
165
- ...(stage() !== 'entered' ? { overflow: 'hidden' } : {}),
166
- ...(stage() === 'hidden' ? { height: '0px' } : stage() === 'entered' ? { height: 'auto' } : {}),
167
- }
168
-
169
- // Initially-visible Collapses keep the original Show-gated inner content,
170
- // preserving the runtime-unmount semantic that frees the inner subtree
171
- // when the collapse is closed long-term. The SSR bug fires only when
172
- // `show: () => false` at setup — the outer wrapper renders (with
173
- // `height: 0; overflow: hidden`) but its children are stripped by the
174
- // inner `<Show when={false}>` → empty wrapper in the prerendered HTML.
175
- // Bad for SEO / social scrapers / accessibility / no-JS.
176
- //
177
- // Mirrors the fix shape applied to `<Transition>` (PR #717), the
178
- // `TransitionRenderer` and `TransitionItem` (this PR). Ecosystem norm:
179
- // content is structural, animation is visual.
180
- //
181
- // For initially-hidden Collapses, the inner content always renders —
182
- // the outer wrapper's `height: 0px; overflow: hidden` already provides
183
- // the visual hiding (genuinely layout-safe — no flex slot collapse;
184
- // the outer wrapper participates in flex as a 0-height box, which is
185
- // the standard CSS collapse behavior). When `show` flips true, the
186
- // existing `watch(stage)` measures `content.scrollHeight` and animates
187
- // height from 0 → that value — no change to the animation path.
188
- //
189
- // Trade-off: for initially-hidden Collapses, the inner subtree is
190
- // ALWAYS mounted (never unmounted after a later close). Initially-
191
- // visible Collapses keep the unmount behavior. Matches the trade-off
192
- // documented across the other three kinetic renderers.
193
- const wasInitiallyShown = show()
194
- const innerContent = wasInitiallyShown ? (
195
- <Show when={shouldRender}>
196
- <div ref={contentRef}>{children}</div>
197
- </Show>
198
- ) : (
199
- <div ref={contentRef}>{children}</div>
200
- )
201
-
202
- // mergeProps (descriptor-preserving) instead of `{ ...htmlProps }` —
203
- // every non-style HTML attr keeps its reactive getter; ref + the
204
- // collapse-controlled style come last so they win (mergeProps is
205
- // last-source-wins). The one-time `htmlProps.style` read above that
206
- // seeds wrapperStyle is intentional: collapse OWNS the style prop
207
- // (height/overflow are animation-driven), so a static merge of the
208
- // user's initial style with the collapse overrides is correct here.
209
- return h(
210
- config.tag,
211
- mergeProps(htmlProps, { ref: wrapperRef, style: wrapperStyle }),
212
- innerContent,
213
- )
214
- }
215
-
216
- export default CollapseRenderer
@@ -1,149 +0,0 @@
1
- import type { VNode } from '@pyreon/core'
2
- import { h } from '@pyreon/core'
3
- import { signal } from '@pyreon/reactivity'
4
- import type { TransitionCallbacks } from '../types'
5
- import TransitionItem from './TransitionItem'
6
- import type { KineticConfig } from './types'
7
-
8
- type GroupRendererProps = {
9
- config: KineticConfig
10
- htmlProps: Record<string, unknown>
11
- appear?: boolean | undefined
12
- timeout?: number | undefined
13
- callbacks: Partial<TransitionCallbacks>
14
- /**
15
- * Children can be a static array OR a reactive accessor `() => VNode[]`.
16
- * When passed as an accessor, GroupRenderer tracks changes and
17
- * animates entering/leaving children automatically.
18
- */
19
- children: VNode[] | (() => VNode[])
20
- }
21
-
22
- type KeyedChild = { key: string | number; element: VNode }
23
-
24
- const isVNode = (child: unknown): child is VNode =>
25
- child != null && typeof child === 'object' && 'type' in (child as object)
26
-
27
- const getKeyedChildren = (children: VNode[]): KeyedChild[] => {
28
- const result: KeyedChild[] = []
29
- for (const child of children) {
30
- if (isVNode(child)) {
31
- const key = (child as VNode & { key?: string | number }).key
32
- if (key != null) {
33
- result.push({ key, element: child })
34
- }
35
- }
36
- }
37
- return result
38
- }
39
-
40
- /**
41
- * Renders children with key-based enter/exit animation (no `show` prop).
42
- * Children that appear (new key) animate in. Children that disappear
43
- * (removed key) stay in DOM during leave animation, then unmount.
44
- * config.tag wraps all children as a container element.
45
- *
46
- * In Pyreon, components run once. Pass children as a reactive accessor
47
- * `() => VNode[]` for the group to detect changes and animate entries/exits.
48
- */
49
- const GroupRenderer = ({
50
- config,
51
- htmlProps,
52
- appear,
53
- timeout,
54
- callbacks,
55
- children,
56
- }: GroupRendererProps): VNode | null => {
57
- const effectiveAppear = appear ?? config.appear ?? false
58
- const effectiveTimeout = timeout ?? config.timeout ?? 5000
59
-
60
- const prevMap = new Map<string | number, VNode>()
61
- const leavingMap = new Map<string | number, VNode>()
62
- const forceUpdate = signal(0)
63
-
64
- // Normalize children to an accessor
65
- const getChildren = typeof children === 'function' ? (children as () => VNode[]) : () => children
66
-
67
- // Track initial keys for appear animation logic
68
- const initialKeyed = getKeyedChildren(getChildren())
69
- const initialKeys = new Set(initialKeyed.map((c) => c.key))
70
- for (const { key, element } of initialKeyed) {
71
- prevMap.set(key, element)
72
- }
73
-
74
- const handleAfterLeave = (key: string | number) => {
75
- leavingMap.delete(key)
76
- callbacks.onAfterLeave?.()
77
- forceUpdate.update((c) => c + 1)
78
- }
79
-
80
- // Reactive accessor — re-evaluates when children() or forceUpdate changes
81
- return (() => {
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
92
- for (const [key, child] of prevMap) {
93
- if (!currentMap.has(key) && !leavingMap.has(key)) {
94
- leavingMap.set(key, child)
95
- }
96
- }
97
-
98
- // Cancel leave if child reappears
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
110
- const allEntries: KeyedChild[] = [...currentKeyed]
111
- for (const [key, element] of leavingMap) {
112
- allEntries.push({ key, element })
113
- }
114
-
115
- const groupedChildren = allEntries.map(({ key, element }) => {
116
- const isInitial = initialKeys.has(key)
117
- const isShowing = currentMap.has(key)
118
-
119
- return (
120
- <TransitionItem
121
- show={() => isShowing}
122
- appear={isInitial ? effectiveAppear : true}
123
- timeout={effectiveTimeout}
124
- enterStyle={config.enterStyle}
125
- enterToStyle={config.enterToStyle}
126
- enterTransition={config.enterTransition}
127
- leaveStyle={config.leaveStyle}
128
- leaveToStyle={config.leaveToStyle}
129
- leaveTransition={config.leaveTransition}
130
- enter={config.enter}
131
- enterFrom={config.enterFrom}
132
- enterTo={config.enterTo}
133
- leave={config.leave}
134
- leaveFrom={config.leaveFrom}
135
- leaveTo={config.leaveTo}
136
- onAfterLeave={() => handleAfterLeave(key)}
137
- >
138
- {element}
139
- </TransitionItem>
140
- )
141
- })
142
-
143
- // By reference — `{ ...htmlProps }` would value-copy and freeze any
144
- // reactive HTML attr the kinetic split preserved as a getter.
145
- return h(config.tag, htmlProps, ...groupedChildren)
146
- }) as unknown as VNode
147
- }
148
-
149
- export default GroupRenderer