@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
|
@@ -14,9 +14,9 @@
|
|
|
14
14
|
// `registerPressableBehavior()`, and its entry does a bare `import './register';` that the barrel
|
|
15
15
|
// does NOT re-export. A bare `import './register';` sitting NEXT TO such a re-export does not work
|
|
16
16
|
// either: Babel merges the two imports of one specifier and the merged dependency stays lazy.
|
|
17
|
-
import { appListenerFor, dlog, registerHostBehavior, requestCommitFor, setBehaviorListener, setNodePressed, } from '@symbiote-native/engine';
|
|
18
|
-
import { createPressHandlers, createPressRuntime, disposePressRuntime, DEFAULT_DELAY_LONG_PRESS_MS, DEFAULT_MIN_PRESS_DURATION_MS,
|
|
19
|
-
import { buildPressableListeners
|
|
17
|
+
import { appListenerFor, dlog, propOf, registerHostBehavior, requestCommitFor, setBehaviorListener, setNodePressed, propsOf, } from '@symbiote-native/engine';
|
|
18
|
+
import { createPressHandlers, createPressRuntime, disposePressRuntime, DEFAULT_DELAY_LONG_PRESS_MS, DEFAULT_MIN_PRESS_DURATION_MS, } from '../state/pressable.js';
|
|
19
|
+
import { buildPressableListeners } from '../view/render-pressable/index.js';
|
|
20
20
|
export const PRESSABLE_TAG = 'pressable';
|
|
21
21
|
const states = new WeakMap();
|
|
22
22
|
function isRecord(value) {
|
|
@@ -56,24 +56,7 @@ function asRectOffset(value) {
|
|
|
56
56
|
function isPressHandler(value) {
|
|
57
57
|
return typeof value === 'function';
|
|
58
58
|
}
|
|
59
|
-
//
|
|
60
|
-
// destructuring — they go into `createPressHandlers` / `buildPressableListeners` and are simply
|
|
61
|
-
// absent from the object it spreads onto its View. A lowered element has no destructure, so every
|
|
62
|
-
// one of them rode into the payload as a key no ViewConfig declares.
|
|
63
|
-
const MACHINE_ONLY_KEYS = [
|
|
64
|
-
// Consumed below and replaced by the resolved `nativeBackgroundAndroid` /
|
|
65
|
-
// `nativeForegroundAndroid`; the raw config is not a native prop.
|
|
66
|
-
'android_ripple',
|
|
67
|
-
'disabled',
|
|
68
|
-
'cancelable',
|
|
69
|
-
'delayLongPress',
|
|
70
|
-
'minPressDuration',
|
|
71
|
-
'unstable_pressDelay',
|
|
72
|
-
'pressRetentionOffset',
|
|
73
|
-
'delayHoverIn',
|
|
74
|
-
'delayHoverOut',
|
|
75
|
-
];
|
|
76
|
-
// Narrowed field by field rather than cast: the bag arrives as `unknown` off `node.props`. A local
|
|
59
|
+
// Narrowed field by field rather than cast: the bag arrives as `unknown` off `propOf`. A local
|
|
77
60
|
// twin of the guard each adapter keeps for its own attrs (Vue's `asAccessibilityState`) — exported
|
|
78
61
|
// to the sibling behaviors that fold the same bag, and deliberately NOT to the shared barrel, which
|
|
79
62
|
// every adapter re-exports wholesale: a narrowing helper is not API anyone should be able to import.
|
|
@@ -93,108 +76,81 @@ export function asAccessibilityState(value) {
|
|
|
93
76
|
state.expanded = value.expanded;
|
|
94
77
|
return state;
|
|
95
78
|
}
|
|
96
|
-
//
|
|
97
|
-
//
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
if (typeof value.color === 'string')
|
|
103
|
-
config.color = value.color;
|
|
104
|
-
if (typeof value.borderless === 'boolean')
|
|
105
|
-
config.borderless = value.borderless;
|
|
106
|
-
if (typeof value.radius === 'number')
|
|
107
|
-
config.radius = value.radius;
|
|
108
|
-
if (typeof value.foreground === 'boolean')
|
|
109
|
-
config.foreground = value.foreground;
|
|
110
|
-
return config;
|
|
111
|
-
}
|
|
112
|
-
// `disabled` reaches a screen reader ONLY as `accessibilityState.disabled` — it is not a native
|
|
113
|
-
// View prop, so the wrapper folds it (`resolveDisabledAccessibilityState`, called by all five) and
|
|
114
|
-
// forwards the composite. Lowering dropped that fold: press suppression still worked, because the
|
|
115
|
-
// machine reads `node.props.disabled` directly, so the button behaved correctly and announced
|
|
116
|
-
// itself as enabled. An accessibility regression with no visual tell and no failing test.
|
|
79
|
+
// THE PAYLOAD FOLD MOVED TO THE ENGINE — `foldPressableProps` in `SymbioteFabricProps.cpp`, and
|
|
80
|
+
// its contract is `core/engine/cpp/tests/js/pressable-payload.itest.ts`. It is not re-implemented
|
|
81
|
+
// here in any form, which is the point: `disabled` -> `accessibilityState`, `accessible`/`focusable`
|
|
82
|
+
// defaulting on, the Android ripple config, and the nine machine-only keys being kept out of the
|
|
83
|
+
// payload are all functions of the TAG alone. That is user-agent behavior — RN does it for every
|
|
84
|
+
// Pressable in every app — and it belongs beside the tree, like a browser's `<button>`.
|
|
117
85
|
//
|
|
118
|
-
//
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
//
|
|
127
|
-
// `rippleProps` returns undefined off Android, so this whole branch is inert on iOS.
|
|
128
|
-
//
|
|
129
|
-
// STILL MISSING ON BOTH PATHS, and lowering did not cause it: RN also dispatches
|
|
130
|
-
// `Commands.hotspotUpdate(x, y)` on pressIn/pressMove and `Commands.setPressed` on
|
|
131
|
-
// pressIn/pressOut, which is what makes the ripple originate at the touch point. Neither our
|
|
132
|
-
// wrapper nor this behavior sends them — grep for `hotspotUpdate` returns nothing in the tree.
|
|
133
|
-
const rippleConfig = asRippleConfig(props.android_ripple);
|
|
134
|
-
const ripple = rippleConfig !== undefined ? rippleProps(rippleConfig) : undefined;
|
|
135
|
-
const out = { ...props };
|
|
136
|
-
for (const key of MACHINE_ONLY_KEYS)
|
|
137
|
-
delete out[key];
|
|
138
|
-
if (ripple !== undefined)
|
|
139
|
-
Object.assign(out, ripple);
|
|
140
|
-
// Written only when the fold produced something: an unconditional assignment would put an
|
|
141
|
-
// `accessibilityState: undefined` key on every lowered Pressable in the tree, and `fabricProps`
|
|
142
|
-
// skipping undefined is a coincidence to lean on, not a contract to rely on here.
|
|
143
|
-
if (resolved !== undefined)
|
|
144
|
-
out.accessibilityState = resolved;
|
|
145
|
-
out.accessible = accessibleUnlessOptedOut(props);
|
|
146
|
-
// Pressable.js:258 — the plain form, with no press-handler or disabled leg. A Touchable composing
|
|
147
|
-
// this tag has already resolved its own three-leg formula and passed the answer in as `focusable`,
|
|
148
|
-
// which `!== false` leaves alone.
|
|
149
|
-
out.focusable = resolvePressableFocusable(booleanOr(props.focusable));
|
|
150
|
-
return out;
|
|
151
|
-
}
|
|
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.
|
|
152
94
|
// RN makes every pressable accessible unless the app opts OUT — `Pressable.js:252`
|
|
153
95
|
// (`accessible: accessible !== false`), and the whole Touchable family repeats it verbatim
|
|
154
96
|
// (`TouchableOpacity.js:303`, `TouchableHighlight.js:337`). `!== false` rather than `?? true`, so
|
|
155
97
|
// only a literal `false` opts out and an explicit `undefined` still reads as accessible.
|
|
156
98
|
//
|
|
157
|
-
// Nothing in this repo did it until 2026-09-09,
|
|
158
|
-
//
|
|
159
|
-
// four equivalence arms — correctly, since those compare the wrapper against the lowered path — so
|
|
160
|
-
// the behavior and every adapter's wrapper have to move in ONE change. Exported for the wrappers
|
|
161
|
-
// that need to say it themselves, and for the tags whose behavior is their only path.
|
|
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.
|
|
162
101
|
export function accessibleUnlessOptedOut(props) {
|
|
163
102
|
return props.accessible !== false;
|
|
164
103
|
}
|
|
165
|
-
// From the STASH, not from
|
|
104
|
+
// From the STASH, not from the props. Every name below is in `ownedListeners`, so `routeProp`
|
|
166
105
|
// diverts the app's `onPress` away from `node.listeners` (where it would evict the behavior's own
|
|
167
106
|
// dispatcher) and into the stash — which makes the stash the only place it exists. Reading
|
|
168
|
-
// `
|
|
107
|
+
// `propOf` here returns undefined for every callback and every press silently does nothing:
|
|
169
108
|
// the behavior runs, the machine runs, and it calls nobody.
|
|
170
109
|
function callbackAt(node, event) {
|
|
171
110
|
const value = appListenerFor(node, event);
|
|
172
111
|
return isPressHandler(value) ? value : undefined;
|
|
173
112
|
}
|
|
174
|
-
// Callbacks come from `callbackAt` (the stash), scalars from `
|
|
113
|
+
// Callbacks come from `callbackAt` (the stash), scalars from `propOf`. The split is not
|
|
175
114
|
// cosmetic: `delayLongPress` and `hitSlop` are ordinary props that `fabricProps` drops as unknown
|
|
176
|
-
// keys, while `onPress` and friends are OWNED event names that never reach
|
|
115
|
+
// keys, while `onPress` and friends are OWNED event names that never reach the props at all.
|
|
177
116
|
function configFor(node) {
|
|
117
|
+
const unstablePressDelay = numberOr(propOf(node, 'unstable_pressDelay'), 0);
|
|
178
118
|
return {
|
|
179
119
|
onPress: callbackAt(node, 'press'),
|
|
180
120
|
onPressIn: callbackAt(node, 'pressIn'),
|
|
181
121
|
onPressOut: callbackAt(node, 'pressOut'),
|
|
182
122
|
onPressMove: callbackAt(node, 'pressMove'),
|
|
183
123
|
onLongPress: callbackAt(node, 'longPress'),
|
|
184
|
-
|
|
185
|
-
|
|
124
|
+
// Pressability.js:471-474 — `normalizeDelay(authored, 10, DEFAULT_LONG_PRESS_DELAY_MS -
|
|
125
|
+
// delayPressIn)`. The subtraction applies only to the FALLBACK, never to an authored value —
|
|
126
|
+
// it exists so the long-press threshold, timed from the grant that also arms
|
|
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).
|
|
131
|
+
delayLongPress: Math.max(10, numberOr(propOf(node, 'delayLongPress'), DEFAULT_DELAY_LONG_PRESS_MS - unstablePressDelay)),
|
|
132
|
+
unstable_pressDelay: unstablePressDelay,
|
|
186
133
|
// RN's Touchables own the deactivation floor in their OWN machine and hand Pressability
|
|
187
134
|
// `minPressDuration: 0` (TouchableOpacity.js:195). While they were wrappers they passed it as
|
|
188
135
|
// an internal input; on the tag there is nowhere else to say it, so the floor has to be a
|
|
189
136
|
// readable prop or every Touchable holds its fade for the machine's 130 ms default.
|
|
190
|
-
minPressDuration: numberOr(node
|
|
191
|
-
hitSlop: asRectOffset(node
|
|
192
|
-
pressRetentionOffset: asRectOffset(node
|
|
137
|
+
minPressDuration: numberOr(propOf(node, 'minPressDuration'), DEFAULT_MIN_PRESS_DURATION_MS),
|
|
138
|
+
hitSlop: asRectOffset(propOf(node, 'hitSlop')),
|
|
139
|
+
pressRetentionOffset: asRectOffset(propOf(node, 'pressRetentionOffset')),
|
|
140
|
+
// Pressable.js's own name is `android_disableSound`; every composed touchable (Highlight,
|
|
141
|
+
// NativeFeedback, WithoutFeedback, Button) instead forwards vendor's `touchSoundDisabled` to
|
|
142
|
+
// this same config field (`TouchableHighlight.js:205`, `TouchableWithoutFeedback.js:199`,
|
|
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.
|
|
147
|
+
android_disableSound: booleanOr(propOf(node, 'android_disableSound')) ??
|
|
148
|
+
booleanOr(propOf(node, 'touchSoundDisabled')),
|
|
193
149
|
};
|
|
194
150
|
}
|
|
195
151
|
// Rebuilding at GESTURE START is the whole reason for the dispatcher indirection, and skipping it
|
|
196
152
|
// is a bug that looks like working code. `attach` runs inside `createElement`, before a single
|
|
197
|
-
// prop has been routed —
|
|
153
|
+
// prop has been routed — the node holds nothing at all there — so a machine built at attach would
|
|
198
154
|
// capture no `onPress` at all and every press would silently do nothing. `createPressHandlers`
|
|
199
155
|
// destructures its config eagerly, so it cannot be handed a live view either; it has to be re-made
|
|
200
156
|
// once the props exist. A gesture is one interaction, so a handful of closures per press is
|
|
@@ -208,16 +164,30 @@ function rebuild(node, state) {
|
|
|
208
164
|
state.isBuilt = true;
|
|
209
165
|
// Re-read every gesture, so a tag whose resolver looks past `disabled` — `./button`, at
|
|
210
166
|
// `aria-disabled` — re-enables on the next touch instead of latching at its first answer.
|
|
167
|
+
const sourceProps = propsOf(source);
|
|
211
168
|
const disabled = state.disabledOf === undefined
|
|
212
|
-
?
|
|
213
|
-
: state.disabledOf(
|
|
169
|
+
? sourceProps.disabled
|
|
170
|
+
: state.disabledOf(sourceProps);
|
|
214
171
|
state.listeners = buildPressableListeners(handlers, {
|
|
215
172
|
disabled: disabled === true ? true : undefined,
|
|
216
|
-
cancelable:
|
|
217
|
-
|
|
218
|
-
: undefined,
|
|
173
|
+
cancelable: resolveCancelable(source),
|
|
174
|
+
blockNativeResponder: propOf(source, 'blockNativeResponder') === true,
|
|
219
175
|
});
|
|
220
176
|
}
|
|
177
|
+
// Pressable is the only member of the family with a `cancelable` prop of its own (`Pressable.js:41`).
|
|
178
|
+
// TouchableOpacity/TouchableHighlight/TouchableNativeFeedback instead expose `rejectResponderTermination`
|
|
179
|
+
// and derive `cancelable: !this.props.rejectResponderTermination` internally
|
|
180
|
+
// (`TouchableOpacity.js:186`, `TouchableHighlight.js:194`, `TouchableNativeFeedback.js:217`) — every
|
|
181
|
+
// composed touchable shares this `rebuild()`, so without this the authored name never reached the
|
|
182
|
+
// machine and every Touchable silently kept the RN native default (yield the responder) regardless of
|
|
183
|
+
// what the app asked for. An explicit `cancelable` still wins, matching Pressable's own precedence.
|
|
184
|
+
function resolveCancelable(source) {
|
|
185
|
+
const cancelable = propOf(source, 'cancelable');
|
|
186
|
+
if (typeof cancelable === 'boolean')
|
|
187
|
+
return cancelable;
|
|
188
|
+
const reject = propOf(source, 'rejectResponderTermination');
|
|
189
|
+
return typeof reject === 'boolean' ? !reject : undefined;
|
|
190
|
+
}
|
|
221
191
|
// WHICHEVER EVENT OPENS THE GESTURE REBUILDS, and pinning that to one name was a real bug.
|
|
222
192
|
// `onStartShouldSetResponder` looks like the opener and is not: `core/engine/src/events/index.ts`
|
|
223
193
|
// bubbles PRESS_IN and only THEN calls `negotiateResponder`, so `pressIn` arrives FIRST on every
|
|
@@ -254,16 +224,35 @@ function dispatch(node, state, key, args) {
|
|
|
254
224
|
//
|
|
255
225
|
// The engine event names, not the `onX` prop spellings. `buildPressableListeners` speaks the prop
|
|
256
226
|
// spelling, so the two are mapped here rather than guessed at either end.
|
|
257
|
-
|
|
227
|
+
/**
|
|
228
|
+
* Engine event name -> the app-facing callback key its dispatcher routes to.
|
|
229
|
+
*
|
|
230
|
+
* AN ARRAY OF PAIRS RATHER THAN A `Map`, and the reason is measured rather than stylistic.
|
|
231
|
+
* `installListeners` below is the only reader, and it runs once per node that carries a press
|
|
232
|
+
* machine — which is every `<TextInput>` on the screen, not just every `<Pressable>`. A `for…of` over
|
|
233
|
+
* a `Map` builds a fresh two-element array per entry for the destructuring to read back: seven
|
|
234
|
+
* allocations per node, for seven fixed pairs that never change. The tuples here already exist, so
|
|
235
|
+
* the same loop allocates nothing but its iterator.
|
|
236
|
+
*
|
|
237
|
+
* Priced on `-O` Hermes by `text-input-attach-ladder.itest.ts`, one pass over the seven pairs:
|
|
238
|
+
* `Map` 1.04 us, this 0.39, two parallel arrays 0.29. The parallel arrays are cheapest and give up
|
|
239
|
+
* the pairing, which is not worth 0.1 us on a table that a drift would silently unwire.
|
|
240
|
+
*
|
|
241
|
+
* NOT the same list as `createPressBehavior`'s `ownedListeners`, and they must not be merged: that
|
|
242
|
+
* one is every name the machine takes as an INPUT (`pressMove` and `longPress` included), this one
|
|
243
|
+
* is only the names it installs a dispatcher for.
|
|
244
|
+
*/
|
|
245
|
+
const EVENT_KEY_PAIRS = [
|
|
258
246
|
['press', 'onPress'],
|
|
259
247
|
['pressIn', 'onPressIn'],
|
|
260
248
|
['pressOut', 'onPressOut'],
|
|
261
249
|
['startShouldSetResponder', 'onStartShouldSetResponder'],
|
|
262
250
|
['responderMove', 'onResponderMove'],
|
|
263
251
|
['responderTerminationRequest', 'onResponderTerminationRequest'],
|
|
264
|
-
]
|
|
252
|
+
['responderGrant', 'onResponderGrant'],
|
|
253
|
+
];
|
|
265
254
|
function installListeners(node, state) {
|
|
266
|
-
for (const [event, key] of
|
|
255
|
+
for (const [event, key] of EVENT_KEY_PAIRS) {
|
|
267
256
|
setBehaviorListener(node, event, symbioteEvent => dispatch(node, state, key, [symbioteEvent]));
|
|
268
257
|
}
|
|
269
258
|
}
|
|
@@ -362,7 +351,6 @@ export function createPressBehavior(refine, disabledOf) {
|
|
|
362
351
|
return {
|
|
363
352
|
attach: attachWith(refine, disabledOf),
|
|
364
353
|
detach,
|
|
365
|
-
foldPayload,
|
|
366
354
|
// Every name the machine needs as an INPUT. The responder pair is not optional — it is how a
|
|
367
355
|
// gesture starts at all, and `RESPONDER_EVENTS` makes those listeners on any node regardless
|
|
368
356
|
// of ViewConfig, so they collide exactly like `press` does.
|
|
@@ -375,6 +363,7 @@ export function createPressBehavior(refine, disabledOf) {
|
|
|
375
363
|
'startShouldSetResponder',
|
|
376
364
|
'responderMove',
|
|
377
365
|
'responderTerminationRequest',
|
|
366
|
+
'responderGrant',
|
|
378
367
|
],
|
|
379
368
|
};
|
|
380
369
|
}
|
|
@@ -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, setBehaviorListener, } from '@symbiote-native/engine';
|
|
32
|
+
import { appListenerFor, dispatchViewCommand, dlog, registerHostBehavior, setBehaviorListener, 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';
|
|
@@ -38,12 +38,25 @@ const SET_NATIVE_REFRESHING = 'setNativeRefreshing';
|
|
|
38
38
|
// authoritative, and an app that drives `refreshing` on its own initiative must never be corrected
|
|
39
39
|
// against a value native never claimed.
|
|
40
40
|
const reported = new WeakMap();
|
|
41
|
+
// TODO(rn-parity, low priority, efficiency not correctness): vendor's `componentDidUpdate`
|
|
42
|
+
// (`RefreshControl.js:139-158`) sends `setNativeRefreshing` only when `refreshing` did NOT change
|
|
43
|
+
// between renders, trusting the ordinary prop diff to carry a real change to native. Ours has no
|
|
44
|
+
// such guard — `evaluateSnapBack` fires the command on any disagreement with `reported`, even when
|
|
45
|
+
// this same commit already carries the authored `refreshing` change (accept-then-finish: app agrees
|
|
46
|
+
// on the gesture, then sets `refreshing: false` to end it — vendor sends zero commands, we send one
|
|
47
|
+
// extra). Both settle on the same native value, so this is a harmless redundant call, not a bug —
|
|
48
|
+
// and NOT a candidate for the same fix as the "repairs a snap-back the app contradicts on a later
|
|
49
|
+
// commit" test below, which relies on this exact unconditional re-check for a genuine cross-
|
|
50
|
+
// framework microtask race. A real fix needs tracking the last-seen authored value alongside
|
|
51
|
+
// `reported`, so a same-commit authored change can be told apart from native drifting on its own —
|
|
52
|
+
// not attempted here.
|
|
53
|
+
//
|
|
41
54
|
// Shared by both triggers — see the module header for why there are two.
|
|
42
55
|
function evaluateSnapBack(node) {
|
|
43
56
|
const lastNativeReport = reported.get(node);
|
|
44
57
|
if (lastNativeReport === undefined)
|
|
45
58
|
return; // no report yet, nothing to disagree with
|
|
46
|
-
const refreshing = node
|
|
59
|
+
const refreshing = propOf(node, 'refreshing') === true;
|
|
47
60
|
if (lastNativeReport === refreshing) {
|
|
48
61
|
dlog(`RefreshControl behavior snap-back no-op refreshing=${refreshing}`);
|
|
49
62
|
return;
|
|
@@ -6,46 +6,34 @@
|
|
|
6
6
|
// wrapper's frame, visual on the scroller (`ScrollView.js:1856`). `nestedScrollEnabled` goes on the
|
|
7
7
|
// inner view so it consumes the gesture before the refresh parent sees it.
|
|
8
8
|
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
//
|
|
28
|
-
//
|
|
29
|
-
//
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
});
|
|
35
|
-
}
|
|
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.
|
|
33
|
+
import { registerScrollViewBehaviors } from './shared.js';
|
|
36
34
|
const android = {
|
|
37
35
|
claimMode: 'wrap',
|
|
38
36
|
slotDerived: ['style'],
|
|
39
|
-
onWrapChange: (base, horizontal) => (owner, wrapper) => {
|
|
40
|
-
// Back to the ordinary composition, not to `undefined` — the plain fold carries the axis and
|
|
41
|
-
// the gesture props, which have nothing to do with the wrap.
|
|
42
|
-
owner.payloadFold =
|
|
43
|
-
wrapper === undefined
|
|
44
|
-
? ownerFold(base, horizontal)
|
|
45
|
-
: wrappedOwnerFold(base, horizontal);
|
|
46
|
-
if (wrapper !== undefined)
|
|
47
|
-
wrapper.payloadFold = wrapperFold(owner, base);
|
|
48
|
-
},
|
|
49
37
|
};
|
|
50
38
|
export function registerScrollViewBehavior() {
|
|
51
39
|
registerScrollViewBehaviors(android);
|
|
@@ -3,3 +3,6 @@
|
|
|
3
3
|
// `render-scroll-view`'s own `Platform.select` defaults to it.
|
|
4
4
|
export { registerScrollViewBehavior } from './index.ios.js';
|
|
5
5
|
export { HORIZONTAL_SCROLL_VIEW_TAG, REFRESH_CONTROL, SCROLL_VIEW_TAG, } from './shared.js';
|
|
6
|
+
// The tag, so a test can locate a committed sticky wrapper by what the engine was TOLD rather than
|
|
7
|
+
// by a key its tag rule writes — see `ILiveNode.tagName`.
|
|
8
|
+
export { STICKY_HEADER_TAG } from './sticky.js';
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { type ISymbioteNode } from '@symbiote-native/engine';
|
|
2
|
+
export declare function markScrollObserved(owner: ISymbioteNode): void;
|
|
3
|
+
export declare const RESPONDER_OWNED_LISTENERS: readonly string[];
|
|
4
|
+
export declare function installResponderPredicates(owner: ISymbioteNode): void;
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
// ScrollView's own participation in the responder negotiation
|
|
2
|
+
// (`core/engine/src/events`) — RN's `ScrollView.js` four predicates
|
|
3
|
+
// (`_handleStartShouldSetResponderCapture`/`_handleStartShouldSetResponder`/
|
|
4
|
+
// `_handleResponderTerminationRequest`) plus the grant/release/momentum bookkeeping they read.
|
|
5
|
+
// Ported line-for-line from `ScrollView.js:1263-1546` — the negotiation engine itself and the
|
|
6
|
+
// `IResponderProps` prop surface already exist and needed no change; this file is ScrollView opting
|
|
7
|
+
// into the same mechanism Pressable already uses (`../pressable.ts:329-384`).
|
|
8
|
+
import { appListenerFor, currentlyFocusedInput, blurTextInput, isRecord, isSymbioteNode, Keyboard, propOf, setBehaviorListener, } from '@symbiote-native/engine';
|
|
9
|
+
import { descriptorFor } from '../../component-names';
|
|
10
|
+
import { TEXT_INPUT_MULTILINE_TAG, TEXT_INPUT_TAG } from '../text-input.js';
|
|
11
|
+
// RN's `IS_ANIMATING_TOUCH_START_THRESHOLD_MS` (ScrollView.js:695) — a momentum end inside this
|
|
12
|
+
// window still counts as animating, so a touch that lands one frame after the list stops still
|
|
13
|
+
// goes to the ScrollView rather than a child it is still settling under.
|
|
14
|
+
const IS_ANIMATING_TOUCH_START_THRESHOLD_MS = 16;
|
|
15
|
+
const stateByOwner = new WeakMap();
|
|
16
|
+
function stateFor(owner) {
|
|
17
|
+
let state = stateByOwner.get(owner);
|
|
18
|
+
if (state === undefined) {
|
|
19
|
+
state = {
|
|
20
|
+
observedScrollSinceBecomingResponder: false,
|
|
21
|
+
becameResponderWhileAnimating: false,
|
|
22
|
+
lastMomentumScrollBeginTime: 0,
|
|
23
|
+
lastMomentumScrollEndTime: 0,
|
|
24
|
+
};
|
|
25
|
+
stateByOwner.set(owner, state);
|
|
26
|
+
}
|
|
27
|
+
return state;
|
|
28
|
+
}
|
|
29
|
+
// RN's `_isAnimating()` (ScrollView.js:1321-1330), unchanged: a momentum scroll in flight
|
|
30
|
+
// (begin > end) or one that just ended within the threshold.
|
|
31
|
+
function isAnimating(state) {
|
|
32
|
+
const sinceEnd = performance.now() - state.lastMomentumScrollEndTime;
|
|
33
|
+
return (sinceEnd < IS_ANIMATING_TOUCH_START_THRESHOLD_MS ||
|
|
34
|
+
state.lastMomentumScrollEndTime < state.lastMomentumScrollBeginTime);
|
|
35
|
+
}
|
|
36
|
+
// The one thing this repo already has that RN builds from native `Keyboard` events:
|
|
37
|
+
// `Keyboard.metrics()` reads the last-known metrics synchronously (`core/engine/src/keyboard`),
|
|
38
|
+
// the same value RN caches on `_keyboardMetrics`. Reused as-is, no new tracking.
|
|
39
|
+
// `node.component` is the resolved Fabric VIEW NAME (`RCTSinglelineTextInputView` on iOS, its own
|
|
40
|
+
// name on Android), not the tag — the tag is consumed at `createElement` and not retained
|
|
41
|
+
// (`core/engine/src/node.ts:466-470`'s own comment). `descriptorFor` resolves the same tag through
|
|
42
|
+
// the same per-platform table TextInput's own registration used, so the two stay in lockstep by
|
|
43
|
+
// construction rather than by a second hardcoded name list here.
|
|
44
|
+
function isTextInputNode(node) {
|
|
45
|
+
return (node.component === descriptorFor(TEXT_INPUT_TAG).component ||
|
|
46
|
+
node.component === descriptorFor(TEXT_INPUT_MULTILINE_TAG).component);
|
|
47
|
+
}
|
|
48
|
+
// RN's `_keyboardIsDismissible()` (ScrollView.js:1527-1541). RN also has `_keyboardEventsAreUnreliable()`
|
|
49
|
+
// — a pre-API-30 Android layout-polling fallback — deliberately not ported: modern targets only,
|
|
50
|
+
// and the omission only ever makes this return `false` more readily (never dismisses when RN
|
|
51
|
+
// would not have been sure either), never a false claim.
|
|
52
|
+
function keyboardIsDismissible() {
|
|
53
|
+
const focused = currentlyFocusedInput();
|
|
54
|
+
const hasFocusedTextInput = focused !== null && isTextInputNode(focused);
|
|
55
|
+
return hasFocusedTextInput && Keyboard.metrics() !== undefined;
|
|
56
|
+
}
|
|
57
|
+
// RN's `_softKeyboardIsDetached()` (ScrollView.js:1543-1546).
|
|
58
|
+
function softKeyboardIsDetached() {
|
|
59
|
+
const metrics = Keyboard.metrics();
|
|
60
|
+
return metrics !== undefined && metrics.height === 0;
|
|
61
|
+
}
|
|
62
|
+
// The should-set listeners run on the SCROLL VIEW's own node (`callOwnListener` sets `event.target`
|
|
63
|
+
// to the node being asked, not the node the touch landed on), so RN's `e.target` has to be read
|
|
64
|
+
// back out of the raw touch payload instead — the same place `core/engine/src/touch-history.ts`
|
|
65
|
+
// and `hasRemainingTouchWithin` (`events/index.ts`) already read a touch's `target` from.
|
|
66
|
+
function touchTargetOf(nativeEvent) {
|
|
67
|
+
for (const key of ['changedTouches', 'touches']) {
|
|
68
|
+
const touches = nativeEvent[key];
|
|
69
|
+
if (!Array.isArray(touches) || touches.length === 0)
|
|
70
|
+
continue;
|
|
71
|
+
const first = touches[0];
|
|
72
|
+
if (!isRecord(first))
|
|
73
|
+
continue;
|
|
74
|
+
const target = first.target;
|
|
75
|
+
if (isSymbioteNode(target))
|
|
76
|
+
return target;
|
|
77
|
+
}
|
|
78
|
+
return undefined;
|
|
79
|
+
}
|
|
80
|
+
function keyboardNeverPersistsTaps(persist) {
|
|
81
|
+
return (persist === undefined ||
|
|
82
|
+
persist === null ||
|
|
83
|
+
persist === false ||
|
|
84
|
+
persist === 'never');
|
|
85
|
+
}
|
|
86
|
+
// `ScrollView.js:1474-1522`. Claims the tap in the CAPTURE phase — before any child sees it —
|
|
87
|
+
// while animating, or (the keyboard-dismiss case) while the default is in force, a dismissible
|
|
88
|
+
// keyboard is up, and the touch did not land on the focused input itself.
|
|
89
|
+
function startShouldSetResponderCapture(owner, event) {
|
|
90
|
+
const state = stateFor(owner);
|
|
91
|
+
if (isAnimating(state))
|
|
92
|
+
return true;
|
|
93
|
+
if (propOf(owner, 'disableScrollViewPanResponder') === true)
|
|
94
|
+
return false;
|
|
95
|
+
if (softKeyboardIsDetached())
|
|
96
|
+
return false;
|
|
97
|
+
if (keyboardNeverPersistsTaps(propOf(owner, 'keyboardShouldPersistTaps')) &&
|
|
98
|
+
keyboardIsDismissible()) {
|
|
99
|
+
const target = touchTargetOf(event.nativeEvent);
|
|
100
|
+
if (target !== undefined && !isTextInputNode(target))
|
|
101
|
+
return true;
|
|
102
|
+
}
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
// `ScrollView.js:1444-1461`. The BUBBLE-phase counterpart — only reachable when capture declined
|
|
106
|
+
// — for `keyboardShouldPersistTaps: 'handled'`: claim only if the tap didn't land on the focused
|
|
107
|
+
// input (letting a child handle it first is the whole point of "handled").
|
|
108
|
+
function startShouldSetResponder(owner, event) {
|
|
109
|
+
if (propOf(owner, 'disableScrollViewPanResponder') === true)
|
|
110
|
+
return false;
|
|
111
|
+
if (propOf(owner, 'keyboardShouldPersistTaps') === 'handled' &&
|
|
112
|
+
keyboardIsDismissible()) {
|
|
113
|
+
const target = touchTargetOf(event.nativeEvent);
|
|
114
|
+
if (target !== currentlyFocusedInput())
|
|
115
|
+
return true;
|
|
116
|
+
}
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
119
|
+
// `ScrollView.js:1404-1406`. Once real scroll motion has been observed, refuse to give the
|
|
120
|
+
// responder up — matches every other adapter's "don't drop a gesture already committed" rule.
|
|
121
|
+
function responderTerminationRequest(owner) {
|
|
122
|
+
return !stateFor(owner).observedScrollSinceBecomingResponder;
|
|
123
|
+
}
|
|
124
|
+
// `ScrollView.js:1334-1341`. `onResponderGrant` is an ordinary `IResponderProps` field an app may
|
|
125
|
+
// also set, so — same reason `scroll`/`layout`/`contentSizeChange` are owned — it is composed
|
|
126
|
+
// through the stash rather than claimed outright.
|
|
127
|
+
function handleResponderGrant(owner, event) {
|
|
128
|
+
const state = stateFor(owner);
|
|
129
|
+
state.observedScrollSinceBecomingResponder = false;
|
|
130
|
+
const app = appListenerFor(owner, 'responderGrant');
|
|
131
|
+
if (typeof app === 'function')
|
|
132
|
+
app(event);
|
|
133
|
+
state.becameResponderWhileAnimating = isAnimating(state);
|
|
134
|
+
}
|
|
135
|
+
// `ScrollView.js:1357-1387`. On release, dismiss the keyboard iff: something is focused,
|
|
136
|
+
// `keyboardShouldPersistTaps` isn't `true`/`'always'`, the keyboard is dismissible, the release
|
|
137
|
+
// didn't land on the focused input, and nothing since becoming responder was a real scroll or an
|
|
138
|
+
// animation-time grant (both mean the touch was already "used" for something else).
|
|
139
|
+
function handleResponderRelease(owner, event) {
|
|
140
|
+
const state = stateFor(owner);
|
|
141
|
+
const app = appListenerFor(owner, 'responderRelease');
|
|
142
|
+
if (typeof app === 'function')
|
|
143
|
+
app(event);
|
|
144
|
+
const focused = currentlyFocusedInput();
|
|
145
|
+
const persist = propOf(owner, 'keyboardShouldPersistTaps');
|
|
146
|
+
if (focused === null ||
|
|
147
|
+
persist === true ||
|
|
148
|
+
persist === 'always' ||
|
|
149
|
+
!keyboardIsDismissible() ||
|
|
150
|
+
state.observedScrollSinceBecomingResponder ||
|
|
151
|
+
state.becameResponderWhileAnimating)
|
|
152
|
+
return;
|
|
153
|
+
if (touchTargetOf(event.nativeEvent) === focused)
|
|
154
|
+
return;
|
|
155
|
+
blurTextInput(focused);
|
|
156
|
+
}
|
|
157
|
+
// `ScrollView.js:1146` — unconditional, exactly as RN sets it from `_handleScroll`: nothing reads
|
|
158
|
+
// the flag unless this node actually holds the responder, so setting it on every scroll is safe.
|
|
159
|
+
export function markScrollObserved(owner) {
|
|
160
|
+
stateFor(owner).observedScrollSinceBecomingResponder = true;
|
|
161
|
+
}
|
|
162
|
+
// `ScrollView.js:1263-1274`. Both are ordinary `on*` props apps use for pagination, so both are
|
|
163
|
+
// composed through the stash exactly like `handleOwnerScroll` composes `onScroll` (`./sticky.ts`).
|
|
164
|
+
function handleMomentumScrollBegin(owner, event) {
|
|
165
|
+
stateFor(owner).lastMomentumScrollBeginTime = performance.now();
|
|
166
|
+
const app = appListenerFor(owner, 'momentumScrollBegin');
|
|
167
|
+
if (typeof app === 'function')
|
|
168
|
+
app(event);
|
|
169
|
+
}
|
|
170
|
+
function handleMomentumScrollEnd(owner, event) {
|
|
171
|
+
stateFor(owner).lastMomentumScrollEndTime = performance.now();
|
|
172
|
+
const app = appListenerFor(owner, 'momentumScrollEnd');
|
|
173
|
+
if (typeof app === 'function')
|
|
174
|
+
app(event);
|
|
175
|
+
}
|
|
176
|
+
export const RESPONDER_OWNED_LISTENERS = [
|
|
177
|
+
'startShouldSetResponderCapture',
|
|
178
|
+
'startShouldSetResponder',
|
|
179
|
+
'responderTerminationRequest',
|
|
180
|
+
'responderGrant',
|
|
181
|
+
'responderRelease',
|
|
182
|
+
'momentumScrollBegin',
|
|
183
|
+
'momentumScrollEnd',
|
|
184
|
+
];
|
|
185
|
+
// Installed once per owner, in `attach` — same beat Pressable wires its own responder pair.
|
|
186
|
+
export function installResponderPredicates(owner) {
|
|
187
|
+
setBehaviorListener(owner, 'startShouldSetResponderCapture', event => startShouldSetResponderCapture(owner, event));
|
|
188
|
+
setBehaviorListener(owner, 'startShouldSetResponder', event => startShouldSetResponder(owner, event));
|
|
189
|
+
setBehaviorListener(owner, 'responderTerminationRequest', () => responderTerminationRequest(owner));
|
|
190
|
+
setBehaviorListener(owner, 'responderGrant', event => {
|
|
191
|
+
handleResponderGrant(owner, event);
|
|
192
|
+
});
|
|
193
|
+
setBehaviorListener(owner, 'responderRelease', event => {
|
|
194
|
+
handleResponderRelease(owner, event);
|
|
195
|
+
});
|
|
196
|
+
setBehaviorListener(owner, 'momentumScrollBegin', event => {
|
|
197
|
+
handleMomentumScrollBegin(owner, event);
|
|
198
|
+
});
|
|
199
|
+
setBehaviorListener(owner, 'momentumScrollEnd', event => {
|
|
200
|
+
handleMomentumScrollEnd(owner, event);
|
|
201
|
+
});
|
|
202
|
+
}
|
|
@@ -1,11 +1,9 @@
|
|
|
1
|
-
import { type IClaimMode
|
|
1
|
+
import { type IClaimMode } from '@symbiote-native/engine';
|
|
2
2
|
export declare const SCROLL_VIEW_TAG = "scroll-view";
|
|
3
3
|
export declare const HORIZONTAL_SCROLL_VIEW_TAG = "horizontal-scroll-view";
|
|
4
4
|
export declare const REFRESH_CONTROL: string;
|
|
5
|
-
export declare function ownerFold(base: IViewStyle, horizontal: boolean): IPayloadFold;
|
|
6
5
|
export interface IScrollPlatform {
|
|
7
6
|
claimMode: IClaimMode;
|
|
8
|
-
onWrapChange?: (base: IViewStyle, horizontal: boolean) => IHostBehavior['onWrapChange'];
|
|
9
7
|
slotDerived?: readonly string[];
|
|
10
8
|
}
|
|
11
9
|
export declare function registerScrollViewBehaviors(platform: IScrollPlatform): void;
|