@symbiote-native/solid 1.0.0 → 2.0.0
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 +9 -10
- package/babel-preset.cjs +0 -5
- package/build/components/activity-indicator-props.d.ts +5 -0
- package/build/components/activity-indicator-props.js +1 -0
- package/build/components/button-props.d.ts +5 -0
- package/build/components/button-props.js +1 -0
- package/build/components/flat-list/index.jsx +3 -3
- package/build/components/{image-background.d.ts → image-background-props.d.ts} +2 -3
- package/build/components/image-background-props.js +1 -0
- package/build/components/image-props.d.ts +5 -0
- package/build/components/image-props.js +6 -0
- package/build/components/{input-accessory-view.d.ts → input-accessory-view-props.d.ts} +1 -2
- package/build/components/input-accessory-view-props.js +5 -0
- package/build/components/keyboard-avoiding-view.jsx +9 -4
- package/build/components/modal.jsx +6 -6
- package/build/components/{pressable.d.ts → pressable-props.d.ts} +8 -7
- package/build/components/pressable-props.js +13 -0
- package/build/components/{refresh-control.d.ts → refresh-control-props.d.ts} +3 -3
- package/build/components/refresh-control-props.js +7 -0
- package/build/components/safe-area-view-props.d.ts +10 -0
- package/build/components/safe-area-view-props.js +5 -0
- package/build/components/{scroll-view/shared.d.ts → scroll-view-props.d.ts} +7 -13
- package/build/components/scroll-view-props.js +29 -0
- package/build/components/switch-props.d.ts +6 -0
- package/build/components/switch-props.js +5 -0
- package/build/components/text-input-props.d.ts +8 -0
- package/build/components/text-input-props.js +1 -0
- package/build/components/{text.d.ts → text-props.d.ts} +3 -3
- package/build/components/text-props.js +11 -0
- package/build/components/touchable-native-feedback/touchable-native-feedback-props.d.ts +8 -0
- package/build/components/touchable-native-feedback/touchable-native-feedback-props.js +1 -0
- package/build/components/touchable-props.d.ts +18 -0
- package/build/components/touchable-props.js +1 -0
- package/build/components/touchable-without-feedback/touchable-without-feedback-props.d.ts +6 -0
- package/build/components/touchable-without-feedback/touchable-without-feedback-props.js +1 -0
- package/build/components/{view.d.ts → view-props.d.ts} +2 -3
- package/build/components/view-props.js +11 -0
- package/build/components/virtualized-list/index.android.js +5 -4
- package/build/components/virtualized-list/index.ios.js +6 -6
- package/build/components/virtualized-list/shared.d.ts +1 -3
- package/build/components/virtualized-list/shared.jsx +87 -98
- package/build/components/virtualized-list/sticky-header.d.ts +6 -0
- package/build/components/{scroll-view → virtualized-list}/sticky-header.jsx +28 -36
- package/build/components.d.ts +20 -30
- package/build/components.js +6 -33
- package/build/create-portal/index.jsx +1 -1
- package/build/descriptor-to-solid.js +1 -1
- package/build/index.d.ts +1 -0
- package/build/index.js +11 -3
- package/build/jsx-runtime.d.ts +39 -2
- package/build/modules/animated/index.d.ts +0 -4
- package/build/modules/animated/index.js +16 -22
- package/build/modules/image.d.ts +3 -0
- package/build/modules/image.js +12 -0
- package/build/register.d.ts +1 -1
- package/build/register.js +12 -28
- package/build/renderer.js +79 -16
- package/build/utils/stable-keys.d.ts +1 -1
- package/build/utils/stable-keys.js +6 -5
- package/metro-css-parser.cjs +2 -1
- package/package.json +5 -10
- package/babel-lower-host-primitives.cjs +0 -323
- package/build/components/activity-indicator/index.android.d.ts +0 -2
- package/build/components/activity-indicator/index.android.js +0 -11
- package/build/components/activity-indicator/index.d.ts +0 -1
- package/build/components/activity-indicator/index.ios.d.ts +0 -2
- package/build/components/activity-indicator/index.ios.js +0 -10
- package/build/components/activity-indicator/index.js +0 -4
- package/build/components/activity-indicator/shared.d.ts +0 -8
- package/build/components/activity-indicator/shared.js +0 -51
- package/build/components/button.d.ts +0 -7
- package/build/components/button.jsx +0 -45
- package/build/components/image-background.jsx +0 -74
- package/build/components/image.d.ts +0 -9
- package/build/components/image.js +0 -75
- package/build/components/input-accessory-view.jsx +0 -39
- package/build/components/pressable.jsx +0 -211
- package/build/components/refresh-control.jsx +0 -36
- package/build/components/safe-area-view.d.ts +0 -11
- package/build/components/safe-area-view.jsx +0 -24
- package/build/components/scroll-view/index.android.d.ts +0 -2
- package/build/components/scroll-view/index.android.js +0 -11
- package/build/components/scroll-view/index.d.ts +0 -2
- package/build/components/scroll-view/index.ios.d.ts +0 -2
- package/build/components/scroll-view/index.ios.js +0 -8
- package/build/components/scroll-view/index.js +0 -9
- package/build/components/scroll-view/shared.jsx +0 -334
- package/build/components/scroll-view/sticky-header.d.ts +0 -18
- package/build/components/switch/index.android.d.ts +0 -2
- package/build/components/switch/index.android.js +0 -12
- package/build/components/switch/index.d.ts +0 -1
- package/build/components/switch/index.ios.d.ts +0 -2
- package/build/components/switch/index.ios.js +0 -11
- package/build/components/switch/index.js +0 -4
- package/build/components/switch/shared.d.ts +0 -11
- package/build/components/switch/shared.js +0 -87
- package/build/components/text-input.d.ts +0 -10
- package/build/components/text-input.js +0 -290
- package/build/components/text.jsx +0 -27
- package/build/components/touchable/index.d.ts +0 -22
- package/build/components/touchable/index.jsx +0 -247
- package/build/components/touchable-native-feedback/index.d.ts +0 -16
- package/build/components/touchable-native-feedback/index.jsx +0 -68
- package/build/components/view.jsx +0 -49
- package/build/state-style.d.ts +0 -1
- package/build/state-style.js +0 -13
|
@@ -1,334 +0,0 @@
|
|
|
1
|
-
// ScrollView — the Solid lifecycle half. The Fabric tree is nested: a scroll view wraps a content
|
|
2
|
-
// view that holds the children (RN's own ScrollView.js shape). Everything platform- and
|
|
3
|
-
// framework-invariant already exists in @symbiote-native/components and is CALLED, never re-derived:
|
|
4
|
-
// `selectScrollIntrinsics` (per-axis tags + base styles), `resolveDecelerationRate`,
|
|
5
|
-
// `resolveScrollForwarding` (which onScroll path, the 1/16 throttle defaults, the inverted viewport
|
|
6
|
-
// capture, collapsableChildren), `didContentSizeChange`, `buildScrollViewHandle`,
|
|
7
|
-
// `splitLayoutProps`, `attachStickyScroll`, `forwardScrollEvent`, `resolveAccessibilityProps`.
|
|
8
|
-
// Solid supplies signals, effects and the element assembly.
|
|
9
|
-
//
|
|
10
|
-
// THREE THINGS ARE SOLID-SPECIFIC AND NONE OF THEM IS COSMETIC.
|
|
11
|
-
//
|
|
12
|
-
// 1. There is no reconciler between what this file returns and the host nodes. React diffs fibers,
|
|
13
|
-
// Vue diffs vnodes, Svelte patches in place; Solid's `insert` REPLACES. So the tree is built ONCE
|
|
14
|
-
// and every prop rides a `spread` render effect on the SAME node — the identity
|
|
15
|
-
// dispatchViewCommand, the commit mirror and native-owned scroll offset all key on. The only
|
|
16
|
-
// rebuild is when the host TAG itself has to change (see `treeShape` below).
|
|
17
|
-
// 2. A JSX element prop is a GETTER that CREATES the element on read, so `refreshControl` is read
|
|
18
|
-
// exactly once. A second read would build a second refresh-control node.
|
|
19
|
-
// 3. The commit is microtask-coalesced (`requestCommit`), so at mount time the scroll node has no
|
|
20
|
-
// Fabric tag. Anything native wired then — here, the sticky scroll attach — goes through
|
|
21
|
-
// `whenCommitted` or it silently no-ops with no retry. The imperative handle is safe without it
|
|
22
|
-
// for a different reason: `buildScrollViewHandle` takes a LAZY node getter and every command
|
|
23
|
-
// re-reads it, so a handle handed out at setup keeps working once the node commits.
|
|
24
|
-
//
|
|
25
|
-
// NOTHING here destructures `props`. Solid props are getters and a component body runs ONCE;
|
|
26
|
-
// `splitProps` is the idiomatic split that keeps the rest reactive, and every read below sits inside
|
|
27
|
-
// an accessor, a memo, or an event handler.
|
|
28
|
-
import { children, createEffect, createMemo, createRenderEffect, createSignal, on, onCleanup, splitProps, untrack, } from 'solid-js';
|
|
29
|
-
import { attachStickyScroll, buildScrollViewHandle, didContentSizeChange, forwardScrollEvent, nextStickyHeaderY, readLayoutDimension, resolveAccessibilityProps, resolveDecelerationRate, resolveScrollForwarding, selectScrollIntrinsics, splitLayoutProps, } from '@symbiote-native/components';
|
|
30
|
-
import { AnimatedValue, dlog, event as animatedEvent, isNativeAnimatedAvailable, isSymbioteNode, resolveClassName, whenCommitted, } from '@symbiote-native/engine';
|
|
31
|
-
import { createElement, insert, insertNode, setProp, spread, } from '../../renderer.js';
|
|
32
|
-
import { withStableKeys } from '../../utils/stable-keys.js';
|
|
33
|
-
import { wrapStickyHeaders, } from './sticky-header.jsx';
|
|
34
|
-
// Consumed by the lifecycle itself; everything else forwards onto the scroll-view host node.
|
|
35
|
-
// onContentSizeChange is synthesized from the content view's onLayout and must never reach Fabric —
|
|
36
|
-
// a function prop crashes Android's folly::dynamic serializer. Same for the sticky props, which
|
|
37
|
-
// native ignores, and `refreshControl`, which is an element.
|
|
38
|
-
const HANDLED_PROPS = [
|
|
39
|
-
'style',
|
|
40
|
-
'class',
|
|
41
|
-
'contentContainerStyle',
|
|
42
|
-
'horizontal',
|
|
43
|
-
'decelerationRate',
|
|
44
|
-
'refreshControl',
|
|
45
|
-
'stickyHeaderIndices',
|
|
46
|
-
'invertStickyHeaders',
|
|
47
|
-
'StickyHeaderComponent',
|
|
48
|
-
'onContentSizeChange',
|
|
49
|
-
'onLayout',
|
|
50
|
-
'onScroll',
|
|
51
|
-
'scrollEventThrottle',
|
|
52
|
-
'children',
|
|
53
|
-
'ref',
|
|
54
|
-
];
|
|
55
|
-
// `maintainVisibleContentPosition`, `snapToAlignment` and `nestedScrollEnabled` are deliberately NOT
|
|
56
|
-
// in that list: the lifecycle READS them (for resolveScrollForwarding and RN's nested-scroll
|
|
57
|
-
// default) while native also needs them, so they forward through `rest` untouched.
|
|
58
|
-
function hostElement(tag) {
|
|
59
|
-
const node = createElement(tag);
|
|
60
|
-
// Narrowing, not defensive: the renderer types createElement over its IHostNode union (which
|
|
61
|
-
// includes the surface), while everything below needs a real host node.
|
|
62
|
-
if (!isSymbioteNode(node))
|
|
63
|
-
throw new Error(`ScrollView: ${tag} did not create a host node`);
|
|
64
|
-
return node;
|
|
65
|
-
}
|
|
66
|
-
export function createScrollView(platform) {
|
|
67
|
-
return function ScrollView(props) {
|
|
68
|
-
const [local, rest] = splitProps(props, HANDLED_PROPS);
|
|
69
|
-
// The scroll-view host node, held by IDENTITY in a plain variable. A store or any proxy wrapper
|
|
70
|
-
// would become a different key than the one the engine's commit mirror holds, and every
|
|
71
|
-
// imperative command would silently no-op (symbiote-engine-core §3).
|
|
72
|
-
let scrollNode = null;
|
|
73
|
-
// A LAZY getter, not the node captured once: it is null until buildTree runs, and null again
|
|
74
|
-
// under it if the tree is ever rebuilt.
|
|
75
|
-
const handle = buildScrollViewHandle(() => scrollNode);
|
|
76
|
-
// Read ONCE and untracked — see the module header, point 2. Nothing reactive is lost: a
|
|
77
|
-
// RefreshControl's live state is `refreshing`, which is a prop of the element itself.
|
|
78
|
-
const refreshControlElement = untrack(() => local.refreshControl);
|
|
79
|
-
const refreshControlNode = isSymbioteNode(refreshControlElement)
|
|
80
|
-
? refreshControlElement
|
|
81
|
-
: undefined;
|
|
82
|
-
const isWrappingRefreshControl = platform.refreshControlMode === 'wrap' &&
|
|
83
|
-
refreshControlNode !== undefined;
|
|
84
|
-
// Drives every sticky header's translateY (RN's _scrollAnimatedValue). Allocated
|
|
85
|
-
// unconditionally, exactly like React's unconditional hook, and held by identity.
|
|
86
|
-
const scrollAnimatedValue = new AnimatedValue(0);
|
|
87
|
-
// Inverted sticky headers stick to the BOTTOM, so they need the viewport height (RN
|
|
88
|
-
// _handleLayout). Captured off the scroll view's own onLayout.
|
|
89
|
-
const [viewportHeight, setViewportHeight] = createSignal(undefined);
|
|
90
|
-
// Sticky-header cross-talk (RN's _headerLayoutYs): a child-index -> measured-y map so each
|
|
91
|
-
// header learns where the NEXT one starts — its push-off collision point. Mutated imperatively
|
|
92
|
-
// from each header's onLayout; the version bump is what lets the PREVIOUS header re-read it.
|
|
93
|
-
const headerLayoutYs = new Map();
|
|
94
|
-
const [headerLayoutVersion, setHeaderLayoutVersion] = createSignal(0);
|
|
95
|
-
// RN fires the content onLayout on every layout pass; only a real size change emits.
|
|
96
|
-
let lastContentSize = null;
|
|
97
|
-
const isHorizontal = () => local.horizontal === true;
|
|
98
|
-
const stickyHeaderIndices = createMemo(() => local.stickyHeaderIndices);
|
|
99
|
-
const hasStickyHeaders = () => {
|
|
100
|
-
const indices = stickyHeaderIndices();
|
|
101
|
-
return indices !== undefined && indices.length > 0;
|
|
102
|
-
};
|
|
103
|
-
const nativeStickyAvailable = () => hasStickyHeaders() && isNativeAnimatedAvailable();
|
|
104
|
-
// A class-name string resolves through the shared registry before it reaches the intrinsic
|
|
105
|
-
// selector, which only understands style objects/arrays.
|
|
106
|
-
const resolvedContentContainerStyle = () => {
|
|
107
|
-
const style = local.contentContainerStyle;
|
|
108
|
-
return typeof style === 'string' ? resolveClassName(style) : style;
|
|
109
|
-
};
|
|
110
|
-
const intrinsics = createMemo(() => selectScrollIntrinsics(isHorizontal(), resolvedContentContainerStyle()));
|
|
111
|
-
// `class` is normally forwarded raw and resolved per-node at commit time, but the Android
|
|
112
|
-
// RefreshControl wrap has to splitLayoutProps() BEFORE that, so a class-only layout prop
|
|
113
|
-
// (flex, height, gap, …) would otherwise never reach the wrapper and it would collapse to
|
|
114
|
-
// nothing.
|
|
115
|
-
const layoutSplitStyle = () => [
|
|
116
|
-
resolveClassName(local.class),
|
|
117
|
-
local.style,
|
|
118
|
-
];
|
|
119
|
-
const splitStyles = createMemo(() => splitLayoutProps(layoutSplitStyle()));
|
|
120
|
-
const forwarding = createMemo(() => resolveScrollForwarding({
|
|
121
|
-
hasStickyHeaders: hasStickyHeaders(),
|
|
122
|
-
nativeStickyAvailable: nativeStickyAvailable(),
|
|
123
|
-
invertStickyHeaders: local.invertStickyHeaders,
|
|
124
|
-
scrollEventThrottle: local.scrollEventThrottle,
|
|
125
|
-
maintainVisibleContentPosition: props.maintainVisibleContentPosition,
|
|
126
|
-
snapToAlignment: props.snapToAlignment,
|
|
127
|
-
}));
|
|
128
|
-
// onScroll: the JS-fallback path wraps the user's handler in Animated.event so the offset drives
|
|
129
|
-
// the AnimatedValue each frame (RN's _scrollAnimatedValueAttachment); the native and plain paths
|
|
130
|
-
// forward it as-is, because the native driver attaches the value on the UI thread instead.
|
|
131
|
-
const scrollHandler = createMemo(() => {
|
|
132
|
-
const userOnScroll = local.onScroll;
|
|
133
|
-
if (forwarding().mode !== 'sticky-js')
|
|
134
|
-
return userOnScroll;
|
|
135
|
-
return animatedEvent([{ nativeEvent: { contentOffset: { y: scrollAnimatedValue } } }], userOnScroll === undefined
|
|
136
|
-
? undefined
|
|
137
|
-
: {
|
|
138
|
-
listener: (...args) => forwardScrollEvent(userOnScroll, args),
|
|
139
|
-
});
|
|
140
|
-
});
|
|
141
|
-
// onLayout on the scroll-view node: capture the viewport height for inverted sticky headers
|
|
142
|
-
// (RN _handleLayout), then call the user's handler. Passed through unchanged otherwise, so a
|
|
143
|
-
// ScrollView with neither never raises the onLayout flag prop and native never measures it.
|
|
144
|
-
const scrollLayoutHandler = createMemo(() => {
|
|
145
|
-
const userOnLayout = local.onLayout;
|
|
146
|
-
if (!forwarding().capturesViewportHeight)
|
|
147
|
-
return userOnLayout;
|
|
148
|
-
return (event) => {
|
|
149
|
-
const height = readLayoutDimension(event, 'height');
|
|
150
|
-
if (height !== undefined)
|
|
151
|
-
setViewportHeight(height);
|
|
152
|
-
userOnLayout?.(event);
|
|
153
|
-
};
|
|
154
|
-
});
|
|
155
|
-
const handleContentLayout = (event) => {
|
|
156
|
-
const width = readLayoutDimension(event, 'width');
|
|
157
|
-
const height = readLayoutDimension(event, 'height');
|
|
158
|
-
if (width === undefined || height === undefined)
|
|
159
|
-
return;
|
|
160
|
-
if (!didContentSizeChange(lastContentSize, { width, height }))
|
|
161
|
-
return;
|
|
162
|
-
lastContentSize = { width, height };
|
|
163
|
-
dlog(`Solid ScrollView contentSizeChange ${width}x${height}`);
|
|
164
|
-
local.onContentSizeChange?.(width, height);
|
|
165
|
-
};
|
|
166
|
-
const onHeaderLayoutY = (index, y) => {
|
|
167
|
-
if (headerLayoutYs.get(index) === y)
|
|
168
|
-
return;
|
|
169
|
-
headerLayoutYs.set(index, y);
|
|
170
|
-
dlog(`Solid ScrollView sticky-header layoutY index=${index} y=${y}`);
|
|
171
|
-
setHeaderLayoutVersion(tick => tick + 1);
|
|
172
|
-
};
|
|
173
|
-
const readNextHeaderLayoutY = (indexOfIndex) => {
|
|
174
|
-
// Read the bump FIRST so this stays a dependency of whichever header calls it — that is what
|
|
175
|
-
// turns "a later header measured" into "the previous header rebuilds its collision range".
|
|
176
|
-
headerLayoutVersion();
|
|
177
|
-
const indices = stickyHeaderIndices();
|
|
178
|
-
if (indices === undefined)
|
|
179
|
-
return undefined;
|
|
180
|
-
return nextStickyHeaderY(indices, indexOfIndex, headerLayoutYs);
|
|
181
|
-
};
|
|
182
|
-
// withStableKeys on BOTH bags: resolveAccessibilityProps has two branches with different key
|
|
183
|
-
// sets, and several keys below are conditional. Solid's `spread` walks only the CURRENT keys and
|
|
184
|
-
// has no removal pass, so a vanished key would keep its last value on the native view forever
|
|
185
|
-
// (.claude/rules/solid-descriptor-bridge.md §1).
|
|
186
|
-
const outerBag = withStableKeys(() => {
|
|
187
|
-
const bag = {
|
|
188
|
-
...resolveAccessibilityProps(rest),
|
|
189
|
-
};
|
|
190
|
-
// RN defaults nested scrolling ON (ScrollView.js `nestedScrollEnabled ?? true`): Android needs
|
|
191
|
-
// it for a scrollable nested inside another to scroll independently; iOS handles nesting
|
|
192
|
-
// natively. The Android RefreshControl wrap forces it, so the inner scroll view takes the
|
|
193
|
-
// gesture before the refresh parent.
|
|
194
|
-
bag.nestedScrollEnabled = isWrappingRefreshControl
|
|
195
|
-
? true
|
|
196
|
-
: (props.nestedScrollEnabled ?? true);
|
|
197
|
-
// Load-bearing on iOS (it flips RCTScrollView's axis), ignored by Android's dedicated
|
|
198
|
-
// horizontal manager — so forward it whenever it is defined.
|
|
199
|
-
if (local.horizontal !== undefined)
|
|
200
|
-
bag.horizontal = local.horizontal;
|
|
201
|
-
const rate = local.decelerationRate;
|
|
202
|
-
if (rate !== undefined)
|
|
203
|
-
bag.decelerationRate = resolveDecelerationRate(rate);
|
|
204
|
-
// Base style UNDER the user style, so an explicit height / flexDirection still wins. Under the
|
|
205
|
-
// Android wrap only the VISUAL half stays here; the LAYOUT half moved to the wrapper.
|
|
206
|
-
bag.style = [
|
|
207
|
-
intrinsics().scrollViewBaseStyle,
|
|
208
|
-
isWrappingRefreshControl ? splitStyles().inner : local.style,
|
|
209
|
-
];
|
|
210
|
-
// Stripped under the wrap: layoutSplitStyle already folded the resolved class into
|
|
211
|
-
// outer/inner, so forwarding it raw too would re-apply its LAYOUT half a second time.
|
|
212
|
-
if (!isWrappingRefreshControl)
|
|
213
|
-
bag.class = local.class;
|
|
214
|
-
bag.onScroll = scrollHandler();
|
|
215
|
-
bag.onLayout = scrollLayoutHandler();
|
|
216
|
-
bag.scrollEventThrottle = forwarding().scrollEventThrottle;
|
|
217
|
-
return bag;
|
|
218
|
-
});
|
|
219
|
-
// `collapsable: false` is load-bearing on Android: the content container is a layout-only view
|
|
220
|
-
// that Fabric would otherwise flatten away, hoisting the cells up as DIRECT children of the
|
|
221
|
-
// scroll view — which hosts exactly one ("addViewAt" crash). iOS never flattens.
|
|
222
|
-
const contentBag = withStableKeys(() => {
|
|
223
|
-
const bag = {
|
|
224
|
-
style: intrinsics().contentStyle,
|
|
225
|
-
collapsable: false,
|
|
226
|
-
};
|
|
227
|
-
// maintainVisibleContentPosition (and Android snapToAlignment) anchor against the metrics of
|
|
228
|
-
// MOUNTED cell views, so RN keeps them un-flattened too (ScrollView.js preserveChildren).
|
|
229
|
-
if (forwarding().collapsableChildren)
|
|
230
|
-
bag.collapsableChildren = false;
|
|
231
|
-
if (local.onContentSizeChange !== undefined)
|
|
232
|
-
bag.onLayout = handleContentLayout;
|
|
233
|
-
return bag;
|
|
234
|
-
});
|
|
235
|
-
function mountChildren(content) {
|
|
236
|
-
if (!hasStickyHeaders()) {
|
|
237
|
-
// The plain path hands `insert` the raw children accessor, so its own nested-effect
|
|
238
|
-
// machinery keeps updates fine-grained — a <For> inside re-runs only its own insert.
|
|
239
|
-
insert(content, () => local.children);
|
|
240
|
-
return;
|
|
241
|
-
}
|
|
242
|
-
// Sticky wrapping needs an INDEXABLE list, which is what solid's `children()` helper resolves
|
|
243
|
-
// to. It costs a coarser update (any child change re-runs the whole wrap), which is why it is
|
|
244
|
-
// created ONLY here and not for every ScrollView — and it must not coexist with the accessor
|
|
245
|
-
// above, since reading `local.children` twice would build the subtree twice.
|
|
246
|
-
const resolved = children(() => local.children);
|
|
247
|
-
insert(content, () => wrapStickyHeaders(resolved.toArray(), {
|
|
248
|
-
stickyHeaderIndices: stickyHeaderIndices() ?? [],
|
|
249
|
-
scrollAnimatedValue,
|
|
250
|
-
readInverted: () => local.invertStickyHeaders,
|
|
251
|
-
readScrollViewHeight: viewportHeight,
|
|
252
|
-
readNextHeaderLayoutY,
|
|
253
|
-
StickyHeaderComponent: local.StickyHeaderComponent,
|
|
254
|
-
onHeaderLayoutY,
|
|
255
|
-
}));
|
|
256
|
-
}
|
|
257
|
-
function buildTree() {
|
|
258
|
-
const { scrollViewIntrinsic, contentIntrinsic } = intrinsics();
|
|
259
|
-
dlog(`Solid ScrollView -> ${scrollViewIntrinsic} (horizontal=${String(isHorizontal())} sticky=${String(hasStickyHeaders())})`);
|
|
260
|
-
const content = hostElement(contentIntrinsic);
|
|
261
|
-
spread(content, contentBag, true);
|
|
262
|
-
mountChildren(content);
|
|
263
|
-
const scroll = hostElement(scrollViewIntrinsic);
|
|
264
|
-
scrollNode = scroll;
|
|
265
|
-
spread(scroll, outerBag, true);
|
|
266
|
-
// iOS: the RefreshControl is a childless SIBLING placed BEFORE the content container (RN
|
|
267
|
-
// ScrollView.js: {refreshControl}{contentContainer}).
|
|
268
|
-
if (refreshControlNode !== undefined && !isWrappingRefreshControl) {
|
|
269
|
-
insertNode(scroll, refreshControlNode);
|
|
270
|
-
}
|
|
271
|
-
insertNode(scroll, content);
|
|
272
|
-
if (refreshControlNode === undefined || !isWrappingRefreshControl)
|
|
273
|
-
return scroll;
|
|
274
|
-
// Android: the RefreshControl WRAPS the scroll view. React does this with cloneElement and Vue
|
|
275
|
-
// by re-invoking the VNode's type; Solid has neither, because the element arrived already
|
|
276
|
-
// built — so the scroll view is nested into the existing node and the outer style is written
|
|
277
|
-
// onto it. A render effect, not a one-shot set, so a later style/class change still moves.
|
|
278
|
-
//
|
|
279
|
-
// This OVERRIDES a `style` the app put on the RefreshControl itself, exactly as React's
|
|
280
|
-
// cloneElement({style}) does. Unlike React's, the two writers are independent render effects,
|
|
281
|
-
// so an app that styles BOTH boxes on Android has no defined winner — pass the ScrollView's
|
|
282
|
-
// layout through `style`/`class` there and leave the control's own `style` alone.
|
|
283
|
-
createRenderEffect(() => {
|
|
284
|
-
setProp(refreshControlNode, 'style', splitStyles().outer);
|
|
285
|
-
});
|
|
286
|
-
insertNode(refreshControlNode, scroll);
|
|
287
|
-
return refreshControlNode;
|
|
288
|
-
}
|
|
289
|
-
// The scroll AXIS picks a different host TAG (horizontal is a separate ViewManager on Android)
|
|
290
|
-
// and sticky headers pick a different children pipeline. Solid cannot swap a tag under a live
|
|
291
|
-
// node, so either flip REBUILDS — which is exactly what React does when an element type changes.
|
|
292
|
-
// `on()` runs buildTree untracked, so every other read inside it (style, class, handlers,
|
|
293
|
-
// contentContainerStyle) re-props the SAME nodes through `spread` instead of rebuilding them.
|
|
294
|
-
const treeShape = createMemo(() => `${String(isHorizontal())}:${String(hasStickyHeaders())}`);
|
|
295
|
-
const tree = createMemo(on(treeShape, () => buildTree()));
|
|
296
|
-
// Solid's `ref` is a COMPILE-TIME construct: by the time a component body reads `props.ref`, a
|
|
297
|
-
// `ref={scroller}` call site has already been rewritten into a callback. The full rationale (and
|
|
298
|
-
// why the declared type still has to be the `Ref<T>` union) is in utils/host-ref.ts, whose
|
|
299
|
-
// helper is typed for a host node — this hands back the imperative handle instead.
|
|
300
|
-
//
|
|
301
|
-
// Called AFTER `tree` has built at least once, not right after `handle` is constructed.
|
|
302
|
-
// `createAnimatedComponent`'s captureRef (resolveHostNode) reads handle.getScrollNode()
|
|
303
|
-
// EAGERLY, once, at ref-call time - it never re-reads the lazy getter later. React's
|
|
304
|
-
// useImperativeHandle and Vue's expose() fire only after the child's own mount work, so they
|
|
305
|
-
// never saw a null handle; Solid's `ref` is called by the component itself and has to be
|
|
306
|
-
// sequenced by hand. Calling it before `buildTree()` gave captureRef a permanently-null node,
|
|
307
|
-
// so Animated.ScrollView's onScroll never native-attached - a scroll-linked header fade froze
|
|
308
|
-
// at its initial value on this adapter only.
|
|
309
|
-
if (typeof local.ref === 'function')
|
|
310
|
-
local.ref(handle);
|
|
311
|
-
// Drive the sticky scroll value on the native UI thread (RN's attachNativeEvent /
|
|
312
|
-
// _updateAnimatedNodeAttachment), so the header interpolations ride scroll natively with no JS
|
|
313
|
-
// jitter. `tree()` is read for its dependency: a rebuild means a NEW scroll node to attach to.
|
|
314
|
-
createEffect(() => {
|
|
315
|
-
tree();
|
|
316
|
-
if (!nativeStickyAvailable())
|
|
317
|
-
return;
|
|
318
|
-
const node = scrollNode;
|
|
319
|
-
if (node === null)
|
|
320
|
-
return;
|
|
321
|
-
let detach;
|
|
322
|
-
// The engine commits on a microtask, so this node has no Fabric tag yet on the first run and
|
|
323
|
-
// the attach would no-op with no retry. whenCommitted is that retry.
|
|
324
|
-
const cancel = whenCommitted(node, () => {
|
|
325
|
-
detach = attachStickyScroll(node, scrollAnimatedValue);
|
|
326
|
-
});
|
|
327
|
-
onCleanup(() => {
|
|
328
|
-
cancel();
|
|
329
|
-
detach?.();
|
|
330
|
-
});
|
|
331
|
-
});
|
|
332
|
-
return tree;
|
|
333
|
-
};
|
|
334
|
-
}
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
import type { JSX } from '../../jsx-runtime';
|
|
2
|
-
import { type IStickyHeaderProps } from '@symbiote-native/components';
|
|
3
|
-
import { type AnimatedValue } from '@symbiote-native/engine';
|
|
4
|
-
export type IStickyHeaderComponentProps = IStickyHeaderProps & {
|
|
5
|
-
children?: JSX.Element;
|
|
6
|
-
};
|
|
7
|
-
export type IStickyHeaderComponentType = (props: IStickyHeaderComponentProps) => JSX.Element;
|
|
8
|
-
export declare function ScrollViewStickyHeader(props: IStickyHeaderComponentProps): JSX.Element;
|
|
9
|
-
export interface IStickyWrapInputs {
|
|
10
|
-
stickyHeaderIndices: number[];
|
|
11
|
-
scrollAnimatedValue: AnimatedValue;
|
|
12
|
-
readInverted: () => boolean | undefined;
|
|
13
|
-
readScrollViewHeight: () => number | undefined;
|
|
14
|
-
readNextHeaderLayoutY: (indexOfIndex: number) => number | undefined;
|
|
15
|
-
StickyHeaderComponent: IStickyHeaderComponentType | undefined;
|
|
16
|
-
onHeaderLayoutY: (index: number, y: number) => void;
|
|
17
|
-
}
|
|
18
|
-
export declare function wrapStickyHeaders(children: readonly JSX.Element[], inputs: IStickyWrapInputs): JSX.Element[];
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
// Switch, Android host binding. AndroidSwitch takes trackColorForTrue / trackColorForFalse plus
|
|
2
|
-
// trackTintColor (the color for the CURRENT value, which RN computes as value ? true : false), and
|
|
3
|
-
// snaps native back via the `setNativeValue` command (Switch.js:221-225).
|
|
4
|
-
import { createSwitch } from './shared.js';
|
|
5
|
-
export const Switch = createSwitch({
|
|
6
|
-
snapBackCommand: 'setNativeValue',
|
|
7
|
-
trackColorProps: (value, trackColor) => ({
|
|
8
|
-
trackColorForFalse: trackColor?.false,
|
|
9
|
-
trackColorForTrue: trackColor?.true,
|
|
10
|
-
trackTintColor: value ? trackColor?.true : trackColor?.false,
|
|
11
|
-
}),
|
|
12
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export * from './index.ios';
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
// Switch, iOS host binding. iOS's native Switch takes onTintColor (ON-track) / tintColor
|
|
2
|
-
// (OFF-track), and snaps native back via the `setValue` command (Switch.js:221-225) — the same
|
|
3
|
-
// facts adapters/react's, adapters/vue's and adapters/svelte's iOS bindings encode.
|
|
4
|
-
import { createSwitch } from './shared.js';
|
|
5
|
-
export const Switch = createSwitch({
|
|
6
|
-
snapBackCommand: 'setValue',
|
|
7
|
-
trackColorProps: (_value, trackColor) => ({
|
|
8
|
-
onTintColor: trackColor?.true,
|
|
9
|
-
tintColor: trackColor?.false,
|
|
10
|
-
}),
|
|
11
|
-
});
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
import type { JSX } from '../../jsx-runtime';
|
|
2
|
-
import { type ISwitchPlatform, type ISwitchProps as ISwitchBaseProps } from '@symbiote-native/components';
|
|
3
|
-
import { type IClassNameValue } from '@symbiote-native/engine';
|
|
4
|
-
export type { ISwitchTrackColor } from '@symbiote-native/components';
|
|
5
|
-
export type ISwitchProps = ISwitchBaseProps & {
|
|
6
|
-
class?: IClassNameValue;
|
|
7
|
-
};
|
|
8
|
-
export type ISwitchHostPlatform = ISwitchPlatform & {
|
|
9
|
-
snapBackCommand: string;
|
|
10
|
-
};
|
|
11
|
-
export declare function createSwitch(platform: ISwitchHostPlatform): (props: ISwitchProps) => JSX.Element;
|
|
@@ -1,87 +0,0 @@
|
|
|
1
|
-
// Switch, the Solid lifecycle half. The logic (the lastNativeReport reducer, valueFromChange, the
|
|
2
|
-
// snap-back decision) lives in @symbiote-native/components/state and the render (value fold, track
|
|
3
|
-
// colors, ios_backgroundColor) in @symbiote-native/components/view — both shared verbatim with
|
|
4
|
-
// React, Vue and Svelte. Solid supplies only the reactivity: a signal holding what native last
|
|
5
|
-
// reported, an accessor per prop, and the effect that snaps native back when the parent rejects a
|
|
6
|
-
// toggle.
|
|
7
|
-
//
|
|
8
|
-
// Switch is controlled exactly like RN's: the parent's onValueChange MUST update `value` for the
|
|
9
|
-
// toggle to stick. If the handler is a no-op, native has already flipped its own grip, so JS
|
|
10
|
-
// commands the old value back down — a plain prop re-push cannot cover that case, because the prop
|
|
11
|
-
// never changed and the retained tree never diverged. The command name is platform-specific (iOS
|
|
12
|
-
// setValue / Android setNativeValue) and arrives via `platform`.
|
|
13
|
-
//
|
|
14
|
-
// NOTHING here destructures `props`. Solid props are getters; destructuring reads them once at
|
|
15
|
-
// setup and freezes the component at its mount-time values. Every read below happens inside an
|
|
16
|
-
// accessor (the descriptor memo, the snap-back effect, handleChange) so it re-runs when the caller's
|
|
17
|
-
// signal changes.
|
|
18
|
-
import { createEffect, createSignal } from 'solid-js';
|
|
19
|
-
import { createInitialSwitchState, renderSwitch, resolveAccessibilityProps, shouldSnapBack, switchReducer, valueFromChange, } from '@symbiote-native/components';
|
|
20
|
-
import { dispatchViewCommand, dlog, } from '@symbiote-native/engine';
|
|
21
|
-
import { descriptorToSolid } from '../../descriptor-to-solid.js';
|
|
22
|
-
// Read by the component itself; everything else forwards onto the host node. onValueChange is pure
|
|
23
|
-
// JS and must never reach Fabric — a function prop crashes Android's folly::dynamic serializer.
|
|
24
|
-
const HANDLED_PROPS = [
|
|
25
|
-
'value',
|
|
26
|
-
'onValueChange',
|
|
27
|
-
'disabled',
|
|
28
|
-
'trackColor',
|
|
29
|
-
'thumbColor',
|
|
30
|
-
'ios_backgroundColor',
|
|
31
|
-
'style',
|
|
32
|
-
];
|
|
33
|
-
// Switch owns its host element rather than rendering through a symbiote View, so it folds
|
|
34
|
-
// aria/role into the canonical accessibility* props here, exactly like React's hook does.
|
|
35
|
-
function forwardProps(props) {
|
|
36
|
-
const forwarded = {
|
|
37
|
-
...resolveAccessibilityProps(props),
|
|
38
|
-
};
|
|
39
|
-
for (const key of HANDLED_PROPS)
|
|
40
|
-
delete forwarded[key];
|
|
41
|
-
return forwarded;
|
|
42
|
-
}
|
|
43
|
-
export function createSwitch(platform) {
|
|
44
|
-
return function Switch(props) {
|
|
45
|
-
const [nativeReport, setNativeReport] = createSignal(createInitialSwitchState());
|
|
46
|
-
// value is a real Fabric prop, folded to a strict boolean: RN sends `value === true`, so an
|
|
47
|
-
// absent prop reads as "off" rather than riding through as undefined.
|
|
48
|
-
const fabricValue = () => props.value === true;
|
|
49
|
-
const handleChange = (event) => {
|
|
50
|
-
const next = valueFromChange(event);
|
|
51
|
-
dlog(`Switch onChange value=${String(next)} eventCount=${String(event.nativeEvent.eventCount)}`);
|
|
52
|
-
if (next === undefined)
|
|
53
|
-
return;
|
|
54
|
-
props.onValueChange?.(next, event);
|
|
55
|
-
setNativeReport(current => switchReducer(current, { type: 'native-reported', value: next }));
|
|
56
|
-
};
|
|
57
|
-
// The node is created once and kept by identity — descriptorToSolid wires every prop through a
|
|
58
|
-
// render effect on this same node, so the imperative command below always has a live target and
|
|
59
|
-
// never chases a replaced node.
|
|
60
|
-
const node = descriptorToSolid(() => renderSwitch({
|
|
61
|
-
value: fabricValue(),
|
|
62
|
-
disabled: props.disabled,
|
|
63
|
-
trackColor: props.trackColor,
|
|
64
|
-
thumbColor: props.thumbColor,
|
|
65
|
-
ios_backgroundColor: props.ios_backgroundColor,
|
|
66
|
-
style: props.style,
|
|
67
|
-
passthrough: { ...forwardProps(props), onChange: handleChange },
|
|
68
|
-
}, platform));
|
|
69
|
-
// createEffect, not createRenderEffect: it runs after the tree is built, the same position
|
|
70
|
-
// React's useLayoutEffect and Vue's flush:'post' watch occupy. The engine commits on a
|
|
71
|
-
// microtask (renderer.ts's requestCommit), so the node has no Fabric tag yet on the first run —
|
|
72
|
-
// which is harmless and not worth a whenCommitted retry here, because shouldSnapBack is false
|
|
73
|
-
// until native has reported, and native cannot report before it exists. Every run that DOES
|
|
74
|
-
// dispatch is therefore downstream of a native event, i.e. long past the first commit.
|
|
75
|
-
createEffect(() => {
|
|
76
|
-
const value = fabricValue();
|
|
77
|
-
const reported = nativeReport();
|
|
78
|
-
if (!shouldSnapBack(reported, value)) {
|
|
79
|
-
dlog(`Switch snap-back no-op reported=${String(reported.lastNativeReport)} value=${value}`);
|
|
80
|
-
return;
|
|
81
|
-
}
|
|
82
|
-
dlog(`Switch ${platform.snapBackCommand} snap-back reported=${String(reported.lastNativeReport)} value=${value}`);
|
|
83
|
-
dispatchViewCommand(node, platform.snapBackCommand, [value]);
|
|
84
|
-
});
|
|
85
|
-
return node;
|
|
86
|
-
};
|
|
87
|
-
}
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
import type { Ref } from 'solid-js';
|
|
2
|
-
import type { JSX } from '../jsx-runtime';
|
|
3
|
-
import { type ITextInputHandle, type ITextInputProps as ITextInputBaseProps } from '@symbiote-native/components';
|
|
4
|
-
import { type IClassNameValue } from '@symbiote-native/engine';
|
|
5
|
-
export type { ITextInputHandle, ITextInputSelection, } from '@symbiote-native/components';
|
|
6
|
-
export type ITextInputProps = ITextInputBaseProps & {
|
|
7
|
-
class?: IClassNameValue;
|
|
8
|
-
ref?: Ref<ITextInputHandle>;
|
|
9
|
-
};
|
|
10
|
-
export declare function TextInput(props: ITextInputProps): JSX.Element;
|