@symbiote-native/components 3.1.1 → 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.
Files changed (35) hide show
  1. package/build/behaviors/activity-indicator/shared.js +29 -71
  2. package/build/behaviors/button.d.ts +0 -9
  3. package/build/behaviors/button.js +47 -233
  4. package/build/behaviors/image-background.js +21 -81
  5. package/build/behaviors/image.js +3 -10
  6. package/build/behaviors/input-accessory-view.js +10 -51
  7. package/build/behaviors/pressable.d.ts +0 -48
  8. package/build/behaviors/pressable.js +59 -149
  9. package/build/behaviors/scroll-view/index.android.js +10 -30
  10. package/build/behaviors/scroll-view/shared.js +54 -181
  11. package/build/behaviors/scroll-view/sticky.d.ts +0 -8
  12. package/build/behaviors/scroll-view/sticky.js +54 -142
  13. package/build/behaviors/text-input.d.ts +0 -8
  14. package/build/behaviors/text-input.js +57 -156
  15. package/build/behaviors/touchable-highlight.js +14 -54
  16. package/build/behaviors/touchable-native-feedback.js +9 -32
  17. package/build/behaviors/touchable-opacity.js +3 -18
  18. package/build/behaviors/touchable-without-feedback.js +6 -24
  19. package/build/component-names/index.android.js +6 -8
  20. package/build/component-names/shared.js +12 -42
  21. package/build/index.js +13 -19
  22. package/build/scroll-view-commands.js +3 -11
  23. package/build/state/pressable.js +18 -43
  24. package/build/state/sticky-header-reducer.js +103 -149
  25. package/build/state/touchable.js +3 -5
  26. package/build/state/virtualized-list-reducer.js +21 -48
  27. package/build/state/virtualized-list.js +63 -148
  28. package/build/text-props.js +3 -13
  29. package/build/view/render-button.js +13 -44
  30. package/build/view/render-input-accessory-view.js +8 -24
  31. package/build/view/render-pressable/index.js +3 -4
  32. package/build/view/render-scroll-view.js +13 -23
  33. package/build/view/render-touchable-native-feedback.js +5 -14
  34. package/host-primitives.cjs +33 -207
  35. package/package.json +3 -3
@@ -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 rather than a layout mistake. RN inverts the tree instead: `AndroidSwipeRefreshLayout`
5
- // WRAPS the scroll view, and the scroll view's style is split across the two boxes — layout on the
6
- // wrapper's frame, visual on the scroller (`ScrollView.js:1856`). `nestedScrollEnabled` goes on the
7
- // inner view so it consumes the gesture before the refresh parent sees it.
8
- //
9
- // THIS FILE IS A CLAIM MODE AND NOTHING ELSE NOW (2026-09-18). It carried two `payloadFold`s until
10
- // then — the last two folds in ScrollView and the last structural blocker in the behavior
11
- // migration — and both are `SymbioteFabricProps.cpp` now: `foldScrollViewProps` takes the visual
12
- // half when its parent is a refresh control, and `foldRefreshWrapperProps` takes the layout half of
13
- // the child it wraps.
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,86 +1,25 @@
1
- // ScrollView's host behavior, the platform-invariant half — and the pilot for four of the engine's
2
- // composed-primitive seams: `buildStructure` + `childHost`, `slotProps`, `slotDerived`, and
3
- // `claimedChildren`.
4
- //
5
- // THE PLATFORM HALF IS THE REFRESHCONTROL, and only that. `index.ios` claims it `beside` the
6
- // content view; `index.android` claims it as a `wrap`, because an Android ScrollView holds exactly
7
- // one child. Everything else here is shared, including the tags, the folds and the content-size
8
- // synthesis.
9
- //
10
- // WHAT IS WIRED. Structure, the style compositions, `decelerationRate` resolution,
11
- // `collapsableChildren`, the synthesized `onContentSizeChange`, the RefreshControl on both
12
- // platforms — and, since the sticky half landed, the raised `scrollEventThrottle`, the scroll
13
- // value that drives the pins, the owner layout an inverted pin needs, and the per-commit walk that
14
- // turns `stickyHeaderIndices` into those same headers. The sticky machinery itself lives in
15
- // `./sticky`, because a `<StickyHeader>` is a CHILD and the three props above are functions of
16
- // whether one registered.
17
- //
18
- // TODO(rn-parity): `keyboardShouldPersistTaps` is a type-only prop everywhere — no capture-phase
19
- // responder negotiation exists to eat a tap-elsewhere and dismiss the keyboard
20
- // (`ScrollView.js:1360-1590`). Needs a `TextInputState.isTextInput`-equivalent, capture-phase
21
- // `startShouldSetResponder`/`responderTerminationRequest` wiring on this tag (the same seam
22
- // `Switch`/`Pressable` already use), and an `_isAnimating()`/momentum signal from the scroll
23
- // machine. Full scope and the reusable infra already in place: audit skill, "Found, NOT fixed:
24
- // ScrollView's keyboardShouldPersistTaps".
25
- //
26
- // TODO(rn-parity): `stickyHeaderHiddenOnScroll` is completely absent — no prop surface, no state,
27
- // no adapter wiring (`grep -rn "stickyHeaderHiddenOnScroll\|hiddenOnScroll"` returns zero hits).
28
- // Vendor composes an `Animated.diffClamp` over the scroll delta and adds it to the ordinary sticky
29
- // translateY (`ScrollViewStickyHeader.js:39,84-103`). Needs a new running clamped-offset state
30
- // ADDED to `STICKY_TRANSLATE_PROP` (our sticky pin is a discrete debounced number, not a live
31
- // `Animated` composition), and the fix touches `reduceSticky`
32
- // (`state/sticky-header-reducer.ts`) — the shared decision function every adapter's own sticky
33
- // component plus Angular's projection controller also run — not just this file. Audit skill,
34
- // "Found, NOT fixed: ScrollView's stickyHeaderHiddenOnScroll".
35
- //
36
- // WHAT A COMPOSED PRIMITIVE COSTS TODAY. Every adapter's ScrollView wrapper builds the same two
37
- // nodes: `selectScrollIntrinsics` picks a scroll intrinsic and a content intrinsic, and the
38
- // wrapper's body nests `<content>{children}</content>` inside `<scroll>`. That body is a framework
39
- // component instance per ScrollView — a Vue instance, a Solid props Proxy, Svelte anchors, an
40
- // Angular LView — which is precisely the currency a tag exists to delete. `foldPayload` gives a tag
41
- // its wrapper's PROP MAPPING; nothing gave it the wrapper's COMPOSITION, so a composed primitive
42
- // could not become a tag at all no matter what its props did. This is that half.
43
- //
44
- // WHY THE TAG CARRIES THE AXIS. `buildStructure` runs at `createElement`, before a single prop is
45
- // routed, so it cannot read `horizontal`. It does not need to: horizontal scroll is already a
46
- // SEPARATE intrinsic (`horizontal-scroll-view` — a different native ViewManager on
47
- // Android, not RCTScrollView with a flag), so the decision the behavior needs is in the tag it was
48
- // looked up by. One behavior per tag, each knowing its own content intrinsic. That is the same
49
- // shape `intrinsicWhen` gives TextInput's `multiline`, arrived at from the other side.
50
- //
51
- // REGISTERED BY SVELTE SINCE 2026-09-07 (`adapters/svelte/src/register.ts`), and by no other
52
- // adapter — this paragraph read "NOT REGISTERED BY ANY ADAPTER" for three days after that stopped
53
- // being true. The hazard still holds for the four that have not registered: `scroll-view` is the
54
- // tag their WRAPPERS emit, and a wrapper builds its own content node from `selectScrollIntrinsics`,
55
- // as does `VirtualizedList`. Registering while either stands silently gives those trees a SECOND
56
- // content node: `RCTScrollView > RCTScrollContentView > RCTScrollContentView`. So the precondition
57
- // per adapter is that nothing else builds the content node.
58
- //
59
- // A SECOND TAG WAS NOT THE ANSWER, and this reverses what this header said until 2026-09-07: a
60
- // second spelling only keeps two owners apart while two paths exist, and it buys a rename across
61
- // every call site now plus another when one path dies. The decision was that the ENGINE becomes the
62
- // single owner of the content node. That cut LANDED 2026-09-11: nothing else builds a content node
63
- // any more, and all five adapters register this behavior through
64
- // `@symbiote-native/components/register`.
65
- //
66
- // STYLE, on both nodes, and the precedence is the part that is easy to get silently wrong. The
67
- // wrapper composes exactly two arrays, and this reproduces both:
68
- //
69
- // owner [scrollViewBaseStyle, style] base UNDER the app's, so an explicit
70
- // flexDirection still wins
71
- // slot [contentContainerStyle, {flexDirection:'row'}] row OVER the app's, on horizontal only
72
- //
73
- // Which is why the two halves use different seams rather than one. `contentContainerStyle` is
74
- // written by the app on the OWNER and belongs to the slot, so it travels through `slotProps` — a
75
- // pure RENAME (`contentContainerStyle` -> the slot's `style`) that goes through the slot's own
76
- // `routeProp` and inherits style merging, class merging and the already-published guard. The
77
- // CONSTANT half is a `payloadFold`, because a fold is where precedence can be expressed: the
78
- // owner's puts the base first, the slot's puts the row direction last. A redirect that also tried
79
- // to compose would have to pick one order for both.
80
- //
81
- // The slot's fold is assigned to the node inside `buildStructure`, not declared on the behavior:
82
- // `IHostBehavior.foldPayload` is the OWNER's, wired by `attachHostBehavior`, and a behavior that
83
- // builds a node owns what that node carries.
1
+ // ScrollView's host behavior, platform-invariant half — pilot for `buildStructure` + `childHost`,
2
+ // `slotProps`, `slotDerived`, `claimedChildren`. Only the RefreshControl claim differs by platform
3
+ // (`index.ios` beside the content view, `index.android` wraps it); everything else is shared.
4
+ // TODO(rn-parity): `keyboardShouldPersistTaps` is type-only — no capture-phase responder
5
+ // negotiation eats a tap-elsewhere to dismiss the keyboard (`ScrollView.js:1360-1590`). Needs
6
+ // `TextInputState.isTextInput`-equivalent wiring on this tag; see audit skill.
7
+ // TODO(rn-parity): `stickyHeaderHiddenOnScroll` is entirely absent (no prop, no state, no
8
+ // wiring). Vendor composes `Animated.diffClamp` over the scroll delta onto the sticky translateY
9
+ // (`ScrollViewStickyHeader.js:39,84-103`); our sticky pin is a discrete number, not live Animated.
10
+ // A composed primitive could not become a tag from props alone: `foldPayload` gives a tag its
11
+ // wrapper's PROP MAPPING, but every adapter's ScrollView wrapper also built the same two nodes as
12
+ // a framework component instance — this file gives the tag the wrapper's COMPOSITION too.
13
+ // `buildStructure` runs at `createElement`, before any prop routes, so it can't read `horizontal`
14
+ // — it doesn't need to: the axis is a SEPARATE intrinsic (`horizontal-scroll-view`, a different
15
+ // native ViewManager on Android), so one behavior per tag already knows its own content intrinsic.
16
+ // The ENGINE is the single owner of the content node: an adapter wrapper building one for the
17
+ // same tag would double-commit it if registered while the wrapper still stands.
18
+ // STYLE precedence differs by node: owner is [base, style] (base UNDER the app's), slot is
19
+ // [contentContainerStyle, rowStyle] (row OVER the app's, horizontal only).
20
+ // `contentContainerStyle` travels through `slotProps` as a pure RENAME onto the slot's `style`;
21
+ // the row-direction CONSTANT is a `payloadFold` instead, assigned to the slot node inside
22
+ // `buildStructure` since `IHostBehavior.foldPayload` is only ever the OWNER's.
84
23
  import { appendChild, appListenerFor, createElement, dlog, registerHostBehavior, setBehaviorListener, setEventListener, } from '@symbiote-native/engine';
85
24
  import { descriptorFor } from '../../component-names';
86
25
  import { didContentSizeChange, readLayoutDimension, } from '../../view/render-scroll-view.js';
@@ -100,71 +39,25 @@ const SLOT_DERIVED = [
100
39
  'snapToAlignment',
101
40
  'removeClippedSubviews',
102
41
  ];
103
- // A `<RefreshControl>` written among the app's children is claimed, and WHAT the owner does with
104
- // it is the one thing that genuinely differs per platform — see the platform files. Resolved
105
- // through `descriptorFor`, so this is `PullToRefreshView` on iOS and `AndroidSwipeRefreshLayout`
106
- // on Android without either name appearing here.
42
+ // A `<RefreshControl>` written among the app's children is claimed; WHAT the owner does with it
43
+ // is the one thing that genuinely differs per platform — see the platform files.
107
44
  export const REFRESH_CONTROL = descriptorFor('refresh-control').component;
108
- // The OWNER's fold: the per-axis base style UNDER the app's (so an explicit `flexDirection` still
109
- // wins), `decelerationRate` resolved from RN's two words to the platform's friction constant, and
110
- // the two props a tag has no wrapper to write for it. The resolution has to happen here because
111
- // 'normal'/'fast' reach Fabric as strings it cannot read.
112
- //
113
- // `horizontal` is a real C++ prop (`BaseScrollViewProps.h:56`) and the separate ViewManager is
114
- // ANDROID's — on iOS both tags resolve to RCTScrollView, so the PROP is what turns the axis there
115
- // and a bare `<horizontal-scroll-view>` would otherwise scroll vertically. Written from the tag
116
- // rather than read off props, which is the same source `buildStructure` picked the content
117
- // intrinsic from; an app that also writes `horizontal` on the vertical tag is contradicting the
118
- // element it chose, and the tag wins.
119
- //
120
- // `nestedScrollEnabled` defaults ON only on the Android RefreshControl WRAP path, as RN does
121
- // (`ScrollView.js:1862`); elsewhere only the authored value is sent.
122
- // THE OWNER'S FOLD IS GONE (2026-09-18) — `foldScrollViewProps` in `SymbioteFabricProps.cpp`. Every
123
- // input it had was the node's own bag plus the AXIS, and the axis is the tag (`scroll-view` vs
124
- // `horizontal-scroll-view`), so it was a tag rule by every criterion: the base style composition,
125
- // `nestedScrollEnabled`, the `horizontal` strip, the asymmetric bounce pair, the two ViewConfig-less
126
- // strips, and `decelerationRate`. Contract:
127
- // `core/engine/cpp/tests/js/scroll-view-payload.itest.ts`.
128
- //
129
- // IT WAS BLOCKED ON A MISSING LOG, not on anything about the rule. An app writing `horizontal` on
130
- // the vertical tag has it IGNORED, and this fold `dlog`'d where to write it instead — while
131
- // `core/engine/cpp` had no logging facility at all, only `throw jsi::JSError`. Moving the rule as
132
- // written would have deleted a diagnostic, which `<keep_logs_gate_behind_DEBUG>` forbids. So
133
- // `SymbioteDebug.h` was built first and this is its first caller
134
- // (`core/engine/cpp/tests/js/native-debug-log.itest.ts`).
135
- //
136
- // `decelerationRate`'s two constants went with it and are `#ifdef ANDROID` there: on iOS BOTH tags
137
- // resolve to `RCTScrollView`, so a component name cannot tell iOS-vertical from Android-vertical the
138
- // way `foldSwitchProps` can. That leaves the Android half outside headless reach — the same gap
139
- // already recorded for `android_ripple`, and the only part of this rule a test here cannot see.
140
- // THE SLOT'S FOLD IS GONE (2026-09-18) — `foldScrollContentProps` in `SymbioteFabricProps.cpp`.
141
- // Its two halves came from different places and the second is why it took until now:
142
- //
143
- // rowStyle a CONSTANT, horizontal only, composed OVER the app's contentContainerStyle
144
- // — a function of the content node's OWN tag, portable from the start
145
- // collapsableChildren DERIVED from `maintainVisibleContentPosition` / `snapToAlignment`, which
146
- // stay on the OWNER
147
- //
148
- // "A per-node rule cannot reach another node" is what this file used to say, and it was a fact about
149
- // the JS FOLD rather than about the engine: the tree lives in C++, so a node knows its parent and
150
- // `fabricProps` now takes `ownerProps` from it. Contract:
151
- // `core/engine/cpp/tests/js/scroll-content-payload.itest.ts`.
152
- // `rowStyle` USED TO BE A PARAMETER HERE and is not one any more: the content node's row direction
153
- // is decided in the engine from that node's OWN tag (`horizontal-scroll-content`), so this builder
154
- // no longer needs to know the axis to build it.
45
+ // foldScrollViewProps in C++ owns the OWNER's fold: the per-axis base style UNDER the app's,
46
+ // decelerationRate resolved from RN's two words to a platform friction constant, and the
47
+ // horizontal strip and bounce pair — all tag rules, since strings reach Fabric unreadable.
48
+ // `horizontal` is a real C++ prop and the separate ViewManager is ANDROID's — on iOS both tags
49
+ // resolve to RCTScrollView, so the prop is what turns the axis there. Written from the TAG, not
50
+ // read off props; an app also writing `horizontal` on the vertical tag loses to the tag.
51
+ // foldScrollContentProps in C++ owns the slot's fold: rowStyle (horizontal-only constant) and
52
+ // collapsableChildren, derived from the OWNER's maintainVisibleContentPosition/snapToAlignment.
155
53
  function buildContent(contentIntrinsic) {
156
54
  return (node) => {
157
55
  const descriptor = descriptorFor(contentIntrinsic);
158
56
  const content = createElement(descriptor.component, descriptor.isText, contentIntrinsic);
159
- // `collapsable: false` WAS SEEDED HERE AND IS NOT ANY MORE (2026-09-18). It is unconditional on
160
- // every content node in both axes (`ScrollView.js:1747`), which makes it a constant of the TAG
161
- // rather than of this builder — `foldScrollContentProps` writes it, and the contract is
162
- // `scroll-content-payload.itest.ts` ("from the rule and not a seed", which asserts the authored
163
- // prop is ABSENT because that is the only thing distinguishing the two routes).
164
- // Lands directly on the owner, because `node.childHost` is still undefined here: the engine
165
- // assigns it from what this returns. That ordering is why `buildStructure` RETURNS the slot
166
- // instead of setting the field itself — a behavior that set it first would redirect its own
167
- // structure into the slot it was building.
57
+ // `collapsable: false` is a TAG constant now (`foldScrollContentProps`, `ScrollView.js:1747`),
58
+ // not seeded here — `scroll-content-payload.itest.ts` asserts the authored prop stays ABSENT.
59
+ // Lands on the owner: `node.childHost` is undefined here, so `buildStructure` RETURNS the
60
+ // slot instead of setting the field itself.
168
61
  appendChild(node, content);
169
62
  return content;
170
63
  };
@@ -173,14 +66,10 @@ function buildContent(contentIntrinsic) {
173
66
  // fire the app's handler — RN dedupes the same way (`_handleContentOnLayout`). Off the node: this
174
67
  // exists only for the ScrollViews an app wired a handler to.
175
68
  const lastContentSize = new WeakMap();
176
- // RN synthesizes onContentSizeChange from the CONTENT view's own onLayout — there is no native
177
- // content-size event (ScrollView.js:1675 `contentSizeChangeProps`). The wrapper wired that by
178
- // rendering an `onLayout` onto its inner node; a tag has no inner node of its own, so the behavior
179
- // installs it on the slot it built.
180
- //
181
- // The app's callback takes `(width, height)`, not an event, which is why `contentSizeChange` is an
182
- // OWNED listener: `setEventListener` wraps an ordinary listener as `(event) => handler(event)` and
183
- // would call a two-number handler with one event. Owned names are stashed raw instead.
69
+ // RN synthesizes onContentSizeChange from the CONTENT view's own onLayout — there's no native
70
+ // content-size event. A tag has no inner node of its own, so the behavior installs it on the slot.
71
+ // The app's callback takes `(width, height)`, not an event, so `contentSizeChange` is an OWNED
72
+ // listener: setEventListener would wrap it as `(event) => handler(event)` and call it wrong.
184
73
  function contentSizeListener(owner) {
185
74
  return (event) => {
186
75
  const handler = appListenerFor(owner, 'contentSizeChange');
@@ -200,16 +89,10 @@ function contentSizeListener(owner) {
200
89
  handler(width, height);
201
90
  };
202
91
  }
203
- // RN installs the content `onLayout` only when the app passed `onContentSizeChange`, and so does
204
- // every wrapper — `onLayout` is a gated event, so wiring it unconditionally would put `onLayout:
205
- // true` in the payload of every ScrollView's content node and buy a native event nobody reads. A
206
- // change to the committed surface in EITHER direction is a bug, so the wiring has to follow the
207
- // prop.
208
- //
209
- // It follows the LISTENER rather than a commit, which is what `onOwnedListenerChange` is for: a
210
- // listener flip changes no payload by itself, so the commit after it is a no-op and a post-commit
211
- // hook would never fire. Measured on exactly this — the wire worked (mount commits for other
212
- // reasons) and the UNWIRE silently did not.
92
+ // RN installs the content `onLayout` only when the app passed `onContentSizeChange`: `onLayout`
93
+ // is gated, so wiring it unconditionally would put `onLayout: true` in every ScrollView's payload.
94
+ // Follows the LISTENER rather than a commit, since a listener flip changes no payload by itself —
95
+ // the commit after it is a no-op and a post-commit hook would never fire.
213
96
  function syncContentSizeWiring(owner, wired) {
214
97
  const slot = owner.childHost;
215
98
  if (slot === undefined)
@@ -233,11 +116,9 @@ function syncOwnedListener(owner, name, wired) {
233
116
  }
234
117
  function scrollBehavior(contentIntrinsic, rowStyle, platform) {
235
118
  return {
236
- // `scroll` and `layout` are owned for the collision reason rather than because the behavior
237
- // consumes them: RN's ScrollView installs `_handleScroll` and `_handleLayout` on the native
238
- // view unconditionally and calls the app's own handler from inside them, and `node.listeners`
239
- // is single-slot — so a behavior that installed either without owning it would silently evict
240
- // the app's.
119
+ // `scroll` and `layout` are owned for the collision reason, not because the behavior consumes
120
+ // them: node.listeners is single-slot, so installing either without owning it would silently
121
+ // evict the app's own handler.
241
122
  ownedListeners: [
242
123
  'contentSizeChange',
243
124
  'scroll',
@@ -248,20 +129,16 @@ function scrollBehavior(contentIntrinsic, rowStyle, platform) {
248
129
  slotDerived: [...SLOT_DERIVED, ...(platform.slotDerived ?? [])],
249
130
  claimedChildren: { [REFRESH_CONTROL]: platform.claimMode },
250
131
  buildStructure: buildContent(contentIntrinsic),
251
- // The scroll dispatcher is installed here and never conditionally: it is what drives the
252
- // sticky AnimatedValue, and a header can register long after this node was created. It costs a
253
- // forward per scroll event on a ScrollView with no sticky child, which is what RN pays too.
254
- // Nothing else is taken — no timer, and the two conditional listeners are wired on a flip.
132
+ // The scroll dispatcher is installed unconditionally: it drives the sticky AnimatedValue, and
133
+ // a header can register long after this node was created.
255
134
  attach(node) {
256
135
  markScrollOwner(node);
257
136
  setBehaviorListener(node, 'scroll', event => handleOwnerScroll(node, event));
258
137
  installResponderPredicates(node);
259
138
  },
260
139
  onOwnedListenerChange: syncOwnedListener,
261
- // The one beat at which the app's children are all present — `stickyHeaderIndices` addresses
262
- // them positionally, and no hook reports a children CHANGE. Costs a Set iteration per commit
263
- // over the ScrollViews alone, and `reconcileStickyIndices` returns on a WeakSet miss for any
264
- // that never used the prop.
140
+ // The one beat where the app's children are all present — no hook reports a children CHANGE,
141
+ // and reconcileStickyIndices returns on a WeakSet miss for any that never used the prop.
265
142
  afterCommit: reconcileStickyIndices,
266
143
  detach(node) {
267
144
  lastContentSize.delete(node);
@@ -274,13 +151,9 @@ function scrollBehavior(contentIntrinsic, rowStyle, platform) {
274
151
  export function registerScrollViewBehaviors(platform) {
275
152
  registerHostBehavior(SCROLL_VIEW_TAG, scrollBehavior('scroll-content', undefined, platform));
276
153
  registerHostBehavior(HORIZONTAL_SCROLL_VIEW_TAG, scrollBehavior('horizontal-scroll-content', { flexDirection: 'row' }, platform));
277
- // REGISTRATIONS WITH NO RUNTIME, and they are what hand the content tags to the host. A tag
278
- // crosses only through `recordSetTag`, which `attachHostBehavior` emits, so a tag with no behavior
279
- // registered carries an EMPTY `tagName` in C++ and no rule can fire for it. These two nodes are
280
- // built by `buildStructure` and named by no app, which is exactly the shape that trap has.
281
- //
282
- // Same reasoning as `activity-indicator-spinner`'s stub: a registration is how this codebase says
283
- // a tag HAS platform semantics, which is the claim being made.
154
+ // REGISTRATIONS WITH NO RUNTIME, what hand the content tags to the host: a tag with no behavior
155
+ // registered carries an EMPTY tagName in C++ and no rule can fire for it. A registration is how
156
+ // this codebase says a tag HAS platform semantics.
284
157
  for (const contentTag of ['scroll-content', 'horizontal-scroll-content'])
285
158
  registerHostBehavior(contentTag, { attach() { }, detach() { } });
286
159
  // With the scroll views, never on its own: a sticky header is meaningless without an owner to
@@ -6,13 +6,5 @@ export declare function hasStickyHeaders(owner: ISymbioteNode): boolean;
6
6
  export declare function syncOwnerLayout(owner: ISymbioteNode): void;
7
7
  export declare function handleOwnerScroll(owner: ISymbioteNode, event: ISymbioteEvent): void;
8
8
  export declare function releaseStickyOwner(owner: ISymbioteNode): void;
9
- /**
10
- * Bring the synthesized wrappers in line with `stickyHeaderIndices`. Called from the ScrollView
11
- * behavior's `afterCommit`, the one beat at which the app's children are all present.
12
- *
13
- * O(slot children) per commit, once — never per mutation. Angular's controller coalesces to one
14
- * pass per change detection for exactly this reason: its per-mutation walk was O(M²) and died at
15
- * 801 children.
16
- */
17
9
  export declare function reconcileStickyIndices(owner: ISymbioteNode): void;
18
10
  export declare const stickyHeaderBehavior: IHostBehavior;