@symbiote-native/angular 3.3.0 → 3.4.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 (179) 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 +14 -7
  114. package/build/angular/index.js +21 -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/services/color-scheme.service.d.ts +1 -1
  132. package/package.json +6 -6
  133. package/src/accessibility-inputs.ts +3 -0
  134. package/src/callback-host.ts +56 -8
  135. package/src/components/flat-list/flat-list-handle.ts +64 -0
  136. package/src/components/flat-list/flat-list-template.ts +16 -0
  137. package/src/components/flat-list/index.ts +6 -48
  138. package/src/components/image-inputs.ts +94 -0
  139. package/src/components/image-props-resolve.ts +172 -0
  140. package/src/components/image-shared.ts +26 -291
  141. package/src/components/input-accessory-view-props.ts +12 -8
  142. package/src/components/keyboard-avoiding-view/index.ts +39 -79
  143. package/src/components/layout-conformance-props.ts +8 -0
  144. package/src/components/modal/index.ts +42 -20
  145. package/src/components/refresh-control-props.ts +29 -25
  146. package/src/components/scroll-view-props.ts +84 -76
  147. package/src/components/section-list/index.ts +34 -37
  148. package/src/components/touchable-props.ts +6 -2
  149. package/src/components/virtualized-list/cell-renderer-context.ts +36 -0
  150. package/src/components/virtualized-list/directives.ts +84 -8
  151. package/src/components/virtualized-list/index.ts +118 -85
  152. package/src/components/virtualized-list/list-bags.ts +38 -19
  153. package/src/components/virtualized-list/list-effects.ts +20 -32
  154. package/src/components/virtualized-list/list-handle.ts +17 -4
  155. package/src/components/virtualized-list/list-inputs.ts +47 -0
  156. package/src/components/virtualized-list/list-props.ts +57 -50
  157. package/src/components/virtualized-list/list-rows.ts +66 -0
  158. package/src/components/virtualized-list/list-styles.ts +30 -1
  159. package/src/components/virtualized-list/list-template.ts +159 -73
  160. package/src/components/virtualized-list/nested-scope.ts +22 -0
  161. package/src/components/virtualized-section-list/directives.ts +41 -40
  162. package/src/components/virtualized-section-list/index.ts +114 -118
  163. package/src/components/virtualized-section-list/section-cell.ts +109 -0
  164. package/src/components/virtualized-section-list/section-list-props.ts +79 -0
  165. package/src/components/virtualized-section-list/section-list-template.ts +34 -26
  166. package/src/components.ts +8 -0
  167. package/src/element-base.ts +17 -0
  168. package/src/element-props.ts +35 -33
  169. package/src/element-text-input-native.ts +31 -0
  170. package/src/elements-controls.ts +9 -1
  171. package/src/elements-layout.ts +27 -0
  172. package/src/elements-press.ts +10 -2
  173. package/src/elements.ts +3 -0
  174. package/src/index.ts +72 -22
  175. package/src/modules/animated/index.ts +2 -47
  176. package/src/modules/app-registry/index.ts +3 -1
  177. package/src/modules/status-bar/index.ts +24 -11
  178. package/src/primitives/index.ts +7 -0
  179. package/src/services/color-scheme.service.ts +1 -1
@@ -30,36 +30,60 @@ import {
30
30
  TemplateRef,
31
31
  ViewContainerRef,
32
32
  inject,
33
+ reflectComponentType,
34
+ type ComponentRef,
33
35
  type EmbeddedViewRef,
34
36
  type OnChanges,
35
37
  type OnDestroy,
36
38
  type SimpleChanges,
39
+ type Type,
37
40
  } from '@angular/core';
38
- import { dlog, isDebug } from '@symbiote-native/engine';
41
+ import { dlog, isDebug, type IViewStyle } from '@symbiote-native/engine';
39
42
  import { countAngular } from '../../diagnostics';
40
43
  import type { ISeparators } from '@symbiote-native/components';
41
44
 
42
45
  let vListOutletInstanceCounter = 0;
43
46
 
47
+ function nextOutletInstanceId(): number {
48
+ vListOutletInstanceCounter += 1;
49
+ return vListOutletInstanceCounter;
50
+ }
51
+
44
52
  // The context a `vListItem` template receives, mirroring RN's renderItem info arg
45
53
  // ({ item, index, separators }). `$implicit` is the item, so `let-item` (no key) binds it.
46
- export interface IVListItemContext<ItemT> {
54
+ export type IVListItemContext<ItemT> = {
47
55
  $implicit: ItemT;
48
56
  index: number;
49
57
  separators: ISeparators;
50
- }
58
+ };
51
59
 
52
60
  // The context a `vListSeparator` template receives, mirroring RN's ItemSeparatorComponent props
53
61
  // (the highlight flag the cell toggles + the items flanking the gap). `$implicit` is the highlight
54
62
  // flag, so `let-highlighted` binds it.
55
- export interface IVListSeparatorContext<ItemT> {
63
+ export type IVListSeparatorContext<ItemT> = {
56
64
  $implicit: boolean;
57
65
  highlighted: boolean;
58
66
  leadingItem?: ItemT;
59
67
  trailingItem?: ItemT;
60
68
  // RN lets a row drive arbitrary separator props via separators.updateProps; they ride here.
61
69
  [key: string]: unknown;
62
- }
70
+ };
71
+
72
+ // The context a `vListCell` template receives, RN's `CellRendererComponent` props
73
+ // The template draws the wrapper and stamps the item and its separator through `content`
74
+ // The item is `unknown`, a wrapping list hands it an entry or a row, not its own item type
75
+ export type IVListCellContext = {
76
+ $implicit: unknown;
77
+ cellKey: string;
78
+ index: number;
79
+ style: IViewStyle | undefined;
80
+ // Wire both onto the wrapper: `(layout)="cell.layout($event)"`, `(focus)="cell.focus()"`
81
+ layout: (event: unknown) => void;
82
+ focus: () => void;
83
+ // Stamp it with `[vListOutlet]="cell.content" [vListOutletContext]="cell.contentContext"`
84
+ content: TemplateRef<unknown> | undefined;
85
+ contentContext: unknown;
86
+ };
63
87
 
64
88
  function isRecord(value: unknown): value is Record<string, unknown> {
65
89
  return typeof value === 'object' && value !== null;
@@ -114,6 +138,20 @@ export class VListSeparatorDirective<ItemT = unknown> {
114
138
  }
115
139
  }
116
140
 
141
+ // `<ng-template vListCell>` replaces the view around each cell (RN CellRendererComponent)
142
+ // It must wire `layout` and `focus` itself, as RN's custom cell must wire `onLayout`
143
+ @Directive({ selector: '[vListCell]', standalone: true })
144
+ export class VListCellDirective {
145
+ readonly templateRef = inject<TemplateRef<IVListCellContext>>(TemplateRef);
146
+
147
+ static ngTemplateContextGuard(
148
+ _dir: VListCellDirective,
149
+ _ctx: unknown,
150
+ ): _ctx is IVListCellContext {
151
+ return true;
152
+ }
153
+ }
154
+
117
155
  // Core-only NgTemplateOutlet twin. The adapter cannot import @angular/common's NgTemplateOutlet
118
156
  // (not a dependency), so the list stamps templates through this: it creates the embedded view when
119
157
  // the template changes and updates the live view's context IN PLACE on a context change, so a
@@ -122,19 +160,32 @@ export class VListSeparatorDirective<ItemT = unknown> {
122
160
  export class VListOutletDirective<C = unknown> implements OnChanges, OnDestroy {
123
161
  @Input({ alias: 'vListOutlet' }) templateRef?: TemplateRef<C>;
124
162
  @Input({ alias: 'vListOutletContext' }) context?: C;
163
+ // RN's `ListItemComponent`: stamped instead of the template, its inputs are the item context
164
+ @Input({ alias: 'vListOutletComponent' }) component?: Type<unknown>;
125
165
 
126
166
  private viewRef: EmbeddedViewRef<C> | null = null;
167
+ private componentRef: ComponentRef<unknown> | null = null;
127
168
  private readonly viewContainer = inject(ViewContainerRef);
128
- private readonly instanceId = (vListOutletInstanceCounter += 1);
169
+ private readonly instanceId = nextOutletInstanceId();
129
170
 
130
171
  ngOnChanges(changes: SimpleChanges): void {
131
- if (changes['templateRef'] !== undefined) {
172
+ if (
173
+ changes['templateRef'] !== undefined ||
174
+ changes['component'] !== undefined
175
+ ) {
132
176
  if (isDebug())
133
177
  dlog(
134
- `Angular VListOutlet#${this.instanceId} templateRef CHANGED (was=${changes['templateRef'].previousValue !== undefined} now=${this.templateRef !== undefined}) -> clear + recreate`,
178
+ `Angular VListOutlet#${this.instanceId} templateRef CHANGED (was=${changes['templateRef']?.previousValue !== undefined} now=${this.templateRef !== undefined}) -> clear + recreate`,
135
179
  );
136
180
  this.viewContainer.clear();
137
181
  countAngular('outletCreates');
182
+ this.componentRef = null;
183
+ this.viewRef = null;
184
+ if (this.component !== undefined) {
185
+ this.componentRef = this.viewContainer.createComponent(this.component);
186
+ this.writeComponentInputs(this.componentRef);
187
+ return;
188
+ }
138
189
  this.viewRef =
139
190
  this.templateRef === undefined
140
191
  ? null
@@ -144,6 +195,11 @@ export class VListOutletDirective<C = unknown> implements OnChanges, OnDestroy {
144
195
  );
145
196
  return;
146
197
  }
198
+ if (this.componentRef !== null) {
199
+ countAngular('outletUpdates');
200
+ this.writeComponentInputs(this.componentRef);
201
+ return;
202
+ }
147
203
  if (this.viewRef !== null && this.context !== undefined) {
148
204
  countAngular('outletUpdates');
149
205
  if (isDebug())
@@ -161,6 +217,26 @@ export class VListOutletDirective<C = unknown> implements OnChanges, OnDestroy {
161
217
  this.viewContainer.clear();
162
218
  }
163
219
 
220
+ // The item context becomes the component's inputs, only those it declares: RN hands a
221
+ // `ListItemComponent` item, index and separators, and one may not read all three
222
+ private writeComponentInputs(ref: ComponentRef<unknown>): void {
223
+ const { context } = this;
224
+ if (!isRecord(context)) return;
225
+ const declared = new Set(
226
+ reflectComponentType(ref.componentType)?.inputs.map(
227
+ input => input.propName,
228
+ ),
229
+ );
230
+ const inputs = {
231
+ item: context['$implicit'],
232
+ index: context['index'],
233
+ separators: context['separators'],
234
+ };
235
+ for (const [name, value] of Object.entries(inputs)) {
236
+ if (declared.has(name)) ref.setInput(name, value);
237
+ }
238
+ }
239
+
164
240
  // Copy the new context's fields onto the live embedded view's context object (whose identity the
165
241
  // consumer's `let-` bindings already read), dropping keys that disappeared. The generic `C` can't
166
242
  // be index-WRITTEN even once narrowed (TS2862), so the copy runs in a non-generic helper the
@@ -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 => {