@symbiote-native/vue 0.3.8 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/components/flat-list/index.js +18 -34
- package/build/components/image-background.js +4 -14
- package/build/components/image.js +9 -20
- package/build/components/keyboard-avoiding-view.js +10 -19
- package/build/components/modal/index.d.ts +4 -4
- package/build/components/pressable.js +24 -31
- package/build/components/refresh-control.js +12 -18
- package/build/components/scroll-view/index.android.js +21 -33
- package/build/components/scroll-view/shared.js +60 -110
- package/build/components/scroll-view/sticky-header.js +41 -59
- package/build/components/section-list/index.js +12 -19
- package/build/components/switch/shared.js +17 -29
- package/build/components/text-input/index.js +29 -49
- package/build/components/virtualized-list/index.js +20 -3
- package/build/components/virtualized-section-list/index.js +17 -25
- package/build/components.js +1 -3
- package/build/host-instance/index.js +2 -4
- package/build/index.d.ts +2 -1
- package/build/index.js +11 -45
- package/build/modules/animated/create-animated-component.js +34 -97
- package/build/modules/animated/index.js +13 -17
- package/build/modules/app-registry/index.js +7 -11
- package/build/modules/status-bar.js +8 -15
- package/build/renderer/index.js +11 -6
- package/build/runtime-helpers/index.js +16 -26
- package/metro-css-parser.cjs +9 -14
- package/metro-vue-transformer.cjs +112 -135
- package/package.json +4 -4
|
@@ -1,23 +1,17 @@
|
|
|
1
|
-
// ScrollView, the Vue lifecycle half. The Fabric tree is nested:
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
// imperative handle. This is the Vue twin of the React adapter's useRef + buildScrollViewHandle.
|
|
1
|
+
// ScrollView, the Vue lifecycle half. The Fabric tree is nested: a scroll view wraps a
|
|
2
|
+
// content view holding the children (RN's ScrollView.js shape). Platform-invariant math
|
|
3
|
+
// (decelerationRate, per-axis intrinsics/base style, content-size dedupe, imperative handle,
|
|
4
|
+
// aria/role fold) lives in @symbiote-native/components, shared with React; Vue supplies only
|
|
5
|
+
// the reactivity - shallowRef host node, setup-scope lastContentSize dedupe, expose() handle -
|
|
6
|
+
// the twin of React's useRef + buildScrollViewHandle.
|
|
8
7
|
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
// forwarding a function prop would reach Fabric and crash Android's folly::dynamic). Scroll events
|
|
13
|
-
// (onScroll/onLayout/…) ARE ViewConfig events, so they forward raw and routeProp turns them into
|
|
14
|
-
// listeners.
|
|
8
|
+
// Attrs are untyped, so every field is narrowed with a runtime guard, never a cast. The legacy
|
|
9
|
+
// onContentSizeChange callback key MUST be consumed if present: it is NOT a ViewConfig event,
|
|
10
|
+
// and forwarding a function prop reaches Fabric and crashes Android's folly::dynamic.
|
|
15
11
|
//
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
// onScroll composition (native attach vs Animated.event) all live here; the per-header component and
|
|
20
|
-
// the children wrap live in scroll-view-sticky-header.ts (the Vue twin of the React file).
|
|
12
|
+
// Sticky headers are real: the scroll AnimatedValue, headerLayoutYs cross-talk map, viewport-
|
|
13
|
+
// height capture, and onScroll composition (native attach vs Animated.event) live here; the
|
|
14
|
+
// per-header component and children wrap live in sticky-header.ts.
|
|
21
15
|
import { defineComponent, h, isVNode, markRaw, onBeforeUnmount, ref, shallowRef, watch, } from '@vue/runtime-core';
|
|
22
16
|
import { attachStickyScroll, buildScrollViewHandle, didContentSizeChange, forwardScrollEvent, readLayoutDimension, resolveAccessibilityProps, resolveDecelerationRate, resolveScrollForwarding, selectScrollIntrinsics, } from '@symbiote-native/components';
|
|
23
17
|
import { AnimatedValue, dlog, event as animatedEvent, isClassNameValue, isNativeAnimatedAvailable, isSymbioteNode, resolveClassName, } from '@symbiote-native/engine';
|
|
@@ -26,23 +20,15 @@ import { normalizeVueAttrs } from '../../utils/normalize-attrs.js';
|
|
|
26
20
|
function isHandler(value) {
|
|
27
21
|
return typeof value === 'function';
|
|
28
22
|
}
|
|
29
|
-
// Objects and arrays are valid StyleProp<ViewStyle
|
|
30
|
-
// engine omits RN's RegisteredStyle brand, so no numeric form). Primitives/null degrade to
|
|
31
|
-
// undefined; the engine flattens whatever object/array reaches it.
|
|
23
|
+
// Objects and arrays are valid StyleProp<ViewStyle>; primitives/null degrade to undefined.
|
|
32
24
|
function isStyleProp(value) {
|
|
33
25
|
return typeof value === 'object' && value !== null;
|
|
34
26
|
}
|
|
35
|
-
// `class`
|
|
36
|
-
//
|
|
37
|
-
// the
|
|
38
|
-
//
|
|
39
|
-
// wrapper's layout style, BEFORE that later resolution ever runs, so a class-only layout prop
|
|
40
|
-
// (flex, height, gap, …) never reaches the wrapper and it collapses to nothing.
|
|
41
|
-
// isClassNameProp is @symbiote-native/engine's own isClassNameValue guard (shared, not redeclared —
|
|
42
|
-
// routeProp's centralized class+style merge needs the identical narrowing).
|
|
27
|
+
// `class` forwards raw via forwardAttrs, resolved later by the renderer's patchProp. But the
|
|
28
|
+
// Android RefreshControl wrap (index.android.ts) reads userStyle alone to splitLayoutProps()
|
|
29
|
+
// the outer wrapper BEFORE that later resolution runs, so a class-only layout prop (flex,
|
|
30
|
+
// height, gap, …) would otherwise never reach the wrapper.
|
|
43
31
|
const isClassNameProp = isClassNameValue;
|
|
44
|
-
// decelerationRate is resolved per-platform (resolveDecelerationRate), so it must be narrowed to
|
|
45
|
-
// its declared shape before that call, unlike the raw pass-through props.
|
|
46
32
|
function asDecelerationRate(value) {
|
|
47
33
|
if (typeof value === 'number')
|
|
48
34
|
return value;
|
|
@@ -50,21 +36,16 @@ function asDecelerationRate(value) {
|
|
|
50
36
|
return value;
|
|
51
37
|
return undefined;
|
|
52
38
|
}
|
|
53
|
-
// stickyHeaderIndices arrives untyped (attrs); narrow to a number[] before the wrap reads it.
|
|
54
39
|
function isNumberArray(value) {
|
|
55
40
|
return Array.isArray(value) && value.every(entry => typeof entry === 'number');
|
|
56
41
|
}
|
|
57
|
-
// A StickyHeaderComponent override is a Vue component: a function (functional) or an object
|
|
58
|
-
// (defineComponent). Narrowed before it is handed to wrapStickyHeaders (no cast).
|
|
59
42
|
function isComponent(value) {
|
|
60
43
|
return typeof value === 'function' || (typeof value === 'object' && value !== null);
|
|
61
44
|
}
|
|
62
|
-
//
|
|
63
|
-
//
|
|
64
|
-
//
|
|
65
|
-
//
|
|
66
|
-
// StickyHeaderComponent) are lifecycle-consumed by the children wrap and must NEVER reach Fabric.
|
|
67
|
-
// style / contentContainerStyle / horizontal / decelerationRate are recomposed.
|
|
45
|
+
// Prop/handler keys the lifecycle consumes itself; everything else forwards onto the scroll-view
|
|
46
|
+
// node. onContentSizeChange is consumed (synthesized from the content onLayout); refreshControl
|
|
47
|
+
// and the sticky-header props are consumed by the platform assemble / children wrap and must
|
|
48
|
+
// NEVER reach Fabric.
|
|
68
49
|
const HANDLED_ATTRS = [
|
|
69
50
|
'style',
|
|
70
51
|
'contentContainerStyle',
|
|
@@ -86,39 +67,29 @@ function forwardAttrs(attrs) {
|
|
|
86
67
|
}
|
|
87
68
|
export function createScrollView(platform) {
|
|
88
69
|
return defineComponent((_props, { slots, attrs: rawAttrs, expose, emit }) => {
|
|
89
|
-
// shallowRef, NOT ref:
|
|
90
|
-
//
|
|
91
|
-
//
|
|
92
|
-
// miss and every scrollTo/scrollToEnd/flashScrollIndicators silently no-op. This is the
|
|
93
|
-
// same rule as the Switch host node.
|
|
70
|
+
// shallowRef, NOT ref: a plain ref() would run the node through Vue's toReactive(),
|
|
71
|
+
// handing back a Proxy the engine's WeakMap mirror doesn't recognize, so
|
|
72
|
+
// scrollTo/scrollToEnd/flashScrollIndicators would silently no-op. Same rule as Switch.
|
|
94
73
|
const nodeRef = shallowRef(null);
|
|
95
74
|
const setNodeRef = (el) => {
|
|
96
75
|
nodeRef.value = isSymbioteNode(el) ? el : null;
|
|
97
76
|
};
|
|
98
|
-
//
|
|
99
|
-
//
|
|
100
|
-
// null and every command would no-op. expose() makes it the value a parent ref
|
|
101
|
-
// sees: the Vue twin of React's useImperativeHandle(forwardedRef, buildScrollViewHandle(…)).
|
|
77
|
+
// Lazy getter, not the node captured once: it is null until the element commits, so an
|
|
78
|
+
// eager capture would freeze null. Vue twin of useImperativeHandle(ref, buildScrollViewHandle).
|
|
102
79
|
expose(buildScrollViewHandle(() => nodeRef.value));
|
|
103
|
-
//
|
|
104
|
-
// contentSizeChange: RN fires the content onLayout on every layout pass; only real size
|
|
105
|
-
// changes emit to the user (didContentSizeChange).
|
|
80
|
+
// RN fires the content onLayout on every layout pass; only a real size change emits.
|
|
106
81
|
let lastContentSize = null;
|
|
107
|
-
//
|
|
108
|
-
//
|
|
109
|
-
//
|
|
110
|
-
// markRaw: it is an engine object, held by IDENTITY, never run through toReactive (the
|
|
111
|
-
// reactivity rule; a deep ref would hand back a Proxy the engine's mirror misses). Allocated
|
|
112
|
-
// unconditionally (like React's unconditional hook); unused when no sticky headers are flagged.
|
|
82
|
+
// Drives every sticky header's translateY (RN's _scrollAnimatedValue). markRaw: an engine
|
|
83
|
+
// object held by identity, never run through toReactive. Allocated unconditionally
|
|
84
|
+
// (unused when no sticky headers are flagged), like React's unconditional hook.
|
|
113
85
|
const scrollAnimatedValue = markRaw(new AnimatedValue(0));
|
|
114
|
-
// Inverted sticky headers stick to the BOTTOM, so they need the viewport height (RN
|
|
115
|
-
//
|
|
86
|
+
// Inverted sticky headers stick to the BOTTOM, so they need the viewport height (RN's
|
|
87
|
+
// _handleLayout). Fed back into the wrapped headers on the next render.
|
|
116
88
|
const viewportHeight = ref(undefined);
|
|
117
|
-
// Sticky-header cross-talk (RN ScrollView.js _headerLayoutYs
|
|
118
|
-
// map
|
|
119
|
-
//
|
|
120
|
-
//
|
|
121
|
-
// freshly-recorded y forward into the previous header's nextHeaderLayoutY (via nextStickyHeaderY).
|
|
89
|
+
// Sticky-header cross-talk (RN ScrollView.js _headerLayoutYs): a child-index -> measured-y
|
|
90
|
+
// map so each header learns where the NEXT sticky header starts (its push-off collision
|
|
91
|
+
// point). Mutated imperatively from each header's onLayout; the bump ref forces the
|
|
92
|
+
// re-render that feeds the freshly-recorded y into the previous header's nextStickyHeaderY.
|
|
122
93
|
const headerLayoutYs = new Map();
|
|
123
94
|
const bumpHeaderLayout = ref(0);
|
|
124
95
|
const onHeaderLayoutY = (index, y) => {
|
|
@@ -128,13 +99,11 @@ export function createScrollView(platform) {
|
|
|
128
99
|
dlog(`Vue ScrollView sticky-header layoutY index=${index} y=${y}`);
|
|
129
100
|
bumpHeaderLayout.value += 1;
|
|
130
101
|
};
|
|
131
|
-
// Native sticky-scroll attach (RN attachNativeEvent
|
|
132
|
-
//
|
|
133
|
-
//
|
|
134
|
-
//
|
|
135
|
-
// reads it
|
|
136
|
-
// each frame. flush:'post' so the engine has committed the node before attachStickyScroll reads
|
|
137
|
-
// its Fabric handle. Detached on unmount (and re-detached if the node identity changes).
|
|
102
|
+
// Native sticky-scroll attach (RN attachNativeEvent): when the native module is available,
|
|
103
|
+
// the scroll value is driven on the UI thread so interpolations ride scroll natively (no
|
|
104
|
+
// JS jitter). A plain non-reactive flag set in render; the post-commit watch reads it once
|
|
105
|
+
// the node commits. flush:'post' so the node has a Fabric handle before attachStickyScroll
|
|
106
|
+
// reads it. Detached on unmount and re-detached if the node identity changes.
|
|
138
107
|
let nativeStickyWanted = false;
|
|
139
108
|
let detachStickyScroll;
|
|
140
109
|
watch(() => nodeRef.value, node => {
|
|
@@ -153,24 +122,17 @@ export function createScrollView(platform) {
|
|
|
153
122
|
return () => {
|
|
154
123
|
// Read the bump so a recorded header y re-runs render and feeds nextStickyHeaderY forward.
|
|
155
124
|
void bumpHeaderLayout.value;
|
|
156
|
-
// Fold kebab template props (:content-container-style) to the
|
|
157
|
-
// Vue templates use kebab, but the prop surface (and HANDLED_ATTRS below) is camelCase.
|
|
125
|
+
// Fold kebab template props (:content-container-style) to the camelCase prop surface.
|
|
158
126
|
const attrs = normalizeVueAttrs(rawAttrs);
|
|
159
127
|
const isHorizontal = attrs.horizontal === true;
|
|
160
128
|
const userStyle = isStyleProp(attrs.style) ? attrs.style : undefined;
|
|
161
|
-
// resolveClassName(undefined) is a cheap {} no-op, so this is safe with no class prop too.
|
|
162
129
|
const classProp = isClassNameProp(attrs.class) ? attrs.class : undefined;
|
|
163
130
|
const layoutSplitStyle = [resolveClassName(classProp), userStyle];
|
|
164
|
-
// A class-name string resolves through the same style registry as `class`/`style` above;
|
|
165
|
-
// an object/array is already style-shaped and passes through as-is.
|
|
166
131
|
const contentContainerStyle = typeof attrs.contentContainerStyle === 'string'
|
|
167
132
|
? resolveClassName(attrs.contentContainerStyle)
|
|
168
133
|
: isStyleProp(attrs.contentContainerStyle)
|
|
169
134
|
? attrs.contentContainerStyle
|
|
170
135
|
: undefined;
|
|
171
|
-
// Sticky headers are a pure-JS layer; the native scroll view ignores
|
|
172
|
-
// stickyHeaderIndices, so we wrap the flagged children below and drive their translateY off
|
|
173
|
-
// the scroll offset. invertStickyHeaders narrows to the inverted (stick-to-bottom) branch.
|
|
174
136
|
const stickyHeaderIndices = isNumberArray(attrs.stickyHeaderIndices)
|
|
175
137
|
? attrs.stickyHeaderIndices
|
|
176
138
|
: undefined;
|
|
@@ -180,10 +142,8 @@ export function createScrollView(platform) {
|
|
|
180
142
|
? attrs.StickyHeaderComponent
|
|
181
143
|
: undefined;
|
|
182
144
|
const { scrollViewIntrinsic, contentIntrinsic, scrollViewBaseStyle, contentStyle } = selectScrollIntrinsics(isHorizontal, contentContainerStyle);
|
|
183
|
-
//
|
|
184
|
-
//
|
|
185
|
-
// only when defined (load-bearing on iOS's RCTScrollView axis, ignored by Android's
|
|
186
|
-
// dedicated manager); decelerationRate is resolved per-platform.
|
|
145
|
+
// RN defaults nested scrolling ON (ScrollView.js `nestedScrollEnabled ?? true`);
|
|
146
|
+
// horizontal forwards only when defined (load-bearing on iOS's RCTScrollView axis).
|
|
187
147
|
const outerProps = {
|
|
188
148
|
...resolveAccessibilityProps(forwardAttrs(attrs)),
|
|
189
149
|
};
|
|
@@ -194,10 +154,10 @@ export function createScrollView(platform) {
|
|
|
194
154
|
const decel = asDecelerationRate(attrs.decelerationRate);
|
|
195
155
|
if (decel !== undefined)
|
|
196
156
|
outerProps.decelerationRate = resolveDecelerationRate(decel);
|
|
197
|
-
//
|
|
198
|
-
//
|
|
199
|
-
//
|
|
200
|
-
//
|
|
157
|
+
// When sticky headers are active, the offset must reach the AnimatedValue (RN's
|
|
158
|
+
// _scrollAnimatedValueAttachment). forwardAttrs already put the user's onScroll/onLayout/
|
|
159
|
+
// scrollEventThrottle on outerProps; here we override per resolveScrollForwarding's
|
|
160
|
+
// decisions (which path, throttle default, inverted capture).
|
|
201
161
|
const nativeStickyAvailable = hasStickyHeaders && isNativeAnimatedAvailable();
|
|
202
162
|
nativeStickyWanted = nativeStickyAvailable;
|
|
203
163
|
const userThrottle = typeof attrs.scrollEventThrottle === 'number' ? attrs.scrollEventThrottle : undefined;
|
|
@@ -212,20 +172,19 @@ export function createScrollView(platform) {
|
|
|
212
172
|
if (hasStickyHeaders) {
|
|
213
173
|
const userOnScroll = isHandler(attrs.onScroll) ? attrs.onScroll : undefined;
|
|
214
174
|
if (forwarding.mode === 'sticky-js') {
|
|
215
|
-
// JS fallback (no native module):
|
|
216
|
-
// the
|
|
217
|
-
// scroll (the jitter), which the native path removes on a real host.
|
|
175
|
+
// JS fallback (no native module): correct, but lags a frame under fast scroll, which
|
|
176
|
+
// the native path removes on a real host.
|
|
218
177
|
outerProps.onScroll = animatedEvent([{ nativeEvent: { contentOffset: { y: scrollAnimatedValue } } }], userOnScroll === undefined
|
|
219
178
|
? undefined
|
|
220
179
|
: { listener: (...args) => forwardScrollEvent(userOnScroll, args) });
|
|
221
180
|
}
|
|
222
|
-
// Native path: the value is driven on the UI thread by the post-commit watch above, so
|
|
223
|
-
//
|
|
181
|
+
// Native path: the value is driven on the UI thread by the post-commit watch above, so
|
|
182
|
+
// onScroll forwards untouched.
|
|
224
183
|
if (forwarding.scrollEventThrottle !== undefined) {
|
|
225
184
|
outerProps.scrollEventThrottle = forwarding.scrollEventThrottle;
|
|
226
185
|
}
|
|
227
|
-
//
|
|
228
|
-
//
|
|
186
|
+
// Capture the viewport height for inverted sticky headers (RN _handleLayout), then call
|
|
187
|
+
// the user's handler.
|
|
229
188
|
if (forwarding.capturesViewportHeight) {
|
|
230
189
|
const userOnLayout = isHandler(attrs.onLayout) ? attrs.onLayout : undefined;
|
|
231
190
|
outerProps.onLayout = (event) => {
|
|
@@ -238,18 +197,15 @@ export function createScrollView(platform) {
|
|
|
238
197
|
}
|
|
239
198
|
}
|
|
240
199
|
dlog(`Vue ScrollView -> ${scrollViewIntrinsic} (horizontal=${isHorizontal} sticky=${hasStickyHeaders})`);
|
|
241
|
-
//
|
|
242
|
-
//
|
|
243
|
-
//
|
|
244
|
-
// false also preserves the cell views maintainVisibleContentPosition / snapToAlignment
|
|
245
|
-
// anchor against (RN preserveChildren). iOS never flattens; both are no-ops there.
|
|
200
|
+
// `collapsable: false` keeps the layout-only content view as a real native view: Android
|
|
201
|
+
// Fabric view-flattens it away otherwise, hoisting the cells as direct children of the
|
|
202
|
+
// scroll view (which hosts exactly one) - an addViewAt crash. iOS never flattens.
|
|
246
203
|
const contentProps = { style: contentStyle, collapsable: false };
|
|
247
204
|
if (forwarding.collapsableChildren) {
|
|
248
205
|
contentProps.collapsableChildren = false;
|
|
249
206
|
}
|
|
250
|
-
//
|
|
251
|
-
//
|
|
252
|
-
// the setup-scope lastContentSize).
|
|
207
|
+
// Synthesized from the content view's own onLayout (RN _handleContentOnLayout); emit only
|
|
208
|
+
// on a real size change.
|
|
253
209
|
contentProps.onLayout = (event) => {
|
|
254
210
|
const width = readLayoutDimension(event, 'width');
|
|
255
211
|
const height = readLayoutDimension(event, 'height');
|
|
@@ -261,23 +217,17 @@ export function createScrollView(platform) {
|
|
|
261
217
|
dlog(`Vue ScrollView contentSizeChange ${width}x${height}`);
|
|
262
218
|
emit('contentSizeChange', width, height);
|
|
263
219
|
};
|
|
264
|
-
// Sticky headers are a pure-JS layer (the native scroll view ignores stickyHeaderIndices);
|
|
265
|
-
// wrap the flagged children so they pin to the scroll offset. No-op when none are flagged.
|
|
266
220
|
const slotChildren = slots.default !== undefined ? slots.default() : [];
|
|
267
221
|
const contentChildren = hasStickyHeaders
|
|
268
222
|
? wrapStickyHeaders(slotChildren, stickyHeaderIndices, scrollAnimatedValue, invertStickyHeaders, viewportHeight.value, stickyHeaderComponent, headerLayoutYs, onHeaderLayoutY)
|
|
269
223
|
: slotChildren;
|
|
270
224
|
const content = h(contentIntrinsic, contentProps, contentChildren);
|
|
271
|
-
// Base style UNDER user style so an explicit user value (height, flexDirection) still wins
|
|
272
|
-
// the scroll node carries overflow:'scroll' (frame clipping) + the per-axis flexDirection.
|
|
225
|
+
// Base style UNDER user style so an explicit user value (height, flexDirection) still wins.
|
|
273
226
|
const scrollProps = {
|
|
274
227
|
...outerProps,
|
|
275
228
|
style: [scrollViewBaseStyle, userStyle],
|
|
276
229
|
ref: setNodeRef,
|
|
277
230
|
};
|
|
278
|
-
// refreshControl arrives as a Vue VNode the app passes (h(RefreshControl, …)); narrow it
|
|
279
|
-
// with isVNode (no cast). Stripped from forwardAttrs (it is in HANDLED_ATTRS) so it never
|
|
280
|
-
// reaches the host as a prop: it is lifecycle-consumed by the platform assemble.
|
|
281
231
|
const refreshControl = isVNode(attrs.refreshControl) ? attrs.refreshControl : undefined;
|
|
282
232
|
return platform.assemble({
|
|
283
233
|
scrollViewIntrinsic,
|
|
@@ -1,16 +1,14 @@
|
|
|
1
1
|
// Sticky headers: the Vue twin of adapters/react/src/scroll-view-sticky-header.tsx, the JS layer
|
|
2
2
|
// RN implements in ScrollView.js / ScrollViewStickyHeader.js.
|
|
3
3
|
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
// flagged child to the scroll offset and translate it to stay pinned. The interpolation mirrors
|
|
10
|
-
// ScrollViewStickyHeader.js (non-inverted + inverted branches) and lives, framework-agnostic, in
|
|
4
|
+
// RN does stickiness PURELY IN JS: ScrollView.js wraps each flagged child in a
|
|
5
|
+
// ScrollViewStickyHeader fed by a single _scrollAnimatedValue that Animated.event drives from
|
|
6
|
+
// onScroll. The native scroll view does NOT honor the index array itself, so forwarding
|
|
7
|
+
// stickyHeaderIndices to native is a silent no-op - we replicate the JS layer instead. The
|
|
8
|
+
// interpolation math (non-inverted + inverted) lives framework-agnostic in
|
|
11
9
|
// @symbiote-native/components (computeStickyInterpolation); this file holds the Vue component
|
|
12
|
-
// shell,
|
|
13
|
-
//
|
|
10
|
+
// shell, layout state, and child-wrapping. Vue supplies only the reactive lifecycle
|
|
11
|
+
// (refs/watch instead of useState/useEffect).
|
|
14
12
|
import { defineComponent, h, isVNode, onBeforeUnmount, ref, shallowRef, watchEffect, markRaw, } from '@vue/runtime-core';
|
|
15
13
|
import { AnimatedValue, AnimatedInterpolation, Platform, dlog, } from '@symbiote-native/engine';
|
|
16
14
|
import { createInitialStickyState, nextStickyHeaderY, readLayoutNumber, reduceSticky, STICKY_HEADER_Z_INDEX, } from '@symbiote-native/components';
|
|
@@ -24,46 +22,39 @@ function isRecord(value) {
|
|
|
24
22
|
function isAnimatedValue(value) {
|
|
25
23
|
return value instanceof AnimatedValue;
|
|
26
24
|
}
|
|
27
|
-
//
|
|
28
|
-
// forward layout to it (RN ScrollViewStickyHeader.js calls the child's onLayout after its own).
|
|
25
|
+
// So the sticky wrapper can forward layout to the child (RN calls the child's onLayout after its own).
|
|
29
26
|
function readChildOnLayout(child) {
|
|
30
27
|
if (!isRecord(child.props))
|
|
31
28
|
return undefined;
|
|
32
29
|
const handler = child.props.onLayout;
|
|
33
30
|
return isHandler(handler) ? handler : undefined;
|
|
34
31
|
}
|
|
35
|
-
// One sticky header. Measures its own y/height via onLayout, interpolates the shared scroll
|
|
36
|
-
// into a translateY
|
|
37
|
-
// collides with it,
|
|
38
|
-
//
|
|
39
|
-
//
|
|
40
|
-
//
|
|
41
|
-
// Fabric as props (scrollAnimatedValue on a host node would crash Android's folly::dynamic).
|
|
32
|
+
// One sticky header. Measures its own y/height via onLayout, interpolates the shared scroll
|
|
33
|
+
// offset into a translateY pinning it to the top (or bottom, inverted) until the next header
|
|
34
|
+
// collides with it, driven through the native driver when available (no JS jitter). Ported from
|
|
35
|
+
// ScrollViewStickyHeader.js, including the Fabric ShadowTree debounce path. inheritAttrs:false
|
|
36
|
+
// so the IStickyHeaderProps inputs never fall through onto Animated.View and reach Fabric as
|
|
37
|
+
// props (scrollAnimatedValue on a host node would crash Android's folly::dynamic).
|
|
42
38
|
export const ScrollViewStickyHeader = defineComponent({
|
|
43
39
|
name: 'ScrollViewStickyHeader',
|
|
44
40
|
inheritAttrs: false,
|
|
45
41
|
setup(_props, { attrs, slots }) {
|
|
46
|
-
//
|
|
47
|
-
//
|
|
48
|
-
// fallback keeps working if the invariant (wrapStickyHeaders always supplies it) ever breaks.
|
|
42
|
+
// Read once - stable across renders (the same markRaw'd AnimatedValue). Held by IDENTITY, never
|
|
43
|
+
// run through toReactive. A fresh fallback keeps working if wrapStickyHeaders ever fails to supply it.
|
|
49
44
|
const scrollAnimatedValue = isAnimatedValue(attrs.scrollAnimatedValue)
|
|
50
45
|
? attrs.scrollAnimatedValue
|
|
51
46
|
: markRaw(new AnimatedValue(0));
|
|
52
|
-
//
|
|
53
|
-
// reactive
|
|
54
|
-
//
|
|
55
|
-
// debounce delay, rebuild ranges — all live in reduceSticky.
|
|
47
|
+
// Mutated in place by reduceSticky. A plain object, not a ref: render reads it gated by the
|
|
48
|
+
// reactive `version` bump + `animatedTranslateY` below. DECISIONS (zero-swallow gate,
|
|
49
|
+
// debounce delay, rebuild ranges) all live in reduceSticky.
|
|
56
50
|
const state = createInitialStickyState();
|
|
57
|
-
//
|
|
58
|
-
// state.translateY (the debounced committed value).
|
|
51
|
+
// Bumped when the reducer commits a new translateY, forcing render to re-read state.translateY.
|
|
59
52
|
const version = ref(0);
|
|
60
|
-
//
|
|
61
|
-
// shallowRef (held by identity, the reactivity rule); the un-measured identity stub until the
|
|
53
|
+
// Engine node -> shallowRef (identity rule); the un-measured identity stub until the
|
|
62
54
|
// rebuild-interpolation effect below replaces it.
|
|
63
55
|
const animatedTranslateY = shallowRef(scrollAnimatedValue.interpolate({ inputRange: [-1, 0], outputRange: [0, 0] }));
|
|
64
56
|
let debounceTimer;
|
|
65
|
-
//
|
|
66
|
-
// listener (an engine call the reducer does not own) and onBeforeUnmount cleans up.
|
|
57
|
+
// Held so the next rebuild detaches the old listener and onBeforeUnmount cleans up.
|
|
67
58
|
let interpolation;
|
|
68
59
|
let listenerId;
|
|
69
60
|
const inputs = () => ({
|
|
@@ -76,8 +67,7 @@ export const ScrollViewStickyHeader = defineComponent({
|
|
|
76
67
|
for (const effect of effects) {
|
|
77
68
|
switch (effect.kind) {
|
|
78
69
|
case 'rebuild-interpolation': {
|
|
79
|
-
// Detach the old listener, build a fresh interpolation
|
|
80
|
-
// wire the settled-value listener (symbiote is always Fabric; RN attaches it only there).
|
|
70
|
+
// Detach the old listener, build a fresh interpolation, and wire the settled-value listener.
|
|
81
71
|
if (interpolation !== undefined && listenerId !== undefined) {
|
|
82
72
|
interpolation.removeListener(listenerId);
|
|
83
73
|
listenerId = undefined;
|
|
@@ -95,8 +85,8 @@ export const ScrollViewStickyHeader = defineComponent({
|
|
|
95
85
|
break;
|
|
96
86
|
}
|
|
97
87
|
case 'schedule-debounce':
|
|
98
|
-
// The animated value updates several times per frame; debounce the settled value
|
|
99
|
-
//
|
|
88
|
+
// The animated value updates several times per frame; debounce the settled value so
|
|
89
|
+
// hit detection stays current (a Fabric issue, worse on Android).
|
|
100
90
|
if (debounceTimer !== undefined)
|
|
101
91
|
clearTimeout(debounceTimer);
|
|
102
92
|
debounceTimer = setTimeout(() => {
|
|
@@ -108,8 +98,7 @@ export const ScrollViewStickyHeader = defineComponent({
|
|
|
108
98
|
version.value += 1;
|
|
109
99
|
break;
|
|
110
100
|
case 'record-header-y':
|
|
111
|
-
// Vue records through attrs.onLayout (the wrapper closure)
|
|
112
|
-
// IStickyHeaderProps contract; the reducer emits no index for it.
|
|
101
|
+
// Vue records through attrs.onLayout (the wrapper closure); the reducer emits no index for it.
|
|
113
102
|
break;
|
|
114
103
|
}
|
|
115
104
|
}
|
|
@@ -117,9 +106,8 @@ export const ScrollViewStickyHeader = defineComponent({
|
|
|
117
106
|
const dispatch = (action) => {
|
|
118
107
|
runEffects(reduceSticky(state, action, inputs()).effects);
|
|
119
108
|
};
|
|
120
|
-
// Rebuild whenever the collision/viewport inputs change (
|
|
121
|
-
//
|
|
122
|
-
// build on mount. scrollAnimatedValue is the stable const (never changes for one ScrollView).
|
|
109
|
+
// Rebuild whenever the collision/viewport inputs change ([inverted, scrollViewHeight,
|
|
110
|
+
// nextHeaderLayoutY]); also runs once on mount.
|
|
123
111
|
watchEffect(() => {
|
|
124
112
|
dispatch({ kind: 'inputs-changed' });
|
|
125
113
|
});
|
|
@@ -130,9 +118,8 @@ export const ScrollViewStickyHeader = defineComponent({
|
|
|
130
118
|
if (debounceTimer !== undefined)
|
|
131
119
|
clearTimeout(debounceTimer);
|
|
132
120
|
});
|
|
133
|
-
//
|
|
134
|
-
//
|
|
135
|
-
// attrs.onLayout), then the child's own onLayout. Matches RN ScrollViewStickyHeader.js._onLayout.
|
|
121
|
+
// Dispatch the layout, fire the wrapper's recorder (onHeaderLayoutY via attrs.onLayout), then
|
|
122
|
+
// the child's own onLayout. Matches RN ScrollViewStickyHeader.js._onLayout.
|
|
136
123
|
const onLayout = (event) => {
|
|
137
124
|
const y = readLayoutNumber(event, 'y');
|
|
138
125
|
const height = readLayoutNumber(event, 'height');
|
|
@@ -151,13 +138,12 @@ export const ScrollViewStickyHeader = defineComponent({
|
|
|
151
138
|
// Read the version bump so a committed translateY re-runs render.
|
|
152
139
|
void version.value;
|
|
153
140
|
// The EXPLICIT debounced translateY overrides the committed transform for hit-testing, while
|
|
154
|
-
//
|
|
141
|
+
// animatedTranslateY does the smooth (native-driven) pin.
|
|
155
142
|
const passthroughAnimatedPropExplicitValues = state.translateY !== null
|
|
156
143
|
? { style: { transform: [{ translateY: state.translateY }] } }
|
|
157
144
|
: null;
|
|
158
145
|
// collapsable:false keeps the wrapper a real Yoga node; zIndex makes the pinned header paint
|
|
159
|
-
// OVER the rows scrolling under it.
|
|
160
|
-
// Animated wrapper's reduceProps rasterizes it into a numeric translateY for the committed tree.
|
|
146
|
+
// OVER the rows scrolling under it.
|
|
161
147
|
return h(Animated.View, {
|
|
162
148
|
style: {
|
|
163
149
|
transform: [{ translateY: animatedTranslateY.value }],
|
|
@@ -171,15 +157,13 @@ export const ScrollViewStickyHeader = defineComponent({
|
|
|
171
157
|
},
|
|
172
158
|
});
|
|
173
159
|
// Wrap each child flagged by `stickyHeaderIndices` in the sticky header component, fed by the
|
|
174
|
-
// shared scroll AnimatedValue.
|
|
175
|
-
// the React adapter's wrapStickyHeaders. Returns the children unchanged when no indices are flagged.
|
|
160
|
+
// shared scroll AnimatedValue. Returns the children unchanged when no indices are flagged.
|
|
176
161
|
//
|
|
177
|
-
// Cross-talk plumbing (RN's _headerLayoutYs + _onStickyHeaderLayout
|
|
178
|
-
//
|
|
179
|
-
//
|
|
180
|
-
//
|
|
181
|
-
//
|
|
182
|
-
// undefined and it sticks indefinitely (correct).
|
|
162
|
+
// Cross-talk plumbing (RN's _headerLayoutYs + _onStickyHeaderLayout): headerLayoutYs is a
|
|
163
|
+
// child-index -> measured-y map; each header reports its own y through onHeaderLayoutY, and we
|
|
164
|
+
// feed every header the y of the NEXT flagged header (the collision point past which it scrolls
|
|
165
|
+
// off). The LAST flagged header has no successor, so its nextHeaderLayoutY stays undefined and
|
|
166
|
+
// it sticks indefinitely (correct).
|
|
183
167
|
export function wrapStickyHeaders(children, stickyHeaderIndices, scrollAnimatedValue, invertStickyHeaders, scrollViewHeight, StickyHeaderComponent, headerLayoutYs, onHeaderLayoutY) {
|
|
184
168
|
if (stickyHeaderIndices === undefined || stickyHeaderIndices.length === 0)
|
|
185
169
|
return children;
|
|
@@ -188,17 +172,15 @@ export function wrapStickyHeaders(children, stickyHeaderIndices, scrollAnimatedV
|
|
|
188
172
|
const indexOfIndex = stickyHeaderIndices.indexOf(index);
|
|
189
173
|
if (indexOfIndex === -1 || !isVNode(child))
|
|
190
174
|
return child;
|
|
191
|
-
// The next flagged header's measured y
|
|
192
|
-
// ScrollView.js:1695 nextIndex). undefined until that header has measured (or for the last).
|
|
175
|
+
// The next flagged header's measured y. undefined until that header has measured (or for the last).
|
|
193
176
|
const nextIndex = stickyHeaderIndices[indexOfIndex + 1];
|
|
194
177
|
const nextHeaderLayoutY = nextStickyHeaderY(stickyHeaderIndices, indexOfIndex, headerLayoutYs);
|
|
195
178
|
dlog(`Vue ScrollView sticky-header wrap index=${index} next=${nextIndex} nextY=${nextHeaderLayoutY}`);
|
|
196
179
|
const props = {
|
|
197
180
|
key: child.key ?? `sticky-${index}`,
|
|
198
181
|
nextHeaderLayoutY,
|
|
199
|
-
//
|
|
200
|
-
//
|
|
201
|
-
// the resulting re-render (the headerLayoutYs bump in scroll-view-shared).
|
|
182
|
+
// Record this header's own y into the parent map; the lookup above feeds it forward to the
|
|
183
|
+
// previous header on the resulting re-render (the headerLayoutYs bump in scroll-view-shared).
|
|
202
184
|
onLayout: (event) => {
|
|
203
185
|
const y = readLayoutNumber(event, 'y');
|
|
204
186
|
if (y !== undefined)
|
|
@@ -1,21 +1,18 @@
|
|
|
1
1
|
// SectionList, the Vue public list-of-sections component. A thin wrapper over
|
|
2
2
|
// VirtualizedSectionList, mirroring RN's layering (SectionList -> VirtualizedSectionList ->
|
|
3
|
-
// VirtualizedList). All section-flattening
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
// the React adapter's SectionList.
|
|
3
|
+
// VirtualizedList). All section-flattening/windowing/imperative-scroll logic lives below; this
|
|
4
|
+
// layer re-exposes the same surface and re-exposes the handle (Vue resolves a parent ref to the
|
|
5
|
+
// exposed object, so the wrapper delegates).
|
|
7
6
|
//
|
|
8
|
-
// Typed-emits generic component
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
// synthesized events (endReached/startReached/refresh) are bridged, gated on listener presence so
|
|
13
|
-
// the inner list keeps its RefreshControl / edge-reached gating.
|
|
7
|
+
// Typed-emits generic component: a GENERIC setup function so section inputs infer ItemT at the
|
|
8
|
+
// call site. As a pure forwarder it consumes nothing itself - every input rides through $attrs
|
|
9
|
+
// straight onto VirtualizedSectionList; only the three synthesized events are bridged, gated on
|
|
10
|
+
// listener presence so the inner list keeps its RefreshControl/edge-reached gating.
|
|
14
11
|
import { defineComponent, getCurrentInstance, h, shallowRef, } from '@vue/runtime-core';
|
|
15
12
|
import { VirtualizedSectionList, } from '../virtualized-section-list/index.js';
|
|
16
13
|
import { normalizeVueAttrs } from '../../utils/normalize-attrs.js';
|
|
17
|
-
// VirtualizedSectionList
|
|
18
|
-
//
|
|
14
|
+
// VirtualizedSectionList's generic construct signature can't be resolved by h()'s overloads, so
|
|
15
|
+
// drive it through a loose functional-component handle.
|
|
19
16
|
const VirtualizedSectionListHost = VirtualizedSectionList;
|
|
20
17
|
function isRecord(value) {
|
|
21
18
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
@@ -36,17 +33,15 @@ function buildDelegate(getInner) {
|
|
|
36
33
|
};
|
|
37
34
|
}
|
|
38
35
|
export const SectionList = defineComponent(
|
|
39
|
-
// _props is read only by the type system
|
|
40
|
-
// the call site. As a pure forwarder, SectionList consumes nothing at runtime — it spreads $attrs.
|
|
36
|
+
// _props is read only by the type system, to let ItemT infer at the call site.
|
|
41
37
|
(_props, { attrs, expose, emit, slots }) => {
|
|
42
38
|
const inner = shallowRef(null);
|
|
43
39
|
const setInner = (instance) => {
|
|
44
40
|
inner.value = isSectionHandle(instance) ? instance : null;
|
|
45
41
|
};
|
|
46
42
|
expose(buildDelegate(() => inner.value));
|
|
47
|
-
//
|
|
48
|
-
//
|
|
49
|
-
// the inner VirtualizedSectionList (and its inner VirtualizedList) keeps its on-demand gating.
|
|
43
|
+
// Emits are stripped from $attrs by Vue; detect listener presence off the instance's own
|
|
44
|
+
// vnode props and bridge each ONLY when listened.
|
|
50
45
|
const instance = getCurrentInstance();
|
|
51
46
|
const listens = (onName) => {
|
|
52
47
|
const vnodeProps = instance?.vnode.props;
|
|
@@ -60,8 +55,6 @@ export const SectionList = defineComponent(
|
|
|
60
55
|
? (info) => emit('startReached', info)
|
|
61
56
|
: undefined;
|
|
62
57
|
const refresh = listens('onRefresh') ? () => emit('refresh') : undefined;
|
|
63
|
-
// Pure forwarder: spread $attrs as props and pass the consumer's scoped slots (#item /
|
|
64
|
-
// #sectionHeader / … ) straight down to VirtualizedSectionList untouched.
|
|
65
58
|
return h(VirtualizedSectionListHost, {
|
|
66
59
|
...normalizeVueAttrs(attrs),
|
|
67
60
|
ref: setInner,
|