@pyreon/elements 0.24.2 → 0.24.3
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 +18 -7
- package/package.json +9 -9
- package/src/Element/component.tsx +29 -5
- package/src/__tests__/Content.test.tsx +10 -2
- package/src/__tests__/Element-slot-reactivity.browser.test.tsx +152 -0
- package/src/__tests__/Element.test.ts +12 -3
- package/src/helpers/Content/component.tsx +16 -2
package/lib/index.js
CHANGED
|
@@ -91,8 +91,15 @@ const StyledComponent = styled$2(component$1, { layer: "elements" })`
|
|
|
91
91
|
* gap, and equalCols styling props to the underlying styled component.
|
|
92
92
|
* Adds a `data-pyr-element` attribute in development for debugging.
|
|
93
93
|
*
|
|
94
|
-
* Children are rendered via core `render()
|
|
94
|
+
* Children are rendered via core `render()`, with function-valued
|
|
95
|
+
* children unwrapped inside a reactive accessor so the compound-layout
|
|
96
|
+
* paths in `Element` keep `content={() => <X />}` reactivity intact
|
|
97
|
+
* (mirrors the `resolveSlot` helper in `Element/component.tsx`).
|
|
95
98
|
*/
|
|
99
|
+
const resolveSlot = (value) => {
|
|
100
|
+
if (typeof value === "function") return value();
|
|
101
|
+
return render(value);
|
|
102
|
+
};
|
|
96
103
|
const Component$9 = (props) => {
|
|
97
104
|
const [own, rest] = splitProps(props, [
|
|
98
105
|
"contentType",
|
|
@@ -123,7 +130,7 @@ const Component$9 = (props) => {
|
|
|
123
130
|
$element: stylingProps,
|
|
124
131
|
...debugProps,
|
|
125
132
|
...rest,
|
|
126
|
-
children:
|
|
133
|
+
children: () => resolveSlot(own.children)
|
|
127
134
|
});
|
|
128
135
|
};
|
|
129
136
|
|
|
@@ -508,6 +515,10 @@ const Component = (props) => {
|
|
|
508
515
|
const shouldBeEmpty = !!rest.dangerouslySetInnerHTML || getShouldBeEmpty(own.tag);
|
|
509
516
|
const isSimpleElement = !own.beforeContent && !own.afterContent;
|
|
510
517
|
const getChildren = () => own.children ?? own.content ?? own.label;
|
|
518
|
+
const resolveSlot = (value) => {
|
|
519
|
+
if (typeof value === "function") return value();
|
|
520
|
+
return render(value);
|
|
521
|
+
};
|
|
511
522
|
const isInline = isInlineElement(own.tag);
|
|
512
523
|
const SUB_TAG = isInline ? "span" : void 0;
|
|
513
524
|
let wrapperDirection = own.direction;
|
|
@@ -563,13 +574,13 @@ const Component = (props) => {
|
|
|
563
574
|
equalCols: own.equalCols,
|
|
564
575
|
extraStyles: own.css
|
|
565
576
|
}),
|
|
566
|
-
children:
|
|
577
|
+
children: () => resolveSlot(getChildren())
|
|
567
578
|
});
|
|
568
579
|
if (isSimpleElement) return /* @__PURE__ */ jsx(Wrapper_default, {
|
|
569
580
|
...rest,
|
|
570
581
|
...WRAPPER_PROPS,
|
|
571
582
|
isInline,
|
|
572
|
-
children:
|
|
583
|
+
children: () => resolveSlot(getChildren())
|
|
573
584
|
});
|
|
574
585
|
return /* @__PURE__ */ jsxs(Wrapper_default, {
|
|
575
586
|
...rest,
|
|
@@ -586,7 +597,7 @@ const Component = (props) => {
|
|
|
586
597
|
alignY: beforeContentAlignY,
|
|
587
598
|
equalCols: own.equalCols,
|
|
588
599
|
gap: own.gap,
|
|
589
|
-
children: own.beforeContent
|
|
600
|
+
children: () => resolveSlot(own.beforeContent)
|
|
590
601
|
}),
|
|
591
602
|
/* @__PURE__ */ jsx(Content_default, {
|
|
592
603
|
tag: SUB_TAG,
|
|
@@ -597,7 +608,7 @@ const Component = (props) => {
|
|
|
597
608
|
alignX: contentAlignX,
|
|
598
609
|
alignY: contentAlignY,
|
|
599
610
|
equalCols: own.equalCols,
|
|
600
|
-
children: getChildren()
|
|
611
|
+
children: () => resolveSlot(getChildren())
|
|
601
612
|
}),
|
|
602
613
|
own.afterContent && /* @__PURE__ */ jsx(Content_default, {
|
|
603
614
|
tag: SUB_TAG,
|
|
@@ -609,7 +620,7 @@ const Component = (props) => {
|
|
|
609
620
|
alignY: afterContentAlignY,
|
|
610
621
|
equalCols: own.equalCols,
|
|
611
622
|
gap: own.gap,
|
|
612
|
-
children: own.afterContent
|
|
623
|
+
children: () => resolveSlot(own.afterContent)
|
|
613
624
|
})
|
|
614
625
|
]
|
|
615
626
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pyreon/elements",
|
|
3
|
-
"version": "0.24.
|
|
3
|
+
"version": "0.24.3",
|
|
4
4
|
"description": "Foundational UI components for Pyreon",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -42,12 +42,12 @@
|
|
|
42
42
|
"typecheck": "tsc --noEmit"
|
|
43
43
|
},
|
|
44
44
|
"devDependencies": {
|
|
45
|
-
"@pyreon/core": "^0.24.
|
|
45
|
+
"@pyreon/core": "^0.24.3",
|
|
46
46
|
"@pyreon/manifest": "0.13.1",
|
|
47
|
-
"@pyreon/reactivity": "^0.24.
|
|
48
|
-
"@pyreon/runtime-dom": "^0.24.
|
|
47
|
+
"@pyreon/reactivity": "^0.24.3",
|
|
48
|
+
"@pyreon/runtime-dom": "^0.24.3",
|
|
49
49
|
"@pyreon/test-utils": "^0.13.11",
|
|
50
|
-
"@pyreon/typescript": "^0.24.
|
|
50
|
+
"@pyreon/typescript": "^0.24.3",
|
|
51
51
|
"@vitest/browser-playwright": "^4.1.4",
|
|
52
52
|
"@vitus-labs/tools-rolldown": "^2.4.0"
|
|
53
53
|
},
|
|
@@ -55,9 +55,9 @@
|
|
|
55
55
|
"node": ">= 22"
|
|
56
56
|
},
|
|
57
57
|
"dependencies": {
|
|
58
|
-
"@pyreon/core": "^0.24.
|
|
59
|
-
"@pyreon/reactivity": "^0.24.
|
|
60
|
-
"@pyreon/ui-core": "^0.24.
|
|
61
|
-
"@pyreon/unistyle": "^0.24.
|
|
58
|
+
"@pyreon/core": "^0.24.3",
|
|
59
|
+
"@pyreon/reactivity": "^0.24.3",
|
|
60
|
+
"@pyreon/ui-core": "^0.24.3",
|
|
61
|
+
"@pyreon/unistyle": "^0.24.3"
|
|
62
62
|
}
|
|
63
63
|
}
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { onMount, splitProps } from '@pyreon/core'
|
|
11
|
+
import type { VNodeChildAtom } from '@pyreon/core'
|
|
11
12
|
import { render } from '@pyreon/ui-core'
|
|
12
13
|
import { PKG_NAME } from '../constants'
|
|
13
14
|
import { Content, Wrapper } from '../helpers'
|
|
@@ -102,6 +103,29 @@ const Component: PyreonElement = (props) => {
|
|
|
102
103
|
// Reading own.content via ?? at setup would capture the value once.
|
|
103
104
|
const getChildren = () => own.children ?? own.content ?? own.label
|
|
104
105
|
|
|
106
|
+
// Resolve a slot value INSIDE a reactive accessor. If the consumer passed a
|
|
107
|
+
// function-returning-VNode (e.g. `content={() => <Icon name={signal()} />}`),
|
|
108
|
+
// unwrap it by calling — its body's signal reads are then tracked by the
|
|
109
|
+
// enclosing mountReactive effect, and the slot re-renders on signal change.
|
|
110
|
+
// Static VNodes / strings / null pass through unchanged to `render()`.
|
|
111
|
+
//
|
|
112
|
+
// Pre-fix: `render(() => <X/>)` treated the function as a COMPONENT and
|
|
113
|
+
// called `h(fn, {})` — the component body ran once at mount, future signal
|
|
114
|
+
// changes inside the body were never observed. Wrapping the JSX position in
|
|
115
|
+
// `{() => resolveSlot(...)}` plus unwrapping function values here is what
|
|
116
|
+
// makes `content={() => ...}` reactive (matches the
|
|
117
|
+
// `{() => show() ? <A/> : null}` pattern documented at
|
|
118
|
+
// runtime-dom/src/nodes.ts:90-93).
|
|
119
|
+
// Return type is the RESOLVED atom (VNodeChildAtom | VNodeChildAtom[]) —
|
|
120
|
+
// never a nested accessor — so the enclosing `() => resolveSlot(...)` IS
|
|
121
|
+
// a valid VNodeChildAccessor in the JSX child position.
|
|
122
|
+
const resolveSlot = (value: unknown): VNodeChildAtom | VNodeChildAtom[] => {
|
|
123
|
+
if (typeof value === 'function') {
|
|
124
|
+
return (value as () => VNodeChildAtom | VNodeChildAtom[])()
|
|
125
|
+
}
|
|
126
|
+
return render(value as Parameters<typeof render>[0]) as VNodeChildAtom | VNodeChildAtom[]
|
|
127
|
+
}
|
|
128
|
+
|
|
105
129
|
const isInline = isInlineElement(own.tag)
|
|
106
130
|
const SUB_TAG = isInline ? 'span' : undefined
|
|
107
131
|
|
|
@@ -201,7 +225,7 @@ const Component: PyreonElement = (props) => {
|
|
|
201
225
|
extraStyles: own.css,
|
|
202
226
|
})}
|
|
203
227
|
>
|
|
204
|
-
{
|
|
228
|
+
{() => resolveSlot(getChildren())}
|
|
205
229
|
</WrapperStyled>
|
|
206
230
|
)
|
|
207
231
|
}
|
|
@@ -209,7 +233,7 @@ const Component: PyreonElement = (props) => {
|
|
|
209
233
|
if (isSimpleElement) {
|
|
210
234
|
return (
|
|
211
235
|
<Wrapper {...rest} {...WRAPPER_PROPS} isInline={isInline}>
|
|
212
|
-
{
|
|
236
|
+
{() => resolveSlot(getChildren())}
|
|
213
237
|
</Wrapper>
|
|
214
238
|
)
|
|
215
239
|
}
|
|
@@ -228,7 +252,7 @@ const Component: PyreonElement = (props) => {
|
|
|
228
252
|
equalCols={own.equalCols}
|
|
229
253
|
gap={own.gap}
|
|
230
254
|
>
|
|
231
|
-
{own.beforeContent}
|
|
255
|
+
{() => resolveSlot(own.beforeContent)}
|
|
232
256
|
</Content>
|
|
233
257
|
)}
|
|
234
258
|
|
|
@@ -242,7 +266,7 @@ const Component: PyreonElement = (props) => {
|
|
|
242
266
|
alignY={contentAlignY}
|
|
243
267
|
equalCols={own.equalCols}
|
|
244
268
|
>
|
|
245
|
-
{getChildren()}
|
|
269
|
+
{() => resolveSlot(getChildren())}
|
|
246
270
|
</Content>
|
|
247
271
|
|
|
248
272
|
{own.afterContent && (
|
|
@@ -257,7 +281,7 @@ const Component: PyreonElement = (props) => {
|
|
|
257
281
|
equalCols={own.equalCols}
|
|
258
282
|
gap={own.gap}
|
|
259
283
|
>
|
|
260
|
-
{own.afterContent}
|
|
284
|
+
{() => resolveSlot(own.afterContent)}
|
|
261
285
|
</Content>
|
|
262
286
|
)}
|
|
263
287
|
</Wrapper>
|
|
@@ -76,9 +76,17 @@ describe('Content component', () => {
|
|
|
76
76
|
expect(result.props['data-pyr-element']).toBe('after')
|
|
77
77
|
})
|
|
78
78
|
|
|
79
|
-
it('passes children through render()', () => {
|
|
79
|
+
it('passes children through render() when the slot accessor is invoked', () => {
|
|
80
|
+
// Content wraps its children in a reactive accessor `() => resolveSlot(...)`
|
|
81
|
+
// — render() is no longer called synchronously at component setup. The
|
|
82
|
+
// accessor is invoked by the runtime when the JSX child position mounts;
|
|
83
|
+
// here we invoke it directly to assert the wiring. The
|
|
84
|
+
// function-unwrap-then-render shape is what keeps `content={() => <X/>}`
|
|
85
|
+
// slot reactivity working (see Element-slot-reactivity.browser.test.tsx).
|
|
80
86
|
const children = 'Some text'
|
|
81
|
-
Content({ children })
|
|
87
|
+
const result = asVNode(Content({ children }))
|
|
88
|
+
expect(typeof result.props.children).toBe('function')
|
|
89
|
+
;(result.props.children as () => unknown)()
|
|
82
90
|
expect(mocks.render).toHaveBeenCalledWith(children)
|
|
83
91
|
})
|
|
84
92
|
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/** @jsxImportSource @pyreon/core */
|
|
2
|
+
/**
|
|
3
|
+
* Regression specs for the Element slot reactivity bug.
|
|
4
|
+
*
|
|
5
|
+
* Pre-fix: `<Element content={() => <Icon name={signal()} />}>` evaluated the
|
|
6
|
+
* function once at mount and baked in the result. Signal changes inside the
|
|
7
|
+
* function body did NOT cause the slot to re-render — even though the
|
|
8
|
+
* `getChildren` helper in Element/component.tsx had a getter shape intended
|
|
9
|
+
* to preserve reactivity.
|
|
10
|
+
*
|
|
11
|
+
* Root cause: the JSX child position read the resolved slot value at
|
|
12
|
+
* component-setup time. The runtime's `mountChild` reactive-function-child
|
|
13
|
+
* handling (`mountReactive`) was never reached because the function was
|
|
14
|
+
* passed to `render()` which treated it as a component (one-shot mount),
|
|
15
|
+
* not as a reactive accessor.
|
|
16
|
+
*
|
|
17
|
+
* Fix: wrap the JSX child position in `{() => ...}` so it becomes a
|
|
18
|
+
* reactive accessor that mountChild routes through mountReactive.
|
|
19
|
+
* Slot values that are themselves functions get unwrapped (called) inside
|
|
20
|
+
* the accessor so their body's signal reads are tracked by the effect.
|
|
21
|
+
*
|
|
22
|
+
* Bisect-verify-with-restore: revert the wrap → these tests fail with
|
|
23
|
+
* stuck slot content; restore → tests pass.
|
|
24
|
+
*/
|
|
25
|
+
import { describe, expect, it } from 'vitest'
|
|
26
|
+
import { signal } from '@pyreon/reactivity'
|
|
27
|
+
import { flush, mountInBrowser } from '@pyreon/test-utils/browser'
|
|
28
|
+
import { Element } from '../Element'
|
|
29
|
+
|
|
30
|
+
describe('Element slot reactivity — function-valued slot props', () => {
|
|
31
|
+
it('content={() => <X />} re-renders when a signal inside the function body changes', async () => {
|
|
32
|
+
const dark = signal(false)
|
|
33
|
+
const { container, unmount } = mountInBrowser(
|
|
34
|
+
<Element
|
|
35
|
+
tag="div"
|
|
36
|
+
data-id="root"
|
|
37
|
+
content={() => <span data-id="icon">{dark() ? 'moon' : 'sun'}</span>}
|
|
38
|
+
/>,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
expect(container.querySelector('[data-id="icon"]')?.textContent).toBe('sun')
|
|
42
|
+
dark.set(true)
|
|
43
|
+
await flush()
|
|
44
|
+
expect(container.querySelector('[data-id="icon"]')?.textContent).toBe('moon')
|
|
45
|
+
dark.set(false)
|
|
46
|
+
await flush()
|
|
47
|
+
expect(container.querySelector('[data-id="icon"]')?.textContent).toBe('sun')
|
|
48
|
+
unmount()
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
it('beforeContent={() => <X />} re-renders when a signal inside changes', async () => {
|
|
52
|
+
const count = signal(0)
|
|
53
|
+
const { container, unmount } = mountInBrowser(
|
|
54
|
+
<Element
|
|
55
|
+
tag="div"
|
|
56
|
+
data-id="root"
|
|
57
|
+
beforeContent={() => <span data-id="badge">{`#${count()}`}</span>}
|
|
58
|
+
content={<span data-id="main">main</span>}
|
|
59
|
+
/>,
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
expect(container.querySelector('[data-id="badge"]')?.textContent).toBe('#0')
|
|
63
|
+
count.set(5)
|
|
64
|
+
await flush()
|
|
65
|
+
expect(container.querySelector('[data-id="badge"]')?.textContent).toBe('#5')
|
|
66
|
+
unmount()
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
it('afterContent={() => <X />} re-renders when a signal inside changes', async () => {
|
|
70
|
+
const tag = signal('draft')
|
|
71
|
+
const { container, unmount } = mountInBrowser(
|
|
72
|
+
<Element
|
|
73
|
+
tag="div"
|
|
74
|
+
data-id="root"
|
|
75
|
+
content={<span data-id="main">main</span>}
|
|
76
|
+
afterContent={() => <span data-id="status">{tag()}</span>}
|
|
77
|
+
/>,
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
expect(container.querySelector('[data-id="status"]')?.textContent).toBe('draft')
|
|
81
|
+
tag.set('published')
|
|
82
|
+
await flush()
|
|
83
|
+
expect(container.querySelector('[data-id="status"]')?.textContent).toBe('published')
|
|
84
|
+
unmount()
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
it('static VNode content (non-function) still works unchanged', () => {
|
|
88
|
+
// Regression guard for the static path — no function unwrap should
|
|
89
|
+
// happen here.
|
|
90
|
+
const { container, unmount } = mountInBrowser(
|
|
91
|
+
<Element tag="div" content={<span data-id="static">hello</span>} />,
|
|
92
|
+
)
|
|
93
|
+
expect(container.querySelector('[data-id="static"]')?.textContent).toBe('hello')
|
|
94
|
+
unmount()
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
it('static beforeContent + afterContent still render (compound path)', () => {
|
|
98
|
+
const { container, unmount } = mountInBrowser(
|
|
99
|
+
<Element
|
|
100
|
+
tag="div"
|
|
101
|
+
beforeContent={<span data-id="b">before</span>}
|
|
102
|
+
content={<span data-id="c">main</span>}
|
|
103
|
+
afterContent={<span data-id="a">after</span>}
|
|
104
|
+
/>,
|
|
105
|
+
)
|
|
106
|
+
expect(container.querySelector('[data-id="b"]')?.textContent).toBe('before')
|
|
107
|
+
expect(container.querySelector('[data-id="c"]')?.textContent).toBe('main')
|
|
108
|
+
expect(container.querySelector('[data-id="a"]')?.textContent).toBe('after')
|
|
109
|
+
unmount()
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
it('null slot stays unrendered; flipping a signal can introduce it', async () => {
|
|
113
|
+
const show = signal(false)
|
|
114
|
+
const { container, unmount } = mountInBrowser(
|
|
115
|
+
<Element
|
|
116
|
+
tag="div"
|
|
117
|
+
data-id="root"
|
|
118
|
+
content={() => (show() ? <span data-id="present">shown</span> : null)}
|
|
119
|
+
/>,
|
|
120
|
+
)
|
|
121
|
+
expect(container.querySelector('[data-id="present"]')).toBeNull()
|
|
122
|
+
show.set(true)
|
|
123
|
+
await flush()
|
|
124
|
+
expect(container.querySelector('[data-id="present"]')?.textContent).toBe('shown')
|
|
125
|
+
show.set(false)
|
|
126
|
+
await flush()
|
|
127
|
+
expect(container.querySelector('[data-id="present"]')).toBeNull()
|
|
128
|
+
unmount()
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
it('children prop (priority over content) — function form is reactive in compound layout', async () => {
|
|
132
|
+
// children takes priority over content per getChildren's `??` chain.
|
|
133
|
+
// Compound layout (when beforeContent OR afterContent exists) routes
|
|
134
|
+
// through a different render path than the simple-element fast path —
|
|
135
|
+
// both must handle function children reactively.
|
|
136
|
+
const text = signal('first')
|
|
137
|
+
const { container, unmount } = mountInBrowser(
|
|
138
|
+
<Element
|
|
139
|
+
tag="div"
|
|
140
|
+
beforeContent={<span data-id="b">b</span>}
|
|
141
|
+
afterContent={<span data-id="a">a</span>}
|
|
142
|
+
>
|
|
143
|
+
{() => <span data-id="kid">{text()}</span>}
|
|
144
|
+
</Element>,
|
|
145
|
+
)
|
|
146
|
+
expect(container.querySelector('[data-id="kid"]')?.textContent).toBe('first')
|
|
147
|
+
text.set('second')
|
|
148
|
+
await flush()
|
|
149
|
+
expect(container.querySelector('[data-id="kid"]')?.textContent).toBe('second')
|
|
150
|
+
unmount()
|
|
151
|
+
})
|
|
152
|
+
})
|
|
@@ -137,7 +137,13 @@ describe('Element', () => {
|
|
|
137
137
|
// Simple element fast path — passes children as a single value, not a
|
|
138
138
|
// 3-slot array wrapping falsy beforeContent/afterContent. This avoids
|
|
139
139
|
// 2 extra mountChild calls per Element in the common case.
|
|
140
|
-
|
|
140
|
+
//
|
|
141
|
+
// Children are wrapped in a reactive accessor (`() => resolveSlot(...)`)
|
|
142
|
+
// so function-valued slot props (e.g. `content={() => <X />}`) stay
|
|
143
|
+
// reactive — see `Element-slot-reactivity.browser.test.tsx`. The
|
|
144
|
+
// accessor's RESOLVED value is the string `'hello'`.
|
|
145
|
+
expect(typeof result.props.children).toBe('function')
|
|
146
|
+
expect((result.props.children as () => unknown)()).toBe('hello')
|
|
141
147
|
})
|
|
142
148
|
|
|
143
149
|
it('passes block prop to Wrapper', () => {
|
|
@@ -606,8 +612,11 @@ describe('Element', () => {
|
|
|
606
612
|
it('prefers children over content', () => {
|
|
607
613
|
const result = asVNode(Element({ children: 'child', content: 'alt' }))
|
|
608
614
|
// Simple-element fast path returns children directly. The fallback
|
|
609
|
-
// chain (children → content → label) is exercised inside getChildren()
|
|
610
|
-
|
|
615
|
+
// chain (children → content → label) is exercised inside getChildren(),
|
|
616
|
+
// which runs INSIDE the reactive accessor wrap — so invoking the
|
|
617
|
+
// accessor reveals the resolved value.
|
|
618
|
+
expect(typeof result.props.children).toBe('function')
|
|
619
|
+
expect((result.props.children as () => unknown)()).toBe('child')
|
|
611
620
|
})
|
|
612
621
|
|
|
613
622
|
it('falls back to content when no children', () => {
|
|
@@ -4,14 +4,28 @@
|
|
|
4
4
|
* gap, and equalCols styling props to the underlying styled component.
|
|
5
5
|
* Adds a `data-pyr-element` attribute in development for debugging.
|
|
6
6
|
*
|
|
7
|
-
* Children are rendered via core `render()
|
|
7
|
+
* Children are rendered via core `render()`, with function-valued
|
|
8
|
+
* children unwrapped inside a reactive accessor so the compound-layout
|
|
9
|
+
* paths in `Element` keep `content={() => <X />}` reactivity intact
|
|
10
|
+
* (mirrors the `resolveSlot` helper in `Element/component.tsx`).
|
|
8
11
|
*/
|
|
9
12
|
import { splitProps } from '@pyreon/core'
|
|
13
|
+
import type { VNodeChildAtom } from '@pyreon/core'
|
|
10
14
|
import { render } from '@pyreon/ui-core'
|
|
11
15
|
import { IS_DEVELOPMENT } from '../../utils'
|
|
12
16
|
import Styled from './styled'
|
|
13
17
|
import type { Props } from './types'
|
|
14
18
|
|
|
19
|
+
// Return type is the RESOLVED atom — see the matching helper in
|
|
20
|
+
// Element/component.tsx for the rationale (keeps `() => resolveSlot(...)`
|
|
21
|
+
// a valid VNodeChildAccessor at the JSX child position).
|
|
22
|
+
const resolveSlot = (value: unknown): VNodeChildAtom | VNodeChildAtom[] => {
|
|
23
|
+
if (typeof value === 'function') {
|
|
24
|
+
return (value as () => VNodeChildAtom | VNodeChildAtom[])()
|
|
25
|
+
}
|
|
26
|
+
return render(value as Parameters<typeof render>[0]) as VNodeChildAtom | VNodeChildAtom[]
|
|
27
|
+
}
|
|
28
|
+
|
|
15
29
|
const Component = (props: Partial<Props>) => {
|
|
16
30
|
const [own, rest] = splitProps(props, [
|
|
17
31
|
'contentType',
|
|
@@ -45,7 +59,7 @@ const Component = (props: Partial<Props>) => {
|
|
|
45
59
|
|
|
46
60
|
return (
|
|
47
61
|
<Styled as={own.tag} $contentType={own.contentType} $element={stylingProps} {...debugProps} {...rest}>
|
|
48
|
-
{
|
|
62
|
+
{() => resolveSlot(own.children)}
|
|
49
63
|
</Styled>
|
|
50
64
|
)
|
|
51
65
|
}
|