@pyreon/kinetic 0.11.4 → 0.11.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/README.md +27 -24
- package/lib/index.d.ts +8 -8
- package/lib/index.js +46 -37
- package/package.json +22 -22
- package/src/Collapse.tsx +44 -44
- package/src/Stagger.tsx +7 -7
- package/src/Transition.tsx +21 -24
- package/src/TransitionGroup.tsx +85 -59
- package/src/__tests__/Collapse.test.tsx +125 -125
- package/src/__tests__/GroupRenderer.test.tsx +233 -190
- package/src/__tests__/StaggerRenderer.test.tsx +99 -99
- package/src/__tests__/Transition.test.tsx +89 -89
- package/src/__tests__/TransitionItem.test.tsx +120 -120
- package/src/__tests__/kinetic.test.tsx +135 -135
- package/src/__tests__/presets.test.ts +15 -15
- package/src/__tests__/useAnimationEnd.test.ts +33 -33
- package/src/__tests__/useReducedMotion.test.ts +22 -22
- package/src/__tests__/useTransitionState.test.ts +38 -38
- package/src/__tests__/utils.test.ts +63 -63
- package/src/index.ts +9 -17
- package/src/kinetic/CollapseRenderer.tsx +42 -42
- package/src/kinetic/GroupRenderer.tsx +88 -65
- package/src/kinetic/StaggerRenderer.tsx +9 -9
- package/src/kinetic/TransitionItem.tsx +18 -18
- package/src/kinetic/TransitionRenderer.tsx +19 -19
- package/src/kinetic/createKineticComponent.tsx +27 -27
- package/src/kinetic/types.ts +13 -13
- package/src/kinetic.ts +4 -4
- package/src/presets.ts +33 -33
- package/src/types.ts +3 -3
- package/src/useAnimationEnd.ts +8 -8
- package/src/useReducedMotion.ts +5 -5
- package/src/useTransitionState.ts +12 -12
- package/src/utils.ts +4 -4
|
@@ -1,116 +1,116 @@
|
|
|
1
|
-
import { describe, expect, it } from
|
|
2
|
-
import type { CSSProperties } from
|
|
3
|
-
import { addClasses, mergeClassNames, mergeStyles, nextFrame, removeClasses } from
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import type { CSSProperties } from '../types'
|
|
3
|
+
import { addClasses, mergeClassNames, mergeStyles, nextFrame, removeClasses } from '../utils'
|
|
4
4
|
|
|
5
|
-
describe(
|
|
6
|
-
it(
|
|
7
|
-
expect(mergeClassNames(
|
|
5
|
+
describe('mergeClassNames', () => {
|
|
6
|
+
it('merges two class strings', () => {
|
|
7
|
+
expect(mergeClassNames('a', 'b')).toBe('a b')
|
|
8
8
|
})
|
|
9
9
|
|
|
10
|
-
it(
|
|
11
|
-
expect(mergeClassNames(
|
|
10
|
+
it('returns existing when additional is undefined', () => {
|
|
11
|
+
expect(mergeClassNames('a', undefined)).toBe('a')
|
|
12
12
|
})
|
|
13
13
|
|
|
14
|
-
it(
|
|
15
|
-
expect(mergeClassNames(undefined,
|
|
14
|
+
it('returns additional when existing is undefined', () => {
|
|
15
|
+
expect(mergeClassNames(undefined, 'b')).toBe('b')
|
|
16
16
|
})
|
|
17
17
|
|
|
18
|
-
it(
|
|
18
|
+
it('returns undefined when both are undefined', () => {
|
|
19
19
|
expect(mergeClassNames(undefined, undefined)).toBeUndefined()
|
|
20
20
|
})
|
|
21
21
|
|
|
22
|
-
it(
|
|
23
|
-
expect(mergeClassNames(
|
|
22
|
+
it('returns undefined when both are empty strings', () => {
|
|
23
|
+
expect(mergeClassNames('', '')).toBeUndefined()
|
|
24
24
|
})
|
|
25
25
|
|
|
26
|
-
it(
|
|
27
|
-
expect(mergeClassNames(
|
|
26
|
+
it('filters out empty strings', () => {
|
|
27
|
+
expect(mergeClassNames('a', '')).toBe('a')
|
|
28
28
|
})
|
|
29
29
|
})
|
|
30
30
|
|
|
31
|
-
describe(
|
|
32
|
-
it(
|
|
33
|
-
const a = { color:
|
|
34
|
-
const b = { color:
|
|
35
|
-
expect(mergeStyles(a, b)).toEqual({ color:
|
|
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
36
|
})
|
|
37
37
|
|
|
38
|
-
it(
|
|
38
|
+
it('returns undefined when both are undefined', () => {
|
|
39
39
|
expect(mergeStyles(undefined, undefined)).toBeUndefined()
|
|
40
40
|
})
|
|
41
41
|
|
|
42
|
-
it(
|
|
43
|
-
const b = { color:
|
|
42
|
+
it('returns b when a is undefined', () => {
|
|
43
|
+
const b = { color: 'blue' } as CSSProperties
|
|
44
44
|
expect(mergeStyles(undefined, b)).toBe(b)
|
|
45
45
|
})
|
|
46
46
|
|
|
47
|
-
it(
|
|
48
|
-
const a = { color:
|
|
47
|
+
it('returns a when b is undefined', () => {
|
|
48
|
+
const a = { color: 'red' } as CSSProperties
|
|
49
49
|
expect(mergeStyles(a, undefined)).toBe(a)
|
|
50
50
|
})
|
|
51
51
|
})
|
|
52
52
|
|
|
53
|
-
describe(
|
|
54
|
-
it(
|
|
55
|
-
const el = document.createElement(
|
|
56
|
-
addClasses(el,
|
|
57
|
-
expect(el.classList.contains(
|
|
58
|
-
expect(el.classList.contains(
|
|
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
59
|
})
|
|
60
60
|
|
|
61
|
-
it(
|
|
62
|
-
const el = document.createElement(
|
|
61
|
+
it('does nothing when classes is undefined', () => {
|
|
62
|
+
const el = document.createElement('div')
|
|
63
63
|
addClasses(el, undefined)
|
|
64
64
|
expect(el.classList.length).toBe(0)
|
|
65
65
|
})
|
|
66
66
|
|
|
67
|
-
it(
|
|
68
|
-
const el = document.createElement(
|
|
69
|
-
addClasses(el,
|
|
67
|
+
it('does nothing when classes is empty string', () => {
|
|
68
|
+
const el = document.createElement('div')
|
|
69
|
+
addClasses(el, '')
|
|
70
70
|
expect(el.classList.length).toBe(0)
|
|
71
71
|
})
|
|
72
72
|
|
|
73
|
-
it(
|
|
74
|
-
const el = document.createElement(
|
|
75
|
-
addClasses(el,
|
|
73
|
+
it('does nothing when classes is whitespace-only', () => {
|
|
74
|
+
const el = document.createElement('div')
|
|
75
|
+
addClasses(el, ' ')
|
|
76
76
|
expect(el.classList.length).toBe(0)
|
|
77
77
|
})
|
|
78
78
|
})
|
|
79
79
|
|
|
80
|
-
describe(
|
|
81
|
-
it(
|
|
82
|
-
const el = document.createElement(
|
|
83
|
-
el.classList.add(
|
|
84
|
-
removeClasses(el,
|
|
85
|
-
expect(el.classList.contains(
|
|
86
|
-
expect(el.classList.contains(
|
|
87
|
-
expect(el.classList.contains(
|
|
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
88
|
})
|
|
89
89
|
|
|
90
|
-
it(
|
|
91
|
-
const el = document.createElement(
|
|
92
|
-
el.classList.add(
|
|
90
|
+
it('does nothing when classes is undefined', () => {
|
|
91
|
+
const el = document.createElement('div')
|
|
92
|
+
el.classList.add('foo')
|
|
93
93
|
removeClasses(el, undefined)
|
|
94
|
-
expect(el.classList.contains(
|
|
94
|
+
expect(el.classList.contains('foo')).toBe(true)
|
|
95
95
|
})
|
|
96
96
|
|
|
97
|
-
it(
|
|
98
|
-
const el = document.createElement(
|
|
99
|
-
el.classList.add(
|
|
100
|
-
removeClasses(el,
|
|
101
|
-
expect(el.classList.contains(
|
|
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
102
|
})
|
|
103
103
|
|
|
104
|
-
it(
|
|
105
|
-
const el = document.createElement(
|
|
106
|
-
el.classList.add(
|
|
107
|
-
removeClasses(el,
|
|
108
|
-
expect(el.classList.contains(
|
|
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
109
|
})
|
|
110
110
|
})
|
|
111
111
|
|
|
112
|
-
describe(
|
|
113
|
-
it(
|
|
112
|
+
describe('nextFrame', () => {
|
|
113
|
+
it('calls callback after double rAF', () => {
|
|
114
114
|
const callbacks: (() => void)[] = []
|
|
115
115
|
const originalRaf = globalThis.requestAnimationFrame
|
|
116
116
|
globalThis.requestAnimationFrame = ((cb: () => void) => {
|
package/src/index.ts
CHANGED
|
@@ -1,23 +1,15 @@
|
|
|
1
|
-
export { default as kinetic } from
|
|
2
|
-
export type { KineticComponent } from
|
|
3
|
-
export type { Preset } from
|
|
4
|
-
export {
|
|
5
|
-
fade,
|
|
6
|
-
presets,
|
|
7
|
-
scaleIn,
|
|
8
|
-
slideDown,
|
|
9
|
-
slideLeft,
|
|
10
|
-
slideRight,
|
|
11
|
-
slideUp,
|
|
12
|
-
} from "./presets"
|
|
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'
|
|
13
5
|
export type {
|
|
14
6
|
ClassTransitionProps,
|
|
15
7
|
StyleTransitionProps,
|
|
16
8
|
TransitionCallbacks,
|
|
17
9
|
TransitionStage,
|
|
18
10
|
TransitionStateResult,
|
|
19
|
-
} from
|
|
20
|
-
export type { UseAnimationEnd } from
|
|
21
|
-
export { default as useAnimationEnd } from
|
|
22
|
-
export type { UseTransitionState } from
|
|
23
|
-
export { default as useTransitionState } from
|
|
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,10 +1,10 @@
|
|
|
1
|
-
import type { VNode } from
|
|
2
|
-
import { createRef, h, Show } from
|
|
3
|
-
import { runUntracked, signal, watch } from
|
|
4
|
-
import type { CSSProperties, TransitionCallbacks, TransitionStage } from
|
|
5
|
-
import useAnimationEnd from
|
|
6
|
-
import { useReducedMotion } from
|
|
7
|
-
import type { KineticConfig } from
|
|
1
|
+
import type { VNode } from '@pyreon/core'
|
|
2
|
+
import { createRef, h, 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
8
|
|
|
9
9
|
type CollapseRendererProps = {
|
|
10
10
|
config: KineticConfig
|
|
@@ -38,11 +38,11 @@ const CollapseRenderer = ({
|
|
|
38
38
|
|
|
39
39
|
const effectiveAppear = appear ?? config.appear ?? false
|
|
40
40
|
const effectiveTimeout = timeout ?? config.timeout ?? 5000
|
|
41
|
-
const effectiveTransition = transition ?? config.transition ??
|
|
41
|
+
const effectiveTransition = transition ?? config.transition ?? 'height 300ms ease'
|
|
42
42
|
|
|
43
43
|
const initialShow = show()
|
|
44
44
|
const needsAppear = effectiveAppear && initialShow
|
|
45
|
-
const stage = signal<TransitionStage>(initialShow ?
|
|
45
|
+
const stage = signal<TransitionStage>(initialShow ? 'entered' : 'hidden')
|
|
46
46
|
let isInitialMount = true
|
|
47
47
|
let appearTriggered = false
|
|
48
48
|
|
|
@@ -50,7 +50,7 @@ const CollapseRenderer = ({
|
|
|
50
50
|
if (needsAppear) {
|
|
51
51
|
const orig = wrapperRef
|
|
52
52
|
const proxy = { current: null as HTMLElement | null }
|
|
53
|
-
Object.defineProperty(proxy,
|
|
53
|
+
Object.defineProperty(proxy, 'current', {
|
|
54
54
|
get() {
|
|
55
55
|
return orig.current
|
|
56
56
|
},
|
|
@@ -58,7 +58,7 @@ const CollapseRenderer = ({
|
|
|
58
58
|
orig.current = node
|
|
59
59
|
if (node && !appearTriggered) {
|
|
60
60
|
appearTriggered = true
|
|
61
|
-
queueMicrotask(() => stage.set(
|
|
61
|
+
queueMicrotask(() => stage.set('entering'))
|
|
62
62
|
}
|
|
63
63
|
},
|
|
64
64
|
})
|
|
@@ -76,10 +76,10 @@ const CollapseRenderer = ({
|
|
|
76
76
|
}
|
|
77
77
|
|
|
78
78
|
const currentStage = runUntracked(() => stage())
|
|
79
|
-
if (showVal && (currentStage ===
|
|
80
|
-
stage.set(
|
|
81
|
-
} else if (!showVal && (currentStage ===
|
|
82
|
-
stage.set(
|
|
79
|
+
if (showVal && (currentStage === 'hidden' || currentStage === 'leaving')) {
|
|
80
|
+
stage.set('entering')
|
|
81
|
+
} else if (!showVal && (currentStage === 'entered' || currentStage === 'entering')) {
|
|
82
|
+
stage.set('leaving')
|
|
83
83
|
}
|
|
84
84
|
},
|
|
85
85
|
{ immediate: true },
|
|
@@ -94,44 +94,44 @@ const CollapseRenderer = ({
|
|
|
94
94
|
if (!wrapper || !content) return
|
|
95
95
|
|
|
96
96
|
if (reducedMotion()) {
|
|
97
|
-
if (currentStage ===
|
|
97
|
+
if (currentStage === 'entering') {
|
|
98
98
|
callbacks.onEnter?.()
|
|
99
|
-
wrapper.style.height =
|
|
100
|
-
wrapper.style.overflow =
|
|
99
|
+
wrapper.style.height = 'auto'
|
|
100
|
+
wrapper.style.overflow = ''
|
|
101
101
|
callbacks.onAfterEnter?.()
|
|
102
|
-
stage.set(
|
|
103
|
-
} else if (currentStage ===
|
|
102
|
+
stage.set('entered')
|
|
103
|
+
} else if (currentStage === 'leaving') {
|
|
104
104
|
callbacks.onLeave?.()
|
|
105
|
-
wrapper.style.height =
|
|
106
|
-
wrapper.style.overflow =
|
|
105
|
+
wrapper.style.height = '0px'
|
|
106
|
+
wrapper.style.overflow = 'hidden'
|
|
107
107
|
callbacks.onAfterLeave?.()
|
|
108
|
-
stage.set(
|
|
108
|
+
stage.set('hidden')
|
|
109
109
|
}
|
|
110
110
|
return
|
|
111
111
|
}
|
|
112
112
|
|
|
113
|
-
if (currentStage ===
|
|
113
|
+
if (currentStage === 'entering') {
|
|
114
114
|
callbacks.onEnter?.()
|
|
115
115
|
const height = content.scrollHeight
|
|
116
|
-
wrapper.style.transition =
|
|
117
|
-
wrapper.style.height =
|
|
118
|
-
wrapper.style.overflow =
|
|
116
|
+
wrapper.style.transition = 'none'
|
|
117
|
+
wrapper.style.height = '0px'
|
|
118
|
+
wrapper.style.overflow = 'hidden'
|
|
119
119
|
// Force reflow
|
|
120
120
|
void wrapper.offsetHeight
|
|
121
121
|
wrapper.style.transition = effectiveTransition
|
|
122
122
|
wrapper.style.height = `${height}px`
|
|
123
123
|
}
|
|
124
124
|
|
|
125
|
-
if (currentStage ===
|
|
125
|
+
if (currentStage === 'leaving') {
|
|
126
126
|
callbacks.onLeave?.()
|
|
127
127
|
const height = content.scrollHeight
|
|
128
|
-
wrapper.style.transition =
|
|
128
|
+
wrapper.style.transition = 'none'
|
|
129
129
|
wrapper.style.height = `${height}px`
|
|
130
|
-
wrapper.style.overflow =
|
|
130
|
+
wrapper.style.overflow = 'hidden'
|
|
131
131
|
// Force reflow
|
|
132
132
|
void wrapper.offsetHeight
|
|
133
133
|
wrapper.style.transition = effectiveTransition
|
|
134
|
-
wrapper.style.height =
|
|
134
|
+
wrapper.style.height = '0px'
|
|
135
135
|
}
|
|
136
136
|
},
|
|
137
137
|
{ immediate: true },
|
|
@@ -139,31 +139,31 @@ const CollapseRenderer = ({
|
|
|
139
139
|
|
|
140
140
|
useAnimationEnd({
|
|
141
141
|
ref: wrapperRef,
|
|
142
|
-
active: () => (stage() ===
|
|
142
|
+
active: () => (stage() === 'entering' || stage() === 'leaving') && !reducedMotion(),
|
|
143
143
|
timeout: effectiveTimeout,
|
|
144
144
|
onEnd: () => {
|
|
145
145
|
const wrapper = wrapperRef.current
|
|
146
|
-
if (stage() ===
|
|
146
|
+
if (stage() === 'entering') {
|
|
147
147
|
if (wrapper) {
|
|
148
|
-
wrapper.style.height =
|
|
149
|
-
wrapper.style.overflow =
|
|
150
|
-
wrapper.style.transition =
|
|
148
|
+
wrapper.style.height = 'auto'
|
|
149
|
+
wrapper.style.overflow = ''
|
|
150
|
+
wrapper.style.transition = ''
|
|
151
151
|
}
|
|
152
152
|
callbacks.onAfterEnter?.()
|
|
153
|
-
stage.set(
|
|
154
|
-
} else if (stage() ===
|
|
153
|
+
stage.set('entered')
|
|
154
|
+
} else if (stage() === 'leaving') {
|
|
155
155
|
callbacks.onAfterLeave?.()
|
|
156
|
-
stage.set(
|
|
156
|
+
stage.set('hidden')
|
|
157
157
|
}
|
|
158
158
|
},
|
|
159
159
|
})
|
|
160
160
|
|
|
161
|
-
const shouldRender = () => stage() !==
|
|
161
|
+
const shouldRender = () => stage() !== 'hidden'
|
|
162
162
|
|
|
163
163
|
const wrapperStyle: CSSProperties = {
|
|
164
164
|
...((htmlProps.style as CSSProperties) ?? {}),
|
|
165
|
-
...(stage() !==
|
|
166
|
-
...(stage() ===
|
|
165
|
+
...(stage() !== 'entered' ? { overflow: 'hidden' } : {}),
|
|
166
|
+
...(stage() === 'hidden' ? { height: '0px' } : stage() === 'entered' ? { height: 'auto' } : {}),
|
|
167
167
|
}
|
|
168
168
|
|
|
169
169
|
return h(
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import type { VNode } from
|
|
2
|
-
import { h } from
|
|
3
|
-
import { signal } from
|
|
4
|
-
import type { TransitionCallbacks } from
|
|
5
|
-
import TransitionItem from
|
|
6
|
-
import type { KineticConfig } from
|
|
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
7
|
|
|
8
8
|
type GroupRendererProps = {
|
|
9
9
|
config: KineticConfig
|
|
@@ -11,13 +11,18 @@ type GroupRendererProps = {
|
|
|
11
11
|
appear?: boolean | undefined
|
|
12
12
|
timeout?: number | undefined
|
|
13
13
|
callbacks: Partial<TransitionCallbacks>
|
|
14
|
-
|
|
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[])
|
|
15
20
|
}
|
|
16
21
|
|
|
17
22
|
type KeyedChild = { key: string | number; element: VNode }
|
|
18
23
|
|
|
19
24
|
const isVNode = (child: unknown): child is VNode =>
|
|
20
|
-
child != null && typeof child ===
|
|
25
|
+
child != null && typeof child === 'object' && 'type' in (child as object)
|
|
21
26
|
|
|
22
27
|
const getKeyedChildren = (children: VNode[]): KeyedChild[] => {
|
|
23
28
|
const result: KeyedChild[] = []
|
|
@@ -37,6 +42,9 @@ const getKeyedChildren = (children: VNode[]): KeyedChild[] => {
|
|
|
37
42
|
* Children that appear (new key) animate in. Children that disappear
|
|
38
43
|
* (removed key) stay in DOM during leave animation, then unmount.
|
|
39
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.
|
|
40
48
|
*/
|
|
41
49
|
const GroupRenderer = ({
|
|
42
50
|
config,
|
|
@@ -51,74 +59,89 @@ const GroupRenderer = ({
|
|
|
51
59
|
|
|
52
60
|
const prevMap = new Map<string | number, VNode>()
|
|
53
61
|
const leavingMap = new Map<string | number, VNode>()
|
|
54
|
-
const
|
|
62
|
+
const forceUpdate = signal(0)
|
|
55
63
|
|
|
56
|
-
|
|
57
|
-
const
|
|
58
|
-
for (const { key, element } of currentKeyed) {
|
|
59
|
-
currentMap.set(key, element)
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
const initialKeys: Set<string | number> = new Set(currentMap.keys())
|
|
63
|
-
|
|
64
|
-
// Detect leaving children
|
|
65
|
-
for (const [key, child] of prevMap) {
|
|
66
|
-
if (!currentMap.has(key)) {
|
|
67
|
-
leavingMap.set(key, child)
|
|
68
|
-
}
|
|
69
|
-
}
|
|
64
|
+
// Normalize children to an accessor
|
|
65
|
+
const getChildren = typeof children === 'function' ? (children as () => VNode[]) : () => children
|
|
70
66
|
|
|
71
|
-
//
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
prevMap.clear()
|
|
77
|
-
for (const [key, element] of currentMap) {
|
|
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) {
|
|
78
71
|
prevMap.set(key, element)
|
|
79
72
|
}
|
|
80
73
|
|
|
81
74
|
const handleAfterLeave = (key: string | number) => {
|
|
82
75
|
leavingMap.delete(key)
|
|
83
76
|
callbacks.onAfterLeave?.()
|
|
84
|
-
|
|
77
|
+
forceUpdate.update((c) => c + 1)
|
|
85
78
|
}
|
|
86
79
|
|
|
87
|
-
//
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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
|
+
}
|
|
92
114
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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
|
+
return h(config.tag, { ...htmlProps }, ...groupedChildren)
|
|
144
|
+
}) as unknown as VNode
|
|
122
145
|
}
|
|
123
146
|
|
|
124
147
|
export default GroupRenderer
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import type { VNode } from
|
|
2
|
-
import { h } from
|
|
3
|
-
import type { CSSProperties, TransitionCallbacks } from
|
|
4
|
-
import { cloneVNode } from
|
|
5
|
-
import TransitionItem from
|
|
6
|
-
import type { KineticConfig } from
|
|
1
|
+
import type { VNode } from '@pyreon/core'
|
|
2
|
+
import { h } from '@pyreon/core'
|
|
3
|
+
import type { CSSProperties, TransitionCallbacks } from '../types'
|
|
4
|
+
import { cloneVNode } from '../utils'
|
|
5
|
+
import TransitionItem from './TransitionItem'
|
|
6
|
+
import type { KineticConfig } from './types'
|
|
7
7
|
|
|
8
8
|
type StaggerRendererProps = {
|
|
9
9
|
config: KineticConfig
|
|
@@ -18,7 +18,7 @@ type StaggerRendererProps = {
|
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
const isVNode = (child: unknown): child is VNode =>
|
|
21
|
-
child != null && typeof child ===
|
|
21
|
+
child != null && typeof child === 'object' && 'type' in (child as object)
|
|
22
22
|
|
|
23
23
|
/**
|
|
24
24
|
* Renders children with staggered enter/exit animation.
|
|
@@ -73,8 +73,8 @@ const StaggerRenderer = ({
|
|
|
73
73
|
{cloneVNode(child, {
|
|
74
74
|
style: {
|
|
75
75
|
...((child.props as Record<string, unknown>)?.style as CSSProperties | undefined),
|
|
76
|
-
|
|
77
|
-
|
|
76
|
+
'--stagger-index': staggerIndex,
|
|
77
|
+
'--stagger-interval': `${effectiveInterval}ms`,
|
|
78
78
|
transitionDelay: `${delay}ms`,
|
|
79
79
|
} as CSSProperties,
|
|
80
80
|
})}
|