@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,290 +0,0 @@
|
|
|
1
|
-
// TextInput — the Solid lifecycle half. The folds/maps (value→text, the W3C/alias resolution) and
|
|
2
|
-
// the controlled-write predicate live in @symbiote-native/components/state; the render (intrinsic +
|
|
3
|
-
// native-prop mapping) in @symbiote-native/components/view. Both are shared verbatim with React,
|
|
4
|
-
// Vue and Svelte. Solid supplies only the reactivity: a signal for the acknowledged event count,
|
|
5
|
-
// setup-scope variables for the host node / the last text native holds / the focus flag, one effect
|
|
6
|
-
// for the controlled write, and the imperative handle handed to `ref`.
|
|
7
|
-
//
|
|
8
|
-
// THE CONTROLLED HANDSHAKE IS NOT A PROP RE-PUSH, and that is the whole subtlety. Native owns its
|
|
9
|
-
// own text and has already changed it by the time the change event arrives. When JS REFUSES that
|
|
10
|
-
// change the `value` prop never changes — so nothing re-commits, and the only thing that can
|
|
11
|
-
// correct native is an imperative setTextAndSelection carrying the ACKNOWLEDGED event count (the
|
|
12
|
-
// one native last reported), which native's eventLag check accepts. Same shape as Switch's
|
|
13
|
-
// snap-back; the counter arithmetic is documented in @symbiote-native/components/state/text-input.
|
|
14
|
-
//
|
|
15
|
-
// A FLAT MODULE, NOT A FOLDER: TextInput has no platform variant to select by filename. The
|
|
16
|
-
// iOS/Android differences (keyboardType, autoComplete vs textContentType, underlineColorAndroid)
|
|
17
|
-
// are resolved platform-AGNOSTICALLY by resolveTextInputProps, which emits both platforms' native
|
|
18
|
-
// props and lets the inert one ride — see its header. Only genuine platform/shared groups get a
|
|
19
|
-
// folder (symbiote-file-layout), so Switch has one and this does not.
|
|
20
|
-
//
|
|
21
|
-
// NOTHING here destructures `props`. Solid props are getters and a component body runs ONCE, so a
|
|
22
|
-
// destructure would freeze the input at its mount-time config; splitProps is the idiomatic split
|
|
23
|
-
// that keeps the rest reactive, and every read below sits inside an accessor, an effect or an event
|
|
24
|
-
// handler. The two deliberate exceptions are marked where they happen.
|
|
25
|
-
import { createEffect, createMemo, createSignal, onCleanup, splitProps, untrack, } from 'solid-js';
|
|
26
|
-
import { eventCountFromChange, foldText, renderTextInput, resolveAccessibilityProps, resolveTextInputProps, shouldCommandText, textFromChange, INITIAL_EVENT_COUNT, SELECTION_NONE, } from '@symbiote-native/components';
|
|
27
|
-
import { blurTextInput, dispatchViewCommand, dlog, setInputBlurred, setInputFocused, whenCommitted, } from '@symbiote-native/engine';
|
|
28
|
-
import { descriptorToSolid } from '../descriptor-to-solid.js';
|
|
29
|
-
// Read by the component itself; everything else forwards onto the host node. onValueChange/onFocus/
|
|
30
|
-
// onBlur must be split off — they are pure JS, and it is our WRAPPED handlers that go on the host
|
|
31
|
-
// (a raw function prop that is not a ViewConfig event crashes Android's folly::dynamic serializer).
|
|
32
|
-
// The alias props (inputMode, enterKeyHint, readOnly, submitBehavior, blurOnSubmit) are inert at
|
|
33
|
-
// the native layer and must be folded, never forwarded raw. Same list as React's destructure.
|
|
34
|
-
const HANDLED_PROPS = [
|
|
35
|
-
'ref',
|
|
36
|
-
'value',
|
|
37
|
-
'defaultValue',
|
|
38
|
-
'multiline',
|
|
39
|
-
'selection',
|
|
40
|
-
'onValueChange',
|
|
41
|
-
'onFocus',
|
|
42
|
-
'onBlur',
|
|
43
|
-
'inputMode',
|
|
44
|
-
'enterKeyHint',
|
|
45
|
-
'readOnly',
|
|
46
|
-
'submitBehavior',
|
|
47
|
-
'blurOnSubmit',
|
|
48
|
-
'cursorColor',
|
|
49
|
-
'selectionColor',
|
|
50
|
-
'selectionHandleColor',
|
|
51
|
-
'keyboardType',
|
|
52
|
-
'returnKeyType',
|
|
53
|
-
'editable',
|
|
54
|
-
'autoComplete',
|
|
55
|
-
'textContentType',
|
|
56
|
-
'autoFocus',
|
|
57
|
-
'showSoftInputOnFocus',
|
|
58
|
-
'underlineColorAndroid',
|
|
59
|
-
];
|
|
60
|
-
export function TextInput(props) {
|
|
61
|
-
const [local, rest] = splitProps(props, HANDLED_PROPS);
|
|
62
|
-
// The count native last acknowledged. A signal because two readers need it live: the render (it
|
|
63
|
-
// rides down as mostRecentEventCount) and the controlled-write effect, which echoes it so
|
|
64
|
-
// native's eventLag lands on 0 and the write applies instead of being dropped as stale.
|
|
65
|
-
const [mostRecentEventCount, setMostRecentEventCount] = createSignal(INITIAL_EVENT_COUNT);
|
|
66
|
-
// The host node, held by IDENTITY in a plain variable — never a store or a deep proxy, which
|
|
67
|
-
// would be a different key than the one the engine's commit mirror holds, and every command below
|
|
68
|
-
// would silently no-op (symbiote-engine-core §3). Null only before the first build and after a
|
|
69
|
-
// multiline swap disposes the old one.
|
|
70
|
-
let host = null;
|
|
71
|
-
// The last text native holds, as far as JS knows. Deliberately NOT reactive: the controlled-write
|
|
72
|
-
// effect both reads and writes it, and a signal would make that a loop. Seeded by a one-time
|
|
73
|
-
// setup read of the mount-time value — the `text` prop already carries that value down via
|
|
74
|
-
// createNode, so the FIRST controlled value is not a divergence and must NOT re-command.
|
|
75
|
-
let lastNativeText = foldText(local.value, local.defaultValue);
|
|
76
|
-
// Mirrored from the focus/blur pair for isFocused(): native exposes no synchronous focus getter,
|
|
77
|
-
// so RN's own TextInputState keeps the same flag.
|
|
78
|
-
let focused = false;
|
|
79
|
-
// autoFocus fires once, on the first host node that commits; guarded so a later multiline swap
|
|
80
|
-
// does not steal focus back.
|
|
81
|
-
let autoFocused = false;
|
|
82
|
-
// The acknowledged count, read WITHOUT subscribing. The imperative handle can be called from
|
|
83
|
-
// anywhere, including inside a caller's effect or memo — a tracked read there would silently
|
|
84
|
-
// enroll their computation in our keystroke counter. React's handle closes over a render value
|
|
85
|
-
// and has the same non-reactive contract.
|
|
86
|
-
const acknowledgedCount = () => untrack(mostRecentEventCount);
|
|
87
|
-
const handleChange = (event) => {
|
|
88
|
-
// Event seam: the controlled handshake hinges on the change payload carrying `text`
|
|
89
|
-
// (+ `eventCount`). iOS and Android Fabric can key these differently, so log the actual shape
|
|
90
|
-
// here; a missing `text` means onValueChange never fires.
|
|
91
|
-
dlog(`TextInput change keys=[${Object.keys(event.nativeEvent).join(',')}] ` +
|
|
92
|
-
`text=${JSON.stringify(event.nativeEvent.text)} count=${JSON.stringify(event.nativeEvent.eventCount)}`);
|
|
93
|
-
const text = textFromChange(event);
|
|
94
|
-
if (text !== undefined) {
|
|
95
|
-
// Ordering matters: record the text first, then bump the acknowledged count, so the count
|
|
96
|
-
// never runs ahead of the text it stands for.
|
|
97
|
-
lastNativeText = text;
|
|
98
|
-
local.onValueChange?.(text, event);
|
|
99
|
-
}
|
|
100
|
-
const count = eventCountFromChange(event);
|
|
101
|
-
if (count !== undefined)
|
|
102
|
-
setMostRecentEventCount(count);
|
|
103
|
-
};
|
|
104
|
-
const handleFocus = (event) => {
|
|
105
|
-
focused = true;
|
|
106
|
-
// Track focus app-wide so Keyboard.dismiss can blur this input without a ref.
|
|
107
|
-
if (host !== null)
|
|
108
|
-
setInputFocused(host);
|
|
109
|
-
local.onFocus?.(event);
|
|
110
|
-
};
|
|
111
|
-
const handleBlur = (event) => {
|
|
112
|
-
focused = false;
|
|
113
|
-
if (host !== null)
|
|
114
|
-
setInputBlurred(host);
|
|
115
|
-
local.onBlur?.(event);
|
|
116
|
-
};
|
|
117
|
-
// One host node for one intrinsic. `multiline` arrives as an ARGUMENT, not as a prop read, so the
|
|
118
|
-
// descriptor's type is constant for this node's whole lifetime — descriptorToSolid builds the
|
|
119
|
-
// element once and only diffs prop VALUES afterwards, and a type that changed under it would
|
|
120
|
-
// throw rather than paint.
|
|
121
|
-
const buildHost = (multiline) => {
|
|
122
|
-
const node = descriptorToSolid(() => renderTextInput({
|
|
123
|
-
multiline,
|
|
124
|
-
text: foldText(local.value, local.defaultValue),
|
|
125
|
-
mostRecentEventCount: mostRecentEventCount(),
|
|
126
|
-
selection: local.selection,
|
|
127
|
-
folded: resolveTextInputProps({
|
|
128
|
-
inputMode: local.inputMode,
|
|
129
|
-
keyboardType: local.keyboardType,
|
|
130
|
-
enterKeyHint: local.enterKeyHint,
|
|
131
|
-
returnKeyType: local.returnKeyType,
|
|
132
|
-
readOnly: local.readOnly,
|
|
133
|
-
editable: local.editable,
|
|
134
|
-
submitBehavior: local.submitBehavior,
|
|
135
|
-
blurOnSubmit: local.blurOnSubmit,
|
|
136
|
-
multiline,
|
|
137
|
-
cursorColor: local.cursorColor,
|
|
138
|
-
selectionColor: local.selectionColor,
|
|
139
|
-
selectionHandleColor: local.selectionHandleColor,
|
|
140
|
-
autoComplete: local.autoComplete,
|
|
141
|
-
textContentType: local.textContentType,
|
|
142
|
-
showSoftInputOnFocus: local.showSoftInputOnFocus,
|
|
143
|
-
underlineColorAndroid: local.underlineColorAndroid,
|
|
144
|
-
}),
|
|
145
|
-
// TextInput owns its host element rather than rendering through a symbiote View, so it
|
|
146
|
-
// folds aria/role into the canonical accessibility* props here. The vanished-key widening
|
|
147
|
-
// that fold needs is descriptorToSolid's job (utils/stable-keys), applied to the whole
|
|
148
|
-
// Descriptor prop bag — nothing to repeat here.
|
|
149
|
-
passthrough: {
|
|
150
|
-
...resolveAccessibilityProps(rest),
|
|
151
|
-
onChange: handleChange,
|
|
152
|
-
onFocus: handleFocus,
|
|
153
|
-
onBlur: handleBlur,
|
|
154
|
-
},
|
|
155
|
-
}));
|
|
156
|
-
host = node;
|
|
157
|
-
// Dropped together with the computations this build owns, so a command can never aim at a node
|
|
158
|
-
// that was swapped out and is no longer mounted.
|
|
159
|
-
onCleanup(() => {
|
|
160
|
-
if (host === node)
|
|
161
|
-
host = null;
|
|
162
|
-
});
|
|
163
|
-
// autoFocus is driven in JS, not as a native prop (RN does the same via TextInputState.focusInput,
|
|
164
|
-
// TextInput.js:538). The engine commits on a microtask here (renderer.ts's requestCommit), so at
|
|
165
|
-
// this point the node has no Fabric tag and a bare dispatchViewCommand would silently no-op —
|
|
166
|
-
// whenCommitted defers it to the commit that assigns the tag. Cancelled on cleanup so an
|
|
167
|
-
// un-committed pending focus cannot outlive the node.
|
|
168
|
-
if (local.autoFocus === true && !autoFocused) {
|
|
169
|
-
autoFocused = true;
|
|
170
|
-
dlog('TextInput autoFocus -> focus command');
|
|
171
|
-
const cancel = whenCommitted(node, () => {
|
|
172
|
-
dispatchViewCommand(node, 'focus', []);
|
|
173
|
-
});
|
|
174
|
-
onCleanup(cancel);
|
|
175
|
-
}
|
|
176
|
-
return node;
|
|
177
|
-
};
|
|
178
|
-
// Controlled write: when JS-side `value` diverges from what native reported, command the new text
|
|
179
|
-
// down with the acknowledged count. A plain prop re-push would race the user's keystrokes.
|
|
180
|
-
//
|
|
181
|
-
// READING THE COUNT IS LOAD-BEARING, not bookkeeping — it is what makes a REFUSED change snap
|
|
182
|
-
// back. On a refusal `value` never changes, so a value-only dependency (Vue's watch shape) would
|
|
183
|
-
// never re-run and native would keep the text JS rejected. The count bumps on every native
|
|
184
|
-
// change, which is React's `useLayoutEffect` with no deps expressed in Solid's terms.
|
|
185
|
-
createEffect(() => {
|
|
186
|
-
const value = local.value;
|
|
187
|
-
const count = mostRecentEventCount();
|
|
188
|
-
const selection = local.selection;
|
|
189
|
-
if (host === null)
|
|
190
|
-
return;
|
|
191
|
-
if (!shouldCommandText(lastNativeText, value))
|
|
192
|
-
return;
|
|
193
|
-
const selStart = selection?.start ?? SELECTION_NONE;
|
|
194
|
-
const selEnd = selection?.end ?? selection?.start ?? SELECTION_NONE;
|
|
195
|
-
dlog(`TextInput setTextAndSelection count=${count} text=${JSON.stringify(value)}`);
|
|
196
|
-
dispatchViewCommand(host, 'setTextAndSelection', [
|
|
197
|
-
count,
|
|
198
|
-
value,
|
|
199
|
-
selStart,
|
|
200
|
-
selEnd,
|
|
201
|
-
]);
|
|
202
|
-
lastNativeText = value;
|
|
203
|
-
});
|
|
204
|
-
// The imperative API RN exposes on a TextInput ref. focus/blur drive native view commands; clear
|
|
205
|
-
// and setSelection reuse setTextAndSelection (the same stale-safe path as a controlled write),
|
|
206
|
-
// echoing the acknowledged count so native applies them. Every method reads `host` LIVE, so a
|
|
207
|
-
// multiline swap cannot leave the handle pointing at a dead node.
|
|
208
|
-
const handle = {
|
|
209
|
-
// Forwarded to the node so a TextInput ref is not poorer than any other host ref. Measured
|
|
210
|
-
// 2026-08-31: the wrapper's handle used to be five methods and thereby CLOSED OVER the node,
|
|
211
|
-
// costing the component path `measure`/`measureInWindow`/`measureLayout`/`setNativeProps`,
|
|
212
|
-
// while a lowered element handed back the bare node and lost `clear`/`isFocused`/
|
|
213
|
-
// `setSelection`. Two different surfaces, crossed by writing `multiline={isLong}` instead of
|
|
214
|
-
// `multiline`. The union is the fix; refusing to lower would only swap which four go missing.
|
|
215
|
-
measure: (callback) => {
|
|
216
|
-
if (host !== null)
|
|
217
|
-
host.measure(callback);
|
|
218
|
-
},
|
|
219
|
-
measureInWindow: (callback) => {
|
|
220
|
-
if (host !== null)
|
|
221
|
-
host.measureInWindow(callback);
|
|
222
|
-
},
|
|
223
|
-
measureLayout: (relativeToNativeNode, onSuccess, onFail) => {
|
|
224
|
-
if (host !== null)
|
|
225
|
-
host.measureLayout(relativeToNativeNode, onSuccess, onFail);
|
|
226
|
-
},
|
|
227
|
-
setNativeProps: (nativeProps) => {
|
|
228
|
-
if (host !== null)
|
|
229
|
-
host.setNativeProps(nativeProps);
|
|
230
|
-
},
|
|
231
|
-
focus: () => {
|
|
232
|
-
if (host !== null)
|
|
233
|
-
dispatchViewCommand(host, 'focus', []);
|
|
234
|
-
},
|
|
235
|
-
blur: () => {
|
|
236
|
-
// Routes through TextInputState so the app-wide focus tracking clears too.
|
|
237
|
-
blurTextInput(host);
|
|
238
|
-
},
|
|
239
|
-
clear: () => {
|
|
240
|
-
if (host === null)
|
|
241
|
-
return;
|
|
242
|
-
dispatchViewCommand(host, 'setTextAndSelection', [
|
|
243
|
-
acknowledgedCount(),
|
|
244
|
-
'',
|
|
245
|
-
0,
|
|
246
|
-
0,
|
|
247
|
-
]);
|
|
248
|
-
lastNativeText = '';
|
|
249
|
-
},
|
|
250
|
-
isFocused: () => focused,
|
|
251
|
-
setSelection: (start, end) => {
|
|
252
|
-
if (host === null)
|
|
253
|
-
return;
|
|
254
|
-
const current = lastNativeText ?? '';
|
|
255
|
-
dispatchViewCommand(host, 'setTextAndSelection', [
|
|
256
|
-
acknowledgedCount(),
|
|
257
|
-
current,
|
|
258
|
-
start,
|
|
259
|
-
end,
|
|
260
|
-
]);
|
|
261
|
-
},
|
|
262
|
-
};
|
|
263
|
-
// Single- and multiline are DIFFERENT native views (RCTSinglelineTextInputView vs
|
|
264
|
-
// RCTMultilineTextInputView), so a runtime flip has to build a new host node rather than re-prop
|
|
265
|
-
// the old one — React remounts, Vue h()s a new type, Svelte swaps an {#if} branch. The memo is
|
|
266
|
-
// that swap: `insert` replaces the previous node with the new one and disposes the old build.
|
|
267
|
-
//
|
|
268
|
-
// The untrack is load-bearing. Building reads descriptorToSolid's own memo, so without it THIS
|
|
269
|
-
// memo would subscribe to every prop the render fn touches and rebuild the entire input — new
|
|
270
|
-
// native view, lost cursor and keyboard — on every keystroke. (Solid's own `createComponent`
|
|
271
|
-
// untracks for exactly this reason; there is no component here to inherit it from.)
|
|
272
|
-
const hostTree = createMemo(() => {
|
|
273
|
-
const multiline = local.multiline === true;
|
|
274
|
-
return untrack(() => buildHost(multiline));
|
|
275
|
-
});
|
|
276
|
-
// Solid's `ref` is a compile-time construct: on a component the compiler has already rewritten
|
|
277
|
-
// `ref={handle}` into a callback prop by the time we read it, so calling what we were handed is
|
|
278
|
-
// the whole job (utils/host-ref.ts explains the rewrite). Not applyHostRef, which is typed for a
|
|
279
|
-
// host node — this ref receives the handle, matching React's forwardRef contract.
|
|
280
|
-
//
|
|
281
|
-
// Called AFTER `hostTree` has built at least once, not right after `handle` is constructed -
|
|
282
|
-
// same reasoning as ScrollView's ref-ordering fix (scroll-view/shared.tsx). `handle`'s methods
|
|
283
|
-
// read `host` LIVE on every call, so calling ref early never caused a visible bug here (unlike
|
|
284
|
-
// ScrollView, whose handle a consumer - createAnimatedComponent's resolveHostNode - unwrapped
|
|
285
|
-
// eagerly, once, at ref-call time). Reordered anyway: a future Animated.TextInput would hit the
|
|
286
|
-
// same permanently-null capture, and matching the safe ordering now costs nothing.
|
|
287
|
-
if (typeof local.ref === 'function')
|
|
288
|
-
local.ref(handle);
|
|
289
|
-
return hostTree;
|
|
290
|
-
}
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
// Text — the Solid host primitive for RN's <Text>. Same children/ref idiom as ./view.tsx; read
|
|
2
|
-
// that file's header first, it carries the reasoning both share.
|
|
3
|
-
//
|
|
4
|
-
// THE NESTING (`RCTText` vs `RCTVirtualText`) IS NOT THIS FILE'S JOB, and that is worth stating
|
|
5
|
-
// because it is the one place React's own renderer needs a context. React Native tracks a
|
|
6
|
-
// TextAncestor context so a <Text> inside another <Text> renders as a virtual span instead of a
|
|
7
|
-
// paragraph host. Here the retained tree already knows: the engine's commit walk carries a
|
|
8
|
-
// `hasTextAncestor` flag down and picks the view name itself
|
|
9
|
-
// (`viewNameFor` in core/engine/src/commit.ts — `node.isText && hasTextAncestor` -> RCTVirtualText),
|
|
10
|
-
// and it re-creates the node from scratch when that kind flips. So there is no Solid context, no
|
|
11
|
-
// provider, and nothing for an adapter to thread; every adapter emits the same flat
|
|
12
|
-
// `symbiote-text` and the engine resolves the position-dependent name for all of them. The
|
|
13
|
-
// Solid equivalent of TextAncestorContext is deliberately ABSENT, not missing.
|
|
14
|
-
import { splitProps } from 'solid-js';
|
|
15
|
-
import { resolveAccessibilityProps, resolveTextProps, } from '@symbiote-native/components';
|
|
16
|
-
import { applyHostRef } from '../utils/host-ref.js';
|
|
17
|
-
import { withStableKeys } from '../utils/stable-keys.js';
|
|
18
|
-
export function Text(props) {
|
|
19
|
-
const [local, rest] = splitProps(props, ['children', 'ref']);
|
|
20
|
-
const bag = withStableKeys(() => resolveTextProps({ ...resolveAccessibilityProps(rest) }));
|
|
21
|
-
const attachRef = (node) => {
|
|
22
|
-
applyHostRef(local.ref, node);
|
|
23
|
-
};
|
|
24
|
-
return (<symbiote-text ref={attachRef} {...bag()}>
|
|
25
|
-
{local.children}
|
|
26
|
-
</symbiote-text>);
|
|
27
|
-
}
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
import type { JSX } from '../../jsx-runtime';
|
|
2
|
-
import { type IPressTimingProps } from '@symbiote-native/components';
|
|
3
|
-
import { type IStyleProp, type IViewStyle } from '@symbiote-native/engine';
|
|
4
|
-
import { type IPressableProps } from '../pressable';
|
|
5
|
-
type ITouchableBaseProps = Omit<IPressableProps, 'style' | 'children'> & IPressTimingProps & {
|
|
6
|
-
style?: IStyleProp<IViewStyle>;
|
|
7
|
-
children?: JSX.Element;
|
|
8
|
-
};
|
|
9
|
-
export interface ITouchableOpacityProps extends ITouchableBaseProps {
|
|
10
|
-
activeOpacity?: number;
|
|
11
|
-
}
|
|
12
|
-
export declare function TouchableOpacity(props: ITouchableOpacityProps): JSX.Element;
|
|
13
|
-
export interface ITouchableHighlightProps extends ITouchableBaseProps {
|
|
14
|
-
activeOpacity?: number;
|
|
15
|
-
underlayColor?: string;
|
|
16
|
-
onShowUnderlay?: () => void;
|
|
17
|
-
onHideUnderlay?: () => void;
|
|
18
|
-
}
|
|
19
|
-
export declare function TouchableHighlight(props: ITouchableHighlightProps): JSX.Element;
|
|
20
|
-
export type ITouchableWithoutFeedbackProps = ITouchableBaseProps;
|
|
21
|
-
export declare function TouchableWithoutFeedback(props: ITouchableWithoutFeedbackProps): JSX.Element;
|
|
22
|
-
export {};
|
|
@@ -1,247 +0,0 @@
|
|
|
1
|
-
// The Touchable* family for Solid, all built on Pressable. Ported against RN's own sources
|
|
2
|
-
// (.vendors/react-native/.../Components/Touchable/*.js), NOT against this repo's React adapter —
|
|
3
|
-
// an audit on 2026-08-19 found the React/Vue/Svelte/Angular ports diverge from RN in ten places.
|
|
4
|
-
// This adapter is the first on the RN-accurate shared API; the other four migrate in phase two,
|
|
5
|
-
// which is why @symbiote-native/components carries both forms for now.
|
|
6
|
-
//
|
|
7
|
-
// Shared (framework-agnostic) half: the press-scheduling machine, the underlay machine, the resting
|
|
8
|
-
// opacity math, the press-handler gate, the extra-style split. Solid owns only the lifecycle —
|
|
9
|
-
// signals, memos, the real timers, and the Animated wiring.
|
|
10
|
-
//
|
|
11
|
-
// NOTHING here destructures `props`. A Solid component body runs ONCE and props are getters, so a
|
|
12
|
-
// destructure would freeze the component at its mount-time config; every read below sits inside an
|
|
13
|
-
// accessor, a memo or an event handler.
|
|
14
|
-
//
|
|
15
|
-
// No conditional picks a branch in any of the three trees. Solid's `insert` REPLACES a subtree
|
|
16
|
-
// rather than diffing it, and a rebuild landing mid-gesture costs the native responder grant
|
|
17
|
-
// (.claude/rules/solid-descriptor-bridge.md §4). Every per-press value here is a prop-bag value,
|
|
18
|
-
// and a prop bag reaches the host through `spread` — a per-key diff on the SAME element.
|
|
19
|
-
import { createEffect, createMemo, createSignal, onCleanup, splitProps, untrack, } from 'solid-js';
|
|
20
|
-
import { createHighlightUnderlayHandlers, createHighlightUnderlayRuntime, createTouchableFeedbackHandlers, createTouchableFeedbackRuntime, hasTouchablePressHandler, resolveHighlightExtraStyles, restingOpacityFromStyle, DEFAULT_ACTIVE_OPACITY, OPACITY_ACTIVE_GRANT_DURATION_MS, OPACITY_INACTIVE_DURATION_MS, TOUCHABLE_MIN_PRESS_DURATION_MS, } from '@symbiote-native/components';
|
|
21
|
-
import { dlog, } from '@symbiote-native/engine';
|
|
22
|
-
import { Animated } from '../../modules/animated/index.js';
|
|
23
|
-
import { TouchablePressable as Pressable, } from '../pressable.jsx';
|
|
24
|
-
// The real timers both shared machines schedule on — core/components carries no timer globals, so
|
|
25
|
-
// scheduling is the adapter's half. Every canceller is retained so unmount cancels what is still in
|
|
26
|
-
// flight: a Solid body runs once, so there is no re-render that would otherwise drop a stale timer,
|
|
27
|
-
// and a fired timer would call back into a disposed scope.
|
|
28
|
-
function createTimeoutScheduler() {
|
|
29
|
-
const pending = new Set();
|
|
30
|
-
onCleanup(() => {
|
|
31
|
-
for (const cancel of [...pending])
|
|
32
|
-
cancel();
|
|
33
|
-
pending.clear();
|
|
34
|
-
});
|
|
35
|
-
return (callback, ms) => {
|
|
36
|
-
const id = setTimeout(() => {
|
|
37
|
-
pending.delete(cancel);
|
|
38
|
-
callback();
|
|
39
|
-
}, ms);
|
|
40
|
-
const cancel = () => {
|
|
41
|
-
clearTimeout(id);
|
|
42
|
-
pending.delete(cancel);
|
|
43
|
-
};
|
|
44
|
-
pending.add(cancel);
|
|
45
|
-
return cancel;
|
|
46
|
-
};
|
|
47
|
-
}
|
|
48
|
-
// `class` is split off with `style`: both describe the FEEDBACK node (the inner Animated.View),
|
|
49
|
-
// not the outer Pressable that owns the responder. Left in `rest` they would land on the wrong one.
|
|
50
|
-
const TOUCHABLE_OPACITY_HANDLED = [
|
|
51
|
-
'activeOpacity',
|
|
52
|
-
'style',
|
|
53
|
-
'class',
|
|
54
|
-
'children',
|
|
55
|
-
'onPressIn',
|
|
56
|
-
'onPressOut',
|
|
57
|
-
'delayPressIn',
|
|
58
|
-
'delayPressOut',
|
|
59
|
-
'minPressDuration',
|
|
60
|
-
];
|
|
61
|
-
export function TouchableOpacity(props) {
|
|
62
|
-
const [local, rest] = splitProps(props, TOUCHABLE_OPACITY_HANDLED);
|
|
63
|
-
// RN's _getChildStyleOpacityWithDefault: the fade settles at the opacity the CALLER's style asks
|
|
64
|
-
// for, not at a hard 1.
|
|
65
|
-
const restingOpacity = createMemo(() => restingOpacityFromStyle(local.style));
|
|
66
|
-
// One Animated.Value per mount, held by IDENTITY in a plain variable — a signal or store would
|
|
67
|
-
// wrap this engine object in a proxy, and the graph's bookkeeping is keyed on the raw instance
|
|
68
|
-
// (symbiote-engine-core §3). untrack because the body must not subscribe to the initial read.
|
|
69
|
-
const opacity = new Animated.Value(untrack(restingOpacity));
|
|
70
|
-
// The shared press-scheduling cell (delayPressIn canceller + activation clock). A setup-scope
|
|
71
|
-
// object, never reactive: the machine mutates it on every event and nothing reads it reactively.
|
|
72
|
-
const runtime = createTouchableFeedbackRuntime();
|
|
73
|
-
const schedule = createTimeoutScheduler();
|
|
74
|
-
function setOpacityTo(toValue, duration) {
|
|
75
|
-
dlog(`TouchableOpacity opacity -> ${toValue} over ${duration}ms`);
|
|
76
|
-
// useNativeDriver: true is RN's own (TouchableOpacity.js:242). opacity is natively drivable and
|
|
77
|
-
// the fade then survives a busy JS thread, which is the whole point of the feedback.
|
|
78
|
-
Animated.timing(opacity, {
|
|
79
|
-
toValue,
|
|
80
|
-
duration,
|
|
81
|
-
easing: Animated.Easing.inOut(Animated.Easing.quad),
|
|
82
|
-
useNativeDriver: true,
|
|
83
|
-
}).start();
|
|
84
|
-
}
|
|
85
|
-
// Rebuilt only when the TIMING config changes — the callbacks read activeOpacity / onPressIn /
|
|
86
|
-
// onPressOut when they fire, so a re-supplied callback is honored without churning the handler
|
|
87
|
-
// identities the host node holds. The runtime persists across rebuilds, so an in-flight
|
|
88
|
-
// delayPressIn timer survives one.
|
|
89
|
-
const handlers = createMemo(() => createTouchableFeedbackHandlers({
|
|
90
|
-
delayPressIn: local.delayPressIn ?? 0,
|
|
91
|
-
delayPressOut: local.delayPressOut ?? 0,
|
|
92
|
-
// RN's Touchables override Pressability's 130ms floor with 0; the active visual is held by
|
|
93
|
-
// the fade's own duration instead.
|
|
94
|
-
minPressDuration: local.minPressDuration ?? TOUCHABLE_MIN_PRESS_DURATION_MS,
|
|
95
|
-
schedule,
|
|
96
|
-
now: Date.now,
|
|
97
|
-
}, runtime, {
|
|
98
|
-
activate(event) {
|
|
99
|
-
setOpacityTo(local.activeOpacity ?? DEFAULT_ACTIVE_OPACITY, OPACITY_ACTIVE_GRANT_DURATION_MS);
|
|
100
|
-
local.onPressIn?.(event);
|
|
101
|
-
},
|
|
102
|
-
deactivate(event) {
|
|
103
|
-
setOpacityTo(restingOpacity(), OPACITY_INACTIVE_DURATION_MS);
|
|
104
|
-
local.onPressOut?.(event);
|
|
105
|
-
},
|
|
106
|
-
}));
|
|
107
|
-
// RN's componentDidUpdate: a changed `disabled` or a changed style opacity re-settles the view,
|
|
108
|
-
// so a Touchable disabled mid-press does not stay stuck at its active opacity. Skipped on the
|
|
109
|
-
// first run — RN does this on UPDATE only, and firing at mount would animate over the initial
|
|
110
|
-
// value the Animated.Value already holds.
|
|
111
|
-
createEffect(previous => {
|
|
112
|
-
const current = { disabled: rest.disabled, resting: restingOpacity() };
|
|
113
|
-
if (previous !== undefined &&
|
|
114
|
-
(previous.disabled !== current.disabled ||
|
|
115
|
-
previous.resting !== current.resting)) {
|
|
116
|
-
setOpacityTo(current.resting, OPACITY_INACTIVE_DURATION_MS);
|
|
117
|
-
}
|
|
118
|
-
return current;
|
|
119
|
-
}, undefined);
|
|
120
|
-
// RN's componentWillUnmount: stop the animation and drop the value back, so a teardown mid-fade
|
|
121
|
-
// leaves no driver running against a node that is gone.
|
|
122
|
-
onCleanup(() => {
|
|
123
|
-
opacity.resetAnimation();
|
|
124
|
-
});
|
|
125
|
-
// Depends on `style` alone. `opacity` is a stable graph node, so a press changes what the node
|
|
126
|
-
// HOLDS, never this array — the per-frame path is value.setValue -> AnimatedProps.update ->
|
|
127
|
-
// setNativeProps, which never re-enters this component.
|
|
128
|
-
const feedbackStyle = createMemo(() => [local.style, { opacity }]);
|
|
129
|
-
// A spread followed by explicit props is safe here only because neither override can be
|
|
130
|
-
// `undefined` — mergeProps takes the first NON-undefined value scanning back-to-front, so an
|
|
131
|
-
// optional override after a spread would silently lose (.claude/rules/solid-descriptor-bridge.md
|
|
132
|
-
// §6). handlers() always returns both functions.
|
|
133
|
-
return (<Pressable {...rest} onPressIn={handlers().handlePressIn} onPressOut={handlers().handlePressOut}>
|
|
134
|
-
<Animated.View style={feedbackStyle()} class={local.class}>
|
|
135
|
-
{local.children}
|
|
136
|
-
</Animated.View>
|
|
137
|
-
</Pressable>);
|
|
138
|
-
}
|
|
139
|
-
// onPress/onPressIn/onPressOut are INTERCEPTED, not forwarded: RN drives the underlay from those
|
|
140
|
-
// three callbacks and then calls the caller's. onLongPress stays in `rest` (Pressable fires it
|
|
141
|
-
// directly) and is only READ here, for the has-press-handler gate.
|
|
142
|
-
const TOUCHABLE_HIGHLIGHT_HANDLED = [
|
|
143
|
-
'activeOpacity',
|
|
144
|
-
'underlayColor',
|
|
145
|
-
'style',
|
|
146
|
-
'children',
|
|
147
|
-
'onPress',
|
|
148
|
-
'onPressIn',
|
|
149
|
-
'onPressOut',
|
|
150
|
-
'onShowUnderlay',
|
|
151
|
-
'onHideUnderlay',
|
|
152
|
-
'delayPressOut',
|
|
153
|
-
];
|
|
154
|
-
export function TouchableHighlight(props) {
|
|
155
|
-
const [local, rest] = splitProps(props, TOUCHABLE_HIGHLIGHT_HANDLED);
|
|
156
|
-
// `shown` is NOT `pressed`: RN holds the underlay for delayPressOut past the tap so a fast press
|
|
157
|
-
// still flashes, and the pressed flag is already false by then.
|
|
158
|
-
const [shown, setShown] = createSignal(false);
|
|
159
|
-
const runtime = createHighlightUnderlayRuntime();
|
|
160
|
-
const schedule = createTimeoutScheduler();
|
|
161
|
-
const hasPressHandler = createMemo(() => hasTouchablePressHandler({
|
|
162
|
-
onPress: local.onPress,
|
|
163
|
-
onPressIn: local.onPressIn,
|
|
164
|
-
onPressOut: local.onPressOut,
|
|
165
|
-
onLongPress: rest.onLongPress,
|
|
166
|
-
}));
|
|
167
|
-
const underlay = createMemo(() => createHighlightUnderlayHandlers({
|
|
168
|
-
delayPressOut: local.delayPressOut ?? 0,
|
|
169
|
-
hasPressHandler: hasPressHandler(),
|
|
170
|
-
schedule,
|
|
171
|
-
}, runtime, {
|
|
172
|
-
setShown,
|
|
173
|
-
onShowUnderlay: () => local.onShowUnderlay?.(),
|
|
174
|
-
onHideUnderlay: () => local.onHideUnderlay?.(),
|
|
175
|
-
}));
|
|
176
|
-
const extraStyles = createMemo(() => resolveHighlightExtraStyles({
|
|
177
|
-
shown: shown(),
|
|
178
|
-
hasPressHandler: hasPressHandler(),
|
|
179
|
-
underlayColor: local.underlayColor,
|
|
180
|
-
activeOpacity: local.activeOpacity,
|
|
181
|
-
}));
|
|
182
|
-
// POINT 7 IS DELIBERATELY NOT FIXED HERE, and this is the whole of the divergence from RN.
|
|
183
|
-
// RN clones its single child with `extra.child` so the lowered opacity lands on the CHILD while
|
|
184
|
-
// the container keeps an opaque underlay. Solid has no cloneElement, so the only way to reach the
|
|
185
|
-
// child is a permanent wrapper view — which inserts a node into the flex chain between this
|
|
186
|
-
// container and the children, silently re-parenting any `flex` a child declares. The fake Fabric
|
|
187
|
-
// runs no Yoga, so no headless test can measure that damage, only assert the extra node exists.
|
|
188
|
-
// Shipping an unmeasurable layout change is worse than the visual approximation, so both styles
|
|
189
|
-
// stay on the container (the pre-audit behavior) and the child style is folded in last.
|
|
190
|
-
// resolveHighlightExtraStyles keeps them SEPARATE regardless, so React's cloneElement path and a
|
|
191
|
-
// future Solid fix are both unblocked. A layout-safe fix needs a real device check, not a test.
|
|
192
|
-
const containerStyle = createMemo(() => {
|
|
193
|
-
const extra = extraStyles();
|
|
194
|
-
if (extra === undefined)
|
|
195
|
-
return local.style;
|
|
196
|
-
return [local.style, extra.underlay, extra.child];
|
|
197
|
-
});
|
|
198
|
-
// Visual first, then the caller's callback — RN's order in _createPressabilityConfig.
|
|
199
|
-
const handlePressIn = (event) => {
|
|
200
|
-
underlay().handlePressIn(event);
|
|
201
|
-
local.onPressIn?.(event);
|
|
202
|
-
};
|
|
203
|
-
const handlePress = (event) => {
|
|
204
|
-
underlay().handlePress(event);
|
|
205
|
-
local.onPress?.(event);
|
|
206
|
-
};
|
|
207
|
-
const handlePressOut = (event) => {
|
|
208
|
-
underlay().handlePressOut(event);
|
|
209
|
-
local.onPressOut?.(event);
|
|
210
|
-
};
|
|
211
|
-
return (<Pressable {...rest} style={containerStyle()} onPress={handlePress} onPressIn={handlePressIn} onPressOut={handlePressOut}>
|
|
212
|
-
{local.children}
|
|
213
|
-
</Pressable>);
|
|
214
|
-
}
|
|
215
|
-
const TOUCHABLE_WITHOUT_FEEDBACK_HANDLED = [
|
|
216
|
-
'children',
|
|
217
|
-
'onPressIn',
|
|
218
|
-
'onPressOut',
|
|
219
|
-
'delayPressIn',
|
|
220
|
-
'delayPressOut',
|
|
221
|
-
'minPressDuration',
|
|
222
|
-
];
|
|
223
|
-
export function TouchableWithoutFeedback(props) {
|
|
224
|
-
const [local, rest] = splitProps(props, TOUCHABLE_WITHOUT_FEEDBACK_HANDLED);
|
|
225
|
-
// RN's TouchableWithoutFeedback builds a FULL Pressability config with delayPressIn /
|
|
226
|
-
// delayPressOut / minPressDuration: 0 — "without feedback" means no VISUAL, not no timing. The
|
|
227
|
-
// same shared machine TouchableOpacity uses runs here with the visual half left empty.
|
|
228
|
-
const runtime = createTouchableFeedbackRuntime();
|
|
229
|
-
const schedule = createTimeoutScheduler();
|
|
230
|
-
const handlers = createMemo(() => createTouchableFeedbackHandlers({
|
|
231
|
-
delayPressIn: local.delayPressIn ?? 0,
|
|
232
|
-
delayPressOut: local.delayPressOut ?? 0,
|
|
233
|
-
minPressDuration: local.minPressDuration ?? TOUCHABLE_MIN_PRESS_DURATION_MS,
|
|
234
|
-
schedule,
|
|
235
|
-
now: Date.now,
|
|
236
|
-
}, runtime, {
|
|
237
|
-
activate(event) {
|
|
238
|
-
local.onPressIn?.(event);
|
|
239
|
-
},
|
|
240
|
-
deactivate(event) {
|
|
241
|
-
local.onPressOut?.(event);
|
|
242
|
-
},
|
|
243
|
-
}));
|
|
244
|
-
return (<Pressable {...rest} onPressIn={handlers().handlePressIn} onPressOut={handlers().handlePressOut}>
|
|
245
|
-
{local.children}
|
|
246
|
-
</Pressable>);
|
|
247
|
-
}
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
import type { JSX } from '../../jsx-runtime';
|
|
2
|
-
import { canUseNativeForeground, rippleBackground, selectableBackground, selectableBackgroundBorderless, type INativeFeedbackBackground } from '@symbiote-native/components';
|
|
3
|
-
import { type IPressableProps } from '../pressable';
|
|
4
|
-
export type { INativeFeedbackBackground, IThemeAttrBackground, IRippleBackground, } from '@symbiote-native/components';
|
|
5
|
-
export interface ITouchableNativeFeedbackProps extends Omit<IPressableProps, 'style' | 'children'> {
|
|
6
|
-
background?: INativeFeedbackBackground;
|
|
7
|
-
useForeground?: boolean;
|
|
8
|
-
children?: JSX.Element;
|
|
9
|
-
}
|
|
10
|
-
declare function TouchableNativeFeedbackImpl(props: ITouchableNativeFeedbackProps): JSX.Element;
|
|
11
|
-
export declare const TouchableNativeFeedback: typeof TouchableNativeFeedbackImpl & {
|
|
12
|
-
SelectableBackground: typeof selectableBackground;
|
|
13
|
-
SelectableBackgroundBorderless: typeof selectableBackgroundBorderless;
|
|
14
|
-
Ripple: typeof rippleBackground;
|
|
15
|
-
canUseNativeForeground: typeof canUseNativeForeground;
|
|
16
|
-
};
|