@symbiote-native/angular 0.7.0 → 1.0.0

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