@symbiote-native/vue 0.4.0 → 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 +14 -5
- package/build/components/image-background.js +4 -5
- package/build/components/image.js +3 -1
- package/build/components/keyboard-avoiding-view.js +39 -14
- package/build/components/modal/index.js +5 -2
- package/build/components/pressable.d.ts +1 -1
- package/build/components/pressable.js +1 -1
- package/build/components/scroll-view/index.android.d.ts +1 -1
- package/build/components/scroll-view/index.android.js +3 -1
- 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 +33 -13
- package/build/components/scroll-view/sticky-header.js +18 -5
- package/build/components/section-list/index.js +4 -2
- package/build/components/switch/shared.js +12 -3
- package/build/components/text-input/index.js +7 -2
- 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 +42 -20
- package/build/components/virtualized-section-list/index.d.ts +5 -0
- package/build/components/virtualized-section-list/index.js +33 -4
- package/build/components.js +8 -3
- package/build/create-portal/index.d.ts +28 -0
- package/build/create-portal/index.js +42 -0
- package/build/host-instance/index.js +3 -1
- package/build/index.d.ts +8 -6
- package/build/index.js +9 -3
- package/build/modules/animated/create-animated-component.js +12 -4
- package/build/modules/animated/index.d.ts +2 -0
- package/build/modules/animated/index.js +18 -5
- package/build/modules/app-registry/index.js +4 -2
- package/build/modules/status-bar.js +4 -2
- package/build/render.d.ts +3 -1
- package/build/render.js +62 -1
- package/build/runtime-helpers/index.d.ts +44 -24
- package/build/runtime-helpers/index.js +107 -28
- package/build/utils/normalize-attrs.js +3 -1
- package/metro-vue-transformer.cjs +138 -97
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# @symbiote-native/vue
|
|
2
2
|
|
|
3
3
|
The **Vue 3 adapter** for [SymbioteNative](../../README.md) — render real native iOS/Android views from
|
|
4
|
-
Vue, on the
|
|
4
|
+
Vue, on the _same_ untouched core as React, with React Native's own renderer never in the path. It
|
|
5
5
|
is a `@vue/runtime-core` `createRenderer` whose nodeOps map each mutation onto the engine's
|
|
6
6
|
four-call API; `@symbiote-native/engine` does the clone-on-write commit into Fabric.
|
|
7
7
|
|
|
@@ -34,7 +34,7 @@ additionally needs a Metro transformer for `.vue` files (see
|
|
|
34
34
|
|
|
35
35
|
## Use it
|
|
36
36
|
|
|
37
|
-
The native entry reaches the
|
|
37
|
+
The native entry reaches the _same_ `registerRunnable` seam as React — only the adapter changes. It
|
|
38
38
|
hands the surface's `rootTag` to `mount` from `@symbiote-native/vue`, which drives the engine through Vue's
|
|
39
39
|
`createRenderer`:
|
|
40
40
|
|
|
@@ -95,19 +95,19 @@ and each adapter supplies only its lifecycle (Vue's `ref`/`watch` + the descript
|
|
|
95
95
|
The one deliberate gap is third-party **React component** packages such as
|
|
96
96
|
`@react-native-community/slider` used directly. Their body calls React hooks off the React
|
|
97
97
|
dispatcher, so they run only under the React adapter — under Vue the dispatcher is null and they
|
|
98
|
-
throw. SymbioteNative makes the
|
|
98
|
+
throw. SymbioteNative makes the _native view_ framework-agnostic, not the library's React _component_; such
|
|
99
99
|
a view becomes reachable from Vue only through a thin wrapper over the same `createNode`-by-ViewConfig
|
|
100
100
|
path SymbioteNative uses for its own primitives. `@symbiote-native/slider` (this repo's own wrapper around
|
|
101
|
-
`@react-native-community/slider`)
|
|
101
|
+
`@react-native-community/slider`) _does_ ship a real Vue build (`@symbiote-native/slider/vue`) through
|
|
102
102
|
exactly that path — it's what makes this one third-party native view usable from Vue at all; any
|
|
103
|
-
|
|
103
|
+
_other_ React-only component package stays React-adapter-only until it gets the same treatment.
|
|
104
104
|
|
|
105
105
|
---
|
|
106
106
|
|
|
107
107
|
## A Vue-specific gotcha — async commit timing
|
|
108
108
|
|
|
109
109
|
Vue batches commits on a microtask (every mutation schedules one `completeRoot`), so a node's
|
|
110
|
-
Fabric tag is assigned
|
|
110
|
+
Fabric tag is assigned _after_ `onMounted` / `watch(flush:'post')` runs. A native bind that reads
|
|
111
111
|
the tag at lifecycle time (native-driver `Animated`, sticky-header scroll attach, `TextInput`
|
|
112
112
|
autoFocus) would race the commit and silently no-op — while the JS-path headless test stays green.
|
|
113
113
|
React doesn't hit this because `react-reconciler` commits synchronously.
|
package/build/bootstrap.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// AppRegistry actually get wired. Lives OUTSIDE the package's main barrel — see that file's
|
|
4
4
|
// header for why anything importing react-native must stay there.
|
|
5
5
|
import { AppRegistry as RNAppRegistry } from 'react-native';
|
|
6
|
-
import { bootstrapHost } from '@symbiote-native/components/bootstrap';
|
|
6
|
+
import { bootstrapHost, } from '@symbiote-native/components/bootstrap';
|
|
7
7
|
import { AppRegistry, setHostRegistrar } from './modules/app-registry/index.js';
|
|
8
8
|
// Mirrors real Vue's createApp(App).mount(selector) two-step idiom.
|
|
9
9
|
export function createApp(App, options = {}) {
|
|
@@ -56,7 +56,7 @@ function buildDelegateHandle(getInner) {
|
|
|
56
56
|
recordInteraction: () => getInner()?.recordInteraction(),
|
|
57
57
|
};
|
|
58
58
|
}
|
|
59
|
-
export const FlatList = defineComponent((props, { attrs, expose, emit, slots }) => {
|
|
59
|
+
export const FlatList = defineComponent((props, { attrs, expose, emit, slots, }) => {
|
|
60
60
|
const inner = shallowRef(null);
|
|
61
61
|
const setInner = (instance) => {
|
|
62
62
|
inner.value = isVirtualizedListHandle(instance) ? instance : null;
|
|
@@ -70,11 +70,17 @@ export const FlatList = defineComponent((props, { attrs, expose, emit, slots })
|
|
|
70
70
|
return vnodeProps != null && typeof vnodeProps[onName] === 'function';
|
|
71
71
|
};
|
|
72
72
|
return () => {
|
|
73
|
-
const data = Array.isArray(props.data)
|
|
73
|
+
const data = Array.isArray(props.data)
|
|
74
|
+
? props.data
|
|
75
|
+
: [];
|
|
74
76
|
const keyExtractor = props.keyExtractor;
|
|
75
77
|
const numColumns = typeof props.numColumns === 'number' ? props.numColumns : SINGLE_COLUMN;
|
|
76
78
|
const viewabilityPairs = props.viewabilityConfigCallbackPairs;
|
|
77
|
-
const chromeSlots = {
|
|
79
|
+
const chromeSlots = {
|
|
80
|
+
header: slots.header,
|
|
81
|
+
footer: slots.footer,
|
|
82
|
+
empty: slots.empty,
|
|
83
|
+
};
|
|
78
84
|
const forwarded = normalizeVueAttrs(attrs);
|
|
79
85
|
dlog(`Vue FlatList over ${data.length} items, ${numColumns} column(s)`);
|
|
80
86
|
const endReached = listens('onEndReached')
|
|
@@ -83,7 +89,9 @@ export const FlatList = defineComponent((props, { attrs, expose, emit, slots })
|
|
|
83
89
|
const startReached = listens('onStartReached')
|
|
84
90
|
? (info) => emit('startReached', info)
|
|
85
91
|
: undefined;
|
|
86
|
-
const refresh = listens('onRefresh')
|
|
92
|
+
const refresh = listens('onRefresh')
|
|
93
|
+
? () => emit('refresh')
|
|
94
|
+
: undefined;
|
|
87
95
|
const scrollToIndexFailed = listens('onScrollToIndexFailed')
|
|
88
96
|
? (info) => emit('scrollToIndexFailed', info)
|
|
89
97
|
: undefined;
|
|
@@ -114,7 +122,8 @@ export const FlatList = defineComponent((props, { attrs, expose, emit, slots })
|
|
|
114
122
|
{ flexDirection: 'row' },
|
|
115
123
|
typeof props.columnWrapperStyle === 'string'
|
|
116
124
|
? resolveClassName(props.columnWrapperStyle)
|
|
117
|
-
: isRecord(props.columnWrapperStyle) ||
|
|
125
|
+
: isRecord(props.columnWrapperStyle) ||
|
|
126
|
+
Array.isArray(props.columnWrapperStyle)
|
|
118
127
|
? props.columnWrapperStyle
|
|
119
128
|
: undefined,
|
|
120
129
|
];
|
|
@@ -34,7 +34,9 @@ function asResizeMode(value) {
|
|
|
34
34
|
return isResizeMode(value) ? value : undefined;
|
|
35
35
|
}
|
|
36
36
|
function asCrossOrigin(value) {
|
|
37
|
-
return value === 'anonymous' || value === 'use-credentials'
|
|
37
|
+
return value === 'anonymous' || value === 'use-credentials'
|
|
38
|
+
? value
|
|
39
|
+
: undefined;
|
|
38
40
|
}
|
|
39
41
|
function isStyleProp(value) {
|
|
40
42
|
return typeof value === 'object' && value !== null;
|
|
@@ -94,10 +96,7 @@ const ImageBackgroundComponent = (_props, { attrs: rawAttrs, slots }) => {
|
|
|
94
96
|
});
|
|
95
97
|
// wrapper = symbiote-view > [imageDescriptor]; the slot children paint AFTER the image (on top).
|
|
96
98
|
const slotChildren = slots.default !== undefined ? slots.default() : [];
|
|
97
|
-
return h(wrapper.type, { ...wrapper.props, key: wrapper.key, class: attrs.class }, [
|
|
98
|
-
...wrapper.children.map(toChildVNode),
|
|
99
|
-
...slotChildren,
|
|
100
|
-
]);
|
|
99
|
+
return h(wrapper.type, { ...wrapper.props, key: wrapper.key, class: attrs.class }, [...wrapper.children.map(toChildVNode), ...slotChildren]);
|
|
101
100
|
};
|
|
102
101
|
ImageBackgroundComponent.displayName = 'ImageBackground';
|
|
103
102
|
ImageBackgroundComponent.inheritAttrs = false;
|
|
@@ -35,7 +35,9 @@ function asResizeMode(value) {
|
|
|
35
35
|
return isResizeMode(value) ? value : undefined;
|
|
36
36
|
}
|
|
37
37
|
function asCrossOrigin(value) {
|
|
38
|
-
return value === 'anonymous' || value === 'use-credentials'
|
|
38
|
+
return value === 'anonymous' || value === 'use-credentials'
|
|
39
|
+
? value
|
|
40
|
+
: undefined;
|
|
39
41
|
}
|
|
40
42
|
function isStyleProp(value) {
|
|
41
43
|
return typeof value === 'object' && value !== null;
|
|
@@ -1,20 +1,23 @@
|
|
|
1
1
|
// KeyboardAvoidingView: the Vue lifecycle half. The inset math + the behavior->style/structure
|
|
2
2
|
// decision live in @symbiote-native/components (render-keyboard-avoiding-view), shared verbatim
|
|
3
3
|
// with React; Vue supplies only the reactivity: a ref holds the inset, onMounted subscribes to
|
|
4
|
-
//
|
|
5
|
-
//
|
|
4
|
+
// this host's two keyboard notifications (keyboardAvoidingEventNamesFor — will* on iOS, did* on
|
|
5
|
+
// Android, and never changeFrame), onUnmounted tears the subscriptions down, and onLayout
|
|
6
|
+
// measures the wrapper frame that feeds the next event's inset.
|
|
6
7
|
//
|
|
7
8
|
// Inputs arrive as attrs (untyped), so each is narrowed with a runtime guard, never a cast.
|
|
8
9
|
// rawAttrs runs through normalizeVueAttrs so a template `:keyboard-vertical-offset` resolves.
|
|
9
|
-
import { defineComponent, h, ref, onMounted, onUnmounted } from '@vue/runtime-core';
|
|
10
|
-
import { Keyboard,
|
|
11
|
-
import { computeInset, readKeyboardFrame, readLayoutFrame, resolveKeyboardAvoidingLayout, resolveAccessibilityProps, DEFAULT_VERTICAL_OFFSET, } from '@symbiote-native/components';
|
|
10
|
+
import { defineComponent, h, ref, onMounted, onUnmounted, } from '@vue/runtime-core';
|
|
11
|
+
import { Keyboard, Platform, dlog, } from '@symbiote-native/engine';
|
|
12
|
+
import { computeInset, keyboardAvoidingEventNamesFor, readKeyboardFrame, readPrefersCrossFadeTransitions, readLayoutFrame, resolveKeyboardAvoidingLayout, resolveAccessibilityProps, DEFAULT_VERTICAL_OFFSET, } from '@symbiote-native/components';
|
|
12
13
|
import { normalizeVueAttrs } from '../utils/normalize-attrs.js';
|
|
13
14
|
function isStyleProp(value) {
|
|
14
15
|
return typeof value === 'object' && value !== null;
|
|
15
16
|
}
|
|
16
17
|
function asBehavior(value) {
|
|
17
|
-
return value === 'height' || value === 'position' || value === 'padding'
|
|
18
|
+
return value === 'height' || value === 'position' || value === 'padding'
|
|
19
|
+
? value
|
|
20
|
+
: undefined;
|
|
18
21
|
}
|
|
19
22
|
const HANDLED_ATTRS = [
|
|
20
23
|
'behavior',
|
|
@@ -38,12 +41,26 @@ export const KeyboardAvoidingView = defineComponent((_props, { attrs: rawAttrs,
|
|
|
38
41
|
// next keyboard event's inset math.
|
|
39
42
|
let frame;
|
|
40
43
|
let initialHeight;
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
+
// A device accessibility setting, read once per mount. Deliberately a plain variable, not a
|
|
45
|
+
// ref: it cannot change during a session, so nothing should re-render when it resolves.
|
|
46
|
+
let prefersCrossFadeTransitions = false;
|
|
47
|
+
// Read at EVENT time rather than captured once: attrs is live, and both the offset and the
|
|
48
|
+
// behavior feed every keyboard notification's inset math.
|
|
49
|
+
const currentAttrs = () => normalizeVueAttrs(rawAttrs);
|
|
50
|
+
const verticalOffset = () => {
|
|
51
|
+
const offset = currentAttrs().keyboardVerticalOffset;
|
|
52
|
+
return typeof offset === 'number' ? offset : DEFAULT_VERTICAL_OFFSET;
|
|
53
|
+
};
|
|
44
54
|
const onShow = (payload) => {
|
|
45
55
|
const keyboard = readKeyboardFrame(payload);
|
|
46
|
-
const next = computeInset(frame, keyboard, verticalOffset()
|
|
56
|
+
const next = computeInset(frame, keyboard, verticalOffset(), {
|
|
57
|
+
behavior: asBehavior(currentAttrs().behavior),
|
|
58
|
+
// The inset CURRENTLY applied (RN's this.state.bottom), read live off the reactive cell:
|
|
59
|
+
// in 'height' mode the wrapper is shrunk by it, so the next onLayout under-reports the
|
|
60
|
+
// frame by exactly that much and core adds it back.
|
|
61
|
+
previousInset: inset.value,
|
|
62
|
+
prefersCrossFadeTransitions,
|
|
63
|
+
});
|
|
47
64
|
dlog(`KeyboardAvoidingView show -> inset ${next}`);
|
|
48
65
|
inset.value = next;
|
|
49
66
|
};
|
|
@@ -53,11 +70,19 @@ export const KeyboardAvoidingView = defineComponent((_props, { attrs: rawAttrs,
|
|
|
53
70
|
};
|
|
54
71
|
let subscriptions = [];
|
|
55
72
|
onMounted(() => {
|
|
73
|
+
// TWO notifications, per host: iOS takes the will* pair so the view rides up with the
|
|
74
|
+
// keyboard animation, Android the did* pair. changeFrame is deliberately absent — with an
|
|
75
|
+
// undocked/split/floating iOS keyboard it fires BEFORE the hide, mid-dismissal.
|
|
76
|
+
const events = keyboardAvoidingEventNamesFor(Platform.OS);
|
|
56
77
|
subscriptions = [
|
|
57
|
-
Keyboard.addListener(
|
|
58
|
-
Keyboard.addListener(
|
|
59
|
-
Keyboard.addListener(KEYBOARD_EVENT.didHide, onHide),
|
|
78
|
+
Keyboard.addListener(events.show, onShow),
|
|
79
|
+
Keyboard.addListener(events.hide, onHide),
|
|
60
80
|
];
|
|
81
|
+
// Nobody awaits this, so it goes through the core wrapper, which answers false on a failed
|
|
82
|
+
// native read instead of leaving an unhandled rejection.
|
|
83
|
+
void readPrefersCrossFadeTransitions().then(enabled => {
|
|
84
|
+
prefersCrossFadeTransitions = enabled;
|
|
85
|
+
});
|
|
61
86
|
});
|
|
62
87
|
onUnmounted(() => {
|
|
63
88
|
for (const subscription of subscriptions)
|
|
@@ -74,7 +99,7 @@ export const KeyboardAvoidingView = defineComponent((_props, { attrs: rawAttrs,
|
|
|
74
99
|
emit('layout', event);
|
|
75
100
|
};
|
|
76
101
|
return () => {
|
|
77
|
-
const attrs =
|
|
102
|
+
const attrs = currentAttrs();
|
|
78
103
|
const behavior = asBehavior(attrs.behavior);
|
|
79
104
|
// RN gates every inset on `enabled ?? true`; only an explicit `false` disables.
|
|
80
105
|
const isEnabled = attrs.enabled !== false;
|
|
@@ -21,7 +21,9 @@ function asString(value) {
|
|
|
21
21
|
return typeof value === 'string' ? value : undefined;
|
|
22
22
|
}
|
|
23
23
|
function asAnimationType(value) {
|
|
24
|
-
return value === 'none' || value === 'slide' || value === 'fade'
|
|
24
|
+
return value === 'none' || value === 'slide' || value === 'fade'
|
|
25
|
+
? value
|
|
26
|
+
: undefined;
|
|
25
27
|
}
|
|
26
28
|
function asPresentationStyle(value) {
|
|
27
29
|
return value === 'fullScreen' ||
|
|
@@ -41,7 +43,8 @@ const ORIENTATIONS = [
|
|
|
41
43
|
function isOrientation(value) {
|
|
42
44
|
// .includes() types its arg as IModalOrientation, so a bare string won't pass; .some keeps the
|
|
43
45
|
// ORIENTATIONS list as the single source of truth without an `as` cast.
|
|
44
|
-
return typeof value === 'string' &&
|
|
46
|
+
return (typeof value === 'string' &&
|
|
47
|
+
ORIENTATIONS.some(orientation => orientation === value));
|
|
45
48
|
}
|
|
46
49
|
function asSupportedOrientations(value) {
|
|
47
50
|
if (!Array.isArray(value))
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { type EmitFn, type VNode } from '@vue/runtime-core';
|
|
2
2
|
import { type IPressState, type IRectOffset, type IPressableAndroidRippleConfig, type IAccessibilityProps, type IAriaProps } from '@symbiote-native/components';
|
|
3
3
|
import { type IClassNameValue, type ISymbioteEvent, type IStyleProp, type IViewStyle } from '@symbiote-native/engine';
|
|
4
|
-
export type { IPressState, IPressableAndroidRippleConfig } from '@symbiote-native/components';
|
|
4
|
+
export type { IPressState, IPressableAndroidRippleConfig, } from '@symbiote-native/components';
|
|
5
5
|
export type IPressableEmits = {
|
|
6
6
|
press: (event: ISymbioteEvent) => boolean;
|
|
7
7
|
pressIn: (event: ISymbioteEvent) => boolean;
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
// forwarded attrs (pure-JS callbacks); the machine's SYNTHESIZED handlers go on the View instead.
|
|
12
12
|
// Children arrive as a (scoped) default slot so `v-slot="{ pressed }"` mirrors React's
|
|
13
13
|
// children-as-function.
|
|
14
|
-
import { defineComponent, h, ref, shallowRef } from '@vue/runtime-core';
|
|
14
|
+
import { defineComponent, h, ref, shallowRef, } from '@vue/runtime-core';
|
|
15
15
|
import { createPressHandlers, createPressRuntime, rippleProps, buildPressableListeners, resolveDisabledAccessibilityState, noteHoverNoop, resolveAccessibilityProps, DEFAULT_DELAY_LONG_PRESS_MS, } from '@symbiote-native/components';
|
|
16
16
|
import { measure, isSymbioteNode, } from '@symbiote-native/engine';
|
|
17
17
|
import { View } from '../components.js';
|
|
@@ -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>;
|
|
@@ -23,7 +23,9 @@ function isRecord(value) {
|
|
|
23
23
|
function isHostType(type) {
|
|
24
24
|
if (typeof type === 'string')
|
|
25
25
|
return true;
|
|
26
|
-
return ((typeof type === 'object' || typeof type === 'function') &&
|
|
26
|
+
return ((typeof type === 'object' || typeof type === 'function') &&
|
|
27
|
+
type !== null &&
|
|
28
|
+
!isVNode(type));
|
|
27
29
|
}
|
|
28
30
|
export const ScrollView = createScrollView({
|
|
29
31
|
assemble: input => {
|
|
@@ -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
|
});
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
import { defineComponent, h, isVNode, markRaw, onBeforeUnmount, ref, shallowRef, watch, } from '@vue/runtime-core';
|
|
16
16
|
import { attachStickyScroll, buildScrollViewHandle, didContentSizeChange, forwardScrollEvent, readLayoutDimension, resolveAccessibilityProps, resolveDecelerationRate, resolveScrollForwarding, selectScrollIntrinsics, } from '@symbiote-native/components';
|
|
17
17
|
import { AnimatedValue, dlog, event as animatedEvent, isClassNameValue, isNativeAnimatedAvailable, isSymbioteNode, resolveClassName, } from '@symbiote-native/engine';
|
|
18
|
-
import { wrapStickyHeaders } from './sticky-header.js';
|
|
18
|
+
import { wrapStickyHeaders, } from './sticky-header.js';
|
|
19
19
|
import { normalizeVueAttrs } from '../../utils/normalize-attrs.js';
|
|
20
20
|
function isHandler(value) {
|
|
21
21
|
return typeof value === 'function';
|
|
@@ -37,10 +37,10 @@ function asDecelerationRate(value) {
|
|
|
37
37
|
return undefined;
|
|
38
38
|
}
|
|
39
39
|
function isNumberArray(value) {
|
|
40
|
-
return Array.isArray(value) && value.every(entry => typeof entry === 'number');
|
|
40
|
+
return (Array.isArray(value) && value.every(entry => typeof entry === 'number'));
|
|
41
41
|
}
|
|
42
42
|
function isComponent(value) {
|
|
43
|
-
return typeof value === 'function' || (typeof value === 'object' && value !== null);
|
|
43
|
+
return (typeof value === 'function' || (typeof value === 'object' && value !== null));
|
|
44
44
|
}
|
|
45
45
|
// Prop/handler keys the lifecycle consumes itself; everything else forwards onto the scroll-view
|
|
46
46
|
// node. onContentSizeChange is consumed (synthesized from the content onLayout); refreshControl
|
|
@@ -126,8 +126,13 @@ export function createScrollView(platform) {
|
|
|
126
126
|
const attrs = normalizeVueAttrs(rawAttrs);
|
|
127
127
|
const isHorizontal = attrs.horizontal === true;
|
|
128
128
|
const userStyle = isStyleProp(attrs.style) ? attrs.style : undefined;
|
|
129
|
-
const classProp = isClassNameProp(attrs.class)
|
|
130
|
-
|
|
129
|
+
const classProp = isClassNameProp(attrs.class)
|
|
130
|
+
? attrs.class
|
|
131
|
+
: undefined;
|
|
132
|
+
const layoutSplitStyle = [
|
|
133
|
+
resolveClassName(classProp),
|
|
134
|
+
userStyle,
|
|
135
|
+
];
|
|
131
136
|
const contentContainerStyle = typeof attrs.contentContainerStyle === 'string'
|
|
132
137
|
? resolveClassName(attrs.contentContainerStyle)
|
|
133
138
|
: isStyleProp(attrs.contentContainerStyle)
|
|
@@ -141,14 +146,16 @@ export function createScrollView(platform) {
|
|
|
141
146
|
const stickyHeaderComponent = isComponent(attrs.StickyHeaderComponent)
|
|
142
147
|
? attrs.StickyHeaderComponent
|
|
143
148
|
: undefined;
|
|
144
|
-
const { scrollViewIntrinsic, contentIntrinsic, scrollViewBaseStyle, contentStyle } = selectScrollIntrinsics(isHorizontal, contentContainerStyle);
|
|
149
|
+
const { scrollViewIntrinsic, contentIntrinsic, scrollViewBaseStyle, contentStyle, } = selectScrollIntrinsics(isHorizontal, contentContainerStyle);
|
|
145
150
|
// RN defaults nested scrolling ON (ScrollView.js `nestedScrollEnabled ?? true`);
|
|
146
151
|
// horizontal forwards only when defined (load-bearing on iOS's RCTScrollView axis).
|
|
147
152
|
const outerProps = {
|
|
148
153
|
...resolveAccessibilityProps(forwardAttrs(attrs)),
|
|
149
154
|
};
|
|
150
155
|
outerProps.nestedScrollEnabled =
|
|
151
|
-
typeof attrs.nestedScrollEnabled === 'boolean'
|
|
156
|
+
typeof attrs.nestedScrollEnabled === 'boolean'
|
|
157
|
+
? attrs.nestedScrollEnabled
|
|
158
|
+
: true;
|
|
152
159
|
if (attrs.horizontal !== undefined)
|
|
153
160
|
outerProps.horizontal = attrs.horizontal;
|
|
154
161
|
const decel = asDecelerationRate(attrs.decelerationRate);
|
|
@@ -160,7 +167,9 @@ export function createScrollView(platform) {
|
|
|
160
167
|
// decisions (which path, throttle default, inverted capture).
|
|
161
168
|
const nativeStickyAvailable = hasStickyHeaders && isNativeAnimatedAvailable();
|
|
162
169
|
nativeStickyWanted = nativeStickyAvailable;
|
|
163
|
-
const userThrottle = typeof attrs.scrollEventThrottle === 'number'
|
|
170
|
+
const userThrottle = typeof attrs.scrollEventThrottle === 'number'
|
|
171
|
+
? attrs.scrollEventThrottle
|
|
172
|
+
: undefined;
|
|
164
173
|
const forwarding = resolveScrollForwarding({
|
|
165
174
|
hasStickyHeaders,
|
|
166
175
|
nativeStickyAvailable,
|
|
@@ -170,13 +179,17 @@ export function createScrollView(platform) {
|
|
|
170
179
|
snapToAlignment: attrs.snapToAlignment,
|
|
171
180
|
});
|
|
172
181
|
if (hasStickyHeaders) {
|
|
173
|
-
const userOnScroll = isHandler(attrs.onScroll)
|
|
182
|
+
const userOnScroll = isHandler(attrs.onScroll)
|
|
183
|
+
? attrs.onScroll
|
|
184
|
+
: undefined;
|
|
174
185
|
if (forwarding.mode === 'sticky-js') {
|
|
175
186
|
// JS fallback (no native module): correct, but lags a frame under fast scroll, which
|
|
176
187
|
// the native path removes on a real host.
|
|
177
188
|
outerProps.onScroll = animatedEvent([{ nativeEvent: { contentOffset: { y: scrollAnimatedValue } } }], userOnScroll === undefined
|
|
178
189
|
? undefined
|
|
179
|
-
: {
|
|
190
|
+
: {
|
|
191
|
+
listener: (...args) => forwardScrollEvent(userOnScroll, args),
|
|
192
|
+
});
|
|
180
193
|
}
|
|
181
194
|
// Native path: the value is driven on the UI thread by the post-commit watch above, so
|
|
182
195
|
// onScroll forwards untouched.
|
|
@@ -186,7 +199,9 @@ export function createScrollView(platform) {
|
|
|
186
199
|
// Capture the viewport height for inverted sticky headers (RN _handleLayout), then call
|
|
187
200
|
// the user's handler.
|
|
188
201
|
if (forwarding.capturesViewportHeight) {
|
|
189
|
-
const userOnLayout = isHandler(attrs.onLayout)
|
|
202
|
+
const userOnLayout = isHandler(attrs.onLayout)
|
|
203
|
+
? attrs.onLayout
|
|
204
|
+
: undefined;
|
|
190
205
|
outerProps.onLayout = (event) => {
|
|
191
206
|
const height = readLayoutDimension(event, 'height');
|
|
192
207
|
if (height !== undefined)
|
|
@@ -200,7 +215,10 @@ export function createScrollView(platform) {
|
|
|
200
215
|
// `collapsable: false` keeps the layout-only content view as a real native view: Android
|
|
201
216
|
// Fabric view-flattens it away otherwise, hoisting the cells as direct children of the
|
|
202
217
|
// scroll view (which hosts exactly one) - an addViewAt crash. iOS never flattens.
|
|
203
|
-
const contentProps = {
|
|
218
|
+
const contentProps = {
|
|
219
|
+
style: contentStyle,
|
|
220
|
+
collapsable: false,
|
|
221
|
+
};
|
|
204
222
|
if (forwarding.collapsableChildren) {
|
|
205
223
|
contentProps.collapsableChildren = false;
|
|
206
224
|
}
|
|
@@ -228,7 +246,9 @@ export function createScrollView(platform) {
|
|
|
228
246
|
style: [scrollViewBaseStyle, userStyle],
|
|
229
247
|
ref: setNodeRef,
|
|
230
248
|
};
|
|
231
|
-
const refreshControl = isVNode(attrs.refreshControl)
|
|
249
|
+
const refreshControl = isVNode(attrs.refreshControl)
|
|
250
|
+
? attrs.refreshControl
|
|
251
|
+
: undefined;
|
|
232
252
|
return platform.assemble({
|
|
233
253
|
scrollViewIntrinsic,
|
|
234
254
|
scrollProps,
|
|
@@ -52,7 +52,10 @@ export const ScrollViewStickyHeader = defineComponent({
|
|
|
52
52
|
const version = ref(0);
|
|
53
53
|
// Engine node -> shallowRef (identity rule); the un-measured identity stub until the
|
|
54
54
|
// rebuild-interpolation effect below replaces it.
|
|
55
|
-
const animatedTranslateY = shallowRef(scrollAnimatedValue.interpolate({
|
|
55
|
+
const animatedTranslateY = shallowRef(scrollAnimatedValue.interpolate({
|
|
56
|
+
inputRange: [-1, 0],
|
|
57
|
+
outputRange: [0, 0],
|
|
58
|
+
}));
|
|
56
59
|
let debounceTimer;
|
|
57
60
|
// Held so the next rebuild detaches the old listener and onBeforeUnmount cleans up.
|
|
58
61
|
let interpolation;
|
|
@@ -60,8 +63,12 @@ export const ScrollViewStickyHeader = defineComponent({
|
|
|
60
63
|
const inputs = () => ({
|
|
61
64
|
os: Platform.OS,
|
|
62
65
|
inverted: typeof attrs.inverted === 'boolean' ? attrs.inverted : undefined,
|
|
63
|
-
scrollViewHeight: typeof attrs.scrollViewHeight === 'number'
|
|
64
|
-
|
|
66
|
+
scrollViewHeight: typeof attrs.scrollViewHeight === 'number'
|
|
67
|
+
? attrs.scrollViewHeight
|
|
68
|
+
: undefined,
|
|
69
|
+
nextHeaderLayoutY: typeof attrs.nextHeaderLayoutY === 'number'
|
|
70
|
+
? attrs.nextHeaderLayoutY
|
|
71
|
+
: undefined,
|
|
65
72
|
});
|
|
66
73
|
const runEffects = (effects) => {
|
|
67
74
|
for (const effect of effects) {
|
|
@@ -124,13 +131,19 @@ export const ScrollViewStickyHeader = defineComponent({
|
|
|
124
131
|
const y = readLayoutNumber(event, 'y');
|
|
125
132
|
const height = readLayoutNumber(event, 'height');
|
|
126
133
|
// Keep the previous value when a field is absent (RN sets state only on a defined read).
|
|
127
|
-
dispatch({
|
|
134
|
+
dispatch({
|
|
135
|
+
kind: 'layout',
|
|
136
|
+
y: y ?? state.layoutY,
|
|
137
|
+
height: height ?? state.layoutHeight,
|
|
138
|
+
});
|
|
128
139
|
const recorder = attrs.onLayout;
|
|
129
140
|
if (isHandler(recorder))
|
|
130
141
|
recorder(event);
|
|
131
142
|
const children = slots.default !== undefined ? slots.default() : [];
|
|
132
143
|
const child = children[0];
|
|
133
|
-
const childOnLayout = isVNode(child)
|
|
144
|
+
const childOnLayout = isVNode(child)
|
|
145
|
+
? readChildOnLayout(child)
|
|
146
|
+
: undefined;
|
|
134
147
|
if (childOnLayout !== undefined)
|
|
135
148
|
childOnLayout(event);
|
|
136
149
|
};
|
|
@@ -34,7 +34,7 @@ function buildDelegate(getInner) {
|
|
|
34
34
|
}
|
|
35
35
|
export const SectionList = defineComponent(
|
|
36
36
|
// _props is read only by the type system, to let ItemT infer at the call site.
|
|
37
|
-
(_props, { attrs, expose, emit, slots }) => {
|
|
37
|
+
(_props, { attrs, expose, emit, slots, }) => {
|
|
38
38
|
const inner = shallowRef(null);
|
|
39
39
|
const setInner = (instance) => {
|
|
40
40
|
inner.value = isSectionHandle(instance) ? instance : null;
|
|
@@ -54,7 +54,9 @@ export const SectionList = defineComponent(
|
|
|
54
54
|
const startReached = listens('onStartReached')
|
|
55
55
|
? (info) => emit('startReached', info)
|
|
56
56
|
: undefined;
|
|
57
|
-
const refresh = listens('onRefresh')
|
|
57
|
+
const refresh = listens('onRefresh')
|
|
58
|
+
? () => emit('refresh')
|
|
59
|
+
: undefined;
|
|
58
60
|
return h(VirtualizedSectionListHost, {
|
|
59
61
|
...normalizeVueAttrs(attrs),
|
|
60
62
|
ref: setInner,
|
|
@@ -73,7 +73,10 @@ export function createSwitch(platform) {
|
|
|
73
73
|
return;
|
|
74
74
|
emit('valueChange', next, event);
|
|
75
75
|
emitModelUpdate(emit, next);
|
|
76
|
-
state.value = switchReducer(state.value, {
|
|
76
|
+
state.value = switchReducer(state.value, {
|
|
77
|
+
type: 'native-reported',
|
|
78
|
+
value: next,
|
|
79
|
+
});
|
|
77
80
|
};
|
|
78
81
|
// Snap-back: when native reported a value the parent rejected, command the JS value back
|
|
79
82
|
// down (the controlled-Switch correction RN does via SwitchCommands.setValue). The
|
|
@@ -97,12 +100,18 @@ export function createSwitch(platform) {
|
|
|
97
100
|
const attrs = normalizeVueAttrs(rawAttrs);
|
|
98
101
|
return descriptorToVue(renderSwitch({
|
|
99
102
|
value: resolveModelValue(attrs, isBoolean) === true,
|
|
100
|
-
disabled: typeof attrs.disabled === 'boolean'
|
|
103
|
+
disabled: typeof attrs.disabled === 'boolean'
|
|
104
|
+
? attrs.disabled
|
|
105
|
+
: undefined,
|
|
101
106
|
trackColor: normalizeTrackColor(attrs.trackColor),
|
|
102
107
|
thumbColor: asString(attrs.thumbColor),
|
|
103
108
|
ios_backgroundColor: asString(attrs.ios_backgroundColor),
|
|
104
109
|
style: isViewStyleObject(attrs.style) ? attrs.style : undefined,
|
|
105
|
-
passthrough: {
|
|
110
|
+
passthrough: {
|
|
111
|
+
...forwardAttrs(attrs),
|
|
112
|
+
ref: setNodeRef,
|
|
113
|
+
onChange: handleChange,
|
|
114
|
+
},
|
|
106
115
|
}, platform));
|
|
107
116
|
};
|
|
108
117
|
}, {
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
// Inputs arrive as attrs (untyped), so each is narrowed with a runtime guard, never a cast.
|
|
9
9
|
// onValueChange MUST be stripped from the forwarded attrs: it is not a ViewConfig event, so
|
|
10
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';
|
|
11
|
+
import { defineComponent, onBeforeUnmount, ref, shallowRef, watch, } from '@vue/runtime-core';
|
|
12
12
|
import { resolveAccessibilityProps, resolveTextInputProps, renderTextInput, foldText, textFromChange, eventCountFromChange, shouldCommandText, INITIAL_EVENT_COUNT, SELECTION_NONE, } from '@symbiote-native/components';
|
|
13
13
|
import { dispatchViewCommand, isSymbioteNode, dlog, blurTextInput, setInputFocused, setInputBlurred, whenCommitted, } from '@symbiote-native/engine';
|
|
14
14
|
import { descriptorToVue } from '../../descriptor-to-vue.js';
|
|
@@ -170,7 +170,12 @@ export const TextInput = defineComponent((_props, { attrs: rawAttrs, emit, expos
|
|
|
170
170
|
const node = nodeRef.value;
|
|
171
171
|
if (node === null)
|
|
172
172
|
return;
|
|
173
|
-
dispatchViewCommand(node, 'setTextAndSelection', [
|
|
173
|
+
dispatchViewCommand(node, 'setTextAndSelection', [
|
|
174
|
+
mostRecentEventCount.value,
|
|
175
|
+
'',
|
|
176
|
+
0,
|
|
177
|
+
0,
|
|
178
|
+
]);
|
|
174
179
|
lastNativeText = '';
|
|
175
180
|
},
|
|
176
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;
|
|
@@ -12,6 +12,10 @@ export interface ITouchableHighlightProps extends ITouchableBaseProps {
|
|
|
12
12
|
underlayColor?: string;
|
|
13
13
|
}
|
|
14
14
|
export type ITouchableWithoutFeedbackProps = ITouchableBaseProps;
|
|
15
|
+
export type ITouchableHighlightEmits = IPressableEmits & {
|
|
16
|
+
showUnderlay: () => boolean;
|
|
17
|
+
hideUnderlay: () => boolean;
|
|
18
|
+
};
|
|
15
19
|
export declare const TouchableOpacity: import("vue").DefineSetupFnComponent<ITouchableOpacityProps, IPressableEmits, {}, ITouchableOpacityProps & {
|
|
16
20
|
onPress?: ((event: ISymbioteEvent) => any) | undefined;
|
|
17
21
|
onPressIn?: ((event: ISymbioteEvent) => any) | undefined;
|
|
@@ -21,7 +25,7 @@ export declare const TouchableOpacity: import("vue").DefineSetupFnComponent<ITou
|
|
|
21
25
|
onHoverIn?: ((event: ISymbioteEvent) => any) | undefined;
|
|
22
26
|
onHoverOut?: ((event: ISymbioteEvent) => any) | undefined;
|
|
23
27
|
}, import("vue").PublicProps>;
|
|
24
|
-
export declare const TouchableHighlight: import("vue").DefineSetupFnComponent<ITouchableHighlightProps,
|
|
28
|
+
export declare const TouchableHighlight: import("vue").DefineSetupFnComponent<ITouchableHighlightProps, ITouchableHighlightEmits, {}, ITouchableHighlightProps & {
|
|
25
29
|
onPress?: ((event: ISymbioteEvent) => any) | undefined;
|
|
26
30
|
onPressIn?: ((event: ISymbioteEvent) => any) | undefined;
|
|
27
31
|
onPressOut?: ((event: ISymbioteEvent) => any) | undefined;
|
|
@@ -29,6 +33,8 @@ export declare const TouchableHighlight: import("vue").DefineSetupFnComponent<IT
|
|
|
29
33
|
onLongPress?: ((event: ISymbioteEvent) => any) | undefined;
|
|
30
34
|
onHoverIn?: ((event: ISymbioteEvent) => any) | undefined;
|
|
31
35
|
onHoverOut?: ((event: ISymbioteEvent) => any) | undefined;
|
|
36
|
+
onShowUnderlay?: (() => any) | undefined;
|
|
37
|
+
onHideUnderlay?: (() => any) | undefined;
|
|
32
38
|
}, import("vue").PublicProps>;
|
|
33
39
|
export declare const TouchableWithoutFeedback: import("vue").DefineSetupFnComponent<ITouchableBaseProps, IPressableEmits, {}, Omit<IPressableProps, "style"> & IPressTimingProps & {
|
|
34
40
|
style?: IStyleProp<IViewStyle>;
|