@symbiote-native/components 3.1.0 → 3.1.2
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 +17 -18
- package/build/behaviors/activity-indicator/shared.js +29 -71
- package/build/behaviors/button.d.ts +0 -9
- package/build/behaviors/button.js +48 -234
- 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 +0 -48
- package/build/behaviors/pressable.js +59 -149
- package/build/behaviors/scroll-view/index.android.js +10 -30
- package/build/behaviors/scroll-view/shared.js +55 -186
- package/build/behaviors/scroll-view/sticky.d.ts +0 -8
- package/build/behaviors/scroll-view/sticky.js +54 -142
- package/build/behaviors/text-input.d.ts +0 -8
- package/build/behaviors/text-input.js +57 -156
- package/build/behaviors/touchable-highlight.js +14 -54
- package/build/behaviors/touchable-native-feedback.js +9 -32
- package/build/behaviors/touchable-opacity.js +3 -18
- package/build/behaviors/touchable-without-feedback.js +6 -24
- package/build/bootstrap/index.d.ts +1 -0
- package/build/bootstrap/index.js +2 -1
- 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 +9 -17
- 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 -53
- 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,49 +1,19 @@
|
|
|
1
|
-
// Sticky headers
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
// `reconcileStickyRecords` recovers it: a paint index that skips
|
|
18
|
-
// anchors.
|
|
19
|
-
// "no render to wrap anything in" a behavior builds nodes with the ordinary mutation API.
|
|
20
|
-
// `wrapForIndex` creates a node, inserts it at the slot and
|
|
21
|
-
// `appendChild`s the child into it — the engine's appendChild
|
|
22
|
-
// detaches from the old parent, so no removal is needed.
|
|
23
|
-
//
|
|
24
|
-
// The wrap is what must NOT be skipped. Writing the pin straight onto the flagged child is the
|
|
25
|
-
// tempting shortcut and it destroys the child's own `transform`: `fabricProps.addStyle` hoists
|
|
26
|
-
// style keys into one payload and later entries WIN, so a pin composed over an app's
|
|
27
|
-
// `transform: [{scale}]` replaces it rather than composing. RN's two nested views compose.
|
|
28
|
-
//
|
|
29
|
-
// WHAT RUNS WHERE. The DECISIONS are `reduceSticky` (`../../state/sticky-header-reducer`), shared
|
|
30
|
-
// with every adapter's own sticky component and untouched here. This module is one more EFFECT
|
|
31
|
-
// RUNNER for it — the fourth, after React/Vue/Svelte's components and Angular's projection
|
|
32
|
-
// controller — and the only one that runs on an engine node with no framework above it. The
|
|
33
|
-
// runner's shape is Angular's (`adapters/angular/src/components/scroll-view/projection.ts`),
|
|
34
|
-
// because that one already drives engine nodes; what changes is where the cross-talk comes from.
|
|
35
|
-
//
|
|
36
|
-
// CROSS-TALK WITHOUT INDICES. Each header is fed the y of the NEXT sticky header, which is the
|
|
37
|
-
// collision point it gets pushed off at. Every existing runner reads that out of an index map
|
|
38
|
-
// (`nextStickyHeaderY(childIndex)`), because indices are what it has. Here the owner keeps its
|
|
39
|
-
// headers in DOCUMENT order and the next one is the next entry — no index, and nothing to renumber
|
|
40
|
-
// when a list windows.
|
|
41
|
-
//
|
|
42
|
-
// THE OWNER HALF, and it is why this file holds both. Three of the scroll view's own props are
|
|
43
|
-
// functions of "does this ScrollView have sticky headers", which only a registration can answer:
|
|
44
|
-
// `scrollEventThrottle` (RN raises it so the offset reaches the AnimatedValue at all), the scroll
|
|
45
|
-
// listener that drives that value, and — inverted only — the viewport height the pin math needs.
|
|
46
|
-
// A separate module would have to export a registry back and forth.
|
|
1
|
+
// Sticky headers, both forms: the CHILD form (`<sticky-header>`) and the INDEX form
|
|
2
|
+
// (`stickyHeaderIndices`, RN's public API), plus the owner-side half feeding them.
|
|
3
|
+
// `<sticky-header>` marks an element the way an index list would in JSX-less code — reading the
|
|
4
|
+
// tag needs no index at all.
|
|
5
|
+
// The index form resolves via `afterCommit` seeing `owner.childHost.children` complete on every
|
|
6
|
+
// commit with a native call. `wrapForIndex` builds a WRAPPING node so the pin composes onto it
|
|
7
|
+
// instead of overwriting the child's own `transform` (`fabricProps.addStyle`: later entries win).
|
|
8
|
+
// DECISIONS are reduceSticky (../../state/sticky-header-reducer), shared with every adapter's own
|
|
9
|
+
// sticky component; this module is one more EFFECT RUNNER for it, running on an engine node with
|
|
10
|
+
// no framework above it.
|
|
11
|
+
// Cross-talk without indices: each header is fed the y of the NEXT sticky header, the collision
|
|
12
|
+
// point it gets pushed off at. The owner keeps its headers in DOCUMENT order, so the next one is
|
|
13
|
+
// just the next entry — no index, nothing to renumber when a list windows.
|
|
14
|
+
// THE OWNER HALF, why this file holds both: three of the scroll view's own props are functions of
|
|
15
|
+
// "does this ScrollView have sticky headers", which only a registration can answer — a separate
|
|
16
|
+
// module would have to export a registry back and forth.
|
|
47
17
|
import { AnimatedProps, AnimatedValue, appendChild, appListenerFor, createElement, dlog, insertBefore, isAnchor, isNativeAnimatedAvailable, Platform, removeChild, requestCommitFor, setBehaviorListener, setProp, whenCommitted, propOf, childrenOf, parentOf, } from '@symbiote-native/engine';
|
|
48
18
|
import { descriptorFor } from '../../component-names';
|
|
49
19
|
import { attachStickyScroll } from '../../scroll-view-commands.js';
|
|
@@ -52,10 +22,8 @@ import { createInitialStickyState, reduceSticky, } from '../../state/sticky-head
|
|
|
52
22
|
import { readLayoutNumber, STICKY_HEADER_Z_INDEX, } from '../../view/render-scroll-sticky.js';
|
|
53
23
|
import { resolveScrollForwarding } from '../../view/render-scroll-view.js';
|
|
54
24
|
export const STICKY_HEADER_TAG = 'sticky-header';
|
|
55
|
-
// The machine's one channel to its tag rule, and the only prop it ever writes. RN's twin is
|
|
56
|
-
//
|
|
57
|
-
// object; ours carries the one number that object ever holds, so it does not borrow the name.
|
|
58
|
-
// `foldStickyHeaderProps` composes it into the style and strips the key — no ViewConfig declares it.
|
|
25
|
+
// The machine's one channel to its tag rule, and the only prop it ever writes. RN's twin is a
|
|
26
|
+
// whole style object; ours carries the one number that object ever holds.
|
|
59
27
|
export const STICKY_TRANSLATE_PROP = 'stickyTranslateY';
|
|
60
28
|
// The scroll views that could own a header, so a header can find its own by walking up. The tag is
|
|
61
29
|
// not on the node (`createElement` looks the behavior up and stores nothing), and the parent chain
|
|
@@ -93,18 +61,12 @@ function ownerSticky(owner) {
|
|
|
93
61
|
stickyOwners.set(owner, created);
|
|
94
62
|
return created;
|
|
95
63
|
}
|
|
96
|
-
// Put the scroll offset on the UI THREAD,
|
|
97
|
-
//
|
|
98
|
-
//
|
|
99
|
-
//
|
|
100
|
-
//
|
|
101
|
-
//
|
|
102
|
-
// into place only once the scroll stopped, which is the JS thread catching up.
|
|
103
|
-
//
|
|
104
|
-
// The interpolation listeners survive this. A tick is not the MOVEMENT — the AnimatedProps leaf
|
|
105
|
-
// owns that — it is the settled value the reducer debounces into the committed transform, RN's
|
|
106
|
-
// `passthroughAnimatedPropExplicitValues`, and a native value still streams to JS while a listener
|
|
107
|
-
// is registered (`AnimatedValue.__makeNative`).
|
|
64
|
+
// Put the scroll offset on the UI THREAD, what every wrapper's ScrollView already does. Without
|
|
65
|
+
// it the offset only reaches scrollValue once per delivered JS scroll event, so during a flick
|
|
66
|
+
// the header snaps into place only once the JS thread catches up, instead of tracking it live.
|
|
67
|
+
// The interpolation listeners survive this: a tick is not the MOVEMENT (the AnimatedProps leaf
|
|
68
|
+
// owns that), it's the settled value the reducer debounces, and a native value still streams to
|
|
69
|
+
// JS while a listener is registered.
|
|
108
70
|
function syncNativeScroll(owner, sticky) {
|
|
109
71
|
const wanted = sticky.members.size > 0 && isNativeAnimatedAvailable();
|
|
110
72
|
if (wanted === (sticky.detachNativeScroll !== undefined))
|
|
@@ -134,15 +96,8 @@ function orderedHeaders(owner, sticky) {
|
|
|
134
96
|
return out;
|
|
135
97
|
}
|
|
136
98
|
// RN raises the scroll event rate for sticky headers so the offset actually reaches the
|
|
137
|
-
// AnimatedValue
|
|
138
|
-
//
|
|
139
|
-
//
|
|
140
|
-
// This used to force `nativeStickyAvailable: false`, on the reasoning that a scroll value made
|
|
141
|
-
// native up front would cut the child listener cascade before a single tick arrived. It was wrong
|
|
142
|
-
// on both halves and it cost a visibly broken pin: no adapter forces that false — every wrapper
|
|
143
|
-
// calls `attachStickyScroll` — and a tick is not what MOVES the header anyway (`syncNativeScroll`).
|
|
144
|
-
// The attach happens after the header has registered, which is the same ordering React's
|
|
145
|
-
// `useEffect` gives it.
|
|
99
|
+
// AnimatedValue. An app value always wins, which is why this reads resolveScrollForwarding rather
|
|
100
|
+
// than a constant.
|
|
146
101
|
function syncThrottle(owner, sticky) {
|
|
147
102
|
const current = propOf(owner, 'scrollEventThrottle');
|
|
148
103
|
// Whatever stands in the key is the APP's unless it is byte-for-byte the value written here —
|
|
@@ -196,9 +151,8 @@ function readContentOffsetY(event) {
|
|
|
196
151
|
return typeof y === 'number' ? y : undefined;
|
|
197
152
|
}
|
|
198
153
|
// The owner's scroll dispatcher: drive the shared value, then hand the app its event. Installed
|
|
199
|
-
// unconditionally
|
|
200
|
-
//
|
|
201
|
-
// listener slot.
|
|
154
|
+
// unconditionally — the app's own `onScroll` is an OWNED name and would otherwise evict this one
|
|
155
|
+
// from the single listener slot.
|
|
202
156
|
export function handleOwnerScroll(owner, event) {
|
|
203
157
|
// RN's `_handleScroll` (`ScrollView.js:1145-1147`) sets this unconditionally too — nothing reads
|
|
204
158
|
// it unless this node actually holds the responder, so an unconditional set on every scroll is
|
|
@@ -236,33 +190,16 @@ export function releaseStickyOwner(owner) {
|
|
|
236
190
|
stickyOwners.delete(owner);
|
|
237
191
|
}
|
|
238
192
|
// ---------------------------------------------------------------- the index form
|
|
239
|
-
// `stickyHeaderIndices
|
|
240
|
-
//
|
|
241
|
-
//
|
|
242
|
-
//
|
|
243
|
-
//
|
|
244
|
-
//
|
|
245
|
-
//
|
|
246
|
-
//
|
|
247
|
-
//
|
|
248
|
-
//
|
|
249
|
-
// component paths today, so this is a real narrowing for them; an app that needs one composes it
|
|
250
|
-
// explicitly around a `<sticky-header>` instead.
|
|
251
|
-
//
|
|
252
|
-
// THE WRAP LANDS ONE COMMIT LATE. `afterCommit` is the only hook that sees `owner.childHost.children`
|
|
253
|
-
// complete, and it runs past `completeRoot` — so a flagged child paints unwrapped for the frame it
|
|
254
|
-
// mounts in and pins from the next commit. Angular escapes this only through a synchronous flush at
|
|
255
|
-
// `RendererFactory2.end()`, a seam a behavior does not have. Pinned by `sticky-indices.test.ts` so
|
|
256
|
-
// it is learned from a green assertion rather than rediscovered on a device.
|
|
257
|
-
//
|
|
258
|
-
// UNSORTED INDICES RESOLVE BY DOCUMENT ORDER, which is what `orderedHeaders` already gives — and it
|
|
259
|
-
// is a choice, because the two existing runners disagree: React reads the next header out of the
|
|
260
|
-
// ARRAY (`ScrollView.js:1695`, `indexOf(index) + 1`), Angular out of the sorted list
|
|
261
|
-
// (`find(entry > index)`). Document order is the one that stays correct, because the value is a
|
|
262
|
-
// COLLISION POINT — the y of the header that pushes this one off — so it has to be the header BELOW
|
|
263
|
-
// on screen; `[2, 0]` under React's rule feeds header 2 the y of a header above it. It is also the
|
|
264
|
-
// only ordering under which the two forms can share a scroll view, since both produce members of
|
|
265
|
-
// one set ordered by where they sit.
|
|
193
|
+
// `stickyHeaderIndices` is deliberately NOT a second machine: a flagged child is MOVED into a
|
|
194
|
+
// synthesized `sticky-header` node, so ordering, cross-talk, throttle, pin and teardown are all
|
|
195
|
+
// the child form's, unchanged. Indices decide only WHICH children get one.
|
|
196
|
+
// `StickyHeaderComponent` (RN's custom wrapper prop) is NOT honoured — a behavior can't instantiate
|
|
197
|
+
// a framework component; an app that needs one composes it explicitly around `<sticky-header>`.
|
|
198
|
+
// The wrap lands one commit late: afterCommit is the only hook seeing childHost.children complete,
|
|
199
|
+
// and it runs past completeRoot, so a flagged child paints unwrapped for one frame.
|
|
200
|
+
// Unsorted indices resolve by DOCUMENT ORDER, which orderedHeaders already gives: the value is a
|
|
201
|
+
// COLLISION POINT (the y of the header that pushes this one off), so it must be the header BELOW
|
|
202
|
+
// on screen — document order is also the only one under which both forms can share a scroll view.
|
|
266
203
|
// The nodes this module synthesized, so a later walk can tell its own wrapper from an app's child.
|
|
267
204
|
const indexWrappers = new WeakSet();
|
|
268
205
|
// Owners currently holding one. The gate: a ScrollView that never used the prop pays one WeakSet
|
|
@@ -285,11 +222,9 @@ function wrapForIndex(slot, child) {
|
|
|
285
222
|
// so the wrapper takes the position the child vacates and nothing has to be removed.
|
|
286
223
|
insertBefore(slot, wrapper, child);
|
|
287
224
|
appendChild(wrapper, child);
|
|
288
|
-
// AFTER both, or the anchor above would resolve to the wrapper itself
|
|
289
|
-
//
|
|
290
|
-
//
|
|
291
|
-
// holds the wrapper in the row's place — so a later `removeChild(owner, row)` takes the wrapper
|
|
292
|
-
// out with it instead of being refused for naming a parent the row no longer has.
|
|
225
|
+
// AFTER both, or the anchor above would resolve to the wrapper itself: the framework keeps
|
|
226
|
+
// naming the row, while the tree holds the wrapper in its place, so a later removeChild(owner,
|
|
227
|
+
// row) takes the wrapper out with it.
|
|
293
228
|
child.wrapper = wrapper;
|
|
294
229
|
}
|
|
295
230
|
function unwrapIndex(slot, wrapper) {
|
|
@@ -302,14 +237,9 @@ function unwrapIndex(slot, wrapper) {
|
|
|
302
237
|
insertBefore(slot, child, wrapper);
|
|
303
238
|
removeChild(slot, wrapper);
|
|
304
239
|
}
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
*
|
|
309
|
-
* O(slot children) per commit, once — never per mutation. Angular's controller coalesces to one
|
|
310
|
-
* pass per change detection for exactly this reason: its per-mutation walk was O(M²) and died at
|
|
311
|
-
* 801 children.
|
|
312
|
-
*/
|
|
240
|
+
// Bring the synthesized wrappers in line with `stickyHeaderIndices`. Called from afterCommit, the
|
|
241
|
+
// one beat where the app's children are all present. O(slot children) per commit, once, never
|
|
242
|
+
// per mutation.
|
|
313
243
|
export function reconcileStickyIndices(owner) {
|
|
314
244
|
const slot = owner.childHost;
|
|
315
245
|
if (slot === undefined)
|
|
@@ -320,10 +250,8 @@ export function reconcileStickyIndices(owner) {
|
|
|
320
250
|
let paintIndex = 0;
|
|
321
251
|
let wrapped = 0;
|
|
322
252
|
let changed = false;
|
|
323
|
-
// Snapshot: wrapping and unwrapping both splice the list being walked.
|
|
324
|
-
//
|
|
325
|
-
// A claimed `<RefreshControl>` needs no filter here, unlike Angular's walk — `hostFor` keeps a
|
|
326
|
-
// claimed child on the OWNER, so it never reaches the slot at all.
|
|
253
|
+
// Snapshot: wrapping and unwrapping both splice the list being walked. A claimed
|
|
254
|
+
// `<RefreshControl>` needs no filter here — hostFor keeps it on the OWNER, never the slot.
|
|
327
255
|
for (const child of [...childrenOf(slot)]) {
|
|
328
256
|
const wrapper = indexWrappers.has(child) ? child : undefined;
|
|
329
257
|
if (wrapper !== undefined) {
|
|
@@ -391,16 +319,9 @@ function nextHeaderY(runtime, node) {
|
|
|
391
319
|
const next = order[order.indexOf(node) + 1];
|
|
392
320
|
return next === undefined ? undefined : sticky.layoutYs.get(next);
|
|
393
321
|
}
|
|
394
|
-
//
|
|
395
|
-
//
|
|
396
|
-
//
|
|
397
|
-
// are constants of the wrapper — the platform's in any app — and the debounced translate is the
|
|
398
|
-
// machine's. Splitting them that way is what let the whole rule move: `foldStickyHeaderProps` in
|
|
399
|
-
// `SymbioteFabricProps.cpp` owns the composition now, and the one live number crosses as an
|
|
400
|
-
// ordinary prop (`STICKY_TRANSLATE_PROP`), which is how RN spells it too.
|
|
401
|
-
//
|
|
402
|
-
// Contract: `core/engine/cpp/tests/js/sticky-header-payload.itest.ts`. There is no JS twin — a
|
|
403
|
-
// payload rule asserted against a second copy of itself is asserted against nothing.
|
|
322
|
+
// foldStickyHeaderProps in C++ owns the wrapper's constants; the debounced translate crosses as
|
|
323
|
+
// an ordinary prop. No JS twin — a payload rule asserted against a second copy of itself proves
|
|
324
|
+
// nothing, so sticky-header-payload.itest.ts is the contract.
|
|
404
325
|
function dispatch(node, action) {
|
|
405
326
|
const runtime = headerRuntimes.get(node);
|
|
406
327
|
if (runtime === undefined || runtime.owner === undefined)
|
|
@@ -429,20 +350,11 @@ function runEffects(node, runtime, effects) {
|
|
|
429
350
|
}, effect.delay);
|
|
430
351
|
break;
|
|
431
352
|
case 'apply-passthrough':
|
|
432
|
-
// The machine's one channel to its tag rule
|
|
433
|
-
//
|
|
434
|
-
//
|
|
435
|
-
//
|
|
436
|
-
//
|
|
437
|
-
// The write marks the node itself, so only the commit request is still owed — dirtying is
|
|
438
|
-
// not publishing.
|
|
439
|
-
//
|
|
440
|
-
// WHY A COMMIT IS REQUESTED AT ALL, and it is NOT witnessed by a headless test: while the
|
|
441
|
-
// pin is JS-driven the animated leaf's own `setNativeProps` has already written the same
|
|
442
|
-
// transform and marked the node, so removing this reddens nothing here. It is the
|
|
443
|
-
// NATIVE-driver path it exists for — there the leaf stops writing JS-side and the committed
|
|
444
|
-
// transform is all hit-testing has, which is why RN keeps that explicit value beside the
|
|
445
|
-
// animated one.
|
|
353
|
+
// The machine's one channel to its tag rule: a prop write is what the C++ rule can read.
|
|
354
|
+
// The write marks the node, so only the commit request is still owed.
|
|
355
|
+
// Not witnessed by a headless test: while the pin is JS-driven the animated leaf's own
|
|
356
|
+
// setNativeProps already wrote the same transform. It's the NATIVE-driver path this exists
|
|
357
|
+
// for, where the committed transform is all hit-testing has.
|
|
446
358
|
setProp(node, STICKY_TRANSLATE_PROP, effect.translateY);
|
|
447
359
|
requestCommitFor(node);
|
|
448
360
|
break;
|
|
@@ -2,13 +2,5 @@ import { type ISymbioteNode } from '@symbiote-native/engine';
|
|
|
2
2
|
import { type ITextInputHandle } from '../state/text-input';
|
|
3
3
|
export declare const TEXT_INPUT_TAG = "text-input";
|
|
4
4
|
export declare const TEXT_INPUT_MULTILINE_TAG = "text-input-multiline";
|
|
5
|
-
/**
|
|
6
|
-
* The imperative API RN exposes on a TextInput ref, built over the engine node. Reached through
|
|
7
|
-
* each adapter's own `host-instance` accessor — the capability, not a shape
|
|
8
|
-
* (`.claude/rules/adapter-parity-audit.md`).
|
|
9
|
-
*
|
|
10
|
-
* `focus`/`blur` are native view commands; `clear` and `setSelection` reuse `setTextAndSelection`,
|
|
11
|
-
* the same stale-safe path a controlled write takes, so they cannot race a keystroke either.
|
|
12
|
-
*/
|
|
13
5
|
export declare function buildTextInputHandle(node: ISymbioteNode): ITextInputHandle;
|
|
14
6
|
export declare function registerTextInputBehavior(): void;
|