@symbiote-native/angular 0.7.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (188) hide show
  1. package/README.md +3 -3
  2. package/babel-lower-host-primitives.cjs +636 -0
  3. package/babel-register-composed.cjs +21 -3
  4. package/build/angular/anchor-host-registry.js +3 -1
  5. package/build/angular/anchor-host-registry.js.map +1 -1
  6. package/build/angular/bootstrap.js +1 -1
  7. package/build/angular/bootstrap.js.map +1 -1
  8. package/build/angular/components/activity-indicator/index.android.js +7 -2
  9. package/build/angular/components/activity-indicator/index.android.js.map +1 -1
  10. package/build/angular/components/activity-indicator/index.ios.js +3 -1
  11. package/build/angular/components/activity-indicator/index.ios.js.map +1 -1
  12. package/build/angular/components/activity-indicator/shared.d.ts +1 -1
  13. package/build/angular/components/activity-indicator/shared.js +1 -1
  14. package/build/angular/components/activity-indicator/shared.js.map +1 -1
  15. package/build/angular/components/button.d.ts +3 -0
  16. package/build/angular/components/button.js +22 -5
  17. package/build/angular/components/button.js.map +1 -1
  18. package/build/angular/components/flat-list/index.d.ts +1 -1
  19. package/build/angular/components/flat-list/index.js +47 -14
  20. package/build/angular/components/flat-list/index.js.map +1 -1
  21. package/build/angular/components/image/index.android.js +4 -10
  22. package/build/angular/components/image/index.android.js.map +1 -1
  23. package/build/angular/components/image/index.ios.js +4 -10
  24. package/build/angular/components/image/index.ios.js.map +1 -1
  25. package/build/angular/components/image/shared.d.ts +4 -4
  26. package/build/angular/components/image/shared.js +51 -33
  27. package/build/angular/components/image/shared.js.map +1 -1
  28. package/build/angular/components/image-background.d.ts +3 -0
  29. package/build/angular/components/image-background.js +32 -9
  30. package/build/angular/components/image-background.js.map +1 -1
  31. package/build/angular/components/input-accessory-view/index.d.ts +2 -1
  32. package/build/angular/components/input-accessory-view/index.js +18 -16
  33. package/build/angular/components/input-accessory-view/index.js.map +1 -1
  34. package/build/angular/components/keyboard-avoiding-view/index.d.ts +3 -1
  35. package/build/angular/components/keyboard-avoiding-view/index.js +56 -19
  36. package/build/angular/components/keyboard-avoiding-view/index.js.map +1 -1
  37. package/build/angular/components/modal/index.d.ts +1 -0
  38. package/build/angular/components/modal/index.js +21 -10
  39. package/build/angular/components/modal/index.js.map +1 -1
  40. package/build/angular/components/pressable/index.d.ts +10 -5
  41. package/build/angular/components/pressable/index.js +61 -16
  42. package/build/angular/components/pressable/index.js.map +1 -1
  43. package/build/angular/components/refresh-control/index.d.ts +2 -1
  44. package/build/angular/components/refresh-control/index.js +13 -9
  45. package/build/angular/components/refresh-control/index.js.map +1 -1
  46. package/build/angular/components/safe-area-view/index.d.ts +3 -1
  47. package/build/angular/components/safe-area-view/index.js +22 -20
  48. package/build/angular/components/safe-area-view/index.js.map +1 -1
  49. package/build/angular/components/scroll-view/index.android.js +44 -14
  50. package/build/angular/components/scroll-view/index.android.js.map +1 -1
  51. package/build/angular/components/scroll-view/index.ios.js +13 -5
  52. package/build/angular/components/scroll-view/index.ios.js.map +1 -1
  53. package/build/angular/components/scroll-view/projection.d.ts +32 -0
  54. package/build/angular/components/scroll-view/projection.js +212 -47
  55. package/build/angular/components/scroll-view/projection.js.map +1 -1
  56. package/build/angular/components/scroll-view/shared.d.ts +2 -0
  57. package/build/angular/components/scroll-view/shared.js +48 -11
  58. package/build/angular/components/scroll-view/shared.js.map +1 -1
  59. package/build/angular/components/scroll-view/sticky-header.js +7 -3
  60. package/build/angular/components/scroll-view/sticky-header.js.map +1 -1
  61. package/build/angular/components/section-list/index.d.ts +9 -1
  62. package/build/angular/components/section-list/index.js +72 -18
  63. package/build/angular/components/section-list/index.js.map +1 -1
  64. package/build/angular/components/switch/index.android.js +23 -9
  65. package/build/angular/components/switch/index.android.js.map +1 -1
  66. package/build/angular/components/switch/index.ios.js +23 -9
  67. package/build/angular/components/switch/index.ios.js.map +1 -1
  68. package/build/angular/components/switch/shared.d.ts +1 -1
  69. package/build/angular/components/switch/shared.js +5 -2
  70. package/build/angular/components/switch/shared.js.map +1 -1
  71. package/build/angular/components/text-input.d.ts +8 -2
  72. package/build/angular/components/text-input.js +79 -34
  73. package/build/angular/components/text-input.js.map +1 -1
  74. package/build/angular/components/touchable/index.d.ts +36 -7
  75. package/build/angular/components/touchable/index.js +280 -72
  76. package/build/angular/components/touchable/index.js.map +1 -1
  77. package/build/angular/components/touchable-native-feedback/index.d.ts +2 -1
  78. package/build/angular/components/touchable-native-feedback/index.js +19 -17
  79. package/build/angular/components/touchable-native-feedback/index.js.map +1 -1
  80. package/build/angular/components/virtualized-list/directives.js +4 -0
  81. package/build/angular/components/virtualized-list/directives.js.map +1 -1
  82. package/build/angular/components/virtualized-list/index.d.ts +8 -2
  83. package/build/angular/components/virtualized-list/index.js +207 -61
  84. package/build/angular/components/virtualized-list/index.js.map +1 -1
  85. package/build/angular/components/virtualized-section-list/directives.js.map +1 -1
  86. package/build/angular/components/virtualized-section-list/index.d.ts +21 -1
  87. package/build/angular/components/virtualized-section-list/index.js +100 -22
  88. package/build/angular/components/virtualized-section-list/index.js.map +1 -1
  89. package/build/angular/components.d.ts +2 -2
  90. package/build/angular/components.js +1 -1
  91. package/build/angular/components.js.map +1 -1
  92. package/build/angular/create-portal/index.d.ts +11 -2
  93. package/build/angular/create-portal/index.js +42 -12
  94. package/build/angular/create-portal/index.js.map +1 -1
  95. package/build/angular/create-tunnel/index.js.map +1 -1
  96. package/build/angular/descriptor-to-angular/index.js +3 -1
  97. package/build/angular/descriptor-to-angular/index.js.map +1 -1
  98. package/build/angular/diagnostics.d.ts +33 -0
  99. package/build/angular/diagnostics.js +82 -0
  100. package/build/angular/diagnostics.js.map +1 -0
  101. package/build/angular/gate-demand.d.ts +37 -0
  102. package/build/angular/gate-demand.js +84 -0
  103. package/build/angular/gate-demand.js.map +1 -0
  104. package/build/angular/host-instance/index.js +1 -1
  105. package/build/angular/host-instance/index.js.map +1 -1
  106. package/build/angular/index.d.ts +6 -2
  107. package/build/angular/index.js +16 -1
  108. package/build/angular/index.js.map +1 -1
  109. package/build/angular/modules/animated/create-animated-component.d.ts +1 -0
  110. package/build/angular/modules/animated/create-animated-component.js +41 -18
  111. package/build/angular/modules/animated/create-animated-component.js.map +1 -1
  112. package/build/angular/modules/app-registry/index.js.map +1 -1
  113. package/build/angular/modules/status-bar/index.js +1 -1
  114. package/build/angular/modules/status-bar/index.js.map +1 -1
  115. package/build/angular/primitives/index.d.ts +31 -2
  116. package/build/angular/primitives/index.js +81 -3
  117. package/build/angular/primitives/index.js.map +1 -1
  118. package/build/angular/primitives/shared.d.ts +9 -1
  119. package/build/angular/primitives/shared.js +49 -3
  120. package/build/angular/primitives/shared.js.map +1 -1
  121. package/build/angular/register.d.ts +1 -0
  122. package/build/angular/register.js +35 -0
  123. package/build/angular/register.js.map +1 -0
  124. package/build/angular/render/index.js +51 -6
  125. package/build/angular/render/index.js.map +1 -1
  126. package/build/angular/renderer/index.d.ts +1 -0
  127. package/build/angular/renderer/index.js +157 -21
  128. package/build/angular/renderer/index.js.map +1 -1
  129. package/build/angular/services/color-scheme.service.js.map +1 -1
  130. package/build/angular/services/window-dimensions.service.js +1 -1
  131. package/build/angular/services/window-dimensions.service.js.map +1 -1
  132. package/build/angular/state-style.d.ts +9 -0
  133. package/build/angular/state-style.js +39 -0
  134. package/build/angular/state-style.js.map +1 -0
  135. package/metro-transformer.cjs +69 -0
  136. package/package.json +27 -8
  137. package/src/anchor-host-registry.ts +3 -1
  138. package/src/bootstrap.ts +4 -1
  139. package/src/components/activity-indicator/index.android.ts +7 -2
  140. package/src/components/activity-indicator/index.ios.ts +3 -1
  141. package/src/components/activity-indicator/shared.ts +32 -13
  142. package/src/components/button.ts +36 -6
  143. package/src/components/flat-list/index.ts +91 -29
  144. package/src/components/image/index.android.ts +9 -6
  145. package/src/components/image/index.ios.ts +9 -6
  146. package/src/components/image/shared.ts +110 -45
  147. package/src/components/image-background.ts +47 -10
  148. package/src/components/input-accessory-view/index.ts +41 -18
  149. package/src/components/keyboard-avoiding-view/index.ts +76 -22
  150. package/src/components/modal/index.ts +39 -12
  151. package/src/components/pressable/index.ts +121 -31
  152. package/src/components/refresh-control/index.ts +36 -13
  153. package/src/components/safe-area-view/index.ts +44 -19
  154. package/src/components/scroll-view/index.android.ts +31 -9
  155. package/src/components/scroll-view/index.ios.ts +17 -4
  156. package/src/components/scroll-view/projection.ts +290 -63
  157. package/src/components/scroll-view/shared.ts +124 -30
  158. package/src/components/scroll-view/sticky-header.ts +25 -8
  159. package/src/components/section-list/index.ts +79 -19
  160. package/src/components/switch/index.android.ts +18 -7
  161. package/src/components/switch/index.ios.ts +18 -7
  162. package/src/components/switch/shared.ts +15 -4
  163. package/src/components/text-input.ts +119 -31
  164. package/src/components/touchable/index.ts +339 -72
  165. package/src/components/touchable-native-feedback/index.ts +50 -18
  166. package/src/components/virtualized-list/directives.ts +19 -5
  167. package/src/components/virtualized-list/index.ts +292 -72
  168. package/src/components/virtualized-section-list/directives.ts +6 -3
  169. package/src/components/virtualized-section-list/index.ts +164 -34
  170. package/src/components.ts +6 -1
  171. package/src/create-portal/index.ts +47 -11
  172. package/src/create-tunnel/index.ts +3 -1
  173. package/src/descriptor-to-angular/index.ts +29 -8
  174. package/src/diagnostics.ts +138 -0
  175. package/src/gate-demand.ts +109 -0
  176. package/src/host-instance/index.ts +9 -3
  177. package/src/index.ts +33 -2
  178. package/src/modules/animated/create-animated-component.ts +67 -19
  179. package/src/modules/app-registry/index.ts +3 -1
  180. package/src/modules/status-bar/index.ts +13 -4
  181. package/src/primitives/index.ts +73 -2
  182. package/src/primitives/shared.ts +70 -9
  183. package/src/register.ts +43 -0
  184. package/src/render/index.ts +59 -7
  185. package/src/renderer/index.ts +209 -37
  186. package/src/services/color-scheme.service.ts +3 -1
  187. package/src/services/window-dimensions.service.ts +11 -4
  188. package/src/state-style.ts +39 -0
@@ -46,7 +46,8 @@ export interface IVSectionContext<ItemT> {
46
46
  // `<ng-template vSectionItem>` — one section item row. The static guard types the `let-` bindings.
47
47
  @Directive({ selector: '[vSectionItem]', standalone: true })
48
48
  export class VSectionItemDirective<ItemT = unknown> {
49
- readonly templateRef = inject<TemplateRef<IVSectionItemContext<ItemT>>>(TemplateRef);
49
+ readonly templateRef =
50
+ inject<TemplateRef<IVSectionItemContext<ItemT>>>(TemplateRef);
50
51
 
51
52
  static ngTemplateContextGuard<T>(
52
53
  _dir: VSectionItemDirective<T>,
@@ -59,7 +60,8 @@ export class VSectionItemDirective<ItemT = unknown> {
59
60
  // `<ng-template vSectionHeader>` — rendered above each section's items (RN renderSectionHeader).
60
61
  @Directive({ selector: '[vSectionHeader]', standalone: true })
61
62
  export class VSectionHeaderDirective<ItemT = unknown> {
62
- readonly templateRef = inject<TemplateRef<IVSectionContext<ItemT>>>(TemplateRef);
63
+ readonly templateRef =
64
+ inject<TemplateRef<IVSectionContext<ItemT>>>(TemplateRef);
63
65
 
64
66
  static ngTemplateContextGuard<T>(
65
67
  _dir: VSectionHeaderDirective<T>,
@@ -72,7 +74,8 @@ export class VSectionHeaderDirective<ItemT = unknown> {
72
74
  // `<ng-template vSectionFooter>` — rendered below each section's items (RN renderSectionFooter).
73
75
  @Directive({ selector: '[vSectionFooter]', standalone: true })
74
76
  export class VSectionFooterDirective<ItemT = unknown> {
75
- readonly templateRef = inject<TemplateRef<IVSectionContext<ItemT>>>(TemplateRef);
77
+ readonly templateRef =
78
+ inject<TemplateRef<IVSectionContext<ItemT>>>(TemplateRef);
76
79
 
77
80
  static ngTemplateContextGuard<T>(
78
81
  _dir: VSectionFooterDirective<T>,
@@ -64,7 +64,10 @@ import {
64
64
  type IVListSeparatorContext,
65
65
  } from '../virtualized-list';
66
66
  import { VListOutletDirective } from '../virtualized-list/directives';
67
- import { stableAnchorStyle, SymbioteStyleInputDirective } from '../../primitives';
67
+ import {
68
+ stableAnchorStyle,
69
+ SymbioteStyleInputDirective,
70
+ } from '../../primitives';
68
71
  import {
69
72
  VSectionFooterDirective,
70
73
  VSectionHeaderDirective,
@@ -85,6 +88,13 @@ export {
85
88
  VSectionItemDirective,
86
89
  VSectionSeparatorDirective,
87
90
  } from './directives';
91
+ import {
92
+ gateWanted,
93
+ injectGateDemandAbove,
94
+ provideGateDemand,
95
+ type IGateDemand,
96
+ type IGatedAccessibilityEvent,
97
+ } from '../../gate-demand';
88
98
  export type { IVSectionContext, IVSectionItemContext } from './directives';
89
99
 
90
100
  // The Angular VirtualizedSectionList prop surface. Mirrors React/Vue's IVirtualizedSectionListProps
@@ -92,9 +102,20 @@ export type { IVSectionContext, IVSectionItemContext } from './directives';
92
102
  // those are the per-adapter children/render fields and become `<ng-template>` directives in Angular.
93
103
  // Everything agnostic is the SAME surface as the
94
104
  // React/Vue adapters, including the a11y/aria prop family forwarded through VirtualizedList.
95
- export interface IVirtualizedSectionListProps<ItemT> extends IAccessibilityProps, IAriaProps {
105
+ export interface IVirtualizedSectionListProps<ItemT>
106
+ extends IAccessibilityProps, IAriaProps {
96
107
  sections: ReadonlyArray<ISection<ItemT>>;
97
108
  keyExtractor?: (item: ItemT, index: number) => string;
109
+ // Fixed-layout fast path, FLAT like RN's: the SECTIONS array plus a flat entry index, where every
110
+ // section contributes two rows beyond its items (header, footer) and the caller accounts for them.
111
+ // A `({ section, index })` form would be our invention, not parity - `VirtualizedSectionList.js`
112
+ // has no getItemLayout code at all, the prop rides through `passThroughProps`; that shape is the
113
+ // community react-native-section-list-get-item-layout, layered on top. Without it a fast scroll
114
+ // outruns measurement and leaves blank windows.
115
+ getItemLayout?: (
116
+ data: ReadonlyArray<ISection<ItemT>> | null,
117
+ index: number,
118
+ ) => { length: number; offset: number; index: number };
98
119
  // Stick each section header to the top as the next section scrolls up. Routed to the inner
99
120
  // VirtualizedList's stickyHeaderIndices. Defaults to `Platform.OS === 'ios'`; Android does not
100
121
  // stick by default. Pass true/false to override.
@@ -146,7 +167,10 @@ export type IVirtualizedSectionListInputs<ItemT> = Omit<
146
167
  @Component({
147
168
  selector: 'VirtualizedSectionList',
148
169
  standalone: true,
149
- hostDirectives: [{ directive: SymbioteStyleInputDirective, inputs: ['style'] }],
170
+ viewProviders: [provideGateDemand(() => VirtualizedSectionList)],
171
+ hostDirectives: [
172
+ { directive: SymbioteStyleInputDirective, inputs: ['style'] },
173
+ ],
150
174
  imports: [
151
175
  VirtualizedList,
152
176
  VListItemDirective,
@@ -163,6 +187,7 @@ export type IVirtualizedSectionListInputs<ItemT> = Omit<
163
187
  [getItem]="getEntry"
164
188
  [getItemCount]="getEntryCount"
165
189
  [keyExtractor]="entryKeyExtractor"
190
+ [getItemLayout]="entryItemLayout"
166
191
  [stickyHeaderIndices]="stickyHeaderIndices"
167
192
  [extraData]="extraData"
168
193
  [inverted]="inverted"
@@ -207,8 +232,12 @@ export type IVirtualizedSectionListInputs<ItemT> = Omit<
207
232
  [accessibilityElementsHidden]="accessibilityElementsHidden"
208
233
  [accessibilityIgnoresInvertColors]="accessibilityIgnoresInvertColors"
209
234
  [accessibilityLanguage]="accessibilityLanguage"
210
- [accessibilityRespondsToUserInteraction]="accessibilityRespondsToUserInteraction"
211
- [accessibilityShowsLargeContentViewer]="accessibilityShowsLargeContentViewer"
235
+ [accessibilityRespondsToUserInteraction]="
236
+ accessibilityRespondsToUserInteraction
237
+ "
238
+ [accessibilityShowsLargeContentViewer]="
239
+ accessibilityShowsLargeContentViewer
240
+ "
212
241
  [accessibilityLargeContentTitle]="accessibilityLargeContentTitle"
213
242
  (accessibilityAction)="resolvedOnAccessibilityAction?.($event)"
214
243
  (accessibilityTap)="resolvedOnAccessibilityTap?.($event)"
@@ -246,7 +275,9 @@ export type IVirtualizedSectionListInputs<ItemT> = Omit<
246
275
  ></ng-container>
247
276
  }
248
277
  @case ('section-separator') {
249
- <ng-container [vListOutlet]="sectionSeparatorDir?.templateRef"></ng-container>
278
+ <ng-container
279
+ [vListOutlet]="sectionSeparatorDir?.templateRef"
280
+ ></ng-container>
250
281
  }
251
282
  @case ('item') {
252
283
  <ng-container
@@ -260,12 +291,16 @@ export type IVirtualizedSectionListInputs<ItemT> = Omit<
260
291
  <!-- list-level slots: forward the app's VirtualizedList directives to the inner list. -->
261
292
  @if (listHeaderDir !== undefined) {
262
293
  <ng-template vListHeader>
263
- <ng-container [vListOutlet]="listHeaderDir.templateRef"></ng-container>
294
+ <ng-container
295
+ [vListOutlet]="listHeaderDir.templateRef"
296
+ ></ng-container>
264
297
  </ng-template>
265
298
  }
266
299
  @if (listFooterDir !== undefined) {
267
300
  <ng-template vListFooter>
268
- <ng-container [vListOutlet]="listFooterDir.templateRef"></ng-container>
301
+ <ng-container
302
+ [vListOutlet]="listFooterDir.templateRef"
303
+ ></ng-container>
269
304
  </ng-template>
270
305
  }
271
306
  @if (listEmptyDir !== undefined) {
@@ -283,7 +318,9 @@ export type IVirtualizedSectionListInputs<ItemT> = Omit<
283
318
  >
284
319
  <ng-container
285
320
  [vListOutlet]="itemSeparatorDir.templateRef"
286
- [vListOutletContext]="itemSeparatorContextOf(highlighted, leadingItem, trailingItem)"
321
+ [vListOutletContext]="
322
+ itemSeparatorContextOf(highlighted, leadingItem, trailingItem)
323
+ "
287
324
  ></ng-container>
288
325
  </ng-template>
289
326
  }
@@ -291,15 +328,26 @@ export type IVirtualizedSectionListInputs<ItemT> = Omit<
291
328
  `,
292
329
  })
293
330
  export class VirtualizedSectionList<ItemT = unknown>
294
- implements IVirtualizedSectionListInputs<ItemT>, IVirtualizedSectionListHandle, DoCheck
331
+ implements
332
+ IVirtualizedSectionListInputs<ItemT>,
333
+ IVirtualizedSectionListHandle,
334
+ DoCheck
295
335
  {
296
336
  @Input({ required: true }) sections!: ReadonlyArray<ISection<ItemT>>;
297
337
  @Input() keyExtractor?: (item: ItemT, index: number) => string;
338
+ @Input() getItemLayout?: (
339
+ data: ReadonlyArray<ISection<ItemT>> | null,
340
+ index: number,
341
+ ) => { length: number; offset: number; index: number };
298
342
  @Input() stickySectionHeadersEnabled?: boolean;
299
343
  @Input() extraData?: unknown;
300
- @Output() readonly endReached = new EventEmitter<{ distanceFromEnd: number }>();
344
+ @Output() readonly endReached = new EventEmitter<{
345
+ distanceFromEnd: number;
346
+ }>();
301
347
  @Input() onEndReachedThreshold?: number;
302
- @Output() readonly startReached = new EventEmitter<{ distanceFromStart: number }>();
348
+ @Output() readonly startReached = new EventEmitter<{
349
+ distanceFromStart: number;
350
+ }>();
303
351
  @Input() onStartReachedThreshold?: number;
304
352
  @Output() readonly refresh = new EventEmitter<void>();
305
353
  @Input() refreshing?: boolean | null;
@@ -360,8 +408,10 @@ export class VirtualizedSectionList<ItemT = unknown>
360
408
  @Input() accessibilityValue?: IAccessibilityProps['accessibilityValue'];
361
409
  @Input() accessibilityActions?: IAccessibilityProps['accessibilityActions'];
362
410
  @Input() accessibilityLabelledBy?: string | string[];
363
- @Input() importantForAccessibility?: IAccessibilityProps['importantForAccessibility'];
364
- @Input() accessibilityLiveRegion?: IAccessibilityProps['accessibilityLiveRegion'];
411
+ @Input()
412
+ importantForAccessibility?: IAccessibilityProps['importantForAccessibility'];
413
+ @Input()
414
+ accessibilityLiveRegion?: IAccessibilityProps['accessibilityLiveRegion'];
365
415
  @Input() screenReaderFocusable?: boolean;
366
416
  @Input() accessibilityViewIsModal?: boolean;
367
417
  @Input() accessibilityElementsHidden?: boolean;
@@ -374,6 +424,16 @@ export class VirtualizedSectionList<ItemT = unknown>
374
424
  @Output() readonly accessibilityTap = new EventEmitter<ISymbioteEvent>();
375
425
  @Output() readonly magicTap = new EventEmitter<ISymbioteEvent>();
376
426
  @Output() readonly accessibilityEscape = new EventEmitter<ISymbioteEvent>();
427
+
428
+ // This wrapper binds the four gated accessibility events on the component it renders, which
429
+ // Angular forces to be unconditional and which would light that component's gates on every
430
+ // instance. It answers for them instead — see `gate-demand.ts`.
431
+ private readonly gateDemandAbove = injectGateDemandAbove();
432
+
433
+ wantsGate(name: IGatedAccessibilityEvent): boolean {
434
+ return gateWanted(this.gateDemandAbove, name, this[name]);
435
+ }
436
+
377
437
  @Input() role?: IAriaProps['role'];
378
438
  @Input() ariaLabel?: string;
379
439
  @Input() ariaLabelledBy?: string;
@@ -393,20 +453,27 @@ export class VirtualizedSectionList<ItemT = unknown>
393
453
  // The section cell templates the app authors (the Angular twin of renderItem /
394
454
  // renderSectionHeader / renderSectionFooter / SectionSeparatorComponent), captured from projected
395
455
  // <ng-template> content and stamped by the synthesized vListItem dispatch.
396
- @ContentChild(VSectionItemDirective) sectionItemDir?: VSectionItemDirective<ItemT>;
397
- @ContentChild(VSectionHeaderDirective) sectionHeaderDir?: VSectionHeaderDirective<ItemT>;
398
- @ContentChild(VSectionFooterDirective) sectionFooterDir?: VSectionFooterDirective<ItemT>;
399
- @ContentChild(VSectionSeparatorDirective) sectionSeparatorDir?: VSectionSeparatorDirective;
456
+ @ContentChild(VSectionItemDirective)
457
+ sectionItemDir?: VSectionItemDirective<ItemT>;
458
+ @ContentChild(VSectionHeaderDirective)
459
+ sectionHeaderDir?: VSectionHeaderDirective<ItemT>;
460
+ @ContentChild(VSectionFooterDirective)
461
+ sectionFooterDir?: VSectionFooterDirective<ItemT>;
462
+ @ContentChild(VSectionSeparatorDirective)
463
+ sectionSeparatorDir?: VSectionSeparatorDirective;
400
464
 
401
465
  // list-level slots reuse VirtualizedList's own directives; VSL forwards them to the inner list.
402
466
  @ContentChild(VListHeaderDirective) listHeaderDir?: VListHeaderDirective;
403
467
  @ContentChild(VListFooterDirective) listFooterDir?: VListFooterDirective;
404
468
  @ContentChild(VListEmptyDirective) listEmptyDir?: VListEmptyDirective;
405
- @ContentChild(VListSeparatorDirective) itemSeparatorDir?: VListSeparatorDirective<ItemT>;
469
+ @ContentChild(VListSeparatorDirective)
470
+ itemSeparatorDir?: VListSeparatorDirective<ItemT>;
406
471
 
407
472
  // The composed inner VirtualizedList. Its instance IS an IVirtualizedListHandle, so the section
408
473
  // handle delegates straight to it. Available from ngAfterViewInit; reads lazily, no-ops pre-commit.
409
- @ViewChild(VirtualizedList) private list?: VirtualizedList<ISectionEntry<ItemT>>;
474
+ @ViewChild(VirtualizedList) private list?: VirtualizedList<
475
+ ISectionEntry<ItemT>
476
+ >;
410
477
 
411
478
  // This component's OWN host — the non-painting anchor `class="..."` at the use site resolves
412
479
  // onto (see anchorHostStyle's doc comment) — NOT `list` above, which targets the real inner
@@ -439,7 +506,10 @@ export class VirtualizedSectionList<ItemT = unknown>
439
506
  this.lastHasSectionSeparator = hasSectionSeparator;
440
507
 
441
508
  const sections = this.sections ?? [];
442
- const { entries, headerIndices } = flattenSections(sections, hasSectionSeparator);
509
+ const { entries, headerIndices } = flattenSections(
510
+ sections,
511
+ hasSectionSeparator,
512
+ );
443
513
  this.flatEntries = entries;
444
514
  this.headerIndices = headerIndices;
445
515
 
@@ -458,42 +528,95 @@ export class VirtualizedSectionList<ItemT = unknown>
458
528
 
459
529
  // The derived flat-stream accessors handed to the inner VirtualizedList. Stable arrow fields so
460
530
  // the bindings keep a constant identity; they read the live flatEntries.
461
- getEntry = (_source: unknown, index: number): ISectionEntry<ItemT> => this.flatEntries[index];
531
+ getEntry = (_source: unknown, index: number): ISectionEntry<ItemT> =>
532
+ this.flatEntries[index];
462
533
  getEntryCount = (): number => this.flatEntries.length;
463
534
  entryKeyExtractor = (entry: ISectionEntry<ItemT>, index: number): string =>
464
535
  sectionEntryKey(entry, index, this.keyExtractor);
465
536
 
537
+ // Hand the callback `sections`, not the entries: RN's inner VirtualizedList gets
538
+ // `data={this.props.sections}` (VirtualizedSectionList.js:216) while ours streams the FLATTENED
539
+ // entries, so the same user code would otherwise see a different argument here than on RN.
540
+ //
541
+ // UPSTREAM-DIVERGENCE(react-native): the flat INDEX matches RN's (two rows per section, header
542
+ // and footer) only while the vSectionSeparator template is unset. With it, flattenSections emits
543
+ // an extra 'section-separator' row per boundary that RN renders inside the neighbouring cell, so
544
+ // indices shift by one per boundary from the second section on. Deliberate - that row is how this
545
+ // adapter paints the separator; a caller combining the two must account for it.
546
+ //
547
+ // Cached on the input's own identity, like resolvedStyle above: the inner VirtualizedList folds
548
+ // getItemLayout into its ngDoCheck dedup array, so a wrapper allocated per getter read would
549
+ // re-run its whole recompute every CD.
550
+ private cachedEntryItemLayout?: (
551
+ data: unknown,
552
+ index: number,
553
+ ) => { length: number; offset: number; index: number };
554
+ private lastGetItemLayout?: IVirtualizedSectionListProps<ItemT>['getItemLayout'];
555
+ get entryItemLayout():
556
+ | ((
557
+ data: unknown,
558
+ index: number,
559
+ ) => { length: number; offset: number; index: number })
560
+ | undefined {
561
+ if (this.getItemLayout !== this.lastGetItemLayout) {
562
+ const getItemLayout = this.getItemLayout;
563
+ this.lastGetItemLayout = getItemLayout;
564
+ this.cachedEntryItemLayout =
565
+ getItemLayout === undefined
566
+ ? undefined
567
+ : (
568
+ _entries: unknown,
569
+ index: number,
570
+ ): { length: number; offset: number; index: number } =>
571
+ getItemLayout(this.sections, index);
572
+ }
573
+ return this.cachedEntryItemLayout;
574
+ }
575
+
466
576
  // Adapts each @Output() into the plain callback VirtualizedList's own @Input() still wants,
467
577
  // gated on `.observed` so an unlistened event forwards `undefined` — the same "nobody cares"
468
578
  // contract the old @Input() callback passthrough had (e.g. onRefresh presence gates whether a
469
579
  // RefreshControl is built downstream; see the Vue twin's `listens('onRefresh')` gate).
470
- get resolvedOnEndReached(): ((info: { distanceFromEnd: number }) => void) | undefined {
471
- return this.endReached.observed ? info => this.endReached.emit(info) : undefined;
580
+ get resolvedOnEndReached():
581
+ ((info: { distanceFromEnd: number }) => void) | undefined {
582
+ return this.endReached.observed
583
+ ? info => this.endReached.emit(info)
584
+ : undefined;
472
585
  }
473
586
 
474
- get resolvedOnStartReached(): ((info: { distanceFromStart: number }) => void) | undefined {
475
- return this.startReached.observed ? info => this.startReached.emit(info) : undefined;
587
+ get resolvedOnStartReached():
588
+ ((info: { distanceFromStart: number }) => void) | undefined {
589
+ return this.startReached.observed
590
+ ? info => this.startReached.emit(info)
591
+ : undefined;
476
592
  }
477
593
 
478
594
  get resolvedOnRefresh(): (() => void) | undefined {
479
595
  return this.refresh.observed ? () => this.refresh.emit() : undefined;
480
596
  }
481
597
 
482
- get resolvedOnAccessibilityAction(): ((event: ISymbioteEvent) => void) | undefined {
598
+ get resolvedOnAccessibilityAction():
599
+ ((event: ISymbioteEvent) => void) | undefined {
483
600
  return this.accessibilityAction.observed
484
601
  ? event => this.accessibilityAction.emit(event)
485
602
  : undefined;
486
603
  }
487
604
 
488
- get resolvedOnAccessibilityTap(): ((event: ISymbioteEvent) => void) | undefined {
489
- return this.accessibilityTap.observed ? event => this.accessibilityTap.emit(event) : undefined;
605
+ get resolvedOnAccessibilityTap():
606
+ ((event: ISymbioteEvent) => void) | undefined {
607
+ return this.accessibilityTap.observed
608
+ ? event => this.accessibilityTap.emit(event)
609
+ : undefined;
490
610
  }
491
611
 
492
612
  get resolvedOnMagicTap(): ((event: ISymbioteEvent) => void) | undefined {
493
- return this.magicTap.observed ? event => this.magicTap.emit(event) : undefined;
613
+ return this.magicTap.observed
614
+ ? event => this.magicTap.emit(event)
615
+ : undefined;
494
616
  }
495
617
 
496
- get resolvedOnAccessibilityEscape(): ((event: ISymbioteEvent) => void) | undefined {
618
+ get resolvedOnAccessibilityEscape():
619
+ ((event: ISymbioteEvent) => void) | undefined {
497
620
  return this.accessibilityEscape.observed
498
621
  ? event => this.accessibilityEscape.emit(event)
499
622
  : undefined;
@@ -516,7 +639,10 @@ export class VirtualizedSectionList<ItemT = unknown>
516
639
  return { $implicit: value.section, section: value.section };
517
640
  }
518
641
 
519
- itemContextOf(value: unknown, separators: ISeparators): IVSectionItemContext<ItemT> | undefined {
642
+ itemContextOf(
643
+ value: unknown,
644
+ separators: ISeparators,
645
+ ): IVSectionItemContext<ItemT> | undefined {
520
646
  if (!this.isEntry(value) || value.kind !== 'item') return undefined;
521
647
  return {
522
648
  $implicit: value.item,
@@ -539,8 +665,12 @@ export class VirtualizedSectionList<ItemT = unknown>
539
665
  return {
540
666
  $implicit: highlighted === true,
541
667
  highlighted: highlighted === true,
542
- leadingItem: this.isEntry(leadingItem) ? unwrapEntryItem(leadingItem) : undefined,
543
- trailingItem: this.isEntry(trailingItem) ? unwrapEntryItem(trailingItem) : undefined,
668
+ leadingItem: this.isEntry(leadingItem)
669
+ ? unwrapEntryItem(leadingItem)
670
+ : undefined,
671
+ trailingItem: this.isEntry(trailingItem)
672
+ ? unwrapEntryItem(trailingItem)
673
+ : undefined,
544
674
  };
545
675
  }
546
676
 
package/src/components.ts CHANGED
@@ -112,7 +112,10 @@ export type {
112
112
  IVSectionItemContext,
113
113
  } from './components/virtualized-section-list';
114
114
  export { SectionList } from './components/section-list';
115
- export type { ISectionListHandle, ISectionListProps } from './components/section-list';
115
+ export type {
116
+ ISectionListHandle,
117
+ ISectionListProps,
118
+ } from './components/section-list';
116
119
 
117
120
  // Re-export primitive hosts so composed components can import them from the public barrel too.
118
121
  export {
@@ -127,6 +130,8 @@ export {
127
130
  HorizontalScrollContentView,
128
131
  TextInputHost,
129
132
  MultilineTextInputHost,
133
+ ManagedTextInputHost,
134
+ ManagedMultilineTextInputHost,
130
135
  SwitchHost,
131
136
  ActivityIndicatorHost,
132
137
  SafeAreaViewHost,
@@ -20,16 +20,23 @@
20
20
  //
21
21
  // The safe, fully-public-API mechanism: create the embedded view DIRECTLY inside a
22
22
  // ViewContainerRef anchored at the destination, so there is nothing to move at all. That
23
- // ViewContainerRef comes from `PortalOutletDirective`, a marker placed on the target host
24
- // (`<View portalOutlet #overlayHost="portalOutlet">`) purely to expose its ViewContainerRef, the
25
- // same export-as-template-variable idiom `#form="ngForm"` uses. This also replaces the
26
- // `isSymbioteNode` runtime guard React/Vue need (there `to` is an arbitrary JS value until
27
- // validated): here `to` is typed as `PortalOutletDirective` the only way to construct one is
28
- // Angular's own template compiler resolving a template reference variable, so `strictTemplates`
29
- // rejects anything else at compile time and there is nothing left to guard at runtime.
23
+ // ViewContainerRef comes from `PortalOutletDirective`, exported to a template variable with the
24
+ // same idiom `#form="ngForm"` uses. This also replaces the `isSymbioteNode` runtime guard
25
+ // React/Vue need (there `to` is an arbitrary JS value until validated): here `to` is typed as
26
+ // `PortalOutletDirective` the only way to construct one is Angular's own template compiler
27
+ // resolving a template reference variable, so `strictTemplates` rejects anything else at compile
28
+ // time and there is nothing left to guard at runtime.
29
+ //
30
+ // THE MARKER GOES ON AN `<ng-container>` INSIDE THE TARGET, never on the target element. A
31
+ // ViewContainerRef anchors AT its host and `createEmbeddedView` inserts after that anchor, so
32
+ // `<View portalOutlet>` delivers a SIBLING where React's `createPortal(node, host)` and Vue's
33
+ // `<Teleport to>` deliver a child. Device-reported 2026-09-02: a card portaled into an absolutely
34
+ // positioned overlay laid out in the scroll flow, since the overlay cannot centre a node that is
35
+ // not in it.
30
36
 
31
37
  import {
32
38
  Directive,
39
+ ElementRef,
33
40
  inject,
34
41
  Input,
35
42
  TemplateRef,
@@ -38,13 +45,40 @@ import {
38
45
  type OnChanges,
39
46
  type OnDestroy,
40
47
  } from '@angular/core';
48
+ import { isAnchor, isSymbioteNode } from '@symbiote-native/engine';
41
49
 
42
50
  /** Marks the destination for `*portal` — place it on whichever already-mounted host should
43
- * paint the portaled content (e.g. a persistent overlay-host View near the app root), then
44
- * export it to a template variable and pass that variable as `*portal`'s target. */
45
- @Directive({ selector: '[portalOutlet]', standalone: true, exportAs: 'portalOutlet' })
51
+ * paint the portaled content on an `<ng-container>` INSIDE that host, never on the host
52
+ * element itself (see the file header) — then export it to a template variable and pass that
53
+ * variable as `*portal`'s target:
54
+ *
55
+ * ```html
56
+ * <View class="overlay-host">
57
+ * <ng-container portalOutlet #overlayHost="portalOutlet"></ng-container>
58
+ * </View>
59
+ * ```
60
+ */
61
+ @Directive({
62
+ selector: '[portalOutlet]',
63
+ standalone: true,
64
+ exportAs: 'portalOutlet',
65
+ })
46
66
  export class PortalOutletDirective {
47
67
  readonly viewContainerRef = inject(ViewContainerRef);
68
+
69
+ constructor() {
70
+ // The wrong placement commits a valid tree that simply differs from React's, so nothing
71
+ // downstream can catch it. It has to fail where it is written.
72
+ const host: unknown = inject(ElementRef).nativeElement;
73
+ if (!isSymbioteNode(host) || !isAnchor(host)) {
74
+ throw new Error(
75
+ 'portalOutlet must sit on an <ng-container> inside the target host, not on the host ' +
76
+ 'element: a ViewContainerRef anchored at an element inserts AFTER it, so the ported ' +
77
+ 'content would become the host\'s sibling. Write <View class="…">' +
78
+ '<ng-container portalOutlet #x="portalOutlet"></ng-container></View>.',
79
+ );
80
+ }
81
+ }
48
82
  }
49
83
 
50
84
  /** `*portal="overlayHost"` — renders the host element into whichever `PortalOutletDirective`
@@ -59,7 +93,9 @@ export class PortalDirective implements OnChanges, OnDestroy {
59
93
 
60
94
  ngOnChanges(): void {
61
95
  this.viewRef?.destroy();
62
- this.viewRef = this.portal.viewContainerRef.createEmbeddedView(this.templateRef);
96
+ this.viewRef = this.portal.viewContainerRef.createEmbeddedView(
97
+ this.templateRef,
98
+ );
63
99
  }
64
100
 
65
101
  ngOnDestroy(): void {
@@ -109,7 +109,9 @@ export class TunnelOut implements OnInit, OnDestroy {
109
109
  private stopEffect: (() => void) | undefined;
110
110
 
111
111
  ngOnInit(): void {
112
- const ref = effect(() => this.sync(this.tunnel.entries()), { injector: this.injector });
112
+ const ref = effect(() => this.sync(this.tunnel.entries()), {
113
+ injector: this.injector,
114
+ });
113
115
  this.stopEffect = () => ref.destroy();
114
116
  }
115
117
 
@@ -15,7 +15,10 @@ import {
15
15
  type OnDestroy,
16
16
  type SimpleChanges,
17
17
  } from '@angular/core';
18
- import type { IDescriptor, IDescriptorChild } from '@symbiote-native/components';
18
+ import type {
19
+ IDescriptor,
20
+ IDescriptorChild,
21
+ } from '@symbiote-native/components';
19
22
 
20
23
  type IRenderedChild = IRenderedElement | IRenderedText;
21
24
 
@@ -43,7 +46,8 @@ export class DescriptorOutlet implements OnChanges, OnDestroy {
43
46
  @Input({ required: true }) node!: IDescriptor;
44
47
 
45
48
  private readonly renderer = inject(Renderer2);
46
- private readonly hostNode = inject<ElementRef<unknown>>(ElementRef).nativeElement;
49
+ private readonly hostNode =
50
+ inject<ElementRef<unknown>>(ElementRef).nativeElement;
47
51
  private rendered: IRenderedElement | undefined;
48
52
 
49
53
  ngOnChanges(changes: SimpleChanges): void {
@@ -57,7 +61,10 @@ export class DescriptorOutlet implements OnChanges, OnDestroy {
57
61
  this.rendered = undefined;
58
62
  }
59
63
 
60
- private patchRoot(rendered: IRenderedElement | undefined, next: IDescriptor): IRenderedElement {
64
+ private patchRoot(
65
+ rendered: IRenderedElement | undefined,
66
+ next: IDescriptor,
67
+ ): IRenderedElement {
61
68
  if (rendered === undefined) {
62
69
  const created = this.createElement(next);
63
70
  this.renderer.appendChild(this.hostNode, created.node);
@@ -67,7 +74,9 @@ export class DescriptorOutlet implements OnChanges, OnDestroy {
67
74
  }
68
75
 
69
76
  private createChild(child: IDescriptorChild): IRenderedChild {
70
- return typeof child === 'string' ? this.createText(child) : this.createElement(child);
77
+ return typeof child === 'string'
78
+ ? this.createText(child)
79
+ : this.createElement(child);
71
80
  }
72
81
 
73
82
  private createText(value: string): IRenderedText {
@@ -127,12 +136,19 @@ export class DescriptorOutlet implements OnChanges, OnDestroy {
127
136
  }
128
137
 
129
138
  this.patchProps(rendered, next.props);
130
- rendered.children = this.patchChildren(rendered.node, rendered.children, next.children);
139
+ rendered.children = this.patchChildren(
140
+ rendered.node,
141
+ rendered.children,
142
+ next.children,
143
+ );
131
144
  rendered.descriptor = next;
132
145
  return rendered;
133
146
  }
134
147
 
135
- private patchProps(rendered: IRenderedElement, nextProps: Record<string, unknown>): void {
148
+ private patchProps(
149
+ rendered: IRenderedElement,
150
+ nextProps: Record<string, unknown>,
151
+ ): void {
136
152
  const prevProps = rendered.props;
137
153
  for (const key of Object.keys(prevProps)) {
138
154
  if (!(key in nextProps)) {
@@ -173,13 +189,18 @@ export class DescriptorOutlet implements OnChanges, OnDestroy {
173
189
 
174
190
  for (let i = common; i < renderedChildren.length; i++) {
175
191
  const rendered = renderedChildren[i];
176
- if (rendered !== undefined) this.renderer.removeChild(parent, rendered.node);
192
+ if (rendered !== undefined)
193
+ this.renderer.removeChild(parent, rendered.node);
177
194
  }
178
195
 
179
196
  return nextRendered;
180
197
  }
181
198
 
182
- private replaceChild(parent: unknown, oldChild: unknown, newChild: unknown): void {
199
+ private replaceChild(
200
+ parent: unknown,
201
+ oldChild: unknown,
202
+ newChild: unknown,
203
+ ): void {
183
204
  this.renderer.insertBefore(parent, newChild, oldChild);
184
205
  this.renderer.removeChild(parent, oldChild);
185
206
  }