@symbiote-native/angular 3.3.0 → 3.5.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 (187) hide show
  1. package/babel-register-composed.cjs +1 -0
  2. package/build/angular/accessibility-inputs.d.ts +2 -1
  3. package/build/angular/accessibility-inputs.js +4 -1
  4. package/build/angular/accessibility-inputs.js.map +1 -1
  5. package/build/angular/callback-host.d.ts +1 -1
  6. package/build/angular/callback-host.js +57 -9
  7. package/build/angular/callback-host.js.map +1 -1
  8. package/build/angular/components/flat-list/flat-list-handle.d.ts +35 -0
  9. package/build/angular/components/flat-list/flat-list-handle.js +48 -0
  10. package/build/angular/components/flat-list/flat-list-handle.js.map +1 -0
  11. package/build/angular/components/flat-list/flat-list-template.d.ts +1 -1
  12. package/build/angular/components/flat-list/flat-list-template.js +16 -0
  13. package/build/angular/components/flat-list/flat-list-template.js.map +1 -1
  14. package/build/angular/components/flat-list/index.d.ts +6 -30
  15. package/build/angular/components/flat-list/index.js +6 -36
  16. package/build/angular/components/flat-list/index.js.map +1 -1
  17. package/build/angular/components/image-inputs.d.ts +4 -0
  18. package/build/angular/components/image-inputs.js +81 -0
  19. package/build/angular/components/image-inputs.js.map +1 -0
  20. package/build/angular/components/image-props-resolve.d.ts +3 -0
  21. package/build/angular/components/image-props-resolve.js +133 -0
  22. package/build/angular/components/image-props-resolve.js.map +1 -0
  23. package/build/angular/components/image-shared.d.ts +8 -6
  24. package/build/angular/components/image-shared.js +23 -212
  25. package/build/angular/components/image-shared.js.map +1 -1
  26. package/build/angular/components/input-accessory-view-props.d.ts +4 -4
  27. package/build/angular/components/keyboard-avoiding-view/index.d.ts +3 -6
  28. package/build/angular/components/keyboard-avoiding-view/index.js +29 -56
  29. package/build/angular/components/keyboard-avoiding-view/index.js.map +1 -1
  30. package/build/angular/components/layout-conformance-props.d.ts +4 -0
  31. package/build/angular/components/layout-conformance-props.js +2 -0
  32. package/build/angular/components/layout-conformance-props.js.map +1 -0
  33. package/build/angular/components/modal/index.d.ts +6 -5
  34. package/build/angular/components/modal/index.js +17 -2
  35. package/build/angular/components/modal/index.js.map +1 -1
  36. package/build/angular/components/refresh-control-props.d.ts +7 -7
  37. package/build/angular/components/scroll-view-props.d.ts +8 -5
  38. package/build/angular/components/section-list/index.d.ts +17 -8
  39. package/build/angular/components/section-list/index.js +37 -28
  40. package/build/angular/components/section-list/index.js.map +1 -1
  41. package/build/angular/components/touchable-props.d.ts +2 -2
  42. package/build/angular/components/virtualized-list/cell-renderer-context.d.ts +12 -0
  43. package/build/angular/components/virtualized-list/cell-renderer-context.js +16 -0
  44. package/build/angular/components/virtualized-list/cell-renderer-context.js.map +1 -0
  45. package/build/angular/components/virtualized-list/directives.d.ts +26 -6
  46. package/build/angular/components/virtualized-list/directives.js +59 -5
  47. package/build/angular/components/virtualized-list/directives.js.map +1 -1
  48. package/build/angular/components/virtualized-list/index.d.ts +19 -15
  49. package/build/angular/components/virtualized-list/index.js +91 -61
  50. package/build/angular/components/virtualized-list/index.js.map +1 -1
  51. package/build/angular/components/virtualized-list/list-bags.d.ts +5 -1
  52. package/build/angular/components/virtualized-list/list-bags.js +31 -19
  53. package/build/angular/components/virtualized-list/list-bags.js.map +1 -1
  54. package/build/angular/components/virtualized-list/list-effects.d.ts +2 -2
  55. package/build/angular/components/virtualized-list/list-effects.js +17 -27
  56. package/build/angular/components/virtualized-list/list-effects.js.map +1 -1
  57. package/build/angular/components/virtualized-list/list-handle.d.ts +4 -1
  58. package/build/angular/components/virtualized-list/list-handle.js +13 -3
  59. package/build/angular/components/virtualized-list/list-handle.js.map +1 -1
  60. package/build/angular/components/virtualized-list/list-inputs.d.ts +19 -5
  61. package/build/angular/components/virtualized-list/list-inputs.js +47 -4
  62. package/build/angular/components/virtualized-list/list-inputs.js.map +1 -1
  63. package/build/angular/components/virtualized-list/list-props.d.ts +9 -3
  64. package/build/angular/components/virtualized-list/list-rows.d.ts +25 -0
  65. package/build/angular/components/virtualized-list/list-rows.js +25 -0
  66. package/build/angular/components/virtualized-list/list-rows.js.map +1 -0
  67. package/build/angular/components/virtualized-list/list-styles.d.ts +4 -0
  68. package/build/angular/components/virtualized-list/list-styles.js +10 -2
  69. package/build/angular/components/virtualized-list/list-styles.js.map +1 -1
  70. package/build/angular/components/virtualized-list/list-template.d.ts +1 -1
  71. package/build/angular/components/virtualized-list/list-template.js +159 -73
  72. package/build/angular/components/virtualized-list/list-template.js.map +1 -1
  73. package/build/angular/components/virtualized-list/nested-scope.d.ts +6 -0
  74. package/build/angular/components/virtualized-list/nested-scope.js +17 -0
  75. package/build/angular/components/virtualized-list/nested-scope.js.map +1 -0
  76. package/build/angular/components/virtualized-section-list/directives.d.ts +21 -9
  77. package/build/angular/components/virtualized-section-list/directives.js +8 -28
  78. package/build/angular/components/virtualized-section-list/directives.js.map +1 -1
  79. package/build/angular/components/virtualized-section-list/index.d.ts +39 -60
  80. package/build/angular/components/virtualized-section-list/index.js +67 -33
  81. package/build/angular/components/virtualized-section-list/index.js.map +1 -1
  82. package/build/angular/components/virtualized-section-list/section-cell.d.ts +29 -0
  83. package/build/angular/components/virtualized-section-list/section-cell.js +82 -0
  84. package/build/angular/components/virtualized-section-list/section-cell.js.map +1 -0
  85. package/build/angular/components/virtualized-section-list/section-list-props.d.ts +56 -0
  86. package/build/angular/components/virtualized-section-list/section-list-props.js +3 -0
  87. package/build/angular/components/virtualized-section-list/section-list-props.js.map +1 -0
  88. package/build/angular/components/virtualized-section-list/section-list-template.d.ts +1 -1
  89. package/build/angular/components/virtualized-section-list/section-list-template.js +34 -26
  90. package/build/angular/components/virtualized-section-list/section-list-template.js.map +1 -1
  91. package/build/angular/components.d.ts +7 -3
  92. package/build/angular/components.js +2 -1
  93. package/build/angular/components.js.map +1 -1
  94. package/build/angular/element-base.d.ts +17 -1
  95. package/build/angular/element-base.js +33 -1
  96. package/build/angular/element-base.js.map +1 -1
  97. package/build/angular/element-props.d.ts +7 -7
  98. package/build/angular/element-text-input-native.d.ts +27 -0
  99. package/build/angular/element-text-input-native.js +52 -0
  100. package/build/angular/element-text-input-native.js.map +1 -0
  101. package/build/angular/elements-controls.d.ts +8 -2
  102. package/build/angular/elements-controls.js +13 -1
  103. package/build/angular/elements-controls.js.map +1 -1
  104. package/build/angular/elements-layout.d.ts +20 -3
  105. package/build/angular/elements-layout.js +37 -3
  106. package/build/angular/elements-layout.js.map +1 -1
  107. package/build/angular/elements-press.d.ts +10 -3
  108. package/build/angular/elements-press.js +13 -1
  109. package/build/angular/elements-press.js.map +1 -1
  110. package/build/angular/elements.d.ts +3 -3
  111. package/build/angular/elements.js +3 -2
  112. package/build/angular/elements.js.map +1 -1
  113. package/build/angular/index.d.ts +15 -7
  114. package/build/angular/index.js +22 -25
  115. package/build/angular/index.js.map +1 -1
  116. package/build/angular/modules/animated/create-animated-component.d.ts +1 -1
  117. package/build/angular/modules/animated/create-animated-component.js +1 -1
  118. package/build/angular/modules/animated/create-animated-component.js.map +1 -1
  119. package/build/angular/modules/animated/index.d.ts +54 -41
  120. package/build/angular/modules/animated/index.js +2 -26
  121. package/build/angular/modules/animated/index.js.map +1 -1
  122. package/build/angular/modules/app-registry/index.d.ts +3 -3
  123. package/build/angular/modules/app-registry/index.js +2 -2
  124. package/build/angular/modules/app-registry/index.js.map +1 -1
  125. package/build/angular/modules/status-bar/index.d.ts +9 -5
  126. package/build/angular/modules/status-bar/index.js +21 -10
  127. package/build/angular/modules/status-bar/index.js.map +1 -1
  128. package/build/angular/primitives/index.d.ts +4 -0
  129. package/build/angular/primitives/index.js +12 -0
  130. package/build/angular/primitives/index.js.map +1 -1
  131. package/build/angular/render/index.d.ts +2 -2
  132. package/build/angular/render/index.js +10 -29
  133. package/build/angular/render/index.js.map +1 -1
  134. package/build/angular/root-tag.d.ts +4 -0
  135. package/build/angular/root-tag.js +10 -0
  136. package/build/angular/root-tag.js.map +1 -0
  137. package/build/angular/services/color-scheme.service.d.ts +1 -1
  138. package/package.json +6 -6
  139. package/src/accessibility-inputs.ts +3 -0
  140. package/src/callback-host.ts +56 -8
  141. package/src/components/flat-list/flat-list-handle.ts +64 -0
  142. package/src/components/flat-list/flat-list-template.ts +16 -0
  143. package/src/components/flat-list/index.ts +6 -48
  144. package/src/components/image-inputs.ts +94 -0
  145. package/src/components/image-props-resolve.ts +172 -0
  146. package/src/components/image-shared.ts +26 -291
  147. package/src/components/input-accessory-view-props.ts +12 -8
  148. package/src/components/keyboard-avoiding-view/index.ts +39 -79
  149. package/src/components/layout-conformance-props.ts +8 -0
  150. package/src/components/modal/index.ts +42 -20
  151. package/src/components/refresh-control-props.ts +29 -25
  152. package/src/components/scroll-view-props.ts +84 -76
  153. package/src/components/section-list/index.ts +34 -37
  154. package/src/components/touchable-props.ts +6 -2
  155. package/src/components/virtualized-list/cell-renderer-context.ts +36 -0
  156. package/src/components/virtualized-list/directives.ts +84 -8
  157. package/src/components/virtualized-list/index.ts +118 -85
  158. package/src/components/virtualized-list/list-bags.ts +38 -19
  159. package/src/components/virtualized-list/list-effects.ts +20 -32
  160. package/src/components/virtualized-list/list-handle.ts +17 -4
  161. package/src/components/virtualized-list/list-inputs.ts +47 -0
  162. package/src/components/virtualized-list/list-props.ts +57 -50
  163. package/src/components/virtualized-list/list-rows.ts +66 -0
  164. package/src/components/virtualized-list/list-styles.ts +30 -1
  165. package/src/components/virtualized-list/list-template.ts +159 -73
  166. package/src/components/virtualized-list/nested-scope.ts +22 -0
  167. package/src/components/virtualized-section-list/directives.ts +41 -40
  168. package/src/components/virtualized-section-list/index.ts +114 -118
  169. package/src/components/virtualized-section-list/section-cell.ts +109 -0
  170. package/src/components/virtualized-section-list/section-list-props.ts +79 -0
  171. package/src/components/virtualized-section-list/section-list-template.ts +34 -26
  172. package/src/components.ts +8 -0
  173. package/src/element-base.ts +17 -0
  174. package/src/element-props.ts +35 -33
  175. package/src/element-text-input-native.ts +31 -0
  176. package/src/elements-controls.ts +9 -1
  177. package/src/elements-layout.ts +27 -0
  178. package/src/elements-press.ts +10 -2
  179. package/src/elements.ts +3 -0
  180. package/src/index.ts +79 -22
  181. package/src/modules/animated/index.ts +2 -47
  182. package/src/modules/app-registry/index.ts +3 -1
  183. package/src/modules/status-bar/index.ts +24 -11
  184. package/src/primitives/index.ts +7 -0
  185. package/src/render/index.ts +23 -50
  186. package/src/root-tag.ts +14 -0
  187. package/src/services/color-scheme.service.ts +1 -1
@@ -9,25 +9,32 @@ import {
9
9
  Component,
10
10
  ElementRef,
11
11
  ViewChild,
12
+ forwardRef,
12
13
  inject,
13
14
  type AfterViewChecked,
14
15
  type DoCheck,
15
16
  type OnDestroy,
17
+ type TemplateRef,
16
18
  } from '@angular/core';
17
19
  import {
18
20
  EMPTY_OFFSET,
19
21
  FIRST_INDEX,
20
22
  LIST_ACTION_KIND,
21
- buildListPlan,
23
+ LIST_SEGMENT_KIND,
22
24
  buildScrollViewHandle,
23
25
  buildViewabilityPairs,
24
- computeWindow,
25
26
  createInitialListState,
27
+ createListNesting,
28
+ deriveWindow,
26
29
  listEffectSignature,
30
+ listHasMore,
31
+ planFromMetrics,
27
32
  reduceList,
28
33
  resolveItemKey,
29
34
  type IListAction,
35
+ type IListNesting,
30
36
  type IListReducerInputs,
37
+ type IListScope,
31
38
  type IListState,
32
39
  type IScrollViewHandle,
33
40
  type IViewableItemsChangedInfo,
@@ -46,11 +53,14 @@ import {
46
53
  SymbioteStyleInputDirective,
47
54
  ViewHost,
48
55
  } from '../../primitives';
49
- import { CellRegistry, type IWindowCell } from './cell-registry';
56
+ import { CellRegistry } from './cell-registry';
57
+ import { buildCellRendererContext } from './cell-renderer-context';
50
58
  import { VListOutletDirective } from './directives';
51
59
  import { ListEffectRunner } from './list-effects';
52
60
  import { VirtualizedListBagsBase } from './list-bags';
53
61
  import type { IVirtualizedListInputs } from './list-props';
62
+ import { injectVirtualizedListScope, ListScopeProvider } from './nested-scope';
63
+ import { stampPlanRows, type IListRow, type IRowsParams } from './list-rows';
54
64
  import { resolveListStyles } from './list-styles';
55
65
  import { VIRTUALIZED_LIST_TEMPLATE } from './list-template';
56
66
 
@@ -66,15 +76,19 @@ export type {
66
76
  IVirtualizedListHandle,
67
77
  } from '@symbiote-native/components';
68
78
  export {
79
+ VListCellDirective,
69
80
  VListEmptyDirective,
70
81
  VListFooterDirective,
71
82
  VListHeaderDirective,
72
83
  VListItemDirective,
84
+ VListOutletDirective,
73
85
  VListSeparatorDirective,
74
86
  } from './directives';
75
- export type { IVListItemContext, IVListSeparatorContext } from './directives';
76
-
77
- type IListPlan = ReturnType<typeof buildListPlan>;
87
+ export type {
88
+ IVListCellContext,
89
+ IVListItemContext,
90
+ IVListSeparatorContext,
91
+ } from './directives';
78
92
 
79
93
  export type {
80
94
  IVirtualizedListInputs,
@@ -89,17 +103,32 @@ export type {
89
103
  ],
90
104
  schemas: [CUSTOM_ELEMENTS_SCHEMA],
91
105
  imports: [SymbioteHostPropsDirective, VListOutletDirective, ViewHost],
106
+ providers: [
107
+ {
108
+ provide: ListScopeProvider,
109
+ useExisting: forwardRef(() => VirtualizedList),
110
+ },
111
+ ],
92
112
  changeDetection: ChangeDetectionStrategy.OnPush,
93
113
  template: VIRTUALIZED_LIST_TEMPLATE,
94
114
  })
95
115
  export class VirtualizedList<ItemT = unknown>
96
116
  extends VirtualizedListBagsBase<ItemT>
97
- implements IVirtualizedListInputs<ItemT>, DoCheck, AfterViewChecked, OnDestroy
117
+ implements
118
+ IVirtualizedListInputs<ItemT>,
119
+ ListScopeProvider,
120
+ DoCheck,
121
+ AfterViewChecked,
122
+ OnDestroy
98
123
  {
99
124
  // A template ref on a bare intrinsic hands back the host node directly
100
125
  @ViewChild('scrollHost', { read: ElementRef })
101
126
  private scrollHostRef?: ElementRef<unknown>;
102
127
 
128
+ // The template holding a cell's item and separator, sits at the root so it resolves up front
129
+ @ViewChild('cellBody', { static: true })
130
+ private cellBody?: TemplateRef<unknown>;
131
+
103
132
  protected get scrollNode(): ISymbioteNode | null {
104
133
  const node = this.scrollHostRef?.nativeElement;
105
134
  return isSymbioteNode(node) ? node : null;
@@ -109,15 +138,16 @@ export class VirtualizedList<ItemT = unknown>
109
138
  () => this.scrollNode,
110
139
  );
111
140
 
141
+ // The template tells a spacer row from a cell row by this, a bare literal would repeat per host
142
+ protected readonly spacerKind = LIST_SEGMENT_KIND.spacer;
143
+
112
144
  // Template-bound view state, assembled by `recomputeView` in `ngDoCheck`
113
145
  itemCount = EMPTY_OFFSET;
114
- windowCells: IWindowCell<ItemT>[] = [];
115
- // The nearest sticky cell mounted outside the window, `gapSpacerStyle` fills up to the window
116
- forcedStickyCell: IWindowCell<ItemT> | null = null;
117
- leadingSpacerStyle: IViewStyle | null = null;
118
- gapSpacerStyle: IViewStyle | null = null;
119
- trailingSpacerStyle: IViewStyle | null = null;
146
+ // The plan's spacers and cells in document order
147
+ rows: IListRow<ItemT>[] = [];
120
148
  cellStyle: IViewStyle | undefined = undefined;
149
+ headerStyle: IViewStyle | undefined = undefined;
150
+ footerStyle: IViewStyle | undefined = undefined;
121
151
 
122
152
  // `renderVersion` bumps on every transition that changes render state, for the recompute dedup
123
153
  private readonly listState: IListState<ItemT> =
@@ -149,11 +179,34 @@ export class VirtualizedList<ItemT = unknown>
149
179
  // This component's own host, the anchor that `class="..."` at the use site resolves onto
150
180
  private readonly elementRef = inject(ElementRef);
151
181
 
152
- private keyFor = (index: number): string => {
182
+ protected readonly keyFor = (index: number): string => {
153
183
  const item = this.getItem(this.data, index);
154
184
  return resolveItemKey(item, index, this.keyExtractor);
155
185
  };
156
186
 
187
+ private readonly parentScope = injectVirtualizedListScope();
188
+ private nestingMemo: IListNesting<ItemT> | undefined = undefined;
189
+
190
+ // Built on first use, the inputs that pick its axis are set only after construction
191
+ protected get nesting(): IListNesting<ItemT> {
192
+ this.nestingMemo ??= createListNesting<ItemT>({
193
+ parent: this.parentScope,
194
+ horizontal: this.isHorizontal,
195
+ getState: () => this.listState,
196
+ dispatch: action => this.dispatch(action),
197
+ getContainerNode: () => this.scrollNode,
198
+ getHasMore: () => listHasMore(this.listState),
199
+ keyFor: this.keyFor,
200
+ handlers: () => this.listHandlers,
201
+ });
202
+ return this.nestingMemo;
203
+ }
204
+
205
+ // The scope the lists in this list's cells inject
206
+ get listScope(): IListScope {
207
+ return this.nesting.scope;
208
+ }
209
+
157
210
  // Edge and viewability listeners map to `.observed`, so the reducer emits only if listened to
158
211
  private buildInputs(): IListReducerInputs<ItemT> {
159
212
  return {
@@ -164,6 +217,7 @@ export class VirtualizedList<ItemT = unknown>
164
217
  getItemLayout: this.getItemLayout,
165
218
  horizontal: this.isHorizontal,
166
219
  windowSize: this.windowSizeValue,
220
+ disableVirtualization: this.disableVirtualization,
167
221
  initialNumToRender: this.initialNumToRenderValue,
168
222
  maxToRenderPerBatch: this.maxToRenderPerBatchValue,
169
223
  updateCellsBatchingPeriod: this.updateCellsBatchingPeriodValue,
@@ -181,6 +235,8 @@ export class VirtualizedList<ItemT = unknown>
181
235
  ),
182
236
  maintainVisibleContentPosition: this.maintainVisibleContentPosition,
183
237
  initialScrollIndex: this.initialScrollIndex,
238
+ findFirstChildWithMore: (first, last) =>
239
+ this.nesting.findFirstChildWithMore(first, last),
184
240
  };
185
241
  }
186
242
 
@@ -204,15 +260,12 @@ export class VirtualizedList<ItemT = unknown>
204
260
  const m = this.listState.metrics;
205
261
  // Mid-fill the window climbs one batch step per render, only a render advances it
206
262
  if (m.first !== m.target.first || m.last !== m.target.last) return false;
207
- const next = computeWindow(
263
+ const next = deriveWindow(
264
+ this.listState,
265
+ this.buildInputs(),
208
266
  m.count,
209
- m.offsets,
210
- m.lengths,
211
- this.listState.scrollOffset,
212
- this.listState.viewportLength,
213
- this.windowSizeValue,
214
- this.initialNumToRenderValue,
215
- );
267
+ m,
268
+ ).target;
216
269
  const isSettled =
217
270
  next.first === m.target.first && next.last === m.target.last;
218
271
  if (!isSettled)
@@ -234,6 +287,7 @@ export class VirtualizedList<ItemT = unknown>
234
287
  this.isHorizontal,
235
288
  this.isInverted,
236
289
  this.windowSizeValue,
290
+ this.disableVirtualization,
237
291
  this.initialNumToRenderValue,
238
292
  this.maxToRenderPerBatchValue,
239
293
  this.stickyHeaderIndices,
@@ -242,11 +296,14 @@ export class VirtualizedList<ItemT = unknown>
242
296
  // Arrives through `addClass` and `removeClass`, never as an input
243
297
  anchorHostStyle(this.elementRef),
244
298
  this.contentContainerStyle,
299
+ this.listHeaderComponentStyle,
300
+ this.listFooterComponentStyle,
245
301
  this.renderVersion,
246
302
  this.headerDir !== undefined,
247
303
  this.footerDir !== undefined,
248
304
  this.emptyDir !== undefined,
249
305
  this.separatorTemplate !== undefined,
306
+ this.cellRendererTpl,
250
307
  ];
251
308
  const previous = this.lastRecompute;
252
309
  this.lastRecompute = recomputeInputs;
@@ -281,6 +338,7 @@ export class VirtualizedList<ItemT = unknown>
281
338
 
282
339
  ngOnDestroy(): void {
283
340
  this.effects.dispose();
341
+ this.nestingMemo?.detach();
284
342
  }
285
343
 
286
344
  private recomputeView(): void {
@@ -297,29 +355,35 @@ export class VirtualizedList<ItemT = unknown>
297
355
  this.showEmpty();
298
356
  return;
299
357
  }
300
- const stickySet =
301
- this.stickyHeaderIndices !== undefined
302
- ? new Set(this.stickyHeaderIndices)
303
- : undefined;
304
- const plan = buildListPlan({
358
+ this.rows = stampPlanRows({
359
+ windowPlan: planFromMetrics(m, this.keyFor, this.stickyHeaderIndices),
360
+ cells: this.cells,
305
361
  count: m.count,
306
- first: m.first,
307
- last: m.last,
308
- offsets: m.offsets,
309
- lengths: m.lengths,
310
- total: m.total,
311
- keyFor: this.keyFor,
312
- stickyIndices: stickySet,
313
- hasHeader: this.headerDir !== undefined,
362
+ isHorizontal: this.isHorizontal,
363
+ hasSeparators: this.separatorTemplate !== undefined,
364
+ rendererFor: this.rendererFor(),
314
365
  });
315
- this.applySpacers(plan);
316
- this.buildCells(plan, stickySet);
366
+ this.cells.endPass();
317
367
  dlog(
318
368
  `Angular VirtualizedList window [${m.first}, ${m.last}] of ${m.count} ` +
319
- `(offset=${this.listState.scrollOffset}, viewport=${this.listState.viewportLength}, rendered=${this.windowCells.length})`,
369
+ `(offset=${this.listState.scrollOffset}, viewport=${this.listState.viewportLength}, rows=${this.rows.length})`,
320
370
  );
321
371
  }
322
372
 
373
+ // Only a list given a `vListCell` template builds the wrapper contexts
374
+ private rendererFor(): IRowsParams<ItemT>['rendererFor'] {
375
+ if (this.cellRendererTpl === undefined) return undefined;
376
+ return cell =>
377
+ buildCellRendererContext({
378
+ cell,
379
+ style: this.cellStyle,
380
+ body: this.cellBody,
381
+ layout: (measure, event, index) =>
382
+ this.handleCellLayout(measure, event, index),
383
+ focus: index => this.handleCellFocus(index),
384
+ });
385
+ }
386
+
323
387
  private applyStyles(total: number): void {
324
388
  const styles = resolveListStyles({
325
389
  isHorizontal: this.isHorizontal,
@@ -328,75 +392,44 @@ export class VirtualizedList<ItemT = unknown>
328
392
  hasHeader: this.headerDir !== undefined,
329
393
  style: this.style,
330
394
  contentContainerStyle: this.contentContainerStyle,
395
+ listHeaderComponentStyle: this.listHeaderComponentStyle,
396
+ listFooterComponentStyle: this.listFooterComponentStyle,
331
397
  anchorStyle: anchorHostStyle(this.elementRef),
332
398
  maintainVisibleContentPosition: this.maintainVisibleContentPosition,
333
399
  });
334
400
  this.resolvedStyle = styles.style;
335
401
  this.resolvedContentContainerStyle = styles.contentContainerStyle;
336
402
  this.cellStyle = styles.cellStyle;
403
+ this.headerStyle = styles.headerStyle;
404
+ this.footerStyle = styles.footerStyle;
337
405
  this.resolvedMaintainVisibleContentPosition =
338
406
  styles.maintainVisibleContentPosition;
339
407
  }
340
408
 
341
409
  private showEmpty(): void {
342
410
  this.cells.clear();
343
- this.windowCells = [];
344
- this.forcedStickyCell = null;
345
- this.leadingSpacerStyle = null;
346
- this.gapSpacerStyle = null;
347
- this.trailingSpacerStyle = null;
411
+ this.rows = [];
348
412
  dlog(
349
413
  `Angular VirtualizedList empty (viewport=${this.listState.viewportLength})`,
350
414
  );
351
415
  }
352
416
 
353
- private applySpacers(plan: IListPlan): void {
354
- this.leadingSpacerStyle = this.spacerStyle(plan.leadingExtent);
355
- this.gapSpacerStyle = this.spacerStyle(plan.gapExtent);
356
- this.trailingSpacerStyle = this.spacerStyle(plan.trailingExtent);
357
- }
358
-
359
- private spacerStyle(extent: number): IViewStyle | null {
360
- if (extent <= EMPTY_OFFSET) return null;
361
- return this.isHorizontal ? { width: extent } : { height: extent };
362
- }
363
-
364
- private buildCells(
365
- plan: IListPlan,
366
- stickySet: Set<number> | undefined,
367
- ): void {
368
- const count = this.listState.metrics.count;
369
- const forced = plan.forcedStickyCell;
370
- dlog(
371
- `STICKY[list] stickySet=${stickySet === undefined ? 'undefined' : JSON.stringify([...stickySet])} ` +
372
- `childPositions=${JSON.stringify(plan.stickyChildPositions)} ` +
373
- `forcedStickyCell=${forced === undefined ? 'none' : forced.index}`,
374
- );
375
- // Sticky by construction: a forced cell exists only for an index in `stickySet`
376
- this.forcedStickyCell =
377
- forced !== undefined
378
- ? this.cells.windowCell(forced.index, forced.key, false, true)
379
- : null;
380
- const hasSeparators = this.separatorTemplate !== undefined;
381
- // The separator gates on the last index of the data, not of the window, or a cell's height
382
- // would shift as it slides past
383
- this.windowCells = plan.cells.map(planned =>
384
- this.cells.windowCell(
385
- planned.index,
386
- planned.key,
387
- hasSeparators && planned.index < count - 1,
388
- stickySet?.has(planned.index) === true,
389
- ),
390
- );
391
- this.cells.endPass();
392
- }
393
-
394
417
  // Angular forbids an `[onLayout]` binding, so the event arrives untyped and is narrowed here
395
418
  handleCellLayout(
396
419
  measure: (event: ISymbioteEvent) => void,
397
420
  event: unknown,
421
+ index: number,
398
422
  ): void {
399
- if (this.isSymbioteEvent(event)) measure(event);
423
+ if (!this.isSymbioteEvent(event)) return;
424
+ measure(event);
425
+ const cellKey = this.keyFor(index);
426
+ this.nesting.scope.registerCellNode(event.currentTarget, cellKey);
427
+ this.nesting.remeasureCell(cellKey);
428
+ }
429
+
430
+ // Focus inside a cell keeps a viewport of cells around it mounted, like RN's focus capture
431
+ handleCellFocus(index: number): void {
432
+ this.dispatch({ kind: LIST_ACTION_KIND.cellFocused, index });
400
433
  }
401
434
 
402
435
  private isSymbioteEvent(value: unknown): value is ISymbioteEvent {
@@ -3,15 +3,14 @@
3
3
 
4
4
  import { Directive, computed, signal, type OnChanges } from '@angular/core';
5
5
  import {
6
- LIST_ACTION_KIND,
7
- readLayoutLength,
8
- readScrollOffset,
6
+ createListHandlers,
9
7
  resolveAccessibilityProps,
10
8
  type IAccessibilityProps,
11
9
  type IAriaProps,
10
+ type IListHandlers,
11
+ type IListNesting,
12
12
  } from '@symbiote-native/components';
13
13
  import {
14
- dlog,
15
14
  type IStyleProp,
16
15
  type ISymbioteEvent,
17
16
  type IViewStyle,
@@ -32,6 +31,23 @@ export abstract class VirtualizedListBagsBase<ItemT>
32
31
  // A fresh object per push, so the commit re-applies a repeated offset
33
32
  commandedOffset: { x: number; y: number } | undefined = undefined;
34
33
 
34
+ protected abstract get nesting(): IListNesting<ItemT>;
35
+
36
+ protected abstract readonly keyFor: (index: number) => string;
37
+
38
+ // Reads of `this` are deferred to the event, so the inputs are set by the time they run
39
+ protected readonly listHandlers: IListHandlers = createListHandlers<ItemT>({
40
+ isHorizontal: () => this.isHorizontal,
41
+ user: () => this,
42
+ nesting: () => this.nesting,
43
+ dispatch: action => this.dispatch(action),
44
+ clearCommandedOffset: () => {
45
+ this.commandedOffset = undefined;
46
+ },
47
+ getNode: () => this.scrollNode,
48
+ keyFor: index => this.keyFor(index),
49
+ });
50
+
35
51
  // Only inputs may feed this signal, window state changes skip `ngOnChanges`
36
52
  private readonly inputsRevision = signal(0);
37
53
 
@@ -64,15 +80,18 @@ export abstract class VirtualizedListBagsBase<ItemT>
64
80
  contentContainerStyle: this.resolvedContentContainerStyle,
65
81
  onScroll: this.onScrollTick,
66
82
  onLayout: this.onLayoutTick,
67
- onScrollBeginDrag: this.onScrollBeginDrag,
68
- onScrollEndDrag: this.onScrollEndDrag,
69
- onMomentumScrollBegin: this.onMomentumScrollBegin,
70
- onMomentumScrollEnd: this.onMomentumScrollEnd,
83
+ onScrollBeginDrag: this.listHandlers.onScrollBeginDrag,
84
+ onScrollEndDrag: this.listHandlers.onScrollEndDrag,
85
+ onMomentumScrollBegin: this.listHandlers.onMomentumScrollBegin,
86
+ onMomentumScrollEnd: this.listHandlers.onMomentumScrollEnd,
87
+ onContentSizeChange: this.listHandlers.onContentSizeChange,
71
88
  scrollEventThrottle: this.scrollEventThrottle,
72
89
  keyboardShouldPersistTaps: this.keyboardShouldPersistTaps,
73
90
  keyboardDismissMode: this.keyboardDismissMode,
74
91
  removeClippedSubviews: this.removeClippedSubviews,
75
92
  nestedScrollEnabled: this.nestedScrollEnabled,
93
+ stickyHeaderHiddenOnScroll: this.stickyHeaderHiddenOnScroll,
94
+ innerViewRef: this.innerViewRef,
76
95
  // Android moves the scrollbar back after the `scale: -1` flip
77
96
  isInvertedVirtualizedList: this.isInverted ? true : undefined,
78
97
  contentOffset: this.commandedOffset,
@@ -101,23 +120,23 @@ export abstract class VirtualizedListBagsBase<ItemT>
101
120
  };
102
121
  }
103
122
 
123
+ // The list above scrolls, so RN renders a `View` with no content container
124
+ nestedViewBag(): Record<string, unknown> {
125
+ return {
126
+ ...this.foldedAccessibility(),
127
+ style: this.resolvedStyle,
128
+ onLayout: this.onLayoutTick,
129
+ };
130
+ }
131
+
104
132
  // `onScroll` stays a callback so an `Animated.event` target can flow through it
105
133
  onScrollTick = (event: ISymbioteEvent): void => {
106
134
  countAngular('scrollTicks');
107
- const offset = readScrollOffset(event, this.isHorizontal);
108
- if (offset === undefined) return;
109
- dlog(`Angular VirtualizedList onScroll offset=${offset}`);
110
- // A real native scroll supersedes any pending commanded offset
111
- this.commandedOffset = undefined;
112
- this.dispatch({ kind: LIST_ACTION_KIND.scroll, offset });
113
- this.onScroll?.(event);
135
+ this.listHandlers.onScroll(event);
114
136
  };
115
137
 
116
138
  onLayoutTick = (event: ISymbioteEvent): void => {
117
- const length = readLayoutLength(event, this.isHorizontal);
118
- if (length === undefined) return;
119
- dlog(`Angular VirtualizedList onLayout viewport=${length}`);
120
- this.dispatch({ kind: LIST_ACTION_KIND.layout, length });
139
+ this.listHandlers.onViewportLayout(event);
121
140
  };
122
141
 
123
142
  handleRefresh = (): void => {
@@ -2,7 +2,6 @@
2
2
  // reducer only knows a delay for (the incremental fill and the viewability debounce)
3
3
 
4
4
  import {
5
- EMPTY_OFFSET,
6
5
  LIST_ACTION_KIND,
7
6
  LIST_EFFECT_KIND,
8
7
  type IListAction,
@@ -24,13 +23,14 @@ export type IEffectHost<ItemT> = {
24
23
  }>;
25
24
  };
26
25
 
27
- type IViewableEffect<ItemT> = Extract<
28
- IListEffect<ItemT>,
29
- { kind: typeof LIST_EFFECT_KIND.fireViewable }
26
+ type IScheduledViewable = Extract<
27
+ IListEffect<unknown>,
28
+ { kind: typeof LIST_EFFECT_KIND.scheduleViewable }
30
29
  >;
31
30
 
32
31
  export class ListEffectRunner<ItemT> {
33
- private viewableTimer: ReturnType<typeof setTimeout> | null = null;
32
+ // RN never cancels a `minimumViewTime` timer, each one comes due and the reducer filters it
33
+ private readonly viewableTimers = new Set<ReturnType<typeof setTimeout>>();
34
34
  private batchTimer: ReturnType<typeof setTimeout> | null = null;
35
35
 
36
36
  constructor(private readonly host: IEffectHost<ItemT>) {}
@@ -40,7 +40,8 @@ export class ListEffectRunner<ItemT> {
40
40
  }
41
41
 
42
42
  dispose(): void {
43
- if (this.viewableTimer !== null) clearTimeout(this.viewableTimer);
43
+ for (const timer of this.viewableTimers) clearTimeout(timer);
44
+ this.viewableTimers.clear();
44
45
  if (this.batchTimer !== null) clearTimeout(this.batchTimer);
45
46
  }
46
47
 
@@ -71,7 +72,12 @@ export class ListEffectRunner<ItemT> {
71
72
  this.scheduleRefill(effect.delay);
72
73
  break;
73
74
  case LIST_EFFECT_KIND.fireViewable:
74
- this.fireViewable(effect, inputs);
75
+ inputs.viewabilityPairs[effect.pairIndex]?.onViewableItemsChanged(
76
+ effect.info,
77
+ );
78
+ break;
79
+ case LIST_EFFECT_KIND.scheduleViewable:
80
+ this.scheduleViewable(effect);
75
81
  break;
76
82
  }
77
83
  }
@@ -84,33 +90,15 @@ export class ListEffectRunner<ItemT> {
84
90
  }, delay);
85
91
  }
86
92
 
87
- private fireViewable(
88
- effect: IViewableEffect<ItemT>,
89
- inputs: IListReducerInputs<ItemT>,
90
- ): void {
91
- const fire = (): void => {
92
- for (const pair of inputs.viewabilityPairs) {
93
- pair.onViewableItemsChanged({
94
- ...effect.info,
95
- viewabilityConfig: pair.viewabilityConfig,
96
- });
97
- }
93
+ private scheduleViewable(effect: IScheduledViewable): void {
94
+ const timer = setTimeout(() => {
95
+ this.viewableTimers.delete(timer);
98
96
  this.host.dispatch({
99
- kind: LIST_ACTION_KIND.viewableFired,
100
- map: effect.map,
97
+ kind: LIST_ACTION_KIND.viewableDue,
98
+ pairIndex: effect.pairIndex,
99
+ indices: effect.indices,
101
100
  });
102
- };
103
- if (this.viewableTimer !== null) {
104
- clearTimeout(this.viewableTimer);
105
- this.viewableTimer = null;
106
- }
107
- if (effect.delay <= EMPTY_OFFSET) {
108
- fire();
109
- return;
110
- }
111
- this.viewableTimer = setTimeout(() => {
112
- this.viewableTimer = null;
113
- fire();
114
101
  }, effect.delay);
102
+ this.viewableTimers.add(timer);
115
103
  }
116
104
  }
@@ -70,23 +70,36 @@ export abstract class VirtualizedListHandleBase<ItemT>
70
70
  this.scrollHandle.flashScrollIndicators();
71
71
  }
72
72
 
73
- getNativeScrollRef(): IScrollViewHandle | null {
74
- return this.scrollNode !== null ? this.scrollHandle : null;
73
+ // Null until the scroll tag commits, RN's "no scroll view yet" answer
74
+ private handleOrNull(): IScrollViewHandle | null {
75
+ return this.scrollNode === null ? null : this.scrollHandle;
76
+ }
77
+
78
+ getNativeScrollRef(): ISymbioteNode | null {
79
+ return this.scrollHandle.getScrollNode();
75
80
  }
76
81
 
77
82
  getScrollableNode(): IScrollViewHandle | null {
78
- return this.scrollNode !== null ? this.scrollHandle : null;
83
+ return this.handleOrNull();
79
84
  }
80
85
 
81
86
  getScrollResponder(): IScrollViewHandle | null {
82
- return this.scrollNode !== null ? this.scrollHandle : null;
87
+ return this.handleOrNull();
83
88
  }
84
89
 
85
90
  getScrollNode(): ISymbioteNode | null {
86
91
  return this.scrollHandle.getScrollNode();
87
92
  }
88
93
 
94
+ getScrollRef(): ISymbioteNode | null {
95
+ return this.scrollHandle.getScrollNode();
96
+ }
97
+
89
98
  recordInteraction(): void {
90
99
  this.dispatch({ kind: LIST_ACTION_KIND.recordInteraction });
91
100
  }
101
+
102
+ setNativeProps(props: Record<string, unknown>): void {
103
+ this.scrollNode?.setNativeProps(props);
104
+ }
92
105
  }