@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
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 = {}) {
|
|
@@ -1,30 +1,25 @@
|
|
|
1
|
-
// FlatList, the Vue convenience surface over VirtualizedList.
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
1
|
+
// FlatList, the Vue convenience surface over VirtualizedList. Takes a plain `data` array and
|
|
2
|
+
// derives getItem/getItemCount; numColumns packs items into rows (a horizontal sub-View), so the
|
|
3
|
+
// virtualized stream is rows, not items. All windowing/viewability/batching/imperative scrolling
|
|
4
|
+
// are inherited from VirtualizedList; the data shaping and row/viewability/separator transforms
|
|
5
|
+
// are shared from @symbiote-native/components.
|
|
6
6
|
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
// (horizontal/inverted/style/getItemLayout/raw scroll/…) still rides through `$attrs` onto the inner
|
|
13
|
-
// VirtualizedList. The inner list still GATES RefreshControl + viewability on callback presence, so
|
|
14
|
-
// each emit bridge is wired to it ONLY when the consumer actually listens (read off the instance
|
|
15
|
-
// vnode props), keeping behavior identical to the prop-callback era.
|
|
7
|
+
// Typed-emits generic component: a GENERIC setup function so events emit with ItemT-typed
|
|
8
|
+
// payloads. For that to infer at the call site, the generic INPUTS must be typed `props`, not
|
|
9
|
+
// $attrs - so they are declared in the runtime `props` array; the passthrough tail rides through
|
|
10
|
+
// $attrs onto the inner VirtualizedList. The inner list still GATES RefreshControl + viewability
|
|
11
|
+
// on callback presence, so each emit bridge is wired ONLY when the consumer actually listens.
|
|
16
12
|
import { defineComponent, getCurrentInstance, h, shallowRef, } from '@vue/runtime-core';
|
|
17
13
|
import { SINGLE_COLUMN, chunkIntoRows, expandRowViewability, firstItemOfRow, lastItemOfRow, rowKeyExtractor, } from '@symbiote-native/components';
|
|
18
14
|
import { dlog, resolveClassName, } from '@symbiote-native/engine';
|
|
19
15
|
import { VirtualizedList } from '../virtualized-list/index.js';
|
|
20
16
|
import { normalizeVueAttrs } from '../../utils/normalize-attrs.js';
|
|
21
|
-
// VirtualizedList
|
|
22
|
-
//
|
|
23
|
-
//
|
|
17
|
+
// VirtualizedList's generic construct signature can't be resolved by h()'s overloads, so drive
|
|
18
|
+
// it through a loose functional-component handle (the ItemT surface is proven at the typed
|
|
19
|
+
// FlatList boundary above).
|
|
24
20
|
const VirtualizedListHost = VirtualizedList;
|
|
25
|
-
//
|
|
26
|
-
//
|
|
27
|
-
// signature widens keyof to `string`).
|
|
21
|
+
// Listed for the runtime `props` declaration (keyof can't derive it: the index signature widens
|
|
22
|
+
// keyof to `string`).
|
|
28
23
|
const PROP_KEYS = [
|
|
29
24
|
'data',
|
|
30
25
|
'keyExtractor',
|
|
@@ -61,41 +56,42 @@ function buildDelegateHandle(getInner) {
|
|
|
61
56
|
recordInteraction: () => getInner()?.recordInteraction(),
|
|
62
57
|
};
|
|
63
58
|
}
|
|
64
|
-
export const FlatList = defineComponent((props, { attrs, expose, emit, slots }) => {
|
|
59
|
+
export const FlatList = defineComponent((props, { attrs, expose, emit, slots, }) => {
|
|
65
60
|
const inner = shallowRef(null);
|
|
66
61
|
const setInner = (instance) => {
|
|
67
62
|
inner.value = isVirtualizedListHandle(instance) ? instance : null;
|
|
68
63
|
};
|
|
69
64
|
expose(buildDelegateHandle(() => inner.value));
|
|
70
|
-
//
|
|
71
|
-
//
|
|
72
|
-
// listener off the instance's own vnode props (what the parent passed) instead.
|
|
65
|
+
// Declared emits are stripped from $attrs, so read the listener off the instance's own vnode
|
|
66
|
+
// props instead, gating each bridge to when the consumer actually listens.
|
|
73
67
|
const instance = getCurrentInstance();
|
|
74
68
|
const listens = (onName) => {
|
|
75
69
|
const vnodeProps = instance?.vnode.props;
|
|
76
70
|
return vnodeProps != null && typeof vnodeProps[onName] === 'function';
|
|
77
71
|
};
|
|
78
72
|
return () => {
|
|
79
|
-
const data = Array.isArray(props.data)
|
|
73
|
+
const data = Array.isArray(props.data)
|
|
74
|
+
? props.data
|
|
75
|
+
: [];
|
|
80
76
|
const keyExtractor = props.keyExtractor;
|
|
81
77
|
const numColumns = typeof props.numColumns === 'number' ? props.numColumns : SINGLE_COLUMN;
|
|
82
78
|
const viewabilityPairs = props.viewabilityConfigCallbackPairs;
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
79
|
+
const chromeSlots = {
|
|
80
|
+
header: slots.header,
|
|
81
|
+
footer: slots.footer,
|
|
82
|
+
empty: slots.empty,
|
|
83
|
+
};
|
|
88
84
|
const forwarded = normalizeVueAttrs(attrs);
|
|
89
85
|
dlog(`Vue FlatList over ${data.length} items, ${numColumns} column(s)`);
|
|
90
|
-
// The synthesized list events become VL handlers ONLY when listened, so the inner list keeps
|
|
91
|
-
// gating (no parasitic RefreshControl / viewability work for an unlistened event).
|
|
92
86
|
const endReached = listens('onEndReached')
|
|
93
87
|
? (info) => emit('endReached', info)
|
|
94
88
|
: undefined;
|
|
95
89
|
const startReached = listens('onStartReached')
|
|
96
90
|
? (info) => emit('startReached', info)
|
|
97
91
|
: undefined;
|
|
98
|
-
const refresh = listens('onRefresh')
|
|
92
|
+
const refresh = listens('onRefresh')
|
|
93
|
+
? () => emit('refresh')
|
|
94
|
+
: undefined;
|
|
99
95
|
const scrollToIndexFailed = listens('onScrollToIndexFailed')
|
|
100
96
|
? (info) => emit('scrollToIndexFailed', info)
|
|
101
97
|
: undefined;
|
|
@@ -117,20 +113,17 @@ export const FlatList = defineComponent((props, { attrs, expose, emit, slots })
|
|
|
117
113
|
onScrollToIndexFailed: scrollToIndexFailed,
|
|
118
114
|
onViewableItemsChanged,
|
|
119
115
|
viewabilityConfigCallbackPairs: viewabilityPairs,
|
|
120
|
-
},
|
|
121
|
-
// A single column forwards the consumer's slots untouched (#item info shape is identical).
|
|
122
|
-
{ ...chromeSlots, item: slots.item, separator: slots.separator });
|
|
116
|
+
}, { ...chromeSlots, item: slots.item, separator: slots.separator });
|
|
123
117
|
}
|
|
124
118
|
// Multi-column: the virtualized stream is rows. Each cell renders its items side by side
|
|
125
119
|
// in a flex-row View so windowing accounts for whole rows.
|
|
126
120
|
const rows = chunkIntoRows(data, numColumns);
|
|
127
|
-
// A class-name string resolves through the shared style registry; an object/array is
|
|
128
|
-
// already style-shaped and passes through as-is.
|
|
129
121
|
const rowStyle = [
|
|
130
122
|
{ flexDirection: 'row' },
|
|
131
123
|
typeof props.columnWrapperStyle === 'string'
|
|
132
124
|
? resolveClassName(props.columnWrapperStyle)
|
|
133
|
-
: isRecord(props.columnWrapperStyle) ||
|
|
125
|
+
: isRecord(props.columnWrapperStyle) ||
|
|
126
|
+
Array.isArray(props.columnWrapperStyle)
|
|
134
127
|
? props.columnWrapperStyle
|
|
135
128
|
: undefined,
|
|
136
129
|
];
|
|
@@ -1,12 +1,10 @@
|
|
|
1
1
|
// ImageBackground: the Vue lifecycle half. The composition (the absolute-fill Image behind the
|
|
2
2
|
// children, the dimension-proxy + style-merge math) lives framework-agnostic in
|
|
3
|
-
// @symbiote-native/components/renderImageBackground
|
|
4
|
-
//
|
|
5
|
-
//
|
|
3
|
+
// @symbiote-native/components/renderImageBackground, shared verbatim with React; Vue narrows the
|
|
4
|
+
// untyped attrs into the typed Image view, folds aria/role, bridges the Descriptor to vnodes, and
|
|
5
|
+
// appends the slot children ON TOP of the inner image.
|
|
6
6
|
//
|
|
7
|
-
// FUNCTIONAL, not a stateful defineComponent:
|
|
8
|
-
// arrive as attrs (untyped); the typed transform fields are narrowed with runtime guards, the
|
|
9
|
-
// forward-only rest is folded so resolveAccessibilityProps lands aria-* onto the inner image.
|
|
7
|
+
// FUNCTIONAL, not a stateful defineComponent: render-only, no state.
|
|
10
8
|
import { h } from '@vue/runtime-core';
|
|
11
9
|
import { renderImageBackground, resolveAccessibilityProps, } from '@symbiote-native/components';
|
|
12
10
|
import { resolveClassName, } from '@symbiote-native/engine';
|
|
@@ -18,8 +16,6 @@ function asString(value) {
|
|
|
18
16
|
function asNumber(value) {
|
|
19
17
|
return typeof value === 'number' ? value : undefined;
|
|
20
18
|
}
|
|
21
|
-
// A source is a structured object/array or an opaque require() id (number) the engine's injected
|
|
22
|
-
// resolver expands; any object/array/number is a valid source to forward.
|
|
23
19
|
function asSource(value) {
|
|
24
20
|
if (typeof value === 'number')
|
|
25
21
|
return value;
|
|
@@ -38,18 +34,16 @@ function asResizeMode(value) {
|
|
|
38
34
|
return isResizeMode(value) ? value : undefined;
|
|
39
35
|
}
|
|
40
36
|
function asCrossOrigin(value) {
|
|
41
|
-
return value === 'anonymous' || value === 'use-credentials'
|
|
37
|
+
return value === 'anonymous' || value === 'use-credentials'
|
|
38
|
+
? value
|
|
39
|
+
: undefined;
|
|
42
40
|
}
|
|
43
|
-
// Object OR array (a style list) passes through: the engine flattens either; primitives degrade
|
|
44
|
-
// to undefined (parity with React, which preserves the StyleProp).
|
|
45
41
|
function isStyleProp(value) {
|
|
46
42
|
return typeof value === 'object' && value !== null;
|
|
47
43
|
}
|
|
48
44
|
function toChildVNode(child) {
|
|
49
45
|
return typeof child === 'string' ? child : descriptorToVue(child);
|
|
50
46
|
}
|
|
51
|
-
// `style` is the WRAPPER View style; `imageStyle` targets the inner Image. The image transform
|
|
52
|
-
// fields are consumed by the source/style fold; everything else forwards onto the inner image.
|
|
53
47
|
const HANDLED_ATTRS = [
|
|
54
48
|
'style',
|
|
55
49
|
'imageStyle',
|
|
@@ -79,8 +73,6 @@ const ImageBackgroundComponent = (_props, { attrs: rawAttrs, slots }) => {
|
|
|
79
73
|
const attrs = normalizeVueAttrs(rawAttrs);
|
|
80
74
|
const wrapper = renderImageBackground({
|
|
81
75
|
style: isStyleProp(attrs.style) ? attrs.style : undefined,
|
|
82
|
-
// A class-name string resolves through the shared style registry, same as `class` above;
|
|
83
|
-
// an object/array is already style-shaped and passes through as-is.
|
|
84
76
|
imageStyle: typeof attrs.imageStyle === 'string'
|
|
85
77
|
? resolveClassName(attrs.imageStyle)
|
|
86
78
|
: isStyleProp(attrs.imageStyle)
|
|
@@ -104,10 +96,7 @@ const ImageBackgroundComponent = (_props, { attrs: rawAttrs, slots }) => {
|
|
|
104
96
|
});
|
|
105
97
|
// wrapper = symbiote-view > [imageDescriptor]; the slot children paint AFTER the image (on top).
|
|
106
98
|
const slotChildren = slots.default !== undefined ? slots.default() : [];
|
|
107
|
-
return h(wrapper.type, { ...wrapper.props, key: wrapper.key, class: attrs.class }, [
|
|
108
|
-
...wrapper.children.map(toChildVNode),
|
|
109
|
-
...slotChildren,
|
|
110
|
-
]);
|
|
99
|
+
return h(wrapper.type, { ...wrapper.props, key: wrapper.key, class: attrs.class }, [...wrapper.children.map(toChildVNode), ...slotChildren]);
|
|
111
100
|
};
|
|
112
101
|
ImageBackgroundComponent.displayName = 'ImageBackground';
|
|
113
102
|
ImageBackgroundComponent.inheritAttrs = false;
|
|
@@ -1,17 +1,12 @@
|
|
|
1
|
-
// Image: the Vue lifecycle half. The full fold (source
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
1
|
+
// Image: the Vue lifecycle half. The full fold (source/src/srcSet resolution, width/height ->
|
|
2
|
+
// style fold, resizeMode/tintColor, alt -> accessibility, and the native source array) lives
|
|
3
|
+
// framework-agnostic in @symbiote-native/components, shared verbatim with React; Vue narrows the
|
|
4
|
+
// untyped attrs into renderImage's typed view, folds aria/role, bridges the Descriptor to a
|
|
5
|
+
// vnode, and carries the Image statics (getSize/prefetch/queryCache/...).
|
|
6
6
|
//
|
|
7
|
-
// FUNCTIONAL, not a stateful defineComponent:
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
// resolves to a useless component proxy; see components.ts). So Image must stay functional.
|
|
11
|
-
//
|
|
12
|
-
// Inputs arrive as attrs (untyped). The typed transform fields are narrowed with runtime guards;
|
|
13
|
-
// the forward-only rest (events, blurRadius, capInsets, testID, accessibility*) is typed as the
|
|
14
|
-
// a11y intersection so resolveAccessibilityProps folds aria-* into accessibility* over it.
|
|
7
|
+
// FUNCTIONAL, not a stateful defineComponent: Animated.Image wraps it via createAnimatedComponent,
|
|
8
|
+
// which captures the host node through a ref that only falls through on a functional component
|
|
9
|
+
// (a defineComponent's ref resolves to a useless component proxy; see components.ts).
|
|
15
10
|
import { imageStatics, renderImage, resolveAccessibilityProps, } from '@symbiote-native/components';
|
|
16
11
|
import { descriptorToVue } from '../descriptor-to-vue.js';
|
|
17
12
|
import { normalizeVueAttrs } from '../utils/normalize-attrs.js';
|
|
@@ -22,8 +17,6 @@ function asString(value) {
|
|
|
22
17
|
function asNumber(value) {
|
|
23
18
|
return typeof value === 'number' ? value : undefined;
|
|
24
19
|
}
|
|
25
|
-
// A source is a structured object/array or an opaque require() id (number) the engine's injected
|
|
26
|
-
// resolver expands; any object/array/number is a valid source to forward (IImageSource is all-optional).
|
|
27
20
|
function asSource(value) {
|
|
28
21
|
if (typeof value === 'number')
|
|
29
22
|
return value;
|
|
@@ -42,14 +35,13 @@ function asResizeMode(value) {
|
|
|
42
35
|
return isResizeMode(value) ? value : undefined;
|
|
43
36
|
}
|
|
44
37
|
function asCrossOrigin(value) {
|
|
45
|
-
return value === 'anonymous' || value === 'use-credentials'
|
|
38
|
+
return value === 'anonymous' || value === 'use-credentials'
|
|
39
|
+
? value
|
|
40
|
+
: undefined;
|
|
46
41
|
}
|
|
47
|
-
// Object OR array (a style list) passes through; readStyleString flattens either, and arrays must
|
|
48
|
-
// survive so `style={[a, b]}` reaches Fabric (parity with React, which preserves the StyleProp).
|
|
49
42
|
function isStyleProp(value) {
|
|
50
43
|
return typeof value === 'object' && value !== null;
|
|
51
44
|
}
|
|
52
|
-
// The typed transform fields renderImage folds; everything else forwards via passthrough.
|
|
53
45
|
const HANDLED_ATTRS = [
|
|
54
46
|
'source',
|
|
55
47
|
'defaultSource',
|
|
@@ -94,6 +86,5 @@ const ImageComponent = (_props, { attrs: rawAttrs }) => {
|
|
|
94
86
|
};
|
|
95
87
|
ImageComponent.displayName = 'Image';
|
|
96
88
|
ImageComponent.inheritAttrs = false;
|
|
97
|
-
// Statics attached like RN (Image.getSize
|
|
98
|
-
// the engine-resolved imageStatics. The component value doubles as the statics namespace.
|
|
89
|
+
// Statics attached like RN (Image.getSize/prefetch/...), shared verbatim with React.
|
|
99
90
|
export const Image = Object.assign(ImageComponent, imageStatics);
|
|
@@ -1,28 +1,24 @@
|
|
|
1
|
-
// KeyboardAvoidingView: the Vue lifecycle half. The inset math + the behavior
|
|
2
|
-
// decision live in @symbiote-native/components (render-keyboard-avoiding-view), shared verbatim
|
|
3
|
-
// React
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
// 'padding', enabled, keyboardVerticalOffset, contentContainerStyle, onLayout.
|
|
1
|
+
// KeyboardAvoidingView: the Vue lifecycle half. The inset math + the behavior->style/structure
|
|
2
|
+
// decision live in @symbiote-native/components (render-keyboard-avoiding-view), shared verbatim
|
|
3
|
+
// with React; Vue supplies only the reactivity: a ref holds the inset, onMounted subscribes to
|
|
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.
|
|
8
7
|
//
|
|
9
|
-
// Inputs arrive as attrs (untyped), so each is narrowed with a runtime guard
|
|
10
|
-
// rawAttrs runs through normalizeVueAttrs
|
|
11
|
-
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import { computeInset, readKeyboardFrame, readLayoutFrame, resolveKeyboardAvoidingLayout, resolveAccessibilityProps, DEFAULT_VERTICAL_OFFSET, } from '@symbiote-native/components';
|
|
8
|
+
// Inputs arrive as attrs (untyped), so each is narrowed with a runtime guard, never a cast.
|
|
9
|
+
// rawAttrs runs through normalizeVueAttrs so a template `:keyboard-vertical-offset` resolves.
|
|
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';
|
|
15
13
|
import { normalizeVueAttrs } from '../utils/normalize-attrs.js';
|
|
16
|
-
// A style prop is an object (a style record) or an array of them; numbers/strings/null degrade
|
|
17
|
-
// to undefined (the engine flattens what it gets). A runtime guard, not a cast.
|
|
18
14
|
function isStyleProp(value) {
|
|
19
15
|
return typeof value === 'object' && value !== null;
|
|
20
16
|
}
|
|
21
17
|
function asBehavior(value) {
|
|
22
|
-
return value === 'height' || value === 'position' || value === 'padding'
|
|
18
|
+
return value === 'height' || value === 'position' || value === 'padding'
|
|
19
|
+
? value
|
|
20
|
+
: undefined;
|
|
23
21
|
}
|
|
24
|
-
// The prop/handler keys the lifecycle consumes itself; everything else (accessibility, testID,
|
|
25
|
-
// aria-*/data-*) forwards onto the wrapper host node.
|
|
26
22
|
const HANDLED_ATTRS = [
|
|
27
23
|
'behavior',
|
|
28
24
|
'enabled',
|
|
@@ -40,19 +36,31 @@ function forwardAttrs(attrs) {
|
|
|
40
36
|
return result;
|
|
41
37
|
}
|
|
42
38
|
export const KeyboardAvoidingView = defineComponent((_props, { attrs: rawAttrs, slots, emit }) => {
|
|
43
|
-
// The inset is a number (plain data), so a plain ref is correct; no engine node is held here
|
|
44
|
-
// (onLayout delivers the frame, so no imperative measure / host-node capture is needed).
|
|
45
39
|
const inset = ref(0);
|
|
46
40
|
// Mutable, not reactive: changing the measured frame alone shouldn't re-render; it feeds the
|
|
47
|
-
// next keyboard event's inset math
|
|
41
|
+
// next keyboard event's inset math.
|
|
48
42
|
let frame;
|
|
49
43
|
let initialHeight;
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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
|
+
};
|
|
53
54
|
const onShow = (payload) => {
|
|
54
55
|
const keyboard = readKeyboardFrame(payload);
|
|
55
|
-
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
|
+
});
|
|
56
64
|
dlog(`KeyboardAvoidingView show -> inset ${next}`);
|
|
57
65
|
inset.value = next;
|
|
58
66
|
};
|
|
@@ -62,11 +70,19 @@ export const KeyboardAvoidingView = defineComponent((_props, { attrs: rawAttrs,
|
|
|
62
70
|
};
|
|
63
71
|
let subscriptions = [];
|
|
64
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);
|
|
65
77
|
subscriptions = [
|
|
66
|
-
Keyboard.addListener(
|
|
67
|
-
Keyboard.addListener(
|
|
68
|
-
Keyboard.addListener(KEYBOARD_EVENT.didHide, onHide),
|
|
78
|
+
Keyboard.addListener(events.show, onShow),
|
|
79
|
+
Keyboard.addListener(events.hide, onHide),
|
|
69
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
|
+
});
|
|
70
86
|
});
|
|
71
87
|
onUnmounted(() => {
|
|
72
88
|
for (const subscription of subscriptions)
|
|
@@ -83,7 +99,7 @@ export const KeyboardAvoidingView = defineComponent((_props, { attrs: rawAttrs,
|
|
|
83
99
|
emit('layout', event);
|
|
84
100
|
};
|
|
85
101
|
return () => {
|
|
86
|
-
const attrs =
|
|
102
|
+
const attrs = currentAttrs();
|
|
87
103
|
const behavior = asBehavior(attrs.behavior);
|
|
88
104
|
// RN gates every inset on `enabled ?? true`; only an explicit `false` disables.
|
|
89
105
|
const isEnabled = attrs.enabled !== false;
|
|
@@ -103,8 +119,8 @@ export const KeyboardAvoidingView = defineComponent((_props, { attrs: rawAttrs,
|
|
|
103
119
|
style: layout.wrapperStyle,
|
|
104
120
|
onLayout: handleLayout,
|
|
105
121
|
};
|
|
106
|
-
// 'nested'
|
|
107
|
-
//
|
|
122
|
+
// 'nested' pushes the children in an inner view by `bottom: inset`; the wrapper modes
|
|
123
|
+
// adjust the single wrapper directly.
|
|
108
124
|
if (layout.kind === 'nested') {
|
|
109
125
|
return h('symbiote-view', wrapperProps, [
|
|
110
126
|
h('symbiote-view', { style: layout.innerStyle }, childNodes),
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { type IAccessibilityProps, type IAriaProps, type IModalAnimationType, type IModalOrientation, type
|
|
2
|
-
import { type IClassNameValue, type IStyleProp, type IViewStyle } from '@symbiote-native/engine';
|
|
1
|
+
import { type IAccessibilityProps, type IAriaProps, type IModalAnimationType, type IModalOrientation, type IModalPresentationStyle } from '@symbiote-native/components';
|
|
2
|
+
import { type IClassNameValue, type IStyleProp, type ISymbioteEvent, type IViewStyle } from '@symbiote-native/engine';
|
|
3
3
|
export type { IModalAnimationType, IModalPresentationStyle, IModalOrientation, IModalOrientationChangeEvent, } from '@symbiote-native/components';
|
|
4
4
|
export interface IModalProps extends IAccessibilityProps, IAriaProps {
|
|
5
5
|
visible?: boolean;
|
|
@@ -19,11 +19,11 @@ export type IModalEmits = {
|
|
|
19
19
|
show: () => boolean;
|
|
20
20
|
dismiss: () => boolean;
|
|
21
21
|
requestClose: () => boolean;
|
|
22
|
-
orientationChange: (event:
|
|
22
|
+
orientationChange: (event: ISymbioteEvent) => boolean;
|
|
23
23
|
};
|
|
24
24
|
export declare const Modal: import("vue").DefineSetupFnComponent<IModalProps, IModalEmits, {}, IModalProps & {
|
|
25
25
|
onShow?: (() => any) | undefined;
|
|
26
26
|
onDismiss?: (() => any) | undefined;
|
|
27
27
|
onRequestClose?: (() => any) | undefined;
|
|
28
|
-
onOrientationChange?: ((event:
|
|
28
|
+
onOrientationChange?: ((event: ISymbioteEvent) => any) | undefined;
|
|
29
29
|
}, import("vue").PublicProps>;
|
|
@@ -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;
|
|
@@ -1,24 +1,22 @@
|
|
|
1
|
-
// Pressable, the Vue lifecycle half. The press lifecycle (
|
|
2
|
-
// Delay deferral,
|
|
3
|
-
// @symbiote-native/components/state as a pure machine over a runtime + host; the render
|
|
4
|
-
// responder listeners,
|
|
5
|
-
// @symbiote-native/components/view, both shared verbatim with
|
|
1
|
+
// Pressable, the Vue lifecycle half. The press lifecycle (long-press timer, unstable_press-
|
|
2
|
+
// Delay deferral, pressRetentionOffset drift test, suppression flags) lives in
|
|
3
|
+
// @symbiote-native/components/state as a pure machine over a runtime + host; the render
|
|
4
|
+
// decisions (responder listeners, disabled->accessibilityState fold, ripple prop) in
|
|
5
|
+
// @symbiote-native/components/view, both shared verbatim with React. Vue supplies the
|
|
6
6
|
// reactivity: a `ref` holds `pressed`, a setup-scope object holds the press runtime, a function
|
|
7
|
-
// ref grabs the responder View's host node
|
|
8
|
-
// twin of the React adapter's useState + useRef(runtime) + useRef(viewRef).
|
|
7
|
+
// ref grabs the responder View's host node.
|
|
9
8
|
//
|
|
10
|
-
// Inputs arrive as attrs (untyped), so each is narrowed with a runtime guard
|
|
11
|
-
//
|
|
12
|
-
// forwarded attrs (
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
import { defineComponent, h, ref, shallowRef } from '@vue/runtime-core';
|
|
9
|
+
// Inputs arrive as attrs (untyped), so each is narrowed with a runtime guard, never a cast. The
|
|
10
|
+
// user's onPress/onPressIn/... are consumed by the machine and MUST be stripped from the
|
|
11
|
+
// forwarded attrs (pure-JS callbacks); the machine's SYNTHESIZED handlers go on the View instead.
|
|
12
|
+
// Children arrive as a (scoped) default slot so `v-slot="{ pressed }"` mirrors React's
|
|
13
|
+
// children-as-function.
|
|
14
|
+
import { defineComponent, h, ref, shallowRef, } from '@vue/runtime-core';
|
|
16
15
|
import { createPressHandlers, createPressRuntime, rippleProps, buildPressableListeners, resolveDisabledAccessibilityState, noteHoverNoop, resolveAccessibilityProps, DEFAULT_DELAY_LONG_PRESS_MS, } from '@symbiote-native/components';
|
|
17
16
|
import { measure, isSymbioteNode, } from '@symbiote-native/engine';
|
|
18
17
|
import { View } from '../components.js';
|
|
19
18
|
import { normalizeVueAttrs } from '../utils/normalize-attrs.js';
|
|
20
|
-
//
|
|
21
|
-
// Touchable* wrapper (they expose the same events), so the runtime list lives in exactly one place.
|
|
19
|
+
// Reused verbatim as the `emits` of every Touchable* wrapper, so the runtime list lives once.
|
|
22
20
|
export const PRESSABLE_EMITS = {
|
|
23
21
|
press: (_event) => true,
|
|
24
22
|
pressIn: (_event) => true,
|
|
@@ -29,9 +27,8 @@ export const PRESSABLE_EMITS = {
|
|
|
29
27
|
hoverOut: (_event) => true,
|
|
30
28
|
};
|
|
31
29
|
// Bridge the whole press surface onto a child Pressable: each host onX callback re-emits the
|
|
32
|
-
// matching event on the wrapper.
|
|
33
|
-
//
|
|
34
|
-
// animation on press-in/out) overrides that one key AFTER the spread.
|
|
30
|
+
// matching event on the wrapper. A wrapper that intercepts an event for its own state (e.g.
|
|
31
|
+
// TouchableOpacity's opacity animation on press-in/out) overrides that one key AFTER the spread.
|
|
35
32
|
export function emitPressableEvents(emit) {
|
|
36
33
|
return {
|
|
37
34
|
onPress: event => emit('press', event),
|
|
@@ -53,7 +50,7 @@ function numberOr(value, fallback) {
|
|
|
53
50
|
return typeof value === 'number' ? value : fallback;
|
|
54
51
|
}
|
|
55
52
|
// A scalar offset, or the per-edge object; anything else is dropped (the machine reads undefined
|
|
56
|
-
// as "no offset"
|
|
53
|
+
// as "no offset" -> RN's defaults).
|
|
57
54
|
function asRectOffset(value) {
|
|
58
55
|
if (typeof value === 'number')
|
|
59
56
|
return value;
|
|
@@ -107,9 +104,8 @@ function resolveStyle(value, state) {
|
|
|
107
104
|
return value(state);
|
|
108
105
|
return value;
|
|
109
106
|
}
|
|
110
|
-
//
|
|
111
|
-
//
|
|
112
|
-
// must never reach the host; the machine's synthesized handlers go on via buildPressableListeners.
|
|
107
|
+
// Everything else forwards onto the View. User press callbacks are pure JS and must never reach
|
|
108
|
+
// the host; the machine's synthesized handlers go on via buildPressableListeners.
|
|
113
109
|
const HANDLED_ATTRS = [
|
|
114
110
|
'onPress',
|
|
115
111
|
'onPressIn',
|
|
@@ -141,16 +137,16 @@ function forwardAttrs(attrs) {
|
|
|
141
137
|
export const Pressable = defineComponent((_props, { slots, attrs: rawAttrs, emit }) => {
|
|
142
138
|
const pressed = ref(false);
|
|
143
139
|
// The mutable press runtime (timers, suppression flags, measured region). A plain setup-scope
|
|
144
|
-
// object, never a ref:
|
|
140
|
+
// object, never a ref: mutated by the machine, never reactively read.
|
|
145
141
|
const runtime = createPressRuntime();
|
|
146
|
-
// shallowRef, NOT ref:
|
|
147
|
-
// WeakMap mirror
|
|
142
|
+
// shallowRef, NOT ref: a plain ref would wrap the node in a reactive Proxy, missing the
|
|
143
|
+
// engine's WeakMap mirror and making measure() no-op.
|
|
148
144
|
const nodeRef = shallowRef(null);
|
|
149
145
|
const setNodeRef = (el) => {
|
|
150
146
|
nodeRef.value = isSymbioteNode(el) ? el : null;
|
|
151
147
|
};
|
|
152
148
|
// The lifecycle seam the machine fills: flip the reactive `pressed`, and expose the responder
|
|
153
|
-
// View's raw frame-measure (or undefined before the node commits
|
|
149
|
+
// View's raw frame-measure (or undefined before the node commits, a radius fallback).
|
|
154
150
|
const host = {
|
|
155
151
|
setPressed: next => {
|
|
156
152
|
pressed.value = next;
|
|
@@ -184,9 +180,8 @@ export const Pressable = defineComponent((_props, { slots, attrs: rawAttrs, emit
|
|
|
184
180
|
const handlers = createPressHandlers(config, runtime, host);
|
|
185
181
|
noteHoverNoop(undefined, undefined);
|
|
186
182
|
const state = { pressed: pressed.value };
|
|
187
|
-
//
|
|
188
|
-
//
|
|
189
|
-
// folds for it). resolveAccessibilityProps merges aria into the accessibilityState we set.
|
|
183
|
+
// Vue's View is a bare host primitive, so Pressable folds disabled into accessibilityState
|
|
184
|
+
// and aria/role itself, rather than the View folding it (as React's does).
|
|
190
185
|
const forwarded = forwardAttrs(attrs);
|
|
191
186
|
forwarded.accessibilityState = resolveDisabledAccessibilityState(asAccessibilityState(attrs.accessibilityState), disabled);
|
|
192
187
|
const folded = resolveAccessibilityProps(forwarded);
|
|
@@ -198,11 +193,9 @@ export const Pressable = defineComponent((_props, { slots, attrs: rawAttrs, emit
|
|
|
198
193
|
if (typeof attrs.android_disableSound === 'boolean')
|
|
199
194
|
viewProps.android_disableSound = attrs.android_disableSound;
|
|
200
195
|
Object.assign(viewProps, buildPressableListeners(handlers, { disabled, cancelable }));
|
|
201
|
-
// Children come from the (scoped) default slot, receiving the press state so a render-prop
|
|
202
|
-
// child (`v-slot="{ pressed }"`) mirrors React's children-as-function.
|
|
203
196
|
const content = slots.default !== undefined ? slots.default(state) : [];
|
|
204
197
|
// android_ripple rides a dedicated inner View; on iOS the prop is undefined, so the child
|
|
205
|
-
// renders unwrapped, no extra node.
|
|
198
|
+
// renders unwrapped, no extra node.
|
|
206
199
|
const ripple = isRecord(attrs.android_ripple)
|
|
207
200
|
? rippleProps(asRippleConfig(attrs.android_ripple) ?? {})
|
|
208
201
|
: undefined;
|