@symbiote-native/angular 0.6.0 → 0.6.2

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 (193) hide show
  1. package/README.md +1 -1
  2. package/babel-register-composed.cjs +11 -3
  3. package/build/angular/anchor-host-registry.d.ts +2 -0
  4. package/build/angular/anchor-host-registry.js +84 -0
  5. package/build/angular/anchor-host-registry.js.map +1 -0
  6. package/build/angular/components/input-accessory-view/index.d.ts +2 -1
  7. package/build/angular/components/input-accessory-view/index.js +28 -39
  8. package/build/angular/components/input-accessory-view/index.js.map +1 -1
  9. package/build/angular/components/scroll-view/projection.js +70 -48
  10. package/build/angular/components/scroll-view/projection.js.map +1 -1
  11. package/build/angular/components/scroll-view/shared.js +28 -25
  12. package/build/angular/components/scroll-view/shared.js.map +1 -1
  13. package/build/angular/components/scroll-view/sticky-header.d.ts +6 -7
  14. package/build/angular/components/scroll-view/sticky-header.js +82 -65
  15. package/build/angular/components/scroll-view/sticky-header.js.map +1 -1
  16. package/build/angular/components/touchable/index.d.ts +2 -5
  17. package/build/angular/components/touchable/index.js +36 -56
  18. package/build/angular/components/touchable/index.js.map +1 -1
  19. package/build/angular/components/virtualized-list/index.d.ts +5 -20
  20. package/build/angular/components/virtualized-list/index.js +145 -275
  21. package/build/angular/components/virtualized-list/index.js.map +1 -1
  22. package/build/angular/components/virtualized-section-list/index.js +2 -4
  23. package/build/angular/components/virtualized-section-list/index.js.map +1 -1
  24. package/build/angular/index.d.ts +2 -1
  25. package/build/angular/index.js +6 -1
  26. package/build/angular/index.js.map +1 -1
  27. package/build/angular/renderer/index.d.ts +0 -1
  28. package/build/angular/renderer/index.js +5 -60
  29. package/build/angular/renderer/index.js.map +1 -1
  30. package/package.json +8 -4
  31. package/src/anchor-host-registry.ts +88 -0
  32. package/src/components/input-accessory-view/index.ts +30 -39
  33. package/src/components/scroll-view/projection.ts +76 -46
  34. package/src/components/scroll-view/shared.ts +27 -19
  35. package/src/components/scroll-view/sticky-header.ts +89 -64
  36. package/src/components/touchable/index.ts +52 -66
  37. package/src/components/virtualized-list/index.ts +157 -368
  38. package/src/components/virtualized-section-list/index.ts +6 -3
  39. package/src/index.ts +6 -1
  40. package/src/renderer/index.ts +5 -63
  41. package/build/bootstrap.d.ts +0 -7
  42. package/build/bootstrap.js +0 -14
  43. package/build/components/activity-indicator/index.android.d.ts +0 -9
  44. package/build/components/activity-indicator/index.android.js +0 -64
  45. package/build/components/activity-indicator/index.d.ts +0 -1
  46. package/build/components/activity-indicator/index.ios.d.ts +0 -5
  47. package/build/components/activity-indicator/index.ios.js +0 -64
  48. package/build/components/activity-indicator/index.js +0 -3
  49. package/build/components/activity-indicator/shared.d.ts +0 -64
  50. package/build/components/activity-indicator/shared.js +0 -424
  51. package/build/components/button.d.ts +0 -60
  52. package/build/components/button.js +0 -456
  53. package/build/components/flat-list/index.d.ts +0 -119
  54. package/build/components/flat-list/index.js +0 -691
  55. package/build/components/image/index.android.d.ts +0 -7
  56. package/build/components/image/index.android.js +0 -88
  57. package/build/components/image/index.d.ts +0 -1
  58. package/build/components/image/index.ios.d.ts +0 -7
  59. package/build/components/image/index.ios.js +0 -88
  60. package/build/components/image/index.js +0 -2
  61. package/build/components/image/shared.d.ts +0 -101
  62. package/build/components/image/shared.js +0 -405
  63. package/build/components/image-background.d.ts +0 -78
  64. package/build/components/image-background.js +0 -583
  65. package/build/components/input-accessory-view/index.d.ts +0 -56
  66. package/build/components/input-accessory-view/index.js +0 -403
  67. package/build/components/keyboard-avoiding-view/index.d.ts +0 -79
  68. package/build/components/keyboard-avoiding-view/index.js +0 -504
  69. package/build/components/modal/index.d.ts +0 -89
  70. package/build/components/modal/index.js +0 -529
  71. package/build/components/pressable/index.d.ts +0 -120
  72. package/build/components/pressable/index.js +0 -639
  73. package/build/components/refresh-control/index.d.ts +0 -83
  74. package/build/components/refresh-control/index.js +0 -509
  75. package/build/components/safe-area-view/index.d.ts +0 -54
  76. package/build/components/safe-area-view/index.js +0 -388
  77. package/build/components/scroll-view/index.android.d.ts +0 -16
  78. package/build/components/scroll-view/index.android.js +0 -228
  79. package/build/components/scroll-view/index.d.ts +0 -3
  80. package/build/components/scroll-view/index.ios.d.ts +0 -6
  81. package/build/components/scroll-view/index.ios.js +0 -113
  82. package/build/components/scroll-view/index.js +0 -5
  83. package/build/components/scroll-view/projection.d.ts +0 -32
  84. package/build/components/scroll-view/projection.js +0 -311
  85. package/build/components/scroll-view/shared.d.ts +0 -225
  86. package/build/components/scroll-view/shared.js +0 -757
  87. package/build/components/scroll-view/sticky-header.d.ts +0 -38
  88. package/build/components/scroll-view/sticky-header.js +0 -257
  89. package/build/components/section-list/index.d.ts +0 -113
  90. package/build/components/section-list/index.js +0 -768
  91. package/build/components/switch/index.android.d.ts +0 -17
  92. package/build/components/switch/index.android.js +0 -107
  93. package/build/components/switch/index.d.ts +0 -1
  94. package/build/components/switch/index.ios.d.ts +0 -16
  95. package/build/components/switch/index.ios.js +0 -106
  96. package/build/components/switch/index.js +0 -3
  97. package/build/components/switch/shared.d.ts +0 -37
  98. package/build/components/switch/shared.js +0 -176
  99. package/build/components/text-input.d.ts +0 -150
  100. package/build/components/text-input.js +0 -821
  101. package/build/components/touchable/index.d.ts +0 -241
  102. package/build/components/touchable/index.js +0 -1672
  103. package/build/components/touchable-native-feedback/index.d.ts +0 -80
  104. package/build/components/touchable-native-feedback/index.js +0 -545
  105. package/build/components/virtualized-list/directives.d.ts +0 -41
  106. package/build/components/virtualized-list/directives.js +0 -249
  107. package/build/components/virtualized-list/index.d.ts +0 -265
  108. package/build/components/virtualized-list/index.js +0 -1343
  109. package/build/components/virtualized-section-list/directives.d.ts +0 -28
  110. package/build/components/virtualized-section-list/directives.js +0 -148
  111. package/build/components/virtualized-section-list/index.d.ts +0 -175
  112. package/build/components/virtualized-section-list/index.js +0 -854
  113. package/build/components.d.ts +0 -39
  114. package/build/components.js +0 -37
  115. package/build/create-portal/index.d.ts +0 -17
  116. package/build/create-portal/index.js +0 -128
  117. package/build/create-tunnel/index.d.ts +0 -29
  118. package/build/create-tunnel/index.js +0 -181
  119. package/build/descriptor-to-angular/index.d.ts +0 -19
  120. package/build/descriptor-to-angular/index.js +0 -186
  121. package/build/host-instance/index.d.ts +0 -2
  122. package/build/host-instance/index.js +0 -36
  123. package/build/index.d.ts +0 -21
  124. package/build/index.js +0 -40
  125. package/build/modules/animated/animated-leaf-binder/index.d.ts +0 -14
  126. package/build/modules/animated/animated-leaf-binder/index.js +0 -90
  127. package/build/modules/animated/animated-leaf-binder.d.ts +0 -14
  128. package/build/modules/animated/animated-leaf-binder.js +0 -90
  129. package/build/modules/animated/create-animated-component.d.ts +0 -48
  130. package/build/modules/animated/create-animated-component.js +0 -425
  131. package/build/modules/animated/index.d.ts +0 -51
  132. package/build/modules/animated/index.js +0 -58
  133. package/build/modules/app-registry/index.d.ts +0 -7
  134. package/build/modules/app-registry/index.js +0 -22
  135. package/build/modules/status-bar/index.d.ts +0 -17
  136. package/build/modules/status-bar/index.js +0 -121
  137. package/build/primitives/index.d.ts +0 -32
  138. package/build/primitives/index.js +0 -386
  139. package/build/primitives/shared.d.ts +0 -83
  140. package/build/primitives/shared.js +0 -230
  141. package/build/render/index.d.ts +0 -11
  142. package/build/render/index.js +0 -170
  143. package/build/renderer/index.d.ts +0 -38
  144. package/build/renderer/index.js +0 -355
  145. package/build/services/color-scheme.service.d.ts +0 -13
  146. package/build/services/color-scheme.service.js +0 -72
  147. package/build/services/index.d.ts +0 -2
  148. package/build/services/index.js +0 -2
  149. package/build/services/window-dimensions.service.d.ts +0 -13
  150. package/build/services/window-dimensions.service.js +0 -83
  151. package/src/__tests__/responder-change-detection.test.ts +0 -102
  152. package/src/__tests__/responder-nested-cd.test.ts +0 -113
  153. package/src/angular-gaps.test.ts +0 -142
  154. package/src/angular-imports.test.ts +0 -100
  155. package/src/components/activity-indicator/activity-indicator-class-style.test.ts +0 -44
  156. package/src/components/activity-indicator/activity-indicator.test.ts +0 -131
  157. package/src/components/flat-list/column-wrapper-style-class.test.ts +0 -116
  158. package/src/components/flat-list/flat-list-array-style.test.ts +0 -131
  159. package/src/components/flat-list/flat-list-class-style.test.ts +0 -74
  160. package/src/components/flat-list/flat-list-scroll-containment.test.ts +0 -107
  161. package/src/components/flat-list/flat-list.test.ts +0 -99
  162. package/src/components/image/image-class-style.test.ts +0 -46
  163. package/src/components/image-background-class-style.test.ts +0 -48
  164. package/src/components/image-background-style-class.test.ts +0 -79
  165. package/src/components/input-accessory-view/input-accessory-view.test.ts +0 -43
  166. package/src/components/keyboard-avoiding-view/keyboard-avoiding-view.test.ts +0 -54
  167. package/src/components/modal/modal.test.ts +0 -43
  168. package/src/components/pressable/pressable.test.ts +0 -85
  169. package/src/components/refresh-control/refresh-control.test.ts +0 -41
  170. package/src/components/safe-area-view/safe-area-view.test.ts +0 -43
  171. package/src/components/scroll-view/android-scroll-view-axis-projection.test.ts +0 -123
  172. package/src/components/scroll-view/content-container-style-class.test.ts +0 -75
  173. package/src/components/scroll-view/scroll-view-class-style.test.ts +0 -134
  174. package/src/components/scroll-view/scroll-view-projection.test.ts +0 -194
  175. package/src/components/switch/android-switch-class.test.ts +0 -42
  176. package/src/components/switch/switch.test.ts +0 -92
  177. package/src/components/text-input-class-style.test.ts +0 -54
  178. package/src/components/touchable/touchable.test.ts +0 -102
  179. package/src/components/touchable-native-feedback/touchable-native-feedback.test.ts +0 -49
  180. package/src/components/virtualized-list/virtualized-list-class-style.test.ts +0 -76
  181. package/src/components/virtualized-section-list/virtualized-section-list-class-style.test.ts +0 -60
  182. package/src/create-portal/create-portal.test.ts +0 -90
  183. package/src/create-tunnel/create-tunnel.test.ts +0 -120
  184. package/src/descriptor-to-angular/descriptor-outlet.test.ts +0 -94
  185. package/src/host-instance/host-instance.test.ts +0 -49
  186. package/src/modules/animated/animated-leaf-binder/animated-leaf-binder.test.ts +0 -116
  187. package/src/modules/animated/animated-leaf-binder.test.ts +0 -116
  188. package/src/modules/animated/animated-scroll-view.test.ts +0 -102
  189. package/src/modules/app-registry/app-registry.test.ts +0 -106
  190. package/src/modules/status-bar/status-bar.test.ts +0 -50
  191. package/src/render/render.test.ts +0 -147
  192. package/src/renderer/renderer.test.ts +0 -202
  193. package/src/services/services.test.ts +0 -163
@@ -51,31 +51,26 @@ import {
51
51
  FIRST_INDEX,
52
52
  INVERTED_X_STYLE,
53
53
  INVERTED_Y_STYLE,
54
- NO_CONTENT_LENGTH_SENT,
55
- averageMeasuredLength,
56
54
  buildListPlan,
57
- buildOffsets,
58
55
  buildViewabilityPairs,
59
- computeEndReached,
60
- computeStartReached,
61
- computeViewableSet,
62
- computeWindow,
63
- diffViewable,
64
- highestMeasuredIndex,
65
- maxMinimumViewTime,
66
- offsetForIndex,
56
+ createInitialListState,
57
+ isSeparatorGapInRange,
58
+ listEffectSignature,
67
59
  readLayoutLength,
68
60
  readScrollOffset,
61
+ reduceList,
69
62
  resolveAccessibilityProps,
70
- throttleWindow,
63
+ resolveItemKey,
71
64
  type IAccessibilityProps,
72
65
  type IAccessibilityStateValue,
73
66
  type IAriaProps,
74
- type ICellLayout,
67
+ type IListAction,
68
+ type IListEffect,
69
+ type IListReducerInputs,
70
+ type IListState,
75
71
  type ISeparatorProps,
76
72
  type ISeparators,
77
73
  type IScrollViewHandle,
78
- type IViewToken,
79
74
  type IViewabilityConfig,
80
75
  type IViewabilityConfigCallbackPair,
81
76
  type IViewableItemsChangedInfo,
@@ -193,19 +188,6 @@ export type IVirtualizedListInputs<ItemT> = Omit<
193
188
  | 'onAccessibilityEscape'
194
189
  >;
195
190
 
196
- // The windowing snapshot recomputed once per CD in ngDoCheck. Plain object (not a signal/computed):
197
- // the OnPush + markForCheck cycle re-runs ngDoCheck whenever scroll / layout / measure change.
198
- interface IMetricsCore {
199
- count: number;
200
- offsets: number[];
201
- lengths: number[];
202
- total: number;
203
- first: number;
204
- last: number;
205
- target: { first: number; last: number };
206
- averageLength: number;
207
- }
208
-
209
191
  // One in-window cell, assembled in ngDoCheck and stamped by the template's @for. The
210
192
  // context object is fresh each pass; VListOutletDirective folds it onto the live embedded view, so
211
193
  // the cell view is reused (the windowing recompute does not tear cells down).
@@ -460,35 +442,19 @@ export class VirtualizedList<ItemT = unknown>
460
442
  // as contentOffset). A fresh object identity each push so the commit re-applies a repeated value.
461
443
  commandedOffset: { x: number; y: number } | undefined = undefined;
462
444
 
463
- // --- reactive scalars (mutated by the native callbacks; markForCheck re-runs the CD) ---
464
- private scrollOffset = EMPTY_OFFSET;
465
- private viewportLength = EMPTY_OFFSET;
466
- private measureVersion = EMPTY_OFFSET;
445
+ // The one folded state cell (the Angular twin of React's stateRef): scroll offset, viewport,
446
+ // measured lengths, committed window, edge/viewability dedup, MVCP anchor — all in IListState,
447
+ // driven by the shared reduceList. `renderVersion` bumps whenever a transition changes render
448
+ // state, so ngDoCheck's recompute-dedup can detect it (listState is a plain object, not tracked).
449
+ private readonly listState: IListState<ItemT> = createInitialListState<ItemT>();
450
+ private renderVersion = EMPTY_OFFSET;
467
451
 
468
- // --- non-render state (the Angular twin of refs that never trigger a render) ---
469
- private metricsCore: IMetricsCore = {
470
- count: EMPTY_OFFSET,
471
- offsets: [],
472
- lengths: [],
473
- total: EMPTY_OFFSET,
474
- first: FIRST_INDEX,
475
- last: -1,
476
- target: { first: FIRST_INDEX, last: -1 },
477
- averageLength: EMPTY_OFFSET,
478
- };
479
- private readonly measured = new Map<number, number>();
480
452
  private readonly cellMeasures = new Map<number, (event: ISymbioteEvent) => void>();
481
453
  private readonly separatorOverrides = new Map<number, Partial<ISeparatorProps<unknown>>>();
482
- // The previously committed window, grown by at most maxToRenderPerBatch per tick (no snap).
483
- private committedWindow: { first: number; last: number } = { first: FIRST_INDEX, last: -1 };
484
- private sentEndForContentLength = NO_CONTENT_LENGTH_SENT;
485
- private sentStartForContentLength = NO_CONTENT_LENGTH_SENT;
486
- private lastViewable = new Map<string, IViewToken<ItemT>>();
454
+ // The adapter-owned debounce (minimumViewTime) and incremental-fill timers; the reducer only
455
+ // hands back a delay.
487
456
  private viewableTimer: ReturnType<typeof setTimeout> | null = null;
488
457
  private batchTimer: ReturnType<typeof setTimeout> | null = null;
489
- private hasInteracted = false;
490
- private appliedInitialScroll = false;
491
- private firstVisibleKey: string | null = null;
492
458
  // Dedupes the after-commit effects: they run only when the windowing signature changed.
493
459
  private lastEffectSignature = '';
494
460
  // Dedupes ngDoCheck's own recompute. WITHOUT this, recomputeView() unconditionally rebuilds
@@ -586,22 +552,18 @@ export class VirtualizedList<ItemT = unknown>
586
552
  const offset = readScrollOffset(event, this.isHorizontal);
587
553
  if (offset === undefined) return;
588
554
  dlog(`Angular VirtualizedList onScroll offset=${offset}`);
589
- // First scroll is the interaction that ungates waitForInteraction configs.
590
- this.hasInteracted = true;
591
- this.scrollOffset = offset;
592
555
  // A real native scroll supersedes any pending commanded offset.
593
556
  this.commandedOffset = undefined;
557
+ this.dispatch({ kind: 'scroll', offset });
594
558
  // Compose, don't clobber: internal windowing ran first, now the user's onScroll.
595
559
  this.onScroll?.(event);
596
- this.cdr.markForCheck();
597
560
  };
598
561
 
599
562
  onLayoutTick = (event: ISymbioteEvent): void => {
600
563
  const length = readLayoutLength(event, this.isHorizontal);
601
564
  if (length === undefined) return;
602
565
  dlog(`Angular VirtualizedList onLayout viewport=${length}`);
603
- this.viewportLength = length;
604
- this.cdr.markForCheck();
566
+ this.dispatch({ kind: 'layout', length });
605
567
  };
606
568
 
607
569
  // RefreshControl's refresh and ScrollView's accessibility events are real @Output()s now, bound
@@ -629,14 +591,110 @@ export class VirtualizedList<ItemT = unknown>
629
591
 
630
592
  private keyFor = (index: number): string => {
631
593
  const item = this.getItem(this.data, index);
632
- return this.keyExtractor ? this.keyExtractor(item, index) : String(index);
594
+ return resolveItemKey(item, index, this.keyExtractor);
633
595
  };
634
596
 
597
+ // The reducer inputs, folded off the @Input()s each call. The edge/viewability listeners map to
598
+ // `.observed` (Angular's "is anyone bound to this @Output()"), so the reducer emits only when the
599
+ // consumer listens — the same on-demand gating the prop-callback era had.
600
+ private buildInputs(): IListReducerInputs<ItemT> {
601
+ return {
602
+ data: this.data,
603
+ getItem: this.getItem,
604
+ getItemCount: this.getItemCount,
605
+ keyExtractor: this.keyExtractor,
606
+ getItemLayout: this.getItemLayout,
607
+ horizontal: this.isHorizontal,
608
+ windowSize: this.windowSizeValue,
609
+ initialNumToRender: this.initialNumToRenderValue,
610
+ maxToRenderPerBatch: this.maxToRenderPerBatchValue,
611
+ updateCellsBatchingPeriod: this.updateCellsBatchingPeriodValue,
612
+ onEndReachedThreshold: this.onEndReachedThresholdValue,
613
+ onStartReachedThreshold: this.onStartReachedThresholdValue,
614
+ onEndReachedActive: this.endReached.observed,
615
+ onStartReachedActive: this.startReached.observed,
616
+ viewabilityPairs: buildViewabilityPairs(
617
+ this.viewableItemsChanged.observed
618
+ ? (info: IViewableItemsChangedInfo<ItemT>): void => this.viewableItemsChanged.emit(info)
619
+ : undefined,
620
+ this.viewabilityConfig,
621
+ this.viewabilityConfigCallbackPairs,
622
+ ),
623
+ maintainVisibleContentPosition: this.maintainVisibleContentPosition,
624
+ initialScrollIndex: this.initialScrollIndex,
625
+ };
626
+ }
627
+
628
+ // Map a native event / imperative call to an action, run the returned effects, and mark the view
629
+ // dirty when render state changed (the native callbacks fire outside Angular's own bindings).
630
+ private dispatch(action: IListAction<ItemT>): void {
631
+ const inputs = this.buildInputs();
632
+ const result = reduceList(this.listState, action, inputs);
633
+ this.runEffects(result.effects, inputs);
634
+ if (result.changed) {
635
+ this.renderVersion += 1;
636
+ this.cdr.markForCheck();
637
+ }
638
+ }
639
+
640
+ private runEffects(effects: IListEffect<ItemT>[], inputs: IListReducerInputs<ItemT>): void {
641
+ for (const effect of effects) {
642
+ switch (effect.kind) {
643
+ case 'scroll-to':
644
+ this.scrollToPixel(effect.offset, effect.animated);
645
+ break;
646
+ case 'fire-end-reached':
647
+ this.endReached.emit({ distanceFromEnd: effect.distanceFromEnd });
648
+ break;
649
+ case 'fire-start-reached':
650
+ this.startReached.emit({ distanceFromStart: effect.distanceFromStart });
651
+ break;
652
+ case 'fire-scroll-to-index-failed':
653
+ this.scrollToIndexFailed.emit({
654
+ index: effect.index,
655
+ highestMeasuredFrameIndex: effect.highestMeasuredFrameIndex,
656
+ averageItemLength: effect.averageItemLength,
657
+ });
658
+ break;
659
+ case 'schedule-refill': {
660
+ if (this.batchTimer !== null) clearTimeout(this.batchTimer);
661
+ this.batchTimer = setTimeout(() => {
662
+ this.batchTimer = null;
663
+ this.dispatch({ kind: 'batch-tick' });
664
+ }, effect.delay);
665
+ break;
666
+ }
667
+ case 'fire-viewable': {
668
+ const pairs = inputs.viewabilityPairs;
669
+ const info = effect.info;
670
+ const map = effect.map;
671
+ const fire = (): void => {
672
+ for (const pair of pairs) pair.onViewableItemsChanged(info);
673
+ this.dispatch({ kind: 'viewable-fired', map });
674
+ };
675
+ if (this.viewableTimer !== null) {
676
+ clearTimeout(this.viewableTimer);
677
+ this.viewableTimer = null;
678
+ }
679
+ if (effect.delay > EMPTY_OFFSET) {
680
+ this.viewableTimer = setTimeout(() => {
681
+ this.viewableTimer = null;
682
+ fire();
683
+ }, effect.delay);
684
+ } else {
685
+ fire();
686
+ }
687
+ break;
688
+ }
689
+ }
690
+ }
691
+ }
692
+
635
693
  // Once-per-CD recompute, BEFORE the template bindings are read (so the freshly computed
636
694
  // template-bound fields render this pass — computing them in ngAfterContentChecked instead would
637
- // trip ExpressionChangedAfterItHasBeenChecked). recomputeMetrics owns the controlled committedWindow
638
- // throttle (the side effect React runs during render); ngDoCheck is its single evaluation point, so
639
- // the window grows exactly one step per CD. The projected templates (ContentChild) are resolved
695
+ // trip ExpressionChangedAfterItHasBeenChecked). The dedup guard runs refresh-metrics (which owns
696
+ // the controlled committedWindow throttle) only when something relevant changed, so the window
697
+ // grows exactly one step per meaningful CD. The projected templates (ContentChild) are resolved
640
698
  // from the second CD on; the first paint (count/viewport still 0) corrects on the layout-driven CD.
641
699
  ngDoCheck(): void {
642
700
  const recomputeInputs: unknown[] = [
@@ -653,9 +711,8 @@ export class VirtualizedList<ItemT = unknown>
653
711
  this.maintainVisibleContentPosition,
654
712
  this.style,
655
713
  this.contentContainerStyle,
656
- this.scrollOffset,
657
- this.viewportLength,
658
- this.measureVersion,
714
+ // Folds scroll / layout / measure / batch-tick: dispatch bumps renderVersion on any change.
715
+ this.renderVersion,
659
716
  this.headerDir !== undefined,
660
717
  this.footerDir !== undefined,
661
718
  this.emptyDir !== undefined,
@@ -670,21 +727,18 @@ export class VirtualizedList<ItemT = unknown>
670
727
  ) {
671
728
  return;
672
729
  }
673
- this.recomputeMetrics();
730
+ // The single derive-per-CD: recompute the window off the current state before the view reads it.
731
+ reduceList(this.listState, { kind: 'refresh-metrics' }, this.buildInputs());
674
732
  this.recomputeView();
675
733
  }
676
734
 
677
735
  ngAfterViewChecked(): void {
678
- const m = this.metricsCore;
679
- const signature = `${this.scrollOffset}|${this.viewportLength}|${m.first}|${m.last}|${m.count}|${m.total}|${this.measureVersion}`;
736
+ const signature = listEffectSignature(this.listState);
680
737
  if (signature === this.lastEffectSignature) return;
681
738
  this.lastEffectSignature = signature;
682
- this.effectBatchFill();
683
- this.effectEndReached();
684
- this.effectStartReached();
685
- this.effectViewability();
686
- this.effectInitialScroll();
687
- this.effectMaintainVisibleContentPosition();
739
+ const inputs = this.buildInputs();
740
+ const result = reduceList(this.listState, { kind: 'commit' }, inputs);
741
+ this.runEffects(result.effects, inputs);
688
742
  }
689
743
 
690
744
  ngOnDestroy(): void {
@@ -692,54 +746,8 @@ export class VirtualizedList<ItemT = unknown>
692
746
  if (this.batchTimer !== null) clearTimeout(this.batchTimer);
693
747
  }
694
748
 
695
- private recomputeMetrics(): void {
696
- const count = this.getItemCount(this.data);
697
- const gil = this.getItemLayout;
698
- const data = this.data;
699
- const fixedLayout =
700
- gil !== undefined
701
- ? (index: number): ICellLayout => {
702
- const layout = gil(data, index);
703
- return { length: layout.length, offset: layout.offset };
704
- }
705
- : undefined;
706
- const averageLength =
707
- fixedLayout !== undefined
708
- ? count > FIRST_INDEX
709
- ? fixedLayout(FIRST_INDEX).length
710
- : EMPTY_OFFSET
711
- : averageMeasuredLength(this.measured);
712
- const { offsets, lengths, total } = buildOffsets(
713
- count,
714
- this.measured,
715
- fixedLayout,
716
- averageLength,
717
- );
718
- const target = computeWindow(
719
- count,
720
- offsets,
721
- lengths,
722
- this.scrollOffset,
723
- this.viewportLength,
724
- this.windowSizeValue,
725
- this.initialNumToRenderValue,
726
- );
727
- const throttled = throttleWindow(target, this.committedWindow, this.maxToRenderPerBatchValue);
728
- this.committedWindow = throttled;
729
- this.metricsCore = {
730
- count,
731
- offsets,
732
- lengths,
733
- total,
734
- first: throttled.first,
735
- last: throttled.last,
736
- target,
737
- averageLength,
738
- };
739
- }
740
-
741
749
  private recomputeView(): void {
742
- const m = this.metricsCore;
750
+ const m = this.listState.metrics;
743
751
  this.itemCount = m.count;
744
752
  const hasHeader = this.headerDir !== undefined;
745
753
  const hasSeparators = this.separatorDir !== undefined;
@@ -774,7 +782,7 @@ export class VirtualizedList<ItemT = unknown>
774
782
  this.leadingSpacerStyle = null;
775
783
  this.trailingSpacerStyle = null;
776
784
  this.renderedStickyIndices = undefined;
777
- dlog(`Angular VirtualizedList empty (viewport=${this.viewportLength})`);
785
+ dlog(`Angular VirtualizedList empty (viewport=${this.listState.viewportLength})`);
778
786
  return;
779
787
  }
780
788
 
@@ -823,7 +831,7 @@ export class VirtualizedList<ItemT = unknown>
823
831
 
824
832
  dlog(
825
833
  `Angular VirtualizedList window [${m.first}, ${m.last}] of ${m.count} ` +
826
- `(offset=${this.scrollOffset}, viewport=${this.viewportLength}, rendered=${cells.length})`,
834
+ `(offset=${this.listState.scrollOffset}, viewport=${this.listState.viewportLength}, rendered=${cells.length})`,
827
835
  );
828
836
  }
829
837
 
@@ -855,14 +863,12 @@ export class VirtualizedList<ItemT = unknown>
855
863
  const existing = this.cellMeasures.get(index);
856
864
  if (existing !== undefined) return existing;
857
865
  const measure = (event: ISymbioteEvent): void => {
858
- if (this.getItemLayout !== undefined) return;
859
866
  const length = readLayoutLength(event, this.isHorizontal);
860
867
  if (length === undefined) return;
861
- if (this.measured.get(index) === length) return;
862
- this.measured.set(index, length);
863
868
  dlog(`Angular VirtualizedList cell ${index} measured length=${length}`);
864
- this.measureVersion += 1;
865
- this.cdr.markForCheck();
869
+ // The reducer guards a fixed getItemLayout and a repeated length; a no-op leaves state and the
870
+ // view untouched (dispatch marks for check only when something changed).
871
+ this.dispatch({ kind: 'measure', index, length });
866
872
  };
867
873
  this.cellMeasures.set(index, measure);
868
874
  return measure;
@@ -879,8 +885,8 @@ export class VirtualizedList<ItemT = unknown>
879
885
  }
880
886
 
881
887
  private mergeSeparator(gapIndex: number, patch: Partial<ISeparatorProps<unknown>>): void {
882
- const count = this.metricsCore.count;
883
- if (gapIndex < FIRST_INDEX || gapIndex > count - 2) return;
888
+ const count = this.listState.metrics.count;
889
+ if (!isSeparatorGapInRange(gapIndex, count)) return;
884
890
  this.separatorOverrides.set(gapIndex, { ...this.separatorOverrides.get(gapIndex), ...patch });
885
891
  this.cdr.markForCheck();
886
892
  }
@@ -904,9 +910,15 @@ export class VirtualizedList<ItemT = unknown>
904
910
  }
905
911
 
906
912
  // ---- imperative handle (the shared IVirtualizedListHandle surface) ----
913
+ // Each scroll resolves to an offset (or a scroll-to-index failure) inside the reducer, then rides
914
+ // the scroll-to effect through scrollToPixel.
907
915
 
908
916
  scrollToOffset(params: { offset: number; animated?: boolean }): void {
909
- this.scrollToPixel(params.offset, params.animated ?? true);
917
+ this.dispatch({
918
+ kind: 'scroll-to-offset',
919
+ offset: params.offset,
920
+ animated: params.animated ?? true,
921
+ });
910
922
  }
911
923
 
912
924
  scrollToIndex(params: {
@@ -915,50 +927,26 @@ export class VirtualizedList<ItemT = unknown>
915
927
  viewOffset?: number;
916
928
  viewPosition?: number;
917
929
  }): void {
918
- const m = this.metricsCore;
919
- // No getItemLayout AND the target is past the last measured cell: report the failure instead of
920
- // scrolling to a fabricated estimate (RN VirtualizedList.js).
921
- if (this.getItemLayout === undefined && params.index > highestMeasuredIndex(this.measured)) {
922
- dlog(
923
- `Angular VirtualizedList onScrollToIndexFailed index=${params.index} ` +
924
- `highestMeasured=${highestMeasuredIndex(this.measured)} (no getItemLayout)`,
925
- );
926
- this.scrollToIndexFailed.emit({
927
- index: params.index,
928
- highestMeasuredFrameIndex: highestMeasuredIndex(this.measured),
929
- averageItemLength: m.averageLength,
930
- });
931
- return;
932
- }
933
- this.scrollToPixel(
934
- this.offsetForIndexLocal(
935
- params.index,
936
- params.viewPosition ?? FIRST_INDEX,
937
- params.viewOffset ?? EMPTY_OFFSET,
938
- ),
939
- params.animated ?? true,
940
- );
930
+ this.dispatch({
931
+ kind: 'scroll-to-index',
932
+ index: params.index,
933
+ animated: params.animated ?? true,
934
+ viewPosition: params.viewPosition ?? FIRST_INDEX,
935
+ viewOffset: params.viewOffset ?? EMPTY_OFFSET,
936
+ });
941
937
  }
942
938
 
943
939
  scrollToItem(params: { item: unknown; animated?: boolean; viewPosition?: number }): void {
944
- const count = this.metricsCore.count;
945
- for (let index = FIRST_INDEX; index < count; index += 1) {
946
- if (this.getItem(this.data, index) === params.item) {
947
- this.scrollToPixel(
948
- this.offsetForIndexLocal(index, params.viewPosition ?? FIRST_INDEX, EMPTY_OFFSET),
949
- params.animated ?? true,
950
- );
951
- return;
952
- }
953
- }
954
- dlog('Angular VirtualizedList scrollToItem: item not found');
940
+ this.dispatch({
941
+ kind: 'scroll-to-item',
942
+ item: params.item,
943
+ animated: params.animated ?? true,
944
+ viewPosition: params.viewPosition ?? FIRST_INDEX,
945
+ });
955
946
  }
956
947
 
957
948
  scrollToEnd(params?: { animated?: boolean }): void {
958
- this.scrollToPixel(
959
- Math.max(EMPTY_OFFSET, this.metricsCore.total - this.viewportLength),
960
- params?.animated ?? true,
961
- );
949
+ this.dispatch({ kind: 'scroll-to-end', animated: params?.animated ?? true });
962
950
  }
963
951
 
964
952
  flashScrollIndicators(): void {
@@ -982,7 +970,7 @@ export class VirtualizedList<ItemT = unknown>
982
970
  }
983
971
 
984
972
  recordInteraction(): void {
985
- this.hasInteracted = true;
973
+ this.dispatch({ kind: 'record-interaction' });
986
974
  }
987
975
 
988
976
  private scrollToPixel(offset: number, animated: boolean): void {
@@ -1001,203 +989,4 @@ export class VirtualizedList<ItemT = unknown>
1001
989
  this.commandedOffset = target;
1002
990
  this.cdr.markForCheck();
1003
991
  }
1004
-
1005
- private offsetForIndexLocal(index: number, viewPosition: number, viewOffset: number): number {
1006
- const m = this.metricsCore;
1007
- return offsetForIndex(
1008
- index,
1009
- viewPosition,
1010
- viewOffset,
1011
- m.count,
1012
- m.offsets,
1013
- m.lengths,
1014
- this.viewportLength,
1015
- );
1016
- }
1017
-
1018
- // ---- after-commit effects (run in ngAfterViewChecked, the post-render seam) ----
1019
-
1020
- // Batch fill: when the throttled window has not reached the target, schedule a re-render so the
1021
- // window keeps filling toward target over successive ticks (RN's incremental fill).
1022
- private effectBatchFill(): void {
1023
- const m = this.metricsCore;
1024
- if (this.batchTimer !== null) {
1025
- clearTimeout(this.batchTimer);
1026
- this.batchTimer = null;
1027
- }
1028
- if (m.first <= m.target.first && m.last >= m.target.last) return;
1029
- this.batchTimer = setTimeout(() => {
1030
- this.batchTimer = null;
1031
- this.measureVersion += 1;
1032
- this.cdr.markForCheck();
1033
- }, this.updateCellsBatchingPeriodValue);
1034
- }
1035
-
1036
- // onEndReached: fire only when the actual last cell is rendered AND within threshold; dedup by
1037
- // content length; re-arm on scroll away from the end (RN _maybeCallOnEdgeReached).
1038
- private effectEndReached(): void {
1039
- if (!this.endReached.observed || this.viewportLength <= EMPTY_OFFSET) return;
1040
- const m = this.metricsCore;
1041
- const { distanceFromEnd, withinThreshold } = computeEndReached(
1042
- m.total,
1043
- this.scrollOffset,
1044
- this.viewportLength,
1045
- this.onEndReachedThresholdValue,
1046
- );
1047
- const lastCellRendered = m.last === m.count - 1;
1048
- if (withinThreshold && lastCellRendered && this.sentEndForContentLength !== m.total) {
1049
- this.sentEndForContentLength = m.total;
1050
- dlog(
1051
- `Angular VirtualizedList endReached distanceFromEnd=${distanceFromEnd} ` +
1052
- `(last=${m.last} of ${m.count}, contentLength=${m.total})`,
1053
- );
1054
- this.endReached.emit({ distanceFromEnd });
1055
- }
1056
- if (!withinThreshold) this.sentEndForContentLength = NO_CONTENT_LENGTH_SENT;
1057
- }
1058
-
1059
- // startReached: the top-edge twin of endReached.
1060
- private effectStartReached(): void {
1061
- if (!this.startReached.observed || this.viewportLength <= EMPTY_OFFSET) return;
1062
- const m = this.metricsCore;
1063
- const { distanceFromStart, withinThreshold } = computeStartReached(
1064
- this.scrollOffset,
1065
- this.viewportLength,
1066
- this.onStartReachedThresholdValue,
1067
- );
1068
- const firstCellRendered = m.first === FIRST_INDEX;
1069
- if (withinThreshold && firstCellRendered && this.sentStartForContentLength !== m.total) {
1070
- this.sentStartForContentLength = m.total;
1071
- dlog(
1072
- `Angular VirtualizedList startReached distanceFromStart=${distanceFromStart} ` +
1073
- `(first=${m.first}, contentLength=${m.total})`,
1074
- );
1075
- this.startReached.emit({ distanceFromStart });
1076
- }
1077
- if (!withinThreshold) this.sentStartForContentLength = NO_CONTENT_LENGTH_SENT;
1078
- }
1079
-
1080
- // Viewability: recompute which rendered cells clear the threshold and, if the viewable set
1081
- // changed, fire viewableItemsChanged + every config/callback pair, honoring minimumViewTime.
1082
- private effectViewability(): void {
1083
- const m = this.metricsCore;
1084
- const pairs = buildViewabilityPairs(
1085
- this.viewableItemsChanged.observed
1086
- ? (info: IViewableItemsChangedInfo<ItemT>): void => this.viewableItemsChanged.emit(info)
1087
- : undefined,
1088
- this.viewabilityConfig,
1089
- this.viewabilityConfigCallbackPairs,
1090
- );
1091
- if (pairs.length === EMPTY_OFFSET || this.viewportLength <= EMPTY_OFFSET) return;
1092
- if (m.count === FIRST_INDEX) return;
1093
-
1094
- const { tokens, map } = computeViewableSet({
1095
- first: m.first,
1096
- last: m.last,
1097
- count: m.count,
1098
- offsets: m.offsets,
1099
- lengths: m.lengths,
1100
- scrollOffset: this.scrollOffset,
1101
- viewportLength: this.viewportLength,
1102
- data: this.data,
1103
- getItem: this.getItem,
1104
- keyExtractor: this.keyExtractor,
1105
- pairs,
1106
- hasInteracted: this.hasInteracted,
1107
- });
1108
- const diff = diffViewable(this.lastViewable, map, tokens);
1109
- if (!diff.hasChanged) return;
1110
-
1111
- const commitAndFire = (): void => {
1112
- this.lastViewable = map;
1113
- dlog(
1114
- `Angular VirtualizedList viewable=${tokens.length} changed=${diff.changed.length} ` +
1115
- `(window [${m.first}, ${m.last}])`,
1116
- );
1117
- const info: IViewableItemsChangedInfo<ItemT> = {
1118
- viewableItems: tokens,
1119
- changed: diff.changed,
1120
- };
1121
- for (const pair of pairs) pair.onViewableItemsChanged(info);
1122
- };
1123
-
1124
- const minimumViewTime = maxMinimumViewTime(pairs);
1125
- if (this.viewableTimer !== null) {
1126
- clearTimeout(this.viewableTimer);
1127
- this.viewableTimer = null;
1128
- }
1129
- if (minimumViewTime > EMPTY_OFFSET) {
1130
- dlog(
1131
- `Angular VirtualizedList viewability debounce ${minimumViewTime}ms (window [${m.first}, ${m.last}])`,
1132
- );
1133
- this.viewableTimer = setTimeout(() => {
1134
- this.viewableTimer = null;
1135
- commitAndFire();
1136
- }, minimumViewTime);
1137
- return;
1138
- }
1139
- commitAndFire();
1140
- }
1141
-
1142
- // initialScrollIndex: once the first viewport is known, jump to that index a single time.
1143
- private effectInitialScroll(): void {
1144
- if (this.initialScrollIndex === undefined || this.appliedInitialScroll) return;
1145
- if (this.viewportLength <= EMPTY_OFFSET || this.metricsCore.count === FIRST_INDEX) return;
1146
- this.appliedInitialScroll = true;
1147
- // The initial jump is instant (RN does not animate initialScrollIndex).
1148
- this.scrollToPixel(
1149
- this.offsetForIndexLocal(this.initialScrollIndex, FIRST_INDEX, EMPTY_OFFSET),
1150
- false,
1151
- );
1152
- }
1153
-
1154
- // maintainVisibleContentPosition JS anchor adjustment: track the key of the item at
1155
- // minIndexForVisible; when a prepend moves it down, add the inserted extent in the leading SPACER
1156
- // (the off-window items native MVCP cannot see) to scrollOffset so the anchored item does not jump
1157
- // (RN getDerivedStateFromProps).
1158
- private effectMaintainVisibleContentPosition(): void {
1159
- const m = this.metricsCore;
1160
- if (this.maintainVisibleContentPosition === undefined || m.count === FIRST_INDEX) {
1161
- this.firstVisibleKey = null;
1162
- return;
1163
- }
1164
- const minIndexForVisible = this.maintainVisibleContentPosition.minIndexForVisible;
1165
- const newFirstVisibleKey =
1166
- m.count > minIndexForVisible ? this.keyFor(minIndexForVisible) : null;
1167
- const prevKey = this.firstVisibleKey;
1168
-
1169
- if (prevKey !== null && newFirstVisibleKey !== null && prevKey !== newFirstVisibleKey) {
1170
- let anchorIndex = -1;
1171
- for (let index = minIndexForVisible; index < m.count; index += 1) {
1172
- if (this.keyFor(index) === prevKey) {
1173
- anchorIndex = index;
1174
- break;
1175
- }
1176
- }
1177
- if (anchorIndex > minIndexForVisible) {
1178
- const spacerEnd = Math.min(anchorIndex, this.committedWindow.first);
1179
- const insertedExtent =
1180
- spacerEnd > minIndexForVisible
1181
- ? m.offsets[spacerEnd] - m.offsets[minIndexForVisible]
1182
- : EMPTY_OFFSET;
1183
- if (insertedExtent > EMPTY_OFFSET) {
1184
- const autoThreshold = this.maintainVisibleContentPosition.autoscrollToTopThreshold;
1185
- const anchoredNearTop = autoThreshold !== undefined && this.scrollOffset <= autoThreshold;
1186
- if (anchoredNearTop) {
1187
- dlog(
1188
- `Angular VirtualizedList MVCP autoscroll-to-top (offset=${this.scrollOffset} <= ${autoThreshold})`,
1189
- );
1190
- this.scrollToPixel(EMPTY_OFFSET, true);
1191
- } else {
1192
- dlog(
1193
- `Angular VirtualizedList MVCP adjust +${insertedExtent}px ` +
1194
- `(anchor "${prevKey}" moved ${minIndexForVisible}->${anchorIndex})`,
1195
- );
1196
- this.scrollToPixel(this.scrollOffset + insertedExtent, false);
1197
- }
1198
- }
1199
- }
1200
- }
1201
- this.firstVisibleKey = newFirstVisibleKey;
1202
- }
1203
992
  }