@symbiote-native/svelte 0.1.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/LICENSE +21 -0
- package/README.md +161 -0
- package/build/bootstrap.d.ts +7 -0
- package/build/bootstrap.js +17 -0
- package/build/class-value.d.ts +6 -0
- package/build/class-value.js +81 -0
- package/build/components/RefreshControl.svelte +38 -0
- package/build/components/RefreshControl.svelte.d.ts +5 -0
- package/build/components/SafeAreaView.svelte +34 -0
- package/build/components/SafeAreaView.svelte.d.ts +5 -0
- package/build/components/Text.svelte +25 -0
- package/build/components/Text.svelte.d.ts +5 -0
- package/build/components/View.svelte +41 -0
- package/build/components/View.svelte.d.ts +5 -0
- package/build/components/activity-indicator/activity-indicator-platform.android.d.ts +3 -0
- package/build/components/activity-indicator/activity-indicator-platform.android.js +5 -0
- package/build/components/activity-indicator/activity-indicator-platform.d.ts +1 -0
- package/build/components/activity-indicator/activity-indicator-platform.ios.d.ts +3 -0
- package/build/components/activity-indicator/activity-indicator-platform.ios.js +6 -0
- package/build/components/activity-indicator/activity-indicator-platform.js +5 -0
- package/build/components/activity-indicator/activity-indicator-props.d.ts +5 -0
- package/build/components/activity-indicator/activity-indicator-props.js +1 -0
- package/build/components/activity-indicator/index.svelte +57 -0
- package/build/components/activity-indicator/index.svelte.d.ts +5 -0
- package/build/components/button-props.d.ts +5 -0
- package/build/components/button-props.js +1 -0
- package/build/components/button.svelte +71 -0
- package/build/components/button.svelte.d.ts +5 -0
- package/build/components/flat-list/flat-list-props.d.ts +10 -0
- package/build/components/flat-list/flat-list-props.js +1 -0
- package/build/components/flat-list/index.svelte +254 -0
- package/build/components/flat-list/index.svelte.d.ts +79 -0
- package/build/components/image/image-logic.d.ts +2 -0
- package/build/components/image/image-logic.js +125 -0
- package/build/components/image/image-props.d.ts +5 -0
- package/build/components/image/image-props.js +1 -0
- package/build/components/image/index.d.ts +3 -0
- package/build/components/image/index.js +10 -0
- package/build/components/image/index.svelte +66 -0
- package/build/components/image/index.svelte.d.ts +5 -0
- package/build/components/image-background/image-background-props.d.ts +10 -0
- package/build/components/image-background/image-background-props.js +1 -0
- package/build/components/image-background/index.svelte +81 -0
- package/build/components/image-background/index.svelte.d.ts +5 -0
- package/build/components/index.d.ts +50 -0
- package/build/components/index.js +24 -0
- package/build/components/input-accessory-view/index.svelte +48 -0
- package/build/components/input-accessory-view/index.svelte.d.ts +5 -0
- package/build/components/input-accessory-view/input-accessory-view-props.d.ts +11 -0
- package/build/components/input-accessory-view/input-accessory-view-props.js +1 -0
- package/build/components/keyboard-avoiding-view/index.svelte +135 -0
- package/build/components/keyboard-avoiding-view/index.svelte.d.ts +5 -0
- package/build/components/keyboard-avoiding-view/keyboard-avoiding-view-props.d.ts +15 -0
- package/build/components/keyboard-avoiding-view/keyboard-avoiding-view-props.js +1 -0
- package/build/components/modal/index.svelte +126 -0
- package/build/components/modal/index.svelte.d.ts +5 -0
- package/build/components/modal/modal-props.d.ts +24 -0
- package/build/components/modal/modal-props.js +1 -0
- package/build/components/pressable/index.svelte +160 -0
- package/build/components/pressable/index.svelte.d.ts +5 -0
- package/build/components/pressable/pressable-props.d.ts +27 -0
- package/build/components/pressable/pressable-props.js +1 -0
- package/build/components/refresh-control-props.d.ts +19 -0
- package/build/components/refresh-control-props.js +1 -0
- package/build/components/safe-area-view-props.d.ts +10 -0
- package/build/components/safe-area-view-props.js +1 -0
- package/build/components/scroll-view/index.svelte +298 -0
- package/build/components/scroll-view/index.svelte.d.ts +18 -0
- package/build/components/scroll-view/scroll-view-platform-types.d.ts +3 -0
- package/build/components/scroll-view/scroll-view-platform-types.js +1 -0
- package/build/components/scroll-view/scroll-view-platform.android.d.ts +3 -0
- package/build/components/scroll-view/scroll-view-platform.android.js +3 -0
- package/build/components/scroll-view/scroll-view-platform.d.ts +1 -0
- package/build/components/scroll-view/scroll-view-platform.ios.d.ts +3 -0
- package/build/components/scroll-view/scroll-view-platform.ios.js +3 -0
- package/build/components/scroll-view/scroll-view-platform.js +4 -0
- package/build/components/scroll-view/scroll-view-props.d.ts +78 -0
- package/build/components/scroll-view/scroll-view-props.js +1 -0
- package/build/components/scroll-view/scroll-view-sticky-context.d.ts +7 -0
- package/build/components/scroll-view/scroll-view-sticky-context.js +1 -0
- package/build/components/scroll-view/sticky-header-props.d.ts +10 -0
- package/build/components/scroll-view/sticky-header-props.js +1 -0
- package/build/components/scroll-view/sticky-header.svelte +254 -0
- package/build/components/scroll-view/sticky-header.svelte.d.ts +5 -0
- package/build/components/section-list/index.svelte +107 -0
- package/build/components/section-list/index.svelte.d.ts +57 -0
- package/build/components/section-list/section-list-props.d.ts +1 -0
- package/build/components/section-list/section-list-props.js +1 -0
- package/build/components/switch/index.svelte +123 -0
- package/build/components/switch/index.svelte.d.ts +5 -0
- package/build/components/switch/switch-platform-types.d.ts +5 -0
- package/build/components/switch/switch-platform-types.js +1 -0
- package/build/components/switch/switch-platform.android.d.ts +3 -0
- package/build/components/switch/switch-platform.android.js +8 -0
- package/build/components/switch/switch-platform.d.ts +1 -0
- package/build/components/switch/switch-platform.ios.d.ts +3 -0
- package/build/components/switch/switch-platform.ios.js +7 -0
- package/build/components/switch/switch-platform.js +5 -0
- package/build/components/switch/switch-props.d.ts +5 -0
- package/build/components/switch/switch-props.js +1 -0
- package/build/components/text-input/index.svelte +300 -0
- package/build/components/text-input/index.svelte.d.ts +12 -0
- package/build/components/text-input/text-input-props.d.ts +5 -0
- package/build/components/text-input/text-input-props.js +1 -0
- package/build/components/text-props.d.ts +23 -0
- package/build/components/text-props.js +1 -0
- package/build/components/touchable-base-props.d.ts +10 -0
- package/build/components/touchable-base-props.js +1 -0
- package/build/components/touchable-highlight/index.svelte +39 -0
- package/build/components/touchable-highlight/index.svelte.d.ts +5 -0
- package/build/components/touchable-highlight/touchable-highlight-props.d.ts +5 -0
- package/build/components/touchable-highlight/touchable-highlight-props.js +1 -0
- package/build/components/touchable-native-feedback/index.d.ts +9 -0
- package/build/components/touchable-native-feedback/index.js +14 -0
- package/build/components/touchable-native-feedback/touchable-native-feedback-props.d.ts +8 -0
- package/build/components/touchable-native-feedback/touchable-native-feedback-props.js +1 -0
- package/build/components/touchable-native-feedback/touchable-native-feedback.svelte +44 -0
- package/build/components/touchable-native-feedback/touchable-native-feedback.svelte.d.ts +5 -0
- package/build/components/touchable-opacity/index.svelte +124 -0
- package/build/components/touchable-opacity/index.svelte.d.ts +5 -0
- package/build/components/touchable-opacity/touchable-opacity-props.d.ts +5 -0
- package/build/components/touchable-opacity/touchable-opacity-props.js +1 -0
- package/build/components/touchable-without-feedback/index.svelte +19 -0
- package/build/components/touchable-without-feedback/index.svelte.d.ts +5 -0
- package/build/components/touchable-without-feedback/touchable-without-feedback-props.d.ts +2 -0
- package/build/components/touchable-without-feedback/touchable-without-feedback-props.js +1 -0
- package/build/components/view-props.d.ts +29 -0
- package/build/components/view-props.js +1 -0
- package/build/components/virtualized-list/index.svelte +702 -0
- package/build/components/virtualized-list/index.svelte.d.ts +79 -0
- package/build/components/virtualized-list/virtualized-list-props.d.ts +70 -0
- package/build/components/virtualized-list/virtualized-list-props.js +73 -0
- package/build/components/virtualized-section-list/index.svelte +206 -0
- package/build/components/virtualized-section-list/index.svelte.d.ts +57 -0
- package/build/components/virtualized-section-list/virtualized-section-list-props.d.ts +62 -0
- package/build/components/virtualized-section-list/virtualized-section-list-props.js +1 -0
- package/build/create-tunnel/index.d.ts +3 -0
- package/build/create-tunnel/index.js +3 -0
- package/build/create-tunnel/tunnel-in.svelte +24 -0
- package/build/create-tunnel/tunnel-in.svelte.d.ts +9 -0
- package/build/create-tunnel/tunnel-out.svelte +14 -0
- package/build/create-tunnel/tunnel-out.svelte.d.ts +7 -0
- package/build/create-tunnel/tunnel.d.ts +9 -0
- package/build/create-tunnel/tunnel.js +9 -0
- package/build/descriptor-to-svelte.d.ts +7 -0
- package/build/descriptor-to-svelte.js +104 -0
- package/build/dom-shim/comment.d.ts +9 -0
- package/build/dom-shim/comment.js +24 -0
- package/build/dom-shim/document-fragment.d.ts +8 -0
- package/build/dom-shim/document-fragment.js +23 -0
- package/build/dom-shim/document.d.ts +23 -0
- package/build/dom-shim/document.js +61 -0
- package/build/dom-shim/element.d.ts +22 -0
- package/build/dom-shim/element.js +130 -0
- package/build/dom-shim/index.d.ts +5 -0
- package/build/dom-shim/index.js +7 -0
- package/build/dom-shim/patch-globals.d.ts +2 -0
- package/build/dom-shim/patch-globals.js +94 -0
- package/build/dom-shim/shim-node.d.ts +30 -0
- package/build/dom-shim/shim-node.js +207 -0
- package/build/dom-shim/text.d.ts +14 -0
- package/build/dom-shim/text.js +97 -0
- package/build/host-instance.d.ts +5 -0
- package/build/host-instance.js +48 -0
- package/build/index.d.ts +28 -0
- package/build/index.js +77 -0
- package/build/modules/accessibility-info/index.android.d.ts +2 -0
- package/build/modules/accessibility-info/index.android.js +4 -0
- package/build/modules/accessibility-info/index.d.ts +1 -0
- package/build/modules/accessibility-info/index.ios.d.ts +2 -0
- package/build/modules/accessibility-info/index.ios.js +3 -0
- package/build/modules/accessibility-info/index.js +5 -0
- package/build/modules/animated/AnimatedImage.svelte +115 -0
- package/build/modules/animated/AnimatedImage.svelte.d.ts +5 -0
- package/build/modules/animated/AnimatedScrollView.svelte +92 -0
- package/build/modules/animated/AnimatedScrollView.svelte.d.ts +17 -0
- package/build/modules/animated/AnimatedText.svelte +58 -0
- package/build/modules/animated/AnimatedText.svelte.d.ts +5 -0
- package/build/modules/animated/AnimatedView.svelte +95 -0
- package/build/modules/animated/AnimatedView.svelte.d.ts +5 -0
- package/build/modules/animated/animated-component-props.d.ts +7 -0
- package/build/modules/animated/animated-component-props.js +1 -0
- package/build/modules/animated/index.d.ts +58 -0
- package/build/modules/animated/index.js +69 -0
- package/build/modules/app-registry/index.d.ts +7 -0
- package/build/modules/app-registry/index.js +28 -0
- package/build/modules/status-bar/index.d.ts +5 -0
- package/build/modules/status-bar/index.js +21 -0
- package/build/modules/status-bar/index.svelte +20 -0
- package/build/modules/status-bar/index.svelte.d.ts +4 -0
- package/build/native-view-bridge.d.ts +4 -0
- package/build/native-view-bridge.js +27 -0
- package/build/preprocessor/forbid-web-only-constructs.d.ts +8 -0
- package/build/preprocessor/forbid-web-only-constructs.js +204 -0
- package/build/preprocessor/scoped-styles.d.ts +10 -0
- package/build/preprocessor/scoped-styles.js +399 -0
- package/build/render.d.ts +7 -0
- package/build/render.js +46 -0
- package/build/root-element.d.ts +3 -0
- package/build/root-element.js +27 -0
- package/build/runes/attachments.d.ts +5 -0
- package/build/runes/attachments.js +73 -0
- package/build/runes/dimensions-value.d.ts +4 -0
- package/build/runes/dimensions-value.js +30 -0
- package/build/runes/media-query.d.ts +8 -0
- package/build/runes/media-query.js +38 -0
- package/build/runes/use-color-scheme.svelte.d.ts +4 -0
- package/build/runes/use-color-scheme.svelte.js +25 -0
- package/build/runes/use-window-dimensions.svelte.d.ts +4 -0
- package/build/runes/use-window-dimensions.svelte.js +43 -0
- package/build/runes/window.d.ts +6 -0
- package/build/runes/window.js +28 -0
- package/build/scope-token.d.ts +2 -0
- package/build/scope-token.js +33 -0
- package/build/style-scope.d.ts +1 -0
- package/build/style-scope.js +29 -0
- package/metro-css-parser.cjs +9 -0
- package/metro-svelte-transformer.cjs +156 -0
- package/package.json +83 -0
|
@@ -0,0 +1,702 @@
|
|
|
1
|
+
<script lang="ts" module>
|
|
2
|
+
// VirtualizedList: real windowing over a hand-authored minimal scroll host. Only cells whose
|
|
3
|
+
// computed offset falls inside the visible window (plus a leading/trailing buffer) render;
|
|
4
|
+
// the rest collapses into two spacer symbiote-view nodes.
|
|
5
|
+
//
|
|
6
|
+
// The orchestration - window recompute, edge-reached, viewability, batch fill, MVCP, imperative
|
|
7
|
+
// scrolls - is the framework-agnostic `reduceList` state machine in @symbiote-native/components
|
|
8
|
+
// (state/virtualized-list-reducer), shared verbatim with React/Vue. This file supplies only
|
|
9
|
+
// Svelte's lifecycle: turns native events into ACTIONS, holds one plain (non-reactive) state cell
|
|
10
|
+
// (`listState`), runs the returned EFFECTS with Svelte primitives, and renders the windowed slice
|
|
11
|
+
// with `{#each plan.cells}` (Lists have no Descriptor render fn, per svelte-adapter-dom-shim §15).
|
|
12
|
+
//
|
|
13
|
+
// This file hand-authors the raw `symbiote-scroll-view`/`symbiote-scroll-content` intrinsics
|
|
14
|
+
// directly rather than rendering <ScrollView>: unlike ScrollView.svelte, it walks an indexable
|
|
15
|
+
// `plan.cells` list instead of an opaque children Snippet, so it can auto-wrap sticky cells
|
|
16
|
+
// itself. `onContentSizeChange` remains the one gap left from that; nestedScrollEnabled, the
|
|
17
|
+
// Android RefreshControl wrap style-split, and JS sticky-header wrapping are all wired directly
|
|
18
|
+
// here instead.
|
|
19
|
+
//
|
|
20
|
+
// RefreshControl attaches directly to these raw scroll intrinsics: a sibling before the content
|
|
21
|
+
// on iOS, wrapping the whole scroll view on Android (PLATFORM.refreshControlMode) - the same
|
|
22
|
+
// shape ScrollView's own index.svelte uses.
|
|
23
|
+
//
|
|
24
|
+
// `<svelte:element>` is forbidden (svelte-adapter-dom-shim skill §4/§7 - its shim surface isn't
|
|
25
|
+
// implemented), so the horizontal/vertical choice is two static branches sharing one
|
|
26
|
+
// `{#snippet listBody()}`, not a dynamic tag.
|
|
27
|
+
import type { IVirtualizedListProps, IVirtualizedListHandle } from './virtualized-list-props';
|
|
28
|
+
|
|
29
|
+
export type { IVirtualizedListProps, IVirtualizedListHandle };
|
|
30
|
+
</script>
|
|
31
|
+
|
|
32
|
+
<script lang="ts" generics="ItemT">
|
|
33
|
+
import {
|
|
34
|
+
DEFAULT_END_REACHED_THRESHOLD,
|
|
35
|
+
DEFAULT_INITIAL_NUM_TO_RENDER,
|
|
36
|
+
DEFAULT_MAX_TO_RENDER_PER_BATCH,
|
|
37
|
+
DEFAULT_START_REACHED_THRESHOLD,
|
|
38
|
+
DEFAULT_UPDATE_CELLS_BATCHING_PERIOD,
|
|
39
|
+
DEFAULT_WINDOW_SIZE,
|
|
40
|
+
EMPTY_OFFSET,
|
|
41
|
+
FIRST_INDEX,
|
|
42
|
+
INVERTED_X_STYLE,
|
|
43
|
+
INVERTED_Y_STYLE,
|
|
44
|
+
attachStickyScroll,
|
|
45
|
+
buildListPlan,
|
|
46
|
+
buildScrollViewHandle,
|
|
47
|
+
buildViewabilityPairs,
|
|
48
|
+
createInitialListState,
|
|
49
|
+
forwardScrollEvent,
|
|
50
|
+
isSeparatorGapInRange,
|
|
51
|
+
listEffectSignature,
|
|
52
|
+
nextStickyHeaderY,
|
|
53
|
+
readLayoutLength,
|
|
54
|
+
readLayoutNumber,
|
|
55
|
+
readScrollOffset,
|
|
56
|
+
reduceList,
|
|
57
|
+
resolveItemKey,
|
|
58
|
+
resolveScrollForwarding,
|
|
59
|
+
selectScrollIntrinsics,
|
|
60
|
+
splitLayoutProps,
|
|
61
|
+
type IListAction,
|
|
62
|
+
type IListEffect,
|
|
63
|
+
type IListReducerInputs,
|
|
64
|
+
type IListState,
|
|
65
|
+
type IScrollViewHandle,
|
|
66
|
+
type ISeparatorProps,
|
|
67
|
+
type ISeparators,
|
|
68
|
+
} from '@symbiote-native/components';
|
|
69
|
+
import {
|
|
70
|
+
AnimatedValue,
|
|
71
|
+
dlog,
|
|
72
|
+
event as animatedEvent,
|
|
73
|
+
isNativeAnimatedAvailable,
|
|
74
|
+
resolveClassName,
|
|
75
|
+
type ISymbioteEvent,
|
|
76
|
+
type ISymbioteNode,
|
|
77
|
+
} from '@symbiote-native/engine';
|
|
78
|
+
import { resolveSvelteClass } from '../../class-value';
|
|
79
|
+
import { setContext } from 'svelte';
|
|
80
|
+
import type { ShimElement } from '../../dom-shim';
|
|
81
|
+
import RefreshControl from '../RefreshControl.svelte';
|
|
82
|
+
import { PLATFORM } from '../scroll-view/scroll-view-platform';
|
|
83
|
+
import ScrollViewStickyHeader from '../scroll-view/sticky-header.svelte';
|
|
84
|
+
import {
|
|
85
|
+
SCROLL_VIEW_STICKY_CONTEXT_KEY,
|
|
86
|
+
type IScrollViewStickyContext,
|
|
87
|
+
} from '../scroll-view/scroll-view-sticky-context';
|
|
88
|
+
import { pickAccessibilityProps, type IVirtualizedListProps as IProps } from './virtualized-list-props';
|
|
89
|
+
import { createAttachmentsSync } from '../../runes/attachments';
|
|
90
|
+
|
|
91
|
+
let props: IProps<ItemT> = $props();
|
|
92
|
+
|
|
93
|
+
// $state.raw, NOT $state: holds the shim element by IDENTITY (same concern as Switch's
|
|
94
|
+
// hostShim / Vue's shallowRef scrollHandle). dispatchViewCommand reads `.engineNode` off the RAW
|
|
95
|
+
// ShimElement the engine's WeakMap mirror actually knows about.
|
|
96
|
+
let hostShim = $state.raw<ShimElement | null>(null);
|
|
97
|
+
|
|
98
|
+
// See View.svelte's note on `{@attach}` - bound to the scroll host, the node a caller
|
|
99
|
+
// means by "the list" (the same node getScrollNode()/scrollTo drive).
|
|
100
|
+
const syncAttachments = createAttachmentsSync();
|
|
101
|
+
$effect(() => {
|
|
102
|
+
syncAttachments(hostShim, props);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
// Offset we're imperatively driving native to before hostShim's engine node is live. Fresh
|
|
106
|
+
// object identity each push so the commit path re-applies it even when the value repeats.
|
|
107
|
+
let commandedOffset = $state.raw<{ x: number; y: number } | undefined>(undefined);
|
|
108
|
+
// Bumped on a render-relevant change so `metrics` re-runs - listState is a PLAIN object
|
|
109
|
+
// (not $state), mutating it triggers nothing on its own.
|
|
110
|
+
let version = $state(0);
|
|
111
|
+
let separatorVersion = $state(0);
|
|
112
|
+
|
|
113
|
+
const scrollHandle: IScrollViewHandle = buildScrollViewHandle(() => hostShim?.engineNode ?? null);
|
|
114
|
+
|
|
115
|
+
// The one folded state cell — the Svelte twin of Vue's plain listState / React's stateRef.
|
|
116
|
+
const listState: IListState<ItemT> = createInitialListState<ItemT>();
|
|
117
|
+
const separatorOverrides = new Map<number, Partial<ISeparatorProps<ItemT>>>();
|
|
118
|
+
let viewableTimer: ReturnType<typeof setTimeout> | null = null;
|
|
119
|
+
let batchTimer: ReturnType<typeof setTimeout> | null = null;
|
|
120
|
+
|
|
121
|
+
// Sticky headers: this file hand-rolls the raw scroll intrinsic (see the header comment), so it
|
|
122
|
+
// can't lean on ScrollView.svelte's sticky wiring directly - it mirrors the same
|
|
123
|
+
// scrollAnimatedValue/attachStickyScroll/context wiring here, applied per windowed cell.
|
|
124
|
+
const scrollAnimatedValue = new AnimatedValue(0);
|
|
125
|
+
let viewportHeight = $state<number | undefined>(undefined);
|
|
126
|
+
// y of each measured sticky header, keyed by ORIGINAL list index (RN's _headerLayoutYs) - plain,
|
|
127
|
+
// not $state; stickyVersion (below) drives re-derivation of the collision math that reads it.
|
|
128
|
+
const headerLayoutYs = new Map<number, number>();
|
|
129
|
+
let stickyVersion = $state(0);
|
|
130
|
+
|
|
131
|
+
setContext<IScrollViewStickyContext>(SCROLL_VIEW_STICKY_CONTEXT_KEY, {
|
|
132
|
+
scrollAnimatedValue,
|
|
133
|
+
getInverted: () => narrowed.inverted,
|
|
134
|
+
getViewportHeight: () => viewportHeight,
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
const narrowed = $derived.by(() => {
|
|
138
|
+
// extraData has no field of its own; reading it tracks it so a change forces this derived to
|
|
139
|
+
// re-run (RN's extraData contract).
|
|
140
|
+
void props.extraData;
|
|
141
|
+
return {
|
|
142
|
+
data: props.data,
|
|
143
|
+
getItem: props.getItem,
|
|
144
|
+
getItemCount: props.getItemCount,
|
|
145
|
+
keyExtractor: props.keyExtractor,
|
|
146
|
+
getItemLayout: props.getItemLayout,
|
|
147
|
+
horizontal: props.horizontal === true,
|
|
148
|
+
inverted: props.inverted === true,
|
|
149
|
+
onEndReached: props.onEndReached,
|
|
150
|
+
onEndReachedThreshold: props.onEndReachedThreshold ?? DEFAULT_END_REACHED_THRESHOLD,
|
|
151
|
+
onStartReached: props.onStartReached,
|
|
152
|
+
onStartReachedThreshold: props.onStartReachedThreshold ?? DEFAULT_START_REACHED_THRESHOLD,
|
|
153
|
+
onRefresh: props.onRefresh,
|
|
154
|
+
refreshing: props.refreshing,
|
|
155
|
+
progressViewOffset: props.progressViewOffset,
|
|
156
|
+
onViewableItemsChanged: props.onViewableItemsChanged,
|
|
157
|
+
viewabilityConfig: props.viewabilityConfig,
|
|
158
|
+
viewabilityConfigCallbackPairs: props.viewabilityConfigCallbackPairs,
|
|
159
|
+
onScrollToIndexFailed: props.onScrollToIndexFailed,
|
|
160
|
+
initialNumToRender: props.initialNumToRender ?? DEFAULT_INITIAL_NUM_TO_RENDER,
|
|
161
|
+
initialScrollIndex: props.initialScrollIndex,
|
|
162
|
+
maxToRenderPerBatch: props.maxToRenderPerBatch ?? DEFAULT_MAX_TO_RENDER_PER_BATCH,
|
|
163
|
+
updateCellsBatchingPeriod:
|
|
164
|
+
props.updateCellsBatchingPeriod ?? DEFAULT_UPDATE_CELLS_BATCHING_PERIOD,
|
|
165
|
+
windowSize: props.windowSize ?? DEFAULT_WINDOW_SIZE,
|
|
166
|
+
stickyHeaderIndices: props.stickyHeaderIndices,
|
|
167
|
+
maintainVisibleContentPosition: props.maintainVisibleContentPosition,
|
|
168
|
+
userOnScroll: props.onScroll,
|
|
169
|
+
onScrollBeginDrag: props.onScrollBeginDrag,
|
|
170
|
+
onScrollEndDrag: props.onScrollEndDrag,
|
|
171
|
+
onMomentumScrollBegin: props.onMomentumScrollBegin,
|
|
172
|
+
onMomentumScrollEnd: props.onMomentumScrollEnd,
|
|
173
|
+
scrollEventThrottle: props.scrollEventThrottle,
|
|
174
|
+
keyboardShouldPersistTaps: props.keyboardShouldPersistTaps,
|
|
175
|
+
keyboardDismissMode: props.keyboardDismissMode,
|
|
176
|
+
style: props.style,
|
|
177
|
+
contentContainerStyle: props.contentContainerStyle,
|
|
178
|
+
class: props.class,
|
|
179
|
+
};
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
function buildInputs(): IListReducerInputs<ItemT> {
|
|
183
|
+
const p = narrowed;
|
|
184
|
+
return {
|
|
185
|
+
data: p.data,
|
|
186
|
+
getItem: p.getItem,
|
|
187
|
+
getItemCount: p.getItemCount,
|
|
188
|
+
keyExtractor: p.keyExtractor,
|
|
189
|
+
getItemLayout: p.getItemLayout,
|
|
190
|
+
horizontal: p.horizontal,
|
|
191
|
+
windowSize: p.windowSize,
|
|
192
|
+
initialNumToRender: p.initialNumToRender,
|
|
193
|
+
maxToRenderPerBatch: p.maxToRenderPerBatch,
|
|
194
|
+
updateCellsBatchingPeriod: p.updateCellsBatchingPeriod,
|
|
195
|
+
onEndReachedThreshold: p.onEndReachedThreshold,
|
|
196
|
+
onStartReachedThreshold: p.onStartReachedThreshold,
|
|
197
|
+
onEndReachedActive: p.onEndReached !== undefined,
|
|
198
|
+
onStartReachedActive: p.onStartReached !== undefined,
|
|
199
|
+
viewabilityPairs: buildViewabilityPairs(
|
|
200
|
+
p.onViewableItemsChanged,
|
|
201
|
+
p.viewabilityConfig,
|
|
202
|
+
p.viewabilityConfigCallbackPairs,
|
|
203
|
+
),
|
|
204
|
+
maintainVisibleContentPosition: p.maintainVisibleContentPosition,
|
|
205
|
+
initialScrollIndex: p.initialScrollIndex,
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function keyFor(index: number): string {
|
|
210
|
+
return resolveItemKey(narrowed.getItem(narrowed.data, index), index, narrowed.keyExtractor);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function scrollToPixel(offset: number, animated: boolean): void {
|
|
214
|
+
const clamped = Math.max(EMPTY_OFFSET, offset);
|
|
215
|
+
const target = narrowed.horizontal
|
|
216
|
+
? { x: clamped, y: EMPTY_OFFSET }
|
|
217
|
+
: { x: EMPTY_OFFSET, y: clamped };
|
|
218
|
+
if (hostShim?.engineNode === undefined) {
|
|
219
|
+
dlog(`VirtualizedList scrollTo offset=${clamped} pending-ref`);
|
|
220
|
+
commandedOffset = target;
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
commandedOffset = undefined;
|
|
224
|
+
dlog(`VirtualizedList scrollTo offset=${clamped} animated=${animated} (horizontal=${narrowed.horizontal})`);
|
|
225
|
+
scrollHandle.scrollTo({ x: target.x, y: target.y, animated });
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function runEffects(effects: IListEffect<ItemT>[], inputs: IListReducerInputs<ItemT>): void {
|
|
229
|
+
const p = narrowed;
|
|
230
|
+
for (const effect of effects) {
|
|
231
|
+
switch (effect.kind) {
|
|
232
|
+
case 'scroll-to':
|
|
233
|
+
scrollToPixel(effect.offset, effect.animated);
|
|
234
|
+
break;
|
|
235
|
+
case 'fire-end-reached':
|
|
236
|
+
p.onEndReached?.({ distanceFromEnd: effect.distanceFromEnd });
|
|
237
|
+
break;
|
|
238
|
+
case 'fire-start-reached':
|
|
239
|
+
p.onStartReached?.({ distanceFromStart: effect.distanceFromStart });
|
|
240
|
+
break;
|
|
241
|
+
case 'fire-scroll-to-index-failed':
|
|
242
|
+
p.onScrollToIndexFailed?.({
|
|
243
|
+
index: effect.index,
|
|
244
|
+
highestMeasuredFrameIndex: effect.highestMeasuredFrameIndex,
|
|
245
|
+
averageItemLength: effect.averageItemLength,
|
|
246
|
+
});
|
|
247
|
+
break;
|
|
248
|
+
case 'schedule-refill': {
|
|
249
|
+
if (batchTimer !== null) clearTimeout(batchTimer);
|
|
250
|
+
batchTimer = setTimeout(() => {
|
|
251
|
+
batchTimer = null;
|
|
252
|
+
dispatch({ kind: 'batch-tick' });
|
|
253
|
+
}, effect.delay);
|
|
254
|
+
break;
|
|
255
|
+
}
|
|
256
|
+
case 'fire-viewable': {
|
|
257
|
+
const pairs = inputs.viewabilityPairs;
|
|
258
|
+
const info = effect.info;
|
|
259
|
+
const map = effect.map;
|
|
260
|
+
const fire = (): void => {
|
|
261
|
+
for (const pair of pairs) pair.onViewableItemsChanged(info);
|
|
262
|
+
dispatch({ kind: 'viewable-fired', map });
|
|
263
|
+
};
|
|
264
|
+
if (viewableTimer !== null) {
|
|
265
|
+
clearTimeout(viewableTimer);
|
|
266
|
+
viewableTimer = null;
|
|
267
|
+
}
|
|
268
|
+
if (effect.delay > EMPTY_OFFSET) {
|
|
269
|
+
viewableTimer = setTimeout(() => {
|
|
270
|
+
viewableTimer = null;
|
|
271
|
+
fire();
|
|
272
|
+
}, effect.delay);
|
|
273
|
+
} else {
|
|
274
|
+
fire();
|
|
275
|
+
}
|
|
276
|
+
break;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function dispatch(action: IListAction<ItemT>): void {
|
|
283
|
+
const inputs = buildInputs();
|
|
284
|
+
const result = reduceList(listState, action, inputs);
|
|
285
|
+
runEffects(result.effects, inputs);
|
|
286
|
+
if (result.changed) version += 1;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// The window is recomputed exactly once here (refresh-metrics), cached until `version` or
|
|
290
|
+
// `narrowed` change - the Svelte twin of Vue's `metrics` computed.
|
|
291
|
+
const metrics = $derived.by(() => {
|
|
292
|
+
void version;
|
|
293
|
+
reduceList(listState, { kind: 'refresh-metrics' }, buildInputs());
|
|
294
|
+
return listState.metrics;
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
const commitSignature = $derived.by(() => {
|
|
298
|
+
void metrics;
|
|
299
|
+
return listEffectSignature(listState);
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
const hasStickyHeaders = $derived(
|
|
303
|
+
narrowed.stickyHeaderIndices !== undefined && narrowed.stickyHeaderIndices.length > 0,
|
|
304
|
+
);
|
|
305
|
+
// Resolved dynamically, exactly like React (adapters/react/.../scroll-view/shared.ts:267). Do
|
|
306
|
+
// not hardcode this false to keep the JS listener alive - RN gates native the same way
|
|
307
|
+
// (AnimatedWithChildren.js:74 `if (!this.__isNative)`) and the pin is the native transform, not
|
|
308
|
+
// the listener, so forcing JS mode only trades away the native driver: header lag on iOS,
|
|
309
|
+
// outright failure on Android (commit debounce 15ms vs iOS's 64ms - render-scroll-sticky.ts).
|
|
310
|
+
const nativeStickyAvailable = $derived(hasStickyHeaders && isNativeAnimatedAvailable());
|
|
311
|
+
// RN's FlatList/SectionList expose no invertStickyHeaders of their own (only ScrollView does) -
|
|
312
|
+
// sticky headers always pin to the top here, matching RN.
|
|
313
|
+
const forwarding = $derived(
|
|
314
|
+
resolveScrollForwarding({
|
|
315
|
+
hasStickyHeaders,
|
|
316
|
+
nativeStickyAvailable,
|
|
317
|
+
invertStickyHeaders: undefined,
|
|
318
|
+
scrollEventThrottle: narrowed.scrollEventThrottle,
|
|
319
|
+
maintainVisibleContentPosition: narrowed.maintainVisibleContentPosition,
|
|
320
|
+
snapToAlignment: undefined,
|
|
321
|
+
}),
|
|
322
|
+
);
|
|
323
|
+
|
|
324
|
+
$effect(() => {
|
|
325
|
+
dlog(
|
|
326
|
+
`VirtualizedList sticky hasStickyHeaders=${hasStickyHeaders} nativeStickyAvailable=${nativeStickyAvailable} mode=${forwarding.mode} scrollEventThrottle=${String(forwarding.scrollEventThrottle)}`,
|
|
327
|
+
);
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
// Drive the sticky scroll value on the native UI thread once the scroll host commits (mirrors
|
|
331
|
+
// ScrollView.svelte). No-op without the native animated module - the JS fallback below still
|
|
332
|
+
// keeps headers pinned via onScroll.
|
|
333
|
+
$effect(() => {
|
|
334
|
+
if (!nativeStickyAvailable) {
|
|
335
|
+
dlog('VirtualizedList sticky attachStickyScroll skipped: nativeStickyAvailable=false');
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
const node = hostShim?.engineNode;
|
|
339
|
+
if (node === undefined) {
|
|
340
|
+
dlog('VirtualizedList sticky attachStickyScroll skipped: engineNode not ready yet');
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
dlog('VirtualizedList sticky attachStickyScroll attached');
|
|
344
|
+
return attachStickyScroll(node, scrollAnimatedValue);
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
function handleScroll(event: ISymbioteEvent): void {
|
|
348
|
+
const offset = readScrollOffset(event, narrowed.horizontal);
|
|
349
|
+
if (offset === undefined) return;
|
|
350
|
+
dlog(`VirtualizedList onScroll offset=${offset}`);
|
|
351
|
+
commandedOffset = undefined;
|
|
352
|
+
dispatch({ kind: 'scroll', offset });
|
|
353
|
+
narrowed.userOnScroll?.(event);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// sticky-js: no native Animated module, so the scroll value is driven off the JS thread via the
|
|
357
|
+
// same wrapped-listener shape ScrollView.svelte uses. sticky-native/plain: forward untouched.
|
|
358
|
+
const onScroll = $derived.by(() => {
|
|
359
|
+
if (forwarding.mode !== 'sticky-js') return handleScroll;
|
|
360
|
+
return animatedEvent(
|
|
361
|
+
[{ nativeEvent: { contentOffset: { y: scrollAnimatedValue } } }],
|
|
362
|
+
{ listener: (...args: unknown[]) => forwardScrollEvent(handleScroll, args) },
|
|
363
|
+
);
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
function onViewportLayout(event: ISymbioteEvent): void {
|
|
367
|
+
const length = readLayoutLength(event, narrowed.horizontal);
|
|
368
|
+
if (length === undefined) return;
|
|
369
|
+
dlog(`VirtualizedList onLayout viewport=${length}`);
|
|
370
|
+
dispatch({ kind: 'layout', length });
|
|
371
|
+
if (forwarding.capturesViewportHeight) {
|
|
372
|
+
const height = readLayoutNumber(event, 'height');
|
|
373
|
+
if (height !== undefined) viewportHeight = height;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// Records a sticky header's measured y (RN's _headerLayoutYs) so the NEXT sticky header down the
|
|
378
|
+
// list can compute its collision point - without this every header would stick indefinitely
|
|
379
|
+
// instead of being pushed off by the one behind it.
|
|
380
|
+
function recordHeaderY(index: number, event: ISymbioteEvent): void {
|
|
381
|
+
const y = readLayoutNumber(event, 'y');
|
|
382
|
+
if (y === undefined) return;
|
|
383
|
+
// This map is the ONLY source of nextHeaderLayoutY for every header ahead of this one; if a
|
|
384
|
+
// later header's onLayout never fires (windowing drops its cell) the collision math upstream
|
|
385
|
+
// falls back to `?? 0` (collisionPoint = -layoutHeight), which is almost certainly wrong.
|
|
386
|
+
dlog(`VirtualizedList recordHeaderY index=${index} y=${y}`);
|
|
387
|
+
headerLayoutYs.set(index, y);
|
|
388
|
+
stickyVersion += 1;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function stickyLayoutFor(index: number): (event: ISymbioteEvent) => void {
|
|
392
|
+
const measure = makeCellMeasure(index);
|
|
393
|
+
return (event: ISymbioteEvent): void => {
|
|
394
|
+
measure(event);
|
|
395
|
+
recordHeaderY(index, event);
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// Only a header MOUNTED RIGHT NOW can collide with this one, matching React (its ScrollView
|
|
400
|
+
// receives `renderedStickyIndices`, not the full section list -
|
|
401
|
+
// adapters/react/.../virtualized-list/index.ts:732). Passing the full index list instead let a
|
|
402
|
+
// header collide against a stale y from one that had already scrolled out and unmounted,
|
|
403
|
+
// freezing it in place - proven by sticky-collision-parity.test.ts against the React reference.
|
|
404
|
+
// The map itself is deliberately NOT pruned (React doesn't either): a measured y stays valid for
|
|
405
|
+
// when that header scrolls back in.
|
|
406
|
+
function nextStickyHeaderYFor(index: number): number | undefined {
|
|
407
|
+
void stickyVersion;
|
|
408
|
+
const indices = narrowed.stickyHeaderIndices;
|
|
409
|
+
if (indices === undefined) return undefined;
|
|
410
|
+
const mounted = new Set(allCells.map(cell => cell.index));
|
|
411
|
+
const rendered = indices.filter(stickyIndex => mounted.has(stickyIndex));
|
|
412
|
+
return nextStickyHeaderY(rendered, rendered.indexOf(index), headerLayoutYs);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
function makeCellMeasure(index: number): (event: ISymbioteEvent) => void {
|
|
416
|
+
return (event: ISymbioteEvent): void => {
|
|
417
|
+
const length = readLayoutLength(event, narrowed.horizontal);
|
|
418
|
+
if (length === undefined) return;
|
|
419
|
+
dlog(`VirtualizedList cell ${index} measured length=${length}`);
|
|
420
|
+
dispatch({ kind: 'measure', index, length });
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
function mergeSeparator(gapIndex: number, patch: Partial<ISeparatorProps<ItemT>>): void {
|
|
425
|
+
if (!isSeparatorGapInRange(gapIndex, listState.metrics.count)) return;
|
|
426
|
+
separatorOverrides.set(gapIndex, { ...separatorOverrides.get(gapIndex), ...patch });
|
|
427
|
+
separatorVersion += 1;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
function makeSeparators(index: number): ISeparators {
|
|
431
|
+
return {
|
|
432
|
+
highlight: (): void => {
|
|
433
|
+
dlog(`VirtualizedList separator highlight cell=${index}`);
|
|
434
|
+
mergeSeparator(index - 1, { highlighted: true });
|
|
435
|
+
mergeSeparator(index, { highlighted: true });
|
|
436
|
+
},
|
|
437
|
+
unhighlight: (): void => {
|
|
438
|
+
dlog(`VirtualizedList separator unhighlight cell=${index}`);
|
|
439
|
+
mergeSeparator(index - 1, { highlighted: false });
|
|
440
|
+
mergeSeparator(index, { highlighted: false });
|
|
441
|
+
},
|
|
442
|
+
updateProps: (select: 'leading' | 'trailing', newProps: Record<string, unknown>): void => {
|
|
443
|
+
mergeSeparator(select === 'leading' ? index - 1 : index, newProps);
|
|
444
|
+
},
|
|
445
|
+
};
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// ---- imperative handle: component instance exports, the Svelte twin of Vue's expose() /
|
|
449
|
+
// React's useImperativeHandle. A parent does `<VirtualizedList bind:this={ref} .../>` and calls
|
|
450
|
+
// `ref.scrollToIndex(...)`. ----
|
|
451
|
+
export function scrollToOffset(params: { offset: number; animated?: boolean }): void {
|
|
452
|
+
dispatch({ kind: 'scroll-to-offset', offset: params.offset, animated: params.animated ?? true });
|
|
453
|
+
}
|
|
454
|
+
export function scrollToIndex(params: {
|
|
455
|
+
index: number;
|
|
456
|
+
animated?: boolean;
|
|
457
|
+
viewOffset?: number;
|
|
458
|
+
viewPosition?: number;
|
|
459
|
+
}): void {
|
|
460
|
+
dispatch({
|
|
461
|
+
kind: 'scroll-to-index',
|
|
462
|
+
index: params.index,
|
|
463
|
+
animated: params.animated ?? true,
|
|
464
|
+
viewPosition: params.viewPosition ?? FIRST_INDEX,
|
|
465
|
+
viewOffset: params.viewOffset ?? EMPTY_OFFSET,
|
|
466
|
+
});
|
|
467
|
+
}
|
|
468
|
+
export function scrollToItem(params: {
|
|
469
|
+
item: unknown;
|
|
470
|
+
animated?: boolean;
|
|
471
|
+
viewPosition?: number;
|
|
472
|
+
}): void {
|
|
473
|
+
dispatch({
|
|
474
|
+
kind: 'scroll-to-item',
|
|
475
|
+
item: params.item,
|
|
476
|
+
animated: params.animated ?? true,
|
|
477
|
+
viewPosition: params.viewPosition ?? FIRST_INDEX,
|
|
478
|
+
});
|
|
479
|
+
}
|
|
480
|
+
export function scrollToEnd(params?: { animated?: boolean }): void {
|
|
481
|
+
dispatch({ kind: 'scroll-to-end', animated: params?.animated ?? true });
|
|
482
|
+
}
|
|
483
|
+
export function flashScrollIndicators(): void {
|
|
484
|
+
scrollHandle.flashScrollIndicators();
|
|
485
|
+
}
|
|
486
|
+
export function getNativeScrollRef(): IScrollViewHandle | null {
|
|
487
|
+
return scrollHandle;
|
|
488
|
+
}
|
|
489
|
+
export function getScrollableNode(): IScrollViewHandle | null {
|
|
490
|
+
return scrollHandle;
|
|
491
|
+
}
|
|
492
|
+
export function getScrollResponder(): IScrollViewHandle | null {
|
|
493
|
+
return scrollHandle;
|
|
494
|
+
}
|
|
495
|
+
export function getScrollNode(): ISymbioteNode | null {
|
|
496
|
+
return scrollHandle.getScrollNode();
|
|
497
|
+
}
|
|
498
|
+
export function recordInteraction(): void {
|
|
499
|
+
dispatch({ kind: 'record-interaction' });
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
// ---- after-commit pass: runs the deferred effects (batch fill, edge-reached, viewability,
|
|
503
|
+
// initial-scroll, MVCP) whenever the windowing signature changes. $effect runs after the DOM
|
|
504
|
+
// update lands, the same after-commit timing Vue's `flush: 'post'` watcher and React's layout
|
|
505
|
+
// effect give the reducer. ----
|
|
506
|
+
$effect(() => {
|
|
507
|
+
void commitSignature;
|
|
508
|
+
const inputs = buildInputs();
|
|
509
|
+
const result = reduceList(listState, { kind: 'commit' }, inputs);
|
|
510
|
+
runEffects(result.effects, inputs);
|
|
511
|
+
});
|
|
512
|
+
|
|
513
|
+
// Clear pending timers on unmount (RN ViewabilityHelper.dispose + the fill timer). A bare
|
|
514
|
+
// `$effect` with no reactive reads runs once on mount; its returned cleanup runs on destroy -
|
|
515
|
+
// the Svelte twin of Vue's onBeforeUnmount.
|
|
516
|
+
$effect(() => {
|
|
517
|
+
return () => {
|
|
518
|
+
if (viewableTimer !== null) clearTimeout(viewableTimer);
|
|
519
|
+
if (batchTimer !== null) clearTimeout(batchTimer);
|
|
520
|
+
};
|
|
521
|
+
});
|
|
522
|
+
|
|
523
|
+
const scrollViewIntrinsics = $derived(selectScrollIntrinsics(narrowed.horizontal, narrowed.contentContainerStyle));
|
|
524
|
+
|
|
525
|
+
const resolvedStyle = $derived(
|
|
526
|
+
narrowed.inverted
|
|
527
|
+
? [narrowed.style, narrowed.horizontal ? INVERTED_X_STYLE : INVERTED_Y_STYLE]
|
|
528
|
+
: narrowed.style,
|
|
529
|
+
);
|
|
530
|
+
const resolvedContentContainerStyle = $derived(
|
|
531
|
+
narrowed.horizontal
|
|
532
|
+
? [scrollViewIntrinsics.contentStyle, { width: metrics.total }]
|
|
533
|
+
: scrollViewIntrinsics.contentStyle,
|
|
534
|
+
);
|
|
535
|
+
|
|
536
|
+
// Android wrap mode only: mirrors ScrollView's own index.svelte - RN's splitLayoutProps routes
|
|
537
|
+
// LAYOUT props (margin/flex/size/position/...) onto the wrapping AndroidSwipeRefreshLayout,
|
|
538
|
+
// VISUAL props (background/padding/border/...) onto the inner scroll view, instead of leaving
|
|
539
|
+
// the wrapper unstyled (which collapses it to zero height).
|
|
540
|
+
const layoutSplit = $derived(
|
|
541
|
+
shouldWrapRefreshControl
|
|
542
|
+
? splitLayoutProps([resolveSvelteClass(narrowed.class), resolvedStyle])
|
|
543
|
+
: undefined,
|
|
544
|
+
);
|
|
545
|
+
|
|
546
|
+
const outerBag = $derived.by(() => {
|
|
547
|
+
const bag: Record<string, unknown> = {
|
|
548
|
+
style: [scrollViewIntrinsics.scrollViewBaseStyle, layoutSplit !== undefined ? layoutSplit.inner : resolvedStyle],
|
|
549
|
+
horizontal: narrowed.horizontal,
|
|
550
|
+
// RN defaults nested scrolling ON (ScrollView.js `nestedScrollEnabled ?? true`). This file
|
|
551
|
+
// hand-rolls the raw scroll intrinsic instead of rendering <ScrollView>, so it never
|
|
552
|
+
// inherited that default - on Android, a list nested inside a page ScrollView never got
|
|
553
|
+
// nested-scroll gesture arbitration and only the outer page scrolled.
|
|
554
|
+
nestedScrollEnabled: true,
|
|
555
|
+
onScroll,
|
|
556
|
+
onLayout: onViewportLayout,
|
|
557
|
+
};
|
|
558
|
+
if (layoutSplit === undefined) bag.class = narrowed.class;
|
|
559
|
+
if (commandedOffset !== undefined) bag.contentOffset = commandedOffset;
|
|
560
|
+
// forwarding.scrollEventThrottle (not the raw prop): folds in the sticky-mode default
|
|
561
|
+
// (1 native / 16 JS-fallback) when unset - without it a sticky header rebuilt off too-sparse
|
|
562
|
+
// scroll events pins/collides late.
|
|
563
|
+
if (forwarding.scrollEventThrottle !== undefined) bag.scrollEventThrottle = forwarding.scrollEventThrottle;
|
|
564
|
+
if (narrowed.onScrollBeginDrag !== undefined) bag.onScrollBeginDrag = narrowed.onScrollBeginDrag;
|
|
565
|
+
if (narrowed.onScrollEndDrag !== undefined) bag.onScrollEndDrag = narrowed.onScrollEndDrag;
|
|
566
|
+
if (narrowed.onMomentumScrollBegin !== undefined) bag.onMomentumScrollBegin = narrowed.onMomentumScrollBegin;
|
|
567
|
+
if (narrowed.onMomentumScrollEnd !== undefined) bag.onMomentumScrollEnd = narrowed.onMomentumScrollEnd;
|
|
568
|
+
if (narrowed.keyboardShouldPersistTaps !== undefined) {
|
|
569
|
+
bag.keyboardShouldPersistTaps = narrowed.keyboardShouldPersistTaps;
|
|
570
|
+
}
|
|
571
|
+
if (narrowed.keyboardDismissMode !== undefined) bag.keyboardDismissMode = narrowed.keyboardDismissMode;
|
|
572
|
+
if (narrowed.stickyHeaderIndices !== undefined) bag.stickyHeaderIndices = narrowed.stickyHeaderIndices;
|
|
573
|
+
if (narrowed.maintainVisibleContentPosition !== undefined) {
|
|
574
|
+
bag.maintainVisibleContentPosition = narrowed.maintainVisibleContentPosition;
|
|
575
|
+
}
|
|
576
|
+
// Object.assign merges a bag already built field-by-field (pickAccessibilityProps), not a raw
|
|
577
|
+
// spread of `props`, so this stays inside the object-bag convention (svelte-adapter-dom-shim
|
|
578
|
+
// skill §3g(c)) - only ONE prop (`p={bag}`) ever lands on the symbiote-* host tag.
|
|
579
|
+
Object.assign(bag, pickAccessibilityProps(props));
|
|
580
|
+
return bag;
|
|
581
|
+
});
|
|
582
|
+
|
|
583
|
+
// Build the real RefreshControl's own prop bag when onRefresh is set; refreshing defaults to
|
|
584
|
+
// false when nullish, same as RN/React.
|
|
585
|
+
const refreshControlProps = $derived.by(() => {
|
|
586
|
+
if (narrowed.onRefresh === undefined) return undefined;
|
|
587
|
+
return {
|
|
588
|
+
refreshing: narrowed.refreshing ?? false,
|
|
589
|
+
onRefresh: narrowed.onRefresh,
|
|
590
|
+
progressViewOffset: narrowed.progressViewOffset,
|
|
591
|
+
};
|
|
592
|
+
});
|
|
593
|
+
// iOS: RefreshControl is a childless sibling before the content container. Android: it WRAPS the
|
|
594
|
+
// whole scroll view (an Android ScrollView accepts only one child) - same PLATFORM.
|
|
595
|
+
// refreshControlMode ScrollView's own index.svelte reads.
|
|
596
|
+
const shouldWrapRefreshControl = $derived(
|
|
597
|
+
PLATFORM.refreshControlMode === 'wrap' && refreshControlProps !== undefined,
|
|
598
|
+
);
|
|
599
|
+
|
|
600
|
+
const contentBag = $derived({ style: resolvedContentContainerStyle, collapsable: false });
|
|
601
|
+
|
|
602
|
+
const stickySet = $derived(
|
|
603
|
+
narrowed.stickyHeaderIndices !== undefined ? new Set(narrowed.stickyHeaderIndices) : undefined,
|
|
604
|
+
);
|
|
605
|
+
$effect(() => {
|
|
606
|
+
dlog(
|
|
607
|
+
`VirtualizedList sticky stickyHeaderIndices=${JSON.stringify(narrowed.stickyHeaderIndices)} ` +
|
|
608
|
+
`first=${metrics.first} last=${metrics.last} windowedCells=${JSON.stringify(plan?.cells.map(c => c.index))} ` +
|
|
609
|
+
`forcedStickyCell=${plan?.forcedStickyCell?.index ?? 'none'} gapExtent=${plan?.gapExtent ?? 0} ` +
|
|
610
|
+
`allCells=${JSON.stringify(allCells.map(c => c.index))}`,
|
|
611
|
+
);
|
|
612
|
+
});
|
|
613
|
+
const hasHeader = $derived(props.header !== undefined);
|
|
614
|
+
const plan = $derived.by(() => {
|
|
615
|
+
void separatorVersion;
|
|
616
|
+
if (metrics.count === FIRST_INDEX) return null;
|
|
617
|
+
return buildListPlan({
|
|
618
|
+
count: metrics.count,
|
|
619
|
+
first: metrics.first,
|
|
620
|
+
last: metrics.last,
|
|
621
|
+
offsets: metrics.offsets,
|
|
622
|
+
lengths: metrics.lengths,
|
|
623
|
+
total: metrics.total,
|
|
624
|
+
keyFor,
|
|
625
|
+
stickyIndices: stickySet,
|
|
626
|
+
hasHeader,
|
|
627
|
+
hasSeparators: props.separator !== undefined,
|
|
628
|
+
});
|
|
629
|
+
});
|
|
630
|
+
// forcedStickyCell prepended to the window cells so BOTH walk through the SAME keyed {#each}.
|
|
631
|
+
// A Svelte {#if}/{#each} split is two different template positions with no shared component
|
|
632
|
+
// identity, so a header toggling between "forced ahead of the window" and "inside the window"
|
|
633
|
+
// would get destroyed and recreated (losing its measured layout, back to its default
|
|
634
|
+
// translateY) - one keyed list keeps the SAME instance across that transition, by cell.key.
|
|
635
|
+
const allCells = $derived(
|
|
636
|
+
plan?.forcedStickyCell !== undefined && plan !== null
|
|
637
|
+
? [plan.forcedStickyCell, ...plan.cells]
|
|
638
|
+
: (plan?.cells ?? []),
|
|
639
|
+
);
|
|
640
|
+
const cellInvertedStyle = $derived(
|
|
641
|
+
narrowed.inverted ? (narrowed.horizontal ? INVERTED_X_STYLE : INVERTED_Y_STYLE) : undefined,
|
|
642
|
+
);
|
|
643
|
+
|
|
644
|
+
// Reads separatorVersion so a .highlight()/.unhighlight()/.updateProps() call (ISeparators, above)
|
|
645
|
+
// reactively refreshes this cell's separator props - a plain object literal inlined in the
|
|
646
|
+
// template would NOT re-track separatorOverrides (a non-reactive Map) on its own.
|
|
647
|
+
function separatorPropsFor(index: number): ISeparatorProps<ItemT> {
|
|
648
|
+
void separatorVersion;
|
|
649
|
+
const overrides = separatorOverrides.get(index);
|
|
650
|
+
return {
|
|
651
|
+
highlighted: overrides?.highlighted === true,
|
|
652
|
+
leadingItem: narrowed.getItem(narrowed.data, index),
|
|
653
|
+
trailingItem: narrowed.getItem(narrowed.data, index + 1),
|
|
654
|
+
...overrides,
|
|
655
|
+
};
|
|
656
|
+
}
|
|
657
|
+
</script>
|
|
658
|
+
|
|
659
|
+
{#snippet listBody()}<!--
|
|
660
|
+
No whitespace between sibling {#if} blocks below, or between each-block siblings, or around the
|
|
661
|
+
spacers. Svelte only trims LEADING/TRAILING whitespace of a fragment; whitespace BETWEEN two
|
|
662
|
+
sibling non-text nodes collapses to a text node and is kept (Svelte compiler utils.js
|
|
663
|
+
clean_nodes, verified against 5.56.8), which would land as a stray RCTRawText child of this
|
|
664
|
+
scroll content - invalid on real Fabric for a non-Text view (dom-shim/text.ts). Caught by
|
|
665
|
+
virtualized-list.smoke.test.ts asserting an exact windowed child count; keep it exact this way
|
|
666
|
+
if you touch this block.
|
|
667
|
+
-->{#if hasHeader}<symbiote-view p={{}}>{@render props.header?.()}</symbiote-view>{/if}{#if metrics.count === FIRST_INDEX}{#if props.empty}<symbiote-view p={{}}>{@render props.empty()}</symbiote-view>{/if}{:else if plan}{#if plan.leadingExtent > EMPTY_OFFSET}<symbiote-view
|
|
668
|
+
p={{ style: narrowed.horizontal ? { width: plan.leadingExtent } : { height: plan.leadingExtent } }}
|
|
669
|
+
></symbiote-view>{/if}{#each allCells as cell (cell.key)}{#if stickySet?.has(cell.index)}<ScrollViewStickyHeader onLayout={stickyLayoutFor(cell.index)} nextHeaderLayoutY={nextStickyHeaderYFor(cell.index)}>{@render props.item({
|
|
670
|
+
item: narrowed.getItem(narrowed.data, cell.index),
|
|
671
|
+
index: cell.index,
|
|
672
|
+
separators: makeSeparators(cell.index),
|
|
673
|
+
})}</ScrollViewStickyHeader>{:else}<symbiote-view p={{ onLayout: makeCellMeasure(cell.index), style: cellInvertedStyle }}>{@render props.item({
|
|
674
|
+
item: narrowed.getItem(narrowed.data, cell.index),
|
|
675
|
+
index: cell.index,
|
|
676
|
+
separators: makeSeparators(cell.index),
|
|
677
|
+
})}</symbiote-view>{/if}{#if plan.forcedStickyCell && cell.index === plan.forcedStickyCell.index && plan.gapExtent > EMPTY_OFFSET}<symbiote-view
|
|
678
|
+
p={{ style: narrowed.horizontal ? { width: plan.gapExtent } : { height: plan.gapExtent } }}
|
|
679
|
+
></symbiote-view>{/if}{#if props.separator && cell.index < metrics.last && cell.index !== plan.forcedStickyCell?.index}<symbiote-view p={{}}>{@render props.separator(separatorPropsFor(cell.index))}</symbiote-view>{/if}{/each}{#if plan.trailingExtent > EMPTY_OFFSET}<symbiote-view
|
|
680
|
+
p={{ style: narrowed.horizontal ? { width: plan.trailingExtent } : { height: plan.trailingExtent } }}
|
|
681
|
+
></symbiote-view>{/if}{/if}{#if props.footer}<symbiote-view p={{}}>{@render props.footer()}</symbiote-view>{/if}
|
|
682
|
+
{/snippet}
|
|
683
|
+
|
|
684
|
+
{#snippet scrollBody()}<!--
|
|
685
|
+
Same no-whitespace-between-siblings rule as listBody() above: the optional sibling
|
|
686
|
+
RefreshControl and the content container are two siblings of one parent when RefreshControl is
|
|
687
|
+
NOT wrapping (iOS), so they must sit edge-to-edge with zero characters between them.
|
|
688
|
+
-->{#if !shouldWrapRefreshControl && refreshControlProps !== undefined}<RefreshControl {...refreshControlProps} />{/if}{#if narrowed.horizontal}<symbiote-horizontal-scroll-content p={contentBag}>{@render listBody()}</symbiote-horizontal-scroll-content>{:else}<symbiote-scroll-content p={contentBag}>{@render listBody()}</symbiote-scroll-content>{/if}{/snippet}
|
|
689
|
+
|
|
690
|
+
{#if shouldWrapRefreshControl && refreshControlProps !== undefined}
|
|
691
|
+
<RefreshControl {...refreshControlProps} style={layoutSplit?.outer}>
|
|
692
|
+
{#if narrowed.horizontal}
|
|
693
|
+
<symbiote-horizontal-scroll-view p={outerBag} bind:this={hostShim}>{@render scrollBody()}</symbiote-horizontal-scroll-view>
|
|
694
|
+
{:else}
|
|
695
|
+
<symbiote-scroll-view p={outerBag} bind:this={hostShim}>{@render scrollBody()}</symbiote-scroll-view>
|
|
696
|
+
{/if}
|
|
697
|
+
</RefreshControl>
|
|
698
|
+
{:else if narrowed.horizontal}
|
|
699
|
+
<symbiote-horizontal-scroll-view p={outerBag} bind:this={hostShim}>{@render scrollBody()}</symbiote-horizontal-scroll-view>
|
|
700
|
+
{:else}
|
|
701
|
+
<symbiote-scroll-view p={outerBag} bind:this={hostShim}>{@render scrollBody()}</symbiote-scroll-view>
|
|
702
|
+
{/if}
|