@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.
Files changed (191) hide show
  1. package/README.md +3 -3
  2. package/bin/symbiote-angular-dev.cjs +20 -19
  3. package/build/angular/anchor-host-registry.js +2 -2
  4. package/build/angular/anchor-host-registry.js.map +1 -1
  5. package/build/angular/bootstrap.js +1 -1
  6. package/build/angular/bootstrap.js.map +1 -1
  7. package/build/angular/components/activity-indicator/index.android.d.ts +2 -1
  8. package/build/angular/components/activity-indicator/index.android.js +12 -4
  9. package/build/angular/components/activity-indicator/index.android.js.map +1 -1
  10. package/build/angular/components/activity-indicator/index.ios.d.ts +2 -1
  11. package/build/angular/components/activity-indicator/index.ios.js +8 -3
  12. package/build/angular/components/activity-indicator/index.ios.js.map +1 -1
  13. package/build/angular/components/activity-indicator/shared.d.ts +1 -1
  14. package/build/angular/components/activity-indicator/shared.js +4 -4
  15. package/build/angular/components/activity-indicator/shared.js.map +1 -1
  16. package/build/angular/components/button.d.ts +4 -2
  17. package/build/angular/components/button.js +41 -22
  18. package/build/angular/components/button.js.map +1 -1
  19. package/build/angular/components/flat-list/index.d.ts +6 -4
  20. package/build/angular/components/flat-list/index.js +80 -50
  21. package/build/angular/components/flat-list/index.js.map +1 -1
  22. package/build/angular/components/image/index.android.d.ts +8 -3
  23. package/build/angular/components/image/index.android.js +42 -10
  24. package/build/angular/components/image/index.android.js.map +1 -1
  25. package/build/angular/components/image/index.ios.d.ts +8 -3
  26. package/build/angular/components/image/index.ios.js +42 -10
  27. package/build/angular/components/image/index.ios.js.map +1 -1
  28. package/build/angular/components/image/shared.d.ts +4 -1
  29. package/build/angular/components/image/shared.js +51 -8
  30. package/build/angular/components/image/shared.js.map +1 -1
  31. package/build/angular/components/image-background.d.ts +2 -1
  32. package/build/angular/components/image-background.js +26 -10
  33. package/build/angular/components/image-background.js.map +1 -1
  34. package/build/angular/components/input-accessory-view/index.d.ts +9 -4
  35. package/build/angular/components/input-accessory-view/index.js +49 -10
  36. package/build/angular/components/input-accessory-view/index.js.map +1 -1
  37. package/build/angular/components/keyboard-avoiding-view/index.d.ts +3 -1
  38. package/build/angular/components/keyboard-avoiding-view/index.js +44 -12
  39. package/build/angular/components/keyboard-avoiding-view/index.js.map +1 -1
  40. package/build/angular/components/modal/index.d.ts +11 -8
  41. package/build/angular/components/modal/index.js +51 -29
  42. package/build/angular/components/modal/index.js.map +1 -1
  43. package/build/angular/components/pressable/index.d.ts +12 -6
  44. package/build/angular/components/pressable/index.js +66 -13
  45. package/build/angular/components/pressable/index.js.map +1 -1
  46. package/build/angular/components/refresh-control/index.d.ts +8 -4
  47. package/build/angular/components/refresh-control/index.js +54 -16
  48. package/build/angular/components/refresh-control/index.js.map +1 -1
  49. package/build/angular/components/safe-area-view/index.d.ts +9 -4
  50. package/build/angular/components/safe-area-view/index.js +48 -9
  51. package/build/angular/components/safe-area-view/index.js.map +1 -1
  52. package/build/angular/components/scroll-view/index.android.d.ts +2 -3
  53. package/build/angular/components/scroll-view/index.android.js +88 -61
  54. package/build/angular/components/scroll-view/index.android.js.map +1 -1
  55. package/build/angular/components/scroll-view/index.ios.d.ts +2 -3
  56. package/build/angular/components/scroll-view/index.ios.js +24 -18
  57. package/build/angular/components/scroll-view/index.ios.js.map +1 -1
  58. package/build/angular/components/scroll-view/projection.d.ts +35 -0
  59. package/build/angular/components/scroll-view/projection.js +279 -53
  60. package/build/angular/components/scroll-view/projection.js.map +1 -1
  61. package/build/angular/components/scroll-view/shared.d.ts +14 -5
  62. package/build/angular/components/scroll-view/shared.js +225 -75
  63. package/build/angular/components/scroll-view/shared.js.map +1 -1
  64. package/build/angular/components/scroll-view/sticky-header.js +21 -22
  65. package/build/angular/components/scroll-view/sticky-header.js.map +1 -1
  66. package/build/angular/components/section-list/index.d.ts +7 -1
  67. package/build/angular/components/section-list/index.js +91 -54
  68. package/build/angular/components/section-list/index.js.map +1 -1
  69. package/build/angular/components/switch/index.android.d.ts +7 -3
  70. package/build/angular/components/switch/index.android.js +52 -10
  71. package/build/angular/components/switch/index.android.js.map +1 -1
  72. package/build/angular/components/switch/index.ios.d.ts +7 -3
  73. package/build/angular/components/switch/index.ios.js +52 -10
  74. package/build/angular/components/switch/index.ios.js.map +1 -1
  75. package/build/angular/components/switch/shared.d.ts +13 -4
  76. package/build/angular/components/switch/shared.js +71 -7
  77. package/build/angular/components/switch/shared.js.map +1 -1
  78. package/build/angular/components/text-input.d.ts +11 -3
  79. package/build/angular/components/text-input.js +68 -8
  80. package/build/angular/components/text-input.js.map +1 -1
  81. package/build/angular/components/touchable/index.d.ts +40 -9
  82. package/build/angular/components/touchable/index.js +303 -67
  83. package/build/angular/components/touchable/index.js.map +1 -1
  84. package/build/angular/components/touchable-native-feedback/index.d.ts +6 -3
  85. package/build/angular/components/touchable-native-feedback/index.js +63 -27
  86. package/build/angular/components/touchable-native-feedback/index.js.map +1 -1
  87. package/build/angular/components/virtualized-list/directives.js +22 -18
  88. package/build/angular/components/virtualized-list/directives.js.map +1 -1
  89. package/build/angular/components/virtualized-list/index.d.ts +15 -4
  90. package/build/angular/components/virtualized-list/index.js +326 -94
  91. package/build/angular/components/virtualized-list/index.js.map +1 -1
  92. package/build/angular/components/virtualized-section-list/directives.js +12 -12
  93. package/build/angular/components/virtualized-section-list/directives.js.map +1 -1
  94. package/build/angular/components/virtualized-section-list/index.d.ts +19 -1
  95. package/build/angular/components/virtualized-section-list/index.js +93 -22
  96. package/build/angular/components/virtualized-section-list/index.js.map +1 -1
  97. package/build/angular/components.d.ts +2 -2
  98. package/build/angular/components.js +5 -0
  99. package/build/angular/components.js.map +1 -1
  100. package/build/angular/create-portal/index.js +34 -36
  101. package/build/angular/create-portal/index.js.map +1 -1
  102. package/build/angular/create-tunnel/index.js +19 -22
  103. package/build/angular/create-tunnel/index.js.map +1 -1
  104. package/build/angular/descriptor-to-angular/index.js +6 -4
  105. package/build/angular/descriptor-to-angular/index.js.map +1 -1
  106. package/build/angular/diagnostics.d.ts +33 -0
  107. package/build/angular/diagnostics.js +82 -0
  108. package/build/angular/diagnostics.js.map +1 -0
  109. package/build/angular/host-instance/index.js +1 -1
  110. package/build/angular/host-instance/index.js.map +1 -1
  111. package/build/angular/index.d.ts +9 -4
  112. package/build/angular/index.js +17 -3
  113. package/build/angular/index.js.map +1 -1
  114. package/build/angular/modules/animated/animated-leaf-binder.d.ts +1 -7
  115. package/build/angular/modules/animated/animated-leaf-binder.js +22 -72
  116. package/build/angular/modules/animated/animated-leaf-binder.js.map +1 -1
  117. package/build/angular/modules/animated/create-animated-component.d.ts +11 -6
  118. package/build/angular/modules/animated/create-animated-component.js +106 -55
  119. package/build/angular/modules/animated/create-animated-component.js.map +1 -1
  120. package/build/angular/modules/status-bar/index.js +4 -4
  121. package/build/angular/modules/status-bar/index.js.map +1 -1
  122. package/build/angular/primitives/index.d.ts +20 -3
  123. package/build/angular/primitives/index.js +84 -48
  124. package/build/angular/primitives/index.js.map +1 -1
  125. package/build/angular/primitives/shared.d.ts +48 -2
  126. package/build/angular/primitives/shared.js +158 -11
  127. package/build/angular/primitives/shared.js.map +1 -1
  128. package/build/angular/render/index.js +69 -38
  129. package/build/angular/render/index.js.map +1 -1
  130. package/build/angular/renderer/index.d.ts +1 -0
  131. package/build/angular/renderer/index.js +37 -2
  132. package/build/angular/renderer/index.js.map +1 -1
  133. package/build/angular/services/color-scheme.service.js +3 -3
  134. package/build/angular/services/color-scheme.service.js.map +1 -1
  135. package/build/angular/services/window-dimensions.service.js +4 -4
  136. package/build/angular/services/window-dimensions.service.js.map +1 -1
  137. package/package.json +10 -8
  138. package/src/anchor-host-registry.ts +2 -2
  139. package/src/bootstrap.ts +4 -1
  140. package/src/components/activity-indicator/index.android.ts +8 -1
  141. package/src/components/activity-indicator/index.ios.ts +4 -0
  142. package/src/components/activity-indicator/shared.ts +32 -13
  143. package/src/components/button.ts +46 -19
  144. package/src/components/flat-list/index.ts +122 -62
  145. package/src/components/image/index.android.ts +58 -7
  146. package/src/components/image/index.ios.ts +58 -7
  147. package/src/components/image/shared.ts +92 -21
  148. package/src/components/image-background.ts +29 -9
  149. package/src/components/input-accessory-view/index.ts +67 -12
  150. package/src/components/keyboard-avoiding-view/index.ts +61 -15
  151. package/src/components/modal/index.ts +72 -33
  152. package/src/components/pressable/index.ts +117 -28
  153. package/src/components/refresh-control/index.ts +74 -18
  154. package/src/components/safe-area-view/index.ts +64 -10
  155. package/src/components/scroll-view/index.android.ts +62 -47
  156. package/src/components/scroll-view/index.ios.ts +19 -17
  157. package/src/components/scroll-view/projection.ts +363 -64
  158. package/src/components/scroll-view/shared.ts +312 -90
  159. package/src/components/scroll-view/sticky-header.ts +36 -24
  160. package/src/components/section-list/index.ts +86 -53
  161. package/src/components/switch/index.android.ts +60 -8
  162. package/src/components/switch/index.ios.ts +60 -8
  163. package/src/components/switch/shared.ts +93 -6
  164. package/src/components/text-input.ts +95 -13
  165. package/src/components/touchable/index.ts +348 -64
  166. package/src/components/touchable-native-feedback/index.ts +94 -31
  167. package/src/components/virtualized-list/directives.ts +19 -5
  168. package/src/components/virtualized-list/index.ts +380 -85
  169. package/src/components/virtualized-section-list/directives.ts +6 -3
  170. package/src/components/virtualized-section-list/index.ts +146 -33
  171. package/src/components.ts +9 -1
  172. package/src/create-portal/index.ts +31 -31
  173. package/src/create-tunnel/index.ts +16 -17
  174. package/src/descriptor-to-angular/index.ts +29 -8
  175. package/src/diagnostics.ts +138 -0
  176. package/src/host-instance/index.ts +9 -3
  177. package/src/index.ts +61 -3
  178. package/src/modules/animated/animated-leaf-binder.ts +28 -77
  179. package/src/modules/animated/create-animated-component.ts +115 -47
  180. package/src/modules/app-registry/index.ts +9 -9
  181. package/src/modules/status-bar/index.ts +13 -4
  182. package/src/primitives/index.ts +47 -2
  183. package/src/primitives/shared.ts +170 -10
  184. package/src/render/index.ts +77 -39
  185. package/src/renderer/index.ts +76 -14
  186. package/src/services/color-scheme.service.ts +3 -1
  187. package/src/services/window-dimensions.service.ts +11 -4
  188. package/build/angular/modules/animated/animated-leaf-binder/index.d.ts +0 -14
  189. package/build/angular/modules/animated/animated-leaf-binder/index.js +0 -91
  190. package/build/angular/modules/animated/animated-leaf-binder/index.js.map +0 -1
  191. 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
- get foldedAccessibility() {
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
- this.renderVersion += 1;
325
- this.cdr.markForCheck();
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({ distanceFromStart: effect.distanceFromStart });
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 ? new Set(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 + (hasHeader ? 1 : 0),
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 ? this.spacerStyle(plan.leadingExtent) : null;
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 ? this.spacerStyle(plan.trailingExtent) : null;
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
- const item = this.getItem(this.data, planned.index);
495
- const context = {
496
- $implicit: item,
497
- index: planned.index,
498
- separators: this.makeSeparators(planned.index),
499
- };
500
- let separatorContext;
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' || key === 'leadingItem' || key === 'trailingItem')
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
- dlog(`Angular VirtualizedList cell ${index} measured length=${length}`);
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, { ...this.separatorOverrides.get(gapIndex), ...patch });
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({ kind: 'scroll-to-end', animated: params?.animated ?? true });
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.5", ngImport: i0, type: VirtualizedList, deps: [], target: i0.ɵɵFactoryTarget.Component });
651
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.5", 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 }], ngImport: i0, template: `
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]="foldedAccessibility.importantForAccessibility"
667
- [accessibilityLiveRegion]="foldedAccessibility.accessibilityLiveRegion"
668
- [screenReaderFocusable]="foldedAccessibility.screenReaderFocusable"
669
- [accessibilityViewIsModal]="foldedAccessibility.accessibilityViewIsModal"
670
- [accessibilityElementsHidden]="foldedAccessibility.accessibilityElementsHidden"
671
- [accessibilityIgnoresInvertColors]="foldedAccessibility.accessibilityIgnoresInvertColors"
672
- [accessibilityLanguage]="foldedAccessibility.accessibilityLanguage"
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 (layout)="handleCellLayout(cell.measure, $event)" [style]="cellStyle">
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
- </symbiote-view>
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
- </symbiote-view>
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.5", ngImport: i0, type: VirtualizedList, decorators: [{
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]="foldedAccessibility.importantForAccessibility"
774
- [accessibilityLiveRegion]="foldedAccessibility.accessibilityLiveRegion"
775
- [screenReaderFocusable]="foldedAccessibility.screenReaderFocusable"
776
- [accessibilityViewIsModal]="foldedAccessibility.accessibilityViewIsModal"
777
- [accessibilityElementsHidden]="foldedAccessibility.accessibilityElementsHidden"
778
- [accessibilityIgnoresInvertColors]="foldedAccessibility.accessibilityIgnoresInvertColors"
779
- [accessibilityLanguage]="foldedAccessibility.accessibilityLanguage"
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 (layout)="handleCellLayout(cell.measure, $event)" [style]="cellStyle">
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
- </symbiote-view>
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
- </symbiote-view>
842
- }
1073
+ }
1074
+ </symbiote-view>
843
1075
  }
844
1076
  @if (trailingSpacerStyle !== null) {
845
1077
  <symbiote-view [style]="trailingSpacerStyle"></symbiote-view>