@symbiote-native/components 2.0.0 → 3.0.1
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 +8 -9
- package/build/accessibility-props.d.ts +1 -1
- package/build/accessibility-props.js +2 -2
- package/build/behaviors/activity-indicator/shared.js +41 -70
- package/build/behaviors/button.d.ts +11 -0
- package/build/behaviors/button.js +101 -89
- package/build/behaviors/image-background.d.ts +1 -0
- package/build/behaviors/image-background.js +97 -81
- package/build/behaviors/image.d.ts +0 -1
- package/build/behaviors/image.js +24 -105
- package/build/behaviors/input-accessory-view.d.ts +0 -1
- package/build/behaviors/input-accessory-view.js +47 -53
- package/build/behaviors/pressable.d.ts +1 -1
- package/build/behaviors/pressable.js +89 -100
- package/build/behaviors/refresh-control.js +15 -2
- package/build/behaviors/scroll-view/index.android.js +25 -37
- package/build/behaviors/scroll-view/index.d.ts +1 -0
- package/build/behaviors/scroll-view/index.js +3 -0
- package/build/behaviors/scroll-view/responder.d.ts +4 -0
- package/build/behaviors/scroll-view/responder.js +202 -0
- package/build/behaviors/scroll-view/shared.d.ts +1 -3
- package/build/behaviors/scroll-view/shared.js +95 -95
- package/build/behaviors/scroll-view/sticky.d.ts +1 -0
- package/build/behaviors/scroll-view/sticky.js +62 -49
- package/build/behaviors/switch.js +43 -86
- package/build/behaviors/text-input.js +219 -107
- package/build/behaviors/touchable-highlight.js +74 -61
- package/build/behaviors/touchable-native-feedback.js +43 -122
- package/build/behaviors/touchable-opacity.js +71 -59
- package/build/behaviors/touchable-without-feedback.js +35 -100
- package/build/component-names/index.android.js +7 -11
- package/build/component-names/index.ios.js +0 -7
- package/build/component-names/shared.d.ts +1 -1
- package/build/index.d.ts +15 -21
- package/build/index.js +21 -28
- package/build/resolve-intrinsic.js +3 -9
- package/build/scroll-view-commands.d.ts +1 -9
- package/build/scroll-view-commands.js +13 -74
- package/build/state/flat-list.d.ts +2 -2
- package/build/state/flat-list.js +10 -2
- package/build/state/pressable.d.ts +6 -1
- package/build/state/pressable.js +63 -28
- package/build/state/section-list.d.ts +2 -0
- package/build/state/section-list.js +14 -7
- package/build/state/text-input.d.ts +7 -40
- package/build/state/text-input.js +17 -186
- package/build/state/touchable.d.ts +1 -0
- package/build/state/touchable.js +11 -8
- package/build/state/virtualized-list-reducer.d.ts +2 -2
- package/build/state/virtualized-list.d.ts +6 -6
- package/build/state/virtualized-list.js +71 -37
- package/build/text-props.d.ts +0 -8
- package/build/text-props.js +14 -25
- package/build/view/render-button.d.ts +1 -29
- package/build/view/render-button.js +44 -81
- package/build/view/render-image/index.d.ts +14 -1
- package/build/view/render-image/index.js +22 -147
- package/build/view/render-input-accessory-view.d.ts +1 -5
- package/build/view/render-input-accessory-view.js +26 -48
- package/build/view/render-keyboard-avoiding-view.d.ts +7 -1
- package/build/view/render-keyboard-avoiding-view.js +40 -1
- package/build/view/render-modal.d.ts +1 -1
- package/build/view/render-modal.js +15 -5
- package/build/view/render-pressable/index.d.ts +1 -0
- package/build/view/render-pressable/index.js +4 -0
- package/build/view/render-scroll-view.d.ts +0 -4
- package/build/view/render-scroll-view.js +3 -49
- package/build/view/render-switch.d.ts +0 -14
- package/build/view/render-switch.js +4 -42
- package/build/view/render-touchable-highlight.d.ts +1 -0
- package/build/view/render-touchable-native-feedback.d.ts +0 -1
- package/build/view/render-touchable-native-feedback.js +15 -9
- package/host-primitives.cjs +49 -203
- package/host-primitives.d.cts +0 -1
- package/package.json +3 -7
- package/build/fold-host-bag.d.ts +0 -15
- package/build/fold-host-bag.js +0 -99
- package/build/view/render-text-input.d.ts +0 -11
- package/build/view/render-text-input.js +0 -39
package/README.md
CHANGED
|
@@ -91,7 +91,7 @@ host node untouched — the render function never names a framework type.
|
|
|
91
91
|
(`createPressHandlers` / `createPressRuntime` in `state/pressable`) plus its render-decision
|
|
92
92
|
helpers (`buildPressableListeners`, `resolveDisabledAccessibilityState`, `shouldClaimResponder`,
|
|
93
93
|
… in `view/render-pressable`), the `Touchable*` timing constants, `Button`'s shared text-style
|
|
94
|
-
fold, `TextInput`'s controlled-value/event-count handshake (`
|
|
94
|
+
fold, `TextInput`'s controlled-value/event-count handshake (`foldText`, `shouldCommandText`,
|
|
95
95
|
`eventCountFromChange`, …), `KeyboardAvoidingView`'s inset math, `ScrollView`'s
|
|
96
96
|
intrinsics/sticky-header math (no full 3-layer split — the adapter owns the element assembly).
|
|
97
97
|
- **Two more shared reducers, driven by every adapter's reactive glue**: the sticky-header effect
|
|
@@ -104,14 +104,13 @@ host node untouched — the render function never names a framework type.
|
|
|
104
104
|
row/section folding helpers. Lists have no `view/render-*.ts` (a cell's content is the
|
|
105
105
|
framework's own children) — the shared layer here is pure state/logic, reused verbatim by every
|
|
106
106
|
adapter.
|
|
107
|
-
- **Host behaviors (`src/behaviors/*.ts`)** —
|
|
108
|
-
|
|
109
|
-
`
|
|
110
|
-
|
|
111
|
-
`
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
seam; see `.claude/rules/host-primitive-tier.md`.
|
|
107
|
+
- **Host behaviors (`src/behaviors/*.ts`)** — a primitive's state machine and prop folds registered
|
|
108
|
+
directly on the engine node (`registerPressableBehavior`, `registerSwitchBehavior`,
|
|
109
|
+
`registerImageBehavior`, `registerTextInputBehavior`, `registerInputAccessoryViewBehavior`, plus
|
|
110
|
+
the folds a tag still needs — `foldImagePayload`, `foldInputAccessoryViewPayload`,
|
|
111
|
+
`buildTextInputHandle`), so `pressable`/`switch`/`text-input`/`image`/`input-accessory-view` are
|
|
112
|
+
bare intrinsic tags rather than framework components. Built on `@symbiote-native/engine`'s
|
|
113
|
+
`registerHostBehavior` seam.
|
|
115
114
|
|
|
116
115
|
## What it does NOT do
|
|
117
116
|
|
|
@@ -64,7 +64,7 @@ export interface IAriaProps {
|
|
|
64
64
|
/**
|
|
65
65
|
* The typed entry point adapters import. The FOLD itself now lives in `@symbiote-native/engine`
|
|
66
66
|
* (`core/engine/src/accessibility-props.ts`) so it runs at the layer every path goes through —
|
|
67
|
-
* including a
|
|
67
|
+
* including a bare tag, which has no component wrapper to run it. This function stays here
|
|
68
68
|
* because the public types do, and because it keeps the typed gate: `hasAnyAriaKey` is checked
|
|
69
69
|
* before anything is allocated, so the ~99% of nodes carrying no alias cost the same as before.
|
|
70
70
|
*
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
// Vue, and the next) folds aria/role into accessibility* identically.
|
|
11
11
|
import { foldAriaProps } from '@symbiote-native/engine';
|
|
12
12
|
// The gate's key list, typed against `IAriaProps` so a new alias added to that interface and
|
|
13
|
-
// forgotten here is a type error. The engine carries its own untyped copy
|
|
13
|
+
// forgotten here is a type error. The engine carries its own untyped copy;
|
|
14
14
|
// `core/engine/src/accessibility-props.ts` is the single implementation of the FOLD, this is only
|
|
15
15
|
// the cheap probe that decides whether to call it.
|
|
16
16
|
const ARIA_KEYS = [
|
|
@@ -44,7 +44,7 @@ function hasAnyAriaKey(props) {
|
|
|
44
44
|
/**
|
|
45
45
|
* The typed entry point adapters import. The FOLD itself now lives in `@symbiote-native/engine`
|
|
46
46
|
* (`core/engine/src/accessibility-props.ts`) so it runs at the layer every path goes through —
|
|
47
|
-
* including a
|
|
47
|
+
* including a bare tag, which has no component wrapper to run it. This function stays here
|
|
48
48
|
* because the public types do, and because it keeps the typed gate: `hasAnyAriaKey` is checked
|
|
49
49
|
* before anything is allocated, so the ~99% of nodes carrying no alias cost the same as before.
|
|
50
50
|
*
|
|
@@ -26,22 +26,17 @@
|
|
|
26
26
|
// Registered by all five adapters since 2026-09-09, in the same commit that deleted the five
|
|
27
27
|
// wrappers — the registry is keyed by TAG, so registering while a wrapper still painted its own
|
|
28
28
|
// spinner would have given every indicator two.
|
|
29
|
-
import { appendChild, createElement, registerHostBehavior, } from '@symbiote-native/engine';
|
|
29
|
+
import { appendChild, createElement, registerHostBehavior, setProp, } from '@symbiote-native/engine';
|
|
30
30
|
import { descriptorFor } from '../../component-names';
|
|
31
31
|
export const ACTIVITY_INDICATOR_TAG = 'activity-indicator';
|
|
32
32
|
// The NATIVE spinner — `ActivityIndicatorView` on iOS, `AndroidProgressBar` on Android. Built by
|
|
33
33
|
// `buildStructure` below and by nothing else; no app writes it.
|
|
34
34
|
export const ACTIVITY_INDICATOR_SPINNER_TAG = 'activity-indicator-spinner';
|
|
35
|
-
//
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
//
|
|
39
|
-
|
|
40
|
-
// Centering wrapper RN puts around the spinner (styles.container).
|
|
41
|
-
const CONTAINER_STYLE = {
|
|
42
|
-
alignItems: 'center',
|
|
43
|
-
justifyContent: 'center',
|
|
44
|
-
};
|
|
35
|
+
// The two size boxes, the default size and the centering style are NOT here any more: they are
|
|
36
|
+
// literals inside `foldActivityIndicatorProps` / `foldActivityIndicatorSpinnerProps`
|
|
37
|
+
// (`SymbioteFabricProps.cpp`). Keeping a JS copy of a value only C++ reads is the mirror this port
|
|
38
|
+
// exists to remove — it would compile, export and test cleanly while nothing on a device consulted
|
|
39
|
+
// it.
|
|
45
40
|
// The props that stay on the centering host instead of travelling to the spinner: RN's own two
|
|
46
41
|
// (`ActivityIndicator.js:113`) plus the spellings the ENGINE resolves against a node's own style.
|
|
47
42
|
const ACTIVITY_INDICATOR_HOST_PROPS = [
|
|
@@ -55,62 +50,21 @@ const ACTIVITY_INDICATOR_HOST_PROPS = [
|
|
|
55
50
|
'class',
|
|
56
51
|
'className',
|
|
57
52
|
];
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
// The HOST's fold: RN's `StyleSheet.compose(styles.container, style)` (ActivityIndicator.js:114).
|
|
74
|
-
// Base first, so an app style still wins.
|
|
75
|
-
const hostFold = props => ({
|
|
76
|
-
...props,
|
|
77
|
-
style: [CONTAINER_STYLE, props.style],
|
|
78
|
-
});
|
|
79
|
-
function isActivityIndicatorSize(value) {
|
|
80
|
-
return value === 'small' || value === 'large' || typeof value === 'number';
|
|
81
|
-
}
|
|
82
|
-
// The SPINNER's fold — RN's own body (`ActivityIndicator.js:99-118`) applied to the node the app
|
|
83
|
-
// never names.
|
|
84
|
-
function spinnerFold(platform) {
|
|
85
|
-
return props => {
|
|
86
|
-
const size = isActivityIndicatorSize(props.size)
|
|
87
|
-
? props.size
|
|
88
|
-
: DEFAULT_SIZE;
|
|
89
|
-
const { sizeStyle, sizeProp } = resolveSize(size);
|
|
90
|
-
const next = {
|
|
91
|
-
...props,
|
|
92
|
-
// RN defaults both to true and every wrapper spelled that `!== false`. A tag has no
|
|
93
|
-
// destructuring default, so the fold is where the default has to live.
|
|
94
|
-
animating: props.animating !== false,
|
|
95
|
-
hidesWhenStopped: props.hidesWhenStopped !== false,
|
|
96
|
-
style: sizeStyle,
|
|
97
|
-
};
|
|
98
|
-
// A NUMBER never reaches native: it sizes the spinner through style alone, and the native enum
|
|
99
|
-
// takes 'small'/'large' only. So the key has to leave, not merely go unwritten.
|
|
100
|
-
if (sizeProp === undefined)
|
|
101
|
-
delete next.size;
|
|
102
|
-
else
|
|
103
|
-
next.size = sizeProp;
|
|
104
|
-
const color = typeof props.color === 'string' ? props.color : platform.defaultColor;
|
|
105
|
-
// Omitted rather than sent as null — Android's theme default is null and Fabric's colour parser
|
|
106
|
-
// rejects one.
|
|
107
|
-
if (color === null)
|
|
108
|
-
delete next.color;
|
|
109
|
-
else
|
|
110
|
-
next.color = color;
|
|
111
|
-
return next;
|
|
112
|
-
};
|
|
113
|
-
}
|
|
53
|
+
// `activeStyle` is deliberately NOT here. It looked like it belonged — it is slot 1 of the same
|
|
54
|
+
// `pushClassStyle` merge `style` and `class` feed — but this primitive has no pressed state, so
|
|
55
|
+
// `routeProp` consumes the key on whichever node it lands on and it never reaches Fabric either
|
|
56
|
+
// way. An entry no test can make fail is an entry that was never wired in.
|
|
57
|
+
// BOTH FOLDS LEFT THIS FILE on 2026-09-18 and neither was replaced by anything here: they are
|
|
58
|
+
// `foldActivityIndicatorProps` and `foldActivityIndicatorSpinnerProps` in `SymbioteFabricProps.cpp`.
|
|
59
|
+
// Every input either read was the node's own bag — no owner, no listener, no live state — which is
|
|
60
|
+
// what made them tag rules rather than composition, and it is why both nodes now cost ZERO trips
|
|
61
|
+
// into JS instead of one each. Contract:
|
|
62
|
+
// `core/engine/cpp/tests/js/activity-indicator-payload.itest.ts`.
|
|
63
|
+
//
|
|
64
|
+
// The size constants, the container style and the default colour went WITH them rather than staying
|
|
65
|
+
// as a second copy for the tests to assert. `platform.defaultColor` survives as the last field of
|
|
66
|
+
// `IActivityIndicatorPlatform` only because the rule's Android half is chosen by COMPONENT NAME in
|
|
67
|
+
// C++, so the JS value would have no reader — see the type's own note.
|
|
114
68
|
// The composition. Returns the spinner as the slot because the prop redirect is gated on
|
|
115
69
|
// `childHost` being set — the redirect is what this slot is FOR, and NOT where children go: RN's
|
|
116
70
|
// ActivityIndicator renders only the spinner (`ActivityIndicator.js:112-118`) and takes no children
|
|
@@ -124,8 +78,8 @@ function buildSpinner(platform) {
|
|
|
124
78
|
// Constants of the platform, never a function of a prop, so they are seeded at build time the
|
|
125
79
|
// way ScrollView seeds `collapsable: false` — empty on iOS, AndroidProgressBar's two
|
|
126
80
|
// requirements on Android.
|
|
127
|
-
|
|
128
|
-
|
|
81
|
+
for (const [key, value] of Object.entries(platform.nativeExtras))
|
|
82
|
+
setProp(spinner, key, value);
|
|
129
83
|
appendChild(node, spinner);
|
|
130
84
|
return spinner;
|
|
131
85
|
};
|
|
@@ -135,7 +89,6 @@ function activityIndicatorBehavior(platform) {
|
|
|
135
89
|
slotPropsExcept: ACTIVITY_INDICATOR_HOST_PROPS,
|
|
136
90
|
slotTakesNoChildren: true,
|
|
137
91
|
buildStructure: buildSpinner(platform),
|
|
138
|
-
foldPayload: hostFold,
|
|
139
92
|
// Required by the interface and deliberately empty: this primitive owns no timer, no listener
|
|
140
93
|
// and no native handshake. Written out rather than shared with a `noop` so the emptiness reads
|
|
141
94
|
// as a decision.
|
|
@@ -146,4 +99,22 @@ function activityIndicatorBehavior(platform) {
|
|
|
146
99
|
// Called by the platform files; nothing else should.
|
|
147
100
|
export function registerActivityIndicatorBehaviors(platform) {
|
|
148
101
|
registerHostBehavior(ACTIVITY_INDICATOR_TAG, activityIndicatorBehavior(platform));
|
|
102
|
+
// A REGISTRATION WITH NO RUNTIME, and it is what hands the spinner's tag to the host.
|
|
103
|
+
//
|
|
104
|
+
// A tag crosses only through `recordSetTag`, which `attachHostBehavior` emits and nothing else
|
|
105
|
+
// does — so a tag with no behavior registered carries an EMPTY `tagName` in C++ and no rule can
|
|
106
|
+
// fire for it. The spinner is built by `buildStructure` and no app ever names it, so it had a tag,
|
|
107
|
+
// had platform semantics, and the host could not see either. Its rule lives in
|
|
108
|
+
// `SymbioteFabricProps.cpp` now (the size translation, RN's two `!== false` defaults, the
|
|
109
|
+
// platform's default colour), which is why this registration has to exist even though there is no
|
|
110
|
+
// JS left to run.
|
|
111
|
+
//
|
|
112
|
+
// Not a workaround for the seam: a registration is how this codebase declares that a tag HAS
|
|
113
|
+
// platform semantics, which is exactly the claim. Emitting the tag from `createElement` for every
|
|
114
|
+
// node was the alternative and is rejected where `attachHostBehavior` explains itself — an app's
|
|
115
|
+
// own `<div>`-equivalent would pay an intern and an op to name something the host has no rule for.
|
|
116
|
+
registerHostBehavior(ACTIVITY_INDICATOR_SPINNER_TAG, {
|
|
117
|
+
attach() { },
|
|
118
|
+
detach() { },
|
|
119
|
+
});
|
|
149
120
|
}
|
|
@@ -1,2 +1,13 @@
|
|
|
1
1
|
export declare const BUTTON_TAG = "button";
|
|
2
|
+
/**
|
|
3
|
+
* NO MEMO, and the earlier version's memo is deliberately gone. It guarded `setProp`'s `Object.is`,
|
|
4
|
+
* which a FRESH style object per call can never satisfy — so pushing unconditionally would have
|
|
5
|
+
* dirtied a node on every commit and re-committed forever
|
|
6
|
+
* (`.claude/rules/list-geometry-feedback-loop.md`). A payload fold does not go through `setProp`:
|
|
7
|
+
* its result reaches `reconcile`, which compares against the mirror with a recursive `propsEqual`
|
|
8
|
+
* (commit.ts) and reuses the committed handle when nothing moved. An equal-but-fresh style is
|
|
9
|
+
* therefore not a change, and there is nothing to feed back.
|
|
10
|
+
*/
|
|
11
|
+
export declare const BUTTON_LABEL_TEXT_TAG = "button-label-text";
|
|
12
|
+
export declare const BUTTON_LABEL_TAG = "button-label";
|
|
2
13
|
export declare function registerButtonBehavior(): void;
|
|
@@ -12,17 +12,23 @@
|
|
|
12
12
|
// Android Button's own `<View style={buttonStyles}>` IS the responder:
|
|
13
13
|
//
|
|
14
14
|
// iOS button RCTView TouchableOpacity's Animated.View — the responder + the fade
|
|
15
|
-
// └ view RCTView
|
|
16
|
-
// └ text RCTText
|
|
17
|
-
// └ raw RCTRawText
|
|
15
|
+
// └ view RCTView no style at all, and no fold
|
|
16
|
+
// └ text RCTText `button-label-text` — foldButtonLabelStyle + RN's Text defaults
|
|
17
|
+
// └ raw RCTRawText `button-label` — foldButtonLabel FOUR nodes
|
|
18
18
|
//
|
|
19
19
|
// Android button RCTView the styled button view, CLONED onto: the responder, the ripple
|
|
20
20
|
// │ background, the whole a11y fold. No fade, no wrapper.
|
|
21
21
|
// └ text RCTText
|
|
22
22
|
// └ raw RCTRawText UPPERCASED (Button.js:352-353) THREE nodes
|
|
23
23
|
//
|
|
24
|
-
// EVERY
|
|
25
|
-
//
|
|
24
|
+
// EVERY RULE IS IN THE ENGINE AS OF 2026-09-18, and off Android this primitive binds no
|
|
25
|
+
// `payloadFold` on any of its four nodes — it costs ZERO trips into JS, down from five. The one that
|
|
26
|
+
// survives is the OWNER's on Android, for the view style and the ripple background.
|
|
27
|
+
//
|
|
28
|
+
// The last to move was the label text's, and it needed a seam none of the others did: its style is a
|
|
29
|
+
// function of the BUTTON's `color` and `disabled`, and the button is its GRANDPARENT here and its
|
|
30
|
+
// parent on Android. `IAncestorLookup` asks for the nearest ancestor carrying a tag — a CSS ancestor
|
|
31
|
+
// selector — so one rule is correct on both trees.
|
|
26
32
|
//
|
|
27
33
|
// ---------------------------------------------------------------------------------------------
|
|
28
34
|
// HOW THE PROJECTION REACHES ITS NODES, given that each `payloadFold` MUST be pure:
|
|
@@ -105,12 +111,9 @@
|
|
|
105
111
|
// since 2026-09-09, in the same commit that deleted the five wrappers, which is what makes it safe:
|
|
106
112
|
// while a wrapper still built its own view and text under this tag, registering would have given
|
|
107
113
|
// every Button a second copy of the subtree.
|
|
108
|
-
import { addDerivedNode, appendChild,
|
|
114
|
+
import { addDerivedNode, appendChild, createElement, createRawText, markPropsDirty, Platform, registerHostBehavior, requestCommitFor, setProp, } from '@symbiote-native/engine';
|
|
109
115
|
import { descriptorFor } from '../component-names';
|
|
110
|
-
import {
|
|
111
|
-
import { BUTTON_ACCESSIBILITY_ROLE, resolveButtonDisabled, resolveButtonImportantForAccessibility, resolveButtonTextStyle, resolveButtonTitle, resolveButtonViewStyle, } from '../view/render-button.js';
|
|
112
|
-
import { backgroundProps, selectableBackground, } from '../view/render-touchable-native-feedback.js';
|
|
113
|
-
import { resolveTouchableFocusable } from '../view/render-pressable/index.js';
|
|
116
|
+
import { resolveButtonDisabled } from '../view/render-button.js';
|
|
114
117
|
import { booleanOr, createPressBehavior, } from './pressable.js';
|
|
115
118
|
import { nativeFeedbackRefinement } from './touchable-native-feedback.js';
|
|
116
119
|
import { createTouchableOpacityBehavior } from './touchable-opacity.js';
|
|
@@ -166,29 +169,39 @@ const buttonDisabled = props => projectionOf(props).disabled;
|
|
|
166
169
|
* (commit.ts) and reuses the committed handle when nothing moved. An equal-but-fresh style is
|
|
167
170
|
* therefore not a change, and there is nothing to feed back.
|
|
168
171
|
*/
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
//
|
|
188
|
-
//
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
172
|
+
// THE VIEW'S FOLD IS GONE AND WAS NOT PORTED — it was doing nothing, on the only platform where it
|
|
173
|
+
// ran. It wrote `style: resolveButtonViewStyle(color, disabled)`, and that function returns the
|
|
174
|
+
// constant `buttonViewStyle` on every platform but Android while the view node is built ONLY in the
|
|
175
|
+
// non-Android branch of `buildStructure`. `buttonViewStyle` off Android is `{}`. So it read two
|
|
176
|
+
// props off its owner, discarded both, and spent a JSI round trip per button per commit to write an
|
|
177
|
+
// empty style.
|
|
178
|
+
//
|
|
179
|
+
// This is the `input-accessory-view` shape again, and the second time this migration has found one:
|
|
180
|
+
// a fold's price is the TRIP, not the body, so a fold that does nothing is the worst value in the
|
|
181
|
+
// file and deleting it is worth as much as porting one that does a lot.
|
|
182
|
+
//
|
|
183
|
+
// Proven not to move the payload rather than argued: `button-derived-payload.itest.ts` pins the
|
|
184
|
+
// view's committed keys, including with an app-set `color` — which lands on the LABEL here and must
|
|
185
|
+
// not reach this node.
|
|
186
|
+
// THE LABEL TEXT'S TAG. Its style is a function of the BUTTON's `color` and `disabled`, and the
|
|
187
|
+
// button is this node's grandparent on iOS (`button -> view -> text`) and its parent on Android —
|
|
188
|
+
// so the rule asks for the NEAREST BUTTON ancestor rather than for a fixed number of hops, which is
|
|
189
|
+
// the same question a CSS ancestor selector asks and is true on both trees.
|
|
190
|
+
//
|
|
191
|
+
// `foldButtonLabelStyle` in `SymbioteFabricProps.cpp`, reached through `IAncestorLookup`. That seam
|
|
192
|
+
// was the thing this fold was waiting for: `ownerProps` answers "my parent" and this node's parent
|
|
193
|
+
// is the wrapping view, which knows none of it.
|
|
194
|
+
export const BUTTON_LABEL_TEXT_TAG = 'button-label-text';
|
|
195
|
+
// The label's own tag. A raw text carrying one looks odd and is not: it has no props an app can
|
|
196
|
+
// write, but its CONTENT is the platform's decision here — RN renders a button's title uppercased on
|
|
197
|
+
// Android and verbatim elsewhere (`Button.js:352-353`), which is a user-agent choice about a control
|
|
198
|
+
// rather than anything the app asked for.
|
|
199
|
+
//
|
|
200
|
+
// That is what the fold here used to do, and it is `foldButtonLabel` in `SymbioteFabricProps.cpp`
|
|
201
|
+
// now, reached off this tag. `button-payload.itest.ts` recorded "a raw text carries no tag at all,
|
|
202
|
+
// so there is nothing for a tag-keyed rule to key on" — true of `createRawText`'s old signature, not
|
|
203
|
+
// of raw texts, and it takes a tag now for exactly this.
|
|
204
|
+
export const BUTTON_LABEL_TAG = 'button-label';
|
|
192
205
|
// ---- the Android touchable -------------------------------------------------------------------
|
|
193
206
|
// Button.js:281-284. Not two variants of one component: see the tree diagram at the top for what
|
|
194
207
|
// wrapping instead of cloning costs. The Android arm composes the bare press machine, so no
|
|
@@ -212,48 +225,37 @@ const touchable = IS_ANDROID
|
|
|
212
225
|
* fold then merges `props.disabled` over it, which composes to RN's
|
|
213
226
|
* `props.disabled ?? aria ?? state.disabled` — the same value, with
|
|
214
227
|
* busy/checked/expanded/selected preserved, without a Button-specific fold.
|
|
228
|
+
*
|
|
229
|
+
* THE TOUCHABLE'S HALF IS NO LONGER A FUNCTION ON EITHER PLATFORM, and the absence is the design
|
|
230
|
+
* rather than a gap: neither `createPressBehavior` nor `createTouchableOpacityBehavior` has a
|
|
231
|
+
* `foldPayload` any more, because both rules moved into the engine (`foldPressableProps` and
|
|
232
|
+
* `foldIdAlias`, `SymbioteFabricProps.cpp`), which names `button` among the tags it serves. So the
|
|
233
|
+
* same work happens, one layer down and before this fold runs — the order is unchanged, the trip
|
|
234
|
+
* into JS is gone.
|
|
235
|
+
*
|
|
236
|
+
* The composition used to be spelled `touchable.foldPayload === undefined ? props : ...`, and that
|
|
237
|
+
* shape is deleted rather than left standing at its `undefined` branch: a conditional call through
|
|
238
|
+
* a field nothing assigns any more is a whole rule that vanishes silently the day the field is
|
|
239
|
+
* removed, which is exactly how it would have gone unnoticed here.
|
|
215
240
|
*/
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
}
|
|
235
|
-
const { color, disabled } = projectionOf(props);
|
|
236
|
-
// TouchableOpacity.js:336 and TouchableNativeFeedback.js:369 — the SAME expression, so the tag
|
|
237
|
-
// owes it on both platforms. `onPress` is an owned name, so it is in the stash and never in
|
|
238
|
-
// `props`; a flip of it dirties nothing by itself, which `onOwnedListenerChange` answers.
|
|
239
|
-
next.focusable = resolveTouchableFocusable(booleanOr(props.focusable), appListenerFor(node, 'press') !== undefined, disabled);
|
|
240
|
-
if (IS_ANDROID) {
|
|
241
|
-
// TNF renders no view, it CLONES onto Button's `<View style={buttonStyles}>`
|
|
242
|
-
// (TouchableNativeFeedback.js:339), so this host IS that view. Overwritten rather than merged
|
|
243
|
-
// because RN's Button declares no `style` prop at all — there is nothing to compose with.
|
|
244
|
-
next.style = resolveButtonViewStyle(color, disabled);
|
|
245
|
-
// Button passes no `background` and no `useForeground`, so TNF resolves the theme's
|
|
246
|
-
// selectable background onto the background slot (TouchableNativeFeedback.js:343-348,
|
|
247
|
-
// :402). The dicts are the shared factories', never restated here.
|
|
248
|
-
Object.assign(next, backgroundProps(selectableBackground(), false));
|
|
249
|
-
}
|
|
250
|
-
// Read by the folds above and declared by no ViewConfig. A key Fabric does not know throws
|
|
251
|
-
// nothing, logs nothing and paints nothing, so the strip has to be here or it is never noticed.
|
|
252
|
-
// `title` needs none — `SLOT_PROPS` redirects it before it can land on this node.
|
|
253
|
-
delete next.color;
|
|
254
|
-
return next;
|
|
255
|
-
};
|
|
256
|
-
}
|
|
241
|
+
// `focusable` LEFT THIS FOLD ON 2026-09-18, and with it the whole fold off Android.
|
|
242
|
+
//
|
|
243
|
+
// It was the last thing here that ran on both platforms, and it stayed because its middle leg is
|
|
244
|
+
// `onPress !== undefined` — an owned listener, stashed in JS. That bit crosses now
|
|
245
|
+
// (`OP_SET_OWNED_LISTENER`), and Button's three-way `disabled` was only ever three PROPS, so
|
|
246
|
+
// `foldButtonProps` resolves the expression itself. It reads the AUTHORED bag rather than the folded
|
|
247
|
+
// one, which is the same Trap A correction this fold carried as `projectionOf(propsOf(node))`.
|
|
248
|
+
//
|
|
249
|
+
// The ANDROID half went the same day, once the test host grew an arm that compiles `#ifdef ANDROID`
|
|
250
|
+
// (`tests/CMakeLists.txt`, `SYMBIOTE_PLATFORM_ANDROID`). It is inside `foldButtonProps` now: the
|
|
251
|
+
// Material view style and the theme's selectable background, which TNF clones onto this very node
|
|
252
|
+
// (`TouchableNativeFeedback.js:339`) because it renders no view of its own.
|
|
253
|
+
//
|
|
254
|
+
// SO BUTTON BINDS NO FOLD ON EITHER PLATFORM, and it is the first primitive to reach that with a
|
|
255
|
+
// subtree — four nodes, four crossings per commit when this migration started.
|
|
256
|
+
//
|
|
257
|
+
// Contract: `core/engine/cpp/tests/js/button-payload.itest.ts` for the platform-invariant half and
|
|
258
|
+
// `android-rules.itest.ts` for the style, the colour override and the disabled greying.
|
|
257
259
|
/**
|
|
258
260
|
* Builds the whole subtree, once, at `attachHostBehavior`.
|
|
259
261
|
*
|
|
@@ -265,18 +267,18 @@ function ownerFold(node) {
|
|
|
265
267
|
*/
|
|
266
268
|
function buildStructure(node) {
|
|
267
269
|
const textDescriptor = descriptorFor('text');
|
|
268
|
-
const text = createElement(textDescriptor.component, textDescriptor.isText,
|
|
269
|
-
// RN's Text
|
|
270
|
-
//
|
|
271
|
-
//
|
|
272
|
-
//
|
|
273
|
-
|
|
270
|
+
const text = createElement(textDescriptor.component, textDescriptor.isText, BUTTON_LABEL_TEXT_TAG);
|
|
271
|
+
// RN's two Text defaults are NOT written here, and that is deliberate as of 2026-09-18: they are
|
|
272
|
+
// the platform's, applied by the payload builder to every `RCTText` (`foldTextDefaults`), so this
|
|
273
|
+
// node inherits them for being a text rather than for being handed them. Seeding them was two
|
|
274
|
+
// writes per button per commit producing the payload the builder already produces — the shape
|
|
275
|
+
// `seedTextDefaults` had in three adapters. `button-derived-payload.itest.ts` reads them off the
|
|
276
|
+
// committed payload and is what proves the node still gets them.
|
|
277
|
+
//
|
|
274
278
|
// Empty until the redirected `title` arrives. The commit walk drops an empty raw text
|
|
275
279
|
// (`isEmptyRawText`, node.ts), so no Fabric node exists for it until it has a label — and that
|
|
276
280
|
// check reads `props.text`, which the redirect writes, not the fold's uppercased output.
|
|
277
|
-
const label = createRawText('');
|
|
278
|
-
text.payloadFold = textFold(node);
|
|
279
|
-
label.payloadFold = labelFold;
|
|
281
|
+
const label = createRawText('', BUTTON_LABEL_TAG);
|
|
280
282
|
// The hop `slotDerived` alone does not make: it marks the slot (the label), and this is past it.
|
|
281
283
|
addDerivedNode(node, text);
|
|
282
284
|
appendChild(text, label);
|
|
@@ -288,7 +290,8 @@ function buildStructure(node) {
|
|
|
288
290
|
else {
|
|
289
291
|
const viewDescriptor = descriptorFor('view');
|
|
290
292
|
const view = createElement(viewDescriptor.component, viewDescriptor.isText, 'view');
|
|
291
|
-
|
|
293
|
+
// No fold: see the note where `viewFold` was. Off Android this node's style was `{}` and this
|
|
294
|
+
// branch is the only one that builds it, so the fold was a crossing bought for an empty object.
|
|
292
295
|
addDerivedNode(node, view);
|
|
293
296
|
appendChild(view, text);
|
|
294
297
|
// Lands on the owner, because `node.childHost` is still undefined here — the engine assigns it
|
|
@@ -296,9 +299,10 @@ function buildStructure(node) {
|
|
|
296
299
|
// setting the field itself.
|
|
297
300
|
appendChild(node, view);
|
|
298
301
|
}
|
|
299
|
-
// See the header: the owner's fold needs its own node, and this
|
|
300
|
-
//
|
|
301
|
-
|
|
302
|
+
// ANDROID ONLY since 2026-09-18. See the header: the owner's fold needs its own node, and this
|
|
303
|
+
// NOTHING IS BOUND HERE ON EITHER PLATFORM as of 2026-09-18, which is the point — a fold with an
|
|
304
|
+
// empty body still costs a full JSI round trip per commit, so leaving one that returns its input
|
|
305
|
+
// is the worst value available (`input-accessory-view`, and Button's own `viewFold`).
|
|
302
306
|
return label;
|
|
303
307
|
}
|
|
304
308
|
// `focusable` is a function of a LISTENER, and a listener flip changes no payload by itself — so
|
|
@@ -315,14 +319,22 @@ export function registerButtonBehavior() {
|
|
|
315
319
|
// that leave with the sweep, and each carries only a pure fold, so this behavior owns no per-node
|
|
316
320
|
// runtime of its own to release.
|
|
317
321
|
//
|
|
318
|
-
// No `foldPayload` here on purpose — `buildStructure` binds the owner's fold to its node.
|
|
319
322
|
const behavior = {
|
|
320
323
|
...touchable,
|
|
321
|
-
foldPayload: undefined,
|
|
322
324
|
buildStructure,
|
|
323
325
|
onOwnedListenerChange,
|
|
324
326
|
slotProps: SLOT_PROPS,
|
|
325
327
|
slotDerived: SLOT_DERIVED,
|
|
326
328
|
};
|
|
329
|
+
// The two DERIVED nodes' tags, registered with no runtime at all. A tag reaches C++ only through
|
|
330
|
+
// `recordSetTag`, which `attachHostBehavior` emits, so a tag nobody registered carries an empty
|
|
331
|
+
// `tagName` in the host and no rule fires for it — however the rule is written. Same shape as the
|
|
332
|
+
// ActivityIndicator spinner's and ImageBackground's inner image.
|
|
333
|
+
//
|
|
334
|
+
// A registration is how this codebase declares a tag HAS platform semantics, which is exactly the
|
|
335
|
+
// claim: the label's style is RN's, not the app's.
|
|
336
|
+
const derived = { attach() { }, detach() { } };
|
|
337
|
+
registerHostBehavior(BUTTON_LABEL_TEXT_TAG, derived);
|
|
338
|
+
registerHostBehavior(BUTTON_LABEL_TAG, derived);
|
|
327
339
|
registerHostBehavior(BUTTON_TAG, behavior);
|
|
328
340
|
}
|