@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
|
@@ -24,7 +24,10 @@
|
|
|
24
24
|
// `<svelte:element>` is forbidden (svelte-adapter-dom-shim skill §4/§7 - its shim surface isn't
|
|
25
25
|
// implemented), so the horizontal/vertical choice is two static branches sharing one
|
|
26
26
|
// `{#snippet listBody()}`, not a dynamic tag.
|
|
27
|
-
import type {
|
|
27
|
+
import type {
|
|
28
|
+
IVirtualizedListProps,
|
|
29
|
+
IVirtualizedListHandle,
|
|
30
|
+
} from './virtualized-list-props';
|
|
28
31
|
|
|
29
32
|
export type { IVirtualizedListProps, IVirtualizedListHandle };
|
|
30
33
|
</script>
|
|
@@ -51,6 +54,7 @@
|
|
|
51
54
|
listEffectSignature,
|
|
52
55
|
nextStickyHeaderY,
|
|
53
56
|
readLayoutLength,
|
|
57
|
+
readLayoutOffset,
|
|
54
58
|
readLayoutNumber,
|
|
55
59
|
readScrollOffset,
|
|
56
60
|
reduceList,
|
|
@@ -85,7 +89,10 @@
|
|
|
85
89
|
SCROLL_VIEW_STICKY_CONTEXT_KEY,
|
|
86
90
|
type IScrollViewStickyContext,
|
|
87
91
|
} from '../scroll-view/scroll-view-sticky-context';
|
|
88
|
-
import {
|
|
92
|
+
import {
|
|
93
|
+
pickAccessibilityProps,
|
|
94
|
+
type IVirtualizedListProps as IProps,
|
|
95
|
+
} from './virtualized-list-props';
|
|
89
96
|
import { createAttachmentsSync } from '../../runes/attachments';
|
|
90
97
|
|
|
91
98
|
let props: IProps<ItemT> = $props();
|
|
@@ -104,13 +111,17 @@
|
|
|
104
111
|
|
|
105
112
|
// Offset we're imperatively driving native to before hostShim's engine node is live. Fresh
|
|
106
113
|
// 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>(
|
|
114
|
+
let commandedOffset = $state.raw<{ x: number; y: number } | undefined>(
|
|
115
|
+
undefined,
|
|
116
|
+
);
|
|
108
117
|
// Bumped on a render-relevant change so `metrics` re-runs - listState is a PLAIN object
|
|
109
118
|
// (not $state), mutating it triggers nothing on its own.
|
|
110
119
|
let version = $state(0);
|
|
111
120
|
let separatorVersion = $state(0);
|
|
112
121
|
|
|
113
|
-
const scrollHandle: IScrollViewHandle = buildScrollViewHandle(
|
|
122
|
+
const scrollHandle: IScrollViewHandle = buildScrollViewHandle(
|
|
123
|
+
() => hostShim?.engineNode ?? null,
|
|
124
|
+
);
|
|
114
125
|
|
|
115
126
|
// The one folded state cell — the Svelte twin of Vue's plain listState / React's stateRef.
|
|
116
127
|
const listState: IListState<ItemT> = createInitialListState<ItemT>();
|
|
@@ -147,9 +158,11 @@
|
|
|
147
158
|
horizontal: props.horizontal === true,
|
|
148
159
|
inverted: props.inverted === true,
|
|
149
160
|
onEndReached: props.onEndReached,
|
|
150
|
-
onEndReachedThreshold:
|
|
161
|
+
onEndReachedThreshold:
|
|
162
|
+
props.onEndReachedThreshold ?? DEFAULT_END_REACHED_THRESHOLD,
|
|
151
163
|
onStartReached: props.onStartReached,
|
|
152
|
-
onStartReachedThreshold:
|
|
164
|
+
onStartReachedThreshold:
|
|
165
|
+
props.onStartReachedThreshold ?? DEFAULT_START_REACHED_THRESHOLD,
|
|
153
166
|
onRefresh: props.onRefresh,
|
|
154
167
|
refreshing: props.refreshing,
|
|
155
168
|
progressViewOffset: props.progressViewOffset,
|
|
@@ -157,9 +170,11 @@
|
|
|
157
170
|
viewabilityConfig: props.viewabilityConfig,
|
|
158
171
|
viewabilityConfigCallbackPairs: props.viewabilityConfigCallbackPairs,
|
|
159
172
|
onScrollToIndexFailed: props.onScrollToIndexFailed,
|
|
160
|
-
initialNumToRender:
|
|
173
|
+
initialNumToRender:
|
|
174
|
+
props.initialNumToRender ?? DEFAULT_INITIAL_NUM_TO_RENDER,
|
|
161
175
|
initialScrollIndex: props.initialScrollIndex,
|
|
162
|
-
maxToRenderPerBatch:
|
|
176
|
+
maxToRenderPerBatch:
|
|
177
|
+
props.maxToRenderPerBatch ?? DEFAULT_MAX_TO_RENDER_PER_BATCH,
|
|
163
178
|
updateCellsBatchingPeriod:
|
|
164
179
|
props.updateCellsBatchingPeriod ?? DEFAULT_UPDATE_CELLS_BATCHING_PERIOD,
|
|
165
180
|
windowSize: props.windowSize ?? DEFAULT_WINDOW_SIZE,
|
|
@@ -207,7 +222,11 @@
|
|
|
207
222
|
}
|
|
208
223
|
|
|
209
224
|
function keyFor(index: number): string {
|
|
210
|
-
return resolveItemKey(
|
|
225
|
+
return resolveItemKey(
|
|
226
|
+
narrowed.getItem(narrowed.data, index),
|
|
227
|
+
index,
|
|
228
|
+
narrowed.keyExtractor,
|
|
229
|
+
);
|
|
211
230
|
}
|
|
212
231
|
|
|
213
232
|
function scrollToPixel(offset: number, animated: boolean): void {
|
|
@@ -221,11 +240,16 @@
|
|
|
221
240
|
return;
|
|
222
241
|
}
|
|
223
242
|
commandedOffset = undefined;
|
|
224
|
-
dlog(
|
|
243
|
+
dlog(
|
|
244
|
+
`VirtualizedList scrollTo offset=${clamped} animated=${animated} (horizontal=${narrowed.horizontal})`,
|
|
245
|
+
);
|
|
225
246
|
scrollHandle.scrollTo({ x: target.x, y: target.y, animated });
|
|
226
247
|
}
|
|
227
248
|
|
|
228
|
-
function runEffects(
|
|
249
|
+
function runEffects(
|
|
250
|
+
effects: IListEffect<ItemT>[],
|
|
251
|
+
inputs: IListReducerInputs<ItemT>,
|
|
252
|
+
): void {
|
|
229
253
|
const p = narrowed;
|
|
230
254
|
for (const effect of effects) {
|
|
231
255
|
switch (effect.kind) {
|
|
@@ -300,14 +324,17 @@
|
|
|
300
324
|
});
|
|
301
325
|
|
|
302
326
|
const hasStickyHeaders = $derived(
|
|
303
|
-
narrowed.stickyHeaderIndices !== undefined &&
|
|
327
|
+
narrowed.stickyHeaderIndices !== undefined &&
|
|
328
|
+
narrowed.stickyHeaderIndices.length > 0,
|
|
304
329
|
);
|
|
305
330
|
// Resolved dynamically, exactly like React (adapters/react/.../scroll-view/shared.ts:267). Do
|
|
306
331
|
// not hardcode this false to keep the JS listener alive - RN gates native the same way
|
|
307
332
|
// (AnimatedWithChildren.js:74 `if (!this.__isNative)`) and the pin is the native transform, not
|
|
308
333
|
// the listener, so forcing JS mode only trades away the native driver: header lag on iOS,
|
|
309
334
|
// outright failure on Android (commit debounce 15ms vs iOS's 64ms - render-scroll-sticky.ts).
|
|
310
|
-
const nativeStickyAvailable = $derived(
|
|
335
|
+
const nativeStickyAvailable = $derived(
|
|
336
|
+
hasStickyHeaders && isNativeAnimatedAvailable(),
|
|
337
|
+
);
|
|
311
338
|
// RN's FlatList/SectionList expose no invertStickyHeaders of their own (only ScrollView does) -
|
|
312
339
|
// sticky headers always pin to the top here, matching RN.
|
|
313
340
|
const forwarding = $derived(
|
|
@@ -332,12 +359,16 @@
|
|
|
332
359
|
// keeps headers pinned via onScroll.
|
|
333
360
|
$effect(() => {
|
|
334
361
|
if (!nativeStickyAvailable) {
|
|
335
|
-
dlog(
|
|
362
|
+
dlog(
|
|
363
|
+
'VirtualizedList sticky attachStickyScroll skipped: nativeStickyAvailable=false',
|
|
364
|
+
);
|
|
336
365
|
return;
|
|
337
366
|
}
|
|
338
367
|
const node = hostShim?.engineNode;
|
|
339
368
|
if (node === undefined) {
|
|
340
|
-
dlog(
|
|
369
|
+
dlog(
|
|
370
|
+
'VirtualizedList sticky attachStickyScroll skipped: engineNode not ready yet',
|
|
371
|
+
);
|
|
341
372
|
return;
|
|
342
373
|
}
|
|
343
374
|
dlog('VirtualizedList sticky attachStickyScroll attached');
|
|
@@ -359,7 +390,10 @@
|
|
|
359
390
|
if (forwarding.mode !== 'sticky-js') return handleScroll;
|
|
360
391
|
return animatedEvent(
|
|
361
392
|
[{ nativeEvent: { contentOffset: { y: scrollAnimatedValue } } }],
|
|
362
|
-
{
|
|
393
|
+
{
|
|
394
|
+
listener: (...args: unknown[]) =>
|
|
395
|
+
forwardScrollEvent(handleScroll, args),
|
|
396
|
+
},
|
|
363
397
|
);
|
|
364
398
|
});
|
|
365
399
|
|
|
@@ -416,14 +450,23 @@
|
|
|
416
450
|
return (event: ISymbioteEvent): void => {
|
|
417
451
|
const length = readLayoutLength(event, narrowed.horizontal);
|
|
418
452
|
if (length === undefined) return;
|
|
419
|
-
|
|
420
|
-
|
|
453
|
+
const offset = readLayoutOffset(event, narrowed.horizontal);
|
|
454
|
+
dlog(
|
|
455
|
+
`VirtualizedList cell ${index} measured length=${length} offset=${offset ?? 'none'}`,
|
|
456
|
+
);
|
|
457
|
+
dispatch({ kind: 'measure', index, length, offset });
|
|
421
458
|
};
|
|
422
459
|
}
|
|
423
460
|
|
|
424
|
-
function mergeSeparator(
|
|
461
|
+
function mergeSeparator(
|
|
462
|
+
gapIndex: number,
|
|
463
|
+
patch: Partial<ISeparatorProps<ItemT>>,
|
|
464
|
+
): void {
|
|
425
465
|
if (!isSeparatorGapInRange(gapIndex, listState.metrics.count)) return;
|
|
426
|
-
separatorOverrides.set(gapIndex, {
|
|
466
|
+
separatorOverrides.set(gapIndex, {
|
|
467
|
+
...separatorOverrides.get(gapIndex),
|
|
468
|
+
...patch,
|
|
469
|
+
});
|
|
427
470
|
separatorVersion += 1;
|
|
428
471
|
}
|
|
429
472
|
|
|
@@ -439,7 +482,10 @@
|
|
|
439
482
|
mergeSeparator(index - 1, { highlighted: false });
|
|
440
483
|
mergeSeparator(index, { highlighted: false });
|
|
441
484
|
},
|
|
442
|
-
updateProps: (
|
|
485
|
+
updateProps: (
|
|
486
|
+
select: 'leading' | 'trailing',
|
|
487
|
+
newProps: Record<string, unknown>,
|
|
488
|
+
): void => {
|
|
443
489
|
mergeSeparator(select === 'leading' ? index - 1 : index, newProps);
|
|
444
490
|
},
|
|
445
491
|
};
|
|
@@ -448,8 +494,15 @@
|
|
|
448
494
|
// ---- imperative handle: component instance exports, the Svelte twin of Vue's expose() /
|
|
449
495
|
// React's useImperativeHandle. A parent does `<VirtualizedList bind:this={ref} .../>` and calls
|
|
450
496
|
// `ref.scrollToIndex(...)`. ----
|
|
451
|
-
export function scrollToOffset(params: {
|
|
452
|
-
|
|
497
|
+
export function scrollToOffset(params: {
|
|
498
|
+
offset: number;
|
|
499
|
+
animated?: boolean;
|
|
500
|
+
}): void {
|
|
501
|
+
dispatch({
|
|
502
|
+
kind: 'scroll-to-offset',
|
|
503
|
+
offset: params.offset,
|
|
504
|
+
animated: params.animated ?? true,
|
|
505
|
+
});
|
|
453
506
|
}
|
|
454
507
|
export function scrollToIndex(params: {
|
|
455
508
|
index: number;
|
|
@@ -520,11 +573,16 @@
|
|
|
520
573
|
};
|
|
521
574
|
});
|
|
522
575
|
|
|
523
|
-
const scrollViewIntrinsics = $derived(
|
|
576
|
+
const scrollViewIntrinsics = $derived(
|
|
577
|
+
selectScrollIntrinsics(narrowed.horizontal, narrowed.contentContainerStyle),
|
|
578
|
+
);
|
|
524
579
|
|
|
525
580
|
const resolvedStyle = $derived(
|
|
526
581
|
narrowed.inverted
|
|
527
|
-
? [
|
|
582
|
+
? [
|
|
583
|
+
narrowed.style,
|
|
584
|
+
narrowed.horizontal ? INVERTED_X_STYLE : INVERTED_Y_STYLE,
|
|
585
|
+
]
|
|
528
586
|
: narrowed.style,
|
|
529
587
|
);
|
|
530
588
|
const resolvedContentContainerStyle = $derived(
|
|
@@ -545,7 +603,10 @@
|
|
|
545
603
|
|
|
546
604
|
const outerBag = $derived.by(() => {
|
|
547
605
|
const bag: Record<string, unknown> = {
|
|
548
|
-
style: [
|
|
606
|
+
style: [
|
|
607
|
+
scrollViewIntrinsics.scrollViewBaseStyle,
|
|
608
|
+
layoutSplit !== undefined ? layoutSplit.inner : resolvedStyle,
|
|
609
|
+
],
|
|
549
610
|
horizontal: narrowed.horizontal,
|
|
550
611
|
// RN defaults nested scrolling ON (ScrollView.js `nestedScrollEnabled ?? true`). This file
|
|
551
612
|
// hand-rolls the raw scroll intrinsic instead of rendering <ScrollView>, so it never
|
|
@@ -560,18 +621,26 @@
|
|
|
560
621
|
// forwarding.scrollEventThrottle (not the raw prop): folds in the sticky-mode default
|
|
561
622
|
// (1 native / 16 JS-fallback) when unset - without it a sticky header rebuilt off too-sparse
|
|
562
623
|
// scroll events pins/collides late.
|
|
563
|
-
if (forwarding.scrollEventThrottle !== undefined)
|
|
564
|
-
|
|
565
|
-
if (narrowed.
|
|
566
|
-
|
|
567
|
-
if (narrowed.
|
|
624
|
+
if (forwarding.scrollEventThrottle !== undefined)
|
|
625
|
+
bag.scrollEventThrottle = forwarding.scrollEventThrottle;
|
|
626
|
+
if (narrowed.onScrollBeginDrag !== undefined)
|
|
627
|
+
bag.onScrollBeginDrag = narrowed.onScrollBeginDrag;
|
|
628
|
+
if (narrowed.onScrollEndDrag !== undefined)
|
|
629
|
+
bag.onScrollEndDrag = narrowed.onScrollEndDrag;
|
|
630
|
+
if (narrowed.onMomentumScrollBegin !== undefined)
|
|
631
|
+
bag.onMomentumScrollBegin = narrowed.onMomentumScrollBegin;
|
|
632
|
+
if (narrowed.onMomentumScrollEnd !== undefined)
|
|
633
|
+
bag.onMomentumScrollEnd = narrowed.onMomentumScrollEnd;
|
|
568
634
|
if (narrowed.keyboardShouldPersistTaps !== undefined) {
|
|
569
635
|
bag.keyboardShouldPersistTaps = narrowed.keyboardShouldPersistTaps;
|
|
570
636
|
}
|
|
571
|
-
if (narrowed.keyboardDismissMode !== undefined)
|
|
572
|
-
|
|
637
|
+
if (narrowed.keyboardDismissMode !== undefined)
|
|
638
|
+
bag.keyboardDismissMode = narrowed.keyboardDismissMode;
|
|
639
|
+
if (narrowed.stickyHeaderIndices !== undefined)
|
|
640
|
+
bag.stickyHeaderIndices = narrowed.stickyHeaderIndices;
|
|
573
641
|
if (narrowed.maintainVisibleContentPosition !== undefined) {
|
|
574
|
-
bag.maintainVisibleContentPosition =
|
|
642
|
+
bag.maintainVisibleContentPosition =
|
|
643
|
+
narrowed.maintainVisibleContentPosition;
|
|
575
644
|
}
|
|
576
645
|
// Object.assign merges a bag already built field-by-field (pickAccessibilityProps), not a raw
|
|
577
646
|
// spread of `props`, so this stays inside the object-bag convention (svelte-adapter-dom-shim
|
|
@@ -597,10 +666,15 @@
|
|
|
597
666
|
PLATFORM.refreshControlMode === 'wrap' && refreshControlProps !== undefined,
|
|
598
667
|
);
|
|
599
668
|
|
|
600
|
-
const contentBag = $derived({
|
|
669
|
+
const contentBag = $derived({
|
|
670
|
+
style: resolvedContentContainerStyle,
|
|
671
|
+
collapsable: false,
|
|
672
|
+
});
|
|
601
673
|
|
|
602
674
|
const stickySet = $derived(
|
|
603
|
-
narrowed.stickyHeaderIndices !== undefined
|
|
675
|
+
narrowed.stickyHeaderIndices !== undefined
|
|
676
|
+
? new Set(narrowed.stickyHeaderIndices)
|
|
677
|
+
: undefined,
|
|
604
678
|
);
|
|
605
679
|
$effect(() => {
|
|
606
680
|
dlog(
|
|
@@ -624,7 +698,6 @@
|
|
|
624
698
|
keyFor,
|
|
625
699
|
stickyIndices: stickySet,
|
|
626
700
|
hasHeader,
|
|
627
|
-
hasSeparators: props.separator !== undefined,
|
|
628
701
|
});
|
|
629
702
|
});
|
|
630
703
|
// forcedStickyCell prepended to the window cells so BOTH walk through the SAME keyed {#each}.
|
|
@@ -638,7 +711,11 @@
|
|
|
638
711
|
: (plan?.cells ?? []),
|
|
639
712
|
);
|
|
640
713
|
const cellInvertedStyle = $derived(
|
|
641
|
-
narrowed.inverted
|
|
714
|
+
narrowed.inverted
|
|
715
|
+
? narrowed.horizontal
|
|
716
|
+
? INVERTED_X_STYLE
|
|
717
|
+
: INVERTED_Y_STYLE
|
|
718
|
+
: undefined,
|
|
642
719
|
);
|
|
643
720
|
|
|
644
721
|
// Reads separatorVersion so a .highlight()/.unhighlight()/.updateProps() call (ISeparators, above)
|
|
@@ -656,47 +733,133 @@
|
|
|
656
733
|
}
|
|
657
734
|
</script>
|
|
658
735
|
|
|
659
|
-
{#snippet listBody()}
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
736
|
+
{#snippet listBody()}
|
|
737
|
+
<!--
|
|
738
|
+
Svelte keeps the whitespace between the sibling blocks below as ' ' text nodes; the shim drops
|
|
739
|
+
each one because this scroll content takes no raw text (dom-shim/text.ts, §16b). That is what
|
|
740
|
+
lets virtualized-list.smoke.test.ts still assert an exact windowed child count.
|
|
741
|
+
-->
|
|
742
|
+
{#if hasHeader}
|
|
743
|
+
<symbiote-view p={{}}>
|
|
744
|
+
{@render props.header?.()}
|
|
745
|
+
</symbiote-view>
|
|
746
|
+
{/if}
|
|
747
|
+
{#if metrics.count === FIRST_INDEX}
|
|
748
|
+
{#if props.empty}
|
|
749
|
+
<symbiote-view p={{}}>
|
|
750
|
+
{@render props.empty()}
|
|
751
|
+
</symbiote-view>
|
|
752
|
+
{/if}
|
|
753
|
+
{:else if plan}
|
|
754
|
+
{#if plan.leadingExtent > EMPTY_OFFSET}
|
|
755
|
+
<symbiote-view
|
|
756
|
+
p={{
|
|
757
|
+
style: narrowed.horizontal
|
|
758
|
+
? { width: plan.leadingExtent }
|
|
759
|
+
: { height: plan.leadingExtent },
|
|
760
|
+
}}
|
|
761
|
+
/>
|
|
762
|
+
{/if}
|
|
763
|
+
{#each allCells as cell (cell.key)}
|
|
764
|
+
{#if stickySet?.has(cell.index)}
|
|
765
|
+
<ScrollViewStickyHeader
|
|
766
|
+
onLayout={stickyLayoutFor(cell.index)}
|
|
767
|
+
nextHeaderLayoutY={nextStickyHeaderYFor(cell.index)}
|
|
768
|
+
>
|
|
769
|
+
{@render props.item({
|
|
770
|
+
item: narrowed.getItem(narrowed.data, cell.index),
|
|
771
|
+
index: cell.index,
|
|
772
|
+
separators: makeSeparators(cell.index),
|
|
773
|
+
})}
|
|
774
|
+
{#if props.separator && cell.index < metrics.count - 1}
|
|
775
|
+
<symbiote-view p={{}}>
|
|
776
|
+
{@render props.separator(separatorPropsFor(cell.index))}
|
|
777
|
+
</symbiote-view>
|
|
778
|
+
{/if}
|
|
779
|
+
</ScrollViewStickyHeader>
|
|
780
|
+
{:else}
|
|
781
|
+
<symbiote-view
|
|
782
|
+
p={{
|
|
783
|
+
onLayout: makeCellMeasure(cell.index),
|
|
784
|
+
style: cellInvertedStyle,
|
|
785
|
+
}}
|
|
786
|
+
>
|
|
787
|
+
{@render props.item({
|
|
788
|
+
item: narrowed.getItem(narrowed.data, cell.index),
|
|
789
|
+
index: cell.index,
|
|
790
|
+
separators: makeSeparators(cell.index),
|
|
791
|
+
})}
|
|
792
|
+
{#if props.separator && cell.index < metrics.count - 1}
|
|
793
|
+
<symbiote-view p={{}}>
|
|
794
|
+
{@render props.separator(separatorPropsFor(cell.index))}
|
|
795
|
+
</symbiote-view>
|
|
796
|
+
{/if}
|
|
797
|
+
</symbiote-view>
|
|
798
|
+
{/if}
|
|
799
|
+
{#if plan.forcedStickyCell && cell.index === plan.forcedStickyCell.index && plan.gapExtent > EMPTY_OFFSET}
|
|
800
|
+
<symbiote-view
|
|
801
|
+
p={{
|
|
802
|
+
style: narrowed.horizontal
|
|
803
|
+
? { width: plan.gapExtent }
|
|
804
|
+
: { height: plan.gapExtent },
|
|
805
|
+
}}
|
|
806
|
+
/>
|
|
807
|
+
{/if}
|
|
808
|
+
{/each}
|
|
809
|
+
{#if plan.trailingExtent > EMPTY_OFFSET}
|
|
810
|
+
<symbiote-view
|
|
811
|
+
p={{
|
|
812
|
+
style: narrowed.horizontal
|
|
813
|
+
? { width: plan.trailingExtent }
|
|
814
|
+
: { height: plan.trailingExtent },
|
|
815
|
+
}}
|
|
816
|
+
/>
|
|
817
|
+
{/if}
|
|
818
|
+
{/if}
|
|
819
|
+
{#if props.footer}
|
|
820
|
+
<symbiote-view p={{}}>
|
|
821
|
+
{@render props.footer()}
|
|
822
|
+
</symbiote-view>
|
|
823
|
+
{/if}
|
|
682
824
|
{/snippet}
|
|
683
825
|
|
|
684
|
-
{#snippet scrollBody()}
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
-->
|
|
826
|
+
{#snippet scrollBody()}
|
|
827
|
+
<!--
|
|
828
|
+
Same as listBody() above: on iOS the non-wrapping RefreshControl and the content container are
|
|
829
|
+
siblings of one parent, and the gap between them is dropped before it reaches Fabric.
|
|
830
|
+
-->
|
|
831
|
+
{#if !shouldWrapRefreshControl && refreshControlProps !== undefined}
|
|
832
|
+
<RefreshControl {...refreshControlProps} />
|
|
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>
|
|
842
|
+
{/if}
|
|
843
|
+
{/snippet}
|
|
689
844
|
|
|
690
845
|
{#if shouldWrapRefreshControl && refreshControlProps !== undefined}
|
|
691
846
|
<RefreshControl {...refreshControlProps} style={layoutSplit?.outer}>
|
|
692
847
|
{#if narrowed.horizontal}
|
|
693
|
-
<symbiote-horizontal-scroll-view p={outerBag} bind:this={hostShim}>
|
|
848
|
+
<symbiote-horizontal-scroll-view p={outerBag} bind:this={hostShim}>
|
|
849
|
+
{@render scrollBody()}
|
|
850
|
+
</symbiote-horizontal-scroll-view>
|
|
694
851
|
{:else}
|
|
695
|
-
<symbiote-scroll-view p={outerBag} bind:this={hostShim}>
|
|
852
|
+
<symbiote-scroll-view p={outerBag} bind:this={hostShim}>
|
|
853
|
+
{@render scrollBody()}
|
|
854
|
+
</symbiote-scroll-view>
|
|
696
855
|
{/if}
|
|
697
856
|
</RefreshControl>
|
|
698
857
|
{:else if narrowed.horizontal}
|
|
699
|
-
<symbiote-horizontal-scroll-view p={outerBag} bind:this={hostShim}>
|
|
858
|
+
<symbiote-horizontal-scroll-view p={outerBag} bind:this={hostShim}>
|
|
859
|
+
{@render scrollBody()}
|
|
860
|
+
</symbiote-horizontal-scroll-view>
|
|
700
861
|
{:else}
|
|
701
|
-
<symbiote-scroll-view p={outerBag} bind:this={hostShim}>
|
|
862
|
+
<symbiote-scroll-view p={outerBag} bind:this={hostShim}>
|
|
863
|
+
{@render scrollBody()}
|
|
864
|
+
</symbiote-scroll-view>
|
|
702
865
|
{/if}
|
|
@@ -47,19 +47,23 @@ export function pickAccessibilityProps(props) {
|
|
|
47
47
|
picked.accessibilityElementsHidden = resolved.accessibilityElementsHidden;
|
|
48
48
|
}
|
|
49
49
|
if (resolved.accessibilityIgnoresInvertColors !== undefined) {
|
|
50
|
-
picked.accessibilityIgnoresInvertColors =
|
|
50
|
+
picked.accessibilityIgnoresInvertColors =
|
|
51
|
+
resolved.accessibilityIgnoresInvertColors;
|
|
51
52
|
}
|
|
52
53
|
if (resolved.accessibilityLanguage !== undefined) {
|
|
53
54
|
picked.accessibilityLanguage = resolved.accessibilityLanguage;
|
|
54
55
|
}
|
|
55
56
|
if (resolved.accessibilityRespondsToUserInteraction !== undefined) {
|
|
56
|
-
picked.accessibilityRespondsToUserInteraction =
|
|
57
|
+
picked.accessibilityRespondsToUserInteraction =
|
|
58
|
+
resolved.accessibilityRespondsToUserInteraction;
|
|
57
59
|
}
|
|
58
60
|
if (resolved.accessibilityShowsLargeContentViewer !== undefined) {
|
|
59
|
-
picked.accessibilityShowsLargeContentViewer =
|
|
61
|
+
picked.accessibilityShowsLargeContentViewer =
|
|
62
|
+
resolved.accessibilityShowsLargeContentViewer;
|
|
60
63
|
}
|
|
61
64
|
if (resolved.accessibilityLargeContentTitle !== undefined) {
|
|
62
|
-
picked.accessibilityLargeContentTitle =
|
|
65
|
+
picked.accessibilityLargeContentTitle =
|
|
66
|
+
resolved.accessibilityLargeContentTitle;
|
|
63
67
|
}
|
|
64
68
|
if (resolved.onAccessibilityAction !== undefined)
|
|
65
69
|
picked.onAccessibilityAction = resolved.onAccessibilityAction;
|
|
@@ -13,7 +13,11 @@
|
|
|
13
13
|
ISection,
|
|
14
14
|
} from './virtualized-section-list-props';
|
|
15
15
|
|
|
16
|
-
export type {
|
|
16
|
+
export type {
|
|
17
|
+
IVirtualizedSectionListProps,
|
|
18
|
+
IVirtualizedSectionListHandle,
|
|
19
|
+
ISection,
|
|
20
|
+
};
|
|
17
21
|
</script>
|
|
18
22
|
|
|
19
23
|
<script lang="ts" generics="ItemT">
|
|
@@ -30,7 +34,10 @@
|
|
|
30
34
|
import { Platform, dlog, type ISymbioteNode } from '@symbiote-native/engine';
|
|
31
35
|
import VirtualizedList from '../virtualized-list/index.svelte';
|
|
32
36
|
import { pickAccessibilityProps } from '../virtualized-list/virtualized-list-props';
|
|
33
|
-
import type {
|
|
37
|
+
import type {
|
|
38
|
+
IVirtualizedListHandle,
|
|
39
|
+
IScrollViewHandle,
|
|
40
|
+
} from '../virtualized-list/virtualized-list-props';
|
|
34
41
|
import type { IVirtualizedSectionListProps as IProps } from './virtualized-section-list-props';
|
|
35
42
|
import { pickAttachmentProps } from '../../runes/attachments';
|
|
36
43
|
|
|
@@ -50,7 +57,11 @@
|
|
|
50
57
|
|
|
51
58
|
// RN sticks section headers by default only on iOS; Android does not unless asked.
|
|
52
59
|
const stickyHeaderIndices = $derived(
|
|
53
|
-
resolveStickySectionHeaders(
|
|
60
|
+
resolveStickySectionHeaders(
|
|
61
|
+
props.stickySectionHeadersEnabled,
|
|
62
|
+
headerIndices,
|
|
63
|
+
Platform.OS,
|
|
64
|
+
),
|
|
54
65
|
);
|
|
55
66
|
// DIAGNOSTIC (2026-08-13, tracking why stickyHeaderIndices comes back undefined on Android
|
|
56
67
|
// despite the demo passing the bare `stickySectionHeadersEnabled` shorthand): the previous
|
|
@@ -78,11 +89,35 @@
|
|
|
78
89
|
function getEntryCount(): number {
|
|
79
90
|
return entries.length;
|
|
80
91
|
}
|
|
81
|
-
function entryKeyExtractor(
|
|
92
|
+
function entryKeyExtractor(
|
|
93
|
+
entry: ISectionEntry<ItemT>,
|
|
94
|
+
index: number,
|
|
95
|
+
): string {
|
|
82
96
|
return sectionEntryKey(entry, index, props.keyExtractor);
|
|
83
97
|
}
|
|
84
98
|
|
|
85
|
-
|
|
99
|
+
// Hand the callback `sections`, not the entries: RN's inner VirtualizedList gets
|
|
100
|
+
// `data={this.props.sections}` (VirtualizedSectionList.js:216) while ours streams the FLATTENED
|
|
101
|
+
// entries, so the same user code would otherwise see a different argument here than on RN.
|
|
102
|
+
//
|
|
103
|
+
// UPSTREAM-DIVERGENCE(react-native): the flat INDEX matches RN's (two rows per section, header
|
|
104
|
+
// and footer) only while the `sectionSeparator` snippet is unset. With it, flattenSections emits
|
|
105
|
+
// an extra 'section-separator' row per boundary that RN renders inside the neighbouring cell, so
|
|
106
|
+
// indices shift by one per boundary from the second section on. Deliberate - that row is how this
|
|
107
|
+
// adapter paints the separator; a caller combining the two must account for it.
|
|
108
|
+
const entryItemLayout = $derived.by(() => {
|
|
109
|
+
const getItemLayout = props.getItemLayout;
|
|
110
|
+
if (getItemLayout === undefined) return undefined;
|
|
111
|
+
return (
|
|
112
|
+
_entries: unknown,
|
|
113
|
+
index: number,
|
|
114
|
+
): { length: number; offset: number; index: number } =>
|
|
115
|
+
getItemLayout(props.sections, index);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
function entrySeparatorProps(
|
|
119
|
+
entryProps: ISeparatorProps<ISectionEntry<ItemT>>,
|
|
120
|
+
): ISeparatorProps<ItemT> {
|
|
86
121
|
return {
|
|
87
122
|
...entryProps,
|
|
88
123
|
leadingItem: unwrapEntryItem(entryProps.leadingItem),
|
|
@@ -100,9 +135,15 @@
|
|
|
100
135
|
viewPosition?: number;
|
|
101
136
|
animated?: boolean;
|
|
102
137
|
}): void {
|
|
103
|
-
const flatIndex = scrollLocationToFlatIndex(
|
|
138
|
+
const flatIndex = scrollLocationToFlatIndex(
|
|
139
|
+
headerIndices,
|
|
140
|
+
params.sectionIndex,
|
|
141
|
+
params.itemIndex,
|
|
142
|
+
);
|
|
104
143
|
if (flatIndex === undefined) {
|
|
105
|
-
dlog(
|
|
144
|
+
dlog(
|
|
145
|
+
`VirtualizedSectionList scrollToLocation: section ${params.sectionIndex} out of range`,
|
|
146
|
+
);
|
|
106
147
|
return;
|
|
107
148
|
}
|
|
108
149
|
dlog(
|
|
@@ -155,7 +196,12 @@
|
|
|
155
196
|
{:else if entry.kind === 'section-separator'}
|
|
156
197
|
{@render props.sectionSeparator?.()}
|
|
157
198
|
{:else}
|
|
158
|
-
{@render props.item({
|
|
199
|
+
{@render props.item({
|
|
200
|
+
item: entry.item,
|
|
201
|
+
index: entry.itemIndex,
|
|
202
|
+
section: entry.section,
|
|
203
|
+
separators,
|
|
204
|
+
})}
|
|
159
205
|
{/if}
|
|
160
206
|
{/snippet}
|
|
161
207
|
|
|
@@ -176,7 +222,8 @@
|
|
|
176
222
|
footer={props.footer}
|
|
177
223
|
empty={props.empty}
|
|
178
224
|
keyExtractor={entryKeyExtractor}
|
|
179
|
-
|
|
225
|
+
getItemLayout={entryItemLayout}
|
|
226
|
+
{stickyHeaderIndices}
|
|
180
227
|
inverted={props.inverted}
|
|
181
228
|
extraData={props.extraData}
|
|
182
229
|
onEndReached={props.onEndReached}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { IVirtualizedSectionListProps, IVirtualizedSectionListHandle, ISection } from './virtualized-section-list-props';
|
|
2
|
-
export type { IVirtualizedSectionListProps, IVirtualizedSectionListHandle, ISection };
|
|
2
|
+
export type { IVirtualizedSectionListProps, IVirtualizedSectionListHandle, ISection, };
|
|
3
3
|
import { type ISymbioteNode } from '@symbiote-native/engine';
|
|
4
4
|
import type { IScrollViewHandle } from '../virtualized-list/virtualized-list-props';
|
|
5
5
|
import type { IVirtualizedSectionListProps as IProps } from './virtualized-section-list-props';
|
|
@@ -25,6 +25,11 @@ export interface IVirtualizedSectionListProps<ItemT> extends IAccessibilityProps
|
|
|
25
25
|
footer?: Snippet;
|
|
26
26
|
empty?: Snippet;
|
|
27
27
|
keyExtractor?: (item: ItemT, index: number) => string;
|
|
28
|
+
getItemLayout?: (data: ReadonlyArray<ISection<ItemT>> | null, index: number) => {
|
|
29
|
+
length: number;
|
|
30
|
+
offset: number;
|
|
31
|
+
index: number;
|
|
32
|
+
};
|
|
28
33
|
stickySectionHeadersEnabled?: boolean;
|
|
29
34
|
extraData?: unknown;
|
|
30
35
|
onEndReached?: (info: {
|