@symbiote-native/angular 0.6.2 → 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/bin/symbiote-angular-dev.cjs +20 -19
- package/build/angular/anchor-host-registry.js +2 -2
- package/build/angular/anchor-host-registry.js.map +1 -1
- package/build/angular/bootstrap.js +1 -1
- package/build/angular/bootstrap.js.map +1 -1
- package/build/angular/components/activity-indicator/index.android.d.ts +2 -1
- package/build/angular/components/activity-indicator/index.android.js +12 -4
- package/build/angular/components/activity-indicator/index.android.js.map +1 -1
- package/build/angular/components/activity-indicator/index.ios.d.ts +2 -1
- package/build/angular/components/activity-indicator/index.ios.js +8 -3
- 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 +4 -4
- package/build/angular/components/activity-indicator/shared.js.map +1 -1
- package/build/angular/components/button.d.ts +4 -2
- package/build/angular/components/button.js +41 -22
- package/build/angular/components/button.js.map +1 -1
- package/build/angular/components/flat-list/index.d.ts +6 -4
- package/build/angular/components/flat-list/index.js +80 -50
- package/build/angular/components/flat-list/index.js.map +1 -1
- package/build/angular/components/image/index.android.d.ts +8 -3
- package/build/angular/components/image/index.android.js +42 -10
- package/build/angular/components/image/index.android.js.map +1 -1
- package/build/angular/components/image/index.ios.d.ts +8 -3
- package/build/angular/components/image/index.ios.js +42 -10
- package/build/angular/components/image/index.ios.js.map +1 -1
- package/build/angular/components/image/shared.d.ts +4 -1
- package/build/angular/components/image/shared.js +51 -8
- package/build/angular/components/image/shared.js.map +1 -1
- package/build/angular/components/image-background.d.ts +2 -1
- package/build/angular/components/image-background.js +26 -10
- package/build/angular/components/image-background.js.map +1 -1
- package/build/angular/components/input-accessory-view/index.d.ts +9 -4
- package/build/angular/components/input-accessory-view/index.js +49 -10
- package/build/angular/components/input-accessory-view/index.js.map +1 -1
- package/build/angular/components/keyboard-avoiding-view/index.d.ts +3 -1
- package/build/angular/components/keyboard-avoiding-view/index.js +44 -12
- package/build/angular/components/keyboard-avoiding-view/index.js.map +1 -1
- package/build/angular/components/modal/index.d.ts +11 -8
- package/build/angular/components/modal/index.js +51 -29
- package/build/angular/components/modal/index.js.map +1 -1
- package/build/angular/components/pressable/index.d.ts +12 -6
- package/build/angular/components/pressable/index.js +66 -13
- package/build/angular/components/pressable/index.js.map +1 -1
- package/build/angular/components/refresh-control/index.d.ts +8 -4
- package/build/angular/components/refresh-control/index.js +54 -16
- package/build/angular/components/refresh-control/index.js.map +1 -1
- package/build/angular/components/safe-area-view/index.d.ts +9 -4
- package/build/angular/components/safe-area-view/index.js +48 -9
- package/build/angular/components/safe-area-view/index.js.map +1 -1
- package/build/angular/components/scroll-view/index.android.d.ts +2 -3
- package/build/angular/components/scroll-view/index.android.js +88 -61
- package/build/angular/components/scroll-view/index.android.js.map +1 -1
- package/build/angular/components/scroll-view/index.ios.d.ts +2 -3
- package/build/angular/components/scroll-view/index.ios.js +24 -18
- package/build/angular/components/scroll-view/index.ios.js.map +1 -1
- package/build/angular/components/scroll-view/projection.d.ts +35 -0
- package/build/angular/components/scroll-view/projection.js +279 -53
- package/build/angular/components/scroll-view/projection.js.map +1 -1
- package/build/angular/components/scroll-view/shared.d.ts +14 -5
- package/build/angular/components/scroll-view/shared.js +225 -75
- package/build/angular/components/scroll-view/shared.js.map +1 -1
- package/build/angular/components/scroll-view/sticky-header.js +21 -22
- package/build/angular/components/scroll-view/sticky-header.js.map +1 -1
- package/build/angular/components/section-list/index.d.ts +7 -1
- package/build/angular/components/section-list/index.js +91 -54
- package/build/angular/components/section-list/index.js.map +1 -1
- package/build/angular/components/switch/index.android.d.ts +7 -3
- package/build/angular/components/switch/index.android.js +52 -10
- package/build/angular/components/switch/index.android.js.map +1 -1
- package/build/angular/components/switch/index.ios.d.ts +7 -3
- package/build/angular/components/switch/index.ios.js +52 -10
- package/build/angular/components/switch/index.ios.js.map +1 -1
- package/build/angular/components/switch/shared.d.ts +13 -4
- package/build/angular/components/switch/shared.js +71 -7
- package/build/angular/components/switch/shared.js.map +1 -1
- package/build/angular/components/text-input.d.ts +11 -3
- package/build/angular/components/text-input.js +68 -8
- package/build/angular/components/text-input.js.map +1 -1
- package/build/angular/components/touchable/index.d.ts +40 -9
- package/build/angular/components/touchable/index.js +303 -67
- package/build/angular/components/touchable/index.js.map +1 -1
- package/build/angular/components/touchable-native-feedback/index.d.ts +6 -3
- package/build/angular/components/touchable-native-feedback/index.js +63 -27
- package/build/angular/components/touchable-native-feedback/index.js.map +1 -1
- package/build/angular/components/virtualized-list/directives.js +22 -18
- package/build/angular/components/virtualized-list/directives.js.map +1 -1
- package/build/angular/components/virtualized-list/index.d.ts +15 -4
- package/build/angular/components/virtualized-list/index.js +326 -94
- package/build/angular/components/virtualized-list/index.js.map +1 -1
- package/build/angular/components/virtualized-section-list/directives.js +12 -12
- package/build/angular/components/virtualized-section-list/directives.js.map +1 -1
- package/build/angular/components/virtualized-section-list/index.d.ts +19 -1
- package/build/angular/components/virtualized-section-list/index.js +93 -22
- package/build/angular/components/virtualized-section-list/index.js.map +1 -1
- package/build/angular/components.d.ts +2 -2
- package/build/angular/components.js +5 -0
- package/build/angular/components.js.map +1 -1
- package/build/angular/create-portal/index.js +34 -36
- package/build/angular/create-portal/index.js.map +1 -1
- package/build/angular/create-tunnel/index.js +19 -22
- package/build/angular/create-tunnel/index.js.map +1 -1
- package/build/angular/descriptor-to-angular/index.js +6 -4
- 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 +9 -4
- package/build/angular/index.js +17 -3
- package/build/angular/index.js.map +1 -1
- package/build/angular/modules/animated/animated-leaf-binder.d.ts +1 -7
- package/build/angular/modules/animated/animated-leaf-binder.js +22 -72
- package/build/angular/modules/animated/animated-leaf-binder.js.map +1 -1
- package/build/angular/modules/animated/create-animated-component.d.ts +11 -6
- package/build/angular/modules/animated/create-animated-component.js +106 -55
- package/build/angular/modules/animated/create-animated-component.js.map +1 -1
- package/build/angular/modules/status-bar/index.js +4 -4
- package/build/angular/modules/status-bar/index.js.map +1 -1
- package/build/angular/primitives/index.d.ts +20 -3
- package/build/angular/primitives/index.js +84 -48
- package/build/angular/primitives/index.js.map +1 -1
- package/build/angular/primitives/shared.d.ts +48 -2
- package/build/angular/primitives/shared.js +158 -11
- package/build/angular/primitives/shared.js.map +1 -1
- package/build/angular/render/index.js +69 -38
- 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 +3 -3
- package/build/angular/services/color-scheme.service.js.map +1 -1
- package/build/angular/services/window-dimensions.service.js +4 -4
- package/build/angular/services/window-dimensions.service.js.map +1 -1
- package/package.json +10 -8
- package/src/anchor-host-registry.ts +2 -2
- package/src/bootstrap.ts +4 -1
- package/src/components/activity-indicator/index.android.ts +8 -1
- package/src/components/activity-indicator/index.ios.ts +4 -0
- package/src/components/activity-indicator/shared.ts +32 -13
- package/src/components/button.ts +46 -19
- package/src/components/flat-list/index.ts +122 -62
- package/src/components/image/index.android.ts +58 -7
- package/src/components/image/index.ios.ts +58 -7
- package/src/components/image/shared.ts +92 -21
- package/src/components/image-background.ts +29 -9
- package/src/components/input-accessory-view/index.ts +67 -12
- package/src/components/keyboard-avoiding-view/index.ts +61 -15
- package/src/components/modal/index.ts +72 -33
- package/src/components/pressable/index.ts +117 -28
- package/src/components/refresh-control/index.ts +74 -18
- package/src/components/safe-area-view/index.ts +64 -10
- package/src/components/scroll-view/index.android.ts +62 -47
- package/src/components/scroll-view/index.ios.ts +19 -17
- package/src/components/scroll-view/projection.ts +363 -64
- package/src/components/scroll-view/shared.ts +312 -90
- package/src/components/scroll-view/sticky-header.ts +36 -24
- package/src/components/section-list/index.ts +86 -53
- package/src/components/switch/index.android.ts +60 -8
- package/src/components/switch/index.ios.ts +60 -8
- package/src/components/switch/shared.ts +93 -6
- package/src/components/text-input.ts +95 -13
- package/src/components/touchable/index.ts +348 -64
- package/src/components/touchable-native-feedback/index.ts +94 -31
- package/src/components/virtualized-list/directives.ts +19 -5
- package/src/components/virtualized-list/index.ts +380 -85
- package/src/components/virtualized-section-list/directives.ts +6 -3
- package/src/components/virtualized-section-list/index.ts +146 -33
- package/src/components.ts +9 -1
- package/src/create-portal/index.ts +31 -31
- package/src/create-tunnel/index.ts +16 -17
- 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 +61 -3
- package/src/modules/animated/animated-leaf-binder.ts +28 -77
- package/src/modules/animated/create-animated-component.ts +115 -47
- package/src/modules/app-registry/index.ts +9 -9
- package/src/modules/status-bar/index.ts +13 -4
- package/src/primitives/index.ts +47 -2
- package/src/primitives/shared.ts +170 -10
- package/src/render/index.ts +77 -39
- 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
- package/build/angular/modules/animated/animated-leaf-binder/index.d.ts +0 -14
- package/build/angular/modules/animated/animated-leaf-binder/index.js +0 -91
- package/build/angular/modules/animated/animated-leaf-binder/index.js.map +0 -1
- package/src/modules/animated/animated-leaf-binder/index.ts +0 -101
|
@@ -35,9 +35,12 @@ import {
|
|
|
35
35
|
Input,
|
|
36
36
|
Output,
|
|
37
37
|
ViewChild,
|
|
38
|
+
computed,
|
|
38
39
|
inject,
|
|
40
|
+
signal,
|
|
39
41
|
type AfterViewChecked,
|
|
40
42
|
type DoCheck,
|
|
43
|
+
type OnChanges,
|
|
41
44
|
type OnDestroy,
|
|
42
45
|
} from '@angular/core';
|
|
43
46
|
import {
|
|
@@ -53,10 +56,12 @@ import {
|
|
|
53
56
|
INVERTED_Y_STYLE,
|
|
54
57
|
buildListPlan,
|
|
55
58
|
buildViewabilityPairs,
|
|
59
|
+
computeWindow,
|
|
56
60
|
createInitialListState,
|
|
57
61
|
isSeparatorGapInRange,
|
|
58
62
|
listEffectSignature,
|
|
59
63
|
readLayoutLength,
|
|
64
|
+
readLayoutOffset,
|
|
60
65
|
readScrollOffset,
|
|
61
66
|
reduceList,
|
|
62
67
|
resolveAccessibilityProps,
|
|
@@ -84,9 +89,14 @@ import {
|
|
|
84
89
|
type ISymbioteNode,
|
|
85
90
|
type IViewStyle,
|
|
86
91
|
} from '@symbiote-native/engine';
|
|
92
|
+
import { countAngular } from '../../diagnostics';
|
|
87
93
|
import { ScrollView } from '../scroll-view';
|
|
88
94
|
import { RefreshControl } from '../refresh-control';
|
|
89
|
-
import {
|
|
95
|
+
import {
|
|
96
|
+
anchorHostStyle,
|
|
97
|
+
SymbioteStyleInputDirective,
|
|
98
|
+
ViewHost,
|
|
99
|
+
} from '../../primitives';
|
|
90
100
|
import {
|
|
91
101
|
VListEmptyDirective,
|
|
92
102
|
VListFooterDirective,
|
|
@@ -123,7 +133,8 @@ export type { IVListItemContext, IVListSeparatorContext } from './directives';
|
|
|
123
133
|
// element-returning props (renderItem, ListHeader/Footer/Empty Component, ItemSeparatorComponent):
|
|
124
134
|
// those are the per-adapter children/render fields and become `<ng-template>` directives in Angular.
|
|
125
135
|
// Everything agnostic is the SAME surface.
|
|
126
|
-
export interface IVirtualizedListProps<ItemT>
|
|
136
|
+
export interface IVirtualizedListProps<ItemT>
|
|
137
|
+
extends IAccessibilityProps, IAriaProps {
|
|
127
138
|
data: unknown;
|
|
128
139
|
getItem: (data: unknown, index: number) => ItemT;
|
|
129
140
|
getItemCount: (data: unknown) => number;
|
|
@@ -202,6 +213,9 @@ interface IWindowCell<ItemT> {
|
|
|
202
213
|
@Component({
|
|
203
214
|
selector: 'VirtualizedList',
|
|
204
215
|
standalone: true,
|
|
216
|
+
hostDirectives: [
|
|
217
|
+
{ directive: SymbioteStyleInputDirective, inputs: ['style'] },
|
|
218
|
+
],
|
|
205
219
|
schemas: [CUSTOM_ELEMENTS_SCHEMA],
|
|
206
220
|
imports: [ScrollView, RefreshControl, VListOutletDirective, ViewHost],
|
|
207
221
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
@@ -210,30 +224,40 @@ interface IWindowCell<ItemT> {
|
|
|
210
224
|
[horizontal]="isHorizontal"
|
|
211
225
|
[style]="resolvedStyle"
|
|
212
226
|
[contentContainerStyle]="resolvedContentContainerStyle"
|
|
213
|
-
[testID]="foldedAccessibility.testID"
|
|
214
|
-
[nativeID]="foldedAccessibility.nativeID"
|
|
215
|
-
[accessible]="foldedAccessibility.accessible"
|
|
216
|
-
[accessibilityLabel]="foldedAccessibility.accessibilityLabel"
|
|
217
|
-
[accessibilityHint]="foldedAccessibility.accessibilityHint"
|
|
218
|
-
[accessibilityRole]="foldedAccessibility.accessibilityRole"
|
|
219
|
-
[accessibilityState]="foldedAccessibility.accessibilityState"
|
|
220
|
-
[accessibilityValue]="foldedAccessibility.accessibilityValue"
|
|
221
|
-
[accessibilityActions]="foldedAccessibility.accessibilityActions"
|
|
222
|
-
[accessibilityLabelledBy]="foldedAccessibility.accessibilityLabelledBy"
|
|
223
|
-
[importantForAccessibility]="
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
[
|
|
227
|
-
[
|
|
228
|
-
[
|
|
229
|
-
|
|
227
|
+
[testID]="foldedAccessibility().testID"
|
|
228
|
+
[nativeID]="foldedAccessibility().nativeID"
|
|
229
|
+
[accessible]="foldedAccessibility().accessible"
|
|
230
|
+
[accessibilityLabel]="foldedAccessibility().accessibilityLabel"
|
|
231
|
+
[accessibilityHint]="foldedAccessibility().accessibilityHint"
|
|
232
|
+
[accessibilityRole]="foldedAccessibility().accessibilityRole"
|
|
233
|
+
[accessibilityState]="foldedAccessibility().accessibilityState"
|
|
234
|
+
[accessibilityValue]="foldedAccessibility().accessibilityValue"
|
|
235
|
+
[accessibilityActions]="foldedAccessibility().accessibilityActions"
|
|
236
|
+
[accessibilityLabelledBy]="foldedAccessibility().accessibilityLabelledBy"
|
|
237
|
+
[importantForAccessibility]="
|
|
238
|
+
foldedAccessibility().importantForAccessibility
|
|
239
|
+
"
|
|
240
|
+
[accessibilityLiveRegion]="foldedAccessibility().accessibilityLiveRegion"
|
|
241
|
+
[screenReaderFocusable]="foldedAccessibility().screenReaderFocusable"
|
|
242
|
+
[accessibilityViewIsModal]="
|
|
243
|
+
foldedAccessibility().accessibilityViewIsModal
|
|
244
|
+
"
|
|
245
|
+
[accessibilityElementsHidden]="
|
|
246
|
+
foldedAccessibility().accessibilityElementsHidden
|
|
247
|
+
"
|
|
248
|
+
[accessibilityIgnoresInvertColors]="
|
|
249
|
+
foldedAccessibility().accessibilityIgnoresInvertColors
|
|
250
|
+
"
|
|
251
|
+
[accessibilityLanguage]="foldedAccessibility().accessibilityLanguage"
|
|
230
252
|
[accessibilityRespondsToUserInteraction]="
|
|
231
|
-
foldedAccessibility.accessibilityRespondsToUserInteraction
|
|
253
|
+
foldedAccessibility().accessibilityRespondsToUserInteraction
|
|
232
254
|
"
|
|
233
255
|
[accessibilityShowsLargeContentViewer]="
|
|
234
|
-
foldedAccessibility.accessibilityShowsLargeContentViewer
|
|
256
|
+
foldedAccessibility().accessibilityShowsLargeContentViewer
|
|
257
|
+
"
|
|
258
|
+
[accessibilityLargeContentTitle]="
|
|
259
|
+
foldedAccessibility().accessibilityLargeContentTitle
|
|
235
260
|
"
|
|
236
|
-
[accessibilityLargeContentTitle]="foldedAccessibility.accessibilityLargeContentTitle"
|
|
237
261
|
(accessibilityAction)="accessibilityActionTick($event)"
|
|
238
262
|
(accessibilityTap)="accessibilityTapTick($event)"
|
|
239
263
|
(magicTap)="magicTapTick($event)"
|
|
@@ -275,21 +299,43 @@ interface IWindowCell<ItemT> {
|
|
|
275
299
|
@if (leadingSpacerStyle !== null) {
|
|
276
300
|
<symbiote-view [style]="leadingSpacerStyle"></symbiote-view>
|
|
277
301
|
}
|
|
302
|
+
@if (forcedStickyCell !== null) {
|
|
303
|
+
<symbiote-view
|
|
304
|
+
(layout)="handleCellLayout(forcedStickyCell.measure, $event)"
|
|
305
|
+
[style]="cellStyle"
|
|
306
|
+
>
|
|
307
|
+
<ng-container
|
|
308
|
+
[vListOutlet]="itemDir?.templateRef"
|
|
309
|
+
[vListOutletContext]="forcedStickyCell.context"
|
|
310
|
+
></ng-container>
|
|
311
|
+
</symbiote-view>
|
|
312
|
+
}
|
|
313
|
+
@if (gapSpacerStyle !== null) {
|
|
314
|
+
<symbiote-view [style]="gapSpacerStyle"></symbiote-view>
|
|
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. -->
|
|
278
323
|
@for (cell of windowCells; track cell.key) {
|
|
279
|
-
<symbiote-view
|
|
324
|
+
<symbiote-view
|
|
325
|
+
(layout)="handleCellLayout(cell.measure, $event)"
|
|
326
|
+
[style]="cellStyle"
|
|
327
|
+
>
|
|
280
328
|
<ng-container
|
|
281
329
|
[vListOutlet]="itemDir?.templateRef"
|
|
282
330
|
[vListOutletContext]="cell.context"
|
|
283
331
|
></ng-container>
|
|
284
|
-
|
|
285
|
-
@if (cell.separatorContext !== undefined) {
|
|
286
|
-
<symbiote-view>
|
|
332
|
+
@if (cell.separatorContext !== undefined) {
|
|
287
333
|
<ng-container
|
|
288
334
|
[vListOutlet]="separatorDir?.templateRef"
|
|
289
335
|
[vListOutletContext]="cell.separatorContext"
|
|
290
336
|
></ng-container>
|
|
291
|
-
|
|
292
|
-
|
|
337
|
+
}
|
|
338
|
+
</symbiote-view>
|
|
293
339
|
}
|
|
294
340
|
@if (trailingSpacerStyle !== null) {
|
|
295
341
|
<symbiote-view [style]="trailingSpacerStyle"></symbiote-view>
|
|
@@ -310,15 +356,22 @@ export class VirtualizedList<ItemT = unknown>
|
|
|
310
356
|
IVirtualizedListHandle,
|
|
311
357
|
DoCheck,
|
|
312
358
|
AfterViewChecked,
|
|
359
|
+
OnChanges,
|
|
313
360
|
OnDestroy
|
|
314
361
|
{
|
|
315
362
|
// The list's edge/viewability/failure events as real Angular events: `(endReached)="…"`, not
|
|
316
363
|
// `[onEndReached]="…"`. See handleRefresh/accessibility*Tick below for how the still-callback-
|
|
317
364
|
// shaped ScrollView/RefreshControl @Input()s are fed from these.
|
|
318
|
-
@Output() readonly endReached = new EventEmitter<{
|
|
319
|
-
|
|
365
|
+
@Output() readonly endReached = new EventEmitter<{
|
|
366
|
+
distanceFromEnd: number;
|
|
367
|
+
}>();
|
|
368
|
+
@Output() readonly startReached = new EventEmitter<{
|
|
369
|
+
distanceFromStart: number;
|
|
370
|
+
}>();
|
|
320
371
|
@Output() readonly refresh = new EventEmitter<void>();
|
|
321
|
-
@Output() readonly viewableItemsChanged = new EventEmitter<
|
|
372
|
+
@Output() readonly viewableItemsChanged = new EventEmitter<
|
|
373
|
+
IViewableItemsChangedInfo<ItemT>
|
|
374
|
+
>();
|
|
322
375
|
@Output() readonly scrollToIndexFailed = new EventEmitter<{
|
|
323
376
|
index: number;
|
|
324
377
|
highestMeasuredFrameIndex: number;
|
|
@@ -352,7 +405,8 @@ export class VirtualizedList<ItemT = unknown>
|
|
|
352
405
|
// (no wrapper) falls back to this component's own `.observed`, unchanged from before.
|
|
353
406
|
@Input() refreshRequested?: boolean;
|
|
354
407
|
@Input() viewabilityConfig?: IViewabilityConfig;
|
|
355
|
-
@Input()
|
|
408
|
+
@Input()
|
|
409
|
+
viewabilityConfigCallbackPairs?: IViewabilityConfigCallbackPair<ItemT>[];
|
|
356
410
|
@Input() initialNumToRender?: number;
|
|
357
411
|
@Input() initialScrollIndex?: number;
|
|
358
412
|
@Input() maxToRenderPerBatch?: number;
|
|
@@ -383,8 +437,10 @@ export class VirtualizedList<ItemT = unknown>
|
|
|
383
437
|
@Input() accessibilityValue?: IAccessibilityProps['accessibilityValue'];
|
|
384
438
|
@Input() accessibilityActions?: IAccessibilityProps['accessibilityActions'];
|
|
385
439
|
@Input() accessibilityLabelledBy?: string | string[];
|
|
386
|
-
@Input()
|
|
387
|
-
|
|
440
|
+
@Input()
|
|
441
|
+
importantForAccessibility?: IAccessibilityProps['importantForAccessibility'];
|
|
442
|
+
@Input()
|
|
443
|
+
accessibilityLiveRegion?: IAccessibilityProps['accessibilityLiveRegion'];
|
|
388
444
|
@Input() screenReaderFocusable?: boolean;
|
|
389
445
|
@Input() accessibilityViewIsModal?: boolean;
|
|
390
446
|
@Input() accessibilityElementsHidden?: boolean;
|
|
@@ -416,7 +472,8 @@ export class VirtualizedList<ItemT = unknown>
|
|
|
416
472
|
@ContentChild(VListHeaderDirective) headerDir?: VListHeaderDirective;
|
|
417
473
|
@ContentChild(VListFooterDirective) footerDir?: VListFooterDirective;
|
|
418
474
|
@ContentChild(VListEmptyDirective) emptyDir?: VListEmptyDirective;
|
|
419
|
-
@ContentChild(VListSeparatorDirective)
|
|
475
|
+
@ContentChild(VListSeparatorDirective)
|
|
476
|
+
separatorDir?: VListSeparatorDirective<ItemT>;
|
|
420
477
|
|
|
421
478
|
// The composed inner scroll view. Its instance IS an IScrollViewHandle (scrollTo / scrollToEnd /
|
|
422
479
|
// flashScrollIndicators / getScrollNode), so the imperative handle delegates straight to it.
|
|
@@ -426,7 +483,13 @@ export class VirtualizedList<ItemT = unknown>
|
|
|
426
483
|
// --- template-bound view state, assembled in ngDoCheck (recomputeView) ---
|
|
427
484
|
itemCount = EMPTY_OFFSET;
|
|
428
485
|
windowCells: IWindowCell<ItemT>[] = [];
|
|
486
|
+
// The nearest sticky-header cell force-mounted outside [first,last] (plan.forcedStickyCell), the
|
|
487
|
+
// Angular twin of RN's _ensureClosestStickyHeader. Rendered ahead of windowCells, right after the
|
|
488
|
+
// leading spacer, with gapSpacerStyle filling the gap to the window's own first cell — see
|
|
489
|
+
// buildListPlan's doc comment for the exact child order this assumes.
|
|
490
|
+
forcedStickyCell: IWindowCell<ItemT> | null = null;
|
|
429
491
|
leadingSpacerStyle: IViewStyle | null = null;
|
|
492
|
+
gapSpacerStyle: IViewStyle | null = null;
|
|
430
493
|
trailingSpacerStyle: IViewStyle | null = null;
|
|
431
494
|
cellStyle: IViewStyle | undefined = undefined;
|
|
432
495
|
renderedStickyIndices: number[] | undefined = undefined;
|
|
@@ -437,7 +500,8 @@ export class VirtualizedList<ItemT = unknown>
|
|
|
437
500
|
resolvedStyle: IViewStyle | undefined = undefined;
|
|
438
501
|
resolvedContentContainerStyle: IStyleProp<IViewStyle> | undefined = undefined;
|
|
439
502
|
resolvedMaintainVisibleContentPosition:
|
|
440
|
-
{ minIndexForVisible: number; autoscrollToTopThreshold?: number }
|
|
503
|
+
| { minIndexForVisible: number; autoscrollToTopThreshold?: number }
|
|
504
|
+
| undefined = undefined;
|
|
441
505
|
// The offset we are imperatively driving native to before the scroll handle attaches (rides down
|
|
442
506
|
// as contentOffset). A fresh object identity each push so the commit re-applies a repeated value.
|
|
443
507
|
commandedOffset: { x: number; y: number } | undefined = undefined;
|
|
@@ -446,11 +510,35 @@ export class VirtualizedList<ItemT = unknown>
|
|
|
446
510
|
// measured lengths, committed window, edge/viewability dedup, MVCP anchor — all in IListState,
|
|
447
511
|
// driven by the shared reduceList. `renderVersion` bumps whenever a transition changes render
|
|
448
512
|
// state, so ngDoCheck's recompute-dedup can detect it (listState is a plain object, not tracked).
|
|
449
|
-
private readonly listState: IListState<ItemT> =
|
|
513
|
+
private readonly listState: IListState<ItemT> =
|
|
514
|
+
createInitialListState<ItemT>();
|
|
450
515
|
private renderVersion = EMPTY_OFFSET;
|
|
451
516
|
|
|
452
|
-
private readonly cellMeasures = new Map<
|
|
453
|
-
|
|
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
|
+
>();
|
|
454
542
|
// The adapter-owned debounce (minimumViewTime) and incremental-fill timers; the reducer only
|
|
455
543
|
// hands back a delay.
|
|
456
544
|
private viewableTimer: ReturnType<typeof setTimeout> | null = null;
|
|
@@ -476,7 +564,27 @@ export class VirtualizedList<ItemT = unknown>
|
|
|
476
564
|
// inner `<ScrollView>` one level down (itself its own separate anchor host).
|
|
477
565
|
private readonly elementRef = inject(ElementRef);
|
|
478
566
|
|
|
479
|
-
|
|
567
|
+
// Bridges the non-reactive @Input fields into the reactive graph so the computed below can
|
|
568
|
+
// memoize a bag derived from them (the same bridge ScrollView's shared.ts uses - read its comment
|
|
569
|
+
// for why signal inputs are not an option while this package's unit suite runs on JIT).
|
|
570
|
+
//
|
|
571
|
+
// ONLY safe for a bag whose every dependency is an @Input. This list's window machinery
|
|
572
|
+
// (listState, renderVersion, windowCells, the measured metrics) is driven from scroll/layout
|
|
573
|
+
// callbacks that never touch ngOnChanges, so nothing derived from it may ride this signal - it
|
|
574
|
+
// would memoize a window that stops re-computing mid-scroll. Make that state its own signal
|
|
575
|
+
// instead of widening this one.
|
|
576
|
+
private readonly inputsRevision = signal(0);
|
|
577
|
+
|
|
578
|
+
// MEASURED: the template feeds 20 separate <ScrollView> inputs off this one bag, and Angular does
|
|
579
|
+
// not cache a getter across binding expressions - as a getter it rebuilt the object 20 times per
|
|
580
|
+
// refresh, and every consumer saw a fresh reference, so all 20 reported "changed" and wrote
|
|
581
|
+
// through to the ScrollView. A computed evaluates once and hands back the SAME object until an
|
|
582
|
+
// input actually changes. Every dependency below is an @Input, which is what makes
|
|
583
|
+
// inputsRevision a complete dependency set here.
|
|
584
|
+
readonly foldedAccessibility = computed<
|
|
585
|
+
IAccessibilityProps & IAriaProps & Record<string, unknown>
|
|
586
|
+
>(() => {
|
|
587
|
+
this.inputsRevision();
|
|
480
588
|
return resolveAccessibilityProps({
|
|
481
589
|
testID: this.testID,
|
|
482
590
|
nativeID: this.nativeID,
|
|
@@ -495,8 +603,10 @@ export class VirtualizedList<ItemT = unknown>
|
|
|
495
603
|
accessibilityElementsHidden: this.accessibilityElementsHidden,
|
|
496
604
|
accessibilityIgnoresInvertColors: this.accessibilityIgnoresInvertColors,
|
|
497
605
|
accessibilityLanguage: this.accessibilityLanguage,
|
|
498
|
-
accessibilityRespondsToUserInteraction:
|
|
499
|
-
|
|
606
|
+
accessibilityRespondsToUserInteraction:
|
|
607
|
+
this.accessibilityRespondsToUserInteraction,
|
|
608
|
+
accessibilityShowsLargeContentViewer:
|
|
609
|
+
this.accessibilityShowsLargeContentViewer,
|
|
500
610
|
accessibilityLargeContentTitle: this.accessibilityLargeContentTitle,
|
|
501
611
|
role: this.role,
|
|
502
612
|
'aria-label': this.ariaLabel,
|
|
@@ -514,7 +624,7 @@ export class VirtualizedList<ItemT = unknown>
|
|
|
514
624
|
'aria-valuenow': this.ariaValueNow,
|
|
515
625
|
'aria-valuetext': this.ariaValueText,
|
|
516
626
|
});
|
|
517
|
-
}
|
|
627
|
+
});
|
|
518
628
|
|
|
519
629
|
get shouldRenderRefreshControl(): boolean {
|
|
520
630
|
return this.refreshRequested ?? this.refresh.observed;
|
|
@@ -536,7 +646,9 @@ export class VirtualizedList<ItemT = unknown>
|
|
|
536
646
|
return this.maxToRenderPerBatch ?? DEFAULT_MAX_TO_RENDER_PER_BATCH;
|
|
537
647
|
}
|
|
538
648
|
private get updateCellsBatchingPeriodValue(): number {
|
|
539
|
-
return
|
|
649
|
+
return (
|
|
650
|
+
this.updateCellsBatchingPeriod ?? DEFAULT_UPDATE_CELLS_BATCHING_PERIOD
|
|
651
|
+
);
|
|
540
652
|
}
|
|
541
653
|
private get onEndReachedThresholdValue(): number {
|
|
542
654
|
return this.onEndReachedThreshold ?? DEFAULT_END_REACHED_THRESHOLD;
|
|
@@ -549,6 +661,7 @@ export class VirtualizedList<ItemT = unknown>
|
|
|
549
661
|
// flow through it, which an @Output() can't carry), so this is a stable arrow field passed
|
|
550
662
|
// straight to [onScroll]. onLayout is a real @Output() now, bound via (layout)="onLayoutTick($event)".
|
|
551
663
|
onScrollTick = (event: ISymbioteEvent): void => {
|
|
664
|
+
countAngular('scrollTicks');
|
|
552
665
|
const offset = readScrollOffset(event, this.isHorizontal);
|
|
553
666
|
if (offset === undefined) return;
|
|
554
667
|
dlog(`Angular VirtualizedList onScroll offset=${offset}`);
|
|
@@ -615,7 +728,8 @@ export class VirtualizedList<ItemT = unknown>
|
|
|
615
728
|
onStartReachedActive: this.startReached.observed,
|
|
616
729
|
viewabilityPairs: buildViewabilityPairs(
|
|
617
730
|
this.viewableItemsChanged.observed
|
|
618
|
-
? (info: IViewableItemsChangedInfo<ItemT>): void =>
|
|
731
|
+
? (info: IViewableItemsChangedInfo<ItemT>): void =>
|
|
732
|
+
this.viewableItemsChanged.emit(info)
|
|
619
733
|
: undefined,
|
|
620
734
|
this.viewabilityConfig,
|
|
621
735
|
this.viewabilityConfigCallbackPairs,
|
|
@@ -631,13 +745,60 @@ export class VirtualizedList<ItemT = unknown>
|
|
|
631
745
|
const inputs = this.buildInputs();
|
|
632
746
|
const result = reduceList(this.listState, action, inputs);
|
|
633
747
|
this.runEffects(result.effects, inputs);
|
|
634
|
-
if (result.changed)
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
748
|
+
if (!result.changed) return;
|
|
749
|
+
this.renderVersion += 1;
|
|
750
|
+
if (this.isWindowSettled(action)) return;
|
|
751
|
+
countAngular('listMarks');
|
|
752
|
+
this.cdr.markForCheck();
|
|
638
753
|
}
|
|
639
754
|
|
|
640
|
-
|
|
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;
|
|
775
|
+
const m = this.listState.metrics;
|
|
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;
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
private runEffects(
|
|
799
|
+
effects: IListEffect<ItemT>[],
|
|
800
|
+
inputs: IListReducerInputs<ItemT>,
|
|
801
|
+
): void {
|
|
641
802
|
for (const effect of effects) {
|
|
642
803
|
switch (effect.kind) {
|
|
643
804
|
case 'scroll-to':
|
|
@@ -647,7 +808,9 @@ export class VirtualizedList<ItemT = unknown>
|
|
|
647
808
|
this.endReached.emit({ distanceFromEnd: effect.distanceFromEnd });
|
|
648
809
|
break;
|
|
649
810
|
case 'fire-start-reached':
|
|
650
|
-
this.startReached.emit({
|
|
811
|
+
this.startReached.emit({
|
|
812
|
+
distanceFromStart: effect.distanceFromStart,
|
|
813
|
+
});
|
|
651
814
|
break;
|
|
652
815
|
case 'fire-scroll-to-index-failed':
|
|
653
816
|
this.scrollToIndexFailed.emit({
|
|
@@ -690,6 +853,14 @@ export class VirtualizedList<ItemT = unknown>
|
|
|
690
853
|
}
|
|
691
854
|
}
|
|
692
855
|
|
|
856
|
+
// foldedAccessibility reads plain @Input fields, which are NOT reactive on their own - this bump
|
|
857
|
+
// is what tells that computed an input changed. It must stay in ngOnChanges: that is the single
|
|
858
|
+
// moment Angular has finished writing every changed input for this pass, and it runs before
|
|
859
|
+
// ngDoCheck below, so the recompute already sees the current values.
|
|
860
|
+
ngOnChanges(): void {
|
|
861
|
+
this.inputsRevision.update(revision => revision + 1);
|
|
862
|
+
}
|
|
863
|
+
|
|
693
864
|
// Once-per-CD recompute, BEFORE the template bindings are read (so the freshly computed
|
|
694
865
|
// template-bound fields render this pass — computing them in ngAfterContentChecked instead would
|
|
695
866
|
// trip ExpressionChangedAfterItHasBeenChecked). The dedup guard runs refresh-metrics (which owns
|
|
@@ -697,6 +868,7 @@ export class VirtualizedList<ItemT = unknown>
|
|
|
697
868
|
// grows exactly one step per meaningful CD. The projected templates (ContentChild) are resolved
|
|
698
869
|
// from the second CD on; the first paint (count/viewport still 0) corrects on the layout-driven CD.
|
|
699
870
|
ngDoCheck(): void {
|
|
871
|
+
countAngular('listChecks');
|
|
700
872
|
const recomputeInputs: unknown[] = [
|
|
701
873
|
this.data,
|
|
702
874
|
this.extraData,
|
|
@@ -710,6 +882,10 @@ export class VirtualizedList<ItemT = unknown>
|
|
|
710
882
|
this.stickyHeaderIndices,
|
|
711
883
|
this.maintainVisibleContentPosition,
|
|
712
884
|
this.style,
|
|
885
|
+
// `recomputeView` folds this into the committed style, but it arrives through
|
|
886
|
+
// addClass/removeClass, never as an @Input - so without it here no entry moves on a class
|
|
887
|
+
// toggle and the gate skips the recompute forever. Reference-stable, so the dedup holds.
|
|
888
|
+
anchorHostStyle(this.elementRef),
|
|
713
889
|
this.contentContainerStyle,
|
|
714
890
|
// Folds scroll / layout / measure / batch-tick: dispatch bumps renderVersion on any change.
|
|
715
891
|
this.renderVersion,
|
|
@@ -728,6 +904,7 @@ export class VirtualizedList<ItemT = unknown>
|
|
|
728
904
|
return;
|
|
729
905
|
}
|
|
730
906
|
// The single derive-per-CD: recompute the window off the current state before the view reads it.
|
|
907
|
+
countAngular('listRecomputes');
|
|
731
908
|
reduceList(this.listState, { kind: 'refresh-metrics' }, this.buildInputs());
|
|
732
909
|
this.recomputeView();
|
|
733
910
|
}
|
|
@@ -752,7 +929,9 @@ export class VirtualizedList<ItemT = unknown>
|
|
|
752
929
|
const hasHeader = this.headerDir !== undefined;
|
|
753
930
|
const hasSeparators = this.separatorDir !== undefined;
|
|
754
931
|
const stickySet =
|
|
755
|
-
this.stickyHeaderIndices !== undefined
|
|
932
|
+
this.stickyHeaderIndices !== undefined
|
|
933
|
+
? new Set(this.stickyHeaderIndices)
|
|
934
|
+
: undefined;
|
|
756
935
|
|
|
757
936
|
this.resolvedContentContainerStyle = this.isHorizontal
|
|
758
937
|
? [this.contentContainerStyle, { width: m.total }]
|
|
@@ -774,15 +953,40 @@ export class VirtualizedList<ItemT = unknown>
|
|
|
774
953
|
: {
|
|
775
954
|
...this.maintainVisibleContentPosition,
|
|
776
955
|
minIndexForVisible:
|
|
777
|
-
this.maintainVisibleContentPosition.minIndexForVisible +
|
|
956
|
+
this.maintainVisibleContentPosition.minIndexForVisible +
|
|
957
|
+
(hasHeader ? 1 : 0),
|
|
778
958
|
};
|
|
779
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
|
+
|
|
780
978
|
if (m.count === FIRST_INDEX) {
|
|
979
|
+
this.cellContexts.clear();
|
|
980
|
+
this.nextCellContexts.clear();
|
|
781
981
|
this.windowCells = [];
|
|
982
|
+
this.forcedStickyCell = null;
|
|
782
983
|
this.leadingSpacerStyle = null;
|
|
984
|
+
this.gapSpacerStyle = null;
|
|
783
985
|
this.trailingSpacerStyle = null;
|
|
784
986
|
this.renderedStickyIndices = undefined;
|
|
785
|
-
dlog(
|
|
987
|
+
dlog(
|
|
988
|
+
`Angular VirtualizedList empty (viewport=${this.listState.viewportLength})`,
|
|
989
|
+
);
|
|
786
990
|
return;
|
|
787
991
|
}
|
|
788
992
|
|
|
@@ -796,38 +1000,54 @@ export class VirtualizedList<ItemT = unknown>
|
|
|
796
1000
|
keyFor: this.keyFor,
|
|
797
1001
|
stickyIndices: stickySet,
|
|
798
1002
|
hasHeader,
|
|
799
|
-
hasSeparators,
|
|
800
1003
|
});
|
|
801
1004
|
this.leadingSpacerStyle =
|
|
802
|
-
plan.leadingExtent > EMPTY_OFFSET
|
|
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;
|
|
803
1010
|
this.trailingSpacerStyle =
|
|
804
|
-
plan.trailingExtent > EMPTY_OFFSET
|
|
1011
|
+
plan.trailingExtent > EMPTY_OFFSET
|
|
1012
|
+
? this.spacerStyle(plan.trailingExtent)
|
|
1013
|
+
: null;
|
|
805
1014
|
this.renderedStickyIndices =
|
|
806
1015
|
stickySet !== undefined && plan.stickyChildPositions.length > 0
|
|
807
1016
|
? plan.stickyChildPositions
|
|
808
1017
|
: undefined;
|
|
1018
|
+
dlog(
|
|
1019
|
+
`STICKY[list] stickySet=${stickySet === undefined ? 'undefined' : JSON.stringify([...stickySet])} ` +
|
|
1020
|
+
`childPositions=${JSON.stringify(plan.stickyChildPositions)} ` +
|
|
1021
|
+
`forcedStickyCell=${plan.forcedStickyCell === undefined ? 'none' : plan.forcedStickyCell.index} ` +
|
|
1022
|
+
`window=[${m.first},${m.last}] hasHeader=${this.headerDir !== undefined}`,
|
|
1023
|
+
);
|
|
1024
|
+
|
|
1025
|
+
this.forcedStickyCell =
|
|
1026
|
+
plan.forcedStickyCell !== undefined
|
|
1027
|
+
? this.buildWindowCell(
|
|
1028
|
+
plan.forcedStickyCell.index,
|
|
1029
|
+
plan.forcedStickyCell.key,
|
|
1030
|
+
false,
|
|
1031
|
+
)
|
|
1032
|
+
: null;
|
|
809
1033
|
|
|
810
1034
|
const cells: IWindowCell<ItemT>[] = [];
|
|
811
1035
|
for (const planned of plan.cells) {
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
}
|
|
822
|
-
cells.push({
|
|
823
|
-
key: planned.key,
|
|
824
|
-
index: planned.index,
|
|
825
|
-
context,
|
|
826
|
-
measure: this.cellMeasure(planned.index),
|
|
827
|
-
separatorContext,
|
|
828
|
-
});
|
|
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
|
+
);
|
|
829
1045
|
}
|
|
830
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>>();
|
|
831
1051
|
|
|
832
1052
|
dlog(
|
|
833
1053
|
`Angular VirtualizedList window [${m.first}, ${m.last}] of ${m.count} ` +
|
|
@@ -835,7 +1055,53 @@ export class VirtualizedList<ItemT = unknown>
|
|
|
835
1055
|
);
|
|
836
1056
|
}
|
|
837
1057
|
|
|
838
|
-
|
|
1058
|
+
// Assembles one cell (window or forced-sticky) — the context, measure closure, and optional
|
|
1059
|
+
// separator context — so both plan.cells and plan.forcedStickyCell build identically.
|
|
1060
|
+
private buildWindowCell(
|
|
1061
|
+
index: number,
|
|
1062
|
+
key: string,
|
|
1063
|
+
includeSeparator: boolean,
|
|
1064
|
+
): IWindowCell<ItemT> {
|
|
1065
|
+
const item = this.getItem(this.data, index);
|
|
1066
|
+
return {
|
|
1067
|
+
key,
|
|
1068
|
+
index,
|
|
1069
|
+
context: this.contextFor(key, index, item),
|
|
1070
|
+
measure: this.cellMeasure(index),
|
|
1071
|
+
separatorContext: includeSeparator
|
|
1072
|
+
? this.buildSeparatorContext(index, item)
|
|
1073
|
+
: undefined,
|
|
1074
|
+
};
|
|
1075
|
+
}
|
|
1076
|
+
|
|
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> {
|
|
839
1105
|
const overrides = this.separatorOverrides.get(index);
|
|
840
1106
|
const highlighted = overrides?.highlighted ?? false;
|
|
841
1107
|
const context: IVListSeparatorContext<ItemT> = {
|
|
@@ -848,7 +1114,12 @@ export class VirtualizedList<ItemT = unknown>
|
|
|
848
1114
|
// arbitrary separator props); the typed fields above stay authoritative.
|
|
849
1115
|
if (overrides !== undefined) {
|
|
850
1116
|
for (const key of Object.keys(overrides)) {
|
|
851
|
-
if (
|
|
1117
|
+
if (
|
|
1118
|
+
key === 'highlighted' ||
|
|
1119
|
+
key === 'leadingItem' ||
|
|
1120
|
+
key === 'trailingItem'
|
|
1121
|
+
)
|
|
1122
|
+
continue;
|
|
852
1123
|
context[key] = overrides[key];
|
|
853
1124
|
}
|
|
854
1125
|
}
|
|
@@ -865,10 +1136,13 @@ export class VirtualizedList<ItemT = unknown>
|
|
|
865
1136
|
const measure = (event: ISymbioteEvent): void => {
|
|
866
1137
|
const length = readLayoutLength(event, this.isHorizontal);
|
|
867
1138
|
if (length === undefined) return;
|
|
868
|
-
|
|
1139
|
+
const offset = readLayoutOffset(event, this.isHorizontal);
|
|
1140
|
+
dlog(
|
|
1141
|
+
`Angular VirtualizedList cell ${index} measured length=${length} offset=${offset ?? 'none'}`,
|
|
1142
|
+
);
|
|
869
1143
|
// The reducer guards a fixed getItemLayout and a repeated length; a no-op leaves state and the
|
|
870
1144
|
// view untouched (dispatch marks for check only when something changed).
|
|
871
|
-
this.dispatch({ kind: 'measure', index, length });
|
|
1145
|
+
this.dispatch({ kind: 'measure', index, length, offset });
|
|
872
1146
|
};
|
|
873
1147
|
this.cellMeasures.set(index, measure);
|
|
874
1148
|
return measure;
|
|
@@ -876,18 +1150,29 @@ export class VirtualizedList<ItemT = unknown>
|
|
|
876
1150
|
|
|
877
1151
|
// Per-cell onLayout rides the engine's structural (layout) event channel — Angular forbids an
|
|
878
1152
|
// [onLayout] property binding (NG8002). $event arrives untyped, narrowed before reaching measure.
|
|
879
|
-
handleCellLayout(
|
|
1153
|
+
handleCellLayout(
|
|
1154
|
+
measure: (event: ISymbioteEvent) => void,
|
|
1155
|
+
event: unknown,
|
|
1156
|
+
): void {
|
|
880
1157
|
if (this.isSymbioteEvent(event)) measure(event);
|
|
881
1158
|
}
|
|
882
1159
|
|
|
883
1160
|
private isSymbioteEvent(value: unknown): value is ISymbioteEvent {
|
|
884
|
-
return
|
|
1161
|
+
return (
|
|
1162
|
+
typeof value === 'object' && value !== null && 'nativeEvent' in value
|
|
1163
|
+
);
|
|
885
1164
|
}
|
|
886
1165
|
|
|
887
|
-
private mergeSeparator(
|
|
1166
|
+
private mergeSeparator(
|
|
1167
|
+
gapIndex: number,
|
|
1168
|
+
patch: Partial<ISeparatorProps<unknown>>,
|
|
1169
|
+
): void {
|
|
888
1170
|
const count = this.listState.metrics.count;
|
|
889
1171
|
if (!isSeparatorGapInRange(gapIndex, count)) return;
|
|
890
|
-
this.separatorOverrides.set(gapIndex, {
|
|
1172
|
+
this.separatorOverrides.set(gapIndex, {
|
|
1173
|
+
...this.separatorOverrides.get(gapIndex),
|
|
1174
|
+
...patch,
|
|
1175
|
+
});
|
|
891
1176
|
this.cdr.markForCheck();
|
|
892
1177
|
}
|
|
893
1178
|
|
|
@@ -903,7 +1188,10 @@ export class VirtualizedList<ItemT = unknown>
|
|
|
903
1188
|
this.mergeSeparator(index - 1, { highlighted: false });
|
|
904
1189
|
this.mergeSeparator(index, { highlighted: false });
|
|
905
1190
|
},
|
|
906
|
-
updateProps: (
|
|
1191
|
+
updateProps: (
|
|
1192
|
+
select: 'leading' | 'trailing',
|
|
1193
|
+
newProps: Record<string, unknown>,
|
|
1194
|
+
): void => {
|
|
907
1195
|
this.mergeSeparator(select === 'leading' ? index - 1 : index, newProps);
|
|
908
1196
|
},
|
|
909
1197
|
};
|
|
@@ -936,7 +1224,11 @@ export class VirtualizedList<ItemT = unknown>
|
|
|
936
1224
|
});
|
|
937
1225
|
}
|
|
938
1226
|
|
|
939
|
-
scrollToItem(params: {
|
|
1227
|
+
scrollToItem(params: {
|
|
1228
|
+
item: unknown;
|
|
1229
|
+
animated?: boolean;
|
|
1230
|
+
viewPosition?: number;
|
|
1231
|
+
}): void {
|
|
940
1232
|
this.dispatch({
|
|
941
1233
|
kind: 'scroll-to-item',
|
|
942
1234
|
item: params.item,
|
|
@@ -946,7 +1238,10 @@ export class VirtualizedList<ItemT = unknown>
|
|
|
946
1238
|
}
|
|
947
1239
|
|
|
948
1240
|
scrollToEnd(params?: { animated?: boolean }): void {
|
|
949
|
-
this.dispatch({
|
|
1241
|
+
this.dispatch({
|
|
1242
|
+
kind: 'scroll-to-end',
|
|
1243
|
+
animated: params?.animated ?? true,
|
|
1244
|
+
});
|
|
950
1245
|
}
|
|
951
1246
|
|
|
952
1247
|
flashScrollIndicators(): void {
|