@symbiote-native/vue 0.3.8 → 0.5.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/README.md +6 -6
- package/build/bootstrap.js +1 -1
- package/build/components/flat-list/index.js +32 -39
- package/build/components/image-background.js +8 -19
- package/build/components/image.js +12 -21
- package/build/components/keyboard-avoiding-view.js +47 -31
- package/build/components/modal/index.d.ts +4 -4
- package/build/components/modal/index.js +5 -2
- package/build/components/pressable.d.ts +1 -1
- package/build/components/pressable.js +25 -32
- package/build/components/refresh-control.js +12 -18
- package/build/components/scroll-view/index.android.d.ts +1 -1
- package/build/components/scroll-view/index.android.js +24 -34
- package/build/components/scroll-view/index.ios.d.ts +1 -1
- package/build/components/scroll-view/index.ios.js +3 -1
- package/build/components/scroll-view/shared.js +93 -123
- package/build/components/scroll-view/sticky-header.js +59 -64
- package/build/components/section-list/index.js +16 -21
- package/build/components/switch/shared.js +29 -32
- package/build/components/text-input/index.js +36 -51
- package/build/components/touchable-native-feedback.js +3 -1
- package/build/components/touchable.d.ts +7 -1
- package/build/components/touchable.js +181 -33
- package/build/components/virtualized-list/index.js +57 -18
- package/build/components/virtualized-section-list/index.d.ts +5 -0
- package/build/components/virtualized-section-list/index.js +50 -29
- package/build/components.js +9 -6
- package/build/create-portal/index.d.ts +28 -0
- package/build/create-portal/index.js +42 -0
- package/build/host-instance/index.js +5 -5
- package/build/index.d.ts +9 -6
- package/build/index.js +18 -46
- package/build/modules/animated/create-animated-component.js +46 -101
- package/build/modules/animated/index.d.ts +2 -0
- package/build/modules/animated/index.js +26 -17
- package/build/modules/app-registry/index.js +11 -13
- package/build/modules/status-bar.js +12 -17
- package/build/render.d.ts +3 -1
- package/build/render.js +62 -1
- package/build/renderer/index.js +11 -6
- package/build/runtime-helpers/index.d.ts +44 -24
- package/build/runtime-helpers/index.js +116 -47
- package/build/utils/normalize-attrs.js +3 -1
- package/metro-css-parser.cjs +9 -14
- package/metro-vue-transformer.cjs +196 -178
- package/package.json +5 -5
|
@@ -1,24 +1,20 @@
|
|
|
1
|
-
// RefreshControl, the Vue lifecycle half. On iOS this is the PullToRefreshView
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
1
|
+
// RefreshControl, the Vue lifecycle half. On iOS this is the PullToRefreshView Fabric node that
|
|
2
|
+
// lives INSIDE a ScrollView (a sibling before the content container); on Android it is
|
|
3
|
+
// AndroidSwipeRefreshLayout and WRAPS the scroll view (a ScrollView there hosts one child). Vue
|
|
4
|
+
// takes the wrapped child via its DEFAULT SLOT (the seam the Android scroll-view wrap re-invokes
|
|
5
|
+
// to host the scroll view inside it; iOS leaves the slot empty), folds aria/role through the
|
|
6
|
+
// shared resolveAccessibilityProps, and forwards the native props onto the
|
|
7
|
+
// symbiote-refresh-control host node.
|
|
8
8
|
//
|
|
9
|
-
// `refreshing` is a controlled prop
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
// (untyped), so the forwarded bag is BUILT at the a11y-intersection type (a genuine narrowing, not
|
|
13
|
-
// a cast) before the aria fold.
|
|
9
|
+
// `refreshing` is a controlled prop the parent pushes down each commit; native reports the
|
|
10
|
+
// gesture via the direct `topRefresh` event, routed to the host onRefresh prop and turned into
|
|
11
|
+
// the typed Vue `refresh` emit.
|
|
14
12
|
import { defineComponent, h } from '@vue/runtime-core';
|
|
15
13
|
import { dlog } from '@symbiote-native/engine';
|
|
16
14
|
import { resolveAccessibilityProps, } from '@symbiote-native/components';
|
|
17
15
|
import { normalizeVueAttrs } from '../utils/normalize-attrs.js';
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
// accessibility* props. Children cross via slots, never attrs. When the Android wrap re-invokes
|
|
21
|
-
// this component, the injected `style` (the outer/layout half) rides through too.
|
|
16
|
+
// Built at the a11y-intersection type (a real narrowing, not a cast), then fold aria-*/role into
|
|
17
|
+
// the canonical accessibility* props. Children cross via slots, never attrs.
|
|
22
18
|
function foldAttrs(attrs) {
|
|
23
19
|
const bag = {};
|
|
24
20
|
for (const key of Object.keys(attrs)) {
|
|
@@ -35,8 +31,6 @@ export const RefreshControl = defineComponent((_props, { attrs: rawAttrs, emit,
|
|
|
35
31
|
if (nativeProps.enabled !== undefined)
|
|
36
32
|
dlog(`RefreshControl enabled=${String(nativeProps.enabled)} (Android-only)`);
|
|
37
33
|
dlog('RefreshControl refresh emit wired');
|
|
38
|
-
// The default slot is the seam: empty on iOS (childless sibling), but the Android scroll-view
|
|
39
|
-
// wrap re-invokes this component with the scroll view as its default slot, so it hosts it.
|
|
40
34
|
return h('symbiote-refresh-control', {
|
|
41
35
|
...nativeProps,
|
|
42
36
|
onRefresh: () => {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type { IScrollViewProps, IScrollViewEmits, IScrollViewHandle } from './shared';
|
|
1
|
+
export type { IScrollViewProps, IScrollViewEmits, IScrollViewHandle, } from './shared';
|
|
2
2
|
export declare const ScrollView: import("vue").DefineSetupFnComponent<import("./shared").IScrollViewProps, import("./shared").IScrollViewEmits, {}, import("./shared").IScrollViewProps & {
|
|
3
3
|
onContentSizeChange?: ((width: number, height: number) => any) | undefined;
|
|
4
4
|
}, import("vue").PublicProps>;
|
|
@@ -1,15 +1,14 @@
|
|
|
1
|
-
// ScrollView on Android. An Android ScrollView accepts only ONE child, so a
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
1
|
+
// ScrollView on Android. An Android ScrollView accepts only ONE child, so a RefreshControl can't
|
|
2
|
+
// be a sibling of the content the way iOS allows ("addViewAt: failed to insert view ... at index
|
|
3
|
+
// 1"). Instead the RefreshControl (AndroidSwipeRefreshLayout) WRAPS the scroll view, nested
|
|
4
|
+
// inside with nestedScrollEnabled so it handles the gesture before the refresh parent, mirroring
|
|
5
|
+
// RN's ScrollView.js android branch.
|
|
6
6
|
//
|
|
7
|
-
// React does this with cloneElement(refreshControl, {style}, scrollView). Vue has
|
|
8
|
-
// so the analog is to RE-INVOKE the user's RefreshControl component type via h():
|
|
9
|
-
// own .props plus the injected outer/layout style, and the inner scroll view as
|
|
10
|
-
// The
|
|
11
|
-
//
|
|
12
|
-
// Metro picks this on an Android host; no Platform.OS read.
|
|
7
|
+
// React does this with cloneElement(refreshControl, {style}, scrollView). Vue has no
|
|
8
|
+
// cloneElement, so the analog is to RE-INVOKE the user's RefreshControl component type via h():
|
|
9
|
+
// same .type, its own .props plus the injected outer/layout style, and the inner scroll view as
|
|
10
|
+
// its DEFAULT SLOT. The node ref stays on the INNER scroll view (not the wrapper), so
|
|
11
|
+
// dispatchViewCommand targets it. Metro picks this file on an Android host; no Platform.OS read.
|
|
13
12
|
// device-verify-pending: the wrap shape mirrors RN, proven on a real host by the absence of the
|
|
14
13
|
// "addViewAt: failed to insert" crash.
|
|
15
14
|
import { h, isVNode } from '@vue/runtime-core';
|
|
@@ -19,38 +18,32 @@ import { createScrollView } from './shared.js';
|
|
|
19
18
|
function isRecord(value) {
|
|
20
19
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
21
20
|
}
|
|
22
|
-
//
|
|
23
|
-
//
|
|
24
|
-
// the symbol constants fail the typeof checks and a nested VNode is ruled out by isVNode. A type
|
|
25
|
-
// predicate (not a plain return) sidesteps the assignability gap; refreshControl is always a real
|
|
26
|
-
// component element in practice.
|
|
21
|
+
// Narrows VNodeTypes (string | Component | nested VNode | the Fragment/Text/... symbol
|
|
22
|
+
// constants) to what h() accepts. refreshControl is always a real component element in practice.
|
|
27
23
|
function isHostType(type) {
|
|
28
24
|
if (typeof type === 'string')
|
|
29
25
|
return true;
|
|
30
|
-
return ((typeof type === 'object' || typeof type === 'function') &&
|
|
26
|
+
return ((typeof type === 'object' || typeof type === 'function') &&
|
|
27
|
+
type !== null &&
|
|
28
|
+
!isVNode(type));
|
|
31
29
|
}
|
|
32
30
|
export const ScrollView = createScrollView({
|
|
33
31
|
assemble: input => {
|
|
34
32
|
if (input.refreshControl === undefined) {
|
|
35
|
-
// No refresh: the same single-child path as Phase 1; the node ref rides input.scrollProps.
|
|
36
33
|
dlog('Vue ScrollView.ANDROID refreshControl=NONE(1child)');
|
|
37
34
|
return h(input.scrollViewIntrinsic, input.scrollProps, [input.content]);
|
|
38
35
|
}
|
|
39
|
-
// RN splits the flattened style across the two boxes
|
|
40
|
-
//
|
|
41
|
-
//
|
|
42
|
-
//
|
|
43
|
-
// it, NOT a hardcoded flex:1 that would override an explicit user height/width.
|
|
36
|
+
// RN splits the flattened style across the two boxes: LAYOUT props (margin/flex/size/
|
|
37
|
+
// position/...) drive the outer AndroidSwipeRefreshLayout frame; VISUAL props (background/
|
|
38
|
+
// padding/border/...) paint the inner scroll view - not a hardcoded flex:1 that would
|
|
39
|
+
// override an explicit user height/width.
|
|
44
40
|
//
|
|
45
|
-
// layoutSplitStyle (not userStyle): a class-only layout prop
|
|
46
|
-
//
|
|
47
|
-
//
|
|
48
|
-
// collapses to nothing. layoutSplitStyle is userStyle merged with the resolved class style.
|
|
41
|
+
// layoutSplitStyle (not userStyle): a class-only layout prop is invisible to userStyle (it
|
|
42
|
+
// never carries the resolved `class` value - see isClassNameProp in shared.ts), so splitting
|
|
43
|
+
// on userStyle alone would starve the wrapper of its layout style.
|
|
49
44
|
const { outer, inner } = splitLayoutProps(input.layoutSplitStyle);
|
|
50
|
-
// `class` is stripped
|
|
51
|
-
//
|
|
52
|
-
// half onto the inner scroll view a second time (on top of the outer wrapper), the same
|
|
53
|
-
// wrapper/inner leak `style` already avoids by living in HANDLED_ATTRS.
|
|
45
|
+
// `class` is stripped here: layoutSplitStyle already folded its resolved value into
|
|
46
|
+
// outer/inner above, so forwarding the raw prop too would re-apply its LAYOUT half a second time.
|
|
54
47
|
const { class: _classAppliedViaSplit, ...innerScrollOuterProps } = input.scrollOuterProps;
|
|
55
48
|
const innerScrollView = h(input.scrollViewIntrinsic, {
|
|
56
49
|
...innerScrollOuterProps,
|
|
@@ -60,15 +53,12 @@ export const ScrollView = createScrollView({
|
|
|
60
53
|
}, [input.content]);
|
|
61
54
|
const rc = input.refreshControl;
|
|
62
55
|
if (!isHostType(rc.type)) {
|
|
63
|
-
// refreshControl is always a component VNode; a symbol/nested-VNode type can't host children.
|
|
64
56
|
// Degrade to the unwrapped scroll view rather than crash (the node ref is already on it).
|
|
65
57
|
dlog('Vue ScrollView.ANDROID refreshControl has no hostable type, rendering scroll view unwrapped');
|
|
66
58
|
return innerScrollView;
|
|
67
59
|
}
|
|
68
60
|
const rcProps = isRecord(rc.props) ? rc.props : {};
|
|
69
61
|
dlog('Vue ScrollView.ANDROID refreshControl=WRAP');
|
|
70
|
-
// Re-invoke the user's RefreshControl: same type, its own props + the outer/layout style, and the
|
|
71
|
-
// inner scroll view as the default slot (the Vue analog of cloneElement(refreshControl, {style}, sv)).
|
|
72
62
|
return h(rc.type, { ...rcProps, style: outer }, { default: () => [innerScrollView] });
|
|
73
63
|
},
|
|
74
64
|
});
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type { IScrollViewProps, IScrollViewEmits, IScrollViewHandle } from './shared';
|
|
1
|
+
export type { IScrollViewProps, IScrollViewEmits, IScrollViewHandle, } from './shared';
|
|
2
2
|
export declare const ScrollView: import("vue").DefineSetupFnComponent<import("./shared").IScrollViewProps, import("./shared").IScrollViewEmits, {}, import("./shared").IScrollViewProps & {
|
|
3
3
|
onContentSizeChange?: ((width: number, height: number) => any) | undefined;
|
|
4
4
|
}, import("vue").PublicProps>;
|
|
@@ -9,7 +9,9 @@ export const ScrollView = createScrollView({
|
|
|
9
9
|
assemble: input => {
|
|
10
10
|
// Sibling placement: RefreshControl before content (RN iOS). The node ref stays on the scroll
|
|
11
11
|
// view via input.scrollProps, so the imperative handle targets it whether or not refresh is on.
|
|
12
|
-
const children = input.refreshControl === undefined
|
|
12
|
+
const children = input.refreshControl === undefined
|
|
13
|
+
? [input.content]
|
|
14
|
+
: [input.refreshControl, input.content];
|
|
13
15
|
return h(input.scrollViewIntrinsic, input.scrollProps, children);
|
|
14
16
|
},
|
|
15
17
|
});
|
|
@@ -1,48 +1,34 @@
|
|
|
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';
|
|
24
|
-
import { wrapStickyHeaders } from './sticky-header.js';
|
|
18
|
+
import { wrapStickyHeaders, } from './sticky-header.js';
|
|
25
19
|
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
|
-
return Array.isArray(value) && value.every(entry => typeof entry === 'number');
|
|
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
|
-
return typeof value === 'function' || (typeof value === 'object' && value !== null);
|
|
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,22 @@ 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
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
129
|
+
const classProp = isClassNameProp(attrs.class)
|
|
130
|
+
? attrs.class
|
|
131
|
+
: undefined;
|
|
132
|
+
const layoutSplitStyle = [
|
|
133
|
+
resolveClassName(classProp),
|
|
134
|
+
userStyle,
|
|
135
|
+
];
|
|
166
136
|
const contentContainerStyle = typeof attrs.contentContainerStyle === 'string'
|
|
167
137
|
? resolveClassName(attrs.contentContainerStyle)
|
|
168
138
|
: isStyleProp(attrs.contentContainerStyle)
|
|
169
139
|
? attrs.contentContainerStyle
|
|
170
140
|
: 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
141
|
const stickyHeaderIndices = isNumberArray(attrs.stickyHeaderIndices)
|
|
175
142
|
? attrs.stickyHeaderIndices
|
|
176
143
|
: undefined;
|
|
@@ -179,28 +146,30 @@ export function createScrollView(platform) {
|
|
|
179
146
|
const stickyHeaderComponent = isComponent(attrs.StickyHeaderComponent)
|
|
180
147
|
? attrs.StickyHeaderComponent
|
|
181
148
|
: undefined;
|
|
182
|
-
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.
|
|
149
|
+
const { scrollViewIntrinsic, contentIntrinsic, scrollViewBaseStyle, contentStyle, } = selectScrollIntrinsics(isHorizontal, contentContainerStyle);
|
|
150
|
+
// RN defaults nested scrolling ON (ScrollView.js `nestedScrollEnabled ?? true`);
|
|
151
|
+
// horizontal forwards only when defined (load-bearing on iOS's RCTScrollView axis).
|
|
187
152
|
const outerProps = {
|
|
188
153
|
...resolveAccessibilityProps(forwardAttrs(attrs)),
|
|
189
154
|
};
|
|
190
155
|
outerProps.nestedScrollEnabled =
|
|
191
|
-
typeof attrs.nestedScrollEnabled === 'boolean'
|
|
156
|
+
typeof attrs.nestedScrollEnabled === 'boolean'
|
|
157
|
+
? attrs.nestedScrollEnabled
|
|
158
|
+
: true;
|
|
192
159
|
if (attrs.horizontal !== undefined)
|
|
193
160
|
outerProps.horizontal = attrs.horizontal;
|
|
194
161
|
const decel = asDecelerationRate(attrs.decelerationRate);
|
|
195
162
|
if (decel !== undefined)
|
|
196
163
|
outerProps.decelerationRate = resolveDecelerationRate(decel);
|
|
197
|
-
//
|
|
198
|
-
//
|
|
199
|
-
//
|
|
200
|
-
//
|
|
164
|
+
// When sticky headers are active, the offset must reach the AnimatedValue (RN's
|
|
165
|
+
// _scrollAnimatedValueAttachment). forwardAttrs already put the user's onScroll/onLayout/
|
|
166
|
+
// scrollEventThrottle on outerProps; here we override per resolveScrollForwarding's
|
|
167
|
+
// decisions (which path, throttle default, inverted capture).
|
|
201
168
|
const nativeStickyAvailable = hasStickyHeaders && isNativeAnimatedAvailable();
|
|
202
169
|
nativeStickyWanted = nativeStickyAvailable;
|
|
203
|
-
const userThrottle = typeof attrs.scrollEventThrottle === 'number'
|
|
170
|
+
const userThrottle = typeof attrs.scrollEventThrottle === 'number'
|
|
171
|
+
? attrs.scrollEventThrottle
|
|
172
|
+
: undefined;
|
|
204
173
|
const forwarding = resolveScrollForwarding({
|
|
205
174
|
hasStickyHeaders,
|
|
206
175
|
nativeStickyAvailable,
|
|
@@ -210,24 +179,29 @@ export function createScrollView(platform) {
|
|
|
210
179
|
snapToAlignment: attrs.snapToAlignment,
|
|
211
180
|
});
|
|
212
181
|
if (hasStickyHeaders) {
|
|
213
|
-
const userOnScroll = isHandler(attrs.onScroll)
|
|
182
|
+
const userOnScroll = isHandler(attrs.onScroll)
|
|
183
|
+
? attrs.onScroll
|
|
184
|
+
: undefined;
|
|
214
185
|
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.
|
|
186
|
+
// JS fallback (no native module): correct, but lags a frame under fast scroll, which
|
|
187
|
+
// the native path removes on a real host.
|
|
218
188
|
outerProps.onScroll = animatedEvent([{ nativeEvent: { contentOffset: { y: scrollAnimatedValue } } }], userOnScroll === undefined
|
|
219
189
|
? undefined
|
|
220
|
-
: {
|
|
190
|
+
: {
|
|
191
|
+
listener: (...args) => forwardScrollEvent(userOnScroll, args),
|
|
192
|
+
});
|
|
221
193
|
}
|
|
222
|
-
// Native path: the value is driven on the UI thread by the post-commit watch above, so
|
|
223
|
-
//
|
|
194
|
+
// Native path: the value is driven on the UI thread by the post-commit watch above, so
|
|
195
|
+
// onScroll forwards untouched.
|
|
224
196
|
if (forwarding.scrollEventThrottle !== undefined) {
|
|
225
197
|
outerProps.scrollEventThrottle = forwarding.scrollEventThrottle;
|
|
226
198
|
}
|
|
227
|
-
//
|
|
228
|
-
//
|
|
199
|
+
// Capture the viewport height for inverted sticky headers (RN _handleLayout), then call
|
|
200
|
+
// the user's handler.
|
|
229
201
|
if (forwarding.capturesViewportHeight) {
|
|
230
|
-
const userOnLayout = isHandler(attrs.onLayout)
|
|
202
|
+
const userOnLayout = isHandler(attrs.onLayout)
|
|
203
|
+
? attrs.onLayout
|
|
204
|
+
: undefined;
|
|
231
205
|
outerProps.onLayout = (event) => {
|
|
232
206
|
const height = readLayoutDimension(event, 'height');
|
|
233
207
|
if (height !== undefined)
|
|
@@ -238,18 +212,18 @@ export function createScrollView(platform) {
|
|
|
238
212
|
}
|
|
239
213
|
}
|
|
240
214
|
dlog(`Vue ScrollView -> ${scrollViewIntrinsic} (horizontal=${isHorizontal} sticky=${hasStickyHeaders})`);
|
|
241
|
-
//
|
|
242
|
-
//
|
|
243
|
-
//
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
215
|
+
// `collapsable: false` keeps the layout-only content view as a real native view: Android
|
|
216
|
+
// Fabric view-flattens it away otherwise, hoisting the cells as direct children of the
|
|
217
|
+
// scroll view (which hosts exactly one) - an addViewAt crash. iOS never flattens.
|
|
218
|
+
const contentProps = {
|
|
219
|
+
style: contentStyle,
|
|
220
|
+
collapsable: false,
|
|
221
|
+
};
|
|
247
222
|
if (forwarding.collapsableChildren) {
|
|
248
223
|
contentProps.collapsableChildren = false;
|
|
249
224
|
}
|
|
250
|
-
//
|
|
251
|
-
//
|
|
252
|
-
// the setup-scope lastContentSize).
|
|
225
|
+
// Synthesized from the content view's own onLayout (RN _handleContentOnLayout); emit only
|
|
226
|
+
// on a real size change.
|
|
253
227
|
contentProps.onLayout = (event) => {
|
|
254
228
|
const width = readLayoutDimension(event, 'width');
|
|
255
229
|
const height = readLayoutDimension(event, 'height');
|
|
@@ -261,24 +235,20 @@ export function createScrollView(platform) {
|
|
|
261
235
|
dlog(`Vue ScrollView contentSizeChange ${width}x${height}`);
|
|
262
236
|
emit('contentSizeChange', width, height);
|
|
263
237
|
};
|
|
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
238
|
const slotChildren = slots.default !== undefined ? slots.default() : [];
|
|
267
239
|
const contentChildren = hasStickyHeaders
|
|
268
240
|
? wrapStickyHeaders(slotChildren, stickyHeaderIndices, scrollAnimatedValue, invertStickyHeaders, viewportHeight.value, stickyHeaderComponent, headerLayoutYs, onHeaderLayoutY)
|
|
269
241
|
: slotChildren;
|
|
270
242
|
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.
|
|
243
|
+
// Base style UNDER user style so an explicit user value (height, flexDirection) still wins.
|
|
273
244
|
const scrollProps = {
|
|
274
245
|
...outerProps,
|
|
275
246
|
style: [scrollViewBaseStyle, userStyle],
|
|
276
247
|
ref: setNodeRef,
|
|
277
248
|
};
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
const refreshControl = isVNode(attrs.refreshControl) ? attrs.refreshControl : undefined;
|
|
249
|
+
const refreshControl = isVNode(attrs.refreshControl)
|
|
250
|
+
? attrs.refreshControl
|
|
251
|
+
: undefined;
|
|
282
252
|
return platform.assemble({
|
|
283
253
|
scrollViewIntrinsic,
|
|
284
254
|
scrollProps,
|