@symbiote-native/angular 0.7.0 → 0.8.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 +3 -3
- package/build/angular/bootstrap.js +1 -1
- package/build/angular/bootstrap.js.map +1 -1
- package/build/angular/components/activity-indicator/index.android.js +7 -2
- package/build/angular/components/activity-indicator/index.android.js.map +1 -1
- package/build/angular/components/activity-indicator/index.ios.js +3 -1
- package/build/angular/components/activity-indicator/index.ios.js.map +1 -1
- package/build/angular/components/activity-indicator/shared.d.ts +1 -1
- package/build/angular/components/activity-indicator/shared.js +1 -1
- package/build/angular/components/activity-indicator/shared.js.map +1 -1
- package/build/angular/components/button.js +13 -5
- package/build/angular/components/button.js.map +1 -1
- package/build/angular/components/flat-list/index.d.ts +1 -1
- package/build/angular/components/flat-list/index.js +47 -14
- package/build/angular/components/flat-list/index.js.map +1 -1
- package/build/angular/components/image/index.android.js +4 -2
- package/build/angular/components/image/index.android.js.map +1 -1
- package/build/angular/components/image/index.ios.js +4 -2
- package/build/angular/components/image/index.ios.js.map +1 -1
- package/build/angular/components/image/shared.js +18 -6
- package/build/angular/components/image/shared.js.map +1 -1
- package/build/angular/components/image-background.js +22 -8
- package/build/angular/components/image-background.js.map +1 -1
- package/build/angular/components/input-accessory-view/index.js +7 -2
- package/build/angular/components/input-accessory-view/index.js.map +1 -1
- package/build/angular/components/keyboard-avoiding-view/index.d.ts +1 -0
- package/build/angular/components/keyboard-avoiding-view/index.js +39 -9
- package/build/angular/components/keyboard-avoiding-view/index.js.map +1 -1
- package/build/angular/components/modal/index.js +7 -2
- package/build/angular/components/modal/index.js.map +1 -1
- package/build/angular/components/pressable/index.d.ts +1 -1
- package/build/angular/components/pressable/index.js +6 -2
- package/build/angular/components/pressable/index.js.map +1 -1
- package/build/angular/components/refresh-control/index.js +3 -1
- package/build/angular/components/refresh-control/index.js.map +1 -1
- package/build/angular/components/safe-area-view/index.js +3 -1
- package/build/angular/components/safe-area-view/index.js.map +1 -1
- package/build/angular/components/scroll-view/index.android.js +44 -14
- package/build/angular/components/scroll-view/index.android.js.map +1 -1
- package/build/angular/components/scroll-view/index.ios.js +13 -5
- package/build/angular/components/scroll-view/index.ios.js.map +1 -1
- package/build/angular/components/scroll-view/projection.d.ts +32 -0
- package/build/angular/components/scroll-view/projection.js +212 -47
- package/build/angular/components/scroll-view/projection.js.map +1 -1
- package/build/angular/components/scroll-view/shared.js +28 -7
- package/build/angular/components/scroll-view/shared.js.map +1 -1
- package/build/angular/components/scroll-view/sticky-header.js +7 -3
- package/build/angular/components/scroll-view/sticky-header.js.map +1 -1
- package/build/angular/components/section-list/index.d.ts +6 -1
- package/build/angular/components/section-list/index.js +63 -18
- package/build/angular/components/section-list/index.js.map +1 -1
- package/build/angular/components/switch/index.android.js +17 -3
- package/build/angular/components/switch/index.android.js.map +1 -1
- package/build/angular/components/switch/index.ios.js +17 -3
- package/build/angular/components/switch/index.ios.js.map +1 -1
- package/build/angular/components/switch/shared.d.ts +1 -1
- package/build/angular/components/switch/shared.js +5 -2
- package/build/angular/components/switch/shared.js.map +1 -1
- package/build/angular/components/text-input.d.ts +1 -1
- package/build/angular/components/text-input.js +25 -5
- package/build/angular/components/text-input.js.map +1 -1
- package/build/angular/components/touchable/index.d.ts +29 -7
- package/build/angular/components/touchable/index.js +246 -69
- package/build/angular/components/touchable/index.js.map +1 -1
- package/build/angular/components/touchable-native-feedback/index.js +2 -2
- package/build/angular/components/touchable-native-feedback/index.js.map +1 -1
- package/build/angular/components/virtualized-list/directives.js +4 -0
- package/build/angular/components/virtualized-list/directives.js.map +1 -1
- package/build/angular/components/virtualized-list/index.d.ts +5 -2
- package/build/angular/components/virtualized-list/index.js +197 -60
- package/build/angular/components/virtualized-list/index.js.map +1 -1
- package/build/angular/components/virtualized-section-list/directives.js.map +1 -1
- package/build/angular/components/virtualized-section-list/index.d.ts +18 -1
- package/build/angular/components/virtualized-section-list/index.js +90 -21
- package/build/angular/components/virtualized-section-list/index.js.map +1 -1
- package/build/angular/components.d.ts +1 -1
- package/build/angular/components.js.map +1 -1
- package/build/angular/create-portal/index.js +5 -1
- package/build/angular/create-portal/index.js.map +1 -1
- package/build/angular/create-tunnel/index.js.map +1 -1
- package/build/angular/descriptor-to-angular/index.js +3 -1
- package/build/angular/descriptor-to-angular/index.js.map +1 -1
- package/build/angular/diagnostics.d.ts +33 -0
- package/build/angular/diagnostics.js +82 -0
- package/build/angular/diagnostics.js.map +1 -0
- package/build/angular/host-instance/index.js +1 -1
- package/build/angular/host-instance/index.js.map +1 -1
- package/build/angular/index.d.ts +5 -2
- package/build/angular/index.js +15 -1
- package/build/angular/index.js.map +1 -1
- package/build/angular/modules/animated/create-animated-component.js +27 -9
- package/build/angular/modules/animated/create-animated-component.js.map +1 -1
- package/build/angular/modules/app-registry/index.js.map +1 -1
- package/build/angular/modules/status-bar/index.js +1 -1
- package/build/angular/modules/status-bar/index.js.map +1 -1
- package/build/angular/primitives/index.d.ts +19 -2
- package/build/angular/primitives/index.js +39 -3
- package/build/angular/primitives/index.js.map +1 -1
- package/build/angular/primitives/shared.d.ts +9 -1
- package/build/angular/primitives/shared.js +49 -3
- package/build/angular/primitives/shared.js.map +1 -1
- package/build/angular/render/index.js +48 -5
- package/build/angular/render/index.js.map +1 -1
- package/build/angular/renderer/index.d.ts +1 -0
- package/build/angular/renderer/index.js +37 -2
- package/build/angular/renderer/index.js.map +1 -1
- package/build/angular/services/color-scheme.service.js.map +1 -1
- package/build/angular/services/window-dimensions.service.js +1 -1
- package/build/angular/services/window-dimensions.service.js.map +1 -1
- package/package.json +5 -5
- package/src/bootstrap.ts +4 -1
- package/src/components/activity-indicator/index.android.ts +7 -2
- package/src/components/activity-indicator/index.ios.ts +3 -1
- package/src/components/activity-indicator/shared.ts +32 -13
- package/src/components/button.ts +19 -6
- package/src/components/flat-list/index.ts +91 -29
- package/src/components/image/index.android.ts +9 -2
- package/src/components/image/index.ios.ts +9 -2
- package/src/components/image/shared.ts +58 -19
- package/src/components/image-background.ts +29 -10
- package/src/components/input-accessory-view/index.ts +27 -10
- package/src/components/keyboard-avoiding-view/index.ts +55 -15
- package/src/components/modal/index.ts +22 -8
- package/src/components/pressable/index.ts +51 -21
- package/src/components/refresh-control/index.ts +22 -8
- package/src/components/safe-area-view/index.ts +16 -7
- package/src/components/scroll-view/index.android.ts +31 -9
- package/src/components/scroll-view/index.ios.ts +17 -4
- package/src/components/scroll-view/projection.ts +290 -63
- package/src/components/scroll-view/shared.ts +93 -26
- package/src/components/scroll-view/sticky-header.ts +25 -8
- package/src/components/section-list/index.ts +61 -19
- package/src/components/switch/index.android.ts +14 -3
- package/src/components/switch/index.ios.ts +14 -3
- package/src/components/switch/shared.ts +15 -4
- package/src/components/text-input.ts +45 -13
- package/src/components/touchable/index.ts +296 -72
- package/src/components/touchable-native-feedback/index.ts +32 -10
- package/src/components/virtualized-list/directives.ts +19 -5
- package/src/components/virtualized-list/index.ts +274 -72
- package/src/components/virtualized-section-list/directives.ts +6 -3
- package/src/components/virtualized-section-list/index.ts +146 -34
- package/src/components.ts +4 -1
- package/src/create-portal/index.ts +8 -2
- package/src/create-tunnel/index.ts +3 -1
- package/src/descriptor-to-angular/index.ts +29 -8
- package/src/diagnostics.ts +138 -0
- package/src/host-instance/index.ts +9 -3
- package/src/index.ts +31 -2
- package/src/modules/animated/create-animated-component.ts +44 -15
- package/src/modules/app-registry/index.ts +3 -1
- package/src/modules/status-bar/index.ts +13 -4
- package/src/primitives/index.ts +46 -2
- package/src/primitives/shared.ts +70 -9
- package/src/render/index.ts +56 -6
- package/src/renderer/index.ts +76 -14
- package/src/services/color-scheme.service.ts +3 -1
- package/src/services/window-dimensions.service.ts +11 -4
|
@@ -56,10 +56,12 @@ import {
|
|
|
56
56
|
INVERTED_Y_STYLE,
|
|
57
57
|
buildListPlan,
|
|
58
58
|
buildViewabilityPairs,
|
|
59
|
+
computeWindow,
|
|
59
60
|
createInitialListState,
|
|
60
61
|
isSeparatorGapInRange,
|
|
61
62
|
listEffectSignature,
|
|
62
63
|
readLayoutLength,
|
|
64
|
+
readLayoutOffset,
|
|
63
65
|
readScrollOffset,
|
|
64
66
|
reduceList,
|
|
65
67
|
resolveAccessibilityProps,
|
|
@@ -87,9 +89,14 @@ import {
|
|
|
87
89
|
type ISymbioteNode,
|
|
88
90
|
type IViewStyle,
|
|
89
91
|
} from '@symbiote-native/engine';
|
|
92
|
+
import { countAngular } from '../../diagnostics';
|
|
90
93
|
import { ScrollView } from '../scroll-view';
|
|
91
94
|
import { RefreshControl } from '../refresh-control';
|
|
92
|
-
import {
|
|
95
|
+
import {
|
|
96
|
+
anchorHostStyle,
|
|
97
|
+
SymbioteStyleInputDirective,
|
|
98
|
+
ViewHost,
|
|
99
|
+
} from '../../primitives';
|
|
93
100
|
import {
|
|
94
101
|
VListEmptyDirective,
|
|
95
102
|
VListFooterDirective,
|
|
@@ -126,7 +133,8 @@ export type { IVListItemContext, IVListSeparatorContext } from './directives';
|
|
|
126
133
|
// element-returning props (renderItem, ListHeader/Footer/Empty Component, ItemSeparatorComponent):
|
|
127
134
|
// those are the per-adapter children/render fields and become `<ng-template>` directives in Angular.
|
|
128
135
|
// Everything agnostic is the SAME surface.
|
|
129
|
-
export interface IVirtualizedListProps<ItemT>
|
|
136
|
+
export interface IVirtualizedListProps<ItemT>
|
|
137
|
+
extends IAccessibilityProps, IAriaProps {
|
|
130
138
|
data: unknown;
|
|
131
139
|
getItem: (data: unknown, index: number) => ItemT;
|
|
132
140
|
getItemCount: (data: unknown) => number;
|
|
@@ -205,7 +213,9 @@ interface IWindowCell<ItemT> {
|
|
|
205
213
|
@Component({
|
|
206
214
|
selector: 'VirtualizedList',
|
|
207
215
|
standalone: true,
|
|
208
|
-
hostDirectives: [
|
|
216
|
+
hostDirectives: [
|
|
217
|
+
{ directive: SymbioteStyleInputDirective, inputs: ['style'] },
|
|
218
|
+
],
|
|
209
219
|
schemas: [CUSTOM_ELEMENTS_SCHEMA],
|
|
210
220
|
imports: [ScrollView, RefreshControl, VListOutletDirective, ViewHost],
|
|
211
221
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
@@ -224,12 +234,20 @@ interface IWindowCell<ItemT> {
|
|
|
224
234
|
[accessibilityValue]="foldedAccessibility().accessibilityValue"
|
|
225
235
|
[accessibilityActions]="foldedAccessibility().accessibilityActions"
|
|
226
236
|
[accessibilityLabelledBy]="foldedAccessibility().accessibilityLabelledBy"
|
|
227
|
-
[importantForAccessibility]="
|
|
237
|
+
[importantForAccessibility]="
|
|
238
|
+
foldedAccessibility().importantForAccessibility
|
|
239
|
+
"
|
|
228
240
|
[accessibilityLiveRegion]="foldedAccessibility().accessibilityLiveRegion"
|
|
229
241
|
[screenReaderFocusable]="foldedAccessibility().screenReaderFocusable"
|
|
230
|
-
[accessibilityViewIsModal]="
|
|
231
|
-
|
|
232
|
-
|
|
242
|
+
[accessibilityViewIsModal]="
|
|
243
|
+
foldedAccessibility().accessibilityViewIsModal
|
|
244
|
+
"
|
|
245
|
+
[accessibilityElementsHidden]="
|
|
246
|
+
foldedAccessibility().accessibilityElementsHidden
|
|
247
|
+
"
|
|
248
|
+
[accessibilityIgnoresInvertColors]="
|
|
249
|
+
foldedAccessibility().accessibilityIgnoresInvertColors
|
|
250
|
+
"
|
|
233
251
|
[accessibilityLanguage]="foldedAccessibility().accessibilityLanguage"
|
|
234
252
|
[accessibilityRespondsToUserInteraction]="
|
|
235
253
|
foldedAccessibility().accessibilityRespondsToUserInteraction
|
|
@@ -237,7 +255,9 @@ interface IWindowCell<ItemT> {
|
|
|
237
255
|
[accessibilityShowsLargeContentViewer]="
|
|
238
256
|
foldedAccessibility().accessibilityShowsLargeContentViewer
|
|
239
257
|
"
|
|
240
|
-
[accessibilityLargeContentTitle]="
|
|
258
|
+
[accessibilityLargeContentTitle]="
|
|
259
|
+
foldedAccessibility().accessibilityLargeContentTitle
|
|
260
|
+
"
|
|
241
261
|
(accessibilityAction)="accessibilityActionTick($event)"
|
|
242
262
|
(accessibilityTap)="accessibilityTapTick($event)"
|
|
243
263
|
(magicTap)="magicTapTick($event)"
|
|
@@ -293,21 +313,29 @@ interface IWindowCell<ItemT> {
|
|
|
293
313
|
@if (gapSpacerStyle !== null) {
|
|
294
314
|
<symbiote-view [style]="gapSpacerStyle"></symbiote-view>
|
|
295
315
|
}
|
|
316
|
+
<!-- The separator sits INSIDE the measuring view, as RN's own cell renderer places it
|
|
317
|
+
(VirtualizedListCellRenderer.js:218-221). As a sibling it is an extra flex child, so
|
|
318
|
+
the chrome between two cells is gap + separator + gap while a spacer collapsing that
|
|
319
|
+
region replaces it with a single gap — every cell below the leading spacer then lands
|
|
320
|
+
short by (separator + gap) and the content jumps by that much whenever the window's
|
|
321
|
+
first index moves. Measured at exactly 17px on device 2026-08-19; see
|
|
322
|
+
.claude/rules/list-geometry-feedback-loop.md. -->
|
|
296
323
|
@for (cell of windowCells; track cell.key) {
|
|
297
|
-
<symbiote-view
|
|
324
|
+
<symbiote-view
|
|
325
|
+
(layout)="handleCellLayout(cell.measure, $event)"
|
|
326
|
+
[style]="cellStyle"
|
|
327
|
+
>
|
|
298
328
|
<ng-container
|
|
299
329
|
[vListOutlet]="itemDir?.templateRef"
|
|
300
330
|
[vListOutletContext]="cell.context"
|
|
301
331
|
></ng-container>
|
|
302
|
-
|
|
303
|
-
@if (cell.separatorContext !== undefined) {
|
|
304
|
-
<symbiote-view>
|
|
332
|
+
@if (cell.separatorContext !== undefined) {
|
|
305
333
|
<ng-container
|
|
306
334
|
[vListOutlet]="separatorDir?.templateRef"
|
|
307
335
|
[vListOutletContext]="cell.separatorContext"
|
|
308
336
|
></ng-container>
|
|
309
|
-
|
|
310
|
-
|
|
337
|
+
}
|
|
338
|
+
</symbiote-view>
|
|
311
339
|
}
|
|
312
340
|
@if (trailingSpacerStyle !== null) {
|
|
313
341
|
<symbiote-view [style]="trailingSpacerStyle"></symbiote-view>
|
|
@@ -334,10 +362,16 @@ export class VirtualizedList<ItemT = unknown>
|
|
|
334
362
|
// The list's edge/viewability/failure events as real Angular events: `(endReached)="…"`, not
|
|
335
363
|
// `[onEndReached]="…"`. See handleRefresh/accessibility*Tick below for how the still-callback-
|
|
336
364
|
// shaped ScrollView/RefreshControl @Input()s are fed from these.
|
|
337
|
-
@Output() readonly endReached = new EventEmitter<{
|
|
338
|
-
|
|
365
|
+
@Output() readonly endReached = new EventEmitter<{
|
|
366
|
+
distanceFromEnd: number;
|
|
367
|
+
}>();
|
|
368
|
+
@Output() readonly startReached = new EventEmitter<{
|
|
369
|
+
distanceFromStart: number;
|
|
370
|
+
}>();
|
|
339
371
|
@Output() readonly refresh = new EventEmitter<void>();
|
|
340
|
-
@Output() readonly viewableItemsChanged = new EventEmitter<
|
|
372
|
+
@Output() readonly viewableItemsChanged = new EventEmitter<
|
|
373
|
+
IViewableItemsChangedInfo<ItemT>
|
|
374
|
+
>();
|
|
341
375
|
@Output() readonly scrollToIndexFailed = new EventEmitter<{
|
|
342
376
|
index: number;
|
|
343
377
|
highestMeasuredFrameIndex: number;
|
|
@@ -371,7 +405,8 @@ export class VirtualizedList<ItemT = unknown>
|
|
|
371
405
|
// (no wrapper) falls back to this component's own `.observed`, unchanged from before.
|
|
372
406
|
@Input() refreshRequested?: boolean;
|
|
373
407
|
@Input() viewabilityConfig?: IViewabilityConfig;
|
|
374
|
-
@Input()
|
|
408
|
+
@Input()
|
|
409
|
+
viewabilityConfigCallbackPairs?: IViewabilityConfigCallbackPair<ItemT>[];
|
|
375
410
|
@Input() initialNumToRender?: number;
|
|
376
411
|
@Input() initialScrollIndex?: number;
|
|
377
412
|
@Input() maxToRenderPerBatch?: number;
|
|
@@ -402,8 +437,10 @@ export class VirtualizedList<ItemT = unknown>
|
|
|
402
437
|
@Input() accessibilityValue?: IAccessibilityProps['accessibilityValue'];
|
|
403
438
|
@Input() accessibilityActions?: IAccessibilityProps['accessibilityActions'];
|
|
404
439
|
@Input() accessibilityLabelledBy?: string | string[];
|
|
405
|
-
@Input()
|
|
406
|
-
|
|
440
|
+
@Input()
|
|
441
|
+
importantForAccessibility?: IAccessibilityProps['importantForAccessibility'];
|
|
442
|
+
@Input()
|
|
443
|
+
accessibilityLiveRegion?: IAccessibilityProps['accessibilityLiveRegion'];
|
|
407
444
|
@Input() screenReaderFocusable?: boolean;
|
|
408
445
|
@Input() accessibilityViewIsModal?: boolean;
|
|
409
446
|
@Input() accessibilityElementsHidden?: boolean;
|
|
@@ -435,7 +472,8 @@ export class VirtualizedList<ItemT = unknown>
|
|
|
435
472
|
@ContentChild(VListHeaderDirective) headerDir?: VListHeaderDirective;
|
|
436
473
|
@ContentChild(VListFooterDirective) footerDir?: VListFooterDirective;
|
|
437
474
|
@ContentChild(VListEmptyDirective) emptyDir?: VListEmptyDirective;
|
|
438
|
-
@ContentChild(VListSeparatorDirective)
|
|
475
|
+
@ContentChild(VListSeparatorDirective)
|
|
476
|
+
separatorDir?: VListSeparatorDirective<ItemT>;
|
|
439
477
|
|
|
440
478
|
// The composed inner scroll view. Its instance IS an IScrollViewHandle (scrollTo / scrollToEnd /
|
|
441
479
|
// flashScrollIndicators / getScrollNode), so the imperative handle delegates straight to it.
|
|
@@ -462,7 +500,8 @@ export class VirtualizedList<ItemT = unknown>
|
|
|
462
500
|
resolvedStyle: IViewStyle | undefined = undefined;
|
|
463
501
|
resolvedContentContainerStyle: IStyleProp<IViewStyle> | undefined = undefined;
|
|
464
502
|
resolvedMaintainVisibleContentPosition:
|
|
465
|
-
{ minIndexForVisible: number; autoscrollToTopThreshold?: number }
|
|
503
|
+
| { minIndexForVisible: number; autoscrollToTopThreshold?: number }
|
|
504
|
+
| undefined = undefined;
|
|
466
505
|
// The offset we are imperatively driving native to before the scroll handle attaches (rides down
|
|
467
506
|
// as contentOffset). A fresh object identity each push so the commit re-applies a repeated value.
|
|
468
507
|
commandedOffset: { x: number; y: number } | undefined = undefined;
|
|
@@ -471,20 +510,41 @@ export class VirtualizedList<ItemT = unknown>
|
|
|
471
510
|
// measured lengths, committed window, edge/viewability dedup, MVCP anchor — all in IListState,
|
|
472
511
|
// driven by the shared reduceList. `renderVersion` bumps whenever a transition changes render
|
|
473
512
|
// state, so ngDoCheck's recompute-dedup can detect it (listState is a plain object, not tracked).
|
|
474
|
-
private readonly listState: IListState<ItemT> =
|
|
513
|
+
private readonly listState: IListState<ItemT> =
|
|
514
|
+
createInitialListState<ItemT>();
|
|
475
515
|
private renderVersion = EMPTY_OFFSET;
|
|
476
516
|
|
|
477
|
-
private readonly cellMeasures = new Map<
|
|
478
|
-
|
|
517
|
+
private readonly cellMeasures = new Map<
|
|
518
|
+
number,
|
|
519
|
+
(event: ISymbioteEvent) => void
|
|
520
|
+
>();
|
|
521
|
+
// A cell's context object is the ONLY thing its stamped view reads, and VListOutletDirective
|
|
522
|
+
// refreshes that view whenever the object's IDENTITY changes. recomputeView runs on every frame
|
|
523
|
+
// the window moves, so minting a fresh context per cell made a four-row slide cost a refresh of
|
|
524
|
+
// EVERY cell in the window. MEASURED on PATH B geometry (544 entries, 320px viewport, RN
|
|
525
|
+
// defaults), per fling frame: 233-cell window, 4 cells genuinely entering, 228 outlet updates —
|
|
526
|
+
// each one a copyContextFields plus a markForCheck that walks to the root. Solid, same geometry,
|
|
527
|
+
// same shared reducer, same engine output: 4. Handing back the SAME object for a cell whose item
|
|
528
|
+
// and index did not move keeps `[vListOutletContext]` reference-equal, so ngOnChanges never fires
|
|
529
|
+
// for it. The fast adapters get this by construction — Solid's <For> is keyed on the cell KEY
|
|
530
|
+
// strings precisely so "the plan's freshly-built cell objects" cannot rebuild every row
|
|
531
|
+
// (adapters/solid/src/components/virtualized-list/shared.tsx, the cellKeys memo).
|
|
532
|
+
private cellContexts = new Map<string, IVListItemContext<ItemT>>();
|
|
533
|
+
private nextCellContexts = new Map<string, IVListItemContext<ItemT>>();
|
|
534
|
+
// Everything a cell's CONTENT can depend on that is not its own item identity. RN's contract for
|
|
535
|
+
// "re-render the cells, the data object itself did not change" is extraData; without it here an
|
|
536
|
+
// extraData flip would reuse every context and repaint nothing.
|
|
537
|
+
private cellContextEpoch: unknown[] | undefined = undefined;
|
|
538
|
+
private readonly separatorOverrides = new Map<
|
|
539
|
+
number,
|
|
540
|
+
Partial<ISeparatorProps<unknown>>
|
|
541
|
+
>();
|
|
479
542
|
// The adapter-owned debounce (minimumViewTime) and incremental-fill timers; the reducer only
|
|
480
543
|
// hands back a delay.
|
|
481
544
|
private viewableTimer: ReturnType<typeof setTimeout> | null = null;
|
|
482
545
|
private batchTimer: ReturnType<typeof setTimeout> | null = null;
|
|
483
546
|
// Dedupes the after-commit effects: they run only when the windowing signature changed.
|
|
484
547
|
private lastEffectSignature = '';
|
|
485
|
-
// Dedupes dispatch's markForCheck against the RENDERED window (see dispatch below). Distinct from
|
|
486
|
-
// lastEffectSignature, which gates the batch-fill timer and deliberately includes scrollOffset.
|
|
487
|
-
private lastRenderSignature = '';
|
|
488
548
|
// Dedupes ngDoCheck's own recompute. WITHOUT this, recomputeView() unconditionally rebuilds
|
|
489
549
|
// windowCells (and each cell's `separators` handle — fresh closures every call) on EVERY CD pass,
|
|
490
550
|
// including ones triggered by something else entirely in the app. A fresh context object flows
|
|
@@ -543,8 +603,10 @@ export class VirtualizedList<ItemT = unknown>
|
|
|
543
603
|
accessibilityElementsHidden: this.accessibilityElementsHidden,
|
|
544
604
|
accessibilityIgnoresInvertColors: this.accessibilityIgnoresInvertColors,
|
|
545
605
|
accessibilityLanguage: this.accessibilityLanguage,
|
|
546
|
-
accessibilityRespondsToUserInteraction:
|
|
547
|
-
|
|
606
|
+
accessibilityRespondsToUserInteraction:
|
|
607
|
+
this.accessibilityRespondsToUserInteraction,
|
|
608
|
+
accessibilityShowsLargeContentViewer:
|
|
609
|
+
this.accessibilityShowsLargeContentViewer,
|
|
548
610
|
accessibilityLargeContentTitle: this.accessibilityLargeContentTitle,
|
|
549
611
|
role: this.role,
|
|
550
612
|
'aria-label': this.ariaLabel,
|
|
@@ -584,7 +646,9 @@ export class VirtualizedList<ItemT = unknown>
|
|
|
584
646
|
return this.maxToRenderPerBatch ?? DEFAULT_MAX_TO_RENDER_PER_BATCH;
|
|
585
647
|
}
|
|
586
648
|
private get updateCellsBatchingPeriodValue(): number {
|
|
587
|
-
return
|
|
649
|
+
return (
|
|
650
|
+
this.updateCellsBatchingPeriod ?? DEFAULT_UPDATE_CELLS_BATCHING_PERIOD
|
|
651
|
+
);
|
|
588
652
|
}
|
|
589
653
|
private get onEndReachedThresholdValue(): number {
|
|
590
654
|
return this.onEndReachedThreshold ?? DEFAULT_END_REACHED_THRESHOLD;
|
|
@@ -597,6 +661,7 @@ export class VirtualizedList<ItemT = unknown>
|
|
|
597
661
|
// flow through it, which an @Output() can't carry), so this is a stable arrow field passed
|
|
598
662
|
// straight to [onScroll]. onLayout is a real @Output() now, bound via (layout)="onLayoutTick($event)".
|
|
599
663
|
onScrollTick = (event: ISymbioteEvent): void => {
|
|
664
|
+
countAngular('scrollTicks');
|
|
600
665
|
const offset = readScrollOffset(event, this.isHorizontal);
|
|
601
666
|
if (offset === undefined) return;
|
|
602
667
|
dlog(`Angular VirtualizedList onScroll offset=${offset}`);
|
|
@@ -663,7 +728,8 @@ export class VirtualizedList<ItemT = unknown>
|
|
|
663
728
|
onStartReachedActive: this.startReached.observed,
|
|
664
729
|
viewabilityPairs: buildViewabilityPairs(
|
|
665
730
|
this.viewableItemsChanged.observed
|
|
666
|
-
? (info: IViewableItemsChangedInfo<ItemT>): void =>
|
|
731
|
+
? (info: IViewableItemsChangedInfo<ItemT>): void =>
|
|
732
|
+
this.viewableItemsChanged.emit(info)
|
|
667
733
|
: undefined,
|
|
668
734
|
this.viewabilityConfig,
|
|
669
735
|
this.viewabilityConfigCallbackPairs,
|
|
@@ -681,23 +747,58 @@ export class VirtualizedList<ItemT = unknown>
|
|
|
681
747
|
this.runEffects(result.effects, inputs);
|
|
682
748
|
if (!result.changed) return;
|
|
683
749
|
this.renderVersion += 1;
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
// The TEMPLATE reads none of those; it reads the window. Marking on the offset repainted the
|
|
687
|
-
// whole ancestor chain 60 times a second on a sticky screen for a window that had not moved.
|
|
688
|
-
const signature = this.renderSignature();
|
|
689
|
-
if (signature === this.lastRenderSignature) return;
|
|
690
|
-
this.lastRenderSignature = signature;
|
|
750
|
+
if (this.isWindowSettled(action)) return;
|
|
751
|
+
countAngular('listMarks');
|
|
691
752
|
this.cdr.markForCheck();
|
|
692
753
|
}
|
|
693
754
|
|
|
694
|
-
// The
|
|
695
|
-
|
|
755
|
+
// The reducer reports `changed` for EVERY scroll offset - it has to, since the offset feeds
|
|
756
|
+
// end-reached distance, viewability and the batch-fill timer, all handled by runEffects. The
|
|
757
|
+
// TEMPLATE reads none of those; it reads the window. Marking on the offset repainted the whole
|
|
758
|
+
// ancestor chain 60 times a second on a sticky screen for a window that had not moved.
|
|
759
|
+
//
|
|
760
|
+
// So the question here is "would a render leave the window exactly where it is", and it is asked
|
|
761
|
+
// of the state as it is NOW. It must NOT be answered by comparing the last-rendered window with
|
|
762
|
+
// the one before it: that window was derived from the PREVIOUS scroll offset, so once two
|
|
763
|
+
// consecutive derives agree the comparison reads "settled" for every offset that ever follows -
|
|
764
|
+
// no mark, no CD, no ngDoCheck, no refresh-metrics, so the window can never move again and the
|
|
765
|
+
// list goes permanently deaf mid-scroll (device: examples/angular Benchmark sticky path B froze
|
|
766
|
+
// at exactly the window the first viewport asked for, blank below it).
|
|
767
|
+
//
|
|
768
|
+
// computeWindow re-run over the CACHED offset table answers it directly. Two scans, no
|
|
769
|
+
// allocation - buildOffsets, the O(count) allocating half, still runs once per render only. It is
|
|
770
|
+
// the same function the render will use, so this cannot drift away from the window it predicts.
|
|
771
|
+
private isWindowSettled(action: IListAction<ItemT>): boolean {
|
|
772
|
+
// A measurement rewrites the very offset table the prediction reads, so it has no cached
|
|
773
|
+
// geometry to answer from.
|
|
774
|
+
if (action.kind === 'measure') return false;
|
|
696
775
|
const m = this.listState.metrics;
|
|
697
|
-
|
|
776
|
+
// Mid-fill: the committed window is still climbing toward target one batch step per render, and
|
|
777
|
+
// only a render advances it. Skipping the mark here is what stalls the incremental fill.
|
|
778
|
+
if (m.first !== m.target.first || m.last !== m.target.last) return false;
|
|
779
|
+
const next = computeWindow(
|
|
780
|
+
m.count,
|
|
781
|
+
m.offsets,
|
|
782
|
+
m.lengths,
|
|
783
|
+
this.listState.scrollOffset,
|
|
784
|
+
this.listState.viewportLength,
|
|
785
|
+
this.windowSizeValue,
|
|
786
|
+
this.initialNumToRenderValue,
|
|
787
|
+
);
|
|
788
|
+
const isSettled =
|
|
789
|
+
next.first === m.target.first && next.last === m.target.last;
|
|
790
|
+
if (!isSettled)
|
|
791
|
+
dlog(
|
|
792
|
+
`Angular VirtualizedList window moves [${m.first}, ${m.last}] -> ` +
|
|
793
|
+
`[${next.first}, ${next.last}] at offset ${this.listState.scrollOffset}`,
|
|
794
|
+
);
|
|
795
|
+
return isSettled;
|
|
698
796
|
}
|
|
699
797
|
|
|
700
|
-
private runEffects(
|
|
798
|
+
private runEffects(
|
|
799
|
+
effects: IListEffect<ItemT>[],
|
|
800
|
+
inputs: IListReducerInputs<ItemT>,
|
|
801
|
+
): void {
|
|
701
802
|
for (const effect of effects) {
|
|
702
803
|
switch (effect.kind) {
|
|
703
804
|
case 'scroll-to':
|
|
@@ -707,7 +808,9 @@ export class VirtualizedList<ItemT = unknown>
|
|
|
707
808
|
this.endReached.emit({ distanceFromEnd: effect.distanceFromEnd });
|
|
708
809
|
break;
|
|
709
810
|
case 'fire-start-reached':
|
|
710
|
-
this.startReached.emit({
|
|
811
|
+
this.startReached.emit({
|
|
812
|
+
distanceFromStart: effect.distanceFromStart,
|
|
813
|
+
});
|
|
711
814
|
break;
|
|
712
815
|
case 'fire-scroll-to-index-failed':
|
|
713
816
|
this.scrollToIndexFailed.emit({
|
|
@@ -765,6 +868,7 @@ export class VirtualizedList<ItemT = unknown>
|
|
|
765
868
|
// grows exactly one step per meaningful CD. The projected templates (ContentChild) are resolved
|
|
766
869
|
// from the second CD on; the first paint (count/viewport still 0) corrects on the layout-driven CD.
|
|
767
870
|
ngDoCheck(): void {
|
|
871
|
+
countAngular('listChecks');
|
|
768
872
|
const recomputeInputs: unknown[] = [
|
|
769
873
|
this.data,
|
|
770
874
|
this.extraData,
|
|
@@ -800,6 +904,7 @@ export class VirtualizedList<ItemT = unknown>
|
|
|
800
904
|
return;
|
|
801
905
|
}
|
|
802
906
|
// The single derive-per-CD: recompute the window off the current state before the view reads it.
|
|
907
|
+
countAngular('listRecomputes');
|
|
803
908
|
reduceList(this.listState, { kind: 'refresh-metrics' }, this.buildInputs());
|
|
804
909
|
this.recomputeView();
|
|
805
910
|
}
|
|
@@ -824,7 +929,9 @@ export class VirtualizedList<ItemT = unknown>
|
|
|
824
929
|
const hasHeader = this.headerDir !== undefined;
|
|
825
930
|
const hasSeparators = this.separatorDir !== undefined;
|
|
826
931
|
const stickySet =
|
|
827
|
-
this.stickyHeaderIndices !== undefined
|
|
932
|
+
this.stickyHeaderIndices !== undefined
|
|
933
|
+
? new Set(this.stickyHeaderIndices)
|
|
934
|
+
: undefined;
|
|
828
935
|
|
|
829
936
|
this.resolvedContentContainerStyle = this.isHorizontal
|
|
830
937
|
? [this.contentContainerStyle, { width: m.total }]
|
|
@@ -846,17 +953,40 @@ export class VirtualizedList<ItemT = unknown>
|
|
|
846
953
|
: {
|
|
847
954
|
...this.maintainVisibleContentPosition,
|
|
848
955
|
minIndexForVisible:
|
|
849
|
-
this.maintainVisibleContentPosition.minIndexForVisible +
|
|
956
|
+
this.maintainVisibleContentPosition.minIndexForVisible +
|
|
957
|
+
(hasHeader ? 1 : 0),
|
|
850
958
|
};
|
|
851
959
|
|
|
960
|
+
// A change in anything a cell's content reads other than its own item invalidates every cached
|
|
961
|
+
// context: identity reuse is only sound while the reused object still describes the cell.
|
|
962
|
+
const epoch: unknown[] = [
|
|
963
|
+
this.data,
|
|
964
|
+
this.extraData,
|
|
965
|
+
this.getItem,
|
|
966
|
+
this.keyExtractor,
|
|
967
|
+
];
|
|
968
|
+
const previousEpoch = this.cellContextEpoch;
|
|
969
|
+
this.cellContextEpoch = epoch;
|
|
970
|
+
if (
|
|
971
|
+
previousEpoch === undefined ||
|
|
972
|
+
previousEpoch.length !== epoch.length ||
|
|
973
|
+
!previousEpoch.every((value, index) => value === epoch[index])
|
|
974
|
+
) {
|
|
975
|
+
this.cellContexts.clear();
|
|
976
|
+
}
|
|
977
|
+
|
|
852
978
|
if (m.count === FIRST_INDEX) {
|
|
979
|
+
this.cellContexts.clear();
|
|
980
|
+
this.nextCellContexts.clear();
|
|
853
981
|
this.windowCells = [];
|
|
854
982
|
this.forcedStickyCell = null;
|
|
855
983
|
this.leadingSpacerStyle = null;
|
|
856
984
|
this.gapSpacerStyle = null;
|
|
857
985
|
this.trailingSpacerStyle = null;
|
|
858
986
|
this.renderedStickyIndices = undefined;
|
|
859
|
-
dlog(
|
|
987
|
+
dlog(
|
|
988
|
+
`Angular VirtualizedList empty (viewport=${this.listState.viewportLength})`,
|
|
989
|
+
);
|
|
860
990
|
return;
|
|
861
991
|
}
|
|
862
992
|
|
|
@@ -870,13 +1000,17 @@ export class VirtualizedList<ItemT = unknown>
|
|
|
870
1000
|
keyFor: this.keyFor,
|
|
871
1001
|
stickyIndices: stickySet,
|
|
872
1002
|
hasHeader,
|
|
873
|
-
hasSeparators,
|
|
874
1003
|
});
|
|
875
1004
|
this.leadingSpacerStyle =
|
|
876
|
-
plan.leadingExtent > EMPTY_OFFSET
|
|
877
|
-
|
|
1005
|
+
plan.leadingExtent > EMPTY_OFFSET
|
|
1006
|
+
? this.spacerStyle(plan.leadingExtent)
|
|
1007
|
+
: null;
|
|
1008
|
+
this.gapSpacerStyle =
|
|
1009
|
+
plan.gapExtent > EMPTY_OFFSET ? this.spacerStyle(plan.gapExtent) : null;
|
|
878
1010
|
this.trailingSpacerStyle =
|
|
879
|
-
plan.trailingExtent > EMPTY_OFFSET
|
|
1011
|
+
plan.trailingExtent > EMPTY_OFFSET
|
|
1012
|
+
? this.spacerStyle(plan.trailingExtent)
|
|
1013
|
+
: null;
|
|
880
1014
|
this.renderedStickyIndices =
|
|
881
1015
|
stickySet !== undefined && plan.stickyChildPositions.length > 0
|
|
882
1016
|
? plan.stickyChildPositions
|
|
@@ -890,15 +1024,30 @@ export class VirtualizedList<ItemT = unknown>
|
|
|
890
1024
|
|
|
891
1025
|
this.forcedStickyCell =
|
|
892
1026
|
plan.forcedStickyCell !== undefined
|
|
893
|
-
? this.buildWindowCell(
|
|
1027
|
+
? this.buildWindowCell(
|
|
1028
|
+
plan.forcedStickyCell.index,
|
|
1029
|
+
plan.forcedStickyCell.key,
|
|
1030
|
+
false,
|
|
1031
|
+
)
|
|
894
1032
|
: null;
|
|
895
1033
|
|
|
896
1034
|
const cells: IWindowCell<ItemT>[] = [];
|
|
897
1035
|
for (const planned of plan.cells) {
|
|
898
|
-
|
|
899
|
-
|
|
1036
|
+
// RN gates the separator on the last index of the DATA, not of the WINDOW
|
|
1037
|
+
// (VirtualizedList.js:793 `const end = getItemCount(data) - 1`), and now that the separator
|
|
1038
|
+
// lives INSIDE the measuring wrapper that distinction is load-bearing: gating on the window
|
|
1039
|
+
// would make a cell's own measured height change as the window slides past it. Device-measured
|
|
1040
|
+
// 2026-08-19 as a run of cells all shifting by exactly the divider's 1px.
|
|
1041
|
+
const includeSeparator = hasSeparators && planned.index < m.count - 1;
|
|
1042
|
+
cells.push(
|
|
1043
|
+
this.buildWindowCell(planned.index, planned.key, includeSeparator),
|
|
1044
|
+
);
|
|
900
1045
|
}
|
|
901
1046
|
this.windowCells = cells;
|
|
1047
|
+
// Swap, don't prune: a cell that left the window is simply absent from the pass that just ran,
|
|
1048
|
+
// so the old map is the garbage and the new one is exactly the live set.
|
|
1049
|
+
this.cellContexts = this.nextCellContexts;
|
|
1050
|
+
this.nextCellContexts = new Map<string, IVListItemContext<ItemT>>();
|
|
902
1051
|
|
|
903
1052
|
dlog(
|
|
904
1053
|
`Angular VirtualizedList window [${m.first}, ${m.last}] of ${m.count} ` +
|
|
@@ -914,21 +1063,45 @@ export class VirtualizedList<ItemT = unknown>
|
|
|
914
1063
|
includeSeparator: boolean,
|
|
915
1064
|
): IWindowCell<ItemT> {
|
|
916
1065
|
const item = this.getItem(this.data, index);
|
|
917
|
-
const context: IVListItemContext<ItemT> = {
|
|
918
|
-
$implicit: item,
|
|
919
|
-
index,
|
|
920
|
-
separators: this.makeSeparators(index),
|
|
921
|
-
};
|
|
922
1066
|
return {
|
|
923
1067
|
key,
|
|
924
1068
|
index,
|
|
925
|
-
context,
|
|
1069
|
+
context: this.contextFor(key, index, item),
|
|
926
1070
|
measure: this.cellMeasure(index),
|
|
927
|
-
separatorContext: includeSeparator
|
|
1071
|
+
separatorContext: includeSeparator
|
|
1072
|
+
? this.buildSeparatorContext(index, item)
|
|
1073
|
+
: undefined,
|
|
928
1074
|
};
|
|
929
1075
|
}
|
|
930
1076
|
|
|
931
|
-
|
|
1077
|
+
// The context for one cell, reused by identity when the cell did not move. See cellContexts.
|
|
1078
|
+
private contextFor(
|
|
1079
|
+
key: string,
|
|
1080
|
+
index: number,
|
|
1081
|
+
item: ItemT,
|
|
1082
|
+
): IVListItemContext<ItemT> {
|
|
1083
|
+
const cached = this.cellContexts.get(key);
|
|
1084
|
+
if (
|
|
1085
|
+
cached !== undefined &&
|
|
1086
|
+
cached.$implicit === item &&
|
|
1087
|
+
cached.index === index
|
|
1088
|
+
) {
|
|
1089
|
+
this.nextCellContexts.set(key, cached);
|
|
1090
|
+
return cached;
|
|
1091
|
+
}
|
|
1092
|
+
const context: IVListItemContext<ItemT> = {
|
|
1093
|
+
$implicit: item,
|
|
1094
|
+
index,
|
|
1095
|
+
separators: this.makeSeparators(index),
|
|
1096
|
+
};
|
|
1097
|
+
this.nextCellContexts.set(key, context);
|
|
1098
|
+
return context;
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
private buildSeparatorContext(
|
|
1102
|
+
index: number,
|
|
1103
|
+
item: ItemT,
|
|
1104
|
+
): IVListSeparatorContext<ItemT> {
|
|
932
1105
|
const overrides = this.separatorOverrides.get(index);
|
|
933
1106
|
const highlighted = overrides?.highlighted ?? false;
|
|
934
1107
|
const context: IVListSeparatorContext<ItemT> = {
|
|
@@ -941,7 +1114,12 @@ export class VirtualizedList<ItemT = unknown>
|
|
|
941
1114
|
// arbitrary separator props); the typed fields above stay authoritative.
|
|
942
1115
|
if (overrides !== undefined) {
|
|
943
1116
|
for (const key of Object.keys(overrides)) {
|
|
944
|
-
if (
|
|
1117
|
+
if (
|
|
1118
|
+
key === 'highlighted' ||
|
|
1119
|
+
key === 'leadingItem' ||
|
|
1120
|
+
key === 'trailingItem'
|
|
1121
|
+
)
|
|
1122
|
+
continue;
|
|
945
1123
|
context[key] = overrides[key];
|
|
946
1124
|
}
|
|
947
1125
|
}
|
|
@@ -958,10 +1136,13 @@ export class VirtualizedList<ItemT = unknown>
|
|
|
958
1136
|
const measure = (event: ISymbioteEvent): void => {
|
|
959
1137
|
const length = readLayoutLength(event, this.isHorizontal);
|
|
960
1138
|
if (length === undefined) return;
|
|
961
|
-
|
|
1139
|
+
const offset = readLayoutOffset(event, this.isHorizontal);
|
|
1140
|
+
dlog(
|
|
1141
|
+
`Angular VirtualizedList cell ${index} measured length=${length} offset=${offset ?? 'none'}`,
|
|
1142
|
+
);
|
|
962
1143
|
// The reducer guards a fixed getItemLayout and a repeated length; a no-op leaves state and the
|
|
963
1144
|
// view untouched (dispatch marks for check only when something changed).
|
|
964
|
-
this.dispatch({ kind: 'measure', index, length });
|
|
1145
|
+
this.dispatch({ kind: 'measure', index, length, offset });
|
|
965
1146
|
};
|
|
966
1147
|
this.cellMeasures.set(index, measure);
|
|
967
1148
|
return measure;
|
|
@@ -969,18 +1150,29 @@ export class VirtualizedList<ItemT = unknown>
|
|
|
969
1150
|
|
|
970
1151
|
// Per-cell onLayout rides the engine's structural (layout) event channel — Angular forbids an
|
|
971
1152
|
// [onLayout] property binding (NG8002). $event arrives untyped, narrowed before reaching measure.
|
|
972
|
-
handleCellLayout(
|
|
1153
|
+
handleCellLayout(
|
|
1154
|
+
measure: (event: ISymbioteEvent) => void,
|
|
1155
|
+
event: unknown,
|
|
1156
|
+
): void {
|
|
973
1157
|
if (this.isSymbioteEvent(event)) measure(event);
|
|
974
1158
|
}
|
|
975
1159
|
|
|
976
1160
|
private isSymbioteEvent(value: unknown): value is ISymbioteEvent {
|
|
977
|
-
return
|
|
1161
|
+
return (
|
|
1162
|
+
typeof value === 'object' && value !== null && 'nativeEvent' in value
|
|
1163
|
+
);
|
|
978
1164
|
}
|
|
979
1165
|
|
|
980
|
-
private mergeSeparator(
|
|
1166
|
+
private mergeSeparator(
|
|
1167
|
+
gapIndex: number,
|
|
1168
|
+
patch: Partial<ISeparatorProps<unknown>>,
|
|
1169
|
+
): void {
|
|
981
1170
|
const count = this.listState.metrics.count;
|
|
982
1171
|
if (!isSeparatorGapInRange(gapIndex, count)) return;
|
|
983
|
-
this.separatorOverrides.set(gapIndex, {
|
|
1172
|
+
this.separatorOverrides.set(gapIndex, {
|
|
1173
|
+
...this.separatorOverrides.get(gapIndex),
|
|
1174
|
+
...patch,
|
|
1175
|
+
});
|
|
984
1176
|
this.cdr.markForCheck();
|
|
985
1177
|
}
|
|
986
1178
|
|
|
@@ -996,7 +1188,10 @@ export class VirtualizedList<ItemT = unknown>
|
|
|
996
1188
|
this.mergeSeparator(index - 1, { highlighted: false });
|
|
997
1189
|
this.mergeSeparator(index, { highlighted: false });
|
|
998
1190
|
},
|
|
999
|
-
updateProps: (
|
|
1191
|
+
updateProps: (
|
|
1192
|
+
select: 'leading' | 'trailing',
|
|
1193
|
+
newProps: Record<string, unknown>,
|
|
1194
|
+
): void => {
|
|
1000
1195
|
this.mergeSeparator(select === 'leading' ? index - 1 : index, newProps);
|
|
1001
1196
|
},
|
|
1002
1197
|
};
|
|
@@ -1029,7 +1224,11 @@ export class VirtualizedList<ItemT = unknown>
|
|
|
1029
1224
|
});
|
|
1030
1225
|
}
|
|
1031
1226
|
|
|
1032
|
-
scrollToItem(params: {
|
|
1227
|
+
scrollToItem(params: {
|
|
1228
|
+
item: unknown;
|
|
1229
|
+
animated?: boolean;
|
|
1230
|
+
viewPosition?: number;
|
|
1231
|
+
}): void {
|
|
1033
1232
|
this.dispatch({
|
|
1034
1233
|
kind: 'scroll-to-item',
|
|
1035
1234
|
item: params.item,
|
|
@@ -1039,7 +1238,10 @@ export class VirtualizedList<ItemT = unknown>
|
|
|
1039
1238
|
}
|
|
1040
1239
|
|
|
1041
1240
|
scrollToEnd(params?: { animated?: boolean }): void {
|
|
1042
|
-
this.dispatch({
|
|
1241
|
+
this.dispatch({
|
|
1242
|
+
kind: 'scroll-to-end',
|
|
1243
|
+
animated: params?.animated ?? true,
|
|
1244
|
+
});
|
|
1043
1245
|
}
|
|
1044
1246
|
|
|
1045
1247
|
flashScrollIndicators(): void {
|
|
@@ -46,7 +46,8 @@ export interface IVSectionContext<ItemT> {
|
|
|
46
46
|
// `<ng-template vSectionItem>` — one section item row. The static guard types the `let-` bindings.
|
|
47
47
|
@Directive({ selector: '[vSectionItem]', standalone: true })
|
|
48
48
|
export class VSectionItemDirective<ItemT = unknown> {
|
|
49
|
-
readonly templateRef =
|
|
49
|
+
readonly templateRef =
|
|
50
|
+
inject<TemplateRef<IVSectionItemContext<ItemT>>>(TemplateRef);
|
|
50
51
|
|
|
51
52
|
static ngTemplateContextGuard<T>(
|
|
52
53
|
_dir: VSectionItemDirective<T>,
|
|
@@ -59,7 +60,8 @@ export class VSectionItemDirective<ItemT = unknown> {
|
|
|
59
60
|
// `<ng-template vSectionHeader>` — rendered above each section's items (RN renderSectionHeader).
|
|
60
61
|
@Directive({ selector: '[vSectionHeader]', standalone: true })
|
|
61
62
|
export class VSectionHeaderDirective<ItemT = unknown> {
|
|
62
|
-
readonly templateRef =
|
|
63
|
+
readonly templateRef =
|
|
64
|
+
inject<TemplateRef<IVSectionContext<ItemT>>>(TemplateRef);
|
|
63
65
|
|
|
64
66
|
static ngTemplateContextGuard<T>(
|
|
65
67
|
_dir: VSectionHeaderDirective<T>,
|
|
@@ -72,7 +74,8 @@ export class VSectionHeaderDirective<ItemT = unknown> {
|
|
|
72
74
|
// `<ng-template vSectionFooter>` — rendered below each section's items (RN renderSectionFooter).
|
|
73
75
|
@Directive({ selector: '[vSectionFooter]', standalone: true })
|
|
74
76
|
export class VSectionFooterDirective<ItemT = unknown> {
|
|
75
|
-
readonly templateRef =
|
|
77
|
+
readonly templateRef =
|
|
78
|
+
inject<TemplateRef<IVSectionContext<ItemT>>>(TemplateRef);
|
|
76
79
|
|
|
77
80
|
static ngTemplateContextGuard<T>(
|
|
78
81
|
_dir: VSectionFooterDirective<T>,
|