@symbiote-native/angular 0.7.0 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/babel-lower-host-primitives.cjs +636 -0
- package/babel-register-composed.cjs +21 -3
- package/build/angular/anchor-host-registry.js +3 -1
- 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.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.d.ts +3 -0
- package/build/angular/components/button.js +22 -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 -10
- package/build/angular/components/image/index.android.js.map +1 -1
- package/build/angular/components/image/index.ios.js +4 -10
- package/build/angular/components/image/index.ios.js.map +1 -1
- package/build/angular/components/image/shared.d.ts +4 -4
- package/build/angular/components/image/shared.js +51 -33
- package/build/angular/components/image/shared.js.map +1 -1
- package/build/angular/components/image-background.d.ts +3 -0
- package/build/angular/components/image-background.js +32 -9
- package/build/angular/components/image-background.js.map +1 -1
- package/build/angular/components/input-accessory-view/index.d.ts +2 -1
- package/build/angular/components/input-accessory-view/index.js +18 -16
- 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 +56 -19
- package/build/angular/components/keyboard-avoiding-view/index.js.map +1 -1
- package/build/angular/components/modal/index.d.ts +1 -0
- package/build/angular/components/modal/index.js +21 -10
- package/build/angular/components/modal/index.js.map +1 -1
- package/build/angular/components/pressable/index.d.ts +10 -5
- package/build/angular/components/pressable/index.js +61 -16
- package/build/angular/components/pressable/index.js.map +1 -1
- package/build/angular/components/refresh-control/index.d.ts +2 -1
- package/build/angular/components/refresh-control/index.js +13 -9
- package/build/angular/components/refresh-control/index.js.map +1 -1
- package/build/angular/components/safe-area-view/index.d.ts +3 -1
- package/build/angular/components/safe-area-view/index.js +22 -20
- 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.d.ts +2 -0
- package/build/angular/components/scroll-view/shared.js +48 -11
- 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 +9 -1
- package/build/angular/components/section-list/index.js +72 -18
- package/build/angular/components/section-list/index.js.map +1 -1
- package/build/angular/components/switch/index.android.js +23 -9
- package/build/angular/components/switch/index.android.js.map +1 -1
- package/build/angular/components/switch/index.ios.js +23 -9
- 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 +8 -2
- package/build/angular/components/text-input.js +79 -34
- package/build/angular/components/text-input.js.map +1 -1
- package/build/angular/components/touchable/index.d.ts +36 -7
- package/build/angular/components/touchable/index.js +280 -72
- package/build/angular/components/touchable/index.js.map +1 -1
- package/build/angular/components/touchable-native-feedback/index.d.ts +2 -1
- package/build/angular/components/touchable-native-feedback/index.js +19 -17
- 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 +8 -2
- package/build/angular/components/virtualized-list/index.js +207 -61
- 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 +21 -1
- package/build/angular/components/virtualized-section-list/index.js +100 -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 +1 -1
- package/build/angular/components.js.map +1 -1
- package/build/angular/create-portal/index.d.ts +11 -2
- package/build/angular/create-portal/index.js +42 -12
- 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/gate-demand.d.ts +37 -0
- package/build/angular/gate-demand.js +84 -0
- package/build/angular/gate-demand.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 +6 -2
- package/build/angular/index.js +16 -1
- package/build/angular/index.js.map +1 -1
- package/build/angular/modules/animated/create-animated-component.d.ts +1 -0
- package/build/angular/modules/animated/create-animated-component.js +41 -18
- 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 +31 -2
- package/build/angular/primitives/index.js +81 -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/register.d.ts +1 -0
- package/build/angular/register.js +35 -0
- package/build/angular/register.js.map +1 -0
- package/build/angular/render/index.js +51 -6
- package/build/angular/render/index.js.map +1 -1
- package/build/angular/renderer/index.d.ts +1 -0
- package/build/angular/renderer/index.js +157 -21
- 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/build/angular/state-style.d.ts +9 -0
- package/build/angular/state-style.js +39 -0
- package/build/angular/state-style.js.map +1 -0
- package/metro-transformer.cjs +69 -0
- package/package.json +27 -8
- package/src/anchor-host-registry.ts +3 -1
- 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 +36 -6
- package/src/components/flat-list/index.ts +91 -29
- package/src/components/image/index.android.ts +9 -6
- package/src/components/image/index.ios.ts +9 -6
- package/src/components/image/shared.ts +110 -45
- package/src/components/image-background.ts +47 -10
- package/src/components/input-accessory-view/index.ts +41 -18
- package/src/components/keyboard-avoiding-view/index.ts +76 -22
- package/src/components/modal/index.ts +39 -12
- package/src/components/pressable/index.ts +121 -31
- package/src/components/refresh-control/index.ts +36 -13
- package/src/components/safe-area-view/index.ts +44 -19
- 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 +124 -30
- package/src/components/scroll-view/sticky-header.ts +25 -8
- package/src/components/section-list/index.ts +79 -19
- package/src/components/switch/index.android.ts +18 -7
- package/src/components/switch/index.ios.ts +18 -7
- package/src/components/switch/shared.ts +15 -4
- package/src/components/text-input.ts +119 -31
- package/src/components/touchable/index.ts +339 -72
- package/src/components/touchable-native-feedback/index.ts +50 -18
- package/src/components/virtualized-list/directives.ts +19 -5
- package/src/components/virtualized-list/index.ts +292 -72
- package/src/components/virtualized-section-list/directives.ts +6 -3
- package/src/components/virtualized-section-list/index.ts +164 -34
- package/src/components.ts +6 -1
- package/src/create-portal/index.ts +47 -11
- 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/gate-demand.ts +109 -0
- package/src/host-instance/index.ts +9 -3
- package/src/index.ts +33 -2
- package/src/modules/animated/create-animated-component.ts +67 -19
- package/src/modules/app-registry/index.ts +3 -1
- package/src/modules/status-bar/index.ts +13 -4
- package/src/primitives/index.ts +73 -2
- package/src/primitives/shared.ts +70 -9
- package/src/register.ts +43 -0
- package/src/render/index.ts +59 -7
- package/src/renderer/index.ts +209 -37
- package/src/services/color-scheme.service.ts +3 -1
- package/src/services/window-dimensions.service.ts +11 -4
- package/src/state-style.ts +39 -0
|
@@ -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,
|
|
@@ -120,13 +127,21 @@ export {
|
|
|
120
127
|
VListItemDirective,
|
|
121
128
|
VListSeparatorDirective,
|
|
122
129
|
} from './directives';
|
|
130
|
+
import {
|
|
131
|
+
gateWanted,
|
|
132
|
+
injectGateDemandAbove,
|
|
133
|
+
provideGateDemand,
|
|
134
|
+
type IGateDemand,
|
|
135
|
+
type IGatedAccessibilityEvent,
|
|
136
|
+
} from '../../gate-demand';
|
|
123
137
|
export type { IVListItemContext, IVListSeparatorContext } from './directives';
|
|
124
138
|
|
|
125
139
|
// The Angular VirtualizedList prop surface. Mirrors React/Vue's IVirtualizedListProps MINUS the
|
|
126
140
|
// element-returning props (renderItem, ListHeader/Footer/Empty Component, ItemSeparatorComponent):
|
|
127
141
|
// those are the per-adapter children/render fields and become `<ng-template>` directives in Angular.
|
|
128
142
|
// Everything agnostic is the SAME surface.
|
|
129
|
-
export interface IVirtualizedListProps<ItemT>
|
|
143
|
+
export interface IVirtualizedListProps<ItemT>
|
|
144
|
+
extends IAccessibilityProps, IAriaProps {
|
|
130
145
|
data: unknown;
|
|
131
146
|
getItem: (data: unknown, index: number) => ItemT;
|
|
132
147
|
getItemCount: (data: unknown) => number;
|
|
@@ -205,7 +220,10 @@ interface IWindowCell<ItemT> {
|
|
|
205
220
|
@Component({
|
|
206
221
|
selector: 'VirtualizedList',
|
|
207
222
|
standalone: true,
|
|
208
|
-
|
|
223
|
+
viewProviders: [provideGateDemand(() => VirtualizedList)],
|
|
224
|
+
hostDirectives: [
|
|
225
|
+
{ directive: SymbioteStyleInputDirective, inputs: ['style'] },
|
|
226
|
+
],
|
|
209
227
|
schemas: [CUSTOM_ELEMENTS_SCHEMA],
|
|
210
228
|
imports: [ScrollView, RefreshControl, VListOutletDirective, ViewHost],
|
|
211
229
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
@@ -224,12 +242,20 @@ interface IWindowCell<ItemT> {
|
|
|
224
242
|
[accessibilityValue]="foldedAccessibility().accessibilityValue"
|
|
225
243
|
[accessibilityActions]="foldedAccessibility().accessibilityActions"
|
|
226
244
|
[accessibilityLabelledBy]="foldedAccessibility().accessibilityLabelledBy"
|
|
227
|
-
[importantForAccessibility]="
|
|
245
|
+
[importantForAccessibility]="
|
|
246
|
+
foldedAccessibility().importantForAccessibility
|
|
247
|
+
"
|
|
228
248
|
[accessibilityLiveRegion]="foldedAccessibility().accessibilityLiveRegion"
|
|
229
249
|
[screenReaderFocusable]="foldedAccessibility().screenReaderFocusable"
|
|
230
|
-
[accessibilityViewIsModal]="
|
|
231
|
-
|
|
232
|
-
|
|
250
|
+
[accessibilityViewIsModal]="
|
|
251
|
+
foldedAccessibility().accessibilityViewIsModal
|
|
252
|
+
"
|
|
253
|
+
[accessibilityElementsHidden]="
|
|
254
|
+
foldedAccessibility().accessibilityElementsHidden
|
|
255
|
+
"
|
|
256
|
+
[accessibilityIgnoresInvertColors]="
|
|
257
|
+
foldedAccessibility().accessibilityIgnoresInvertColors
|
|
258
|
+
"
|
|
233
259
|
[accessibilityLanguage]="foldedAccessibility().accessibilityLanguage"
|
|
234
260
|
[accessibilityRespondsToUserInteraction]="
|
|
235
261
|
foldedAccessibility().accessibilityRespondsToUserInteraction
|
|
@@ -237,7 +263,9 @@ interface IWindowCell<ItemT> {
|
|
|
237
263
|
[accessibilityShowsLargeContentViewer]="
|
|
238
264
|
foldedAccessibility().accessibilityShowsLargeContentViewer
|
|
239
265
|
"
|
|
240
|
-
[accessibilityLargeContentTitle]="
|
|
266
|
+
[accessibilityLargeContentTitle]="
|
|
267
|
+
foldedAccessibility().accessibilityLargeContentTitle
|
|
268
|
+
"
|
|
241
269
|
(accessibilityAction)="accessibilityActionTick($event)"
|
|
242
270
|
(accessibilityTap)="accessibilityTapTick($event)"
|
|
243
271
|
(magicTap)="magicTapTick($event)"
|
|
@@ -293,21 +321,29 @@ interface IWindowCell<ItemT> {
|
|
|
293
321
|
@if (gapSpacerStyle !== null) {
|
|
294
322
|
<symbiote-view [style]="gapSpacerStyle"></symbiote-view>
|
|
295
323
|
}
|
|
324
|
+
<!-- The separator sits INSIDE the measuring view, as RN's own cell renderer places it
|
|
325
|
+
(VirtualizedListCellRenderer.js:218-221). As a sibling it is an extra flex child, so
|
|
326
|
+
the chrome between two cells is gap + separator + gap while a spacer collapsing that
|
|
327
|
+
region replaces it with a single gap — every cell below the leading spacer then lands
|
|
328
|
+
short by (separator + gap) and the content jumps by that much whenever the window's
|
|
329
|
+
first index moves. Measured at exactly 17px on device 2026-08-19; see
|
|
330
|
+
.claude/rules/list-geometry-feedback-loop.md. -->
|
|
296
331
|
@for (cell of windowCells; track cell.key) {
|
|
297
|
-
<symbiote-view
|
|
332
|
+
<symbiote-view
|
|
333
|
+
(layout)="handleCellLayout(cell.measure, $event)"
|
|
334
|
+
[style]="cellStyle"
|
|
335
|
+
>
|
|
298
336
|
<ng-container
|
|
299
337
|
[vListOutlet]="itemDir?.templateRef"
|
|
300
338
|
[vListOutletContext]="cell.context"
|
|
301
339
|
></ng-container>
|
|
302
|
-
|
|
303
|
-
@if (cell.separatorContext !== undefined) {
|
|
304
|
-
<symbiote-view>
|
|
340
|
+
@if (cell.separatorContext !== undefined) {
|
|
305
341
|
<ng-container
|
|
306
342
|
[vListOutlet]="separatorDir?.templateRef"
|
|
307
343
|
[vListOutletContext]="cell.separatorContext"
|
|
308
344
|
></ng-container>
|
|
309
|
-
|
|
310
|
-
|
|
345
|
+
}
|
|
346
|
+
</symbiote-view>
|
|
311
347
|
}
|
|
312
348
|
@if (trailingSpacerStyle !== null) {
|
|
313
349
|
<symbiote-view [style]="trailingSpacerStyle"></symbiote-view>
|
|
@@ -334,10 +370,16 @@ export class VirtualizedList<ItemT = unknown>
|
|
|
334
370
|
// The list's edge/viewability/failure events as real Angular events: `(endReached)="…"`, not
|
|
335
371
|
// `[onEndReached]="…"`. See handleRefresh/accessibility*Tick below for how the still-callback-
|
|
336
372
|
// shaped ScrollView/RefreshControl @Input()s are fed from these.
|
|
337
|
-
@Output() readonly endReached = new EventEmitter<{
|
|
338
|
-
|
|
373
|
+
@Output() readonly endReached = new EventEmitter<{
|
|
374
|
+
distanceFromEnd: number;
|
|
375
|
+
}>();
|
|
376
|
+
@Output() readonly startReached = new EventEmitter<{
|
|
377
|
+
distanceFromStart: number;
|
|
378
|
+
}>();
|
|
339
379
|
@Output() readonly refresh = new EventEmitter<void>();
|
|
340
|
-
@Output() readonly viewableItemsChanged = new EventEmitter<
|
|
380
|
+
@Output() readonly viewableItemsChanged = new EventEmitter<
|
|
381
|
+
IViewableItemsChangedInfo<ItemT>
|
|
382
|
+
>();
|
|
341
383
|
@Output() readonly scrollToIndexFailed = new EventEmitter<{
|
|
342
384
|
index: number;
|
|
343
385
|
highestMeasuredFrameIndex: number;
|
|
@@ -347,6 +389,16 @@ export class VirtualizedList<ItemT = unknown>
|
|
|
347
389
|
@Output() readonly accessibilityTap = new EventEmitter<ISymbioteEvent>();
|
|
348
390
|
@Output() readonly magicTap = new EventEmitter<ISymbioteEvent>();
|
|
349
391
|
@Output() readonly accessibilityEscape = new EventEmitter<ISymbioteEvent>();
|
|
392
|
+
|
|
393
|
+
// This wrapper binds the four gated accessibility events on the component it renders, which
|
|
394
|
+
// Angular forces to be unconditional and which would light that component's gates on every
|
|
395
|
+
// instance. It answers for them instead — see `gate-demand.ts`.
|
|
396
|
+
private readonly gateDemandAbove = injectGateDemandAbove();
|
|
397
|
+
|
|
398
|
+
wantsGate(name: IGatedAccessibilityEvent): boolean {
|
|
399
|
+
return gateWanted(this.gateDemandAbove, name, this[name]);
|
|
400
|
+
}
|
|
401
|
+
|
|
350
402
|
@Input({ required: true }) data!: unknown;
|
|
351
403
|
@Input({ required: true }) getItem!: (data: unknown, index: number) => ItemT;
|
|
352
404
|
@Input({ required: true }) getItemCount!: (data: unknown) => number;
|
|
@@ -371,7 +423,8 @@ export class VirtualizedList<ItemT = unknown>
|
|
|
371
423
|
// (no wrapper) falls back to this component's own `.observed`, unchanged from before.
|
|
372
424
|
@Input() refreshRequested?: boolean;
|
|
373
425
|
@Input() viewabilityConfig?: IViewabilityConfig;
|
|
374
|
-
@Input()
|
|
426
|
+
@Input()
|
|
427
|
+
viewabilityConfigCallbackPairs?: IViewabilityConfigCallbackPair<ItemT>[];
|
|
375
428
|
@Input() initialNumToRender?: number;
|
|
376
429
|
@Input() initialScrollIndex?: number;
|
|
377
430
|
@Input() maxToRenderPerBatch?: number;
|
|
@@ -402,8 +455,10 @@ export class VirtualizedList<ItemT = unknown>
|
|
|
402
455
|
@Input() accessibilityValue?: IAccessibilityProps['accessibilityValue'];
|
|
403
456
|
@Input() accessibilityActions?: IAccessibilityProps['accessibilityActions'];
|
|
404
457
|
@Input() accessibilityLabelledBy?: string | string[];
|
|
405
|
-
@Input()
|
|
406
|
-
|
|
458
|
+
@Input()
|
|
459
|
+
importantForAccessibility?: IAccessibilityProps['importantForAccessibility'];
|
|
460
|
+
@Input()
|
|
461
|
+
accessibilityLiveRegion?: IAccessibilityProps['accessibilityLiveRegion'];
|
|
407
462
|
@Input() screenReaderFocusable?: boolean;
|
|
408
463
|
@Input() accessibilityViewIsModal?: boolean;
|
|
409
464
|
@Input() accessibilityElementsHidden?: boolean;
|
|
@@ -435,7 +490,8 @@ export class VirtualizedList<ItemT = unknown>
|
|
|
435
490
|
@ContentChild(VListHeaderDirective) headerDir?: VListHeaderDirective;
|
|
436
491
|
@ContentChild(VListFooterDirective) footerDir?: VListFooterDirective;
|
|
437
492
|
@ContentChild(VListEmptyDirective) emptyDir?: VListEmptyDirective;
|
|
438
|
-
@ContentChild(VListSeparatorDirective)
|
|
493
|
+
@ContentChild(VListSeparatorDirective)
|
|
494
|
+
separatorDir?: VListSeparatorDirective<ItemT>;
|
|
439
495
|
|
|
440
496
|
// The composed inner scroll view. Its instance IS an IScrollViewHandle (scrollTo / scrollToEnd /
|
|
441
497
|
// flashScrollIndicators / getScrollNode), so the imperative handle delegates straight to it.
|
|
@@ -462,7 +518,8 @@ export class VirtualizedList<ItemT = unknown>
|
|
|
462
518
|
resolvedStyle: IViewStyle | undefined = undefined;
|
|
463
519
|
resolvedContentContainerStyle: IStyleProp<IViewStyle> | undefined = undefined;
|
|
464
520
|
resolvedMaintainVisibleContentPosition:
|
|
465
|
-
{ minIndexForVisible: number; autoscrollToTopThreshold?: number }
|
|
521
|
+
| { minIndexForVisible: number; autoscrollToTopThreshold?: number }
|
|
522
|
+
| undefined = undefined;
|
|
466
523
|
// The offset we are imperatively driving native to before the scroll handle attaches (rides down
|
|
467
524
|
// as contentOffset). A fresh object identity each push so the commit re-applies a repeated value.
|
|
468
525
|
commandedOffset: { x: number; y: number } | undefined = undefined;
|
|
@@ -471,20 +528,41 @@ export class VirtualizedList<ItemT = unknown>
|
|
|
471
528
|
// measured lengths, committed window, edge/viewability dedup, MVCP anchor — all in IListState,
|
|
472
529
|
// driven by the shared reduceList. `renderVersion` bumps whenever a transition changes render
|
|
473
530
|
// state, so ngDoCheck's recompute-dedup can detect it (listState is a plain object, not tracked).
|
|
474
|
-
private readonly listState: IListState<ItemT> =
|
|
531
|
+
private readonly listState: IListState<ItemT> =
|
|
532
|
+
createInitialListState<ItemT>();
|
|
475
533
|
private renderVersion = EMPTY_OFFSET;
|
|
476
534
|
|
|
477
|
-
private readonly cellMeasures = new Map<
|
|
478
|
-
|
|
535
|
+
private readonly cellMeasures = new Map<
|
|
536
|
+
number,
|
|
537
|
+
(event: ISymbioteEvent) => void
|
|
538
|
+
>();
|
|
539
|
+
// A cell's context object is the ONLY thing its stamped view reads, and VListOutletDirective
|
|
540
|
+
// refreshes that view whenever the object's IDENTITY changes. recomputeView runs on every frame
|
|
541
|
+
// the window moves, so minting a fresh context per cell made a four-row slide cost a refresh of
|
|
542
|
+
// EVERY cell in the window. MEASURED on PATH B geometry (544 entries, 320px viewport, RN
|
|
543
|
+
// defaults), per fling frame: 233-cell window, 4 cells genuinely entering, 228 outlet updates —
|
|
544
|
+
// each one a copyContextFields plus a markForCheck that walks to the root. Solid, same geometry,
|
|
545
|
+
// same shared reducer, same engine output: 4. Handing back the SAME object for a cell whose item
|
|
546
|
+
// and index did not move keeps `[vListOutletContext]` reference-equal, so ngOnChanges never fires
|
|
547
|
+
// for it. The fast adapters get this by construction — Solid's <For> is keyed on the cell KEY
|
|
548
|
+
// strings precisely so "the plan's freshly-built cell objects" cannot rebuild every row
|
|
549
|
+
// (adapters/solid/src/components/virtualized-list/shared.tsx, the cellKeys memo).
|
|
550
|
+
private cellContexts = new Map<string, IVListItemContext<ItemT>>();
|
|
551
|
+
private nextCellContexts = new Map<string, IVListItemContext<ItemT>>();
|
|
552
|
+
// Everything a cell's CONTENT can depend on that is not its own item identity. RN's contract for
|
|
553
|
+
// "re-render the cells, the data object itself did not change" is extraData; without it here an
|
|
554
|
+
// extraData flip would reuse every context and repaint nothing.
|
|
555
|
+
private cellContextEpoch: unknown[] | undefined = undefined;
|
|
556
|
+
private readonly separatorOverrides = new Map<
|
|
557
|
+
number,
|
|
558
|
+
Partial<ISeparatorProps<unknown>>
|
|
559
|
+
>();
|
|
479
560
|
// The adapter-owned debounce (minimumViewTime) and incremental-fill timers; the reducer only
|
|
480
561
|
// hands back a delay.
|
|
481
562
|
private viewableTimer: ReturnType<typeof setTimeout> | null = null;
|
|
482
563
|
private batchTimer: ReturnType<typeof setTimeout> | null = null;
|
|
483
564
|
// Dedupes the after-commit effects: they run only when the windowing signature changed.
|
|
484
565
|
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
566
|
// Dedupes ngDoCheck's own recompute. WITHOUT this, recomputeView() unconditionally rebuilds
|
|
489
567
|
// windowCells (and each cell's `separators` handle — fresh closures every call) on EVERY CD pass,
|
|
490
568
|
// including ones triggered by something else entirely in the app. A fresh context object flows
|
|
@@ -543,8 +621,10 @@ export class VirtualizedList<ItemT = unknown>
|
|
|
543
621
|
accessibilityElementsHidden: this.accessibilityElementsHidden,
|
|
544
622
|
accessibilityIgnoresInvertColors: this.accessibilityIgnoresInvertColors,
|
|
545
623
|
accessibilityLanguage: this.accessibilityLanguage,
|
|
546
|
-
accessibilityRespondsToUserInteraction:
|
|
547
|
-
|
|
624
|
+
accessibilityRespondsToUserInteraction:
|
|
625
|
+
this.accessibilityRespondsToUserInteraction,
|
|
626
|
+
accessibilityShowsLargeContentViewer:
|
|
627
|
+
this.accessibilityShowsLargeContentViewer,
|
|
548
628
|
accessibilityLargeContentTitle: this.accessibilityLargeContentTitle,
|
|
549
629
|
role: this.role,
|
|
550
630
|
'aria-label': this.ariaLabel,
|
|
@@ -584,7 +664,9 @@ export class VirtualizedList<ItemT = unknown>
|
|
|
584
664
|
return this.maxToRenderPerBatch ?? DEFAULT_MAX_TO_RENDER_PER_BATCH;
|
|
585
665
|
}
|
|
586
666
|
private get updateCellsBatchingPeriodValue(): number {
|
|
587
|
-
return
|
|
667
|
+
return (
|
|
668
|
+
this.updateCellsBatchingPeriod ?? DEFAULT_UPDATE_CELLS_BATCHING_PERIOD
|
|
669
|
+
);
|
|
588
670
|
}
|
|
589
671
|
private get onEndReachedThresholdValue(): number {
|
|
590
672
|
return this.onEndReachedThreshold ?? DEFAULT_END_REACHED_THRESHOLD;
|
|
@@ -597,6 +679,7 @@ export class VirtualizedList<ItemT = unknown>
|
|
|
597
679
|
// flow through it, which an @Output() can't carry), so this is a stable arrow field passed
|
|
598
680
|
// straight to [onScroll]. onLayout is a real @Output() now, bound via (layout)="onLayoutTick($event)".
|
|
599
681
|
onScrollTick = (event: ISymbioteEvent): void => {
|
|
682
|
+
countAngular('scrollTicks');
|
|
600
683
|
const offset = readScrollOffset(event, this.isHorizontal);
|
|
601
684
|
if (offset === undefined) return;
|
|
602
685
|
dlog(`Angular VirtualizedList onScroll offset=${offset}`);
|
|
@@ -663,7 +746,8 @@ export class VirtualizedList<ItemT = unknown>
|
|
|
663
746
|
onStartReachedActive: this.startReached.observed,
|
|
664
747
|
viewabilityPairs: buildViewabilityPairs(
|
|
665
748
|
this.viewableItemsChanged.observed
|
|
666
|
-
? (info: IViewableItemsChangedInfo<ItemT>): void =>
|
|
749
|
+
? (info: IViewableItemsChangedInfo<ItemT>): void =>
|
|
750
|
+
this.viewableItemsChanged.emit(info)
|
|
667
751
|
: undefined,
|
|
668
752
|
this.viewabilityConfig,
|
|
669
753
|
this.viewabilityConfigCallbackPairs,
|
|
@@ -681,23 +765,58 @@ export class VirtualizedList<ItemT = unknown>
|
|
|
681
765
|
this.runEffects(result.effects, inputs);
|
|
682
766
|
if (!result.changed) return;
|
|
683
767
|
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;
|
|
768
|
+
if (this.isWindowSettled(action)) return;
|
|
769
|
+
countAngular('listMarks');
|
|
691
770
|
this.cdr.markForCheck();
|
|
692
771
|
}
|
|
693
772
|
|
|
694
|
-
// The
|
|
695
|
-
|
|
773
|
+
// The reducer reports `changed` for EVERY scroll offset - it has to, since the offset feeds
|
|
774
|
+
// end-reached distance, viewability and the batch-fill timer, all handled by runEffects. The
|
|
775
|
+
// TEMPLATE reads none of those; it reads the window. Marking on the offset repainted the whole
|
|
776
|
+
// ancestor chain 60 times a second on a sticky screen for a window that had not moved.
|
|
777
|
+
//
|
|
778
|
+
// So the question here is "would a render leave the window exactly where it is", and it is asked
|
|
779
|
+
// of the state as it is NOW. It must NOT be answered by comparing the last-rendered window with
|
|
780
|
+
// the one before it: that window was derived from the PREVIOUS scroll offset, so once two
|
|
781
|
+
// consecutive derives agree the comparison reads "settled" for every offset that ever follows -
|
|
782
|
+
// no mark, no CD, no ngDoCheck, no refresh-metrics, so the window can never move again and the
|
|
783
|
+
// list goes permanently deaf mid-scroll (device: examples/angular Benchmark sticky path B froze
|
|
784
|
+
// at exactly the window the first viewport asked for, blank below it).
|
|
785
|
+
//
|
|
786
|
+
// computeWindow re-run over the CACHED offset table answers it directly. Two scans, no
|
|
787
|
+
// allocation - buildOffsets, the O(count) allocating half, still runs once per render only. It is
|
|
788
|
+
// the same function the render will use, so this cannot drift away from the window it predicts.
|
|
789
|
+
private isWindowSettled(action: IListAction<ItemT>): boolean {
|
|
790
|
+
// A measurement rewrites the very offset table the prediction reads, so it has no cached
|
|
791
|
+
// geometry to answer from.
|
|
792
|
+
if (action.kind === 'measure') return false;
|
|
696
793
|
const m = this.listState.metrics;
|
|
697
|
-
|
|
794
|
+
// Mid-fill: the committed window is still climbing toward target one batch step per render, and
|
|
795
|
+
// only a render advances it. Skipping the mark here is what stalls the incremental fill.
|
|
796
|
+
if (m.first !== m.target.first || m.last !== m.target.last) return false;
|
|
797
|
+
const next = computeWindow(
|
|
798
|
+
m.count,
|
|
799
|
+
m.offsets,
|
|
800
|
+
m.lengths,
|
|
801
|
+
this.listState.scrollOffset,
|
|
802
|
+
this.listState.viewportLength,
|
|
803
|
+
this.windowSizeValue,
|
|
804
|
+
this.initialNumToRenderValue,
|
|
805
|
+
);
|
|
806
|
+
const isSettled =
|
|
807
|
+
next.first === m.target.first && next.last === m.target.last;
|
|
808
|
+
if (!isSettled)
|
|
809
|
+
dlog(
|
|
810
|
+
`Angular VirtualizedList window moves [${m.first}, ${m.last}] -> ` +
|
|
811
|
+
`[${next.first}, ${next.last}] at offset ${this.listState.scrollOffset}`,
|
|
812
|
+
);
|
|
813
|
+
return isSettled;
|
|
698
814
|
}
|
|
699
815
|
|
|
700
|
-
private runEffects(
|
|
816
|
+
private runEffects(
|
|
817
|
+
effects: IListEffect<ItemT>[],
|
|
818
|
+
inputs: IListReducerInputs<ItemT>,
|
|
819
|
+
): void {
|
|
701
820
|
for (const effect of effects) {
|
|
702
821
|
switch (effect.kind) {
|
|
703
822
|
case 'scroll-to':
|
|
@@ -707,7 +826,9 @@ export class VirtualizedList<ItemT = unknown>
|
|
|
707
826
|
this.endReached.emit({ distanceFromEnd: effect.distanceFromEnd });
|
|
708
827
|
break;
|
|
709
828
|
case 'fire-start-reached':
|
|
710
|
-
this.startReached.emit({
|
|
829
|
+
this.startReached.emit({
|
|
830
|
+
distanceFromStart: effect.distanceFromStart,
|
|
831
|
+
});
|
|
711
832
|
break;
|
|
712
833
|
case 'fire-scroll-to-index-failed':
|
|
713
834
|
this.scrollToIndexFailed.emit({
|
|
@@ -765,6 +886,7 @@ export class VirtualizedList<ItemT = unknown>
|
|
|
765
886
|
// grows exactly one step per meaningful CD. The projected templates (ContentChild) are resolved
|
|
766
887
|
// from the second CD on; the first paint (count/viewport still 0) corrects on the layout-driven CD.
|
|
767
888
|
ngDoCheck(): void {
|
|
889
|
+
countAngular('listChecks');
|
|
768
890
|
const recomputeInputs: unknown[] = [
|
|
769
891
|
this.data,
|
|
770
892
|
this.extraData,
|
|
@@ -800,6 +922,7 @@ export class VirtualizedList<ItemT = unknown>
|
|
|
800
922
|
return;
|
|
801
923
|
}
|
|
802
924
|
// The single derive-per-CD: recompute the window off the current state before the view reads it.
|
|
925
|
+
countAngular('listRecomputes');
|
|
803
926
|
reduceList(this.listState, { kind: 'refresh-metrics' }, this.buildInputs());
|
|
804
927
|
this.recomputeView();
|
|
805
928
|
}
|
|
@@ -824,7 +947,9 @@ export class VirtualizedList<ItemT = unknown>
|
|
|
824
947
|
const hasHeader = this.headerDir !== undefined;
|
|
825
948
|
const hasSeparators = this.separatorDir !== undefined;
|
|
826
949
|
const stickySet =
|
|
827
|
-
this.stickyHeaderIndices !== undefined
|
|
950
|
+
this.stickyHeaderIndices !== undefined
|
|
951
|
+
? new Set(this.stickyHeaderIndices)
|
|
952
|
+
: undefined;
|
|
828
953
|
|
|
829
954
|
this.resolvedContentContainerStyle = this.isHorizontal
|
|
830
955
|
? [this.contentContainerStyle, { width: m.total }]
|
|
@@ -846,17 +971,40 @@ export class VirtualizedList<ItemT = unknown>
|
|
|
846
971
|
: {
|
|
847
972
|
...this.maintainVisibleContentPosition,
|
|
848
973
|
minIndexForVisible:
|
|
849
|
-
this.maintainVisibleContentPosition.minIndexForVisible +
|
|
974
|
+
this.maintainVisibleContentPosition.minIndexForVisible +
|
|
975
|
+
(hasHeader ? 1 : 0),
|
|
850
976
|
};
|
|
851
977
|
|
|
978
|
+
// A change in anything a cell's content reads other than its own item invalidates every cached
|
|
979
|
+
// context: identity reuse is only sound while the reused object still describes the cell.
|
|
980
|
+
const epoch: unknown[] = [
|
|
981
|
+
this.data,
|
|
982
|
+
this.extraData,
|
|
983
|
+
this.getItem,
|
|
984
|
+
this.keyExtractor,
|
|
985
|
+
];
|
|
986
|
+
const previousEpoch = this.cellContextEpoch;
|
|
987
|
+
this.cellContextEpoch = epoch;
|
|
988
|
+
if (
|
|
989
|
+
previousEpoch === undefined ||
|
|
990
|
+
previousEpoch.length !== epoch.length ||
|
|
991
|
+
!previousEpoch.every((value, index) => value === epoch[index])
|
|
992
|
+
) {
|
|
993
|
+
this.cellContexts.clear();
|
|
994
|
+
}
|
|
995
|
+
|
|
852
996
|
if (m.count === FIRST_INDEX) {
|
|
997
|
+
this.cellContexts.clear();
|
|
998
|
+
this.nextCellContexts.clear();
|
|
853
999
|
this.windowCells = [];
|
|
854
1000
|
this.forcedStickyCell = null;
|
|
855
1001
|
this.leadingSpacerStyle = null;
|
|
856
1002
|
this.gapSpacerStyle = null;
|
|
857
1003
|
this.trailingSpacerStyle = null;
|
|
858
1004
|
this.renderedStickyIndices = undefined;
|
|
859
|
-
dlog(
|
|
1005
|
+
dlog(
|
|
1006
|
+
`Angular VirtualizedList empty (viewport=${this.listState.viewportLength})`,
|
|
1007
|
+
);
|
|
860
1008
|
return;
|
|
861
1009
|
}
|
|
862
1010
|
|
|
@@ -870,13 +1018,17 @@ export class VirtualizedList<ItemT = unknown>
|
|
|
870
1018
|
keyFor: this.keyFor,
|
|
871
1019
|
stickyIndices: stickySet,
|
|
872
1020
|
hasHeader,
|
|
873
|
-
hasSeparators,
|
|
874
1021
|
});
|
|
875
1022
|
this.leadingSpacerStyle =
|
|
876
|
-
plan.leadingExtent > EMPTY_OFFSET
|
|
877
|
-
|
|
1023
|
+
plan.leadingExtent > EMPTY_OFFSET
|
|
1024
|
+
? this.spacerStyle(plan.leadingExtent)
|
|
1025
|
+
: null;
|
|
1026
|
+
this.gapSpacerStyle =
|
|
1027
|
+
plan.gapExtent > EMPTY_OFFSET ? this.spacerStyle(plan.gapExtent) : null;
|
|
878
1028
|
this.trailingSpacerStyle =
|
|
879
|
-
plan.trailingExtent > EMPTY_OFFSET
|
|
1029
|
+
plan.trailingExtent > EMPTY_OFFSET
|
|
1030
|
+
? this.spacerStyle(plan.trailingExtent)
|
|
1031
|
+
: null;
|
|
880
1032
|
this.renderedStickyIndices =
|
|
881
1033
|
stickySet !== undefined && plan.stickyChildPositions.length > 0
|
|
882
1034
|
? plan.stickyChildPositions
|
|
@@ -890,15 +1042,30 @@ export class VirtualizedList<ItemT = unknown>
|
|
|
890
1042
|
|
|
891
1043
|
this.forcedStickyCell =
|
|
892
1044
|
plan.forcedStickyCell !== undefined
|
|
893
|
-
? this.buildWindowCell(
|
|
1045
|
+
? this.buildWindowCell(
|
|
1046
|
+
plan.forcedStickyCell.index,
|
|
1047
|
+
plan.forcedStickyCell.key,
|
|
1048
|
+
false,
|
|
1049
|
+
)
|
|
894
1050
|
: null;
|
|
895
1051
|
|
|
896
1052
|
const cells: IWindowCell<ItemT>[] = [];
|
|
897
1053
|
for (const planned of plan.cells) {
|
|
898
|
-
|
|
899
|
-
|
|
1054
|
+
// RN gates the separator on the last index of the DATA, not of the WINDOW
|
|
1055
|
+
// (VirtualizedList.js:793 `const end = getItemCount(data) - 1`), and now that the separator
|
|
1056
|
+
// lives INSIDE the measuring wrapper that distinction is load-bearing: gating on the window
|
|
1057
|
+
// would make a cell's own measured height change as the window slides past it. Device-measured
|
|
1058
|
+
// 2026-08-19 as a run of cells all shifting by exactly the divider's 1px.
|
|
1059
|
+
const includeSeparator = hasSeparators && planned.index < m.count - 1;
|
|
1060
|
+
cells.push(
|
|
1061
|
+
this.buildWindowCell(planned.index, planned.key, includeSeparator),
|
|
1062
|
+
);
|
|
900
1063
|
}
|
|
901
1064
|
this.windowCells = cells;
|
|
1065
|
+
// Swap, don't prune: a cell that left the window is simply absent from the pass that just ran,
|
|
1066
|
+
// so the old map is the garbage and the new one is exactly the live set.
|
|
1067
|
+
this.cellContexts = this.nextCellContexts;
|
|
1068
|
+
this.nextCellContexts = new Map<string, IVListItemContext<ItemT>>();
|
|
902
1069
|
|
|
903
1070
|
dlog(
|
|
904
1071
|
`Angular VirtualizedList window [${m.first}, ${m.last}] of ${m.count} ` +
|
|
@@ -914,21 +1081,45 @@ export class VirtualizedList<ItemT = unknown>
|
|
|
914
1081
|
includeSeparator: boolean,
|
|
915
1082
|
): IWindowCell<ItemT> {
|
|
916
1083
|
const item = this.getItem(this.data, index);
|
|
917
|
-
const context: IVListItemContext<ItemT> = {
|
|
918
|
-
$implicit: item,
|
|
919
|
-
index,
|
|
920
|
-
separators: this.makeSeparators(index),
|
|
921
|
-
};
|
|
922
1084
|
return {
|
|
923
1085
|
key,
|
|
924
1086
|
index,
|
|
925
|
-
context,
|
|
1087
|
+
context: this.contextFor(key, index, item),
|
|
926
1088
|
measure: this.cellMeasure(index),
|
|
927
|
-
separatorContext: includeSeparator
|
|
1089
|
+
separatorContext: includeSeparator
|
|
1090
|
+
? this.buildSeparatorContext(index, item)
|
|
1091
|
+
: undefined,
|
|
928
1092
|
};
|
|
929
1093
|
}
|
|
930
1094
|
|
|
931
|
-
|
|
1095
|
+
// The context for one cell, reused by identity when the cell did not move. See cellContexts.
|
|
1096
|
+
private contextFor(
|
|
1097
|
+
key: string,
|
|
1098
|
+
index: number,
|
|
1099
|
+
item: ItemT,
|
|
1100
|
+
): IVListItemContext<ItemT> {
|
|
1101
|
+
const cached = this.cellContexts.get(key);
|
|
1102
|
+
if (
|
|
1103
|
+
cached !== undefined &&
|
|
1104
|
+
cached.$implicit === item &&
|
|
1105
|
+
cached.index === index
|
|
1106
|
+
) {
|
|
1107
|
+
this.nextCellContexts.set(key, cached);
|
|
1108
|
+
return cached;
|
|
1109
|
+
}
|
|
1110
|
+
const context: IVListItemContext<ItemT> = {
|
|
1111
|
+
$implicit: item,
|
|
1112
|
+
index,
|
|
1113
|
+
separators: this.makeSeparators(index),
|
|
1114
|
+
};
|
|
1115
|
+
this.nextCellContexts.set(key, context);
|
|
1116
|
+
return context;
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
private buildSeparatorContext(
|
|
1120
|
+
index: number,
|
|
1121
|
+
item: ItemT,
|
|
1122
|
+
): IVListSeparatorContext<ItemT> {
|
|
932
1123
|
const overrides = this.separatorOverrides.get(index);
|
|
933
1124
|
const highlighted = overrides?.highlighted ?? false;
|
|
934
1125
|
const context: IVListSeparatorContext<ItemT> = {
|
|
@@ -941,7 +1132,12 @@ export class VirtualizedList<ItemT = unknown>
|
|
|
941
1132
|
// arbitrary separator props); the typed fields above stay authoritative.
|
|
942
1133
|
if (overrides !== undefined) {
|
|
943
1134
|
for (const key of Object.keys(overrides)) {
|
|
944
|
-
if (
|
|
1135
|
+
if (
|
|
1136
|
+
key === 'highlighted' ||
|
|
1137
|
+
key === 'leadingItem' ||
|
|
1138
|
+
key === 'trailingItem'
|
|
1139
|
+
)
|
|
1140
|
+
continue;
|
|
945
1141
|
context[key] = overrides[key];
|
|
946
1142
|
}
|
|
947
1143
|
}
|
|
@@ -958,10 +1154,13 @@ export class VirtualizedList<ItemT = unknown>
|
|
|
958
1154
|
const measure = (event: ISymbioteEvent): void => {
|
|
959
1155
|
const length = readLayoutLength(event, this.isHorizontal);
|
|
960
1156
|
if (length === undefined) return;
|
|
961
|
-
|
|
1157
|
+
const offset = readLayoutOffset(event, this.isHorizontal);
|
|
1158
|
+
dlog(
|
|
1159
|
+
`Angular VirtualizedList cell ${index} measured length=${length} offset=${offset ?? 'none'}`,
|
|
1160
|
+
);
|
|
962
1161
|
// The reducer guards a fixed getItemLayout and a repeated length; a no-op leaves state and the
|
|
963
1162
|
// view untouched (dispatch marks for check only when something changed).
|
|
964
|
-
this.dispatch({ kind: 'measure', index, length });
|
|
1163
|
+
this.dispatch({ kind: 'measure', index, length, offset });
|
|
965
1164
|
};
|
|
966
1165
|
this.cellMeasures.set(index, measure);
|
|
967
1166
|
return measure;
|
|
@@ -969,18 +1168,29 @@ export class VirtualizedList<ItemT = unknown>
|
|
|
969
1168
|
|
|
970
1169
|
// Per-cell onLayout rides the engine's structural (layout) event channel — Angular forbids an
|
|
971
1170
|
// [onLayout] property binding (NG8002). $event arrives untyped, narrowed before reaching measure.
|
|
972
|
-
handleCellLayout(
|
|
1171
|
+
handleCellLayout(
|
|
1172
|
+
measure: (event: ISymbioteEvent) => void,
|
|
1173
|
+
event: unknown,
|
|
1174
|
+
): void {
|
|
973
1175
|
if (this.isSymbioteEvent(event)) measure(event);
|
|
974
1176
|
}
|
|
975
1177
|
|
|
976
1178
|
private isSymbioteEvent(value: unknown): value is ISymbioteEvent {
|
|
977
|
-
return
|
|
1179
|
+
return (
|
|
1180
|
+
typeof value === 'object' && value !== null && 'nativeEvent' in value
|
|
1181
|
+
);
|
|
978
1182
|
}
|
|
979
1183
|
|
|
980
|
-
private mergeSeparator(
|
|
1184
|
+
private mergeSeparator(
|
|
1185
|
+
gapIndex: number,
|
|
1186
|
+
patch: Partial<ISeparatorProps<unknown>>,
|
|
1187
|
+
): void {
|
|
981
1188
|
const count = this.listState.metrics.count;
|
|
982
1189
|
if (!isSeparatorGapInRange(gapIndex, count)) return;
|
|
983
|
-
this.separatorOverrides.set(gapIndex, {
|
|
1190
|
+
this.separatorOverrides.set(gapIndex, {
|
|
1191
|
+
...this.separatorOverrides.get(gapIndex),
|
|
1192
|
+
...patch,
|
|
1193
|
+
});
|
|
984
1194
|
this.cdr.markForCheck();
|
|
985
1195
|
}
|
|
986
1196
|
|
|
@@ -996,7 +1206,10 @@ export class VirtualizedList<ItemT = unknown>
|
|
|
996
1206
|
this.mergeSeparator(index - 1, { highlighted: false });
|
|
997
1207
|
this.mergeSeparator(index, { highlighted: false });
|
|
998
1208
|
},
|
|
999
|
-
updateProps: (
|
|
1209
|
+
updateProps: (
|
|
1210
|
+
select: 'leading' | 'trailing',
|
|
1211
|
+
newProps: Record<string, unknown>,
|
|
1212
|
+
): void => {
|
|
1000
1213
|
this.mergeSeparator(select === 'leading' ? index - 1 : index, newProps);
|
|
1001
1214
|
},
|
|
1002
1215
|
};
|
|
@@ -1029,7 +1242,11 @@ export class VirtualizedList<ItemT = unknown>
|
|
|
1029
1242
|
});
|
|
1030
1243
|
}
|
|
1031
1244
|
|
|
1032
|
-
scrollToItem(params: {
|
|
1245
|
+
scrollToItem(params: {
|
|
1246
|
+
item: unknown;
|
|
1247
|
+
animated?: boolean;
|
|
1248
|
+
viewPosition?: number;
|
|
1249
|
+
}): void {
|
|
1033
1250
|
this.dispatch({
|
|
1034
1251
|
kind: 'scroll-to-item',
|
|
1035
1252
|
item: params.item,
|
|
@@ -1039,7 +1256,10 @@ export class VirtualizedList<ItemT = unknown>
|
|
|
1039
1256
|
}
|
|
1040
1257
|
|
|
1041
1258
|
scrollToEnd(params?: { animated?: boolean }): void {
|
|
1042
|
-
this.dispatch({
|
|
1259
|
+
this.dispatch({
|
|
1260
|
+
kind: 'scroll-to-end',
|
|
1261
|
+
animated: params?.animated ?? true,
|
|
1262
|
+
});
|
|
1043
1263
|
}
|
|
1044
1264
|
|
|
1045
1265
|
flashScrollIndicators(): void {
|