@symbiote-native/svelte 1.0.0 → 3.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/README.md +11 -13
- package/build/components/{activity-indicator/activity-indicator-props.d.ts → activity-indicator-props.d.ts} +1 -1
- package/build/components/flat-list/index.svelte +9 -5
- package/build/components/{image-background/image-background-props.d.ts → image-background-props.d.ts} +1 -1
- package/build/components/index.d.ts +6 -24
- package/build/components/index.js +0 -18
- package/build/components/keyboard-avoiding-view/index.svelte +16 -7
- package/build/components/modal/index.svelte +7 -7
- package/build/components/pressable/pressable-props.d.ts +3 -0
- package/build/components/refresh-control-props.d.ts +1 -0
- package/build/components/text-props.d.ts +1 -0
- package/build/components/touchable-highlight/touchable-highlight-props.d.ts +2 -0
- package/build/components/touchable-native-feedback/touchable-native-feedback-props.d.ts +1 -0
- package/build/components/touchable-without-feedback/touchable-without-feedback-props.d.ts +3 -1
- package/build/components/virtualized-list/index.svelte +79 -257
- package/build/components/virtualized-list/virtualized-list-props.d.ts +2 -2
- package/build/components/virtualized-list/virtualized-list-props.js +24 -4
- package/build/create-portal/index.js +2 -2
- package/build/dom-shim/canonical-prop-names.d.ts +2 -0
- package/build/dom-shim/canonical-prop-names.js +323 -0
- package/build/dom-shim/document.js +1 -1
- package/build/dom-shim/element.d.ts +10 -3
- package/build/dom-shim/element.js +233 -31
- package/build/dom-shim/patch-globals.js +31 -5
- package/build/dom-shim/shim-node.d.ts +6 -0
- package/build/dom-shim/shim-node.js +48 -1
- package/build/dom-shim/style-cache.d.ts +1 -0
- package/build/dom-shim/style-cache.js +27 -0
- package/build/dom-shim/text.js +5 -4
- package/build/host-instance.d.ts +2 -3
- package/build/host-instance.js +9 -3
- package/build/index.d.ts +7 -4
- package/build/index.js +17 -5
- package/build/intrinsic-elements.d.ts +63 -0
- package/build/modules/animated/index.d.ts +30 -46
- package/build/modules/animated/index.js +24 -34
- package/build/modules/image/index.d.ts +3 -0
- package/build/modules/image/index.js +15 -0
- package/build/preprocessor/scoped-styles.js +1 -1
- package/build/register.d.ts +1 -1
- package/build/register.js +13 -31
- package/build/root-element.js +1 -1
- package/metro-css-parser.cjs +2 -1
- package/metro-svelte-transformer.cjs +1 -17
- package/package.json +8 -16
- package/build/components/RefreshControl.svelte +0 -40
- package/build/components/RefreshControl.svelte.d.ts +0 -5
- package/build/components/SafeAreaView.svelte +0 -34
- package/build/components/SafeAreaView.svelte.d.ts +0 -5
- package/build/components/Text.svelte +0 -34
- package/build/components/Text.svelte.d.ts +0 -5
- package/build/components/View.svelte +0 -43
- package/build/components/View.svelte.d.ts +0 -5
- package/build/components/activity-indicator/activity-indicator-platform.android.d.ts +0 -3
- package/build/components/activity-indicator/activity-indicator-platform.android.js +0 -5
- package/build/components/activity-indicator/activity-indicator-platform.d.ts +0 -1
- package/build/components/activity-indicator/activity-indicator-platform.ios.d.ts +0 -3
- package/build/components/activity-indicator/activity-indicator-platform.ios.js +0 -6
- package/build/components/activity-indicator/activity-indicator-platform.js +0 -5
- package/build/components/activity-indicator/index.svelte +0 -63
- package/build/components/activity-indicator/index.svelte.d.ts +0 -5
- package/build/components/button.svelte +0 -76
- package/build/components/button.svelte.d.ts +0 -5
- package/build/components/image/image-logic.d.ts +0 -2
- package/build/components/image/image-logic.js +0 -135
- package/build/components/image/index.d.ts +0 -3
- package/build/components/image/index.js +0 -10
- package/build/components/image/index.svelte +0 -66
- package/build/components/image/index.svelte.d.ts +0 -5
- package/build/components/image-background/index.svelte +0 -87
- package/build/components/image-background/index.svelte.d.ts +0 -5
- package/build/components/input-accessory-view/index.svelte +0 -51
- package/build/components/input-accessory-view/index.svelte.d.ts +0 -5
- package/build/components/pressable/index.svelte +0 -185
- package/build/components/pressable/index.svelte.d.ts +0 -9
- package/build/components/scroll-view/index.svelte +0 -331
- package/build/components/scroll-view/index.svelte.d.ts +0 -18
- package/build/components/scroll-view/scroll-view-platform-types.d.ts +0 -3
- package/build/components/scroll-view/scroll-view-platform.android.d.ts +0 -3
- package/build/components/scroll-view/scroll-view-platform.android.js +0 -3
- package/build/components/scroll-view/scroll-view-platform.d.ts +0 -1
- package/build/components/scroll-view/scroll-view-platform.ios.d.ts +0 -3
- package/build/components/scroll-view/scroll-view-platform.ios.js +0 -3
- package/build/components/scroll-view/scroll-view-platform.js +0 -4
- package/build/components/scroll-view/scroll-view-sticky-context.d.ts +0 -7
- package/build/components/scroll-view/scroll-view-sticky-context.js +0 -1
- package/build/components/scroll-view/sticky-header-props.d.ts +0 -10
- package/build/components/scroll-view/sticky-header-props.js +0 -1
- package/build/components/scroll-view/sticky-header.svelte +0 -287
- package/build/components/scroll-view/sticky-header.svelte.d.ts +0 -5
- package/build/components/switch/index.svelte +0 -134
- package/build/components/switch/index.svelte.d.ts +0 -5
- package/build/components/text-input/index.svelte +0 -372
- package/build/components/text-input/index.svelte.d.ts +0 -17
- package/build/components/touchable-highlight/index.svelte +0 -139
- package/build/components/touchable-highlight/index.svelte.d.ts +0 -5
- package/build/components/touchable-native-feedback/index.d.ts +0 -9
- package/build/components/touchable-native-feedback/index.js +0 -14
- package/build/components/touchable-native-feedback/touchable-native-feedback.svelte +0 -49
- package/build/components/touchable-native-feedback/touchable-native-feedback.svelte.d.ts +0 -5
- package/build/components/touchable-opacity/index.svelte +0 -181
- package/build/components/touchable-opacity/index.svelte.d.ts +0 -5
- package/build/components/touchable-without-feedback/index.svelte +0 -75
- package/build/components/touchable-without-feedback/index.svelte.d.ts +0 -5
- package/build/dom-shim/fold-host-bag.d.ts +0 -1
- package/build/dom-shim/fold-host-bag.js +0 -7
- package/build/modules/animated/animated-component-props.d.ts +0 -7
- package/build/modules/animated/animated-component-props.js +0 -1
- package/build/modules/animated/create-animated-component.d.ts +0 -5
- package/build/modules/animated/create-animated-component.js +0 -131
- package/build/preprocessor/lower-host-primitives.d.ts +0 -10
- package/build/preprocessor/lower-host-primitives.js +0 -455
- package/build/state-style.d.ts +0 -1
- package/build/state-style.js +0 -8
- /package/build/components/{activity-indicator/activity-indicator-props.js → activity-indicator-props.js} +0 -0
- /package/build/components/{image-background/image-background-props.js → image-background-props.js} +0 -0
- /package/build/{components/scroll-view/scroll-view-platform-types.js → intrinsic-elements.js} +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<script lang="ts" module>
|
|
2
2
|
// VirtualizedList: real windowing over a hand-authored minimal scroll host. Only cells whose
|
|
3
3
|
// computed offset falls inside the visible window (plus a leading/trailing buffer) render;
|
|
4
|
-
// the rest collapses into two spacer
|
|
4
|
+
// the rest collapses into two spacer view nodes.
|
|
5
5
|
//
|
|
6
6
|
// The orchestration - window recompute, edge-reached, viewability, batch fill, MVCP, imperative
|
|
7
7
|
// scrolls - is the framework-agnostic `reduceList` state machine in @symbiote-native/components
|
|
@@ -10,16 +10,22 @@
|
|
|
10
10
|
// (`listState`), runs the returned EFFECTS with Svelte primitives, and renders the windowed slice
|
|
11
11
|
// with `{#each plan.cells}` (Lists have no Descriptor render fn, per svelte-adapter-dom-shim §15).
|
|
12
12
|
//
|
|
13
|
-
// This file
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
// Android RefreshControl wrap style-split, and JS sticky-header wrapping are all wired directly
|
|
18
|
-
// here instead.
|
|
13
|
+
// This file authors the raw `scroll-view` intrinsic directly, and needs to: it walks an indexable
|
|
14
|
+
// `plan.cells` list rather than taking an opaque children Snippet, so it can mark sticky cells
|
|
15
|
+
// itself. (There is no ScrollView component to render instead — it was deleted 2026-09-10 and an
|
|
16
|
+
// app writes the tag too.)
|
|
19
17
|
//
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
//
|
|
18
|
+
// It does NOT author the CONTENT node. `registerScrollViewBehavior()` puts a `buildStructure` on
|
|
19
|
+
// the scroll tags, and exactly one thing may build `RCTScrollContentView` — emitting one here as
|
|
20
|
+
// well nests a second one inside the engine's. Everything that used to live here because the
|
|
21
|
+
// content node did (`contentContainerStyle`, the axis base style, `nestedScrollEnabled`, the
|
|
22
|
+
// Android RefreshControl wrap style-split, the JS sticky-header wrapping and its collision map)
|
|
23
|
+
// is the behavior's now; `contentContainerStyle` travels as an OWNER prop and the behavior
|
|
24
|
+
// renames it onto the slot.
|
|
25
|
+
//
|
|
26
|
+
// RefreshControl is written as an ordinary child of the scroll tag on both platforms: the
|
|
27
|
+
// behavior CLAIMS it, keeping it beside the content view on iOS and wrapping the scroll view
|
|
28
|
+
// with it on Android.
|
|
23
29
|
//
|
|
24
30
|
// `<svelte:element>` is forbidden (svelte-adapter-dom-shim skill §4/§7 - its shim surface isn't
|
|
25
31
|
// implemented), so the horizontal/vertical choice is two static branches sharing one
|
|
@@ -34,34 +40,25 @@
|
|
|
34
40
|
|
|
35
41
|
<script lang="ts" generics="ItemT">
|
|
36
42
|
import {
|
|
37
|
-
DEFAULT_END_REACHED_THRESHOLD,
|
|
38
43
|
DEFAULT_INITIAL_NUM_TO_RENDER,
|
|
39
44
|
DEFAULT_MAX_TO_RENDER_PER_BATCH,
|
|
40
|
-
DEFAULT_START_REACHED_THRESHOLD,
|
|
41
45
|
DEFAULT_UPDATE_CELLS_BATCHING_PERIOD,
|
|
42
46
|
DEFAULT_WINDOW_SIZE,
|
|
43
47
|
EMPTY_OFFSET,
|
|
44
48
|
FIRST_INDEX,
|
|
45
49
|
INVERTED_X_STYLE,
|
|
46
50
|
INVERTED_Y_STYLE,
|
|
47
|
-
attachStickyScroll,
|
|
48
51
|
buildListPlan,
|
|
49
52
|
buildScrollViewHandle,
|
|
50
53
|
buildViewabilityPairs,
|
|
51
54
|
createInitialListState,
|
|
52
|
-
forwardScrollEvent,
|
|
53
55
|
isSeparatorGapInRange,
|
|
54
56
|
listEffectSignature,
|
|
55
|
-
nextStickyHeaderY,
|
|
56
57
|
readLayoutLength,
|
|
57
58
|
readLayoutOffset,
|
|
58
|
-
readLayoutNumber,
|
|
59
59
|
readScrollOffset,
|
|
60
60
|
reduceList,
|
|
61
61
|
resolveItemKey,
|
|
62
|
-
resolveScrollForwarding,
|
|
63
|
-
selectScrollIntrinsics,
|
|
64
|
-
splitLayoutProps,
|
|
65
62
|
type IListAction,
|
|
66
63
|
type IListEffect,
|
|
67
64
|
type IListReducerInputs,
|
|
@@ -71,24 +68,11 @@
|
|
|
71
68
|
type ISeparators,
|
|
72
69
|
} from '@symbiote-native/components';
|
|
73
70
|
import {
|
|
74
|
-
AnimatedValue,
|
|
75
71
|
dlog,
|
|
76
|
-
event as animatedEvent,
|
|
77
|
-
isNativeAnimatedAvailable,
|
|
78
|
-
resolveClassName,
|
|
79
72
|
type ISymbioteEvent,
|
|
80
73
|
type ISymbioteNode,
|
|
81
74
|
} from '@symbiote-native/engine';
|
|
82
|
-
import { resolveSvelteClass } from '../../class-value';
|
|
83
|
-
import { setContext } from 'svelte';
|
|
84
75
|
import type { ShimElement } from '../../dom-shim';
|
|
85
|
-
import RefreshControl from '../RefreshControl.svelte';
|
|
86
|
-
import { PLATFORM } from '../scroll-view/scroll-view-platform';
|
|
87
|
-
import ScrollViewStickyHeader from '../scroll-view/sticky-header.svelte';
|
|
88
|
-
import {
|
|
89
|
-
SCROLL_VIEW_STICKY_CONTEXT_KEY,
|
|
90
|
-
type IScrollViewStickyContext,
|
|
91
|
-
} from '../scroll-view/scroll-view-sticky-context';
|
|
92
76
|
import {
|
|
93
77
|
pickAccessibilityProps,
|
|
94
78
|
type IVirtualizedListProps as IProps,
|
|
@@ -129,22 +113,6 @@
|
|
|
129
113
|
let viewableTimer: ReturnType<typeof setTimeout> | null = null;
|
|
130
114
|
let batchTimer: ReturnType<typeof setTimeout> | null = null;
|
|
131
115
|
|
|
132
|
-
// Sticky headers: this file hand-rolls the raw scroll intrinsic (see the header comment), so it
|
|
133
|
-
// can't lean on ScrollView.svelte's sticky wiring directly - it mirrors the same
|
|
134
|
-
// scrollAnimatedValue/attachStickyScroll/context wiring here, applied per windowed cell.
|
|
135
|
-
const scrollAnimatedValue = new AnimatedValue(0);
|
|
136
|
-
let viewportHeight = $state<number | undefined>(undefined);
|
|
137
|
-
// y of each measured sticky header, keyed by ORIGINAL list index (RN's _headerLayoutYs) - plain,
|
|
138
|
-
// not $state; stickyVersion (below) drives re-derivation of the collision math that reads it.
|
|
139
|
-
const headerLayoutYs = new Map<number, number>();
|
|
140
|
-
let stickyVersion = $state(0);
|
|
141
|
-
|
|
142
|
-
setContext<IScrollViewStickyContext>(SCROLL_VIEW_STICKY_CONTEXT_KEY, {
|
|
143
|
-
scrollAnimatedValue,
|
|
144
|
-
getInverted: () => narrowed.inverted,
|
|
145
|
-
getViewportHeight: () => viewportHeight,
|
|
146
|
-
});
|
|
147
|
-
|
|
148
116
|
const narrowed = $derived.by(() => {
|
|
149
117
|
// extraData has no field of its own; reading it tracks it so a change forces this derived to
|
|
150
118
|
// re-run (RN's extraData contract).
|
|
@@ -158,11 +126,9 @@
|
|
|
158
126
|
horizontal: props.horizontal === true,
|
|
159
127
|
inverted: props.inverted === true,
|
|
160
128
|
onEndReached: props.onEndReached,
|
|
161
|
-
onEndReachedThreshold:
|
|
162
|
-
props.onEndReachedThreshold ?? DEFAULT_END_REACHED_THRESHOLD,
|
|
129
|
+
onEndReachedThreshold: props.onEndReachedThreshold,
|
|
163
130
|
onStartReached: props.onStartReached,
|
|
164
|
-
onStartReachedThreshold:
|
|
165
|
-
props.onStartReachedThreshold ?? DEFAULT_START_REACHED_THRESHOLD,
|
|
131
|
+
onStartReachedThreshold: props.onStartReachedThreshold,
|
|
166
132
|
onRefresh: props.onRefresh,
|
|
167
133
|
refreshing: props.refreshing,
|
|
168
134
|
progressViewOffset: props.progressViewOffset,
|
|
@@ -282,7 +248,12 @@
|
|
|
282
248
|
const info = effect.info;
|
|
283
249
|
const map = effect.map;
|
|
284
250
|
const fire = (): void => {
|
|
285
|
-
for (const pair of pairs)
|
|
251
|
+
for (const pair of pairs) {
|
|
252
|
+
pair.onViewableItemsChanged({
|
|
253
|
+
...info,
|
|
254
|
+
viewabilityConfig: pair.viewabilityConfig,
|
|
255
|
+
});
|
|
256
|
+
}
|
|
286
257
|
dispatch({ kind: 'viewable-fired', map });
|
|
287
258
|
};
|
|
288
259
|
if (viewableTimer !== null) {
|
|
@@ -323,58 +294,6 @@
|
|
|
323
294
|
return listEffectSignature(listState);
|
|
324
295
|
});
|
|
325
296
|
|
|
326
|
-
const hasStickyHeaders = $derived(
|
|
327
|
-
narrowed.stickyHeaderIndices !== undefined &&
|
|
328
|
-
narrowed.stickyHeaderIndices.length > 0,
|
|
329
|
-
);
|
|
330
|
-
// Resolved dynamically, exactly like React (adapters/react/.../scroll-view/shared.ts:267). Do
|
|
331
|
-
// not hardcode this false to keep the JS listener alive - RN gates native the same way
|
|
332
|
-
// (AnimatedWithChildren.js:74 `if (!this.__isNative)`) and the pin is the native transform, not
|
|
333
|
-
// the listener, so forcing JS mode only trades away the native driver: header lag on iOS,
|
|
334
|
-
// outright failure on Android (commit debounce 15ms vs iOS's 64ms - render-scroll-sticky.ts).
|
|
335
|
-
const nativeStickyAvailable = $derived(
|
|
336
|
-
hasStickyHeaders && isNativeAnimatedAvailable(),
|
|
337
|
-
);
|
|
338
|
-
// RN's FlatList/SectionList expose no invertStickyHeaders of their own (only ScrollView does) -
|
|
339
|
-
// sticky headers always pin to the top here, matching RN.
|
|
340
|
-
const forwarding = $derived(
|
|
341
|
-
resolveScrollForwarding({
|
|
342
|
-
hasStickyHeaders,
|
|
343
|
-
nativeStickyAvailable,
|
|
344
|
-
invertStickyHeaders: undefined,
|
|
345
|
-
scrollEventThrottle: narrowed.scrollEventThrottle,
|
|
346
|
-
maintainVisibleContentPosition: narrowed.maintainVisibleContentPosition,
|
|
347
|
-
snapToAlignment: undefined,
|
|
348
|
-
}),
|
|
349
|
-
);
|
|
350
|
-
|
|
351
|
-
$effect(() => {
|
|
352
|
-
dlog(
|
|
353
|
-
`VirtualizedList sticky hasStickyHeaders=${hasStickyHeaders} nativeStickyAvailable=${nativeStickyAvailable} mode=${forwarding.mode} scrollEventThrottle=${String(forwarding.scrollEventThrottle)}`,
|
|
354
|
-
);
|
|
355
|
-
});
|
|
356
|
-
|
|
357
|
-
// Drive the sticky scroll value on the native UI thread once the scroll host commits (mirrors
|
|
358
|
-
// ScrollView.svelte). No-op without the native animated module - the JS fallback below still
|
|
359
|
-
// keeps headers pinned via onScroll.
|
|
360
|
-
$effect(() => {
|
|
361
|
-
if (!nativeStickyAvailable) {
|
|
362
|
-
dlog(
|
|
363
|
-
'VirtualizedList sticky attachStickyScroll skipped: nativeStickyAvailable=false',
|
|
364
|
-
);
|
|
365
|
-
return;
|
|
366
|
-
}
|
|
367
|
-
const node = hostShim?.engineNode;
|
|
368
|
-
if (node === undefined) {
|
|
369
|
-
dlog(
|
|
370
|
-
'VirtualizedList sticky attachStickyScroll skipped: engineNode not ready yet',
|
|
371
|
-
);
|
|
372
|
-
return;
|
|
373
|
-
}
|
|
374
|
-
dlog('VirtualizedList sticky attachStickyScroll attached');
|
|
375
|
-
return attachStickyScroll(node, scrollAnimatedValue);
|
|
376
|
-
});
|
|
377
|
-
|
|
378
297
|
function handleScroll(event: ISymbioteEvent): void {
|
|
379
298
|
const offset = readScrollOffset(event, narrowed.horizontal);
|
|
380
299
|
if (offset === undefined) return;
|
|
@@ -384,66 +303,11 @@
|
|
|
384
303
|
narrowed.userOnScroll?.(event);
|
|
385
304
|
}
|
|
386
305
|
|
|
387
|
-
// sticky-js: no native Animated module, so the scroll value is driven off the JS thread via the
|
|
388
|
-
// same wrapped-listener shape ScrollView.svelte uses. sticky-native/plain: forward untouched.
|
|
389
|
-
const onScroll = $derived.by(() => {
|
|
390
|
-
if (forwarding.mode !== 'sticky-js') return handleScroll;
|
|
391
|
-
return animatedEvent(
|
|
392
|
-
[{ nativeEvent: { contentOffset: { y: scrollAnimatedValue } } }],
|
|
393
|
-
{
|
|
394
|
-
listener: (...args: unknown[]) =>
|
|
395
|
-
forwardScrollEvent(handleScroll, args),
|
|
396
|
-
},
|
|
397
|
-
);
|
|
398
|
-
});
|
|
399
|
-
|
|
400
306
|
function onViewportLayout(event: ISymbioteEvent): void {
|
|
401
307
|
const length = readLayoutLength(event, narrowed.horizontal);
|
|
402
308
|
if (length === undefined) return;
|
|
403
309
|
dlog(`VirtualizedList onLayout viewport=${length}`);
|
|
404
310
|
dispatch({ kind: 'layout', length });
|
|
405
|
-
if (forwarding.capturesViewportHeight) {
|
|
406
|
-
const height = readLayoutNumber(event, 'height');
|
|
407
|
-
if (height !== undefined) viewportHeight = height;
|
|
408
|
-
}
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
// Records a sticky header's measured y (RN's _headerLayoutYs) so the NEXT sticky header down the
|
|
412
|
-
// list can compute its collision point - without this every header would stick indefinitely
|
|
413
|
-
// instead of being pushed off by the one behind it.
|
|
414
|
-
function recordHeaderY(index: number, event: ISymbioteEvent): void {
|
|
415
|
-
const y = readLayoutNumber(event, 'y');
|
|
416
|
-
if (y === undefined) return;
|
|
417
|
-
// This map is the ONLY source of nextHeaderLayoutY for every header ahead of this one; if a
|
|
418
|
-
// later header's onLayout never fires (windowing drops its cell) the collision math upstream
|
|
419
|
-
// falls back to `?? 0` (collisionPoint = -layoutHeight), which is almost certainly wrong.
|
|
420
|
-
dlog(`VirtualizedList recordHeaderY index=${index} y=${y}`);
|
|
421
|
-
headerLayoutYs.set(index, y);
|
|
422
|
-
stickyVersion += 1;
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
function stickyLayoutFor(index: number): (event: ISymbioteEvent) => void {
|
|
426
|
-
const measure = makeCellMeasure(index);
|
|
427
|
-
return (event: ISymbioteEvent): void => {
|
|
428
|
-
measure(event);
|
|
429
|
-
recordHeaderY(index, event);
|
|
430
|
-
};
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
// Only a header MOUNTED RIGHT NOW can collide with this one, matching React (its ScrollView
|
|
434
|
-
// receives `renderedStickyIndices`, not the full section list -
|
|
435
|
-
// adapters/react/.../virtualized-list/index.ts:732). Passing the full index list instead let a
|
|
436
|
-
// header collide against a stale y from one that had already scrolled out and unmounted,
|
|
437
|
-
// freezing it in place - proven by sticky-collision-parity.test.ts against the React reference.
|
|
438
|
-
// The map itself is deliberately NOT pruned (React doesn't either): a measured y stays valid for
|
|
439
|
-
// when that header scrolls back in.
|
|
440
|
-
function nextStickyHeaderYFor(index: number): number | undefined {
|
|
441
|
-
void stickyVersion;
|
|
442
|
-
const indices = narrowed.stickyHeaderIndices;
|
|
443
|
-
if (indices === undefined) return undefined;
|
|
444
|
-
const mounted = new Set(allCells.map(cell => cell.index));
|
|
445
|
-
const rendered = indices.filter(stickyIndex => mounted.has(stickyIndex));
|
|
446
|
-
return nextStickyHeaderY(rendered, rendered.indexOf(index), headerLayoutYs);
|
|
447
311
|
}
|
|
448
312
|
|
|
449
313
|
function makeCellMeasure(index: number): (event: ISymbioteEvent) => void {
|
|
@@ -573,10 +437,6 @@
|
|
|
573
437
|
};
|
|
574
438
|
});
|
|
575
439
|
|
|
576
|
-
const scrollViewIntrinsics = $derived(
|
|
577
|
-
selectScrollIntrinsics(narrowed.horizontal, narrowed.contentContainerStyle),
|
|
578
|
-
);
|
|
579
|
-
|
|
580
440
|
const resolvedStyle = $derived(
|
|
581
441
|
narrowed.inverted
|
|
582
442
|
? [
|
|
@@ -585,44 +445,30 @@
|
|
|
585
445
|
]
|
|
586
446
|
: narrowed.style,
|
|
587
447
|
);
|
|
448
|
+
// Travels on the OWNER: `slotProps` renames it onto the content node the behavior built, so it
|
|
449
|
+
// goes through the slot's own routeProp and inherits class resolution and style merging. The
|
|
450
|
+
// horizontal `flexDirection: 'row'` is the behavior's constant and is composed OVER this, so
|
|
451
|
+
// only the measured total width is added here.
|
|
588
452
|
const resolvedContentContainerStyle = $derived(
|
|
589
453
|
narrowed.horizontal
|
|
590
|
-
? [
|
|
591
|
-
:
|
|
592
|
-
);
|
|
593
|
-
|
|
594
|
-
// Android wrap mode only: mirrors ScrollView's own index.svelte - RN's splitLayoutProps routes
|
|
595
|
-
// LAYOUT props (margin/flex/size/position/...) onto the wrapping AndroidSwipeRefreshLayout,
|
|
596
|
-
// VISUAL props (background/padding/border/...) onto the inner scroll view, instead of leaving
|
|
597
|
-
// the wrapper unstyled (which collapses it to zero height).
|
|
598
|
-
const layoutSplit = $derived(
|
|
599
|
-
shouldWrapRefreshControl
|
|
600
|
-
? splitLayoutProps([resolveSvelteClass(narrowed.class), resolvedStyle])
|
|
601
|
-
: undefined,
|
|
454
|
+
? [narrowed.contentContainerStyle, { width: metrics.total }]
|
|
455
|
+
: narrowed.contentContainerStyle,
|
|
602
456
|
);
|
|
603
457
|
|
|
604
458
|
const outerBag = $derived.by(() => {
|
|
605
459
|
const bag: Record<string, unknown> = {
|
|
606
|
-
style:
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
horizontal: narrowed.horizontal,
|
|
611
|
-
// RN defaults nested scrolling ON (ScrollView.js `nestedScrollEnabled ?? true`). This file
|
|
612
|
-
// hand-rolls the raw scroll intrinsic instead of rendering <ScrollView>, so it never
|
|
613
|
-
// inherited that default - on Android, a list nested inside a page ScrollView never got
|
|
614
|
-
// nested-scroll gesture arbitration and only the outer page scrolled.
|
|
615
|
-
nestedScrollEnabled: true,
|
|
616
|
-
onScroll,
|
|
460
|
+
style: resolvedStyle,
|
|
461
|
+
class: narrowed.class,
|
|
462
|
+
contentContainerStyle: resolvedContentContainerStyle,
|
|
463
|
+
onScroll: handleScroll,
|
|
617
464
|
onLayout: onViewportLayout,
|
|
618
465
|
};
|
|
619
|
-
if (layoutSplit === undefined) bag.class = narrowed.class;
|
|
620
466
|
if (commandedOffset !== undefined) bag.contentOffset = commandedOffset;
|
|
621
|
-
//
|
|
622
|
-
//
|
|
623
|
-
//
|
|
624
|
-
if (
|
|
625
|
-
bag.scrollEventThrottle =
|
|
467
|
+
// The RAW prop. A sticky header raises the throttle from the behavior, once it has actually
|
|
468
|
+
// registered — folding a sticky default in here would hand the behavior a number it reads back
|
|
469
|
+
// as the app's and could never take away again.
|
|
470
|
+
if (narrowed.scrollEventThrottle !== undefined)
|
|
471
|
+
bag.scrollEventThrottle = narrowed.scrollEventThrottle;
|
|
626
472
|
if (narrowed.onScrollBeginDrag !== undefined)
|
|
627
473
|
bag.onScrollBeginDrag = narrowed.onScrollBeginDrag;
|
|
628
474
|
if (narrowed.onScrollEndDrag !== undefined)
|
|
@@ -636,8 +482,11 @@
|
|
|
636
482
|
}
|
|
637
483
|
if (narrowed.keyboardDismissMode !== undefined)
|
|
638
484
|
bag.keyboardDismissMode = narrowed.keyboardDismissMode;
|
|
639
|
-
|
|
640
|
-
|
|
485
|
+
// `stickyHeaderIndices` is deliberately NOT forwarded. The behavior honours it by numbering the
|
|
486
|
+
// owner's own PAINT children, and this list's indices are into the DATA stream — a windowed
|
|
487
|
+
// list paints a spacer, a header and a slice, so index 3 of the data is almost never paint
|
|
488
|
+
// child 3. The cells that should pin carry the `sticky-header` tag directly instead, which is
|
|
489
|
+
// the one form that survives windowing.
|
|
641
490
|
if (narrowed.maintainVisibleContentPosition !== undefined) {
|
|
642
491
|
bag.maintainVisibleContentPosition =
|
|
643
492
|
narrowed.maintainVisibleContentPosition;
|
|
@@ -659,18 +508,6 @@
|
|
|
659
508
|
progressViewOffset: narrowed.progressViewOffset,
|
|
660
509
|
};
|
|
661
510
|
});
|
|
662
|
-
// iOS: RefreshControl is a childless sibling before the content container. Android: it WRAPS the
|
|
663
|
-
// whole scroll view (an Android ScrollView accepts only one child) - same PLATFORM.
|
|
664
|
-
// refreshControlMode ScrollView's own index.svelte reads.
|
|
665
|
-
const shouldWrapRefreshControl = $derived(
|
|
666
|
-
PLATFORM.refreshControlMode === 'wrap' && refreshControlProps !== undefined,
|
|
667
|
-
);
|
|
668
|
-
|
|
669
|
-
const contentBag = $derived({
|
|
670
|
-
style: resolvedContentContainerStyle,
|
|
671
|
-
collapsable: false,
|
|
672
|
-
});
|
|
673
|
-
|
|
674
511
|
const stickySet = $derived(
|
|
675
512
|
narrowed.stickyHeaderIndices !== undefined
|
|
676
513
|
? new Set(narrowed.stickyHeaderIndices)
|
|
@@ -740,19 +577,19 @@
|
|
|
740
577
|
lets virtualized-list.smoke.test.ts still assert an exact windowed child count.
|
|
741
578
|
-->
|
|
742
579
|
{#if hasHeader}
|
|
743
|
-
<
|
|
580
|
+
<view p={{}}>
|
|
744
581
|
{@render props.header?.()}
|
|
745
|
-
</
|
|
582
|
+
</view>
|
|
746
583
|
{/if}
|
|
747
584
|
{#if metrics.count === FIRST_INDEX}
|
|
748
585
|
{#if props.empty}
|
|
749
|
-
<
|
|
586
|
+
<view p={{}}>
|
|
750
587
|
{@render props.empty()}
|
|
751
|
-
</
|
|
588
|
+
</view>
|
|
752
589
|
{/if}
|
|
753
590
|
{:else if plan}
|
|
754
591
|
{#if plan.leadingExtent > EMPTY_OFFSET}
|
|
755
|
-
<
|
|
592
|
+
<view
|
|
756
593
|
p={{
|
|
757
594
|
style: narrowed.horizontal
|
|
758
595
|
? { width: plan.leadingExtent }
|
|
@@ -762,23 +599,24 @@
|
|
|
762
599
|
{/if}
|
|
763
600
|
{#each allCells as cell (cell.key)}
|
|
764
601
|
{#if stickySet?.has(cell.index)}
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
602
|
+
<!-- The TAG, not a component: `registerScrollViewBehavior()` registers `sticky-header`
|
|
603
|
+
alongside the scroll tags, and its behavior finds this ScrollView by walking up. The
|
|
604
|
+
collision point comes from the owner's DOCUMENT order, so nothing here computes or
|
|
605
|
+
forwards an index. `onLayout` is forwarded by the behavior, not replaced. -->
|
|
606
|
+
<sticky-header p={{ onLayout: makeCellMeasure(cell.index) }}>
|
|
769
607
|
{@render props.item({
|
|
770
608
|
item: narrowed.getItem(narrowed.data, cell.index),
|
|
771
609
|
index: cell.index,
|
|
772
610
|
separators: makeSeparators(cell.index),
|
|
773
611
|
})}
|
|
774
612
|
{#if props.separator && cell.index < metrics.count - 1}
|
|
775
|
-
<
|
|
613
|
+
<view p={{}}>
|
|
776
614
|
{@render props.separator(separatorPropsFor(cell.index))}
|
|
777
|
-
</
|
|
615
|
+
</view>
|
|
778
616
|
{/if}
|
|
779
|
-
</
|
|
617
|
+
</sticky-header>
|
|
780
618
|
{:else}
|
|
781
|
-
<
|
|
619
|
+
<view
|
|
782
620
|
p={{
|
|
783
621
|
onLayout: makeCellMeasure(cell.index),
|
|
784
622
|
style: cellInvertedStyle,
|
|
@@ -790,14 +628,14 @@
|
|
|
790
628
|
separators: makeSeparators(cell.index),
|
|
791
629
|
})}
|
|
792
630
|
{#if props.separator && cell.index < metrics.count - 1}
|
|
793
|
-
<
|
|
631
|
+
<view p={{}}>
|
|
794
632
|
{@render props.separator(separatorPropsFor(cell.index))}
|
|
795
|
-
</
|
|
633
|
+
</view>
|
|
796
634
|
{/if}
|
|
797
|
-
</
|
|
635
|
+
</view>
|
|
798
636
|
{/if}
|
|
799
637
|
{#if plan.forcedStickyCell && cell.index === plan.forcedStickyCell.index && plan.gapExtent > EMPTY_OFFSET}
|
|
800
|
-
<
|
|
638
|
+
<view
|
|
801
639
|
p={{
|
|
802
640
|
style: narrowed.horizontal
|
|
803
641
|
? { width: plan.gapExtent }
|
|
@@ -807,7 +645,7 @@
|
|
|
807
645
|
{/if}
|
|
808
646
|
{/each}
|
|
809
647
|
{#if plan.trailingExtent > EMPTY_OFFSET}
|
|
810
|
-
<
|
|
648
|
+
<view
|
|
811
649
|
p={{
|
|
812
650
|
style: narrowed.horizontal
|
|
813
651
|
? { width: plan.trailingExtent }
|
|
@@ -817,49 +655,33 @@
|
|
|
817
655
|
{/if}
|
|
818
656
|
{/if}
|
|
819
657
|
{#if props.footer}
|
|
820
|
-
<
|
|
658
|
+
<view p={{}}>
|
|
821
659
|
{@render props.footer()}
|
|
822
|
-
</
|
|
660
|
+
</view>
|
|
823
661
|
{/if}
|
|
824
662
|
{/snippet}
|
|
825
663
|
|
|
826
664
|
{#snippet scrollBody()}
|
|
827
665
|
<!--
|
|
828
|
-
Same as listBody() above:
|
|
829
|
-
|
|
666
|
+
Same as listBody() above: the RefreshControl and the cells are siblings of one parent here, and
|
|
667
|
+
the whitespace between them is dropped before it reaches Fabric.
|
|
668
|
+
|
|
669
|
+
RefreshControl is written as an ordinary child on BOTH platforms — the behavior CLAIMS it, so
|
|
670
|
+
the engine keeps it beside the content view on iOS and inverts the tree on Android. Everything
|
|
671
|
+
after it lands in the content node the behavior built.
|
|
830
672
|
-->
|
|
831
|
-
{#if
|
|
832
|
-
<
|
|
833
|
-
{/if}
|
|
834
|
-
{#if narrowed.horizontal}
|
|
835
|
-
<symbiote-horizontal-scroll-content p={contentBag}>
|
|
836
|
-
{@render listBody()}
|
|
837
|
-
</symbiote-horizontal-scroll-content>
|
|
838
|
-
{:else}
|
|
839
|
-
<symbiote-scroll-content p={contentBag}>
|
|
840
|
-
{@render listBody()}
|
|
841
|
-
</symbiote-scroll-content>
|
|
673
|
+
{#if refreshControlProps !== undefined}
|
|
674
|
+
<refresh-control p={refreshControlProps} />
|
|
842
675
|
{/if}
|
|
676
|
+
{@render listBody()}
|
|
843
677
|
{/snippet}
|
|
844
678
|
|
|
845
|
-
{#if
|
|
846
|
-
<
|
|
847
|
-
{#if narrowed.horizontal}
|
|
848
|
-
<symbiote-horizontal-scroll-view p={outerBag} bind:this={hostShim}>
|
|
849
|
-
{@render scrollBody()}
|
|
850
|
-
</symbiote-horizontal-scroll-view>
|
|
851
|
-
{:else}
|
|
852
|
-
<symbiote-scroll-view p={outerBag} bind:this={hostShim}>
|
|
853
|
-
{@render scrollBody()}
|
|
854
|
-
</symbiote-scroll-view>
|
|
855
|
-
{/if}
|
|
856
|
-
</RefreshControl>
|
|
857
|
-
{:else if narrowed.horizontal}
|
|
858
|
-
<symbiote-horizontal-scroll-view p={outerBag} bind:this={hostShim}>
|
|
679
|
+
{#if narrowed.horizontal}
|
|
680
|
+
<horizontal-scroll-view p={outerBag} bind:this={hostShim}>
|
|
859
681
|
{@render scrollBody()}
|
|
860
|
-
</
|
|
682
|
+
</horizontal-scroll-view>
|
|
861
683
|
{:else}
|
|
862
|
-
<
|
|
684
|
+
<scroll-view p={outerBag} bind:this={hostShim}>
|
|
863
685
|
{@render scrollBody()}
|
|
864
|
-
</
|
|
686
|
+
</scroll-view>
|
|
865
687
|
{/if}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Snippet } from 'svelte';
|
|
2
|
-
import type
|
|
2
|
+
import { type IStyleProp, type ISymbioteEvent, type ISymbioteNode, type IViewStyle } from '@symbiote-native/engine';
|
|
3
3
|
import { type IAccessibilityProps, type IAriaProps, type ISeparatorProps, type ISeparators, type IViewabilityConfig, type IViewabilityConfigCallbackPair, type IViewableItemsChangedInfo, type IVirtualizedListHandle } from '@symbiote-native/components';
|
|
4
4
|
import type { ISvelteClassValue } from '../../class-value';
|
|
5
5
|
export type { IVirtualizedListHandle };
|
|
@@ -67,4 +67,4 @@ export interface IVirtualizedListProps<ItemT> extends IAccessibilityProps, IAria
|
|
|
67
67
|
class?: ISvelteClassValue;
|
|
68
68
|
}
|
|
69
69
|
export type { ISeparators, ISeparatorProps, ISymbioteEvent, ISymbioteNode };
|
|
70
|
-
export declare function pickAccessibilityProps<T extends IAccessibilityProps & IAriaProps>(props: T): IAccessibilityProps;
|
|
70
|
+
export declare function pickAccessibilityProps<T extends IAccessibilityProps & IAriaProps>(props: T): IAccessibilityProps & IAriaProps;
|
|
@@ -1,14 +1,18 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { ARIA_ALIAS_KEYS, } from '@symbiote-native/engine';
|
|
2
|
+
import {} from '@symbiote-native/components';
|
|
2
3
|
// Every IAccessibilityProps field, named explicitly (the object-bag convention this adapter's
|
|
3
4
|
// custom-element host tags require — svelte-adapter-dom-shim skill §3g(c) — forbids a raw
|
|
4
5
|
// `...rest` spread landing on a symbiote-* tag, so this mirrors React's `...accessibilityRest`
|
|
5
6
|
// field-by-field instead of reusing its spread). Shared by every list component (VirtualizedList's
|
|
6
7
|
// own host-bag construction AND FlatList's/VirtualizedSectionList's/SectionList's component-to-
|
|
7
8
|
// component forwarding down to VirtualizedList) so the field list lives in exactly one place.
|
|
8
|
-
// resolveAccessibilityProps
|
|
9
|
-
//
|
|
9
|
+
// It used to call `resolveAccessibilityProps` first, folding `aria-*`/`role` into their
|
|
10
|
+
// `accessibility*` twins so there were canonical names to pick BY NAME. That fold is the device's
|
|
11
|
+
// rule now, so the aria keys are picked and forwarded RAW instead (the loop at the end) and the
|
|
12
|
+
// engine folds once, at the leaf. Picking twice across a forwarding hop stays a no-op for the same
|
|
13
|
+
// reason folding twice was: a pick is idempotent.
|
|
10
14
|
export function pickAccessibilityProps(props) {
|
|
11
|
-
const resolved =
|
|
15
|
+
const resolved = props;
|
|
12
16
|
const picked = {};
|
|
13
17
|
if (resolved.testID !== undefined)
|
|
14
18
|
picked.testID = resolved.testID;
|
|
@@ -73,5 +77,21 @@ export function pickAccessibilityProps(props) {
|
|
|
73
77
|
picked.onMagicTap = resolved.onMagicTap;
|
|
74
78
|
if (resolved.onAccessibilityEscape !== undefined)
|
|
75
79
|
picked.onAccessibilityEscape = resolved.onAccessibilityEscape;
|
|
80
|
+
// THE ARIA HALF, forwarded RAW and DERIVED from the engine's own list rather than named again
|
|
81
|
+
// here. `ARIA_ALIAS_KEYS` is exported for exactly this — its comment says a second hand-written
|
|
82
|
+
// copy is what `adapter-parity-audit.md` records going stale one member at a time — so this loop
|
|
83
|
+
// gains a new alias the day the engine does.
|
|
84
|
+
//
|
|
85
|
+
// Raw, because the fold is the device's rule (`foldAriaProps`, `SymbioteFabricProps.cpp`) and it
|
|
86
|
+
// reads these names literally off the bag it commits. Folding them here would be a second
|
|
87
|
+
// implementation of it, and the one this file used to hold: `resolveAccessibilityProps`.
|
|
88
|
+
for (const key of ARIA_ALIAS_KEYS) {
|
|
89
|
+
const value = props[key];
|
|
90
|
+
// `Object.assign` with a computed key, not `picked[key] = value`: the key is a union and the
|
|
91
|
+
// value is its correlated member type, which TypeScript cannot check across a loop. This is the
|
|
92
|
+
// spelling that stays sound without an `as`.
|
|
93
|
+
if (value !== undefined)
|
|
94
|
+
Object.assign(picked, { [key]: value });
|
|
95
|
+
}
|
|
76
96
|
return picked;
|
|
77
97
|
}
|
|
@@ -55,7 +55,7 @@
|
|
|
55
55
|
// Node identity | nodes are MOVED (the old | content is re-created per TunnelOut
|
|
56
56
|
// | parent empties) |
|
|
57
57
|
import { pop, push, snippet, user_effect } from 'svelte/internal/client';
|
|
58
|
-
import { appendChild as engineAppendChild, removeChild as engineRemoveChild, dlog, isSymbioteNode, SymbioteSurface, } from '@symbiote-native/engine';
|
|
58
|
+
import { appendChild as engineAppendChild, removeChild as engineRemoveChild, componentOf, dlog, isSymbioteNode, SymbioteSurface, } from '@symbiote-native/engine';
|
|
59
59
|
import { ShimComment, ShimElement, ShimNode } from '../dom-shim/index.js';
|
|
60
60
|
// The Svelte flavour of React's `isSymbioteNode` guard, and it needs its own wording: React tells
|
|
61
61
|
// the caller to check for a forgotten `.current`, which does not exist here. A Svelte host ref is
|
|
@@ -75,7 +75,7 @@ function targetLabel(target) {
|
|
|
75
75
|
return `surface#${target.rootTag}`;
|
|
76
76
|
if (target instanceof ShimElement)
|
|
77
77
|
return target.tagName;
|
|
78
|
-
return target
|
|
78
|
+
return componentOf(target);
|
|
79
79
|
}
|
|
80
80
|
// Attach the fragment host under `target` and hand back the matching detach. Three branches
|
|
81
81
|
// because the three target kinds sit at different layers:
|