@symbiote-native/svelte 0.2.1 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/bootstrap.js +1 -1
- package/build/class-value.js +1 -1
- package/build/components/RefreshControl.svelte +4 -2
- package/build/components/Text.svelte +11 -2
- package/build/components/View.svelte +3 -1
- package/build/components/activity-indicator/index.svelte +8 -2
- package/build/components/button.svelte +8 -3
- package/build/components/flat-list/index.svelte +46 -11
- package/build/components/image/image-logic.js +13 -3
- package/build/components/image-background/index.svelte +8 -2
- package/build/components/index.d.ts +2 -2
- package/build/components/input-accessory-view/index.svelte +4 -1
- package/build/components/keyboard-avoiding-view/index.svelte +34 -11
- package/build/components/modal/index.svelte +4 -1
- package/build/components/pressable/index.svelte +48 -23
- package/build/components/pressable/index.svelte.d.ts +5 -1
- package/build/components/pressable/pressable-props.d.ts +1 -1
- package/build/components/safe-area-view-props.d.ts +1 -0
- package/build/components/scroll-view/index.svelte +54 -21
- package/build/components/scroll-view/sticky-header.svelte +49 -16
- package/build/components/section-list/index.svelte +6 -1
- package/build/components/switch/index.svelte +16 -5
- package/build/components/text-input/index.svelte +80 -8
- package/build/components/text-input/index.svelte.d.ts +5 -0
- package/build/components/touchable-highlight/index.svelte +115 -15
- package/build/components/touchable-highlight/touchable-highlight-props.d.ts +2 -0
- package/build/components/touchable-native-feedback/touchable-native-feedback.svelte +8 -3
- package/build/components/touchable-opacity/index.svelte +113 -56
- package/build/components/touchable-without-feedback/index.svelte +60 -4
- package/build/components/virtualized-list/index.svelte +233 -70
- package/build/components/virtualized-list/virtualized-list-props.js +8 -4
- package/build/components/virtualized-section-list/index.svelte +56 -9
- package/build/components/virtualized-section-list/index.svelte.d.ts +1 -1
- package/build/components/virtualized-section-list/virtualized-section-list-props.d.ts +5 -0
- package/build/create-portal/index.d.ts +22 -0
- package/build/create-portal/index.js +160 -0
- package/build/descriptor-to-svelte.js +15 -21
- package/build/dom-shim/element.d.ts +9 -3
- package/build/dom-shim/element.js +77 -25
- package/build/dom-shim/fold-host-bag.d.ts +1 -0
- package/build/dom-shim/fold-host-bag.js +7 -0
- package/build/dom-shim/index.d.ts +1 -0
- package/build/dom-shim/index.js +3 -0
- package/build/dom-shim/patch-globals.js +4 -2
- package/build/dom-shim/shim-node.js +11 -0
- package/build/dom-shim/text.js +38 -1
- package/build/host-instance.js +9 -5
- package/build/index.d.ts +6 -3
- package/build/index.js +20 -11
- package/build/modules/animated/create-animated-component.d.ts +5 -0
- package/build/modules/animated/create-animated-component.js +131 -0
- package/build/modules/animated/index.d.ts +24 -14
- package/build/modules/animated/index.js +30 -30
- package/build/modules/status-bar/index.js +1 -1
- package/build/modules/status-bar/index.svelte +4 -1
- package/build/native-view-bridge.d.ts +1 -1
- package/build/native-view-bridge.js +1 -1
- package/build/preprocessor/collapse-text-whitespace.js +20 -7
- package/build/preprocessor/forbid-web-only-constructs.js +8 -2
- package/build/preprocessor/lower-host-primitives.d.ts +10 -0
- package/build/preprocessor/lower-host-primitives.js +455 -0
- package/build/preprocessor/scoped-styles.js +141 -104
- package/build/register.d.ts +1 -0
- package/build/register.js +33 -0
- package/build/render.js +48 -3
- package/build/scope-token.d.ts +3 -2
- package/build/scope-token.js +6 -17
- package/build/state-style.d.ts +1 -0
- package/build/state-style.js +8 -0
- package/build/style-scope.d.ts +2 -1
- package/build/style-scope.js +10 -8
- package/metro-svelte-transformer.cjs +48 -14
- package/package.json +28 -10
- package/build/modules/animated/AnimatedImage.svelte +0 -115
- package/build/modules/animated/AnimatedImage.svelte.d.ts +0 -5
- package/build/modules/animated/AnimatedScrollView.svelte +0 -92
- package/build/modules/animated/AnimatedScrollView.svelte.d.ts +0 -17
- package/build/modules/animated/AnimatedText.svelte +0 -58
- package/build/modules/animated/AnimatedText.svelte.d.ts +0 -5
- package/build/modules/animated/AnimatedView.svelte +0 -95
- package/build/modules/animated/AnimatedView.svelte.d.ts +0 -5
|
@@ -85,7 +85,8 @@
|
|
|
85
85
|
}: IScrollViewProps = $props();
|
|
86
86
|
|
|
87
87
|
$effect(() => {
|
|
88
|
-
if (stickyHeaderIndices === undefined || stickyHeaderIndices.length === 0)
|
|
88
|
+
if (stickyHeaderIndices === undefined || stickyHeaderIndices.length === 0)
|
|
89
|
+
return;
|
|
89
90
|
dlog(
|
|
90
91
|
'ScrollView.stickyHeaderIndices is not auto-honored on Svelte (no index-wrap mechanism — ' +
|
|
91
92
|
'see scroll-view-props.ts KNOWN GAP); compose ScrollViewStickyHeader manually instead',
|
|
@@ -103,7 +104,11 @@
|
|
|
103
104
|
// Plain exported functions on the instance script are what a parent's `bind:this={ref}` sees —
|
|
104
105
|
// the Svelte mechanism for exposing an imperative handle, the twin of React's
|
|
105
106
|
// useImperativeHandle / Vue's expose().
|
|
106
|
-
export function scrollTo(options?: {
|
|
107
|
+
export function scrollTo(options?: {
|
|
108
|
+
x?: number;
|
|
109
|
+
y?: number;
|
|
110
|
+
animated?: boolean;
|
|
111
|
+
}): void {
|
|
107
112
|
handle.scrollTo(options);
|
|
108
113
|
}
|
|
109
114
|
export function scrollToEnd(options?: { animated?: boolean }): void {
|
|
@@ -117,7 +122,9 @@
|
|
|
117
122
|
}
|
|
118
123
|
|
|
119
124
|
const isHorizontal = $derived(horizontal === true);
|
|
120
|
-
const hasStickyHeaders = $derived(
|
|
125
|
+
const hasStickyHeaders = $derived(
|
|
126
|
+
stickyHeaderIndices !== undefined && stickyHeaderIndices.length > 0,
|
|
127
|
+
);
|
|
121
128
|
const shouldWrapRefreshControl = $derived(
|
|
122
129
|
PLATFORM.refreshControlMode === 'wrap' && refreshControl !== undefined,
|
|
123
130
|
);
|
|
@@ -131,7 +138,9 @@
|
|
|
131
138
|
// style and collapse it to nothing — e.g. App.svelte's `class="screen"` (flex:1) on the
|
|
132
139
|
// top-level ScrollView, which left the wrapper with no height for its content to grow into.
|
|
133
140
|
const layoutSplit = $derived(
|
|
134
|
-
shouldWrapRefreshControl
|
|
141
|
+
shouldWrapRefreshControl
|
|
142
|
+
? splitLayoutProps([resolveSvelteClass(className), style])
|
|
143
|
+
: undefined,
|
|
135
144
|
);
|
|
136
145
|
|
|
137
146
|
// A single AnimatedValue tracks the scroll offset (RN's _scrollAnimatedValue), allocated once
|
|
@@ -161,7 +170,9 @@
|
|
|
161
170
|
// it solely `if (isFabric)`). Forcing the JS path to keep that listener alive only gives up the
|
|
162
171
|
// native driver, putting the pin on the JS thread — drift on iOS, outright failure on Android
|
|
163
172
|
// (commit debounce 15ms vs iOS's 64ms — render-scroll-sticky.ts).
|
|
164
|
-
const nativeStickyAvailable = $derived(
|
|
173
|
+
const nativeStickyAvailable = $derived(
|
|
174
|
+
hasStickyHeaders && isNativeAnimatedAvailable(),
|
|
175
|
+
);
|
|
165
176
|
|
|
166
177
|
// Native sticky-scroll attach (RN attachNativeEvent / _updateAnimatedNodeAttachment) — NOT used
|
|
167
178
|
// for sticky headers (see the comment above); kept for other native-event-attach consumers.
|
|
@@ -178,7 +189,9 @@
|
|
|
178
189
|
: contentContainerStyle,
|
|
179
190
|
);
|
|
180
191
|
|
|
181
|
-
const intrinsics = $derived(
|
|
192
|
+
const intrinsics = $derived(
|
|
193
|
+
selectScrollIntrinsics(isHorizontal, resolvedContentContainerStyle),
|
|
194
|
+
);
|
|
182
195
|
|
|
183
196
|
const forwarding = $derived(
|
|
184
197
|
resolveScrollForwarding({
|
|
@@ -186,7 +199,8 @@
|
|
|
186
199
|
nativeStickyAvailable,
|
|
187
200
|
invertStickyHeaders,
|
|
188
201
|
scrollEventThrottle,
|
|
189
|
-
maintainVisibleContentPosition:
|
|
202
|
+
maintainVisibleContentPosition:
|
|
203
|
+
passthrough.maintainVisibleContentPosition,
|
|
190
204
|
snapToAlignment: passthrough.snapToAlignment,
|
|
191
205
|
}),
|
|
192
206
|
);
|
|
@@ -220,7 +234,10 @@
|
|
|
220
234
|
[{ nativeEvent: { contentOffset: { y: scrollAnimatedValue } } }],
|
|
221
235
|
onScroll === undefined
|
|
222
236
|
? undefined
|
|
223
|
-
: {
|
|
237
|
+
: {
|
|
238
|
+
listener: (...args: unknown[]) =>
|
|
239
|
+
forwardScrollEvent(onScroll, args),
|
|
240
|
+
},
|
|
224
241
|
);
|
|
225
242
|
});
|
|
226
243
|
|
|
@@ -243,7 +260,9 @@
|
|
|
243
260
|
// layoutSplit already folded className's resolved value into inner/outer above — forwarding
|
|
244
261
|
// the raw class here too would re-apply its LAYOUT half onto the inner scroll view a second
|
|
245
262
|
// time (on top of the wrapper).
|
|
246
|
-
...(className !== undefined && layoutSplit === undefined
|
|
263
|
+
...(className !== undefined && layoutSplit === undefined
|
|
264
|
+
? { class: className }
|
|
265
|
+
: {}),
|
|
247
266
|
style: scrollStyle,
|
|
248
267
|
onScroll: resolvedScrollHandler,
|
|
249
268
|
onLayout: handleScrollLayout,
|
|
@@ -260,7 +279,9 @@
|
|
|
260
279
|
style: intrinsics.contentStyle,
|
|
261
280
|
collapsable: false,
|
|
262
281
|
...(forwarding.collapsableChildren ? { collapsableChildren: false } : {}),
|
|
263
|
-
...(onContentSizeChange !== undefined
|
|
282
|
+
...(onContentSizeChange !== undefined
|
|
283
|
+
? { onLayout: handleContentLayout }
|
|
284
|
+
: {}),
|
|
264
285
|
}));
|
|
265
286
|
// See View.svelte's note on `{@attach}`.
|
|
266
287
|
const syncAttachments = createAttachmentsSync();
|
|
@@ -270,29 +291,41 @@
|
|
|
270
291
|
</script>
|
|
271
292
|
|
|
272
293
|
{#snippet scrollBody()}
|
|
273
|
-
<!--
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
fails silently. Do not reformat this join. -->
|
|
294
|
+
<!-- The gap between these two sibling blocks survives clean_nodes as a ' ' text node, but
|
|
295
|
+
the shim drops a whitespace-only node whose parent takes no raw text, so it never
|
|
296
|
+
reaches Fabric (dom-shim/text.ts, svelte-adapter-dom-shim §16b). -->
|
|
277
297
|
{#if !shouldWrapRefreshControl && refreshControl !== undefined}
|
|
278
298
|
<RefreshControl {...refreshControl} />
|
|
279
|
-
{/if}
|
|
280
|
-
|
|
299
|
+
{/if}
|
|
300
|
+
{#if isHorizontal}
|
|
301
|
+
<symbiote-horizontal-scroll-content p={contentBag}>
|
|
302
|
+
{@render children?.()}
|
|
303
|
+
</symbiote-horizontal-scroll-content>
|
|
281
304
|
{:else}
|
|
282
|
-
<symbiote-scroll-content p={contentBag}>
|
|
305
|
+
<symbiote-scroll-content p={contentBag}>
|
|
306
|
+
{@render children?.()}
|
|
307
|
+
</symbiote-scroll-content>
|
|
283
308
|
{/if}
|
|
284
309
|
{/snippet}
|
|
285
310
|
|
|
286
311
|
{#if shouldWrapRefreshControl && refreshControl !== undefined}
|
|
287
312
|
<RefreshControl {...refreshControl} style={layoutSplit?.outer}>
|
|
288
313
|
{#if isHorizontal}
|
|
289
|
-
<symbiote-horizontal-scroll-view p={outerBag} bind:this={hostShim}>
|
|
314
|
+
<symbiote-horizontal-scroll-view p={outerBag} bind:this={hostShim}>
|
|
315
|
+
{@render scrollBody()}
|
|
316
|
+
</symbiote-horizontal-scroll-view>
|
|
290
317
|
{:else}
|
|
291
|
-
<symbiote-scroll-view p={outerBag} bind:this={hostShim}>
|
|
318
|
+
<symbiote-scroll-view p={outerBag} bind:this={hostShim}>
|
|
319
|
+
{@render scrollBody()}
|
|
320
|
+
</symbiote-scroll-view>
|
|
292
321
|
{/if}
|
|
293
322
|
</RefreshControl>
|
|
294
323
|
{:else if isHorizontal}
|
|
295
|
-
<symbiote-horizontal-scroll-view p={outerBag} bind:this={hostShim}>
|
|
324
|
+
<symbiote-horizontal-scroll-view p={outerBag} bind:this={hostShim}>
|
|
325
|
+
{@render scrollBody()}
|
|
326
|
+
</symbiote-horizontal-scroll-view>
|
|
296
327
|
{:else}
|
|
297
|
-
<symbiote-scroll-view p={outerBag} bind:this={hostShim}>
|
|
328
|
+
<symbiote-scroll-view p={outerBag} bind:this={hostShim}>
|
|
329
|
+
{@render scrollBody()}
|
|
330
|
+
</symbiote-scroll-view>
|
|
298
331
|
{/if}
|
|
@@ -29,7 +29,8 @@
|
|
|
29
29
|
//
|
|
30
30
|
// Native-Animated-driven, like React's ScrollViewStickyHeader.js: the pinned translateY rides
|
|
31
31
|
// as the raw AnimatedInterpolation node in style.transform on a real Animated.View
|
|
32
|
-
// (
|
|
32
|
+
// (createAnimatedComponent(View)), whose `__makeNative()` connects it straight to the native
|
|
33
|
+
// shadow tree,
|
|
33
34
|
// so once native the pin tracks scroll on the UI thread with zero JS per frame - the
|
|
34
35
|
// interpolation's own .addListener() below only drives the separate debounced
|
|
35
36
|
// `passthroughAnimatedPropExplicitValues` (RN's own hit-testing sync).
|
|
@@ -51,9 +52,16 @@
|
|
|
51
52
|
// why ScrollView/VirtualizedList force `nativeStickyAvailable = false`: the correct bootstrap
|
|
52
53
|
// path, not a workaround.
|
|
53
54
|
import type { IStickyHeaderComponentProps } from './sticky-header-props';
|
|
55
|
+
import View from '../View.svelte';
|
|
56
|
+
import { createAnimatedComponent } from '../../modules/animated/create-animated-component';
|
|
54
57
|
|
|
55
58
|
export type { IStickyHeaderComponentProps };
|
|
56
59
|
|
|
60
|
+
// Wrapped here, not imported from modules/animated: that barrel imports ScrollView, which
|
|
61
|
+
// imports this file, so naming it would close a module cycle. Module scope, so every header
|
|
62
|
+
// instance shares one component identity.
|
|
63
|
+
const AnimatedView = createAnimatedComponent(View);
|
|
64
|
+
|
|
57
65
|
// Diagnostic-only: a module-scope counter so every instance across every ScrollViewStickyHeader
|
|
58
66
|
// in a device log is individually identifiable (see the mount/unmount dlog calls below).
|
|
59
67
|
let stickyHeaderInstanceSeq = 0;
|
|
@@ -70,8 +78,13 @@
|
|
|
70
78
|
type IStickyEffect,
|
|
71
79
|
type IStickyReducerInputs,
|
|
72
80
|
} from '@symbiote-native/components';
|
|
73
|
-
import {
|
|
74
|
-
|
|
81
|
+
import {
|
|
82
|
+
AnimatedValue,
|
|
83
|
+
Platform,
|
|
84
|
+
dlog,
|
|
85
|
+
type AnimatedInterpolation,
|
|
86
|
+
type ISymbioteEvent,
|
|
87
|
+
} from '@symbiote-native/engine';
|
|
75
88
|
import {
|
|
76
89
|
SCROLL_VIEW_STICKY_CONTEXT_KEY,
|
|
77
90
|
type IScrollViewStickyContext,
|
|
@@ -91,18 +104,26 @@
|
|
|
91
104
|
// Resolve the scroll wiring from the parent ScrollView's context when not given explicitly.
|
|
92
105
|
// A fresh standalone AnimatedValue is the fallback for a header used outside a ScrollView
|
|
93
106
|
// (never sticks, but never crashes either).
|
|
94
|
-
const stickyContext = getContext<IScrollViewStickyContext | undefined>(
|
|
107
|
+
const stickyContext = getContext<IScrollViewStickyContext | undefined>(
|
|
108
|
+
SCROLL_VIEW_STICKY_CONTEXT_KEY,
|
|
109
|
+
);
|
|
95
110
|
$effect(() => {
|
|
96
111
|
if (stickyContext === undefined && scrollAnimatedValueProp === undefined) {
|
|
97
|
-
dlog(
|
|
112
|
+
dlog(
|
|
113
|
+
'ScrollViewStickyHeader used outside a ScrollView — sticky positioning is a no-op',
|
|
114
|
+
);
|
|
98
115
|
}
|
|
99
116
|
});
|
|
100
117
|
// svelte-ignore state_referenced_locally -- captured ONCE by identity; it does not change
|
|
101
118
|
// after mount, so re-deriving it would be wrong.
|
|
102
119
|
const scrollAnimatedValue =
|
|
103
|
-
scrollAnimatedValueProp ??
|
|
120
|
+
scrollAnimatedValueProp ??
|
|
121
|
+
stickyContext?.scrollAnimatedValue ??
|
|
122
|
+
new AnimatedValue(0);
|
|
104
123
|
const inverted = $derived(invertedProp ?? stickyContext?.getInverted());
|
|
105
|
-
const scrollViewHeight = $derived(
|
|
124
|
+
const scrollViewHeight = $derived(
|
|
125
|
+
scrollViewHeightProp ?? stickyContext?.getViewportHeight(),
|
|
126
|
+
);
|
|
106
127
|
|
|
107
128
|
// The one folded state cell (RN's scattered useState/useRef collapsed into IStickyHeaderState),
|
|
108
129
|
// mutated in place by reduceSticky. Named `stickyState`, not `state`: a local binding literally
|
|
@@ -115,7 +136,10 @@
|
|
|
115
136
|
// The interpolation node currently bound into style.transform — held by IDENTITY ($state.raw),
|
|
116
137
|
// rebuilt on every 'rebuild-interpolation' effect.
|
|
117
138
|
let animatedTranslateY = $state.raw<AnimatedInterpolation>(
|
|
118
|
-
scrollAnimatedValue.interpolate({
|
|
139
|
+
scrollAnimatedValue.interpolate({
|
|
140
|
+
inputRange: [-1, 0],
|
|
141
|
+
outputRange: [0, 0],
|
|
142
|
+
}),
|
|
119
143
|
);
|
|
120
144
|
let listenerId: string | undefined;
|
|
121
145
|
let debounceTimer: ReturnType<typeof setTimeout> | undefined;
|
|
@@ -139,14 +163,18 @@
|
|
|
139
163
|
// Detach the old listener, build a fresh interpolation onto the shared scroll value, and
|
|
140
164
|
// wire the settled-value listener (drives ONLY the debounced hit-testing passthrough
|
|
141
165
|
// below — the visible pin rides the native AnimatedProps connection in the markup).
|
|
142
|
-
if (listenerId !== undefined)
|
|
166
|
+
if (listenerId !== undefined)
|
|
167
|
+
animatedTranslateY.removeListener(listenerId);
|
|
143
168
|
const next = scrollAnimatedValue.interpolate({
|
|
144
169
|
inputRange: effect.inputRange,
|
|
145
170
|
outputRange: effect.outputRange,
|
|
146
171
|
});
|
|
147
|
-
listenerId = next.addListener(
|
|
148
|
-
|
|
149
|
-
|
|
172
|
+
listenerId = next.addListener(
|
|
173
|
+
({ value }: { value: number | string }): void => {
|
|
174
|
+
if (typeof value === 'number')
|
|
175
|
+
dispatch({ kind: 'animated-tick', value });
|
|
176
|
+
},
|
|
177
|
+
);
|
|
150
178
|
animatedTranslateY = next;
|
|
151
179
|
break;
|
|
152
180
|
}
|
|
@@ -160,7 +188,9 @@
|
|
|
160
188
|
case 'apply-passthrough':
|
|
161
189
|
// The actual committed translateY — a freeze at a huge/wrong number here is the
|
|
162
190
|
// visible-disappearance symptom to watch for.
|
|
163
|
-
dlog(
|
|
191
|
+
dlog(
|
|
192
|
+
`ScrollViewStickyHeader[y=${stickyState.layoutY}] apply-passthrough translateY=${effect.translateY}`,
|
|
193
|
+
);
|
|
164
194
|
version += 1;
|
|
165
195
|
break;
|
|
166
196
|
case 'record-header-y':
|
|
@@ -192,8 +222,11 @@
|
|
|
192
222
|
// Detach the listener + clear the debounce on unmount.
|
|
193
223
|
$effect(() => {
|
|
194
224
|
return (): void => {
|
|
195
|
-
dlog(
|
|
196
|
-
|
|
225
|
+
dlog(
|
|
226
|
+
`ScrollViewStickyHeader#${instanceId}[y=${stickyState.layoutY}] unmount`,
|
|
227
|
+
);
|
|
228
|
+
if (listenerId !== undefined)
|
|
229
|
+
animatedTranslateY.removeListener(listenerId);
|
|
197
230
|
if (debounceTimer !== undefined) clearTimeout(debounceTimer);
|
|
198
231
|
};
|
|
199
232
|
});
|
|
@@ -213,7 +246,7 @@
|
|
|
213
246
|
|
|
214
247
|
// The EXPLICIT debounced translateY overrides the committed transform for hit-testing, while
|
|
215
248
|
// `animatedTranslateY` (in the markup below) does the smooth native-driven pin — per RN
|
|
216
|
-
// ScrollViewStickyHeader.js, and
|
|
249
|
+
// ScrollViewStickyHeader.js, and createAnimatedComponent's own passthroughAnimatedPropExplicitValues
|
|
217
250
|
// contract (readPassthroughStyle).
|
|
218
251
|
const passthroughAnimatedPropExplicitValues = $derived.by(() => {
|
|
219
252
|
void version;
|
|
@@ -6,7 +6,11 @@
|
|
|
6
6
|
// (a Svelte `bind:this` on THIS component resolves to whatever THIS component exports, so the
|
|
7
7
|
// wrapper must re-declare + delegate rather than re-exporting the child's exports directly) —
|
|
8
8
|
// the Svelte twin of the React/Vue SectionList.
|
|
9
|
-
import type {
|
|
9
|
+
import type {
|
|
10
|
+
ISectionListProps,
|
|
11
|
+
ISectionListHandle,
|
|
12
|
+
ISection,
|
|
13
|
+
} from './section-list-props';
|
|
10
14
|
|
|
11
15
|
export type { ISectionListProps, ISectionListHandle, ISection };
|
|
12
16
|
</script>
|
|
@@ -77,6 +81,7 @@
|
|
|
77
81
|
footer={props.footer}
|
|
78
82
|
empty={props.empty}
|
|
79
83
|
keyExtractor={props.keyExtractor}
|
|
84
|
+
getItemLayout={props.getItemLayout}
|
|
80
85
|
stickySectionHeadersEnabled={props.stickySectionHeadersEnabled}
|
|
81
86
|
inverted={props.inverted}
|
|
82
87
|
extraData={props.extraData}
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// render-only). Reuses the shared logic verbatim (switchReducer / valueFromChange /
|
|
4
4
|
// shouldSnapBack, exactly like React's useReducer + Vue's ref-based reducer wiring) and
|
|
5
5
|
// calls renderSwitch() itself for the prop assembly — its Descriptor is always exactly one
|
|
6
|
-
// `symbiote-switch` node with zero children (core/components/src/view/render-switch.ts). Root
|
|
6
|
+
// `symbiote-switch-managed` node with zero children (core/components/src/view/render-switch.ts). Root
|
|
7
7
|
// stays a literal tag (bind:this needs a known tag); `createDescriptorChildrenSync` is still
|
|
8
8
|
// wired for its (always-empty) children, matching every other category-1 component uniformly
|
|
9
9
|
// — the same reason React still routes a childless Switch through descriptorToReact rather
|
|
@@ -27,7 +27,11 @@
|
|
|
27
27
|
valueFromChange,
|
|
28
28
|
renderSwitch,
|
|
29
29
|
} from '@symbiote-native/components';
|
|
30
|
-
import {
|
|
30
|
+
import {
|
|
31
|
+
dispatchViewCommand,
|
|
32
|
+
dlog,
|
|
33
|
+
type ISymbioteEvent,
|
|
34
|
+
} from '@symbiote-native/engine';
|
|
31
35
|
import { PLATFORM } from './switch-platform';
|
|
32
36
|
import { createDescriptorChildrenSync } from '../../descriptor-to-svelte';
|
|
33
37
|
import { createAttachmentsSync } from '../../runes/attachments';
|
|
@@ -67,7 +71,10 @@
|
|
|
67
71
|
// accept/reject control via the snap-back effect below — see TextInput's identical
|
|
68
72
|
// mechanism (text-input/index.svelte) for why echoing unconditionally would break that.
|
|
69
73
|
if (onValueChange === undefined) value = next;
|
|
70
|
-
switchState = switchReducer(switchState, {
|
|
74
|
+
switchState = switchReducer(switchState, {
|
|
75
|
+
type: 'native-reported',
|
|
76
|
+
value: next,
|
|
77
|
+
});
|
|
71
78
|
}
|
|
72
79
|
|
|
73
80
|
// Snap-back: when native reported a value the parent rejected (the value prop did not
|
|
@@ -102,7 +109,11 @@
|
|
|
102
109
|
thumbColor,
|
|
103
110
|
ios_backgroundColor,
|
|
104
111
|
style,
|
|
105
|
-
passthrough: {
|
|
112
|
+
passthrough: {
|
|
113
|
+
...passthrough,
|
|
114
|
+
class: className,
|
|
115
|
+
onChange: handleChange,
|
|
116
|
+
},
|
|
106
117
|
},
|
|
107
118
|
PLATFORM,
|
|
108
119
|
),
|
|
@@ -120,4 +131,4 @@
|
|
|
120
131
|
});
|
|
121
132
|
</script>
|
|
122
133
|
|
|
123
|
-
<symbiote-switch p={descriptor.props} bind:this={hostShim} />
|
|
134
|
+
<symbiote-switch-managed p={descriptor.props} bind:this={hostShim} />
|
|
@@ -48,6 +48,10 @@
|
|
|
48
48
|
setInputFocused,
|
|
49
49
|
setInputBlurred,
|
|
50
50
|
type ISymbioteEvent,
|
|
51
|
+
type ISymbioteNode,
|
|
52
|
+
type IMeasureOnSuccess,
|
|
53
|
+
type IMeasureInWindowOnSuccess,
|
|
54
|
+
type IMeasureLayoutOnSuccess,
|
|
51
55
|
} from '@symbiote-native/engine';
|
|
52
56
|
import { createDescriptorChildrenSync } from '../../descriptor-to-svelte';
|
|
53
57
|
import { createAttachmentsSync } from '../../runes/attachments';
|
|
@@ -89,7 +93,9 @@
|
|
|
89
93
|
// plain, direct property read on the raw `$props()` proxy. `source` is a closed interface (no
|
|
90
94
|
// index signature), so `Object.keys` + bracket-indexing would need an `as` cast; `Object.entries`
|
|
91
95
|
// falls onto TS's `entries(o: {}): [string, any][]` overload instead, which needs no cast.
|
|
92
|
-
function forwardProps(
|
|
96
|
+
function forwardProps(
|
|
97
|
+
source: Omit<ITextInputProps, 'value'>,
|
|
98
|
+
): Record<string, unknown> {
|
|
93
99
|
const resolved = resolveAccessibilityProps(source);
|
|
94
100
|
const result: Record<string, unknown> = {};
|
|
95
101
|
for (const [key, propValue] of Object.entries(resolved)) {
|
|
@@ -205,9 +211,17 @@
|
|
|
205
211
|
if (engineNode === undefined) return;
|
|
206
212
|
if (!shouldCommandText(lastNativeText, currentValue)) return;
|
|
207
213
|
const selStart = rest.selection?.start ?? SELECTION_NONE;
|
|
208
|
-
const selEnd =
|
|
209
|
-
|
|
210
|
-
|
|
214
|
+
const selEnd =
|
|
215
|
+
rest.selection?.end ?? rest.selection?.start ?? SELECTION_NONE;
|
|
216
|
+
dlog(
|
|
217
|
+
`TextInput setTextAndSelection count=${count} text=${JSON.stringify(currentValue)}`,
|
|
218
|
+
);
|
|
219
|
+
dispatchViewCommand(engineNode, 'setTextAndSelection', [
|
|
220
|
+
count,
|
|
221
|
+
currentValue,
|
|
222
|
+
selStart,
|
|
223
|
+
selEnd,
|
|
224
|
+
]);
|
|
211
225
|
lastNativeText = currentValue;
|
|
212
226
|
});
|
|
213
227
|
|
|
@@ -217,7 +231,8 @@
|
|
|
217
231
|
// controlled-write effect above relies on.
|
|
218
232
|
$effect(() => {
|
|
219
233
|
const engineNode = hostShim?.engineNode;
|
|
220
|
-
if (autoFocused || engineNode === undefined || rest.autoFocus !== true)
|
|
234
|
+
if (autoFocused || engineNode === undefined || rest.autoFocus !== true)
|
|
235
|
+
return;
|
|
221
236
|
autoFocused = true;
|
|
222
237
|
dlog('TextInput autoFocus -> focus command');
|
|
223
238
|
dispatchViewCommand(engineNode, 'focus', []);
|
|
@@ -241,7 +256,12 @@
|
|
|
241
256
|
export function clear(): void {
|
|
242
257
|
const engineNode = hostShim?.engineNode;
|
|
243
258
|
if (engineNode === undefined) return;
|
|
244
|
-
dispatchViewCommand(engineNode, 'setTextAndSelection', [
|
|
259
|
+
dispatchViewCommand(engineNode, 'setTextAndSelection', [
|
|
260
|
+
mostRecentEventCount,
|
|
261
|
+
'',
|
|
262
|
+
0,
|
|
263
|
+
0,
|
|
264
|
+
]);
|
|
245
265
|
lastNativeText = '';
|
|
246
266
|
}
|
|
247
267
|
|
|
@@ -261,6 +281,43 @@
|
|
|
261
281
|
]);
|
|
262
282
|
}
|
|
263
283
|
|
|
284
|
+
// The four host-ref methods, FORWARDED to the engine node rather than reimplemented.
|
|
285
|
+
//
|
|
286
|
+
// Without them a TextInput ref was poorer than any other host ref in the adapter: every other
|
|
287
|
+
// primitive hands back a node carrying measure/measureInWindow/measureLayout/setNativeProps, and
|
|
288
|
+
// this wrapper closed the node behind five methods of its own. The lowered path lost the opposite
|
|
289
|
+
// four, so the two paths were not "one narrowing the other" but two different surfaces.
|
|
290
|
+
// `ITextInputHandle` is the union of both halves, and `text-input.smoke.test.ts` derives what this
|
|
291
|
+
// component must expose from `buildTextInputHandle` — so a method added to core fails here rather
|
|
292
|
+
// than going missing on whichever path an app happened to take.
|
|
293
|
+
//
|
|
294
|
+
// Not typed as `satisfies ITextInputHandle`: these are instance-script exports, so the component's
|
|
295
|
+
// imperative surface is structurally invisible to the compiler — which is exactly why the test is
|
|
296
|
+
// the guard here and the type is the guard on React, Angular and Solid.
|
|
297
|
+
export function measure(callback: IMeasureOnSuccess): void {
|
|
298
|
+
hostShim?.engineNode?.measure(callback);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
export function measureInWindow(callback: IMeasureInWindowOnSuccess): void {
|
|
302
|
+
hostShim?.engineNode?.measureInWindow(callback);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
export function measureLayout(
|
|
306
|
+
relativeToNativeNode: ISymbioteNode | number,
|
|
307
|
+
onSuccess: IMeasureLayoutOnSuccess,
|
|
308
|
+
onFail?: () => void,
|
|
309
|
+
): void {
|
|
310
|
+
hostShim?.engineNode?.measureLayout(
|
|
311
|
+
relativeToNativeNode,
|
|
312
|
+
onSuccess,
|
|
313
|
+
onFail,
|
|
314
|
+
);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
export function setNativeProps(nativeProps: Record<string, unknown>): void {
|
|
318
|
+
hostShim?.engineNode?.setNativeProps(nativeProps);
|
|
319
|
+
}
|
|
320
|
+
|
|
264
321
|
const descriptor = $derived(
|
|
265
322
|
renderTextInput({
|
|
266
323
|
multiline: isMultiline,
|
|
@@ -293,8 +350,23 @@
|
|
|
293
350
|
});
|
|
294
351
|
</script>
|
|
295
352
|
|
|
353
|
+
<!--
|
|
354
|
+
The `-managed` tags, matching `renderTextInput`'s intrinsics. They resolve to the SAME native view
|
|
355
|
+
as the plain ones; the separate spelling is what keeps the host behavior off this node. The machine
|
|
356
|
+
is registered against `symbiote-text-input` / `…-multiline`, which only the LOWERING transform
|
|
357
|
+
emits, and this wrapper runs the same lifecycle itself (focus/blur mirror, event counter,
|
|
358
|
+
controlled write, autoFocus) — so sharing a tag would give one node two owners.
|
|
359
|
+
|
|
360
|
+
Written literally rather than as `<svelte:element this={descriptor.type}>` for the reason the
|
|
361
|
+
header records, which means this pair does NOT follow `render-text-input.ts` on its own: if those
|
|
362
|
+
intrinsics are renamed again, this file has to move with them. `text-input-tag.test.ts` is what
|
|
363
|
+
makes that a failing test instead of a silent double-owner.
|
|
364
|
+
-->
|
|
296
365
|
{#if isMultiline}
|
|
297
|
-
<symbiote-text-input-multiline
|
|
366
|
+
<symbiote-text-input-multiline-managed
|
|
367
|
+
p={descriptor.props}
|
|
368
|
+
bind:this={hostShim}
|
|
369
|
+
/>
|
|
298
370
|
{:else}
|
|
299
|
-
<symbiote-text-input p={descriptor.props} bind:this={hostShim} />
|
|
371
|
+
<symbiote-text-input-managed p={descriptor.props} bind:this={hostShim} />
|
|
300
372
|
{/if}
|
|
@@ -1,12 +1,17 @@
|
|
|
1
1
|
import type { ITextInputProps } from './text-input-props';
|
|
2
2
|
export type { ITextInputProps };
|
|
3
3
|
export type { ITextInputHandle } from '@symbiote-native/components';
|
|
4
|
+
import { type ISymbioteNode, type IMeasureOnSuccess, type IMeasureInWindowOnSuccess, type IMeasureLayoutOnSuccess } from '@symbiote-native/engine';
|
|
4
5
|
declare const Index: import("svelte").Component<ITextInputProps, {
|
|
5
6
|
focus: () => void;
|
|
6
7
|
blur: () => void;
|
|
7
8
|
clear: () => void;
|
|
8
9
|
isFocused: () => boolean;
|
|
9
10
|
setSelection: (start: number, end: number) => void;
|
|
11
|
+
measure: (callback: IMeasureOnSuccess) => void;
|
|
12
|
+
measureInWindow: (callback: IMeasureInWindowOnSuccess) => void;
|
|
13
|
+
measureLayout: (relativeToNativeNode: ISymbioteNode | number, onSuccess: IMeasureLayoutOnSuccess, onFail?: () => void) => void;
|
|
14
|
+
setNativeProps: (nativeProps: Record<string, unknown>) => void;
|
|
10
15
|
}, "value">;
|
|
11
16
|
type Index = ReturnType<typeof Index>;
|
|
12
17
|
export default Index;
|