@symbiote-native/components 3.1.1 → 3.1.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/build/behaviors/activity-indicator/shared.js +29 -71
- package/build/behaviors/button.d.ts +0 -9
- package/build/behaviors/button.js +47 -233
- package/build/behaviors/image-background.js +21 -81
- package/build/behaviors/image.js +3 -10
- package/build/behaviors/input-accessory-view.js +10 -51
- package/build/behaviors/pressable.d.ts +5 -51
- package/build/behaviors/pressable.js +101 -161
- package/build/behaviors/refresh-control.js +8 -2
- package/build/behaviors/scroll-view/index.android.js +10 -30
- package/build/behaviors/scroll-view/responder.d.ts +3 -2
- package/build/behaviors/scroll-view/responder.js +19 -21
- package/build/behaviors/scroll-view/shared.js +69 -185
- package/build/behaviors/scroll-view/sticky.d.ts +0 -8
- package/build/behaviors/scroll-view/sticky.js +54 -142
- package/build/behaviors/switch.js +18 -4
- package/build/behaviors/text-input.d.ts +0 -8
- package/build/behaviors/text-input.js +77 -162
- package/build/behaviors/touchable-highlight.js +14 -54
- package/build/behaviors/touchable-native-feedback.js +9 -32
- package/build/behaviors/touchable-opacity.d.ts +0 -7
- package/build/behaviors/touchable-opacity.js +65 -81
- package/build/behaviors/touchable-without-feedback.js +25 -41
- package/build/component-names/index.android.js +6 -8
- package/build/component-names/shared.js +12 -42
- package/build/index.js +13 -19
- package/build/scroll-view-commands.js +3 -11
- package/build/state/pressable.js +18 -43
- package/build/state/sticky-header-reducer.js +103 -149
- package/build/state/touchable.js +3 -5
- package/build/state/virtualized-list-reducer.js +21 -48
- package/build/state/virtualized-list.js +63 -148
- package/build/text-props.js +3 -13
- package/build/view/render-button.js +13 -44
- package/build/view/render-input-accessory-view.js +8 -24
- package/build/view/render-pressable/index.js +3 -4
- package/build/view/render-scroll-view.js +13 -23
- package/build/view/render-touchable-native-feedback.js +5 -14
- package/host-primitives.cjs +33 -207
- package/package.json +3 -3
|
@@ -1,20 +1,10 @@
|
|
|
1
1
|
// The press machine as an ENGINE-NODE behavior, so a pressable can be an intrinsic tag instead of
|
|
2
|
-
// a framework component
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
// /
|
|
7
|
-
|
|
8
|
-
//
|
|
9
|
-
// REGISTRATION IS THE HAZARD, not the machine. Metro enables `inlineRequires` in production only,
|
|
10
|
-
// moving a `require` to the first place its binding is used as a VALUE, and a barrel's
|
|
11
|
-
// `export { X } from './x'` compiles to a lazy getter. A module whose only job is a side effect is
|
|
12
|
-
// never named as a value, so re-exporting it means it NEVER RUNS in a release build — dev perfect,
|
|
13
|
-
// release silently pressless. Each adapter therefore keeps its own `src/register.ts` calling
|
|
14
|
-
// `registerPressableBehavior()`, and its entry does a bare `import './register';` that the barrel
|
|
15
|
-
// does NOT re-export. A bare `import './register';` sitting NEXT TO such a re-export does not work
|
|
16
|
-
// either: Babel merges the two imports of one specifier and the merged dependency stays lazy.
|
|
17
|
-
import { appListenerFor, dispatchViewCommand, dlog, Platform, propOf, registerHostBehavior, requestCommitFor, setBehaviorListener, setNodePressed, propsOf, } from '@symbiote-native/engine';
|
|
2
|
+
// a framework component. Written once; every adapter inherits it by registering, none re-implements
|
|
3
|
+
// it. The machine itself is unchanged and still shared with the component path.
|
|
4
|
+
// Registration, not the machine, is the hazard: Metro's inlineRequires makes a barrel re-export of
|
|
5
|
+
// a side-effect-only module never run in a release build. Each adapter keeps its own
|
|
6
|
+
// `src/register.ts` calling registerPressableBehavior(), reached only by a bare side-effect import.
|
|
7
|
+
import { appListenerFor, dispatchViewCommand, dlog, Platform, propOf, registerHostBehavior, requestCommitFor, setNodeDispatch, setNodePressed, propsOf, } from '@symbiote-native/engine';
|
|
18
8
|
import { createPressHandlers, createPressRuntime, disposePressRuntime, DEFAULT_DELAY_LONG_PRESS_MS, DEFAULT_MIN_PRESS_DURATION_MS, } from '../state/pressable.js';
|
|
19
9
|
import { buildPressableListeners } from '../view/render-pressable/index.js';
|
|
20
10
|
export const PRESSABLE_TAG = 'pressable';
|
|
@@ -56,10 +46,9 @@ function asRectOffset(value) {
|
|
|
56
46
|
function isPressHandler(value) {
|
|
57
47
|
return typeof value === 'function';
|
|
58
48
|
}
|
|
59
|
-
// Narrowed field by field rather than cast: the bag arrives as `unknown` off
|
|
60
|
-
//
|
|
61
|
-
//
|
|
62
|
-
// every adapter re-exports wholesale: a narrowing helper is not API anyone should be able to import.
|
|
49
|
+
// Narrowed field by field rather than cast: the bag arrives as `unknown` off propOf. Exported to
|
|
50
|
+
// sibling behaviors folding the same bag, deliberately NOT to the shared barrel — a narrowing
|
|
51
|
+
// helper isn't API anyone should import.
|
|
63
52
|
export function asAccessibilityState(value) {
|
|
64
53
|
if (!isRecord(value))
|
|
65
54
|
return undefined;
|
|
@@ -76,36 +65,19 @@ export function asAccessibilityState(value) {
|
|
|
76
65
|
state.expanded = value.expanded;
|
|
77
66
|
return state;
|
|
78
67
|
}
|
|
79
|
-
//
|
|
80
|
-
//
|
|
81
|
-
//
|
|
82
|
-
//
|
|
83
|
-
//
|
|
84
|
-
//
|
|
85
|
-
//
|
|
86
|
-
// It cost a trip: a `payloadFold` marshals the whole bag out and the whole bag back, ~17 us per
|
|
87
|
-
// pressable per commit, and a benchmark row carries two.
|
|
88
|
-
//
|
|
89
|
-
// What is still here is the MACHINE, which is where a browser keeps it too: timers, the responder
|
|
90
|
-
// claim, hit-slop retention, and the callbacks into app code.
|
|
91
|
-
//
|
|
92
|
-
// The one thing that did NOT move with it is `hitSlop`, and that is deliberate — it is a real
|
|
93
|
-
// native View prop, so it never was part of the fold.
|
|
94
|
-
// RN makes every pressable accessible unless the app opts OUT — `Pressable.js:252`
|
|
95
|
-
// (`accessible: accessible !== false`), and the whole Touchable family repeats it verbatim
|
|
96
|
-
// (`TouchableOpacity.js:303`, `TouchableHighlight.js:337`). `!== false` rather than `?? true`, so
|
|
97
|
-
// only a literal `false` opts out and an explicit `undefined` still reads as accessible.
|
|
98
|
-
//
|
|
99
|
-
// Nothing in this repo did it until 2026-09-09, so a Pressable reached a screen reader as a plain
|
|
100
|
-
// view unless the app wrote the prop. Exported so anything composing this tag can say it too.
|
|
68
|
+
// The payload fold moved to the engine (foldPressableProps in SymbioteFabricProps.cpp), not
|
|
69
|
+
// re-implemented here: disabled -> accessibilityState, accessible/focusable defaulting on, the
|
|
70
|
+
// ripple config and the machine-only keys are all functions of the TAG alone, user-agent behavior.
|
|
71
|
+
// What's still here is the MACHINE: timers, the responder claim, hit-slop retention, callbacks
|
|
72
|
+
// into app code. `hitSlop` did NOT move with the fold — it's a real native View prop.
|
|
73
|
+
// RN makes every pressable accessible unless the app opts OUT (`accessible !== false`), and the
|
|
74
|
+
// whole Touchable family repeats it. `!== false` not `?? true`, so only a literal `false` opts out.
|
|
101
75
|
export function accessibleUnlessOptedOut(props) {
|
|
102
76
|
return props.accessible !== false;
|
|
103
77
|
}
|
|
104
|
-
// From the STASH, not
|
|
105
|
-
//
|
|
106
|
-
//
|
|
107
|
-
// `propOf` here returns undefined for every callback and every press silently does nothing:
|
|
108
|
-
// the behavior runs, the machine runs, and it calls nobody.
|
|
78
|
+
// From the STASH, not the props: every name below is in ownedListeners, so routeProp diverts the
|
|
79
|
+
// app's onPress away from node.listeners into the stash. Reading propOf here would return
|
|
80
|
+
// undefined for every callback and every press would silently call nobody.
|
|
109
81
|
function callbackAt(node, event) {
|
|
110
82
|
const value = appListenerFor(node, event);
|
|
111
83
|
return isPressHandler(value) ? value : undefined;
|
|
@@ -121,29 +93,19 @@ function configFor(node) {
|
|
|
121
93
|
onPressOut: callbackAt(node, 'pressOut'),
|
|
122
94
|
onPressMove: callbackAt(node, 'pressMove'),
|
|
123
95
|
onLongPress: callbackAt(node, 'longPress'),
|
|
124
|
-
//
|
|
125
|
-
//
|
|
126
|
-
//
|
|
127
|
-
// `unstable_pressDelay`, lands at a constant 500ms from touch-down by default, not
|
|
128
|
-
// 500ms + unstable_pressDelay. See `createPressHandlers`'s `handlePressIn` for the other half
|
|
129
|
-
// (the timer must be ARMED at grant, not after the pressDelay fires, or this compensation
|
|
130
|
-
// does nothing).
|
|
96
|
+
// The subtraction applies only to the FALLBACK, never an authored value — it keeps the
|
|
97
|
+
// long-press threshold at a constant 500ms from touch-down by default, not
|
|
98
|
+
// 500ms + unstable_pressDelay (see createPressHandlers.handlePressIn for the other half).
|
|
131
99
|
delayLongPress: Math.max(10, numberOr(propOf(node, 'delayLongPress'), DEFAULT_DELAY_LONG_PRESS_MS - unstablePressDelay)),
|
|
132
100
|
unstable_pressDelay: unstablePressDelay,
|
|
133
|
-
// RN's Touchables own the deactivation floor in their
|
|
134
|
-
// `minPressDuration: 0
|
|
135
|
-
// an internal input; on the tag there is nowhere else to say it, so the floor has to be a
|
|
136
|
-
// readable prop or every Touchable holds its fade for the machine's 130 ms default.
|
|
101
|
+
// RN's Touchables own the deactivation floor in their own machine, handing Pressability
|
|
102
|
+
// `minPressDuration: 0`. On the tag there's nowhere else to say it, so it's a readable prop.
|
|
137
103
|
minPressDuration: numberOr(propOf(node, 'minPressDuration'), DEFAULT_MIN_PRESS_DURATION_MS),
|
|
138
104
|
hitSlop: asRectOffset(propOf(node, 'hitSlop')),
|
|
139
105
|
pressRetentionOffset: asRectOffset(propOf(node, 'pressRetentionOffset')),
|
|
140
|
-
// Pressable
|
|
141
|
-
//
|
|
142
|
-
//
|
|
143
|
-
// `TouchableNativeFeedback.js:228`). `node` here is whichever tag AUTHORS the prop — itself for
|
|
144
|
-
// a plain pressable/highlight/button, the owner for a clone-onto-child touchable, since
|
|
145
|
-
// `configFor` is always called with that source — so reading both names off it resolves
|
|
146
|
-
// correctly for every composition without a per-tag override.
|
|
106
|
+
// Pressable's own name is `android_disableSound`; every composed touchable instead forwards
|
|
107
|
+
// `touchSoundDisabled` to this same field. `node` here is whichever tag AUTHORS the prop, so
|
|
108
|
+
// reading both names resolves correctly for every composition without a per-tag override.
|
|
147
109
|
android_disableSound: booleanOr(propOf(node, 'android_disableSound')) ??
|
|
148
110
|
booleanOr(propOf(node, 'touchSoundDisabled')),
|
|
149
111
|
};
|
|
@@ -164,12 +126,8 @@ function hotspotAt(nativeEvent, key) {
|
|
|
164
126
|
const value = nativeEvent[key];
|
|
165
127
|
return typeof value === 'number' ? value : 0;
|
|
166
128
|
}
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
* useAndroidRippleForView.js:77-104). The JS responder takes the touch before Android's own
|
|
170
|
-
* pressed handling, so without them the ripple never animates. Hotspot first: it starts under
|
|
171
|
-
* the finger.
|
|
172
|
-
*/
|
|
129
|
+
// The Android ripple's three view commands around the app's callbacks. The JS responder takes
|
|
130
|
+
// the touch before Android's own pressed handling, so without them the ripple never animates.
|
|
173
131
|
export function withNativeFeedbackCommands(node, config) {
|
|
174
132
|
const hotspot = (event) => {
|
|
175
133
|
dispatchViewCommand(node, 'hotspotUpdate', [
|
|
@@ -194,13 +152,9 @@ export function withNativeFeedbackCommands(node, config) {
|
|
|
194
152
|
},
|
|
195
153
|
};
|
|
196
154
|
}
|
|
197
|
-
// Rebuilding at GESTURE START is the whole reason for the dispatcher indirection
|
|
198
|
-
//
|
|
199
|
-
//
|
|
200
|
-
// capture no `onPress` at all and every press would silently do nothing. `createPressHandlers`
|
|
201
|
-
// destructures its config eagerly, so it cannot be handed a live view either; it has to be re-made
|
|
202
|
-
// once the props exist. A gesture is one interaction, so a handful of closures per press is
|
|
203
|
-
// invisible — unlike doing it per prop write, which is the cost this whole tier exists to remove.
|
|
155
|
+
// Rebuilding at GESTURE START is the whole reason for the dispatcher indirection: `attach` runs
|
|
156
|
+
// inside createElement before a single prop is routed, so a machine built at attach would capture
|
|
157
|
+
// no onPress at all. A gesture is one interaction, so a handful of closures per press is invisible.
|
|
204
158
|
function rebuild(node, state) {
|
|
205
159
|
// `state.source` for everything READ, `node` for the refinement, which acts on the responder
|
|
206
160
|
// (dispatching a view command needs the committed node, not the one holding the props).
|
|
@@ -223,13 +177,9 @@ function rebuild(node, state) {
|
|
|
223
177
|
blockNativeResponder: propOf(source, 'blockNativeResponder') === true,
|
|
224
178
|
});
|
|
225
179
|
}
|
|
226
|
-
// Pressable is the only member
|
|
227
|
-
//
|
|
228
|
-
//
|
|
229
|
-
// (`TouchableOpacity.js:186`, `TouchableHighlight.js:194`, `TouchableNativeFeedback.js:217`) — every
|
|
230
|
-
// composed touchable shares this `rebuild()`, so without this the authored name never reached the
|
|
231
|
-
// machine and every Touchable silently kept the RN native default (yield the responder) regardless of
|
|
232
|
-
// what the app asked for. An explicit `cancelable` still wins, matching Pressable's own precedence.
|
|
180
|
+
// Pressable is the only family member with its own `cancelable` prop; the Touchables instead
|
|
181
|
+
// expose `rejectResponderTermination` and derive cancelable internally. Without this the authored
|
|
182
|
+
// name never reaches the machine. An explicit `cancelable` still wins, matching Pressable's rule.
|
|
233
183
|
function resolveCancelable(source) {
|
|
234
184
|
const cancelable = propOf(source, 'cancelable');
|
|
235
185
|
if (typeof cancelable === 'boolean')
|
|
@@ -237,24 +187,17 @@ function resolveCancelable(source) {
|
|
|
237
187
|
const reject = propOf(source, 'rejectResponderTermination');
|
|
238
188
|
return typeof reject === 'boolean' ? !reject : undefined;
|
|
239
189
|
}
|
|
240
|
-
// WHICHEVER EVENT OPENS THE GESTURE REBUILDS
|
|
241
|
-
//
|
|
242
|
-
//
|
|
243
|
-
//
|
|
244
|
-
//
|
|
245
|
-
// Fabric, and `onPress` still fired because by then the machine existed. That is exactly the
|
|
246
|
-
// device report: the callback works, the button does not light up.
|
|
247
|
-
//
|
|
248
|
-
// So the trigger is a FLAG, not a name: build if this gesture has not built yet, and clear it when
|
|
249
|
-
// the gesture ends. Order-independent, and it survives the engine reordering its own events.
|
|
250
|
-
//
|
|
251
|
-
// A key `buildPressableListeners` omitted — every one of them when `disabled` is true — resolves to
|
|
252
|
-
// undefined here and the dispatcher returns undefined, which is what an absent listener would do.
|
|
190
|
+
// WHICHEVER EVENT OPENS THE GESTURE REBUILDS: `onStartShouldSetResponder` looks like the opener
|
|
191
|
+
// and isn't, since the engine bubbles PRESS_IN and only then negotiates the responder, so
|
|
192
|
+
// `pressIn` arrives first. Rebuilding only on the responder claim handed it an empty listener bag.
|
|
193
|
+
// The trigger is a FLAG, not a name: build if this gesture hasn't built yet, clear on gesture end
|
|
194
|
+
// — order-independent, survives the engine reordering its own events.
|
|
253
195
|
const GESTURE_END_KEYS = new Set([
|
|
254
196
|
'onPressOut',
|
|
255
197
|
'onResponderTerminationRequest',
|
|
256
198
|
]);
|
|
257
|
-
function dispatch(node,
|
|
199
|
+
function dispatch(node, options, key, args) {
|
|
200
|
+
const state = stateOf(node, options);
|
|
258
201
|
if (!state.isBuilt)
|
|
259
202
|
rebuild(node, state);
|
|
260
203
|
const listener = state.listeners[key];
|
|
@@ -267,31 +210,10 @@ function dispatch(node, state, key, args) {
|
|
|
267
210
|
state.isBuilt = false;
|
|
268
211
|
return result;
|
|
269
212
|
}
|
|
270
|
-
//
|
|
271
|
-
//
|
|
272
|
-
//
|
|
273
|
-
|
|
274
|
-
// The engine event names, not the `onX` prop spellings. `buildPressableListeners` speaks the prop
|
|
275
|
-
// spelling, so the two are mapped here rather than guessed at either end.
|
|
276
|
-
/**
|
|
277
|
-
* Engine event name -> the app-facing callback key its dispatcher routes to.
|
|
278
|
-
*
|
|
279
|
-
* AN ARRAY OF PAIRS RATHER THAN A `Map`, and the reason is measured rather than stylistic.
|
|
280
|
-
* `installListeners` below is the only reader, and it runs once per node that carries a press
|
|
281
|
-
* machine — which is every `<TextInput>` on the screen, not just every `<Pressable>`. A `for…of` over
|
|
282
|
-
* a `Map` builds a fresh two-element array per entry for the destructuring to read back: seven
|
|
283
|
-
* allocations per node, for seven fixed pairs that never change. The tuples here already exist, so
|
|
284
|
-
* the same loop allocates nothing but its iterator.
|
|
285
|
-
*
|
|
286
|
-
* Priced on `-O` Hermes by `text-input-attach-ladder.itest.ts`, one pass over the seven pairs:
|
|
287
|
-
* `Map` 1.04 us, this 0.39, two parallel arrays 0.29. The parallel arrays are cheapest and give up
|
|
288
|
-
* the pairing, which is not worth 0.1 us on a table that a drift would silently unwire.
|
|
289
|
-
*
|
|
290
|
-
* NOT the same list as `createPressBehavior`'s `ownedListeners`, and they must not be merged: that
|
|
291
|
-
* one is every name the machine takes as an INPUT (`pressMove` and `longPress` included), this one
|
|
292
|
-
* is only the names it installs a dispatcher for.
|
|
293
|
-
*/
|
|
294
|
-
const EVENT_KEY_PAIRS = [
|
|
213
|
+
// Engine event name -> the app-facing callback key it routes to. NOT the same list as
|
|
214
|
+
// `createPressBehavior`'s `ownedListeners`, which is every name the machine takes as an INPUT;
|
|
215
|
+
// this is only what it answers when Fabric fires
|
|
216
|
+
const DISPATCH_KEYS = new Map([
|
|
295
217
|
['press', 'onPress'],
|
|
296
218
|
['pressIn', 'onPressIn'],
|
|
297
219
|
['pressOut', 'onPressOut'],
|
|
@@ -299,30 +221,55 @@ const EVENT_KEY_PAIRS = [
|
|
|
299
221
|
['responderMove', 'onResponderMove'],
|
|
300
222
|
['responderTerminationRequest', 'onResponderTerminationRequest'],
|
|
301
223
|
['responderGrant', 'onResponderGrant'],
|
|
302
|
-
];
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
224
|
+
]);
|
|
225
|
+
// The per-node half of what the seven dispatcher closures used to capture. A behavior that
|
|
226
|
+
// registered no refinement leaves its nodes out of here entirely
|
|
227
|
+
const attached = new WeakMap();
|
|
228
|
+
// ONE object for every pressable in the app, where seven closures plus the `Map` holding them used
|
|
229
|
+
// to sit per node. `attach` runs inside `createElement`, so what it allocates every list item pays
|
|
230
|
+
// Exported for a tag that is a pressable PLUS something with names of its own (`./text-input`): it
|
|
231
|
+
// composes a union and delegates here, since a node holds exactly one dispatch
|
|
232
|
+
export const PRESS_DISPATCH = {
|
|
233
|
+
names: new Set(DISPATCH_KEYS.keys()),
|
|
234
|
+
deliver(node, name, event) {
|
|
235
|
+
const key = DISPATCH_KEYS.get(name);
|
|
236
|
+
if (key === undefined)
|
|
237
|
+
return undefined;
|
|
238
|
+
return dispatch(node, attached.get(node) ?? NO_OPTIONS, key, [event]);
|
|
239
|
+
},
|
|
240
|
+
};
|
|
308
241
|
function attachWith(refine, disabledOf) {
|
|
309
|
-
|
|
242
|
+
// One bag per BEHAVIOR, not per node: `refine` and `disabledOf` are fixed at registration, and a
|
|
243
|
+
// behavior with neither shares the empty one so a plain `<pressable>` stores nothing
|
|
244
|
+
const options = refine === undefined && disabledOf === undefined
|
|
245
|
+
? NO_OPTIONS
|
|
246
|
+
: { refine, disabledOf };
|
|
247
|
+
return node => attach(node, options);
|
|
310
248
|
}
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
*
|
|
319
|
-
* Re-callable on the same node: a second call replaces the state and the dispatchers, which is what
|
|
320
|
-
* a re-arm after `detachPressMachine` needs.
|
|
321
|
-
*/
|
|
322
|
-
export function attachPressMachine(node, options = {}) {
|
|
249
|
+
const NO_OPTIONS = {};
|
|
250
|
+
// The machine on `node`, reading props and callbacks off `options.source` when different.
|
|
251
|
+
// Exported for touchable-native-feedback, whose tag commits nothing and adopts the app's single
|
|
252
|
+
// child as the responder; every other caller goes through createPressBehavior where they're equal.
|
|
253
|
+
// Re-callable on the same node: a second call replaces the state and dispatchers, which is what
|
|
254
|
+
// re-arming after detachPressMachine needs.
|
|
255
|
+
export function attachPressMachine(node, options = NO_OPTIONS) {
|
|
323
256
|
attach(node, options);
|
|
324
257
|
}
|
|
258
|
+
// One pointer, plus one map slot for a refined behavior: everything below is read from `dispatch`
|
|
259
|
+
// alone, so a pressable nobody touches never builds a gesture runtime
|
|
260
|
+
// A re-attach drops the old state so the new options take effect on the next gesture
|
|
325
261
|
function attach(node, options) {
|
|
262
|
+
states.delete(node);
|
|
263
|
+
if (options === NO_OPTIONS)
|
|
264
|
+
attached.delete(node);
|
|
265
|
+
else
|
|
266
|
+
attached.set(node, options);
|
|
267
|
+
setNodeDispatch(node, PRESS_DISPATCH);
|
|
268
|
+
}
|
|
269
|
+
function stateOf(node, options) {
|
|
270
|
+
const existing = states.get(node);
|
|
271
|
+
if (existing !== undefined)
|
|
272
|
+
return existing;
|
|
326
273
|
const timers = new Set();
|
|
327
274
|
const runtime = createPressRuntime();
|
|
328
275
|
const host = {
|
|
@@ -330,10 +277,8 @@ function attach(node, options) {
|
|
|
330
277
|
// through the style registry's `:active` variant, and never crosses into it.
|
|
331
278
|
setPressed: pressed => {
|
|
332
279
|
setNodePressed(node, pressed);
|
|
333
|
-
// Dirtying is not publishing
|
|
334
|
-
//
|
|
335
|
-
// adapter does either. `setNodeHidden`'s React twin never hit this because the reconciler is
|
|
336
|
-
// already in its commit phase when it calls.
|
|
280
|
+
// Dirtying is not publishing: a press arrives outside every renderer mutation path, so
|
|
281
|
+
// nothing else schedules a commit.
|
|
337
282
|
requestCommitFor(node);
|
|
338
283
|
},
|
|
339
284
|
// `measure` needs a committed Fabric tag, which a node has by the time a human can touch it.
|
|
@@ -367,20 +312,21 @@ function attach(node, options) {
|
|
|
367
312
|
isBuilt: false,
|
|
368
313
|
};
|
|
369
314
|
states.set(node, state);
|
|
370
|
-
|
|
315
|
+
return state;
|
|
371
316
|
}
|
|
372
317
|
/** See `attachPressMachine`: the same teardown `createPressBehavior` registers as its `detach`. */
|
|
373
318
|
export function detachPressMachine(node) {
|
|
374
319
|
detach(node);
|
|
375
320
|
}
|
|
376
321
|
function detach(node) {
|
|
322
|
+
setNodeDispatch(node, undefined);
|
|
323
|
+
attached.delete(node);
|
|
377
324
|
const state = states.get(node);
|
|
325
|
+
// A pressable nobody touched has no state at all, and the two releases above are all it owes
|
|
378
326
|
if (state === undefined)
|
|
379
327
|
return;
|
|
380
|
-
// The machine's own teardown
|
|
381
|
-
//
|
|
382
|
-
// `state.timers` — the 130ms floor's deferred `pressOut` included — so the loop below already
|
|
383
|
-
// cancels them. Kept as the contract, and for a timer armed by some future route.
|
|
328
|
+
// The machine's own teardown. Not load-bearing here: host.schedule puts every timer the machine
|
|
329
|
+
// arms into state.timers, so the loop below already cancels them — kept as the contract.
|
|
384
330
|
disposePressRuntime(state.runtime);
|
|
385
331
|
for (const id of state.timers)
|
|
386
332
|
clearTimeout(id);
|
|
@@ -388,15 +334,9 @@ function detach(node) {
|
|
|
388
334
|
states.delete(node);
|
|
389
335
|
dlog('pressable behavior detached');
|
|
390
336
|
}
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
*
|
|
395
|
-
* Spread into the caller's own behavior and wrap `attach`/`detach` around these — the touchable
|
|
396
|
-
* family needs a per-node Animated value opened before the machine and closed after it. The
|
|
397
|
-
* WeakMap holding the machine's own state is keyed by node, so one node may hold exactly one of
|
|
398
|
-
* these; a tag composing it therefore must not also register the plain `pressable` behavior.
|
|
399
|
-
*/
|
|
337
|
+
// The press machine as behavior parts, so a tag that is a pressable PLUS something can compose
|
|
338
|
+
// it instead of re-implementing it — the touchable family wraps attach/detach for its own
|
|
339
|
+
// per-node Animated value. One node may hold exactly one of these; don't also register `pressable`.
|
|
400
340
|
export function createPressBehavior(refine, disabledOf) {
|
|
401
341
|
return {
|
|
402
342
|
attach: attachWith(refine, disabledOf),
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
// `behaviors/switch.ts`'s module header, verbatim. Same shape, same two triggers, same reasons —
|
|
30
30
|
// an ACCEPTING app's state reaches `node.props` only after its own reconciliation, and a REJECTING
|
|
31
31
|
// app writes no prop at all, so the commit that `afterCommit` waits for never comes.
|
|
32
|
-
import { appListenerFor, dispatchViewCommand, dlog, registerHostBehavior,
|
|
32
|
+
import { appListenerFor, dispatchViewCommand, dlog, registerHostBehavior, setNodeDispatch, propOf, } from '@symbiote-native/engine';
|
|
33
33
|
export const REFRESH_CONTROL_TAG = 'refresh-control';
|
|
34
34
|
// RN's own name for the command, sent to whichever of the two native views the platform resolved.
|
|
35
35
|
const SET_NATIVE_REFRESHING = 'setNativeRefreshing';
|
|
@@ -75,10 +75,16 @@ function onRefresh(node, event) {
|
|
|
75
75
|
listener(event);
|
|
76
76
|
queueMicrotask(() => evaluateSnapBack(node));
|
|
77
77
|
}
|
|
78
|
+
// ONE object for every RefreshControl in the app, where a closure per node used to be
|
|
79
|
+
const REFRESH_DISPATCH = {
|
|
80
|
+
names: new Set(['refresh']),
|
|
81
|
+
deliver: (node, _name, event) => onRefresh(node, event),
|
|
82
|
+
};
|
|
78
83
|
function attach(node) {
|
|
79
|
-
|
|
84
|
+
setNodeDispatch(node, REFRESH_DISPATCH);
|
|
80
85
|
}
|
|
81
86
|
function detach(node) {
|
|
87
|
+
setNodeDispatch(node, undefined);
|
|
82
88
|
reported.delete(node);
|
|
83
89
|
}
|
|
84
90
|
// Idempotent: an adapter entry may be imported more than once in a bundle, and re-registering the
|
|
@@ -1,35 +1,15 @@
|
|
|
1
1
|
// ScrollView's behavior on Android, where a RefreshControl is not a child at all.
|
|
2
|
-
//
|
|
3
2
|
// An Android ScrollView holds exactly ONE child, so a sibling refresh control is an `addViewAt`
|
|
4
|
-
// crash
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
// the child
|
|
14
|
-
//
|
|
15
|
-
// WHY THEY RESISTED THREE ITERATIONS, and what changed. Every seam the engine had read UP —
|
|
16
|
-
// `ownerProps`, `IOwner.tagName`, `IAncestorLookup` — and the wrapper is the scroll view's PARENT
|
|
17
|
-
// asking for the scroll view's style, the one direction none of them go. `IFirstChild` is that
|
|
18
|
-
// direction, and it is the same argument `ownerProps` already made rather than a new one: the tree
|
|
19
|
-
// lives in C++, so reading another node is a pointer hop, and RN itself builds this parent FROM its
|
|
20
|
-
// child (`cloneElement(refreshControl, {style: outer}, scrollView)`).
|
|
21
|
-
//
|
|
22
|
-
// `slotDerived: ['style']` STAYS AND IS NOW LOAD-BEARING FOR THE ENGINE'S RULE. A rule re-runs when
|
|
23
|
-
// ITS node is dirty; the wrapper derives from a node that is not itself, so an owner style write
|
|
24
|
-
// must mark it (`routeProp`'s `node.wrapper` branch). Without this entry the wrapper freezes at its
|
|
25
|
-
// mount frame while the scroller visibly restyles inside it —
|
|
26
|
-
// `core/engine/cpp/tests/js/scroll-view-wrap-payload.itest.ts` is the case that says so.
|
|
27
|
-
//
|
|
28
|
-
// AND NOTHING HERE IS `#ifdef ANDROID` OR `Platform.OS`, on either side: the engine's rules are
|
|
29
|
-
// gated on TOPOLOGY. iOS claims the refresh control BESIDE the content, so a scroll view is never
|
|
30
|
-
// one's child there and neither branch can fire. That is strictly better than a compile-time split
|
|
31
|
-
// for the reason `Switch`/`AndroidSwitch` already showed — and it is why the wrap's payload fixture
|
|
32
|
-
// runs on the ORDINARY test host rather than needing the Android arm.
|
|
3
|
+
// crash. RN inverts the tree instead: `AndroidSwipeRefreshLayout` WRAPS the scroll view
|
|
4
|
+
// (`ScrollView.js:1856`) — layout style on the wrapper, visual style on the scroller.
|
|
5
|
+
// This file is a CLAIM MODE and nothing else: both prop folds are `SymbioteFabricProps.cpp` now —
|
|
6
|
+
// `foldScrollViewProps` takes the visual half when its parent is a refresh control,
|
|
7
|
+
// `foldRefreshWrapperProps` the layout half of the child it wraps.
|
|
8
|
+
// `slotDerived: ['style']` is load-bearing: the wrapper derives from a node that is not itself, so
|
|
9
|
+
// an owner style write must mark IT dirty (`routeProp`'s `node.wrapper` branch) or it freezes at
|
|
10
|
+
// its mount frame while the scroller visibly restyles (`scroll-view-wrap-payload.itest.ts`).
|
|
11
|
+
// Gated on TOPOLOGY, not `#ifdef ANDROID`/`Platform.OS`: iOS claims the refresh control BESIDE
|
|
12
|
+
// the content, so a scroll view is never one's child there and neither branch can fire.
|
|
33
13
|
import { registerScrollViewBehaviors } from './shared.js';
|
|
34
14
|
const android = {
|
|
35
15
|
claimMode: 'wrap',
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { type ISymbioteNode } from '@symbiote-native/engine';
|
|
1
|
+
import { type ISymbioteEvent, type ISymbioteNode } from '@symbiote-native/engine';
|
|
2
2
|
export declare function markScrollObserved(owner: ISymbioteNode): void;
|
|
3
3
|
export declare const RESPONDER_OWNED_LISTENERS: readonly string[];
|
|
4
|
-
export declare
|
|
4
|
+
export declare const RESPONDER_DISPATCHED_NAMES: ReadonlySet<string>;
|
|
5
|
+
export declare function deliverResponderEvent(owner: ISymbioteNode, name: string, event: ISymbioteEvent): unknown;
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
// Ported line-for-line from `ScrollView.js:1263-1546` — the negotiation engine itself and the
|
|
6
6
|
// `IResponderProps` prop surface already exist and needed no change; this file is ScrollView opting
|
|
7
7
|
// into the same mechanism Pressable already uses (`../pressable.ts:329-384`).
|
|
8
|
-
import { appListenerFor, currentlyFocusedInput, blurTextInput, dlog, isDebug, isRecord, isSymbioteNode, Keyboard, Platform, propOf,
|
|
8
|
+
import { appListenerFor, currentlyFocusedInput, blurTextInput, dlog, isDebug, isRecord, isSymbioteNode, Keyboard, Platform, propOf, } from '@symbiote-native/engine';
|
|
9
9
|
import { descriptorFor } from '../../component-names';
|
|
10
10
|
import { TEXT_INPUT_MULTILINE_TAG, TEXT_INPUT_TAG } from '../text-input.js';
|
|
11
11
|
// RN's `IS_ANIMATING_TOUCH_START_THRESHOLD_MS` (ScrollView.js:695) — a momentum end inside this
|
|
@@ -208,24 +208,22 @@ export const RESPONDER_OWNED_LISTENERS = [
|
|
|
208
208
|
'momentumScrollBegin',
|
|
209
209
|
'momentumScrollEnd',
|
|
210
210
|
];
|
|
211
|
-
//
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
handleMomentumScrollEnd(owner, event);
|
|
230
|
-
});
|
|
211
|
+
// The eight names an owner answers, as ONE module-level table where eight closures per node used to
|
|
212
|
+
// be. `./shared` composes it with its own `scroll` into the owner's `IEventDispatch`
|
|
213
|
+
const HANDLERS = new Map([
|
|
214
|
+
['startShouldSetResponderCapture', startShouldSetResponderCapture],
|
|
215
|
+
['startShouldSetResponder', startShouldSetResponder],
|
|
216
|
+
[
|
|
217
|
+
'responderTerminationRequest',
|
|
218
|
+
(owner) => responderTerminationRequest(owner),
|
|
219
|
+
],
|
|
220
|
+
['responderGrant', handleResponderGrant],
|
|
221
|
+
['responderRelease', handleResponderRelease],
|
|
222
|
+
['momentumScrollBegin', handleMomentumScrollBegin],
|
|
223
|
+
['scrollBeginDrag', handleScrollBeginDrag],
|
|
224
|
+
['momentumScrollEnd', handleMomentumScrollEnd],
|
|
225
|
+
]);
|
|
226
|
+
export const RESPONDER_DISPATCHED_NAMES = new Set(HANDLERS.keys());
|
|
227
|
+
export function deliverResponderEvent(owner, name, event) {
|
|
228
|
+
return HANDLERS.get(name)?.(owner, event);
|
|
231
229
|
}
|