@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,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,60 +22,59 @@ 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
|
-
const animatedTranslateY = shallowRef(scrollAnimatedValue.interpolate({
|
|
55
|
+
const animatedTranslateY = shallowRef(scrollAnimatedValue.interpolate({
|
|
56
|
+
inputRange: [-1, 0],
|
|
57
|
+
outputRange: [0, 0],
|
|
58
|
+
}));
|
|
64
59
|
let debounceTimer;
|
|
65
|
-
//
|
|
66
|
-
// listener (an engine call the reducer does not own) and onBeforeUnmount cleans up.
|
|
60
|
+
// Held so the next rebuild detaches the old listener and onBeforeUnmount cleans up.
|
|
67
61
|
let interpolation;
|
|
68
62
|
let listenerId;
|
|
69
63
|
const inputs = () => ({
|
|
70
64
|
os: Platform.OS,
|
|
71
65
|
inverted: typeof attrs.inverted === 'boolean' ? attrs.inverted : undefined,
|
|
72
|
-
scrollViewHeight: typeof attrs.scrollViewHeight === 'number'
|
|
73
|
-
|
|
66
|
+
scrollViewHeight: typeof attrs.scrollViewHeight === 'number'
|
|
67
|
+
? attrs.scrollViewHeight
|
|
68
|
+
: undefined,
|
|
69
|
+
nextHeaderLayoutY: typeof attrs.nextHeaderLayoutY === 'number'
|
|
70
|
+
? attrs.nextHeaderLayoutY
|
|
71
|
+
: undefined,
|
|
74
72
|
});
|
|
75
73
|
const runEffects = (effects) => {
|
|
76
74
|
for (const effect of effects) {
|
|
77
75
|
switch (effect.kind) {
|
|
78
76
|
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).
|
|
77
|
+
// Detach the old listener, build a fresh interpolation, and wire the settled-value listener.
|
|
81
78
|
if (interpolation !== undefined && listenerId !== undefined) {
|
|
82
79
|
interpolation.removeListener(listenerId);
|
|
83
80
|
listenerId = undefined;
|
|
@@ -95,8 +92,8 @@ export const ScrollViewStickyHeader = defineComponent({
|
|
|
95
92
|
break;
|
|
96
93
|
}
|
|
97
94
|
case 'schedule-debounce':
|
|
98
|
-
// The animated value updates several times per frame; debounce the settled value
|
|
99
|
-
//
|
|
95
|
+
// The animated value updates several times per frame; debounce the settled value so
|
|
96
|
+
// hit detection stays current (a Fabric issue, worse on Android).
|
|
100
97
|
if (debounceTimer !== undefined)
|
|
101
98
|
clearTimeout(debounceTimer);
|
|
102
99
|
debounceTimer = setTimeout(() => {
|
|
@@ -108,8 +105,7 @@ export const ScrollViewStickyHeader = defineComponent({
|
|
|
108
105
|
version.value += 1;
|
|
109
106
|
break;
|
|
110
107
|
case 'record-header-y':
|
|
111
|
-
// Vue records through attrs.onLayout (the wrapper closure)
|
|
112
|
-
// IStickyHeaderProps contract; the reducer emits no index for it.
|
|
108
|
+
// Vue records through attrs.onLayout (the wrapper closure); the reducer emits no index for it.
|
|
113
109
|
break;
|
|
114
110
|
}
|
|
115
111
|
}
|
|
@@ -117,9 +113,8 @@ export const ScrollViewStickyHeader = defineComponent({
|
|
|
117
113
|
const dispatch = (action) => {
|
|
118
114
|
runEffects(reduceSticky(state, action, inputs()).effects);
|
|
119
115
|
};
|
|
120
|
-
// Rebuild whenever the collision/viewport inputs change (
|
|
121
|
-
//
|
|
122
|
-
// build on mount. scrollAnimatedValue is the stable const (never changes for one ScrollView).
|
|
116
|
+
// Rebuild whenever the collision/viewport inputs change ([inverted, scrollViewHeight,
|
|
117
|
+
// nextHeaderLayoutY]); also runs once on mount.
|
|
123
118
|
watchEffect(() => {
|
|
124
119
|
dispatch({ kind: 'inputs-changed' });
|
|
125
120
|
});
|
|
@@ -130,20 +125,25 @@ export const ScrollViewStickyHeader = defineComponent({
|
|
|
130
125
|
if (debounceTimer !== undefined)
|
|
131
126
|
clearTimeout(debounceTimer);
|
|
132
127
|
});
|
|
133
|
-
//
|
|
134
|
-
//
|
|
135
|
-
// attrs.onLayout), then the child's own onLayout. Matches RN ScrollViewStickyHeader.js._onLayout.
|
|
128
|
+
// Dispatch the layout, fire the wrapper's recorder (onHeaderLayoutY via attrs.onLayout), then
|
|
129
|
+
// the child's own onLayout. Matches RN ScrollViewStickyHeader.js._onLayout.
|
|
136
130
|
const onLayout = (event) => {
|
|
137
131
|
const y = readLayoutNumber(event, 'y');
|
|
138
132
|
const height = readLayoutNumber(event, 'height');
|
|
139
133
|
// Keep the previous value when a field is absent (RN sets state only on a defined read).
|
|
140
|
-
dispatch({
|
|
134
|
+
dispatch({
|
|
135
|
+
kind: 'layout',
|
|
136
|
+
y: y ?? state.layoutY,
|
|
137
|
+
height: height ?? state.layoutHeight,
|
|
138
|
+
});
|
|
141
139
|
const recorder = attrs.onLayout;
|
|
142
140
|
if (isHandler(recorder))
|
|
143
141
|
recorder(event);
|
|
144
142
|
const children = slots.default !== undefined ? slots.default() : [];
|
|
145
143
|
const child = children[0];
|
|
146
|
-
const childOnLayout = isVNode(child)
|
|
144
|
+
const childOnLayout = isVNode(child)
|
|
145
|
+
? readChildOnLayout(child)
|
|
146
|
+
: undefined;
|
|
147
147
|
if (childOnLayout !== undefined)
|
|
148
148
|
childOnLayout(event);
|
|
149
149
|
};
|
|
@@ -151,13 +151,12 @@ export const ScrollViewStickyHeader = defineComponent({
|
|
|
151
151
|
// Read the version bump so a committed translateY re-runs render.
|
|
152
152
|
void version.value;
|
|
153
153
|
// The EXPLICIT debounced translateY overrides the committed transform for hit-testing, while
|
|
154
|
-
//
|
|
154
|
+
// animatedTranslateY does the smooth (native-driven) pin.
|
|
155
155
|
const passthroughAnimatedPropExplicitValues = state.translateY !== null
|
|
156
156
|
? { style: { transform: [{ translateY: state.translateY }] } }
|
|
157
157
|
: null;
|
|
158
158
|
// 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.
|
|
159
|
+
// OVER the rows scrolling under it.
|
|
161
160
|
return h(Animated.View, {
|
|
162
161
|
style: {
|
|
163
162
|
transform: [{ translateY: animatedTranslateY.value }],
|
|
@@ -171,15 +170,13 @@ export const ScrollViewStickyHeader = defineComponent({
|
|
|
171
170
|
},
|
|
172
171
|
});
|
|
173
172
|
// 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.
|
|
173
|
+
// shared scroll AnimatedValue. Returns the children unchanged when no indices are flagged.
|
|
176
174
|
//
|
|
177
|
-
// Cross-talk plumbing (RN's _headerLayoutYs + _onStickyHeaderLayout
|
|
178
|
-
//
|
|
179
|
-
//
|
|
180
|
-
//
|
|
181
|
-
//
|
|
182
|
-
// undefined and it sticks indefinitely (correct).
|
|
175
|
+
// Cross-talk plumbing (RN's _headerLayoutYs + _onStickyHeaderLayout): headerLayoutYs is a
|
|
176
|
+
// child-index -> measured-y map; each header reports its own y through onHeaderLayoutY, and we
|
|
177
|
+
// feed every header the y of the NEXT flagged header (the collision point past which it scrolls
|
|
178
|
+
// off). The LAST flagged header has no successor, so its nextHeaderLayoutY stays undefined and
|
|
179
|
+
// it sticks indefinitely (correct).
|
|
183
180
|
export function wrapStickyHeaders(children, stickyHeaderIndices, scrollAnimatedValue, invertStickyHeaders, scrollViewHeight, StickyHeaderComponent, headerLayoutYs, onHeaderLayoutY) {
|
|
184
181
|
if (stickyHeaderIndices === undefined || stickyHeaderIndices.length === 0)
|
|
185
182
|
return children;
|
|
@@ -188,17 +185,15 @@ export function wrapStickyHeaders(children, stickyHeaderIndices, scrollAnimatedV
|
|
|
188
185
|
const indexOfIndex = stickyHeaderIndices.indexOf(index);
|
|
189
186
|
if (indexOfIndex === -1 || !isVNode(child))
|
|
190
187
|
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).
|
|
188
|
+
// The next flagged header's measured y. undefined until that header has measured (or for the last).
|
|
193
189
|
const nextIndex = stickyHeaderIndices[indexOfIndex + 1];
|
|
194
190
|
const nextHeaderLayoutY = nextStickyHeaderY(stickyHeaderIndices, indexOfIndex, headerLayoutYs);
|
|
195
191
|
dlog(`Vue ScrollView sticky-header wrap index=${index} next=${nextIndex} nextY=${nextHeaderLayoutY}`);
|
|
196
192
|
const props = {
|
|
197
193
|
key: child.key ?? `sticky-${index}`,
|
|
198
194
|
nextHeaderLayoutY,
|
|
199
|
-
//
|
|
200
|
-
//
|
|
201
|
-
// the resulting re-render (the headerLayoutYs bump in scroll-view-shared).
|
|
195
|
+
// Record this header's own y into the parent map; the lookup above feeds it forward to the
|
|
196
|
+
// previous header on the resulting re-render (the headerLayoutYs bump in scroll-view-shared).
|
|
202
197
|
onLayout: (event) => {
|
|
203
198
|
const y = readLayoutNumber(event, 'y');
|
|
204
199
|
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
|
-
|
|
41
|
-
(_props, { attrs, expose, emit, slots }) => {
|
|
36
|
+
// _props is read only by the type system, to let ItemT infer at the call site.
|
|
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;
|
|
@@ -59,9 +54,9 @@ export const SectionList = defineComponent(
|
|
|
59
54
|
const startReached = listens('onStartReached')
|
|
60
55
|
? (info) => emit('startReached', info)
|
|
61
56
|
: undefined;
|
|
62
|
-
const refresh = listens('onRefresh')
|
|
63
|
-
|
|
64
|
-
|
|
57
|
+
const refresh = listens('onRefresh')
|
|
58
|
+
? () => emit('refresh')
|
|
59
|
+
: undefined;
|
|
65
60
|
return h(VirtualizedSectionListHost, {
|
|
66
61
|
...normalizeVueAttrs(attrs),
|
|
67
62
|
ref: setInner,
|
|
@@ -1,14 +1,12 @@
|
|
|
1
|
-
// Switch, the Vue lifecycle half. The logic (
|
|
2
|
-
//
|
|
3
|
-
// @symbiote-native/components/view, both shared verbatim with
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
// twin of the React adapter's useReducer + useLayoutEffect + dispatchViewCommand.
|
|
1
|
+
// Switch, the Vue lifecycle half. The logic (lastNativeReport reducer, valueFromChange, the
|
|
2
|
+
// snap-back decision) lives in @symbiote-native/components/state and the render in
|
|
3
|
+
// @symbiote-native/components/view, both shared verbatim with React. Vue supplies the
|
|
4
|
+
// reactivity: a ref holds what native last reported, a function ref grabs the host node, and a
|
|
5
|
+
// post-flush watch snaps native back when the parent rejects a toggle.
|
|
7
6
|
//
|
|
8
|
-
// Inputs arrive as attrs (untyped), so each is narrowed with a runtime guard
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
// and crash Android's folly::dynamic.
|
|
7
|
+
// Inputs arrive as attrs (untyped), so each is narrowed with a runtime guard, never a cast.
|
|
8
|
+
// onValueChange MUST be stripped from the forwarded attrs: it is not a ViewConfig event, so
|
|
9
|
+
// leaking it would reach Fabric as a function prop and crash Android's folly::dynamic.
|
|
12
10
|
import { defineComponent, ref, shallowRef, watch } from '@vue/runtime-core';
|
|
13
11
|
import { renderSwitch, switchReducer, createInitialSwitchState, shouldSnapBack, valueFromChange, } from '@symbiote-native/components';
|
|
14
12
|
import { dispatchViewCommand, isSymbioteNode, dlog, } from '@symbiote-native/engine';
|
|
@@ -24,7 +22,6 @@ function asString(value) {
|
|
|
24
22
|
function isBoolean(value) {
|
|
25
23
|
return typeof value === 'boolean';
|
|
26
24
|
}
|
|
27
|
-
// trackColor arrives untyped; keep only the string false/true entries the render fn reads.
|
|
28
25
|
function normalizeTrackColor(value) {
|
|
29
26
|
if (!isRecord(value))
|
|
30
27
|
return undefined;
|
|
@@ -36,12 +33,11 @@ function normalizeTrackColor(value) {
|
|
|
36
33
|
return trackColor;
|
|
37
34
|
}
|
|
38
35
|
// The render fn's style param is a plain object; array/registered styles degrade to undefined
|
|
39
|
-
// (the engine flattens those on its own).
|
|
36
|
+
// (the engine flattens those on its own).
|
|
40
37
|
function isViewStyleObject(value) {
|
|
41
38
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
42
39
|
}
|
|
43
|
-
//
|
|
44
|
-
// testID, …) forwards onto the host node. onValueChange is pure JS and must never reach Fabric.
|
|
40
|
+
// Everything else forwards onto the host node. onValueChange is pure JS and must never reach Fabric.
|
|
45
41
|
const HANDLED_ATTRS = [
|
|
46
42
|
'value',
|
|
47
43
|
'modelValue',
|
|
@@ -62,35 +58,30 @@ function forwardAttrs(attrs) {
|
|
|
62
58
|
}
|
|
63
59
|
export function createSwitch(platform) {
|
|
64
60
|
return defineComponent((_props, { attrs: rawAttrs, emit }) => {
|
|
65
|
-
// shallowRef, NOT ref:
|
|
66
|
-
//
|
|
67
|
-
//
|
|
68
|
-
// keyed on the raw node, so every imperative command (dispatchViewCommand / measure /
|
|
69
|
-
// setNativeProps) would miss on the Proxy and silently no-op. shallowRef stores the raw
|
|
70
|
-
// node untouched. General rule for this adapter: host nodes live in shallowRef / markRaw.
|
|
61
|
+
// shallowRef, NOT ref: a plain ref() would hand back a reactive Proxy the engine's WeakMap
|
|
62
|
+
// mirror doesn't recognize, so every imperative command would silently no-op. General rule
|
|
63
|
+
// for this adapter: host nodes live in shallowRef / markRaw.
|
|
71
64
|
const nodeRef = shallowRef(null);
|
|
72
65
|
const state = ref(createInitialSwitchState());
|
|
73
|
-
// The host node our Vue renderer creates IS an engine SymbioteNode; Vue hands it to this
|
|
74
|
-
// function ref on mount. A stable function ref (defined once) isn't re-invoked per patch.
|
|
75
66
|
const setNodeRef = (el) => {
|
|
76
67
|
nodeRef.value = isSymbioteNode(el) ? el : null;
|
|
77
68
|
};
|
|
78
69
|
const handleChange = (event) => {
|
|
79
|
-
// Vue-facing event is emitted here; the host still receives this internal onChange
|
|
80
|
-
// handler so the controlled snap-back state stays synchronized with native.
|
|
81
70
|
const next = valueFromChange(event);
|
|
82
71
|
dlog(`Switch onChange value=${String(next)} eventCount=${String(event.nativeEvent.eventCount)}`);
|
|
83
72
|
if (next === undefined)
|
|
84
73
|
return;
|
|
85
74
|
emit('valueChange', next, event);
|
|
86
75
|
emitModelUpdate(emit, next);
|
|
87
|
-
state.value = switchReducer(state.value, {
|
|
76
|
+
state.value = switchReducer(state.value, {
|
|
77
|
+
type: 'native-reported',
|
|
78
|
+
value: next,
|
|
79
|
+
});
|
|
88
80
|
};
|
|
89
|
-
// Snap-back: when native reported a value the parent rejected
|
|
90
|
-
//
|
|
91
|
-
//
|
|
92
|
-
//
|
|
93
|
-
// before the command reads its Fabric handle.
|
|
81
|
+
// Snap-back: when native reported a value the parent rejected, command the JS value back
|
|
82
|
+
// down (the controlled-Switch correction RN does via SwitchCommands.setValue). The
|
|
83
|
+
// decision is shared with React (shouldSnapBack); only the command name is platform-
|
|
84
|
+
// imperative. flush:'post' so the node is committed before the command reads its Fabric handle.
|
|
94
85
|
watch(() => ({
|
|
95
86
|
fabricValue: resolveModelValue(rawAttrs, isBoolean) === true,
|
|
96
87
|
switchState: state.value,
|
|
@@ -109,12 +100,18 @@ export function createSwitch(platform) {
|
|
|
109
100
|
const attrs = normalizeVueAttrs(rawAttrs);
|
|
110
101
|
return descriptorToVue(renderSwitch({
|
|
111
102
|
value: resolveModelValue(attrs, isBoolean) === true,
|
|
112
|
-
disabled: typeof attrs.disabled === 'boolean'
|
|
103
|
+
disabled: typeof attrs.disabled === 'boolean'
|
|
104
|
+
? attrs.disabled
|
|
105
|
+
: undefined,
|
|
113
106
|
trackColor: normalizeTrackColor(attrs.trackColor),
|
|
114
107
|
thumbColor: asString(attrs.thumbColor),
|
|
115
108
|
ios_backgroundColor: asString(attrs.ios_backgroundColor),
|
|
116
109
|
style: isViewStyleObject(attrs.style) ? attrs.style : undefined,
|
|
117
|
-
passthrough: {
|
|
110
|
+
passthrough: {
|
|
111
|
+
...forwardAttrs(attrs),
|
|
112
|
+
ref: setNodeRef,
|
|
113
|
+
onChange: handleChange,
|
|
114
|
+
},
|
|
118
115
|
}, platform));
|
|
119
116
|
};
|
|
120
117
|
}, {
|
|
@@ -1,18 +1,14 @@
|
|
|
1
|
-
// TextInput, the Vue lifecycle half. The folds/maps (value->text,
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
// and expose() wires the imperative handle. The Vue twin of the React useState/useRef +
|
|
8
|
-
// useLayoutEffect + useImperativeHandle.
|
|
1
|
+
// TextInput, the Vue lifecycle half. The folds/maps (value->text, W3C/alias resolution) and the
|
|
2
|
+
// controlled-write predicate live in @symbiote-native/components/state, the render in
|
|
3
|
+
// @symbiote-native/components/view, both shared verbatim with React. Vue supplies only the
|
|
4
|
+
// reactivity: a shallowRef holds the host node, a ref holds the acknowledged event count, setup-
|
|
5
|
+
// scope `let`s hold the last native text + focus flag, a post-flush watch runs the
|
|
6
|
+
// controlled-write command, and expose() wires the imperative handle.
|
|
9
7
|
//
|
|
10
|
-
// Inputs arrive as attrs (untyped), so each is narrowed with a runtime guard
|
|
11
|
-
// onValueChange MUST be stripped from the forwarded attrs: it is not a ViewConfig event
|
|
12
|
-
//
|
|
13
|
-
|
|
14
|
-
// from @symbiote-native/engine, the same framework-agnostic singleton both adapters share.
|
|
15
|
-
import { defineComponent, onBeforeUnmount, ref, shallowRef, watch } from '@vue/runtime-core';
|
|
8
|
+
// Inputs arrive as attrs (untyped), so each is narrowed with a runtime guard, never a cast.
|
|
9
|
+
// onValueChange MUST be stripped from the forwarded attrs: it is not a ViewConfig event, so
|
|
10
|
+
// leaking it would reach Fabric as a function prop and crash Android's folly::dynamic.
|
|
11
|
+
import { defineComponent, onBeforeUnmount, ref, shallowRef, watch, } from '@vue/runtime-core';
|
|
16
12
|
import { resolveAccessibilityProps, resolveTextInputProps, renderTextInput, foldText, textFromChange, eventCountFromChange, shouldCommandText, INITIAL_EVENT_COUNT, SELECTION_NONE, } from '@symbiote-native/components';
|
|
17
13
|
import { dispatchViewCommand, isSymbioteNode, dlog, blurTextInput, setInputFocused, setInputBlurred, whenCommitted, } from '@symbiote-native/engine';
|
|
18
14
|
import { descriptorToVue } from '../../descriptor-to-vue.js';
|
|
@@ -30,7 +26,6 @@ function isString(value) {
|
|
|
30
26
|
function asBoolean(value) {
|
|
31
27
|
return typeof value === 'boolean' ? value : undefined;
|
|
32
28
|
}
|
|
33
|
-
// selection arrives untyped; keep only the numeric start/end the render fn + controlled write read.
|
|
34
29
|
function normalizeSelection(value) {
|
|
35
30
|
if (!isRecord(value))
|
|
36
31
|
return undefined;
|
|
@@ -41,9 +36,7 @@ function normalizeSelection(value) {
|
|
|
41
36
|
selection.end = value.end;
|
|
42
37
|
return selection;
|
|
43
38
|
}
|
|
44
|
-
//
|
|
45
|
-
// everything else (placeholder, secureTextEntry, the remaining native events, accessibility,
|
|
46
|
-
// testID, style…) forwards onto the host node. onFocus/onBlur are re-supplied as our wrapped
|
|
39
|
+
// Everything else forwards onto the host node. onFocus/onBlur are re-supplied as our wrapped
|
|
47
40
|
// handlers; onValueChange is pure JS and must never reach Fabric.
|
|
48
41
|
const HANDLED_ATTRS = [
|
|
49
42
|
'value',
|
|
@@ -80,38 +73,30 @@ function forwardAttrs(attrs) {
|
|
|
80
73
|
return result;
|
|
81
74
|
}
|
|
82
75
|
export const TextInput = defineComponent((_props, { attrs: rawAttrs, emit, expose }) => {
|
|
83
|
-
// shallowRef, NOT ref:
|
|
84
|
-
//
|
|
85
|
-
//
|
|
86
|
-
// (dispatchViewCommand / focus / blur / setTextAndSelection) would miss and silently no-op.
|
|
87
|
-
// Same rule applies to the Switch / ScrollView host node.
|
|
76
|
+
// shallowRef, NOT ref: a plain ref() would hand back a reactive Proxy the engine's WeakMap
|
|
77
|
+
// mirror doesn't recognize, so every imperative command would silently no-op. Same rule as
|
|
78
|
+
// the Switch / ScrollView host node.
|
|
88
79
|
const nodeRef = shallowRef(null);
|
|
89
80
|
const setNodeRef = (el) => {
|
|
90
81
|
nodeRef.value = isSymbioteNode(el) ? el : null;
|
|
91
82
|
};
|
|
92
|
-
// The count native last acknowledged. A ref so
|
|
93
|
-
//
|
|
94
|
-
// it so native's eventLag lands on 0.
|
|
83
|
+
// The count native last acknowledged. A ref so render echoes it as mostRecentEventCount; the
|
|
84
|
+
// controlled write commands it so native's eventLag lands on 0.
|
|
95
85
|
const mostRecentEventCount = ref(INITIAL_EVENT_COUNT);
|
|
96
|
-
//
|
|
97
|
-
//
|
|
98
|
-
// divergence and must NOT re-command). A setup-scope `let`: no render needed when it changes.
|
|
86
|
+
// Seeded from the mount-time value (the `text` prop already carries it down via createNode,
|
|
87
|
+
// so the FIRST controlled value is not a divergence and must NOT re-command).
|
|
99
88
|
let lastNativeText = foldText(resolveModelValue(rawAttrs, isString), asString(rawAttrs.defaultValue));
|
|
100
|
-
//
|
|
101
|
-
// synchronous focus getter (RN's TextInputState holds the same).
|
|
89
|
+
// Mirrored from focus/blur events for isFocused(): native exposes no synchronous focus getter.
|
|
102
90
|
let focused = false;
|
|
103
|
-
//
|
|
104
|
-
// not re-focus.
|
|
91
|
+
// Guard so a later node identity change does not re-focus.
|
|
105
92
|
let autoFocused = false;
|
|
106
93
|
const handleChange = (event) => {
|
|
107
|
-
//
|
|
108
|
-
// (+ `eventCount`). iOS and Android Fabric can key these differently, so log the actual shape.
|
|
94
|
+
// iOS and Android Fabric can key the change payload differently, so log the actual shape.
|
|
109
95
|
dlog(`TextInput change keys=[${Object.keys(event.nativeEvent).join(',')}] ` +
|
|
110
96
|
`text=${JSON.stringify(event.nativeEvent.text)} count=${JSON.stringify(event.nativeEvent.eventCount)}`);
|
|
111
97
|
const text = textFromChange(event);
|
|
112
98
|
if (text !== undefined) {
|
|
113
|
-
// Record the text first,
|
|
114
|
-
// stands for.
|
|
99
|
+
// Record the text first, so the count never runs ahead of the text it stands for.
|
|
115
100
|
lastNativeText = text;
|
|
116
101
|
emit('valueChange', text, event);
|
|
117
102
|
emitModelUpdate(emit, text);
|
|
@@ -136,10 +121,8 @@ export const TextInput = defineComponent((_props, { attrs: rawAttrs, emit, expos
|
|
|
136
121
|
emit('blur', event);
|
|
137
122
|
};
|
|
138
123
|
// Controlled write: when JS-side `value` diverges from what native reported, command the new
|
|
139
|
-
// text down with the acknowledged count
|
|
140
|
-
// keystrokes.
|
|
141
|
-
// a change). flush:'post' so the engine has committed the node before the command reads its
|
|
142
|
-
// Fabric handle; the predicate makes it a no-op on mount (value === the seed).
|
|
124
|
+
// text down with the acknowledged count - a plain prop re-push would race the user's
|
|
125
|
+
// keystrokes. flush:'post' so the node is committed before the command reads its Fabric handle.
|
|
143
126
|
watch(() => resolveModelValue(rawAttrs, isString), value => {
|
|
144
127
|
const node = nodeRef.value;
|
|
145
128
|
if (node === null)
|
|
@@ -158,11 +141,10 @@ export const TextInput = defineComponent((_props, { attrs: rawAttrs, emit, expos
|
|
|
158
141
|
]);
|
|
159
142
|
lastNativeText = value;
|
|
160
143
|
}, { flush: 'post' });
|
|
161
|
-
// autoFocus is driven in JS
|
|
162
|
-
//
|
|
163
|
-
//
|
|
164
|
-
//
|
|
165
|
-
// assigns the tag. Cancelled on unmount so an un-committed pending focus can't leak.
|
|
144
|
+
// autoFocus is driven in JS: once the node first commits, command `focus` down once. Under
|
|
145
|
+
// Vue's async-batched commit the node has no Fabric tag yet at post-flush, so
|
|
146
|
+
// whenCommitted defers it to the commit that assigns the tag. Cancelled on unmount so an
|
|
147
|
+
// un-committed pending focus can't leak.
|
|
166
148
|
let cancelAutoFocus;
|
|
167
149
|
watch(nodeRef, node => {
|
|
168
150
|
if (autoFocused || node === null || rawAttrs.autoFocus !== true)
|
|
@@ -172,10 +154,8 @@ export const TextInput = defineComponent((_props, { attrs: rawAttrs, emit, expos
|
|
|
172
154
|
cancelAutoFocus = whenCommitted(node, () => dispatchViewCommand(node, 'focus', []));
|
|
173
155
|
}, { flush: 'post' });
|
|
174
156
|
onBeforeUnmount(() => cancelAutoFocus?.());
|
|
175
|
-
//
|
|
176
|
-
//
|
|
177
|
-
// view commands; clear and setSelection reuse setTextAndSelection (the same stale-safe path as
|
|
178
|
-
// a controlled write) echoing the acknowledged event count.
|
|
157
|
+
// Methods read nodeRef.value / the count LIVE, so no stale capture. clear/setSelection reuse
|
|
158
|
+
// setTextAndSelection, the same path as a controlled write.
|
|
179
159
|
expose({
|
|
180
160
|
focus: () => {
|
|
181
161
|
const node = nodeRef.value;
|
|
@@ -190,7 +170,12 @@ export const TextInput = defineComponent((_props, { attrs: rawAttrs, emit, expos
|
|
|
190
170
|
const node = nodeRef.value;
|
|
191
171
|
if (node === null)
|
|
192
172
|
return;
|
|
193
|
-
dispatchViewCommand(node, 'setTextAndSelection', [
|
|
173
|
+
dispatchViewCommand(node, 'setTextAndSelection', [
|
|
174
|
+
mostRecentEventCount.value,
|
|
175
|
+
'',
|
|
176
|
+
0,
|
|
177
|
+
0,
|
|
178
|
+
]);
|
|
194
179
|
lastNativeText = '';
|
|
195
180
|
},
|
|
196
181
|
isFocused: () => focused,
|
|
@@ -32,7 +32,9 @@ function asBackground(value) {
|
|
|
32
32
|
return {
|
|
33
33
|
type: 'ThemeAttrAndroid',
|
|
34
34
|
attribute: value.attribute,
|
|
35
|
-
rippleRadius: typeof value.rippleRadius === 'number'
|
|
35
|
+
rippleRadius: typeof value.rippleRadius === 'number'
|
|
36
|
+
? value.rippleRadius
|
|
37
|
+
: undefined,
|
|
36
38
|
};
|
|
37
39
|
}
|
|
38
40
|
return undefined;
|