@symbiote-native/angular 2.0.0 → 3.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 +5 -4
- package/babel-register-composed.cjs +0 -8
- package/build/angular/callback-host.d.ts +25 -0
- package/build/angular/callback-host.js +211 -0
- package/build/angular/callback-host.js.map +1 -0
- package/build/angular/change-detection-flush.d.ts +14 -2
- package/build/angular/change-detection-flush.js +54 -6
- package/build/angular/change-detection-flush.js.map +1 -1
- package/build/angular/components/flat-list/index.js +39 -86
- package/build/angular/components/flat-list/index.js.map +1 -1
- package/build/angular/components/image-shared.js +49 -110
- package/build/angular/components/image-shared.js.map +1 -1
- package/build/angular/components/keyboard-avoiding-view/index.js +33 -72
- package/build/angular/components/keyboard-avoiding-view/index.js.map +1 -1
- package/build/angular/components/modal/index.js +58 -103
- package/build/angular/components/modal/index.js.map +1 -1
- package/build/angular/components/pressable-props.d.ts +2 -0
- package/build/angular/components/section-list/index.js +20 -103
- package/build/angular/components/section-list/index.js.map +1 -1
- package/build/angular/components/touchable-props.d.ts +1 -0
- package/build/angular/components/virtualized-list/directives.js +32 -22
- package/build/angular/components/virtualized-list/directives.js.map +1 -1
- package/build/angular/components/virtualized-list/index.d.ts +7 -1
- package/build/angular/components/virtualized-list/index.js +350 -359
- package/build/angular/components/virtualized-list/index.js.map +1 -1
- package/build/angular/components/virtualized-section-list/directives.js +20 -12
- package/build/angular/components/virtualized-section-list/directives.js.map +1 -1
- package/build/angular/components/virtualized-section-list/index.js +36 -142
- package/build/angular/components/virtualized-section-list/index.js.map +1 -1
- package/build/angular/components.d.ts +1 -1
- package/build/angular/components.js +1 -1
- package/build/angular/components.js.map +1 -1
- package/build/angular/create-portal/index.js +9 -8
- package/build/angular/create-portal/index.js.map +1 -1
- package/build/angular/create-tunnel/index.js +13 -12
- package/build/angular/create-tunnel/index.js.map +1 -1
- package/build/angular/descriptor-to-angular/index.js +6 -6
- package/build/angular/descriptor-to-angular/index.js.map +1 -1
- package/build/angular/diagnostics.d.ts +13 -0
- package/build/angular/diagnostics.js +18 -0
- package/build/angular/diagnostics.js.map +1 -1
- package/build/angular/element-props.d.ts +1 -3
- package/build/angular/elements.d.ts +46 -32
- package/build/angular/elements.js +217 -434
- package/build/angular/elements.js.map +1 -1
- package/build/angular/index.d.ts +3 -1
- package/build/angular/index.js +44 -13
- package/build/angular/index.js.map +1 -1
- package/build/angular/modules/animated/animated-leaf-binder.js +0 -2
- package/build/angular/modules/animated/animated-leaf-binder.js.map +1 -1
- package/build/angular/modules/animated/create-animated-component.js +69 -74
- package/build/angular/modules/animated/create-animated-component.js.map +1 -1
- package/build/angular/modules/animated/index.d.ts +30 -19
- package/build/angular/modules/status-bar/index.js +2 -8
- package/build/angular/modules/status-bar/index.js.map +1 -1
- package/build/angular/primitives/index.d.ts +14 -28
- package/build/angular/primitives/index.js +44 -107
- package/build/angular/primitives/index.js.map +1 -1
- package/build/angular/primitives/shared.d.ts +1 -0
- package/build/angular/primitives/shared.js +47 -32
- package/build/angular/primitives/shared.js.map +1 -1
- package/build/angular/render/index.js +34 -1
- package/build/angular/render/index.js.map +1 -1
- package/build/angular/renderer/index.d.ts +56 -0
- package/build/angular/renderer/index.js +295 -114
- package/build/angular/renderer/index.js.map +1 -1
- package/build/angular/runtime-matching.d.ts +9 -0
- package/build/angular/runtime-matching.js +82 -0
- package/build/angular/runtime-matching.js.map +1 -0
- package/build/angular/services/color-scheme.service.js +4 -4
- 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/build/angular/style-host.d.ts +11 -0
- package/build/angular/style-host.js +86 -0
- package/build/angular/style-host.js.map +1 -0
- package/metro-config.cjs +32 -16
- package/package.json +5 -5
- package/src/callback-host.ts +229 -0
- package/src/change-detection-flush.ts +62 -9
- package/src/components/flat-list/index.ts +2 -1
- package/src/components/keyboard-avoiding-view/index.ts +8 -0
- package/src/components/pressable-props.ts +8 -0
- package/src/components/touchable-props.ts +4 -0
- package/src/components/virtualized-list/index.ts +98 -45
- package/src/components.ts +0 -2
- package/src/diagnostics.ts +18 -0
- package/src/element-props.ts +1 -5
- package/src/elements.ts +159 -38
- package/src/index.ts +47 -14
- package/src/primitives/index.ts +14 -70
- package/src/primitives/shared.ts +21 -6
- package/src/render/index.ts +44 -0
- package/src/renderer/index.ts +347 -104
- package/src/runtime-matching.ts +80 -0
- package/src/style-host.ts +94 -0
- package/tsconfig.angular.base.json +11 -0
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
// Lists have no Descriptor render fn — the cell content is the framework's own children.
|
|
25
25
|
// Cells/spacers are plain view host elements.
|
|
26
26
|
import { CUSTOM_ELEMENTS_SCHEMA, ChangeDetectionStrategy, ChangeDetectorRef, Component, ContentChild, ElementRef, EventEmitter, Input, Output, ViewChild, computed, inject, signal, } from '@angular/core';
|
|
27
|
-
import {
|
|
27
|
+
import { DEFAULT_INITIAL_NUM_TO_RENDER, DEFAULT_MAX_TO_RENDER_PER_BATCH, 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, buildScrollViewHandle, } from '@symbiote-native/components';
|
|
28
28
|
import { dlog, flattenStyle, isSymbioteNode, } from '@symbiote-native/engine';
|
|
29
29
|
import { countAngular } from '../../diagnostics';
|
|
30
30
|
import { anchorHostStyle, SymbioteHostPropsDirective, SymbioteStyleInputDirective, ViewHost, } from '../../primitives';
|
|
@@ -40,250 +40,208 @@ export { VListEmptyDirective, VListFooterDirective, VListHeaderDirective, VListI
|
|
|
40
40
|
// ngc's partial evaluator needs the decorator's `template` to be one literal), so the body is
|
|
41
41
|
// duplicated across the two branches — the same shape Svelte's own `{#if horizontal}` takes.
|
|
42
42
|
export class VirtualizedList {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
43
|
+
constructor() {
|
|
44
|
+
// The list's edge/viewability/failure events as real Angular events: `(endReached)="…"`, not
|
|
45
|
+
// `[onEndReached]="…"`. See handleRefresh/accessibility*Tick below for how the still-callback-
|
|
46
|
+
// shaped scroll-view/refresh-control bag keys are fed from these.
|
|
47
|
+
this.endReached = new EventEmitter();
|
|
48
|
+
this.startReached = new EventEmitter();
|
|
49
|
+
this.refresh = new EventEmitter();
|
|
50
|
+
this.viewableItemsChanged = new EventEmitter();
|
|
51
|
+
this.scrollToIndexFailed = new EventEmitter();
|
|
52
|
+
this.accessibilityAction = new EventEmitter();
|
|
53
|
+
this.accessibilityTap = new EventEmitter();
|
|
54
|
+
this.magicTap = new EventEmitter();
|
|
55
|
+
this.accessibilityEscape = new EventEmitter();
|
|
56
|
+
// This wrapper binds the four gated accessibility events on the component it renders, which
|
|
57
|
+
// Angular forces to be unconditional and which would light that component's gates on every
|
|
58
|
+
// instance. It answers for them instead — see `gate-demand.ts`.
|
|
59
|
+
this.gateDemandAbove = injectGateDemandAbove();
|
|
60
|
+
// `buildScrollViewHandle` reads the node lazily through the getter above and no-ops before
|
|
61
|
+
// commit — the same function every other adapter feeds from its own host-instance accessor.
|
|
62
|
+
this.scrollHandle = buildScrollViewHandle(() => this.scrollNode);
|
|
63
|
+
// --- template-bound view state, assembled in ngDoCheck (recomputeView) ---
|
|
64
|
+
this.itemCount = EMPTY_OFFSET;
|
|
65
|
+
this.windowCells = [];
|
|
66
|
+
// The nearest sticky-header cell force-mounted outside [first,last] (plan.forcedStickyCell), the
|
|
67
|
+
// Angular twin of RN's _ensureClosestStickyHeader. Rendered ahead of windowCells, right after the
|
|
68
|
+
// leading spacer, with gapSpacerStyle filling the gap to the window's own first cell — see
|
|
69
|
+
// buildListPlan's doc comment for the exact child order this assumes.
|
|
70
|
+
this.forcedStickyCell = null;
|
|
71
|
+
this.leadingSpacerStyle = null;
|
|
72
|
+
this.gapSpacerStyle = null;
|
|
73
|
+
this.trailingSpacerStyle = null;
|
|
74
|
+
this.cellStyle = undefined;
|
|
75
|
+
// Bound to the template's `[style]="resolvedStyle"`, which Angular compiles to the built-in
|
|
76
|
+
// ɵɵstyleMap instruction — it only understands a flat object, never an array (RN's own
|
|
77
|
+
// `style={[a, b]}` composition idiom crashes deep inside Angular's styling engine), so this
|
|
78
|
+
// is always pre-flattened via the engine's own flattenStyle, never left as an array.
|
|
79
|
+
this.resolvedStyle = undefined;
|
|
80
|
+
this.resolvedContentContainerStyle = undefined;
|
|
81
|
+
this.resolvedMaintainVisibleContentPosition = undefined;
|
|
82
|
+
// The offset we are imperatively driving native to before the scroll handle attaches (rides down
|
|
83
|
+
// as contentOffset). A fresh object identity each push so the commit re-applies a repeated value.
|
|
84
|
+
this.commandedOffset = undefined;
|
|
85
|
+
// The one folded state cell (the Angular twin of React's stateRef): scroll offset, viewport,
|
|
86
|
+
// measured lengths, committed window, edge/viewability dedup, MVCP anchor — all in IListState,
|
|
87
|
+
// driven by the shared reduceList. `renderVersion` bumps whenever a transition changes render
|
|
88
|
+
// state, so ngDoCheck's recompute-dedup can detect it (listState is a plain object, not tracked).
|
|
89
|
+
this.listState = createInitialListState();
|
|
90
|
+
this.renderVersion = EMPTY_OFFSET;
|
|
91
|
+
this.cellMeasures = new Map();
|
|
92
|
+
// A cell's context object is the ONLY thing its stamped view reads, and VListOutletDirective
|
|
93
|
+
// refreshes that view whenever the object's IDENTITY changes. recomputeView runs on every frame
|
|
94
|
+
// the window moves, so minting a fresh context per cell made a four-row slide cost a refresh of
|
|
95
|
+
// EVERY cell in the window. MEASURED on PATH B geometry (544 entries, 320px viewport, RN
|
|
96
|
+
// defaults), per fling frame: 233-cell window, 4 cells genuinely entering, 228 outlet updates —
|
|
97
|
+
// each one a copyContextFields plus a markForCheck that walks to the root. Solid, same geometry,
|
|
98
|
+
// same shared reducer, same engine output: 4. Handing back the SAME object for a cell whose item
|
|
99
|
+
// and index did not move keeps `[vListOutletContext]` reference-equal, so ngOnChanges never fires
|
|
100
|
+
// for it. The fast adapters get this by construction — Solid's <For> is keyed on the cell KEY
|
|
101
|
+
// strings precisely so "the plan's freshly-built cell objects" cannot rebuild every row
|
|
102
|
+
// (adapters/solid/src/components/virtualized-list/shared.tsx, the cellKeys memo).
|
|
103
|
+
this.cellContexts = new Map();
|
|
104
|
+
this.nextCellContexts = new Map();
|
|
105
|
+
// Everything a cell's CONTENT can depend on that is not its own item identity. RN's contract for
|
|
106
|
+
// "re-render the cells, the data object itself did not change" is extraData; without it here an
|
|
107
|
+
// extraData flip would reuse every context and repaint nothing.
|
|
108
|
+
this.cellContextEpoch = undefined;
|
|
109
|
+
this.separatorOverrides = new Map();
|
|
110
|
+
// The adapter-owned debounce (minimumViewTime) and incremental-fill timers; the reducer only
|
|
111
|
+
// hands back a delay.
|
|
112
|
+
this.viewableTimer = null;
|
|
113
|
+
this.batchTimer = null;
|
|
114
|
+
// Dedupes the after-commit effects: they run only when the windowing signature changed.
|
|
115
|
+
this.lastEffectSignature = '';
|
|
116
|
+
// Dedupes ngDoCheck's own recompute. WITHOUT this, recomputeView() unconditionally rebuilds
|
|
117
|
+
// windowCells (and each cell's `separators` handle — fresh closures every call) on EVERY CD pass,
|
|
118
|
+
// including ones triggered by something else entirely in the app. A fresh context object flows
|
|
119
|
+
// into VListOutletDirective, which (correctly) sees its `context` @Input() change and calls
|
|
120
|
+
// `viewRef.markForCheck()` — which reschedules ANOTHER change-detection tick via the zoneless
|
|
121
|
+
// scheduler (adapters/angular/src/render.ts). Recomputing unconditionally therefore free-runs
|
|
122
|
+
// forever the moment a list actually has cells to stamp (a real, previously-dormant bug only
|
|
123
|
+
// exposed once flat-list.test.ts's projection fix let cells render for the first time — see that
|
|
124
|
+
// test's comment). Guarding recompute behind "did anything relevant actually change" breaks the
|
|
125
|
+
// cycle: unrelated CD passes reuse the same windowCells/context identities, so
|
|
126
|
+
// VListOutletDirective's ngOnChanges sees no change and never reschedules.
|
|
127
|
+
this.lastRecompute = undefined;
|
|
128
|
+
this.cdr = inject(ChangeDetectorRef);
|
|
129
|
+
// This component's OWN host — the non-painting anchor `class="..."` at the use site resolves
|
|
130
|
+
// onto (see anchorHostStyle's doc comment) — NOT `scrollHostRef` above, which targets the real
|
|
131
|
+
// inner `<scroll-view>`/`<horizontal-scroll-view>` tag one level down.
|
|
132
|
+
this.elementRef = inject(ElementRef);
|
|
133
|
+
// Bridges the non-reactive @Input fields into the reactive graph so `foldedAccessibility` below
|
|
134
|
+
// can memoize a bag derived from them (read its own comment for why signal inputs are not an
|
|
135
|
+
// option while this package's unit suite runs on JIT).
|
|
136
|
+
//
|
|
137
|
+
// ONLY safe for a bag whose every dependency is an @Input. This list's window machinery
|
|
138
|
+
// (listState, renderVersion, windowCells, the measured metrics) is driven from scroll/layout
|
|
139
|
+
// callbacks that never touch ngOnChanges, so nothing derived from it may ride this signal - it
|
|
140
|
+
// would memoize a window that stops re-computing mid-scroll. Make that state its own signal
|
|
141
|
+
// instead of widening this one.
|
|
142
|
+
this.inputsRevision = signal(0, /* @ts-ignore */
|
|
143
|
+
...(ngDevMode ? [{ debugName: "inputsRevision" }] : /* istanbul ignore next */ []));
|
|
144
|
+
// MEASURED (while ScrollView was still a wrapper): the template fed 20 separate inputs off this
|
|
145
|
+
// one bag, and Angular does not cache a getter across binding expressions - as a getter it
|
|
146
|
+
// rebuilt the object 20 times per refresh, and every consumer saw a fresh reference, so all 20
|
|
147
|
+
// reported "changed" and wrote through. A computed evaluates once and hands back the SAME object
|
|
148
|
+
// until an input actually changes. Every dependency below is an @Input, which is what makes
|
|
149
|
+
// inputsRevision a complete dependency set here — `scrollViewBag()` reuses this result rather
|
|
150
|
+
// than re-running the fold.
|
|
151
|
+
this.foldedAccessibility = computed(() => {
|
|
152
|
+
this.inputsRevision();
|
|
153
|
+
return resolveAccessibilityProps({
|
|
154
|
+
testID: this.testID,
|
|
155
|
+
nativeID: this.nativeID,
|
|
156
|
+
accessible: this.accessible,
|
|
157
|
+
accessibilityLabel: this.accessibilityLabel,
|
|
158
|
+
accessibilityHint: this.accessibilityHint,
|
|
159
|
+
accessibilityRole: this.accessibilityRole,
|
|
160
|
+
accessibilityState: this.accessibilityState,
|
|
161
|
+
accessibilityValue: this.accessibilityValue,
|
|
162
|
+
accessibilityActions: this.accessibilityActions,
|
|
163
|
+
accessibilityLabelledBy: this.accessibilityLabelledBy,
|
|
164
|
+
importantForAccessibility: this.importantForAccessibility,
|
|
165
|
+
accessibilityLiveRegion: this.accessibilityLiveRegion,
|
|
166
|
+
screenReaderFocusable: this.screenReaderFocusable,
|
|
167
|
+
accessibilityViewIsModal: this.accessibilityViewIsModal,
|
|
168
|
+
accessibilityElementsHidden: this.accessibilityElementsHidden,
|
|
169
|
+
accessibilityIgnoresInvertColors: this.accessibilityIgnoresInvertColors,
|
|
170
|
+
accessibilityLanguage: this.accessibilityLanguage,
|
|
171
|
+
accessibilityRespondsToUserInteraction: this.accessibilityRespondsToUserInteraction,
|
|
172
|
+
accessibilityShowsLargeContentViewer: this.accessibilityShowsLargeContentViewer,
|
|
173
|
+
accessibilityLargeContentTitle: this.accessibilityLargeContentTitle,
|
|
174
|
+
role: this.role,
|
|
175
|
+
'aria-label': this.ariaLabel,
|
|
176
|
+
'aria-labelledby': this.ariaLabelledBy,
|
|
177
|
+
'aria-live': this.ariaLive,
|
|
178
|
+
'aria-hidden': this.ariaHidden,
|
|
179
|
+
'aria-busy': this.ariaBusy,
|
|
180
|
+
'aria-checked': this.ariaChecked,
|
|
181
|
+
'aria-disabled': this.ariaDisabled,
|
|
182
|
+
'aria-expanded': this.ariaExpanded,
|
|
183
|
+
'aria-selected': this.ariaSelected,
|
|
184
|
+
'aria-modal': this.ariaModal,
|
|
185
|
+
'aria-valuemax': this.ariaValueMax,
|
|
186
|
+
'aria-valuemin': this.ariaValueMin,
|
|
187
|
+
'aria-valuenow': this.ariaValueNow,
|
|
188
|
+
'aria-valuetext': this.ariaValueText,
|
|
189
|
+
});
|
|
190
|
+
}, /* @ts-ignore */
|
|
191
|
+
...(ngDevMode ? [{ debugName: "foldedAccessibility" }] : /* istanbul ignore next */ []));
|
|
192
|
+
// onScroll stays a plain callback bag key (an Animated.event(...) target must be able to flow
|
|
193
|
+
// through it, which an @Output() can't carry), so this is a stable arrow field read straight into
|
|
194
|
+
// scrollViewBag()'s `onScroll`. onLayout is unconditional too — this list always needs its own
|
|
195
|
+
// layout for windowing, regardless of whether an app subscribes to anything.
|
|
196
|
+
this.onScrollTick = (event) => {
|
|
197
|
+
countAngular('scrollTicks');
|
|
198
|
+
const offset = readScrollOffset(event, this.isHorizontal);
|
|
199
|
+
if (offset === undefined)
|
|
200
|
+
return;
|
|
201
|
+
dlog(`Angular VirtualizedList onScroll offset=${offset}`);
|
|
202
|
+
// A real native scroll supersedes any pending commanded offset.
|
|
203
|
+
this.commandedOffset = undefined;
|
|
204
|
+
this.dispatch({ kind: 'scroll', offset });
|
|
205
|
+
// Compose, don't clobber: internal windowing ran first, now the user's onScroll.
|
|
206
|
+
this.onScroll?.(event);
|
|
207
|
+
};
|
|
208
|
+
this.onLayoutTick = (event) => {
|
|
209
|
+
const length = readLayoutLength(event, this.isHorizontal);
|
|
210
|
+
if (length === undefined)
|
|
211
|
+
return;
|
|
212
|
+
dlog(`Angular VirtualizedList onLayout viewport=${length}`);
|
|
213
|
+
this.dispatch({ kind: 'layout', length });
|
|
214
|
+
};
|
|
215
|
+
// RefreshControl's refresh and the scroll tag's accessibility events reach here as plain
|
|
216
|
+
// listener-prop values in scrollViewBag()/refreshControlBag() — these fields just adapt
|
|
217
|
+
// VirtualizedList's own @Output() into a plain re-emit callback.
|
|
218
|
+
this.handleRefresh = () => {
|
|
219
|
+
this.refresh.emit();
|
|
220
|
+
};
|
|
221
|
+
this.accessibilityActionTick = (event) => {
|
|
222
|
+
this.accessibilityAction.emit(event);
|
|
223
|
+
};
|
|
224
|
+
this.accessibilityTapTick = (event) => {
|
|
225
|
+
this.accessibilityTap.emit(event);
|
|
226
|
+
};
|
|
227
|
+
this.magicTapTick = (event) => {
|
|
228
|
+
this.magicTap.emit(event);
|
|
229
|
+
};
|
|
230
|
+
this.accessibilityEscapeTick = (event) => {
|
|
231
|
+
this.accessibilityEscape.emit(event);
|
|
232
|
+
};
|
|
233
|
+
this.keyFor = (index) => {
|
|
234
|
+
const item = this.getItem(this.data, index);
|
|
235
|
+
return resolveItemKey(item, index, this.keyExtractor);
|
|
236
|
+
};
|
|
237
|
+
}
|
|
59
238
|
wantsGate(name) {
|
|
60
239
|
return gateWanted(this.gateDemandAbove, name, this[name]);
|
|
61
240
|
}
|
|
62
|
-
data;
|
|
63
|
-
getItem;
|
|
64
|
-
getItemCount;
|
|
65
|
-
keyExtractor;
|
|
66
|
-
getItemLayout;
|
|
67
|
-
horizontal;
|
|
68
|
-
inverted;
|
|
69
|
-
extraData;
|
|
70
|
-
onEndReachedThreshold;
|
|
71
|
-
onStartReachedThreshold;
|
|
72
|
-
refreshing;
|
|
73
|
-
progressViewOffset;
|
|
74
|
-
// Set explicitly by a wrapper (FlatList/VirtualizedSectionList) that ALWAYS binds
|
|
75
|
-
// `(refresh)="refresh.emit()"` on this component to re-forward the event outward — that binding
|
|
76
|
-
// itself makes `this.refresh.observed` permanently true (Angular subscribes unconditionally the
|
|
77
|
-
// moment a template writes `(refresh)="…"`, regardless of what the handler does), so `.observed`
|
|
78
|
-
// can no longer tell "the app actually wants pull-to-refresh" from "a wrapper is just forwarding
|
|
79
|
-
// the event". A wrapper passes its OWN public `refresh.observed` here instead; direct usage
|
|
80
|
-
// (no wrapper) falls back to this component's own `.observed`, unchanged from before.
|
|
81
|
-
refreshRequested;
|
|
82
|
-
viewabilityConfig;
|
|
83
|
-
viewabilityConfigCallbackPairs;
|
|
84
|
-
initialNumToRender;
|
|
85
|
-
initialScrollIndex;
|
|
86
|
-
maxToRenderPerBatch;
|
|
87
|
-
updateCellsBatchingPeriod;
|
|
88
|
-
windowSize;
|
|
89
|
-
stickyHeaderIndices;
|
|
90
|
-
maintainVisibleContentPosition;
|
|
91
|
-
onScroll;
|
|
92
|
-
onScrollBeginDrag;
|
|
93
|
-
onScrollEndDrag;
|
|
94
|
-
onMomentumScrollBegin;
|
|
95
|
-
onMomentumScrollEnd;
|
|
96
|
-
scrollEventThrottle;
|
|
97
|
-
keyboardShouldPersistTaps;
|
|
98
|
-
keyboardDismissMode;
|
|
99
|
-
style;
|
|
100
|
-
contentContainerStyle;
|
|
101
|
-
testID;
|
|
102
|
-
nativeID;
|
|
103
|
-
accessible;
|
|
104
|
-
accessibilityLabel;
|
|
105
|
-
accessibilityHint;
|
|
106
|
-
accessibilityRole;
|
|
107
|
-
accessibilityState;
|
|
108
|
-
accessibilityValue;
|
|
109
|
-
accessibilityActions;
|
|
110
|
-
accessibilityLabelledBy;
|
|
111
|
-
importantForAccessibility;
|
|
112
|
-
accessibilityLiveRegion;
|
|
113
|
-
screenReaderFocusable;
|
|
114
|
-
accessibilityViewIsModal;
|
|
115
|
-
accessibilityElementsHidden;
|
|
116
|
-
accessibilityIgnoresInvertColors;
|
|
117
|
-
accessibilityLanguage;
|
|
118
|
-
accessibilityRespondsToUserInteraction;
|
|
119
|
-
accessibilityShowsLargeContentViewer;
|
|
120
|
-
accessibilityLargeContentTitle;
|
|
121
|
-
role;
|
|
122
|
-
ariaLabel;
|
|
123
|
-
ariaLabelledBy;
|
|
124
|
-
ariaLive;
|
|
125
|
-
ariaHidden;
|
|
126
|
-
ariaBusy;
|
|
127
|
-
ariaChecked;
|
|
128
|
-
ariaDisabled;
|
|
129
|
-
ariaExpanded;
|
|
130
|
-
ariaSelected;
|
|
131
|
-
ariaModal;
|
|
132
|
-
ariaValueMax;
|
|
133
|
-
ariaValueMin;
|
|
134
|
-
ariaValueNow;
|
|
135
|
-
ariaValueText;
|
|
136
|
-
// The cell + slot templates the app authors (the Angular twin of renderItem / ListHeaderComponent
|
|
137
|
-
// / ListFooterComponent / ListEmptyComponent / ItemSeparatorComponent), captured from projected
|
|
138
|
-
// <ng-template> content. Resolved by ngAfterContentInit, read each ngDoCheck (from the 2nd CD on).
|
|
139
|
-
itemDir;
|
|
140
|
-
headerDir;
|
|
141
|
-
footerDir;
|
|
142
|
-
emptyDir;
|
|
143
|
-
separatorDir;
|
|
144
|
-
// The inner scroll TAG's own engine node — a template ref on a bare intrinsic hands back the
|
|
145
|
-
// host node directly (`isSymbioteNode(elementRef.nativeElement)`, matching `anchorHostStyle`'s
|
|
146
|
-
// own reader below), so there is no wrapper component left to read a handle off. Present from
|
|
147
|
-
// whichever branch (`scroll-view` / `horizontal-scroll-view`) is currently mounted, once the
|
|
148
|
-
// template has run at least one CD pass.
|
|
149
|
-
scrollHostRef;
|
|
150
241
|
get scrollNode() {
|
|
151
242
|
const node = this.scrollHostRef?.nativeElement;
|
|
152
243
|
return isSymbioteNode(node) ? node : null;
|
|
153
244
|
}
|
|
154
|
-
// `buildScrollViewHandle` reads the node lazily through the getter above and no-ops before
|
|
155
|
-
// commit — the same function every other adapter feeds from its own host-instance accessor.
|
|
156
|
-
scrollHandle = buildScrollViewHandle(() => this.scrollNode);
|
|
157
|
-
// --- template-bound view state, assembled in ngDoCheck (recomputeView) ---
|
|
158
|
-
itemCount = EMPTY_OFFSET;
|
|
159
|
-
windowCells = [];
|
|
160
|
-
// The nearest sticky-header cell force-mounted outside [first,last] (plan.forcedStickyCell), the
|
|
161
|
-
// Angular twin of RN's _ensureClosestStickyHeader. Rendered ahead of windowCells, right after the
|
|
162
|
-
// leading spacer, with gapSpacerStyle filling the gap to the window's own first cell — see
|
|
163
|
-
// buildListPlan's doc comment for the exact child order this assumes.
|
|
164
|
-
forcedStickyCell = null;
|
|
165
|
-
leadingSpacerStyle = null;
|
|
166
|
-
gapSpacerStyle = null;
|
|
167
|
-
trailingSpacerStyle = null;
|
|
168
|
-
cellStyle = undefined;
|
|
169
|
-
renderedStickyIndices = undefined;
|
|
170
|
-
// Bound to the template's `[style]="resolvedStyle"`, which Angular compiles to the built-in
|
|
171
|
-
// ɵɵstyleMap instruction — it only understands a flat object, never an array (RN's own
|
|
172
|
-
// `style={[a, b]}` composition idiom crashes deep inside Angular's styling engine), so this
|
|
173
|
-
// is always pre-flattened via the engine's own flattenStyle, never left as an array.
|
|
174
|
-
resolvedStyle = undefined;
|
|
175
|
-
resolvedContentContainerStyle = undefined;
|
|
176
|
-
resolvedMaintainVisibleContentPosition = undefined;
|
|
177
|
-
// The offset we are imperatively driving native to before the scroll handle attaches (rides down
|
|
178
|
-
// as contentOffset). A fresh object identity each push so the commit re-applies a repeated value.
|
|
179
|
-
commandedOffset = undefined;
|
|
180
|
-
// The one folded state cell (the Angular twin of React's stateRef): scroll offset, viewport,
|
|
181
|
-
// measured lengths, committed window, edge/viewability dedup, MVCP anchor — all in IListState,
|
|
182
|
-
// driven by the shared reduceList. `renderVersion` bumps whenever a transition changes render
|
|
183
|
-
// state, so ngDoCheck's recompute-dedup can detect it (listState is a plain object, not tracked).
|
|
184
|
-
listState = createInitialListState();
|
|
185
|
-
renderVersion = EMPTY_OFFSET;
|
|
186
|
-
cellMeasures = new Map();
|
|
187
|
-
// A cell's context object is the ONLY thing its stamped view reads, and VListOutletDirective
|
|
188
|
-
// refreshes that view whenever the object's IDENTITY changes. recomputeView runs on every frame
|
|
189
|
-
// the window moves, so minting a fresh context per cell made a four-row slide cost a refresh of
|
|
190
|
-
// EVERY cell in the window. MEASURED on PATH B geometry (544 entries, 320px viewport, RN
|
|
191
|
-
// defaults), per fling frame: 233-cell window, 4 cells genuinely entering, 228 outlet updates —
|
|
192
|
-
// each one a copyContextFields plus a markForCheck that walks to the root. Solid, same geometry,
|
|
193
|
-
// same shared reducer, same engine output: 4. Handing back the SAME object for a cell whose item
|
|
194
|
-
// and index did not move keeps `[vListOutletContext]` reference-equal, so ngOnChanges never fires
|
|
195
|
-
// for it. The fast adapters get this by construction — Solid's <For> is keyed on the cell KEY
|
|
196
|
-
// strings precisely so "the plan's freshly-built cell objects" cannot rebuild every row
|
|
197
|
-
// (adapters/solid/src/components/virtualized-list/shared.tsx, the cellKeys memo).
|
|
198
|
-
cellContexts = new Map();
|
|
199
|
-
nextCellContexts = new Map();
|
|
200
|
-
// Everything a cell's CONTENT can depend on that is not its own item identity. RN's contract for
|
|
201
|
-
// "re-render the cells, the data object itself did not change" is extraData; without it here an
|
|
202
|
-
// extraData flip would reuse every context and repaint nothing.
|
|
203
|
-
cellContextEpoch = undefined;
|
|
204
|
-
separatorOverrides = new Map();
|
|
205
|
-
// The adapter-owned debounce (minimumViewTime) and incremental-fill timers; the reducer only
|
|
206
|
-
// hands back a delay.
|
|
207
|
-
viewableTimer = null;
|
|
208
|
-
batchTimer = null;
|
|
209
|
-
// Dedupes the after-commit effects: they run only when the windowing signature changed.
|
|
210
|
-
lastEffectSignature = '';
|
|
211
|
-
// Dedupes ngDoCheck's own recompute. WITHOUT this, recomputeView() unconditionally rebuilds
|
|
212
|
-
// windowCells (and each cell's `separators` handle — fresh closures every call) on EVERY CD pass,
|
|
213
|
-
// including ones triggered by something else entirely in the app. A fresh context object flows
|
|
214
|
-
// into VListOutletDirective, which (correctly) sees its `context` @Input() change and calls
|
|
215
|
-
// `viewRef.markForCheck()` — which reschedules ANOTHER change-detection tick via the zoneless
|
|
216
|
-
// scheduler (adapters/angular/src/render.ts). Recomputing unconditionally therefore free-runs
|
|
217
|
-
// forever the moment a list actually has cells to stamp (a real, previously-dormant bug only
|
|
218
|
-
// exposed once flat-list.test.ts's projection fix let cells render for the first time — see that
|
|
219
|
-
// test's comment). Guarding recompute behind "did anything relevant actually change" breaks the
|
|
220
|
-
// cycle: unrelated CD passes reuse the same windowCells/context identities, so
|
|
221
|
-
// VListOutletDirective's ngOnChanges sees no change and never reschedules.
|
|
222
|
-
lastRecompute = undefined;
|
|
223
|
-
cdr = inject(ChangeDetectorRef);
|
|
224
|
-
// This component's OWN host — the non-painting anchor `class="..."` at the use site resolves
|
|
225
|
-
// onto (see anchorHostStyle's doc comment) — NOT `scrollHostRef` above, which targets the real
|
|
226
|
-
// inner `<scroll-view>`/`<horizontal-scroll-view>` tag one level down.
|
|
227
|
-
elementRef = inject(ElementRef);
|
|
228
|
-
// Bridges the non-reactive @Input fields into the reactive graph so `foldedAccessibility` below
|
|
229
|
-
// can memoize a bag derived from them (read its own comment for why signal inputs are not an
|
|
230
|
-
// option while this package's unit suite runs on JIT).
|
|
231
|
-
//
|
|
232
|
-
// ONLY safe for a bag whose every dependency is an @Input. This list's window machinery
|
|
233
|
-
// (listState, renderVersion, windowCells, the measured metrics) is driven from scroll/layout
|
|
234
|
-
// callbacks that never touch ngOnChanges, so nothing derived from it may ride this signal - it
|
|
235
|
-
// would memoize a window that stops re-computing mid-scroll. Make that state its own signal
|
|
236
|
-
// instead of widening this one.
|
|
237
|
-
inputsRevision = signal(0, /* @ts-ignore */
|
|
238
|
-
...(ngDevMode ? [{ debugName: "inputsRevision" }] : /* istanbul ignore next */ []));
|
|
239
|
-
// MEASURED (while ScrollView was still a wrapper): the template fed 20 separate inputs off this
|
|
240
|
-
// one bag, and Angular does not cache a getter across binding expressions - as a getter it
|
|
241
|
-
// rebuilt the object 20 times per refresh, and every consumer saw a fresh reference, so all 20
|
|
242
|
-
// reported "changed" and wrote through. A computed evaluates once and hands back the SAME object
|
|
243
|
-
// until an input actually changes. Every dependency below is an @Input, which is what makes
|
|
244
|
-
// inputsRevision a complete dependency set here — `scrollViewBag()` reuses this result rather
|
|
245
|
-
// than re-running the fold.
|
|
246
|
-
foldedAccessibility = computed(() => {
|
|
247
|
-
this.inputsRevision();
|
|
248
|
-
return resolveAccessibilityProps({
|
|
249
|
-
testID: this.testID,
|
|
250
|
-
nativeID: this.nativeID,
|
|
251
|
-
accessible: this.accessible,
|
|
252
|
-
accessibilityLabel: this.accessibilityLabel,
|
|
253
|
-
accessibilityHint: this.accessibilityHint,
|
|
254
|
-
accessibilityRole: this.accessibilityRole,
|
|
255
|
-
accessibilityState: this.accessibilityState,
|
|
256
|
-
accessibilityValue: this.accessibilityValue,
|
|
257
|
-
accessibilityActions: this.accessibilityActions,
|
|
258
|
-
accessibilityLabelledBy: this.accessibilityLabelledBy,
|
|
259
|
-
importantForAccessibility: this.importantForAccessibility,
|
|
260
|
-
accessibilityLiveRegion: this.accessibilityLiveRegion,
|
|
261
|
-
screenReaderFocusable: this.screenReaderFocusable,
|
|
262
|
-
accessibilityViewIsModal: this.accessibilityViewIsModal,
|
|
263
|
-
accessibilityElementsHidden: this.accessibilityElementsHidden,
|
|
264
|
-
accessibilityIgnoresInvertColors: this.accessibilityIgnoresInvertColors,
|
|
265
|
-
accessibilityLanguage: this.accessibilityLanguage,
|
|
266
|
-
accessibilityRespondsToUserInteraction: this.accessibilityRespondsToUserInteraction,
|
|
267
|
-
accessibilityShowsLargeContentViewer: this.accessibilityShowsLargeContentViewer,
|
|
268
|
-
accessibilityLargeContentTitle: this.accessibilityLargeContentTitle,
|
|
269
|
-
role: this.role,
|
|
270
|
-
'aria-label': this.ariaLabel,
|
|
271
|
-
'aria-labelledby': this.ariaLabelledBy,
|
|
272
|
-
'aria-live': this.ariaLive,
|
|
273
|
-
'aria-hidden': this.ariaHidden,
|
|
274
|
-
'aria-busy': this.ariaBusy,
|
|
275
|
-
'aria-checked': this.ariaChecked,
|
|
276
|
-
'aria-disabled': this.ariaDisabled,
|
|
277
|
-
'aria-expanded': this.ariaExpanded,
|
|
278
|
-
'aria-selected': this.ariaSelected,
|
|
279
|
-
'aria-modal': this.ariaModal,
|
|
280
|
-
'aria-valuemax': this.ariaValueMax,
|
|
281
|
-
'aria-valuemin': this.ariaValueMin,
|
|
282
|
-
'aria-valuenow': this.ariaValueNow,
|
|
283
|
-
'aria-valuetext': this.ariaValueText,
|
|
284
|
-
});
|
|
285
|
-
}, /* @ts-ignore */
|
|
286
|
-
...(ngDevMode ? [{ debugName: "foldedAccessibility" }] : /* istanbul ignore next */ []));
|
|
287
245
|
get shouldRenderRefreshControl() {
|
|
288
246
|
return this.refreshRequested ?? this.refresh.observed;
|
|
289
247
|
}
|
|
@@ -308,7 +266,10 @@ export class VirtualizedList {
|
|
|
308
266
|
keyboardShouldPersistTaps: this.keyboardShouldPersistTaps,
|
|
309
267
|
keyboardDismissMode: this.keyboardDismissMode,
|
|
310
268
|
contentOffset: this.commandedOffset,
|
|
311
|
-
stickyHeaderIndices
|
|
269
|
+
// `stickyHeaderIndices` is deliberately NOT forwarded — it numbers the scroll view's PAINT
|
|
270
|
+
// children, and a windowed list paints a header, a spacer and a slice, so the positions move
|
|
271
|
+
// every time the window slides and the behavior re-wraps a different child each pass. The
|
|
272
|
+
// cells carry the `sticky-header` TAG instead, which pins by DOCUMENT order.
|
|
312
273
|
maintainVisibleContentPosition: this.resolvedMaintainVisibleContentPosition,
|
|
313
274
|
// The four boolean-gated Fabric events (`.claude/rules/fabric-boolean-event-gates.md`): the
|
|
314
275
|
// TAG has no `.observed` of its own, so `wantsGate` (own subscriber, or the demand from
|
|
@@ -354,56 +315,11 @@ export class VirtualizedList {
|
|
|
354
315
|
return (this.updateCellsBatchingPeriod ?? DEFAULT_UPDATE_CELLS_BATCHING_PERIOD);
|
|
355
316
|
}
|
|
356
317
|
get onEndReachedThresholdValue() {
|
|
357
|
-
return this.onEndReachedThreshold
|
|
318
|
+
return this.onEndReachedThreshold;
|
|
358
319
|
}
|
|
359
320
|
get onStartReachedThresholdValue() {
|
|
360
|
-
return this.onStartReachedThreshold
|
|
321
|
+
return this.onStartReachedThreshold;
|
|
361
322
|
}
|
|
362
|
-
// onScroll stays a plain callback bag key (an Animated.event(...) target must be able to flow
|
|
363
|
-
// through it, which an @Output() can't carry), so this is a stable arrow field read straight into
|
|
364
|
-
// scrollViewBag()'s `onScroll`. onLayout is unconditional too — this list always needs its own
|
|
365
|
-
// layout for windowing, regardless of whether an app subscribes to anything.
|
|
366
|
-
onScrollTick = (event) => {
|
|
367
|
-
countAngular('scrollTicks');
|
|
368
|
-
const offset = readScrollOffset(event, this.isHorizontal);
|
|
369
|
-
if (offset === undefined)
|
|
370
|
-
return;
|
|
371
|
-
dlog(`Angular VirtualizedList onScroll offset=${offset}`);
|
|
372
|
-
// A real native scroll supersedes any pending commanded offset.
|
|
373
|
-
this.commandedOffset = undefined;
|
|
374
|
-
this.dispatch({ kind: 'scroll', offset });
|
|
375
|
-
// Compose, don't clobber: internal windowing ran first, now the user's onScroll.
|
|
376
|
-
this.onScroll?.(event);
|
|
377
|
-
};
|
|
378
|
-
onLayoutTick = (event) => {
|
|
379
|
-
const length = readLayoutLength(event, this.isHorizontal);
|
|
380
|
-
if (length === undefined)
|
|
381
|
-
return;
|
|
382
|
-
dlog(`Angular VirtualizedList onLayout viewport=${length}`);
|
|
383
|
-
this.dispatch({ kind: 'layout', length });
|
|
384
|
-
};
|
|
385
|
-
// RefreshControl's refresh and the scroll tag's accessibility events reach here as plain
|
|
386
|
-
// listener-prop values in scrollViewBag()/refreshControlBag() — these fields just adapt
|
|
387
|
-
// VirtualizedList's own @Output() into a plain re-emit callback.
|
|
388
|
-
handleRefresh = () => {
|
|
389
|
-
this.refresh.emit();
|
|
390
|
-
};
|
|
391
|
-
accessibilityActionTick = (event) => {
|
|
392
|
-
this.accessibilityAction.emit(event);
|
|
393
|
-
};
|
|
394
|
-
accessibilityTapTick = (event) => {
|
|
395
|
-
this.accessibilityTap.emit(event);
|
|
396
|
-
};
|
|
397
|
-
magicTapTick = (event) => {
|
|
398
|
-
this.magicTap.emit(event);
|
|
399
|
-
};
|
|
400
|
-
accessibilityEscapeTick = (event) => {
|
|
401
|
-
this.accessibilityEscape.emit(event);
|
|
402
|
-
};
|
|
403
|
-
keyFor = (index) => {
|
|
404
|
-
const item = this.getItem(this.data, index);
|
|
405
|
-
return resolveItemKey(item, index, this.keyExtractor);
|
|
406
|
-
};
|
|
407
323
|
// The reducer inputs, folded off the @Input()s each call. The edge/viewability listeners map to
|
|
408
324
|
// `.observed` (Angular's "is anyone bound to this @Output()"), so the reducer emits only when the
|
|
409
325
|
// consumer listens — the same on-demand gating the prop-callback era had.
|
|
@@ -512,8 +428,12 @@ export class VirtualizedList {
|
|
|
512
428
|
const info = effect.info;
|
|
513
429
|
const map = effect.map;
|
|
514
430
|
const fire = () => {
|
|
515
|
-
for (const pair of pairs)
|
|
516
|
-
pair.onViewableItemsChanged(
|
|
431
|
+
for (const pair of pairs) {
|
|
432
|
+
pair.onViewableItemsChanged({
|
|
433
|
+
...info,
|
|
434
|
+
viewabilityConfig: pair.viewabilityConfig,
|
|
435
|
+
});
|
|
436
|
+
}
|
|
517
437
|
this.dispatch({ kind: 'viewable-fired', map });
|
|
518
438
|
};
|
|
519
439
|
if (this.viewableTimer !== null) {
|
|
@@ -654,7 +574,6 @@ export class VirtualizedList {
|
|
|
654
574
|
this.leadingSpacerStyle = null;
|
|
655
575
|
this.gapSpacerStyle = null;
|
|
656
576
|
this.trailingSpacerStyle = null;
|
|
657
|
-
this.renderedStickyIndices = undefined;
|
|
658
577
|
dlog(`Angular VirtualizedList empty (viewport=${this.listState.viewportLength})`);
|
|
659
578
|
return;
|
|
660
579
|
}
|
|
@@ -679,17 +598,16 @@ export class VirtualizedList {
|
|
|
679
598
|
plan.trailingExtent > EMPTY_OFFSET
|
|
680
599
|
? this.spacerStyle(plan.trailingExtent)
|
|
681
600
|
: null;
|
|
682
|
-
|
|
683
|
-
stickySet !== undefined && plan.stickyChildPositions.length > 0
|
|
684
|
-
? plan.stickyChildPositions
|
|
685
|
-
: undefined;
|
|
601
|
+
// `plan.stickyChildPositions` is deliberately NOT read — it was the input to the index form.
|
|
686
602
|
dlog(`STICKY[list] stickySet=${stickySet === undefined ? 'undefined' : JSON.stringify([...stickySet])} ` +
|
|
687
603
|
`childPositions=${JSON.stringify(plan.stickyChildPositions)} ` +
|
|
688
604
|
`forcedStickyCell=${plan.forcedStickyCell === undefined ? 'none' : plan.forcedStickyCell.index} ` +
|
|
689
605
|
`window=[${m.first},${m.last}] hasHeader=${this.headerDir !== undefined}`);
|
|
690
606
|
this.forcedStickyCell =
|
|
691
607
|
plan.forcedStickyCell !== undefined
|
|
692
|
-
? this.buildWindowCell(plan.forcedStickyCell.index, plan.forcedStickyCell.key, false
|
|
608
|
+
? this.buildWindowCell(plan.forcedStickyCell.index, plan.forcedStickyCell.key, false,
|
|
609
|
+
// Sticky by construction — `forcedStickyCell` exists only for an index in `stickySet`.
|
|
610
|
+
true)
|
|
693
611
|
: null;
|
|
694
612
|
const cells = [];
|
|
695
613
|
for (const planned of plan.cells) {
|
|
@@ -699,7 +617,7 @@ export class VirtualizedList {
|
|
|
699
617
|
// would make a cell's own measured height change as the window slides past it. Device-measured
|
|
700
618
|
// 2026-08-19 as a run of cells all shifting by exactly the divider's 1px.
|
|
701
619
|
const includeSeparator = hasSeparators && planned.index < m.count - 1;
|
|
702
|
-
cells.push(this.buildWindowCell(planned.index, planned.key, includeSeparator));
|
|
620
|
+
cells.push(this.buildWindowCell(planned.index, planned.key, includeSeparator, stickySet?.has(planned.index) === true));
|
|
703
621
|
}
|
|
704
622
|
this.windowCells = cells;
|
|
705
623
|
// Swap, don't prune: a cell that left the window is simply absent from the pass that just ran,
|
|
@@ -711,7 +629,7 @@ export class VirtualizedList {
|
|
|
711
629
|
}
|
|
712
630
|
// Assembles one cell (window or forced-sticky) — the context, measure closure, and optional
|
|
713
631
|
// separator context — so both plan.cells and plan.forcedStickyCell build identically.
|
|
714
|
-
buildWindowCell(index, key, includeSeparator) {
|
|
632
|
+
buildWindowCell(index, key, includeSeparator, isSticky) {
|
|
715
633
|
const item = this.getItem(this.data, index);
|
|
716
634
|
return {
|
|
717
635
|
key,
|
|
@@ -721,6 +639,7 @@ export class VirtualizedList {
|
|
|
721
639
|
separatorContext: includeSeparator
|
|
722
640
|
? this.buildSeparatorContext(index, item)
|
|
723
641
|
: undefined,
|
|
642
|
+
isSticky,
|
|
724
643
|
};
|
|
725
644
|
}
|
|
726
645
|
// The context for one cell, reused by identity when the cell did not move. See cellContexts.
|
|
@@ -883,8 +802,8 @@ export class VirtualizedList {
|
|
|
883
802
|
this.commandedOffset = target;
|
|
884
803
|
this.cdr.markForCheck();
|
|
885
804
|
}
|
|
886
|
-
static
|
|
887
|
-
static
|
|
805
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: VirtualizedList, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
806
|
+
static { this.ɵ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: "scrollHostRef", first: true, predicate: ["scrollHost"], descendants: true, read: ElementRef }], usesOnChanges: true, hostDirectives: [{ directive: i1.SymbioteStyleInputDirective, inputs: ["style", "style"] }], ngImport: i0, template: `
|
|
888
807
|
@if (isHorizontal) {
|
|
889
808
|
<horizontal-scroll-view #scrollHost [symbioteHostProps]="scrollViewBag()">
|
|
890
809
|
@if (shouldRenderRefreshControl) {
|
|
@@ -910,7 +829,7 @@ export class VirtualizedList {
|
|
|
910
829
|
<view [style]="leadingSpacerStyle"></view>
|
|
911
830
|
}
|
|
912
831
|
@if (forcedStickyCell !== null) {
|
|
913
|
-
<
|
|
832
|
+
<sticky-header
|
|
914
833
|
(layout)="handleCellLayout(forcedStickyCell.measure, $event)"
|
|
915
834
|
[style]="cellStyle"
|
|
916
835
|
>
|
|
@@ -918,27 +837,45 @@ export class VirtualizedList {
|
|
|
918
837
|
[vListOutlet]="itemDir?.templateRef"
|
|
919
838
|
[vListOutletContext]="forcedStickyCell.context"
|
|
920
839
|
></ng-container>
|
|
921
|
-
</
|
|
840
|
+
</sticky-header>
|
|
922
841
|
}
|
|
923
842
|
@if (gapSpacerStyle !== null) {
|
|
924
843
|
<view [style]="gapSpacerStyle"></view>
|
|
925
844
|
}
|
|
926
845
|
@for (cell of windowCells; track cell.key) {
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
[vListOutlet]="itemDir?.templateRef"
|
|
933
|
-
[vListOutletContext]="cell.context"
|
|
934
|
-
></ng-container>
|
|
935
|
-
@if (cell.separatorContext !== undefined) {
|
|
846
|
+
@if (cell.isSticky) {
|
|
847
|
+
<sticky-header
|
|
848
|
+
(layout)="handleCellLayout(cell.measure, $event)"
|
|
849
|
+
[style]="cellStyle"
|
|
850
|
+
>
|
|
936
851
|
<ng-container
|
|
937
|
-
[vListOutlet]="
|
|
938
|
-
[vListOutletContext]="cell.
|
|
852
|
+
[vListOutlet]="itemDir?.templateRef"
|
|
853
|
+
[vListOutletContext]="cell.context"
|
|
939
854
|
></ng-container>
|
|
940
|
-
|
|
941
|
-
|
|
855
|
+
@if (cell.separatorContext !== undefined) {
|
|
856
|
+
<ng-container
|
|
857
|
+
[vListOutlet]="separatorDir?.templateRef"
|
|
858
|
+
[vListOutletContext]="cell.separatorContext"
|
|
859
|
+
></ng-container>
|
|
860
|
+
}
|
|
861
|
+
</sticky-header>
|
|
862
|
+
} @else {
|
|
863
|
+
<view
|
|
864
|
+
(layout)="handleCellLayout(cell.measure, $event)"
|
|
865
|
+
[style]="cellStyle"
|
|
866
|
+
>
|
|
867
|
+
<ng-container
|
|
868
|
+
[vListOutlet]="itemDir?.templateRef"
|
|
869
|
+
[vListOutletContext]="cell.context"
|
|
870
|
+
></ng-container>
|
|
871
|
+
@if (cell.separatorContext !== undefined) {
|
|
872
|
+
<ng-container
|
|
873
|
+
[vListOutlet]="separatorDir?.templateRef"
|
|
874
|
+
[vListOutletContext]="cell.separatorContext"
|
|
875
|
+
></ng-container>
|
|
876
|
+
}
|
|
877
|
+
</view>
|
|
878
|
+
}
|
|
942
879
|
}
|
|
943
880
|
@if (trailingSpacerStyle !== null) {
|
|
944
881
|
<view [style]="trailingSpacerStyle"></view>
|
|
@@ -976,7 +913,7 @@ export class VirtualizedList {
|
|
|
976
913
|
<view [style]="leadingSpacerStyle"></view>
|
|
977
914
|
}
|
|
978
915
|
@if (forcedStickyCell !== null) {
|
|
979
|
-
<
|
|
916
|
+
<sticky-header
|
|
980
917
|
(layout)="handleCellLayout(forcedStickyCell.measure, $event)"
|
|
981
918
|
[style]="cellStyle"
|
|
982
919
|
>
|
|
@@ -984,7 +921,7 @@ export class VirtualizedList {
|
|
|
984
921
|
[vListOutlet]="itemDir?.templateRef"
|
|
985
922
|
[vListOutletContext]="forcedStickyCell.context"
|
|
986
923
|
></ng-container>
|
|
987
|
-
</
|
|
924
|
+
</sticky-header>
|
|
988
925
|
}
|
|
989
926
|
@if (gapSpacerStyle !== null) {
|
|
990
927
|
<view [style]="gapSpacerStyle"></view>
|
|
@@ -997,21 +934,39 @@ export class VirtualizedList {
|
|
|
997
934
|
first index moves. Measured at exactly 17px on device 2026-08-19; see
|
|
998
935
|
.claude/rules/list-geometry-feedback-loop.md. -->
|
|
999
936
|
@for (cell of windowCells; track cell.key) {
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
[vListOutlet]="itemDir?.templateRef"
|
|
1006
|
-
[vListOutletContext]="cell.context"
|
|
1007
|
-
></ng-container>
|
|
1008
|
-
@if (cell.separatorContext !== undefined) {
|
|
937
|
+
@if (cell.isSticky) {
|
|
938
|
+
<sticky-header
|
|
939
|
+
(layout)="handleCellLayout(cell.measure, $event)"
|
|
940
|
+
[style]="cellStyle"
|
|
941
|
+
>
|
|
1009
942
|
<ng-container
|
|
1010
|
-
[vListOutlet]="
|
|
1011
|
-
[vListOutletContext]="cell.
|
|
943
|
+
[vListOutlet]="itemDir?.templateRef"
|
|
944
|
+
[vListOutletContext]="cell.context"
|
|
1012
945
|
></ng-container>
|
|
1013
|
-
|
|
1014
|
-
|
|
946
|
+
@if (cell.separatorContext !== undefined) {
|
|
947
|
+
<ng-container
|
|
948
|
+
[vListOutlet]="separatorDir?.templateRef"
|
|
949
|
+
[vListOutletContext]="cell.separatorContext"
|
|
950
|
+
></ng-container>
|
|
951
|
+
}
|
|
952
|
+
</sticky-header>
|
|
953
|
+
} @else {
|
|
954
|
+
<view
|
|
955
|
+
(layout)="handleCellLayout(cell.measure, $event)"
|
|
956
|
+
[style]="cellStyle"
|
|
957
|
+
>
|
|
958
|
+
<ng-container
|
|
959
|
+
[vListOutlet]="itemDir?.templateRef"
|
|
960
|
+
[vListOutletContext]="cell.context"
|
|
961
|
+
></ng-container>
|
|
962
|
+
@if (cell.separatorContext !== undefined) {
|
|
963
|
+
<ng-container
|
|
964
|
+
[vListOutlet]="separatorDir?.templateRef"
|
|
965
|
+
[vListOutletContext]="cell.separatorContext"
|
|
966
|
+
></ng-container>
|
|
967
|
+
}
|
|
968
|
+
</view>
|
|
969
|
+
}
|
|
1015
970
|
}
|
|
1016
971
|
@if (trailingSpacerStyle !== null) {
|
|
1017
972
|
<view [style]="trailingSpacerStyle"></view>
|
|
@@ -1025,7 +980,7 @@ export class VirtualizedList {
|
|
|
1025
980
|
}
|
|
1026
981
|
</scroll-view>
|
|
1027
982
|
}
|
|
1028
|
-
`, isInline: true, dependencies: [{ kind: "directive", type: SymbioteHostPropsDirective, selector: "[symbioteHostProps]", inputs: ["symbioteHostProps"], exportAs: ["symbioteHost"] }, { kind: "directive", type: VListOutletDirective, selector: "[vListOutlet]", inputs: ["vListOutlet", "vListOutletContext"] }, { kind: "component", type: ViewHost, selector: "view" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
983
|
+
`, isInline: true, dependencies: [{ kind: "directive", type: SymbioteHostPropsDirective, selector: "[symbioteHostProps]", inputs: ["symbioteHostProps"], exportAs: ["symbioteHost"] }, { kind: "directive", type: VListOutletDirective, selector: "[vListOutlet]", inputs: ["vListOutlet", "vListOutletContext"] }, { kind: "component", type: ViewHost, selector: "view" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
1029
984
|
}
|
|
1030
985
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: VirtualizedList, decorators: [{
|
|
1031
986
|
type: Component,
|
|
@@ -1064,7 +1019,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.8", ngImpor
|
|
|
1064
1019
|
<view [style]="leadingSpacerStyle"></view>
|
|
1065
1020
|
}
|
|
1066
1021
|
@if (forcedStickyCell !== null) {
|
|
1067
|
-
<
|
|
1022
|
+
<sticky-header
|
|
1068
1023
|
(layout)="handleCellLayout(forcedStickyCell.measure, $event)"
|
|
1069
1024
|
[style]="cellStyle"
|
|
1070
1025
|
>
|
|
@@ -1072,27 +1027,45 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.8", ngImpor
|
|
|
1072
1027
|
[vListOutlet]="itemDir?.templateRef"
|
|
1073
1028
|
[vListOutletContext]="forcedStickyCell.context"
|
|
1074
1029
|
></ng-container>
|
|
1075
|
-
</
|
|
1030
|
+
</sticky-header>
|
|
1076
1031
|
}
|
|
1077
1032
|
@if (gapSpacerStyle !== null) {
|
|
1078
1033
|
<view [style]="gapSpacerStyle"></view>
|
|
1079
1034
|
}
|
|
1080
1035
|
@for (cell of windowCells; track cell.key) {
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
[vListOutlet]="itemDir?.templateRef"
|
|
1087
|
-
[vListOutletContext]="cell.context"
|
|
1088
|
-
></ng-container>
|
|
1089
|
-
@if (cell.separatorContext !== undefined) {
|
|
1036
|
+
@if (cell.isSticky) {
|
|
1037
|
+
<sticky-header
|
|
1038
|
+
(layout)="handleCellLayout(cell.measure, $event)"
|
|
1039
|
+
[style]="cellStyle"
|
|
1040
|
+
>
|
|
1090
1041
|
<ng-container
|
|
1091
|
-
[vListOutlet]="
|
|
1092
|
-
[vListOutletContext]="cell.
|
|
1042
|
+
[vListOutlet]="itemDir?.templateRef"
|
|
1043
|
+
[vListOutletContext]="cell.context"
|
|
1093
1044
|
></ng-container>
|
|
1094
|
-
|
|
1095
|
-
|
|
1045
|
+
@if (cell.separatorContext !== undefined) {
|
|
1046
|
+
<ng-container
|
|
1047
|
+
[vListOutlet]="separatorDir?.templateRef"
|
|
1048
|
+
[vListOutletContext]="cell.separatorContext"
|
|
1049
|
+
></ng-container>
|
|
1050
|
+
}
|
|
1051
|
+
</sticky-header>
|
|
1052
|
+
} @else {
|
|
1053
|
+
<view
|
|
1054
|
+
(layout)="handleCellLayout(cell.measure, $event)"
|
|
1055
|
+
[style]="cellStyle"
|
|
1056
|
+
>
|
|
1057
|
+
<ng-container
|
|
1058
|
+
[vListOutlet]="itemDir?.templateRef"
|
|
1059
|
+
[vListOutletContext]="cell.context"
|
|
1060
|
+
></ng-container>
|
|
1061
|
+
@if (cell.separatorContext !== undefined) {
|
|
1062
|
+
<ng-container
|
|
1063
|
+
[vListOutlet]="separatorDir?.templateRef"
|
|
1064
|
+
[vListOutletContext]="cell.separatorContext"
|
|
1065
|
+
></ng-container>
|
|
1066
|
+
}
|
|
1067
|
+
</view>
|
|
1068
|
+
}
|
|
1096
1069
|
}
|
|
1097
1070
|
@if (trailingSpacerStyle !== null) {
|
|
1098
1071
|
<view [style]="trailingSpacerStyle"></view>
|
|
@@ -1130,7 +1103,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.8", ngImpor
|
|
|
1130
1103
|
<view [style]="leadingSpacerStyle"></view>
|
|
1131
1104
|
}
|
|
1132
1105
|
@if (forcedStickyCell !== null) {
|
|
1133
|
-
<
|
|
1106
|
+
<sticky-header
|
|
1134
1107
|
(layout)="handleCellLayout(forcedStickyCell.measure, $event)"
|
|
1135
1108
|
[style]="cellStyle"
|
|
1136
1109
|
>
|
|
@@ -1138,7 +1111,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.8", ngImpor
|
|
|
1138
1111
|
[vListOutlet]="itemDir?.templateRef"
|
|
1139
1112
|
[vListOutletContext]="forcedStickyCell.context"
|
|
1140
1113
|
></ng-container>
|
|
1141
|
-
</
|
|
1114
|
+
</sticky-header>
|
|
1142
1115
|
}
|
|
1143
1116
|
@if (gapSpacerStyle !== null) {
|
|
1144
1117
|
<view [style]="gapSpacerStyle"></view>
|
|
@@ -1151,21 +1124,39 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.8", ngImpor
|
|
|
1151
1124
|
first index moves. Measured at exactly 17px on device 2026-08-19; see
|
|
1152
1125
|
.claude/rules/list-geometry-feedback-loop.md. -->
|
|
1153
1126
|
@for (cell of windowCells; track cell.key) {
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
[vListOutlet]="itemDir?.templateRef"
|
|
1160
|
-
[vListOutletContext]="cell.context"
|
|
1161
|
-
></ng-container>
|
|
1162
|
-
@if (cell.separatorContext !== undefined) {
|
|
1127
|
+
@if (cell.isSticky) {
|
|
1128
|
+
<sticky-header
|
|
1129
|
+
(layout)="handleCellLayout(cell.measure, $event)"
|
|
1130
|
+
[style]="cellStyle"
|
|
1131
|
+
>
|
|
1163
1132
|
<ng-container
|
|
1164
|
-
[vListOutlet]="
|
|
1165
|
-
[vListOutletContext]="cell.
|
|
1133
|
+
[vListOutlet]="itemDir?.templateRef"
|
|
1134
|
+
[vListOutletContext]="cell.context"
|
|
1166
1135
|
></ng-container>
|
|
1167
|
-
|
|
1168
|
-
|
|
1136
|
+
@if (cell.separatorContext !== undefined) {
|
|
1137
|
+
<ng-container
|
|
1138
|
+
[vListOutlet]="separatorDir?.templateRef"
|
|
1139
|
+
[vListOutletContext]="cell.separatorContext"
|
|
1140
|
+
></ng-container>
|
|
1141
|
+
}
|
|
1142
|
+
</sticky-header>
|
|
1143
|
+
} @else {
|
|
1144
|
+
<view
|
|
1145
|
+
(layout)="handleCellLayout(cell.measure, $event)"
|
|
1146
|
+
[style]="cellStyle"
|
|
1147
|
+
>
|
|
1148
|
+
<ng-container
|
|
1149
|
+
[vListOutlet]="itemDir?.templateRef"
|
|
1150
|
+
[vListOutletContext]="cell.context"
|
|
1151
|
+
></ng-container>
|
|
1152
|
+
@if (cell.separatorContext !== undefined) {
|
|
1153
|
+
<ng-container
|
|
1154
|
+
[vListOutlet]="separatorDir?.templateRef"
|
|
1155
|
+
[vListOutletContext]="cell.separatorContext"
|
|
1156
|
+
></ng-container>
|
|
1157
|
+
}
|
|
1158
|
+
</view>
|
|
1159
|
+
}
|
|
1169
1160
|
}
|
|
1170
1161
|
@if (trailingSpacerStyle !== null) {
|
|
1171
1162
|
<view [style]="trailingSpacerStyle"></view>
|