@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
|
@@ -23,14 +23,16 @@
|
|
|
23
23
|
//
|
|
24
24
|
// Lists have no Descriptor render fn — the cell content is the framework's own children.
|
|
25
25
|
// Cells/spacers are plain symbiote-view host elements.
|
|
26
|
-
import { CUSTOM_ELEMENTS_SCHEMA, ChangeDetectionStrategy, ChangeDetectorRef, Component, ContentChild, ElementRef, EventEmitter, Input, Output, ViewChild, inject, } from '@angular/core';
|
|
27
|
-
import { DEFAULT_END_REACHED_THRESHOLD, DEFAULT_INITIAL_NUM_TO_RENDER, DEFAULT_MAX_TO_RENDER_PER_BATCH, DEFAULT_START_REACHED_THRESHOLD, DEFAULT_UPDATE_CELLS_BATCHING_PERIOD, DEFAULT_WINDOW_SIZE, EMPTY_OFFSET, FIRST_INDEX, INVERTED_X_STYLE, INVERTED_Y_STYLE, buildListPlan, buildViewabilityPairs, createInitialListState, isSeparatorGapInRange, listEffectSignature, readLayoutLength, readScrollOffset, reduceList, resolveAccessibilityProps, resolveItemKey, } from '@symbiote-native/components';
|
|
26
|
+
import { CUSTOM_ELEMENTS_SCHEMA, ChangeDetectionStrategy, ChangeDetectorRef, Component, ContentChild, ElementRef, EventEmitter, Input, Output, ViewChild, computed, inject, signal, } from '@angular/core';
|
|
27
|
+
import { DEFAULT_END_REACHED_THRESHOLD, DEFAULT_INITIAL_NUM_TO_RENDER, DEFAULT_MAX_TO_RENDER_PER_BATCH, DEFAULT_START_REACHED_THRESHOLD, DEFAULT_UPDATE_CELLS_BATCHING_PERIOD, DEFAULT_WINDOW_SIZE, EMPTY_OFFSET, FIRST_INDEX, INVERTED_X_STYLE, INVERTED_Y_STYLE, buildListPlan, buildViewabilityPairs, computeWindow, createInitialListState, isSeparatorGapInRange, listEffectSignature, readLayoutLength, readLayoutOffset, readScrollOffset, reduceList, resolveAccessibilityProps, resolveItemKey, } from '@symbiote-native/components';
|
|
28
28
|
import { dlog, flattenStyle, } from '@symbiote-native/engine';
|
|
29
|
+
import { countAngular } from '../../diagnostics';
|
|
29
30
|
import { ScrollView } from '../scroll-view';
|
|
30
31
|
import { RefreshControl } from '../refresh-control';
|
|
31
|
-
import { anchorHostStyle, ViewHost } from '../../primitives';
|
|
32
|
+
import { anchorHostStyle, SymbioteStyleInputDirective, ViewHost, } from '../../primitives';
|
|
32
33
|
import { VListEmptyDirective, VListFooterDirective, VListHeaderDirective, VListItemDirective, VListOutletDirective, VListSeparatorDirective, } from './directives';
|
|
33
34
|
import * as i0 from "@angular/core";
|
|
35
|
+
import * as i1 from "../../primitives/shared";
|
|
34
36
|
export { VListEmptyDirective, VListFooterDirective, VListHeaderDirective, VListItemDirective, VListSeparatorDirective, } from './directives';
|
|
35
37
|
export class VirtualizedList {
|
|
36
38
|
// The list's edge/viewability/failure events as real Angular events: `(endReached)="…"`, not
|
|
@@ -134,7 +136,13 @@ export class VirtualizedList {
|
|
|
134
136
|
// --- template-bound view state, assembled in ngDoCheck (recomputeView) ---
|
|
135
137
|
itemCount = EMPTY_OFFSET;
|
|
136
138
|
windowCells = [];
|
|
139
|
+
// The nearest sticky-header cell force-mounted outside [first,last] (plan.forcedStickyCell), the
|
|
140
|
+
// Angular twin of RN's _ensureClosestStickyHeader. Rendered ahead of windowCells, right after the
|
|
141
|
+
// leading spacer, with gapSpacerStyle filling the gap to the window's own first cell — see
|
|
142
|
+
// buildListPlan's doc comment for the exact child order this assumes.
|
|
143
|
+
forcedStickyCell = null;
|
|
137
144
|
leadingSpacerStyle = null;
|
|
145
|
+
gapSpacerStyle = null;
|
|
138
146
|
trailingSpacerStyle = null;
|
|
139
147
|
cellStyle = undefined;
|
|
140
148
|
renderedStickyIndices = undefined;
|
|
@@ -155,6 +163,23 @@ export class VirtualizedList {
|
|
|
155
163
|
listState = createInitialListState();
|
|
156
164
|
renderVersion = EMPTY_OFFSET;
|
|
157
165
|
cellMeasures = new Map();
|
|
166
|
+
// A cell's context object is the ONLY thing its stamped view reads, and VListOutletDirective
|
|
167
|
+
// refreshes that view whenever the object's IDENTITY changes. recomputeView runs on every frame
|
|
168
|
+
// the window moves, so minting a fresh context per cell made a four-row slide cost a refresh of
|
|
169
|
+
// EVERY cell in the window. MEASURED on PATH B geometry (544 entries, 320px viewport, RN
|
|
170
|
+
// defaults), per fling frame: 233-cell window, 4 cells genuinely entering, 228 outlet updates —
|
|
171
|
+
// each one a copyContextFields plus a markForCheck that walks to the root. Solid, same geometry,
|
|
172
|
+
// same shared reducer, same engine output: 4. Handing back the SAME object for a cell whose item
|
|
173
|
+
// and index did not move keeps `[vListOutletContext]` reference-equal, so ngOnChanges never fires
|
|
174
|
+
// for it. The fast adapters get this by construction — Solid's <For> is keyed on the cell KEY
|
|
175
|
+
// strings precisely so "the plan's freshly-built cell objects" cannot rebuild every row
|
|
176
|
+
// (adapters/solid/src/components/virtualized-list/shared.tsx, the cellKeys memo).
|
|
177
|
+
cellContexts = new Map();
|
|
178
|
+
nextCellContexts = new Map();
|
|
179
|
+
// Everything a cell's CONTENT can depend on that is not its own item identity. RN's contract for
|
|
180
|
+
// "re-render the cells, the data object itself did not change" is extraData; without it here an
|
|
181
|
+
// extraData flip would reuse every context and repaint nothing.
|
|
182
|
+
cellContextEpoch = undefined;
|
|
158
183
|
separatorOverrides = new Map();
|
|
159
184
|
// The adapter-owned debounce (minimumViewTime) and incremental-fill timers; the reducer only
|
|
160
185
|
// hands back a delay.
|
|
@@ -179,7 +204,25 @@ export class VirtualizedList {
|
|
|
179
204
|
// onto (see anchorHostStyle's doc comment) — NOT `scrollView` above, which targets the real
|
|
180
205
|
// inner `<ScrollView>` one level down (itself its own separate anchor host).
|
|
181
206
|
elementRef = inject(ElementRef);
|
|
182
|
-
|
|
207
|
+
// Bridges the non-reactive @Input fields into the reactive graph so the computed below can
|
|
208
|
+
// memoize a bag derived from them (the same bridge ScrollView's shared.ts uses - read its comment
|
|
209
|
+
// for why signal inputs are not an option while this package's unit suite runs on JIT).
|
|
210
|
+
//
|
|
211
|
+
// ONLY safe for a bag whose every dependency is an @Input. This list's window machinery
|
|
212
|
+
// (listState, renderVersion, windowCells, the measured metrics) is driven from scroll/layout
|
|
213
|
+
// callbacks that never touch ngOnChanges, so nothing derived from it may ride this signal - it
|
|
214
|
+
// would memoize a window that stops re-computing mid-scroll. Make that state its own signal
|
|
215
|
+
// instead of widening this one.
|
|
216
|
+
inputsRevision = signal(0, /* @ts-ignore */
|
|
217
|
+
...(ngDevMode ? [{ debugName: "inputsRevision" }] : /* istanbul ignore next */ []));
|
|
218
|
+
// MEASURED: the template feeds 20 separate <ScrollView> inputs off this one bag, and Angular does
|
|
219
|
+
// not cache a getter across binding expressions - as a getter it rebuilt the object 20 times per
|
|
220
|
+
// refresh, and every consumer saw a fresh reference, so all 20 reported "changed" and wrote
|
|
221
|
+
// through to the ScrollView. A computed evaluates once and hands back the SAME object until an
|
|
222
|
+
// input actually changes. Every dependency below is an @Input, which is what makes
|
|
223
|
+
// inputsRevision a complete dependency set here.
|
|
224
|
+
foldedAccessibility = computed(() => {
|
|
225
|
+
this.inputsRevision();
|
|
183
226
|
return resolveAccessibilityProps({
|
|
184
227
|
testID: this.testID,
|
|
185
228
|
nativeID: this.nativeID,
|
|
@@ -217,7 +260,8 @@ export class VirtualizedList {
|
|
|
217
260
|
'aria-valuenow': this.ariaValueNow,
|
|
218
261
|
'aria-valuetext': this.ariaValueText,
|
|
219
262
|
});
|
|
220
|
-
}
|
|
263
|
+
}, /* @ts-ignore */
|
|
264
|
+
...(ngDevMode ? [{ debugName: "foldedAccessibility" }] : /* istanbul ignore next */ []));
|
|
221
265
|
get shouldRenderRefreshControl() {
|
|
222
266
|
return this.refreshRequested ?? this.refresh.observed;
|
|
223
267
|
}
|
|
@@ -237,7 +281,7 @@ export class VirtualizedList {
|
|
|
237
281
|
return this.maxToRenderPerBatch ?? DEFAULT_MAX_TO_RENDER_PER_BATCH;
|
|
238
282
|
}
|
|
239
283
|
get updateCellsBatchingPeriodValue() {
|
|
240
|
-
return this.updateCellsBatchingPeriod ?? DEFAULT_UPDATE_CELLS_BATCHING_PERIOD;
|
|
284
|
+
return (this.updateCellsBatchingPeriod ?? DEFAULT_UPDATE_CELLS_BATCHING_PERIOD);
|
|
241
285
|
}
|
|
242
286
|
get onEndReachedThresholdValue() {
|
|
243
287
|
return this.onEndReachedThreshold ?? DEFAULT_END_REACHED_THRESHOLD;
|
|
@@ -249,6 +293,7 @@ export class VirtualizedList {
|
|
|
249
293
|
// flow through it, which an @Output() can't carry), so this is a stable arrow field passed
|
|
250
294
|
// straight to [onScroll]. onLayout is a real @Output() now, bound via (layout)="onLayoutTick($event)".
|
|
251
295
|
onScrollTick = (event) => {
|
|
296
|
+
countAngular('scrollTicks');
|
|
252
297
|
const offset = readScrollOffset(event, this.isHorizontal);
|
|
253
298
|
if (offset === undefined)
|
|
254
299
|
return;
|
|
@@ -320,10 +365,46 @@ export class VirtualizedList {
|
|
|
320
365
|
const inputs = this.buildInputs();
|
|
321
366
|
const result = reduceList(this.listState, action, inputs);
|
|
322
367
|
this.runEffects(result.effects, inputs);
|
|
323
|
-
if (result.changed)
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
368
|
+
if (!result.changed)
|
|
369
|
+
return;
|
|
370
|
+
this.renderVersion += 1;
|
|
371
|
+
if (this.isWindowSettled(action))
|
|
372
|
+
return;
|
|
373
|
+
countAngular('listMarks');
|
|
374
|
+
this.cdr.markForCheck();
|
|
375
|
+
}
|
|
376
|
+
// The reducer reports `changed` for EVERY scroll offset - it has to, since the offset feeds
|
|
377
|
+
// end-reached distance, viewability and the batch-fill timer, all handled by runEffects. The
|
|
378
|
+
// TEMPLATE reads none of those; it reads the window. Marking on the offset repainted the whole
|
|
379
|
+
// ancestor chain 60 times a second on a sticky screen for a window that had not moved.
|
|
380
|
+
//
|
|
381
|
+
// So the question here is "would a render leave the window exactly where it is", and it is asked
|
|
382
|
+
// of the state as it is NOW. It must NOT be answered by comparing the last-rendered window with
|
|
383
|
+
// the one before it: that window was derived from the PREVIOUS scroll offset, so once two
|
|
384
|
+
// consecutive derives agree the comparison reads "settled" for every offset that ever follows -
|
|
385
|
+
// no mark, no CD, no ngDoCheck, no refresh-metrics, so the window can never move again and the
|
|
386
|
+
// list goes permanently deaf mid-scroll (device: examples/angular Benchmark sticky path B froze
|
|
387
|
+
// at exactly the window the first viewport asked for, blank below it).
|
|
388
|
+
//
|
|
389
|
+
// computeWindow re-run over the CACHED offset table answers it directly. Two scans, no
|
|
390
|
+
// allocation - buildOffsets, the O(count) allocating half, still runs once per render only. It is
|
|
391
|
+
// the same function the render will use, so this cannot drift away from the window it predicts.
|
|
392
|
+
isWindowSettled(action) {
|
|
393
|
+
// A measurement rewrites the very offset table the prediction reads, so it has no cached
|
|
394
|
+
// geometry to answer from.
|
|
395
|
+
if (action.kind === 'measure')
|
|
396
|
+
return false;
|
|
397
|
+
const m = this.listState.metrics;
|
|
398
|
+
// Mid-fill: the committed window is still climbing toward target one batch step per render, and
|
|
399
|
+
// only a render advances it. Skipping the mark here is what stalls the incremental fill.
|
|
400
|
+
if (m.first !== m.target.first || m.last !== m.target.last)
|
|
401
|
+
return false;
|
|
402
|
+
const next = computeWindow(m.count, m.offsets, m.lengths, this.listState.scrollOffset, this.listState.viewportLength, this.windowSizeValue, this.initialNumToRenderValue);
|
|
403
|
+
const isSettled = next.first === m.target.first && next.last === m.target.last;
|
|
404
|
+
if (!isSettled)
|
|
405
|
+
dlog(`Angular VirtualizedList window moves [${m.first}, ${m.last}] -> ` +
|
|
406
|
+
`[${next.first}, ${next.last}] at offset ${this.listState.scrollOffset}`);
|
|
407
|
+
return isSettled;
|
|
327
408
|
}
|
|
328
409
|
runEffects(effects, inputs) {
|
|
329
410
|
for (const effect of effects) {
|
|
@@ -335,7 +416,9 @@ export class VirtualizedList {
|
|
|
335
416
|
this.endReached.emit({ distanceFromEnd: effect.distanceFromEnd });
|
|
336
417
|
break;
|
|
337
418
|
case 'fire-start-reached':
|
|
338
|
-
this.startReached.emit({
|
|
419
|
+
this.startReached.emit({
|
|
420
|
+
distanceFromStart: effect.distanceFromStart,
|
|
421
|
+
});
|
|
339
422
|
break;
|
|
340
423
|
case 'fire-scroll-to-index-failed':
|
|
341
424
|
this.scrollToIndexFailed.emit({
|
|
@@ -380,6 +463,13 @@ export class VirtualizedList {
|
|
|
380
463
|
}
|
|
381
464
|
}
|
|
382
465
|
}
|
|
466
|
+
// foldedAccessibility reads plain @Input fields, which are NOT reactive on their own - this bump
|
|
467
|
+
// is what tells that computed an input changed. It must stay in ngOnChanges: that is the single
|
|
468
|
+
// moment Angular has finished writing every changed input for this pass, and it runs before
|
|
469
|
+
// ngDoCheck below, so the recompute already sees the current values.
|
|
470
|
+
ngOnChanges() {
|
|
471
|
+
this.inputsRevision.update(revision => revision + 1);
|
|
472
|
+
}
|
|
383
473
|
// Once-per-CD recompute, BEFORE the template bindings are read (so the freshly computed
|
|
384
474
|
// template-bound fields render this pass — computing them in ngAfterContentChecked instead would
|
|
385
475
|
// trip ExpressionChangedAfterItHasBeenChecked). The dedup guard runs refresh-metrics (which owns
|
|
@@ -387,6 +477,7 @@ export class VirtualizedList {
|
|
|
387
477
|
// grows exactly one step per meaningful CD. The projected templates (ContentChild) are resolved
|
|
388
478
|
// from the second CD on; the first paint (count/viewport still 0) corrects on the layout-driven CD.
|
|
389
479
|
ngDoCheck() {
|
|
480
|
+
countAngular('listChecks');
|
|
390
481
|
const recomputeInputs = [
|
|
391
482
|
this.data,
|
|
392
483
|
this.extraData,
|
|
@@ -400,6 +491,10 @@ export class VirtualizedList {
|
|
|
400
491
|
this.stickyHeaderIndices,
|
|
401
492
|
this.maintainVisibleContentPosition,
|
|
402
493
|
this.style,
|
|
494
|
+
// `recomputeView` folds this into the committed style, but it arrives through
|
|
495
|
+
// addClass/removeClass, never as an @Input - so without it here no entry moves on a class
|
|
496
|
+
// toggle and the gate skips the recompute forever. Reference-stable, so the dedup holds.
|
|
497
|
+
anchorHostStyle(this.elementRef),
|
|
403
498
|
this.contentContainerStyle,
|
|
404
499
|
// Folds scroll / layout / measure / batch-tick: dispatch bumps renderVersion on any change.
|
|
405
500
|
this.renderVersion,
|
|
@@ -416,6 +511,7 @@ export class VirtualizedList {
|
|
|
416
511
|
return;
|
|
417
512
|
}
|
|
418
513
|
// The single derive-per-CD: recompute the window off the current state before the view reads it.
|
|
514
|
+
countAngular('listRecomputes');
|
|
419
515
|
reduceList(this.listState, { kind: 'refresh-metrics' }, this.buildInputs());
|
|
420
516
|
this.recomputeView();
|
|
421
517
|
}
|
|
@@ -439,7 +535,9 @@ export class VirtualizedList {
|
|
|
439
535
|
this.itemCount = m.count;
|
|
440
536
|
const hasHeader = this.headerDir !== undefined;
|
|
441
537
|
const hasSeparators = this.separatorDir !== undefined;
|
|
442
|
-
const stickySet = this.stickyHeaderIndices !== undefined
|
|
538
|
+
const stickySet = this.stickyHeaderIndices !== undefined
|
|
539
|
+
? new Set(this.stickyHeaderIndices)
|
|
540
|
+
: undefined;
|
|
443
541
|
this.resolvedContentContainerStyle = this.isHorizontal
|
|
444
542
|
? [this.contentContainerStyle, { width: m.total }]
|
|
445
543
|
: this.contentContainerStyle;
|
|
@@ -459,11 +557,31 @@ export class VirtualizedList {
|
|
|
459
557
|
? undefined
|
|
460
558
|
: {
|
|
461
559
|
...this.maintainVisibleContentPosition,
|
|
462
|
-
minIndexForVisible: this.maintainVisibleContentPosition.minIndexForVisible +
|
|
560
|
+
minIndexForVisible: this.maintainVisibleContentPosition.minIndexForVisible +
|
|
561
|
+
(hasHeader ? 1 : 0),
|
|
463
562
|
};
|
|
563
|
+
// A change in anything a cell's content reads other than its own item invalidates every cached
|
|
564
|
+
// context: identity reuse is only sound while the reused object still describes the cell.
|
|
565
|
+
const epoch = [
|
|
566
|
+
this.data,
|
|
567
|
+
this.extraData,
|
|
568
|
+
this.getItem,
|
|
569
|
+
this.keyExtractor,
|
|
570
|
+
];
|
|
571
|
+
const previousEpoch = this.cellContextEpoch;
|
|
572
|
+
this.cellContextEpoch = epoch;
|
|
573
|
+
if (previousEpoch === undefined ||
|
|
574
|
+
previousEpoch.length !== epoch.length ||
|
|
575
|
+
!previousEpoch.every((value, index) => value === epoch[index])) {
|
|
576
|
+
this.cellContexts.clear();
|
|
577
|
+
}
|
|
464
578
|
if (m.count === FIRST_INDEX) {
|
|
579
|
+
this.cellContexts.clear();
|
|
580
|
+
this.nextCellContexts.clear();
|
|
465
581
|
this.windowCells = [];
|
|
582
|
+
this.forcedStickyCell = null;
|
|
466
583
|
this.leadingSpacerStyle = null;
|
|
584
|
+
this.gapSpacerStyle = null;
|
|
467
585
|
this.trailingSpacerStyle = null;
|
|
468
586
|
this.renderedStickyIndices = undefined;
|
|
469
587
|
dlog(`Angular VirtualizedList empty (viewport=${this.listState.viewportLength})`);
|
|
@@ -479,40 +597,78 @@ export class VirtualizedList {
|
|
|
479
597
|
keyFor: this.keyFor,
|
|
480
598
|
stickyIndices: stickySet,
|
|
481
599
|
hasHeader,
|
|
482
|
-
hasSeparators,
|
|
483
600
|
});
|
|
484
601
|
this.leadingSpacerStyle =
|
|
485
|
-
plan.leadingExtent > EMPTY_OFFSET
|
|
602
|
+
plan.leadingExtent > EMPTY_OFFSET
|
|
603
|
+
? this.spacerStyle(plan.leadingExtent)
|
|
604
|
+
: null;
|
|
605
|
+
this.gapSpacerStyle =
|
|
606
|
+
plan.gapExtent > EMPTY_OFFSET ? this.spacerStyle(plan.gapExtent) : null;
|
|
486
607
|
this.trailingSpacerStyle =
|
|
487
|
-
plan.trailingExtent > EMPTY_OFFSET
|
|
608
|
+
plan.trailingExtent > EMPTY_OFFSET
|
|
609
|
+
? this.spacerStyle(plan.trailingExtent)
|
|
610
|
+
: null;
|
|
488
611
|
this.renderedStickyIndices =
|
|
489
612
|
stickySet !== undefined && plan.stickyChildPositions.length > 0
|
|
490
613
|
? plan.stickyChildPositions
|
|
491
614
|
: undefined;
|
|
615
|
+
dlog(`STICKY[list] stickySet=${stickySet === undefined ? 'undefined' : JSON.stringify([...stickySet])} ` +
|
|
616
|
+
`childPositions=${JSON.stringify(plan.stickyChildPositions)} ` +
|
|
617
|
+
`forcedStickyCell=${plan.forcedStickyCell === undefined ? 'none' : plan.forcedStickyCell.index} ` +
|
|
618
|
+
`window=[${m.first},${m.last}] hasHeader=${this.headerDir !== undefined}`);
|
|
619
|
+
this.forcedStickyCell =
|
|
620
|
+
plan.forcedStickyCell !== undefined
|
|
621
|
+
? this.buildWindowCell(plan.forcedStickyCell.index, plan.forcedStickyCell.key, false)
|
|
622
|
+
: null;
|
|
492
623
|
const cells = [];
|
|
493
624
|
for (const planned of plan.cells) {
|
|
494
|
-
|
|
495
|
-
const
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
if (hasSeparators && planned.index < m.last) {
|
|
502
|
-
separatorContext = this.buildSeparatorContext(planned.index, item);
|
|
503
|
-
}
|
|
504
|
-
cells.push({
|
|
505
|
-
key: planned.key,
|
|
506
|
-
index: planned.index,
|
|
507
|
-
context,
|
|
508
|
-
measure: this.cellMeasure(planned.index),
|
|
509
|
-
separatorContext,
|
|
510
|
-
});
|
|
625
|
+
// RN gates the separator on the last index of the DATA, not of the WINDOW
|
|
626
|
+
// (VirtualizedList.js:793 `const end = getItemCount(data) - 1`), and now that the separator
|
|
627
|
+
// lives INSIDE the measuring wrapper that distinction is load-bearing: gating on the window
|
|
628
|
+
// would make a cell's own measured height change as the window slides past it. Device-measured
|
|
629
|
+
// 2026-08-19 as a run of cells all shifting by exactly the divider's 1px.
|
|
630
|
+
const includeSeparator = hasSeparators && planned.index < m.count - 1;
|
|
631
|
+
cells.push(this.buildWindowCell(planned.index, planned.key, includeSeparator));
|
|
511
632
|
}
|
|
512
633
|
this.windowCells = cells;
|
|
634
|
+
// Swap, don't prune: a cell that left the window is simply absent from the pass that just ran,
|
|
635
|
+
// so the old map is the garbage and the new one is exactly the live set.
|
|
636
|
+
this.cellContexts = this.nextCellContexts;
|
|
637
|
+
this.nextCellContexts = new Map();
|
|
513
638
|
dlog(`Angular VirtualizedList window [${m.first}, ${m.last}] of ${m.count} ` +
|
|
514
639
|
`(offset=${this.listState.scrollOffset}, viewport=${this.listState.viewportLength}, rendered=${cells.length})`);
|
|
515
640
|
}
|
|
641
|
+
// Assembles one cell (window or forced-sticky) — the context, measure closure, and optional
|
|
642
|
+
// separator context — so both plan.cells and plan.forcedStickyCell build identically.
|
|
643
|
+
buildWindowCell(index, key, includeSeparator) {
|
|
644
|
+
const item = this.getItem(this.data, index);
|
|
645
|
+
return {
|
|
646
|
+
key,
|
|
647
|
+
index,
|
|
648
|
+
context: this.contextFor(key, index, item),
|
|
649
|
+
measure: this.cellMeasure(index),
|
|
650
|
+
separatorContext: includeSeparator
|
|
651
|
+
? this.buildSeparatorContext(index, item)
|
|
652
|
+
: undefined,
|
|
653
|
+
};
|
|
654
|
+
}
|
|
655
|
+
// The context for one cell, reused by identity when the cell did not move. See cellContexts.
|
|
656
|
+
contextFor(key, index, item) {
|
|
657
|
+
const cached = this.cellContexts.get(key);
|
|
658
|
+
if (cached !== undefined &&
|
|
659
|
+
cached.$implicit === item &&
|
|
660
|
+
cached.index === index) {
|
|
661
|
+
this.nextCellContexts.set(key, cached);
|
|
662
|
+
return cached;
|
|
663
|
+
}
|
|
664
|
+
const context = {
|
|
665
|
+
$implicit: item,
|
|
666
|
+
index,
|
|
667
|
+
separators: this.makeSeparators(index),
|
|
668
|
+
};
|
|
669
|
+
this.nextCellContexts.set(key, context);
|
|
670
|
+
return context;
|
|
671
|
+
}
|
|
516
672
|
buildSeparatorContext(index, item) {
|
|
517
673
|
const overrides = this.separatorOverrides.get(index);
|
|
518
674
|
const highlighted = overrides?.highlighted ?? false;
|
|
@@ -526,7 +682,9 @@ export class VirtualizedList {
|
|
|
526
682
|
// arbitrary separator props); the typed fields above stay authoritative.
|
|
527
683
|
if (overrides !== undefined) {
|
|
528
684
|
for (const key of Object.keys(overrides)) {
|
|
529
|
-
if (key === 'highlighted' ||
|
|
685
|
+
if (key === 'highlighted' ||
|
|
686
|
+
key === 'leadingItem' ||
|
|
687
|
+
key === 'trailingItem')
|
|
530
688
|
continue;
|
|
531
689
|
context[key] = overrides[key];
|
|
532
690
|
}
|
|
@@ -544,10 +702,11 @@ export class VirtualizedList {
|
|
|
544
702
|
const length = readLayoutLength(event, this.isHorizontal);
|
|
545
703
|
if (length === undefined)
|
|
546
704
|
return;
|
|
547
|
-
|
|
705
|
+
const offset = readLayoutOffset(event, this.isHorizontal);
|
|
706
|
+
dlog(`Angular VirtualizedList cell ${index} measured length=${length} offset=${offset ?? 'none'}`);
|
|
548
707
|
// The reducer guards a fixed getItemLayout and a repeated length; a no-op leaves state and the
|
|
549
708
|
// view untouched (dispatch marks for check only when something changed).
|
|
550
|
-
this.dispatch({ kind: 'measure', index, length });
|
|
709
|
+
this.dispatch({ kind: 'measure', index, length, offset });
|
|
551
710
|
};
|
|
552
711
|
this.cellMeasures.set(index, measure);
|
|
553
712
|
return measure;
|
|
@@ -559,13 +718,16 @@ export class VirtualizedList {
|
|
|
559
718
|
measure(event);
|
|
560
719
|
}
|
|
561
720
|
isSymbioteEvent(value) {
|
|
562
|
-
return typeof value === 'object' && value !== null && 'nativeEvent' in value;
|
|
721
|
+
return (typeof value === 'object' && value !== null && 'nativeEvent' in value);
|
|
563
722
|
}
|
|
564
723
|
mergeSeparator(gapIndex, patch) {
|
|
565
724
|
const count = this.listState.metrics.count;
|
|
566
725
|
if (!isSeparatorGapInRange(gapIndex, count))
|
|
567
726
|
return;
|
|
568
|
-
this.separatorOverrides.set(gapIndex, {
|
|
727
|
+
this.separatorOverrides.set(gapIndex, {
|
|
728
|
+
...this.separatorOverrides.get(gapIndex),
|
|
729
|
+
...patch,
|
|
730
|
+
});
|
|
569
731
|
this.cdr.markForCheck();
|
|
570
732
|
}
|
|
571
733
|
makeSeparators(index) {
|
|
@@ -613,7 +775,10 @@ export class VirtualizedList {
|
|
|
613
775
|
});
|
|
614
776
|
}
|
|
615
777
|
scrollToEnd(params) {
|
|
616
|
-
this.dispatch({
|
|
778
|
+
this.dispatch({
|
|
779
|
+
kind: 'scroll-to-end',
|
|
780
|
+
animated: params?.animated ?? true,
|
|
781
|
+
});
|
|
617
782
|
}
|
|
618
783
|
flashScrollIndicators() {
|
|
619
784
|
this.scrollView?.flashScrollIndicators();
|
|
@@ -647,36 +812,46 @@ export class VirtualizedList {
|
|
|
647
812
|
this.commandedOffset = target;
|
|
648
813
|
this.cdr.markForCheck();
|
|
649
814
|
}
|
|
650
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.
|
|
651
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.
|
|
815
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: VirtualizedList, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
816
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.8", type: VirtualizedList, isStandalone: true, selector: "VirtualizedList", inputs: { data: "data", getItem: "getItem", getItemCount: "getItemCount", keyExtractor: "keyExtractor", getItemLayout: "getItemLayout", horizontal: "horizontal", inverted: "inverted", extraData: "extraData", onEndReachedThreshold: "onEndReachedThreshold", onStartReachedThreshold: "onStartReachedThreshold", refreshing: "refreshing", progressViewOffset: "progressViewOffset", refreshRequested: "refreshRequested", viewabilityConfig: "viewabilityConfig", viewabilityConfigCallbackPairs: "viewabilityConfigCallbackPairs", initialNumToRender: "initialNumToRender", initialScrollIndex: "initialScrollIndex", maxToRenderPerBatch: "maxToRenderPerBatch", updateCellsBatchingPeriod: "updateCellsBatchingPeriod", windowSize: "windowSize", stickyHeaderIndices: "stickyHeaderIndices", maintainVisibleContentPosition: "maintainVisibleContentPosition", onScroll: "onScroll", onScrollBeginDrag: "onScrollBeginDrag", onScrollEndDrag: "onScrollEndDrag", onMomentumScrollBegin: "onMomentumScrollBegin", onMomentumScrollEnd: "onMomentumScrollEnd", scrollEventThrottle: "scrollEventThrottle", keyboardShouldPersistTaps: "keyboardShouldPersistTaps", keyboardDismissMode: "keyboardDismissMode", style: "style", contentContainerStyle: "contentContainerStyle", testID: "testID", nativeID: "nativeID", accessible: "accessible", accessibilityLabel: "accessibilityLabel", accessibilityHint: "accessibilityHint", accessibilityRole: "accessibilityRole", accessibilityState: "accessibilityState", accessibilityValue: "accessibilityValue", accessibilityActions: "accessibilityActions", accessibilityLabelledBy: "accessibilityLabelledBy", importantForAccessibility: "importantForAccessibility", accessibilityLiveRegion: "accessibilityLiveRegion", screenReaderFocusable: "screenReaderFocusable", accessibilityViewIsModal: "accessibilityViewIsModal", accessibilityElementsHidden: "accessibilityElementsHidden", accessibilityIgnoresInvertColors: "accessibilityIgnoresInvertColors", accessibilityLanguage: "accessibilityLanguage", accessibilityRespondsToUserInteraction: "accessibilityRespondsToUserInteraction", accessibilityShowsLargeContentViewer: "accessibilityShowsLargeContentViewer", accessibilityLargeContentTitle: "accessibilityLargeContentTitle", role: "role", ariaLabel: "ariaLabel", ariaLabelledBy: "ariaLabelledBy", ariaLive: "ariaLive", ariaHidden: "ariaHidden", ariaBusy: "ariaBusy", ariaChecked: "ariaChecked", ariaDisabled: "ariaDisabled", ariaExpanded: "ariaExpanded", ariaSelected: "ariaSelected", ariaModal: "ariaModal", ariaValueMax: "ariaValueMax", ariaValueMin: "ariaValueMin", ariaValueNow: "ariaValueNow", ariaValueText: "ariaValueText" }, outputs: { endReached: "endReached", startReached: "startReached", refresh: "refresh", viewableItemsChanged: "viewableItemsChanged", scrollToIndexFailed: "scrollToIndexFailed", accessibilityAction: "accessibilityAction", accessibilityTap: "accessibilityTap", magicTap: "magicTap", accessibilityEscape: "accessibilityEscape" }, queries: [{ propertyName: "itemDir", first: true, predicate: VListItemDirective, descendants: true }, { propertyName: "headerDir", first: true, predicate: VListHeaderDirective, descendants: true }, { propertyName: "footerDir", first: true, predicate: VListFooterDirective, descendants: true }, { propertyName: "emptyDir", first: true, predicate: VListEmptyDirective, descendants: true }, { propertyName: "separatorDir", first: true, predicate: VListSeparatorDirective, descendants: true }], viewQueries: [{ propertyName: "scrollView", first: true, predicate: ScrollView, descendants: true }], usesOnChanges: true, hostDirectives: [{ directive: i1.SymbioteStyleInputDirective, inputs: ["style", "style"] }], ngImport: i0, template: `
|
|
652
817
|
<ScrollView
|
|
653
818
|
[horizontal]="isHorizontal"
|
|
654
819
|
[style]="resolvedStyle"
|
|
655
820
|
[contentContainerStyle]="resolvedContentContainerStyle"
|
|
656
|
-
[testID]="foldedAccessibility.testID"
|
|
657
|
-
[nativeID]="foldedAccessibility.nativeID"
|
|
658
|
-
[accessible]="foldedAccessibility.accessible"
|
|
659
|
-
[accessibilityLabel]="foldedAccessibility.accessibilityLabel"
|
|
660
|
-
[accessibilityHint]="foldedAccessibility.accessibilityHint"
|
|
661
|
-
[accessibilityRole]="foldedAccessibility.accessibilityRole"
|
|
662
|
-
[accessibilityState]="foldedAccessibility.accessibilityState"
|
|
663
|
-
[accessibilityValue]="foldedAccessibility.accessibilityValue"
|
|
664
|
-
[accessibilityActions]="foldedAccessibility.accessibilityActions"
|
|
665
|
-
[accessibilityLabelledBy]="foldedAccessibility.accessibilityLabelledBy"
|
|
666
|
-
[importantForAccessibility]="
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
[
|
|
670
|
-
[
|
|
671
|
-
[
|
|
672
|
-
|
|
821
|
+
[testID]="foldedAccessibility().testID"
|
|
822
|
+
[nativeID]="foldedAccessibility().nativeID"
|
|
823
|
+
[accessible]="foldedAccessibility().accessible"
|
|
824
|
+
[accessibilityLabel]="foldedAccessibility().accessibilityLabel"
|
|
825
|
+
[accessibilityHint]="foldedAccessibility().accessibilityHint"
|
|
826
|
+
[accessibilityRole]="foldedAccessibility().accessibilityRole"
|
|
827
|
+
[accessibilityState]="foldedAccessibility().accessibilityState"
|
|
828
|
+
[accessibilityValue]="foldedAccessibility().accessibilityValue"
|
|
829
|
+
[accessibilityActions]="foldedAccessibility().accessibilityActions"
|
|
830
|
+
[accessibilityLabelledBy]="foldedAccessibility().accessibilityLabelledBy"
|
|
831
|
+
[importantForAccessibility]="
|
|
832
|
+
foldedAccessibility().importantForAccessibility
|
|
833
|
+
"
|
|
834
|
+
[accessibilityLiveRegion]="foldedAccessibility().accessibilityLiveRegion"
|
|
835
|
+
[screenReaderFocusable]="foldedAccessibility().screenReaderFocusable"
|
|
836
|
+
[accessibilityViewIsModal]="
|
|
837
|
+
foldedAccessibility().accessibilityViewIsModal
|
|
838
|
+
"
|
|
839
|
+
[accessibilityElementsHidden]="
|
|
840
|
+
foldedAccessibility().accessibilityElementsHidden
|
|
841
|
+
"
|
|
842
|
+
[accessibilityIgnoresInvertColors]="
|
|
843
|
+
foldedAccessibility().accessibilityIgnoresInvertColors
|
|
844
|
+
"
|
|
845
|
+
[accessibilityLanguage]="foldedAccessibility().accessibilityLanguage"
|
|
673
846
|
[accessibilityRespondsToUserInteraction]="
|
|
674
|
-
foldedAccessibility.accessibilityRespondsToUserInteraction
|
|
847
|
+
foldedAccessibility().accessibilityRespondsToUserInteraction
|
|
675
848
|
"
|
|
676
849
|
[accessibilityShowsLargeContentViewer]="
|
|
677
|
-
foldedAccessibility.accessibilityShowsLargeContentViewer
|
|
850
|
+
foldedAccessibility().accessibilityShowsLargeContentViewer
|
|
851
|
+
"
|
|
852
|
+
[accessibilityLargeContentTitle]="
|
|
853
|
+
foldedAccessibility().accessibilityLargeContentTitle
|
|
678
854
|
"
|
|
679
|
-
[accessibilityLargeContentTitle]="foldedAccessibility.accessibilityLargeContentTitle"
|
|
680
855
|
(accessibilityAction)="accessibilityActionTick($event)"
|
|
681
856
|
(accessibilityTap)="accessibilityTapTick($event)"
|
|
682
857
|
(magicTap)="magicTapTick($event)"
|
|
@@ -718,21 +893,43 @@ export class VirtualizedList {
|
|
|
718
893
|
@if (leadingSpacerStyle !== null) {
|
|
719
894
|
<symbiote-view [style]="leadingSpacerStyle"></symbiote-view>
|
|
720
895
|
}
|
|
896
|
+
@if (forcedStickyCell !== null) {
|
|
897
|
+
<symbiote-view
|
|
898
|
+
(layout)="handleCellLayout(forcedStickyCell.measure, $event)"
|
|
899
|
+
[style]="cellStyle"
|
|
900
|
+
>
|
|
901
|
+
<ng-container
|
|
902
|
+
[vListOutlet]="itemDir?.templateRef"
|
|
903
|
+
[vListOutletContext]="forcedStickyCell.context"
|
|
904
|
+
></ng-container>
|
|
905
|
+
</symbiote-view>
|
|
906
|
+
}
|
|
907
|
+
@if (gapSpacerStyle !== null) {
|
|
908
|
+
<symbiote-view [style]="gapSpacerStyle"></symbiote-view>
|
|
909
|
+
}
|
|
910
|
+
<!-- The separator sits INSIDE the measuring view, as RN's own cell renderer places it
|
|
911
|
+
(VirtualizedListCellRenderer.js:218-221). As a sibling it is an extra flex child, so
|
|
912
|
+
the chrome between two cells is gap + separator + gap while a spacer collapsing that
|
|
913
|
+
region replaces it with a single gap — every cell below the leading spacer then lands
|
|
914
|
+
short by (separator + gap) and the content jumps by that much whenever the window's
|
|
915
|
+
first index moves. Measured at exactly 17px on device 2026-08-19; see
|
|
916
|
+
.claude/rules/list-geometry-feedback-loop.md. -->
|
|
721
917
|
@for (cell of windowCells; track cell.key) {
|
|
722
|
-
<symbiote-view
|
|
918
|
+
<symbiote-view
|
|
919
|
+
(layout)="handleCellLayout(cell.measure, $event)"
|
|
920
|
+
[style]="cellStyle"
|
|
921
|
+
>
|
|
723
922
|
<ng-container
|
|
724
923
|
[vListOutlet]="itemDir?.templateRef"
|
|
725
924
|
[vListOutletContext]="cell.context"
|
|
726
925
|
></ng-container>
|
|
727
|
-
|
|
728
|
-
@if (cell.separatorContext !== undefined) {
|
|
729
|
-
<symbiote-view>
|
|
926
|
+
@if (cell.separatorContext !== undefined) {
|
|
730
927
|
<ng-container
|
|
731
928
|
[vListOutlet]="separatorDir?.templateRef"
|
|
732
929
|
[vListOutletContext]="cell.separatorContext"
|
|
733
930
|
></ng-container>
|
|
734
|
-
|
|
735
|
-
|
|
931
|
+
}
|
|
932
|
+
</symbiote-view>
|
|
736
933
|
}
|
|
737
934
|
@if (trailingSpacerStyle !== null) {
|
|
738
935
|
<symbiote-view [style]="trailingSpacerStyle"></symbiote-view>
|
|
@@ -747,11 +944,14 @@ export class VirtualizedList {
|
|
|
747
944
|
</ScrollView>
|
|
748
945
|
`, isInline: true, dependencies: [{ kind: "component", type: ScrollView, selector: "ScrollView", inputs: ["style", "contentContainerStyle", "horizontal", "scrollEnabled", "showsVerticalScrollIndicator", "showsHorizontalScrollIndicator", "pagingEnabled", "bounces", "decelerationRate", "scrollEventThrottle", "contentInset", "contentOffset", "removeClippedSubviews", "snapToInterval", "snapToOffsets", "snapToAlignment", "snapToStart", "snapToEnd", "disableIntervalMomentum", "stickyHeaderIndices", "invertStickyHeaders", "StickyHeaderComponent", "keyboardDismissMode", "keyboardShouldPersistTaps", "maintainVisibleContentPosition", "alwaysBounceHorizontal", "alwaysBounceVertical", "centerContent", "scrollIndicatorInsets", "indicatorStyle", "directionalLockEnabled", "automaticallyAdjustKeyboardInsets", "contentInsetAdjustmentBehavior", "minimumZoomScale", "maximumZoomScale", "zoomScale", "bouncesZoom", "pinchGestureEnabled", "nestedScrollEnabled", "overScrollMode", "fadingEdgeLength", "persistentScrollbar", "endFillColor", "onScroll", "onScrollBeginDrag", "onScrollEndDrag", "onMomentumScrollBegin", "onMomentumScrollEnd", "testID", "nativeID", "accessible", "accessibilityLabel", "accessibilityHint", "accessibilityRole", "accessibilityState", "accessibilityValue", "accessibilityActions", "accessibilityLabelledBy", "importantForAccessibility", "accessibilityLiveRegion", "screenReaderFocusable", "accessibilityViewIsModal", "accessibilityElementsHidden", "accessibilityIgnoresInvertColors", "accessibilityLanguage", "accessibilityRespondsToUserInteraction", "accessibilityShowsLargeContentViewer", "accessibilityLargeContentTitle", "role", "aria-label", "aria-labelledby", "aria-live", "aria-hidden", "aria-busy", "aria-checked", "aria-disabled", "aria-expanded", "aria-selected", "aria-modal", "aria-valuemax", "aria-valuemin", "aria-valuenow", "aria-valuetext"] }, { kind: "component", type: RefreshControl, selector: "RefreshControl", inputs: ["refreshing", "tintColor", "title", "titleColor", "progressViewOffset", "colors", "progressBackgroundColor", "size", "enabled", "style", "testID", "nativeID", "accessible", "accessibilityLabel", "accessibilityHint", "accessibilityRole", "accessibilityState", "accessibilityValue", "accessibilityActions", "accessibilityLabelledBy", "importantForAccessibility", "accessibilityLiveRegion", "screenReaderFocusable", "accessibilityViewIsModal", "accessibilityElementsHidden", "accessibilityIgnoresInvertColors", "accessibilityLanguage", "accessibilityRespondsToUserInteraction", "accessibilityShowsLargeContentViewer", "accessibilityLargeContentTitle", "role", "aria-label", "aria-labelledby", "aria-live", "aria-hidden", "aria-busy", "aria-checked", "aria-disabled", "aria-expanded", "aria-selected", "aria-modal", "aria-valuemax", "aria-valuemin", "aria-valuenow", "aria-valuetext"], outputs: ["refresh", "accessibilityAction", "accessibilityTap", "magicTap", "accessibilityEscape"] }, { kind: "directive", type: VListOutletDirective, selector: "[vListOutlet]", inputs: ["vListOutlet", "vListOutletContext"] }, { kind: "component", type: ViewHost, selector: "symbiote-view, View" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
749
946
|
}
|
|
750
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.
|
|
947
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: VirtualizedList, decorators: [{
|
|
751
948
|
type: Component,
|
|
752
949
|
args: [{
|
|
753
950
|
selector: 'VirtualizedList',
|
|
754
951
|
standalone: true,
|
|
952
|
+
hostDirectives: [
|
|
953
|
+
{ directive: SymbioteStyleInputDirective, inputs: ['style'] },
|
|
954
|
+
],
|
|
755
955
|
schemas: [CUSTOM_ELEMENTS_SCHEMA],
|
|
756
956
|
imports: [ScrollView, RefreshControl, VListOutletDirective, ViewHost],
|
|
757
957
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
@@ -760,30 +960,40 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImpor
|
|
|
760
960
|
[horizontal]="isHorizontal"
|
|
761
961
|
[style]="resolvedStyle"
|
|
762
962
|
[contentContainerStyle]="resolvedContentContainerStyle"
|
|
763
|
-
[testID]="foldedAccessibility.testID"
|
|
764
|
-
[nativeID]="foldedAccessibility.nativeID"
|
|
765
|
-
[accessible]="foldedAccessibility.accessible"
|
|
766
|
-
[accessibilityLabel]="foldedAccessibility.accessibilityLabel"
|
|
767
|
-
[accessibilityHint]="foldedAccessibility.accessibilityHint"
|
|
768
|
-
[accessibilityRole]="foldedAccessibility.accessibilityRole"
|
|
769
|
-
[accessibilityState]="foldedAccessibility.accessibilityState"
|
|
770
|
-
[accessibilityValue]="foldedAccessibility.accessibilityValue"
|
|
771
|
-
[accessibilityActions]="foldedAccessibility.accessibilityActions"
|
|
772
|
-
[accessibilityLabelledBy]="foldedAccessibility.accessibilityLabelledBy"
|
|
773
|
-
[importantForAccessibility]="
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
[
|
|
777
|
-
[
|
|
778
|
-
[
|
|
779
|
-
|
|
963
|
+
[testID]="foldedAccessibility().testID"
|
|
964
|
+
[nativeID]="foldedAccessibility().nativeID"
|
|
965
|
+
[accessible]="foldedAccessibility().accessible"
|
|
966
|
+
[accessibilityLabel]="foldedAccessibility().accessibilityLabel"
|
|
967
|
+
[accessibilityHint]="foldedAccessibility().accessibilityHint"
|
|
968
|
+
[accessibilityRole]="foldedAccessibility().accessibilityRole"
|
|
969
|
+
[accessibilityState]="foldedAccessibility().accessibilityState"
|
|
970
|
+
[accessibilityValue]="foldedAccessibility().accessibilityValue"
|
|
971
|
+
[accessibilityActions]="foldedAccessibility().accessibilityActions"
|
|
972
|
+
[accessibilityLabelledBy]="foldedAccessibility().accessibilityLabelledBy"
|
|
973
|
+
[importantForAccessibility]="
|
|
974
|
+
foldedAccessibility().importantForAccessibility
|
|
975
|
+
"
|
|
976
|
+
[accessibilityLiveRegion]="foldedAccessibility().accessibilityLiveRegion"
|
|
977
|
+
[screenReaderFocusable]="foldedAccessibility().screenReaderFocusable"
|
|
978
|
+
[accessibilityViewIsModal]="
|
|
979
|
+
foldedAccessibility().accessibilityViewIsModal
|
|
980
|
+
"
|
|
981
|
+
[accessibilityElementsHidden]="
|
|
982
|
+
foldedAccessibility().accessibilityElementsHidden
|
|
983
|
+
"
|
|
984
|
+
[accessibilityIgnoresInvertColors]="
|
|
985
|
+
foldedAccessibility().accessibilityIgnoresInvertColors
|
|
986
|
+
"
|
|
987
|
+
[accessibilityLanguage]="foldedAccessibility().accessibilityLanguage"
|
|
780
988
|
[accessibilityRespondsToUserInteraction]="
|
|
781
|
-
foldedAccessibility.accessibilityRespondsToUserInteraction
|
|
989
|
+
foldedAccessibility().accessibilityRespondsToUserInteraction
|
|
782
990
|
"
|
|
783
991
|
[accessibilityShowsLargeContentViewer]="
|
|
784
|
-
foldedAccessibility.accessibilityShowsLargeContentViewer
|
|
992
|
+
foldedAccessibility().accessibilityShowsLargeContentViewer
|
|
993
|
+
"
|
|
994
|
+
[accessibilityLargeContentTitle]="
|
|
995
|
+
foldedAccessibility().accessibilityLargeContentTitle
|
|
785
996
|
"
|
|
786
|
-
[accessibilityLargeContentTitle]="foldedAccessibility.accessibilityLargeContentTitle"
|
|
787
997
|
(accessibilityAction)="accessibilityActionTick($event)"
|
|
788
998
|
(accessibilityTap)="accessibilityTapTick($event)"
|
|
789
999
|
(magicTap)="magicTapTick($event)"
|
|
@@ -825,21 +1035,43 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImpor
|
|
|
825
1035
|
@if (leadingSpacerStyle !== null) {
|
|
826
1036
|
<symbiote-view [style]="leadingSpacerStyle"></symbiote-view>
|
|
827
1037
|
}
|
|
1038
|
+
@if (forcedStickyCell !== null) {
|
|
1039
|
+
<symbiote-view
|
|
1040
|
+
(layout)="handleCellLayout(forcedStickyCell.measure, $event)"
|
|
1041
|
+
[style]="cellStyle"
|
|
1042
|
+
>
|
|
1043
|
+
<ng-container
|
|
1044
|
+
[vListOutlet]="itemDir?.templateRef"
|
|
1045
|
+
[vListOutletContext]="forcedStickyCell.context"
|
|
1046
|
+
></ng-container>
|
|
1047
|
+
</symbiote-view>
|
|
1048
|
+
}
|
|
1049
|
+
@if (gapSpacerStyle !== null) {
|
|
1050
|
+
<symbiote-view [style]="gapSpacerStyle"></symbiote-view>
|
|
1051
|
+
}
|
|
1052
|
+
<!-- The separator sits INSIDE the measuring view, as RN's own cell renderer places it
|
|
1053
|
+
(VirtualizedListCellRenderer.js:218-221). As a sibling it is an extra flex child, so
|
|
1054
|
+
the chrome between two cells is gap + separator + gap while a spacer collapsing that
|
|
1055
|
+
region replaces it with a single gap — every cell below the leading spacer then lands
|
|
1056
|
+
short by (separator + gap) and the content jumps by that much whenever the window's
|
|
1057
|
+
first index moves. Measured at exactly 17px on device 2026-08-19; see
|
|
1058
|
+
.claude/rules/list-geometry-feedback-loop.md. -->
|
|
828
1059
|
@for (cell of windowCells; track cell.key) {
|
|
829
|
-
<symbiote-view
|
|
1060
|
+
<symbiote-view
|
|
1061
|
+
(layout)="handleCellLayout(cell.measure, $event)"
|
|
1062
|
+
[style]="cellStyle"
|
|
1063
|
+
>
|
|
830
1064
|
<ng-container
|
|
831
1065
|
[vListOutlet]="itemDir?.templateRef"
|
|
832
1066
|
[vListOutletContext]="cell.context"
|
|
833
1067
|
></ng-container>
|
|
834
|
-
|
|
835
|
-
@if (cell.separatorContext !== undefined) {
|
|
836
|
-
<symbiote-view>
|
|
1068
|
+
@if (cell.separatorContext !== undefined) {
|
|
837
1069
|
<ng-container
|
|
838
1070
|
[vListOutlet]="separatorDir?.templateRef"
|
|
839
1071
|
[vListOutletContext]="cell.separatorContext"
|
|
840
1072
|
></ng-container>
|
|
841
|
-
|
|
842
|
-
|
|
1073
|
+
}
|
|
1074
|
+
</symbiote-view>
|
|
843
1075
|
}
|
|
844
1076
|
@if (trailingSpacerStyle !== null) {
|
|
845
1077
|
<symbiote-view [style]="trailingSpacerStyle"></symbiote-view>
|