@pyreon/kinetic 0.11.3 → 0.11.5
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/lib/index.js +46 -37
- package/package.json +4 -4
- package/src/Transition.tsx +3 -6
- package/src/TransitionGroup.tsx +80 -54
- package/src/__tests__/GroupRenderer.test.tsx +153 -110
- package/src/kinetic/GroupRenderer.tsx +81 -58
package/lib/index.js
CHANGED
|
@@ -415,55 +415,64 @@ const getKeyedChildren = (children) => {
|
|
|
415
415
|
* Children that appear (new key) animate in. Children that disappear
|
|
416
416
|
* (removed key) stay in DOM during leave animation, then unmount.
|
|
417
417
|
* config.tag wraps all children as a container element.
|
|
418
|
+
*
|
|
419
|
+
* In Pyreon, components run once. Pass children as a reactive accessor
|
|
420
|
+
* `() => VNode[]` for the group to detect changes and animate entries/exits.
|
|
418
421
|
*/
|
|
419
422
|
const GroupRenderer = ({ config, htmlProps, appear, timeout, callbacks, children }) => {
|
|
420
423
|
const effectiveAppear = appear ?? config.appear ?? false;
|
|
421
424
|
const effectiveTimeout = timeout ?? config.timeout ?? 5e3;
|
|
422
425
|
const prevMap = /* @__PURE__ */ new Map();
|
|
423
426
|
const leavingMap = /* @__PURE__ */ new Map();
|
|
424
|
-
const
|
|
425
|
-
const
|
|
426
|
-
const
|
|
427
|
-
|
|
428
|
-
const
|
|
429
|
-
for (const [key, child] of prevMap) if (!currentMap.has(key)) leavingMap.set(key, child);
|
|
430
|
-
for (const key of currentMap.keys()) leavingMap.delete(key);
|
|
431
|
-
prevMap.clear();
|
|
432
|
-
for (const [key, element] of currentMap) prevMap.set(key, element);
|
|
427
|
+
const forceUpdate = signal(0);
|
|
428
|
+
const getChildren = typeof children === "function" ? children : () => children;
|
|
429
|
+
const initialKeyed = getKeyedChildren(getChildren());
|
|
430
|
+
const initialKeys = new Set(initialKeyed.map((c) => c.key));
|
|
431
|
+
for (const { key, element } of initialKeyed) prevMap.set(key, element);
|
|
433
432
|
const handleAfterLeave = (key) => {
|
|
434
433
|
leavingMap.delete(key);
|
|
435
434
|
callbacks.onAfterLeave?.();
|
|
436
|
-
|
|
435
|
+
forceUpdate.update((c) => c + 1);
|
|
437
436
|
};
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
const
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
437
|
+
return (() => {
|
|
438
|
+
forceUpdate();
|
|
439
|
+
const currentKeyed = getKeyedChildren(getChildren());
|
|
440
|
+
const currentMap = /* @__PURE__ */ new Map();
|
|
441
|
+
for (const { key, element } of currentKeyed) currentMap.set(key, element);
|
|
442
|
+
for (const [key, child] of prevMap) if (!currentMap.has(key) && !leavingMap.has(key)) leavingMap.set(key, child);
|
|
443
|
+
for (const key of currentMap.keys()) leavingMap.delete(key);
|
|
444
|
+
prevMap.clear();
|
|
445
|
+
for (const [key, element] of currentMap) prevMap.set(key, element);
|
|
446
|
+
const allEntries = [...currentKeyed];
|
|
447
|
+
for (const [key, element] of leavingMap) allEntries.push({
|
|
448
|
+
key,
|
|
449
|
+
element
|
|
450
|
+
});
|
|
451
|
+
const groupedChildren = allEntries.map(({ key, element }) => {
|
|
452
|
+
const isInitial = initialKeys.has(key);
|
|
453
|
+
const isShowing = currentMap.has(key);
|
|
454
|
+
return /* @__PURE__ */ jsx(TransitionItem, {
|
|
455
|
+
show: () => isShowing,
|
|
456
|
+
appear: isInitial ? effectiveAppear : true,
|
|
457
|
+
timeout: effectiveTimeout,
|
|
458
|
+
enterStyle: config.enterStyle,
|
|
459
|
+
enterToStyle: config.enterToStyle,
|
|
460
|
+
enterTransition: config.enterTransition,
|
|
461
|
+
leaveStyle: config.leaveStyle,
|
|
462
|
+
leaveToStyle: config.leaveToStyle,
|
|
463
|
+
leaveTransition: config.leaveTransition,
|
|
464
|
+
enter: config.enter,
|
|
465
|
+
enterFrom: config.enterFrom,
|
|
466
|
+
enterTo: config.enterTo,
|
|
467
|
+
leave: config.leave,
|
|
468
|
+
leaveFrom: config.leaveFrom,
|
|
469
|
+
leaveTo: config.leaveTo,
|
|
470
|
+
onAfterLeave: () => handleAfterLeave(key),
|
|
471
|
+
children: element
|
|
472
|
+
});
|
|
464
473
|
});
|
|
474
|
+
return h(config.tag, { ...htmlProps }, ...groupedChildren);
|
|
465
475
|
});
|
|
466
|
-
return h(config.tag, { ...htmlProps }, ...groupedChildren);
|
|
467
476
|
};
|
|
468
477
|
|
|
469
478
|
//#endregion
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pyreon/kinetic",
|
|
3
|
-
"version": "0.11.
|
|
3
|
+
"version": "0.11.5",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "https://github.com/pyreon/pyreon",
|
|
@@ -44,11 +44,11 @@
|
|
|
44
44
|
"typecheck": "tsc --noEmit"
|
|
45
45
|
},
|
|
46
46
|
"peerDependencies": {
|
|
47
|
-
"@pyreon/core": "^0.11.
|
|
48
|
-
"@pyreon/reactivity": "^0.11.
|
|
47
|
+
"@pyreon/core": "^0.11.5",
|
|
48
|
+
"@pyreon/reactivity": "^0.11.5"
|
|
49
49
|
},
|
|
50
50
|
"devDependencies": {
|
|
51
51
|
"@vitus-labs/tools-rolldown": "^1.15.3",
|
|
52
|
-
"@pyreon/typescript": "^0.11.
|
|
52
|
+
"@pyreon/typescript": "^0.11.5"
|
|
53
53
|
}
|
|
54
54
|
}
|
package/src/Transition.tsx
CHANGED
|
@@ -114,12 +114,11 @@ const Transition = ({
|
|
|
114
114
|
})
|
|
115
115
|
|
|
116
116
|
const elementRef = createRef<HTMLElement>()
|
|
117
|
+
const childProps = (children.props ?? {}) as Record<string, unknown>
|
|
117
118
|
const mergedRef = mergeRefs(
|
|
118
119
|
elementRef,
|
|
119
120
|
stateRef,
|
|
120
|
-
|
|
121
|
-
| ((el: HTMLElement | null) => void)
|
|
122
|
-
| undefined,
|
|
121
|
+
childProps.ref as ((el: HTMLElement | null) => void) | undefined,
|
|
123
122
|
)
|
|
124
123
|
|
|
125
124
|
const callbacks = {
|
|
@@ -198,9 +197,7 @@ const Transition = ({
|
|
|
198
197
|
: cloneVNode(children, {
|
|
199
198
|
ref: mergedRef,
|
|
200
199
|
style: mergeStyles(
|
|
201
|
-
|
|
202
|
-
| Record<string, string | number | undefined>
|
|
203
|
-
| undefined,
|
|
200
|
+
childProps.style as Record<string, string | number | undefined> | undefined,
|
|
204
201
|
{ display: "none" },
|
|
205
202
|
),
|
|
206
203
|
})
|
package/src/TransitionGroup.tsx
CHANGED
|
@@ -8,7 +8,12 @@ export type TransitionGroupProps = ClassTransitionProps &
|
|
|
8
8
|
TransitionCallbacks & {
|
|
9
9
|
appear?: boolean | undefined
|
|
10
10
|
timeout?: number | undefined
|
|
11
|
-
|
|
11
|
+
/**
|
|
12
|
+
* Children can be a static array OR a reactive accessor `() => VNode[]`.
|
|
13
|
+
* When passed as an accessor, TransitionGroup tracks changes and
|
|
14
|
+
* animates entering/leaving children automatically.
|
|
15
|
+
*/
|
|
16
|
+
children: VNode[] | (() => VNode[])
|
|
12
17
|
}
|
|
13
18
|
|
|
14
19
|
type KeyedChild = { key: string | number; element: VNode }
|
|
@@ -29,6 +34,14 @@ const getKeyedChildren = (children: VNode[]): KeyedChild[] => {
|
|
|
29
34
|
return result
|
|
30
35
|
}
|
|
31
36
|
|
|
37
|
+
/**
|
|
38
|
+
* Renders children with key-based enter/exit animations.
|
|
39
|
+
*
|
|
40
|
+
* In Pyreon, components run once. For TransitionGroup to detect children
|
|
41
|
+
* changes, pass children as a reactive accessor: `() => VNode[]`.
|
|
42
|
+
* The component uses a reactive accessor internally to diff previous vs
|
|
43
|
+
* current children and animate entries/exits.
|
|
44
|
+
*/
|
|
32
45
|
const TransitionGroup = ({
|
|
33
46
|
children,
|
|
34
47
|
appear = false,
|
|
@@ -38,71 +51,84 @@ const TransitionGroup = ({
|
|
|
38
51
|
}: TransitionGroupProps): VNode | null => {
|
|
39
52
|
const prevMap = new Map<string | number, VNode>()
|
|
40
53
|
const leavingMap = new Map<string | number, VNode>()
|
|
41
|
-
const
|
|
54
|
+
const forceUpdate = signal(0)
|
|
42
55
|
|
|
43
|
-
//
|
|
44
|
-
const
|
|
45
|
-
const currentMap = new Map<string | number, VNode>()
|
|
46
|
-
for (const { key, element } of currentKeyed) {
|
|
47
|
-
currentMap.set(key, element)
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
// Track initial keys to know which children were present on first render
|
|
51
|
-
const initialKeys: Set<string | number> = new Set(currentMap.keys())
|
|
52
|
-
|
|
53
|
-
// Detect leaving children (were in prev but not in current)
|
|
54
|
-
for (const [key, child] of prevMap) {
|
|
55
|
-
if (!currentMap.has(key)) {
|
|
56
|
-
leavingMap.set(key, child)
|
|
57
|
-
}
|
|
58
|
-
}
|
|
56
|
+
// Normalize children to an accessor for uniform handling
|
|
57
|
+
const getChildren = typeof children === "function" ? (children as () => VNode[]) : () => children
|
|
59
58
|
|
|
60
|
-
//
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
// Update prev
|
|
66
|
-
prevMap.clear()
|
|
67
|
-
for (const [key, element] of currentMap) {
|
|
59
|
+
// Track initial keys for appear animation logic
|
|
60
|
+
const initialKeyed = getKeyedChildren(getChildren())
|
|
61
|
+
const initialKeys = new Set(initialKeyed.map((c) => c.key))
|
|
62
|
+
for (const { key, element } of initialKeyed) {
|
|
68
63
|
prevMap.set(key, element)
|
|
69
64
|
}
|
|
70
65
|
|
|
71
66
|
const handleAfterLeave = (key: string | number) => {
|
|
72
67
|
leavingMap.delete(key)
|
|
73
68
|
onAfterLeave?.()
|
|
74
|
-
|
|
69
|
+
forceUpdate.update((c) => c + 1)
|
|
75
70
|
}
|
|
76
71
|
|
|
77
|
-
//
|
|
78
|
-
|
|
72
|
+
// Reactive accessor — re-evaluates when children() or forceUpdate changes.
|
|
73
|
+
// The runtime mounts this via mountReactive + effect, creating a
|
|
74
|
+
// reactive scope that tracks signal reads.
|
|
75
|
+
return (() => {
|
|
76
|
+
// Read forceUpdate to re-evaluate when leaving children finish
|
|
77
|
+
forceUpdate()
|
|
78
|
+
|
|
79
|
+
const currentChildren = getChildren()
|
|
80
|
+
const currentKeyed = getKeyedChildren(currentChildren)
|
|
81
|
+
const currentMap = new Map<string | number, VNode>()
|
|
82
|
+
for (const { key, element } of currentKeyed) {
|
|
83
|
+
currentMap.set(key, element)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Detect leaving children (were in prev but not in current)
|
|
87
|
+
for (const [key, child] of prevMap) {
|
|
88
|
+
if (!currentMap.has(key) && !leavingMap.has(key)) {
|
|
89
|
+
leavingMap.set(key, child)
|
|
90
|
+
}
|
|
91
|
+
}
|
|
79
92
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
93
|
+
// If a leaving child reappears, cancel the leave
|
|
94
|
+
for (const key of currentMap.keys()) {
|
|
95
|
+
leavingMap.delete(key)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Update prev for next diff
|
|
99
|
+
prevMap.clear()
|
|
100
|
+
for (const [key, element] of currentMap) {
|
|
101
|
+
prevMap.set(key, element)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Merge current + leaving, preserving insertion order
|
|
105
|
+
const allEntries: KeyedChild[] = [...currentKeyed]
|
|
106
|
+
for (const [key, element] of leavingMap) {
|
|
107
|
+
allEntries.push({ key, element })
|
|
108
|
+
}
|
|
83
109
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
)
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
)
|
|
110
|
+
return (
|
|
111
|
+
<>
|
|
112
|
+
{allEntries.map(({ key, element }) => {
|
|
113
|
+
const isInitial = initialKeys.has(key)
|
|
114
|
+
const isShowing = currentMap.has(key)
|
|
115
|
+
|
|
116
|
+
return (
|
|
117
|
+
<Transition
|
|
118
|
+
key={key}
|
|
119
|
+
show={() => isShowing}
|
|
120
|
+
appear={isInitial ? appear : true}
|
|
121
|
+
timeout={timeout}
|
|
122
|
+
{...transitionProps}
|
|
123
|
+
onAfterLeave={() => handleAfterLeave(key)}
|
|
124
|
+
>
|
|
125
|
+
{element}
|
|
126
|
+
</Transition>
|
|
127
|
+
)
|
|
128
|
+
})}
|
|
129
|
+
</>
|
|
130
|
+
)
|
|
131
|
+
}) as unknown as VNode
|
|
106
132
|
}
|
|
107
133
|
|
|
108
134
|
export default TransitionGroup
|
|
@@ -40,6 +40,13 @@ const makeConfig = (overrides: Partial<KineticConfig> = {}): KineticConfig => ({
|
|
|
40
40
|
...overrides,
|
|
41
41
|
})
|
|
42
42
|
|
|
43
|
+
/** Unwrap reactive accessors returned by GroupRenderer. */
|
|
44
|
+
const unwrap = (val: any): any => {
|
|
45
|
+
let result = val
|
|
46
|
+
while (typeof result === "function") result = result()
|
|
47
|
+
return result
|
|
48
|
+
}
|
|
49
|
+
|
|
43
50
|
const makeKeyedChild = (key: string | number, text: string): VNode => ({
|
|
44
51
|
type: "span",
|
|
45
52
|
props: { "data-testid": `child-${key}` },
|
|
@@ -52,12 +59,14 @@ describe("GroupRenderer", () => {
|
|
|
52
59
|
const config = makeConfig()
|
|
53
60
|
const children = [makeKeyedChild("a", "Alpha"), makeKeyedChild("b", "Beta")]
|
|
54
61
|
|
|
55
|
-
const vnode =
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
62
|
+
const vnode = unwrap(
|
|
63
|
+
GroupRenderer({
|
|
64
|
+
config,
|
|
65
|
+
htmlProps: {},
|
|
66
|
+
callbacks: {},
|
|
67
|
+
children,
|
|
68
|
+
}),
|
|
69
|
+
)
|
|
61
70
|
|
|
62
71
|
expect(vnode).not.toBeNull()
|
|
63
72
|
expect(vnode?.type).toBe("div")
|
|
@@ -67,12 +76,14 @@ describe("GroupRenderer", () => {
|
|
|
67
76
|
const config = makeConfig({ tag: "ul" })
|
|
68
77
|
const children = [makeKeyedChild("a", "Alpha")]
|
|
69
78
|
|
|
70
|
-
const vnode =
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
79
|
+
const vnode = unwrap(
|
|
80
|
+
GroupRenderer({
|
|
81
|
+
config,
|
|
82
|
+
htmlProps: {},
|
|
83
|
+
callbacks: {},
|
|
84
|
+
children,
|
|
85
|
+
}),
|
|
86
|
+
)
|
|
76
87
|
|
|
77
88
|
expect(vnode?.type).toBe("ul")
|
|
78
89
|
})
|
|
@@ -81,12 +92,14 @@ describe("GroupRenderer", () => {
|
|
|
81
92
|
const config = makeConfig()
|
|
82
93
|
const children = [makeKeyedChild("a", "Alpha")]
|
|
83
94
|
|
|
84
|
-
const vnode =
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
95
|
+
const vnode = unwrap(
|
|
96
|
+
GroupRenderer({
|
|
97
|
+
config,
|
|
98
|
+
htmlProps: { "data-testid": "group-wrapper", class: "my-group" },
|
|
99
|
+
callbacks: {},
|
|
100
|
+
children,
|
|
101
|
+
}),
|
|
102
|
+
)
|
|
90
103
|
|
|
91
104
|
const props = vnode?.props as Record<string, unknown>
|
|
92
105
|
expect(props?.["data-testid"]).toBe("group-wrapper")
|
|
@@ -97,12 +110,14 @@ describe("GroupRenderer", () => {
|
|
|
97
110
|
const config = makeConfig()
|
|
98
111
|
const children = [makeKeyedChild("a", "Alpha"), makeKeyedChild("b", "Beta")]
|
|
99
112
|
|
|
100
|
-
const vnode =
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
113
|
+
const vnode = unwrap(
|
|
114
|
+
GroupRenderer({
|
|
115
|
+
config,
|
|
116
|
+
htmlProps: {},
|
|
117
|
+
callbacks: {},
|
|
118
|
+
children,
|
|
119
|
+
}),
|
|
120
|
+
)
|
|
106
121
|
|
|
107
122
|
// The wrapper div should have children that are TransitionItem VNodes
|
|
108
123
|
const wrapperChildren = vnode?.children
|
|
@@ -122,21 +137,25 @@ describe("GroupRenderer", () => {
|
|
|
122
137
|
|
|
123
138
|
// First render with initial children
|
|
124
139
|
const initialChildren = [makeKeyedChild("a", "Alpha")]
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
140
|
+
unwrap(
|
|
141
|
+
GroupRenderer({
|
|
142
|
+
config,
|
|
143
|
+
htmlProps: {},
|
|
144
|
+
callbacks: {},
|
|
145
|
+
children: initialChildren,
|
|
146
|
+
}),
|
|
147
|
+
)
|
|
131
148
|
|
|
132
149
|
// Second render with a new child added
|
|
133
150
|
const updatedChildren = [makeKeyedChild("a", "Alpha"), makeKeyedChild("b", "Beta")]
|
|
134
|
-
const vnode =
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
151
|
+
const vnode = unwrap(
|
|
152
|
+
GroupRenderer({
|
|
153
|
+
config,
|
|
154
|
+
htmlProps: {},
|
|
155
|
+
callbacks: {},
|
|
156
|
+
children: updatedChildren,
|
|
157
|
+
}),
|
|
158
|
+
)
|
|
140
159
|
|
|
141
160
|
const wrapperChildren = vnode?.children as VNode[]
|
|
142
161
|
expect(wrapperChildren.length).toBe(2)
|
|
@@ -153,12 +172,14 @@ describe("GroupRenderer", () => {
|
|
|
153
172
|
})
|
|
154
173
|
const children = [makeKeyedChild("a", "Alpha")]
|
|
155
174
|
|
|
156
|
-
const vnode =
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
175
|
+
const vnode = unwrap(
|
|
176
|
+
GroupRenderer({
|
|
177
|
+
config,
|
|
178
|
+
htmlProps: {},
|
|
179
|
+
callbacks: {},
|
|
180
|
+
children,
|
|
181
|
+
}),
|
|
182
|
+
)
|
|
162
183
|
|
|
163
184
|
const wrapperChildren = vnode?.children as VNode[]
|
|
164
185
|
const transitionItemVNode = wrapperChildren[0] as VNode
|
|
@@ -183,12 +204,14 @@ describe("GroupRenderer", () => {
|
|
|
183
204
|
})
|
|
184
205
|
const children = [makeKeyedChild("a", "Alpha")]
|
|
185
206
|
|
|
186
|
-
const vnode =
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
207
|
+
const vnode = unwrap(
|
|
208
|
+
GroupRenderer({
|
|
209
|
+
config,
|
|
210
|
+
htmlProps: {},
|
|
211
|
+
callbacks: {},
|
|
212
|
+
children,
|
|
213
|
+
}),
|
|
214
|
+
)
|
|
192
215
|
|
|
193
216
|
const wrapperChildren = vnode?.children as VNode[]
|
|
194
217
|
const tiProps = wrapperChildren[0]?.props as Record<string, unknown>
|
|
@@ -205,12 +228,14 @@ describe("GroupRenderer", () => {
|
|
|
205
228
|
const config = makeConfig({ appear: true })
|
|
206
229
|
const children = [makeKeyedChild("a", "Alpha")]
|
|
207
230
|
|
|
208
|
-
const vnode =
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
231
|
+
const vnode = unwrap(
|
|
232
|
+
GroupRenderer({
|
|
233
|
+
config,
|
|
234
|
+
htmlProps: {},
|
|
235
|
+
callbacks: {},
|
|
236
|
+
children,
|
|
237
|
+
}),
|
|
238
|
+
)
|
|
214
239
|
|
|
215
240
|
const wrapperChildren = vnode?.children as VNode[]
|
|
216
241
|
const tiProps = wrapperChildren[0]?.props as Record<string, unknown>
|
|
@@ -223,12 +248,14 @@ describe("GroupRenderer", () => {
|
|
|
223
248
|
const config = makeConfig({ timeout: 2000 })
|
|
224
249
|
const children = [makeKeyedChild("a", "Alpha")]
|
|
225
250
|
|
|
226
|
-
const vnode =
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
251
|
+
const vnode = unwrap(
|
|
252
|
+
GroupRenderer({
|
|
253
|
+
config,
|
|
254
|
+
htmlProps: {},
|
|
255
|
+
callbacks: {},
|
|
256
|
+
children,
|
|
257
|
+
}),
|
|
258
|
+
)
|
|
232
259
|
|
|
233
260
|
const wrapperChildren = vnode?.children as VNode[]
|
|
234
261
|
const tiProps = wrapperChildren[0]?.props as Record<string, unknown>
|
|
@@ -240,12 +267,14 @@ describe("GroupRenderer", () => {
|
|
|
240
267
|
const config = makeConfig()
|
|
241
268
|
const children = [makeKeyedChild("a", "Alpha")]
|
|
242
269
|
|
|
243
|
-
const vnode =
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
270
|
+
const vnode = unwrap(
|
|
271
|
+
GroupRenderer({
|
|
272
|
+
config,
|
|
273
|
+
htmlProps: {},
|
|
274
|
+
callbacks: {},
|
|
275
|
+
children,
|
|
276
|
+
}),
|
|
277
|
+
)
|
|
249
278
|
|
|
250
279
|
const wrapperChildren = vnode?.children as VNode[]
|
|
251
280
|
const tiProps = wrapperChildren[0]?.props as Record<string, unknown>
|
|
@@ -258,12 +287,14 @@ describe("GroupRenderer", () => {
|
|
|
258
287
|
const keyedChild = makeKeyedChild("a", "Alpha")
|
|
259
288
|
const unkeyedChild: VNode = { type: "span", props: {}, children: ["No key"], key: null }
|
|
260
289
|
|
|
261
|
-
const vnode =
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
290
|
+
const vnode = unwrap(
|
|
291
|
+
GroupRenderer({
|
|
292
|
+
config,
|
|
293
|
+
htmlProps: {},
|
|
294
|
+
callbacks: {},
|
|
295
|
+
children: [keyedChild, unkeyedChild],
|
|
296
|
+
}),
|
|
297
|
+
)
|
|
267
298
|
|
|
268
299
|
// Only keyed child should be wrapped in TransitionItem
|
|
269
300
|
const wrapperChildren = vnode?.children as VNode[]
|
|
@@ -274,13 +305,15 @@ describe("GroupRenderer", () => {
|
|
|
274
305
|
const config = makeConfig({ appear: false })
|
|
275
306
|
const children = [makeKeyedChild("a", "Alpha")]
|
|
276
307
|
|
|
277
|
-
const vnode =
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
308
|
+
const vnode = unwrap(
|
|
309
|
+
GroupRenderer({
|
|
310
|
+
config,
|
|
311
|
+
htmlProps: {},
|
|
312
|
+
appear: true,
|
|
313
|
+
callbacks: {},
|
|
314
|
+
children,
|
|
315
|
+
}),
|
|
316
|
+
)
|
|
284
317
|
|
|
285
318
|
const wrapperChildren = vnode?.children as VNode[]
|
|
286
319
|
const tiProps = wrapperChildren[0]?.props as Record<string, unknown>
|
|
@@ -292,13 +325,15 @@ describe("GroupRenderer", () => {
|
|
|
292
325
|
const config = makeConfig({ timeout: 2000 })
|
|
293
326
|
const children = [makeKeyedChild("a", "Alpha")]
|
|
294
327
|
|
|
295
|
-
const vnode =
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
328
|
+
const vnode = unwrap(
|
|
329
|
+
GroupRenderer({
|
|
330
|
+
config,
|
|
331
|
+
htmlProps: {},
|
|
332
|
+
timeout: 3000,
|
|
333
|
+
callbacks: {},
|
|
334
|
+
children,
|
|
335
|
+
}),
|
|
336
|
+
)
|
|
302
337
|
|
|
303
338
|
const wrapperChildren = vnode?.children as VNode[]
|
|
304
339
|
const tiProps = wrapperChildren[0]?.props as Record<string, unknown>
|
|
@@ -311,12 +346,14 @@ describe("GroupRenderer", () => {
|
|
|
311
346
|
const config = makeConfig()
|
|
312
347
|
const children = [makeKeyedChild("a", "Alpha")]
|
|
313
348
|
|
|
314
|
-
const vnode =
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
349
|
+
const vnode = unwrap(
|
|
350
|
+
GroupRenderer({
|
|
351
|
+
config,
|
|
352
|
+
htmlProps: {},
|
|
353
|
+
callbacks: { onAfterLeave },
|
|
354
|
+
children,
|
|
355
|
+
}),
|
|
356
|
+
)
|
|
320
357
|
|
|
321
358
|
// Each TransitionItem child gets an onAfterLeave that calls handleAfterLeave(key)
|
|
322
359
|
const wrapperChildren = vnode?.children as VNode[]
|
|
@@ -333,12 +370,14 @@ describe("GroupRenderer", () => {
|
|
|
333
370
|
const config = makeConfig()
|
|
334
371
|
const children = [makeKeyedChild("a", "Alpha"), makeKeyedChild("b", "Beta")]
|
|
335
372
|
|
|
336
|
-
const vnode =
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
373
|
+
const vnode = unwrap(
|
|
374
|
+
GroupRenderer({
|
|
375
|
+
config,
|
|
376
|
+
htmlProps: {},
|
|
377
|
+
callbacks: {},
|
|
378
|
+
children,
|
|
379
|
+
}),
|
|
380
|
+
)
|
|
342
381
|
|
|
343
382
|
const wrapperChildren = vnode?.children as VNode[]
|
|
344
383
|
|
|
@@ -353,12 +392,14 @@ describe("GroupRenderer", () => {
|
|
|
353
392
|
const config = makeConfig({ appear: false })
|
|
354
393
|
const children = [makeKeyedChild("a", "Alpha")]
|
|
355
394
|
|
|
356
|
-
const vnode =
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
395
|
+
const vnode = unwrap(
|
|
396
|
+
GroupRenderer({
|
|
397
|
+
config,
|
|
398
|
+
htmlProps: {},
|
|
399
|
+
callbacks: {},
|
|
400
|
+
children,
|
|
401
|
+
}),
|
|
402
|
+
)
|
|
362
403
|
|
|
363
404
|
const wrapperChildren = vnode?.children as VNode[]
|
|
364
405
|
const tiProps = wrapperChildren[0]?.props as Record<string, unknown>
|
|
@@ -371,12 +412,14 @@ describe("GroupRenderer", () => {
|
|
|
371
412
|
const config = makeConfig()
|
|
372
413
|
const children = [makeKeyedChild("a", "Alpha")]
|
|
373
414
|
|
|
374
|
-
const vnode =
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
415
|
+
const vnode = unwrap(
|
|
416
|
+
GroupRenderer({
|
|
417
|
+
config,
|
|
418
|
+
htmlProps: {},
|
|
419
|
+
callbacks: {},
|
|
420
|
+
children,
|
|
421
|
+
}),
|
|
422
|
+
)
|
|
380
423
|
|
|
381
424
|
const wrapperChildren = vnode?.children as VNode[]
|
|
382
425
|
const tiVNode = wrapperChildren[0] as VNode
|
|
@@ -11,7 +11,12 @@ 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 }
|
|
@@ -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
|