@symbiote-native/angular 0.0.0-canary-20260724081123

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 (276) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +157 -0
  3. package/babel-linker.cjs +7 -0
  4. package/babel-register-composed.cjs +127 -0
  5. package/bin/symbiote-angular-dev.cjs +93 -0
  6. package/build/angular/anchor-host-registry.d.ts +2 -0
  7. package/build/angular/anchor-host-registry.js +84 -0
  8. package/build/angular/anchor-host-registry.js.map +1 -0
  9. package/build/angular/bootstrap.d.ts +7 -0
  10. package/build/angular/bootstrap.js +15 -0
  11. package/build/angular/bootstrap.js.map +1 -0
  12. package/build/angular/components/activity-indicator/index.android.d.ts +12 -0
  13. package/build/angular/components/activity-indicator/index.android.js +21 -0
  14. package/build/angular/components/activity-indicator/index.android.js.map +1 -0
  15. package/build/angular/components/activity-indicator/index.d.ts +1 -0
  16. package/build/angular/components/activity-indicator/index.ios.d.ts +8 -0
  17. package/build/angular/components/activity-indicator/index.ios.js +21 -0
  18. package/build/angular/components/activity-indicator/index.ios.js.map +1 -0
  19. package/build/angular/components/activity-indicator/index.js +4 -0
  20. package/build/angular/components/activity-indicator/index.js.map +1 -0
  21. package/build/angular/components/activity-indicator/shared.d.ts +67 -0
  22. package/build/angular/components/activity-indicator/shared.js +260 -0
  23. package/build/angular/components/activity-indicator/shared.js.map +1 -0
  24. package/build/angular/components/button.d.ts +63 -0
  25. package/build/angular/components/button.js +332 -0
  26. package/build/angular/components/button.js.map +1 -0
  27. package/build/angular/components/flat-list/index.d.ts +122 -0
  28. package/build/angular/components/flat-list/index.js +688 -0
  29. package/build/angular/components/flat-list/index.js.map +1 -0
  30. package/build/angular/components/image/index.android.d.ts +10 -0
  31. package/build/angular/components/image/index.android.js +59 -0
  32. package/build/angular/components/image/index.android.js.map +1 -0
  33. package/build/angular/components/image/index.d.ts +1 -0
  34. package/build/angular/components/image/index.ios.d.ts +10 -0
  35. package/build/angular/components/image/index.ios.js +59 -0
  36. package/build/angular/components/image/index.ios.js.map +1 -0
  37. package/build/angular/components/image/index.js +3 -0
  38. package/build/angular/components/image/index.js.map +1 -0
  39. package/build/angular/components/image/shared.d.ts +101 -0
  40. package/build/angular/components/image/shared.js +406 -0
  41. package/build/angular/components/image/shared.js.map +1 -0
  42. package/build/angular/components/image-background.d.ts +81 -0
  43. package/build/angular/components/image-background.js +418 -0
  44. package/build/angular/components/image-background.js.map +1 -0
  45. package/build/angular/components/input-accessory-view/index.d.ts +60 -0
  46. package/build/angular/components/input-accessory-view/index.js +251 -0
  47. package/build/angular/components/input-accessory-view/index.js.map +1 -0
  48. package/build/angular/components/keyboard-avoiding-view/index.d.ts +82 -0
  49. package/build/angular/components/keyboard-avoiding-view/index.js +358 -0
  50. package/build/angular/components/keyboard-avoiding-view/index.js.map +1 -0
  51. package/build/angular/components/modal/index.d.ts +92 -0
  52. package/build/angular/components/modal/index.js +360 -0
  53. package/build/angular/components/modal/index.js.map +1 -0
  54. package/build/angular/components/pressable/index.d.ts +123 -0
  55. package/build/angular/components/pressable/index.js +420 -0
  56. package/build/angular/components/pressable/index.js.map +1 -0
  57. package/build/angular/components/refresh-control/index.d.ts +86 -0
  58. package/build/angular/components/refresh-control/index.js +342 -0
  59. package/build/angular/components/refresh-control/index.js.map +1 -0
  60. package/build/angular/components/safe-area-view/index.d.ts +57 -0
  61. package/build/angular/components/safe-area-view/index.js +248 -0
  62. package/build/angular/components/safe-area-view/index.js.map +1 -0
  63. package/build/angular/components/scroll-view/index.android.d.ts +21 -0
  64. package/build/angular/components/scroll-view/index.android.js +235 -0
  65. package/build/angular/components/scroll-view/index.android.js.map +1 -0
  66. package/build/angular/components/scroll-view/index.d.ts +3 -0
  67. package/build/angular/components/scroll-view/index.ios.d.ts +9 -0
  68. package/build/angular/components/scroll-view/index.ios.js +86 -0
  69. package/build/angular/components/scroll-view/index.ios.js.map +1 -0
  70. package/build/angular/components/scroll-view/index.js +6 -0
  71. package/build/angular/components/scroll-view/index.js.map +1 -0
  72. package/build/angular/components/scroll-view/projection.d.ts +32 -0
  73. package/build/angular/components/scroll-view/projection.js +334 -0
  74. package/build/angular/components/scroll-view/projection.js.map +1 -0
  75. package/build/angular/components/scroll-view/shared.d.ts +230 -0
  76. package/build/angular/components/scroll-view/shared.js +682 -0
  77. package/build/angular/components/scroll-view/shared.js.map +1 -0
  78. package/build/angular/components/scroll-view/sticky-header.d.ts +40 -0
  79. package/build/angular/components/scroll-view/sticky-header.js +225 -0
  80. package/build/angular/components/scroll-view/sticky-header.js.map +1 -0
  81. package/build/angular/components/section-list/index.d.ts +116 -0
  82. package/build/angular/components/section-list/index.js +641 -0
  83. package/build/angular/components/section-list/index.js.map +1 -0
  84. package/build/angular/components/switch/index.android.d.ts +20 -0
  85. package/build/angular/components/switch/index.android.js +70 -0
  86. package/build/angular/components/switch/index.android.js.map +1 -0
  87. package/build/angular/components/switch/index.d.ts +1 -0
  88. package/build/angular/components/switch/index.ios.d.ts +19 -0
  89. package/build/angular/components/switch/index.ios.js +69 -0
  90. package/build/angular/components/switch/index.ios.js.map +1 -0
  91. package/build/angular/components/switch/index.js +4 -0
  92. package/build/angular/components/switch/index.js.map +1 -0
  93. package/build/angular/components/switch/shared.d.ts +40 -0
  94. package/build/angular/components/switch/shared.js +127 -0
  95. package/build/angular/components/switch/shared.js.map +1 -0
  96. package/build/angular/components/text-input.d.ts +153 -0
  97. package/build/angular/components/text-input.js +599 -0
  98. package/build/angular/components/text-input.js.map +1 -0
  99. package/build/angular/components/touchable/index.d.ts +245 -0
  100. package/build/angular/components/touchable/index.js +1186 -0
  101. package/build/angular/components/touchable/index.js.map +1 -0
  102. package/build/angular/components/touchable-native-feedback/index.d.ts +83 -0
  103. package/build/angular/components/touchable-native-feedback/index.js +371 -0
  104. package/build/angular/components/touchable-native-feedback/index.js.map +1 -0
  105. package/build/angular/components/virtualized-list/directives.d.ts +54 -0
  106. package/build/angular/components/virtualized-list/directives.js +153 -0
  107. package/build/angular/components/virtualized-list/directives.js.map +1 -0
  108. package/build/angular/components/virtualized-list/index.d.ts +253 -0
  109. package/build/angular/components/virtualized-list/index.js +1031 -0
  110. package/build/angular/components/virtualized-list/index.js.map +1 -0
  111. package/build/angular/components/virtualized-section-list/directives.d.ts +37 -0
  112. package/build/angular/components/virtualized-section-list/directives.js +76 -0
  113. package/build/angular/components/virtualized-section-list/directives.js.map +1 -0
  114. package/build/angular/components/virtualized-section-list/index.d.ts +178 -0
  115. package/build/angular/components/virtualized-section-list/index.js +720 -0
  116. package/build/angular/components/virtualized-section-list/index.js.map +1 -0
  117. package/build/angular/components.d.ts +39 -0
  118. package/build/angular/components.js +38 -0
  119. package/build/angular/components.js.map +1 -0
  120. package/build/angular/create-portal/index.d.ts +22 -0
  121. package/build/angular/create-portal/index.js +74 -0
  122. package/build/angular/create-portal/index.js.map +1 -0
  123. package/build/angular/create-tunnel/index.d.ts +34 -0
  124. package/build/angular/create-tunnel/index.js +126 -0
  125. package/build/angular/create-tunnel/index.js.map +1 -0
  126. package/build/angular/descriptor-to-angular/index.d.ts +22 -0
  127. package/build/angular/descriptor-to-angular/index.js +142 -0
  128. package/build/angular/descriptor-to-angular/index.js.map +1 -0
  129. package/build/angular/host-instance/index.d.ts +2 -0
  130. package/build/angular/host-instance/index.js +37 -0
  131. package/build/angular/host-instance/index.js.map +1 -0
  132. package/build/angular/index.d.ts +22 -0
  133. package/build/angular/index.js +46 -0
  134. package/build/angular/index.js.map +1 -0
  135. package/build/angular/modules/animated/animated-leaf-binder/index.d.ts +14 -0
  136. package/build/angular/modules/animated/animated-leaf-binder/index.js +91 -0
  137. package/build/angular/modules/animated/animated-leaf-binder/index.js.map +1 -0
  138. package/build/angular/modules/animated/animated-leaf-binder.d.ts +14 -0
  139. package/build/angular/modules/animated/animated-leaf-binder.js +91 -0
  140. package/build/angular/modules/animated/animated-leaf-binder.js.map +1 -0
  141. package/build/angular/modules/animated/create-animated-component.d.ts +59 -0
  142. package/build/angular/modules/animated/create-animated-component.js +364 -0
  143. package/build/angular/modules/animated/create-animated-component.js.map +1 -0
  144. package/build/angular/modules/animated/index.d.ts +51 -0
  145. package/build/angular/modules/animated/index.js +59 -0
  146. package/build/angular/modules/animated/index.js.map +1 -0
  147. package/build/angular/modules/app-registry/index.d.ts +7 -0
  148. package/build/angular/modules/app-registry/index.js +23 -0
  149. package/build/angular/modules/app-registry/index.js.map +1 -0
  150. package/build/angular/modules/status-bar/index.d.ts +20 -0
  151. package/build/angular/modules/status-bar/index.js +59 -0
  152. package/build/angular/modules/status-bar/index.js.map +1 -0
  153. package/build/angular/primitives/index.d.ts +63 -0
  154. package/build/angular/primitives/index.js +189 -0
  155. package/build/angular/primitives/index.js.map +1 -0
  156. package/build/angular/primitives/shared.d.ts +88 -0
  157. package/build/angular/primitives/shared.js +171 -0
  158. package/build/angular/primitives/shared.js.map +1 -0
  159. package/build/angular/render/index.d.ts +11 -0
  160. package/build/angular/render/index.js +171 -0
  161. package/build/angular/render/index.js.map +1 -0
  162. package/build/angular/renderer/index.d.ts +37 -0
  163. package/build/angular/renderer/index.js +301 -0
  164. package/build/angular/renderer/index.js.map +1 -0
  165. package/build/angular/services/color-scheme.service.d.ts +16 -0
  166. package/build/angular/services/color-scheme.service.js +31 -0
  167. package/build/angular/services/color-scheme.service.js.map +1 -0
  168. package/build/angular/services/index.d.ts +2 -0
  169. package/build/angular/services/index.js +3 -0
  170. package/build/angular/services/index.js.map +1 -0
  171. package/build/angular/services/window-dimensions.service.d.ts +16 -0
  172. package/build/angular/services/window-dimensions.service.js +42 -0
  173. package/build/angular/services/window-dimensions.service.js.map +1 -0
  174. package/metro-config.cjs +48 -0
  175. package/metro-css-parser.cjs +13 -0
  176. package/package.json +80 -0
  177. package/src/__tests__/responder-change-detection.test.ts +102 -0
  178. package/src/__tests__/responder-nested-cd.test.ts +113 -0
  179. package/src/anchor-host-registry.ts +88 -0
  180. package/src/angular-gaps.test.ts +142 -0
  181. package/src/angular-imports.test.ts +100 -0
  182. package/src/bootstrap.ts +26 -0
  183. package/src/components/activity-indicator/activity-indicator-class-style.test.ts +44 -0
  184. package/src/components/activity-indicator/activity-indicator.test.ts +131 -0
  185. package/src/components/activity-indicator/index.android.ts +16 -0
  186. package/src/components/activity-indicator/index.ios.ts +17 -0
  187. package/src/components/activity-indicator/index.ts +4 -0
  188. package/src/components/activity-indicator/shared.ts +206 -0
  189. package/src/components/button.ts +193 -0
  190. package/src/components/flat-list/column-wrapper-style-class.test.ts +116 -0
  191. package/src/components/flat-list/flat-list-array-style.test.ts +131 -0
  192. package/src/components/flat-list/flat-list-class-style.test.ts +74 -0
  193. package/src/components/flat-list/flat-list-scroll-containment.test.ts +107 -0
  194. package/src/components/flat-list/flat-list.test.ts +99 -0
  195. package/src/components/flat-list/index.ts +574 -0
  196. package/src/components/image/image-class-style.test.ts +46 -0
  197. package/src/components/image/index.android.ts +48 -0
  198. package/src/components/image/index.ios.ts +48 -0
  199. package/src/components/image/index.ts +3 -0
  200. package/src/components/image/shared.ts +479 -0
  201. package/src/components/image-background-class-style.test.ts +48 -0
  202. package/src/components/image-background-style-class.test.ts +79 -0
  203. package/src/components/image-background.ts +270 -0
  204. package/src/components/input-accessory-view/index.ts +185 -0
  205. package/src/components/input-accessory-view/input-accessory-view.test.ts +43 -0
  206. package/src/components/keyboard-avoiding-view/index.ts +303 -0
  207. package/src/components/keyboard-avoiding-view/keyboard-avoiding-view.test.ts +54 -0
  208. package/src/components/modal/index.ts +299 -0
  209. package/src/components/modal/modal.test.ts +43 -0
  210. package/src/components/pressable/index.ts +373 -0
  211. package/src/components/pressable/pressable.test.ts +85 -0
  212. package/src/components/refresh-control/index.ts +284 -0
  213. package/src/components/refresh-control/refresh-control.test.ts +41 -0
  214. package/src/components/safe-area-view/index.ts +177 -0
  215. package/src/components/safe-area-view/safe-area-view.test.ts +43 -0
  216. package/src/components/scroll-view/android-scroll-view-axis-projection.test.ts +123 -0
  217. package/src/components/scroll-view/content-container-style-class.test.ts +75 -0
  218. package/src/components/scroll-view/index.android.ts +190 -0
  219. package/src/components/scroll-view/index.ios.ts +79 -0
  220. package/src/components/scroll-view/index.ts +7 -0
  221. package/src/components/scroll-view/projection.ts +414 -0
  222. package/src/components/scroll-view/scroll-view-class-style.test.ts +134 -0
  223. package/src/components/scroll-view/scroll-view-projection.test.ts +194 -0
  224. package/src/components/scroll-view/shared.ts +844 -0
  225. package/src/components/scroll-view/sticky-header.ts +255 -0
  226. package/src/components/section-list/index.ts +445 -0
  227. package/src/components/switch/android-switch-class.test.ts +42 -0
  228. package/src/components/switch/index.android.ts +61 -0
  229. package/src/components/switch/index.ios.ts +60 -0
  230. package/src/components/switch/index.ts +4 -0
  231. package/src/components/switch/shared.ts +197 -0
  232. package/src/components/switch/switch.test.ts +92 -0
  233. package/src/components/text-input-class-style.test.ts +54 -0
  234. package/src/components/text-input.ts +531 -0
  235. package/src/components/touchable/index.ts +629 -0
  236. package/src/components/touchable/touchable.test.ts +102 -0
  237. package/src/components/touchable-native-feedback/index.ts +254 -0
  238. package/src/components/touchable-native-feedback/touchable-native-feedback.test.ts +49 -0
  239. package/src/components/virtualized-list/directives.ts +168 -0
  240. package/src/components/virtualized-list/index.ts +992 -0
  241. package/src/components/virtualized-list/virtualized-list-class-style.test.ts +76 -0
  242. package/src/components/virtualized-section-list/directives.ts +90 -0
  243. package/src/components/virtualized-section-list/index.ts +601 -0
  244. package/src/components/virtualized-section-list/virtualized-section-list-class-style.test.ts +60 -0
  245. package/src/components.ts +149 -0
  246. package/src/create-portal/create-portal.test.ts +90 -0
  247. package/src/create-portal/index.ts +74 -0
  248. package/src/create-tunnel/create-tunnel.test.ts +120 -0
  249. package/src/create-tunnel/index.ts +136 -0
  250. package/src/descriptor-to-angular/descriptor-outlet.test.ts +94 -0
  251. package/src/descriptor-to-angular/index.ts +190 -0
  252. package/src/host-instance/host-instance.test.ts +49 -0
  253. package/src/host-instance/index.ts +36 -0
  254. package/src/index.ts +261 -0
  255. package/src/modules/animated/animated-leaf-binder/animated-leaf-binder.test.ts +116 -0
  256. package/src/modules/animated/animated-leaf-binder/index.ts +101 -0
  257. package/src/modules/animated/animated-leaf-binder.test.ts +116 -0
  258. package/src/modules/animated/animated-leaf-binder.ts +101 -0
  259. package/src/modules/animated/animated-scroll-view.test.ts +102 -0
  260. package/src/modules/animated/create-animated-component.ts +365 -0
  261. package/src/modules/animated/index.ts +105 -0
  262. package/src/modules/app-registry/app-registry.test.ts +106 -0
  263. package/src/modules/app-registry/index.ts +60 -0
  264. package/src/modules/status-bar/index.ts +57 -0
  265. package/src/modules/status-bar/status-bar.test.ts +50 -0
  266. package/src/primitives/index.ts +119 -0
  267. package/src/primitives/shared.ts +190 -0
  268. package/src/render/index.ts +235 -0
  269. package/src/render/render.test.ts +147 -0
  270. package/src/renderer/index.ts +347 -0
  271. package/src/renderer/renderer.test.ts +203 -0
  272. package/src/services/color-scheme.service.ts +27 -0
  273. package/src/services/index.ts +2 -0
  274. package/src/services/services.test.ts +163 -0
  275. package/src/services/window-dimensions.service.ts +41 -0
  276. package/tsconfig.angular.base.json +30 -0
@@ -0,0 +1,601 @@
1
+ // VirtualizedSectionList, the Angular wrapper that flattens sections into one virtualized stream
2
+ // over VirtualizedList. Each section contributes a header row, its item rows, then a footer row
3
+ // (RN counts 2 per section); the flattened tagged sequence is fed to VirtualizedList as one list,
4
+ // so headers/items/footers are all windowed by the same machinery. The flattening, entry keying,
5
+ // separator-item unwrap, and scrollToLocation mapping are shared from @symbiote-native/components; this
6
+ // file wires Angular's lifecycle (typed @Input/@Output surface + the imperative handle + the
7
+ // per-cell-tag template dispatch). The Angular twin of the React/Vue adapters' VirtualizedSectionList.
8
+ //
9
+ // The KEY mechanic — per-cell-tag template dispatch. VirtualizedList renders every cell through ONE
10
+ // `vListItem` template, but a section list has DIFFERENT cell types (section header, item, section
11
+ // footer, between-section separators). So VSL supplies the inner list a SINGLE synthesized
12
+ // `vListItem` template that @switch-es on the flattened entry's tag and stamps the matching app
13
+ // template (captured via @ContentChild on the vSection* directives) through VListOutletDirective.
14
+ // list-level header/footer/empty/item-separator reuse VirtualizedList's own directives, forwarded
15
+ // the same way. Only the cell-AUTHORING shape is framework-specific; the rest of the surface
16
+ // stays shared with React/Vue.
17
+ //
18
+ // Accessibility / aria props are forwarded unchanged to the inner VirtualizedList, which owns the
19
+ // shared aria→accessibility* fold before the props reach the ScrollView host.
20
+
21
+ import {
22
+ ChangeDetectionStrategy,
23
+ ChangeDetectorRef,
24
+ Component,
25
+ ContentChild,
26
+ ElementRef,
27
+ EventEmitter,
28
+ Input,
29
+ Output,
30
+ ViewChild,
31
+ inject,
32
+ type DoCheck,
33
+ } from '@angular/core';
34
+ import {
35
+ flattenSections,
36
+ resolveStickySectionHeaders,
37
+ scrollLocationToFlatIndex,
38
+ sectionEntryKey,
39
+ unwrapEntryItem,
40
+ type IAccessibilityProps,
41
+ type IAccessibilityStateValue,
42
+ type IAriaProps,
43
+ type ISection,
44
+ type ISectionEntry,
45
+ type ISeparators,
46
+ type IScrollViewHandle,
47
+ type IVirtualizedSectionListHandle,
48
+ } from '@symbiote-native/components';
49
+ import {
50
+ Platform,
51
+ dlog,
52
+ type IStyleProp,
53
+ type ISymbioteEvent,
54
+ type ISymbioteNode,
55
+ type IViewStyle,
56
+ } from '@symbiote-native/engine';
57
+ import {
58
+ VirtualizedList,
59
+ VListEmptyDirective,
60
+ VListFooterDirective,
61
+ VListHeaderDirective,
62
+ VListItemDirective,
63
+ VListSeparatorDirective,
64
+ type IVListSeparatorContext,
65
+ } from '../virtualized-list';
66
+ import { VListOutletDirective } from '../virtualized-list/directives';
67
+ import { stableAnchorStyle } from '../../primitives';
68
+ import {
69
+ VSectionFooterDirective,
70
+ VSectionHeaderDirective,
71
+ VSectionItemDirective,
72
+ VSectionSeparatorDirective,
73
+ type IVSectionContext,
74
+ type IVSectionItemContext,
75
+ } from './directives';
76
+
77
+ export type { ISection } from '@symbiote-native/components';
78
+ // Re-export the shared handle type so section-list imports it from '../virtualized-section-list'.
79
+ export type { IVirtualizedSectionListHandle } from '@symbiote-native/components';
80
+ // Re-export the section authoring directives + their contexts so flat consumers (and SectionList)
81
+ // import them from '../virtualized-section-list', mirroring how VirtualizedList re-exports its own.
82
+ export {
83
+ VSectionFooterDirective,
84
+ VSectionHeaderDirective,
85
+ VSectionItemDirective,
86
+ VSectionSeparatorDirective,
87
+ } from './directives';
88
+ export type { IVSectionContext, IVSectionItemContext } from './directives';
89
+
90
+ // The Angular VirtualizedSectionList prop surface. Mirrors React/Vue's IVirtualizedSectionListProps
91
+ // MINUS the element-returning props (renderItem, renderSectionHeader/Footer, the *Component slots):
92
+ // those are the per-adapter children/render fields and become `<ng-template>` directives in Angular.
93
+ // Everything agnostic is the SAME surface as the
94
+ // React/Vue adapters, including the a11y/aria prop family forwarded through VirtualizedList.
95
+ export interface IVirtualizedSectionListProps<ItemT> extends IAccessibilityProps, IAriaProps {
96
+ sections: ReadonlyArray<ISection<ItemT>>;
97
+ keyExtractor?: (item: ItemT, index: number) => string;
98
+ // Stick each section header to the top as the next section scrolls up. Routed to the inner
99
+ // VirtualizedList's stickyHeaderIndices. Defaults to `Platform.OS === 'ios'`; Android does not
100
+ // stick by default. Pass true/false to override.
101
+ stickySectionHeadersEnabled?: boolean;
102
+ extraData?: unknown;
103
+ onEndReached?: (info: { distanceFromEnd: number }) => void;
104
+ onEndReachedThreshold?: number;
105
+ onStartReached?: (info: { distanceFromStart: number }) => void;
106
+ onStartReachedThreshold?: number;
107
+ onRefresh?: () => void;
108
+ refreshing?: boolean | null;
109
+ progressViewOffset?: number;
110
+ initialNumToRender?: number;
111
+ initialScrollIndex?: number;
112
+ maxToRenderPerBatch?: number;
113
+ updateCellsBatchingPeriod?: number;
114
+ windowSize?: number;
115
+ inverted?: boolean;
116
+ maintainVisibleContentPosition?: {
117
+ minIndexForVisible: number;
118
+ autoscrollToTopThreshold?: number;
119
+ };
120
+ onScroll?: (event: ISymbioteEvent) => void;
121
+ onScrollBeginDrag?: (event: ISymbioteEvent) => void;
122
+ onScrollEndDrag?: (event: ISymbioteEvent) => void;
123
+ onMomentumScrollBegin?: (event: ISymbioteEvent) => void;
124
+ onMomentumScrollEnd?: (event: ISymbioteEvent) => void;
125
+ scrollEventThrottle?: number;
126
+ keyboardShouldPersistTaps?: boolean | 'always' | 'never' | 'handled';
127
+ keyboardDismissMode?: 'none' | 'on-drag' | 'interactive';
128
+ style?: IStyleProp<IViewStyle>;
129
+ contentContainerStyle?: IStyleProp<IViewStyle>;
130
+ }
131
+
132
+ // What the VirtualizedSectionList component itself takes as plain @Input()s: the full surface
133
+ // minus the list-lifecycle events, which it exposes as real @Output() EventEmitters instead (see
134
+ // the class below) — mirrors IAngularPressableInputs in pressable/index.ts.
135
+ export type IVirtualizedSectionListInputs<ItemT> = Omit<
136
+ IVirtualizedSectionListProps<ItemT>,
137
+ | 'onEndReached'
138
+ | 'onStartReached'
139
+ | 'onRefresh'
140
+ | 'onAccessibilityAction'
141
+ | 'onAccessibilityTap'
142
+ | 'onMagicTap'
143
+ | 'onAccessibilityEscape'
144
+ >;
145
+
146
+ @Component({
147
+ selector: 'VirtualizedSectionList',
148
+ standalone: true,
149
+ imports: [
150
+ VirtualizedList,
151
+ VListItemDirective,
152
+ VListHeaderDirective,
153
+ VListFooterDirective,
154
+ VListEmptyDirective,
155
+ VListSeparatorDirective,
156
+ VListOutletDirective,
157
+ ],
158
+ changeDetection: ChangeDetectionStrategy.OnPush,
159
+ template: `
160
+ <VirtualizedList
161
+ [data]="flatEntries"
162
+ [getItem]="getEntry"
163
+ [getItemCount]="getEntryCount"
164
+ [keyExtractor]="entryKeyExtractor"
165
+ [stickyHeaderIndices]="stickyHeaderIndices"
166
+ [extraData]="extraData"
167
+ [inverted]="inverted"
168
+ [refreshing]="refreshing"
169
+ [progressViewOffset]="progressViewOffset"
170
+ [refreshRequested]="refreshRequested ?? refresh.observed"
171
+ (refresh)="resolvedOnRefresh?.()"
172
+ (endReached)="resolvedOnEndReached?.($event)"
173
+ [onEndReachedThreshold]="onEndReachedThreshold"
174
+ (startReached)="resolvedOnStartReached?.($event)"
175
+ [onStartReachedThreshold]="onStartReachedThreshold"
176
+ [initialNumToRender]="initialNumToRender"
177
+ [initialScrollIndex]="initialScrollIndex"
178
+ [maxToRenderPerBatch]="maxToRenderPerBatch"
179
+ [updateCellsBatchingPeriod]="updateCellsBatchingPeriod"
180
+ [windowSize]="windowSize"
181
+ [maintainVisibleContentPosition]="maintainVisibleContentPosition"
182
+ [onScroll]="onScroll"
183
+ [onScrollBeginDrag]="onScrollBeginDrag"
184
+ [onScrollEndDrag]="onScrollEndDrag"
185
+ [onMomentumScrollBegin]="onMomentumScrollBegin"
186
+ [onMomentumScrollEnd]="onMomentumScrollEnd"
187
+ [scrollEventThrottle]="scrollEventThrottle"
188
+ [keyboardShouldPersistTaps]="keyboardShouldPersistTaps"
189
+ [keyboardDismissMode]="keyboardDismissMode"
190
+ [style]="resolvedStyle"
191
+ [contentContainerStyle]="contentContainerStyle"
192
+ [testID]="testID"
193
+ [nativeID]="nativeID"
194
+ [accessible]="accessible"
195
+ [accessibilityLabel]="accessibilityLabel"
196
+ [accessibilityHint]="accessibilityHint"
197
+ [accessibilityRole]="accessibilityRole"
198
+ [accessibilityState]="accessibilityState"
199
+ [accessibilityValue]="accessibilityValue"
200
+ [accessibilityActions]="accessibilityActions"
201
+ [accessibilityLabelledBy]="accessibilityLabelledBy"
202
+ [importantForAccessibility]="importantForAccessibility"
203
+ [accessibilityLiveRegion]="accessibilityLiveRegion"
204
+ [screenReaderFocusable]="screenReaderFocusable"
205
+ [accessibilityViewIsModal]="accessibilityViewIsModal"
206
+ [accessibilityElementsHidden]="accessibilityElementsHidden"
207
+ [accessibilityIgnoresInvertColors]="accessibilityIgnoresInvertColors"
208
+ [accessibilityLanguage]="accessibilityLanguage"
209
+ [accessibilityRespondsToUserInteraction]="accessibilityRespondsToUserInteraction"
210
+ [accessibilityShowsLargeContentViewer]="accessibilityShowsLargeContentViewer"
211
+ [accessibilityLargeContentTitle]="accessibilityLargeContentTitle"
212
+ (accessibilityAction)="resolvedOnAccessibilityAction?.($event)"
213
+ (accessibilityTap)="resolvedOnAccessibilityTap?.($event)"
214
+ (magicTap)="resolvedOnMagicTap?.($event)"
215
+ (accessibilityEscape)="resolvedOnAccessibilityEscape?.($event)"
216
+ [role]="role"
217
+ [ariaLabel]="ariaLabel"
218
+ [ariaLabelledBy]="ariaLabelledBy"
219
+ [ariaLive]="ariaLive"
220
+ [ariaHidden]="ariaHidden"
221
+ [ariaBusy]="ariaBusy"
222
+ [ariaChecked]="ariaChecked"
223
+ [ariaDisabled]="ariaDisabled"
224
+ [ariaExpanded]="ariaExpanded"
225
+ [ariaSelected]="ariaSelected"
226
+ [ariaModal]="ariaModal"
227
+ [ariaValueMax]="ariaValueMax"
228
+ [ariaValueMin]="ariaValueMin"
229
+ [ariaValueNow]="ariaValueNow"
230
+ [ariaValueText]="ariaValueText"
231
+ >
232
+ <!-- The single synthesized cell template: dispatch per flattened entry tag. -->
233
+ <ng-template vListItem let-entry let-separators="separators">
234
+ @switch (entryKind(entry)) {
235
+ @case ('header') {
236
+ <ng-container
237
+ [vListOutlet]="sectionHeaderDir?.templateRef"
238
+ [vListOutletContext]="sectionContextOf(entry)"
239
+ ></ng-container>
240
+ }
241
+ @case ('footer') {
242
+ <ng-container
243
+ [vListOutlet]="sectionFooterDir?.templateRef"
244
+ [vListOutletContext]="sectionContextOf(entry)"
245
+ ></ng-container>
246
+ }
247
+ @case ('section-separator') {
248
+ <ng-container [vListOutlet]="sectionSeparatorDir?.templateRef"></ng-container>
249
+ }
250
+ @case ('item') {
251
+ <ng-container
252
+ [vListOutlet]="sectionItemDir?.templateRef"
253
+ [vListOutletContext]="itemContextOf(entry, separators)"
254
+ ></ng-container>
255
+ }
256
+ }
257
+ </ng-template>
258
+
259
+ <!-- list-level slots: forward the app's VirtualizedList directives to the inner list. -->
260
+ @if (listHeaderDir !== undefined) {
261
+ <ng-template vListHeader>
262
+ <ng-container [vListOutlet]="listHeaderDir.templateRef"></ng-container>
263
+ </ng-template>
264
+ }
265
+ @if (listFooterDir !== undefined) {
266
+ <ng-template vListFooter>
267
+ <ng-container [vListOutlet]="listFooterDir.templateRef"></ng-container>
268
+ </ng-template>
269
+ }
270
+ @if (listEmptyDir !== undefined) {
271
+ <ng-template vListEmpty>
272
+ <ng-container [vListOutlet]="listEmptyDir.templateRef"></ng-container>
273
+ </ng-template>
274
+ }
275
+ <!-- item separator: forward, unwrapping each flattened entry back to its ItemT. -->
276
+ @if (itemSeparatorDir !== undefined) {
277
+ <ng-template
278
+ vListSeparator
279
+ let-highlighted
280
+ let-leadingItem="leadingItem"
281
+ let-trailingItem="trailingItem"
282
+ >
283
+ <ng-container
284
+ [vListOutlet]="itemSeparatorDir.templateRef"
285
+ [vListOutletContext]="itemSeparatorContextOf(highlighted, leadingItem, trailingItem)"
286
+ ></ng-container>
287
+ </ng-template>
288
+ }
289
+ </VirtualizedList>
290
+ `,
291
+ })
292
+ export class VirtualizedSectionList<ItemT = unknown>
293
+ implements IVirtualizedSectionListInputs<ItemT>, IVirtualizedSectionListHandle, DoCheck
294
+ {
295
+ @Input({ required: true }) sections!: ReadonlyArray<ISection<ItemT>>;
296
+ @Input() keyExtractor?: (item: ItemT, index: number) => string;
297
+ @Input() stickySectionHeadersEnabled?: boolean;
298
+ @Input() extraData?: unknown;
299
+ @Output() readonly endReached = new EventEmitter<{ distanceFromEnd: number }>();
300
+ @Input() onEndReachedThreshold?: number;
301
+ @Output() readonly startReached = new EventEmitter<{ distanceFromStart: number }>();
302
+ @Input() onStartReachedThreshold?: number;
303
+ @Output() readonly refresh = new EventEmitter<void>();
304
+ @Input() refreshing?: boolean | null;
305
+ @Input() progressViewOffset?: number;
306
+ // See VirtualizedList's own refreshRequested doc comment: `(refresh)="resolvedOnRefresh?.()"`
307
+ // above always subscribes to the inner VirtualizedList's refresh output (to re-forward it),
308
+ // which would otherwise make its `.observed` permanently true. SectionList passes ITS OWN
309
+ // public `refresh.observed` here; direct usage falls back to this component's own `.observed`.
310
+ @Input() refreshRequested?: boolean;
311
+ @Input() initialNumToRender?: number;
312
+ @Input() initialScrollIndex?: number;
313
+ @Input() maxToRenderPerBatch?: number;
314
+ @Input() updateCellsBatchingPeriod?: number;
315
+ @Input() windowSize?: number;
316
+ @Input() inverted?: boolean;
317
+ @Input() maintainVisibleContentPosition?: {
318
+ minIndexForVisible: number;
319
+ autoscrollToTopThreshold?: number;
320
+ };
321
+ @Input() onScroll?: (event: ISymbioteEvent) => void;
322
+ @Input() onScrollBeginDrag?: (event: ISymbioteEvent) => void;
323
+ @Input() onScrollEndDrag?: (event: ISymbioteEvent) => void;
324
+ @Input() onMomentumScrollBegin?: (event: ISymbioteEvent) => void;
325
+ @Input() onMomentumScrollEnd?: (event: ISymbioteEvent) => void;
326
+ @Input() scrollEventThrottle?: number;
327
+ @Input() keyboardShouldPersistTaps?: boolean | 'always' | 'never' | 'handled';
328
+ @Input() keyboardDismissMode?: 'none' | 'on-drag' | 'interactive';
329
+ @Input() style?: IStyleProp<IViewStyle>;
330
+ @Input() contentContainerStyle?: IStyleProp<IViewStyle>;
331
+ @Input() testID?: string;
332
+ @Input() nativeID?: string;
333
+
334
+ // Bound to the template's `[style]="resolvedStyle"`, which Angular compiles to the built-in
335
+ // ɵɵstyleMap instruction — it only understands a flat object, never an array (RN's own
336
+ // `style={[a, b]}` composition idiom crashes deep inside Angular's styling engine), so this
337
+ // flattens `style` via the engine's own flattenStyle before it ever reaches that binding.
338
+ // anchorHostStyle merges in this component's OWN anchor's class-derived style (see its doc
339
+ // comment) — `elementRef` below is VirtualizedSectionList's OWN host, not `list`'s inner
340
+ // VirtualizedList. A plain `flattenStyle([...])` here would allocate a FRESH object on every
341
+ // getter read, which — bound onto the inner VirtualizedList's own `@Input() style` — defeats
342
+ // its `ngDoCheck` dedup gate and free-runs change detection forever (see stableAnchorStyle's
343
+ // doc comment and the `flat-list-array-style.test.ts` regression it fixes).
344
+ // `cachedResolvedStyle` is the getter's own persisted "previous" value across reads.
345
+ private cachedResolvedStyle: Record<string, unknown> | undefined;
346
+ get resolvedStyle(): IViewStyle {
347
+ this.cachedResolvedStyle = stableAnchorStyle(
348
+ this.elementRef,
349
+ this.style,
350
+ this.cachedResolvedStyle,
351
+ );
352
+ return this.cachedResolvedStyle;
353
+ }
354
+ @Input() accessible?: boolean;
355
+ @Input() accessibilityLabel?: string;
356
+ @Input() accessibilityHint?: string;
357
+ @Input() accessibilityRole?: IAccessibilityProps['accessibilityRole'];
358
+ @Input() accessibilityState?: IAccessibilityStateValue;
359
+ @Input() accessibilityValue?: IAccessibilityProps['accessibilityValue'];
360
+ @Input() accessibilityActions?: IAccessibilityProps['accessibilityActions'];
361
+ @Input() accessibilityLabelledBy?: string | string[];
362
+ @Input() importantForAccessibility?: IAccessibilityProps['importantForAccessibility'];
363
+ @Input() accessibilityLiveRegion?: IAccessibilityProps['accessibilityLiveRegion'];
364
+ @Input() screenReaderFocusable?: boolean;
365
+ @Input() accessibilityViewIsModal?: boolean;
366
+ @Input() accessibilityElementsHidden?: boolean;
367
+ @Input() accessibilityIgnoresInvertColors?: boolean;
368
+ @Input() accessibilityLanguage?: string;
369
+ @Input() accessibilityRespondsToUserInteraction?: boolean;
370
+ @Input() accessibilityShowsLargeContentViewer?: boolean;
371
+ @Input() accessibilityLargeContentTitle?: string;
372
+ @Output() readonly accessibilityAction = new EventEmitter<ISymbioteEvent>();
373
+ @Output() readonly accessibilityTap = new EventEmitter<ISymbioteEvent>();
374
+ @Output() readonly magicTap = new EventEmitter<ISymbioteEvent>();
375
+ @Output() readonly accessibilityEscape = new EventEmitter<ISymbioteEvent>();
376
+ @Input() role?: IAriaProps['role'];
377
+ @Input() ariaLabel?: string;
378
+ @Input() ariaLabelledBy?: string;
379
+ @Input() ariaLive?: IAriaProps['aria-live'];
380
+ @Input() ariaHidden?: boolean;
381
+ @Input() ariaBusy?: boolean;
382
+ @Input() ariaChecked?: boolean | 'mixed';
383
+ @Input() ariaDisabled?: boolean;
384
+ @Input() ariaExpanded?: boolean;
385
+ @Input() ariaSelected?: boolean;
386
+ @Input() ariaModal?: boolean;
387
+ @Input() ariaValueMax?: number;
388
+ @Input() ariaValueMin?: number;
389
+ @Input() ariaValueNow?: number;
390
+ @Input() ariaValueText?: string;
391
+
392
+ // The section cell templates the app authors (the Angular twin of renderItem /
393
+ // renderSectionHeader / renderSectionFooter / SectionSeparatorComponent), captured from projected
394
+ // <ng-template> content and stamped by the synthesized vListItem dispatch.
395
+ @ContentChild(VSectionItemDirective) sectionItemDir?: VSectionItemDirective<ItemT>;
396
+ @ContentChild(VSectionHeaderDirective) sectionHeaderDir?: VSectionHeaderDirective<ItemT>;
397
+ @ContentChild(VSectionFooterDirective) sectionFooterDir?: VSectionFooterDirective<ItemT>;
398
+ @ContentChild(VSectionSeparatorDirective) sectionSeparatorDir?: VSectionSeparatorDirective;
399
+
400
+ // list-level slots reuse VirtualizedList's own directives; VSL forwards them to the inner list.
401
+ @ContentChild(VListHeaderDirective) listHeaderDir?: VListHeaderDirective;
402
+ @ContentChild(VListFooterDirective) listFooterDir?: VListFooterDirective;
403
+ @ContentChild(VListEmptyDirective) listEmptyDir?: VListEmptyDirective;
404
+ @ContentChild(VListSeparatorDirective) itemSeparatorDir?: VListSeparatorDirective<ItemT>;
405
+
406
+ // The composed inner VirtualizedList. Its instance IS an IVirtualizedListHandle, so the section
407
+ // handle delegates straight to it. Available from ngAfterViewInit; reads lazily, no-ops pre-commit.
408
+ @ViewChild(VirtualizedList) private list?: VirtualizedList<ISectionEntry<ItemT>>;
409
+
410
+ // This component's OWN host — the non-painting anchor `class="..."` at the use site resolves
411
+ // onto (see anchorHostStyle's doc comment) — NOT `list` above, which targets the real inner
412
+ // `<VirtualizedList>` one level down (itself its own separate anchor host).
413
+ private readonly elementRef = inject(ElementRef);
414
+
415
+ // --- template-bound state, recomputed in ngDoCheck when sections / separator presence change ---
416
+ flatEntries: ISectionEntry<ItemT>[] = [];
417
+ stickyHeaderIndices: number[] | undefined = undefined;
418
+
419
+ // headerIndices are the flat positions of every section header; scrollToLocation maps a
420
+ // (sectionIndex, itemIndex) coordinate through them without re-deriving the flat layout.
421
+ private headerIndices: number[] = [];
422
+ // Memo guards so flattenSections does not re-run on every CD (the inner list marks for check on
423
+ // every scroll tick, which re-runs VSL's ngDoCheck too).
424
+ private lastSectionsRef: ReadonlyArray<ISection<ItemT>> | null = null;
425
+ private lastHasSectionSeparator = false;
426
+
427
+ private readonly cdr = inject(ChangeDetectorRef);
428
+
429
+ ngDoCheck(): void {
430
+ const hasSectionSeparator = this.sectionSeparatorDir !== undefined;
431
+ if (
432
+ this.sections === this.lastSectionsRef &&
433
+ hasSectionSeparator === this.lastHasSectionSeparator
434
+ ) {
435
+ return;
436
+ }
437
+ this.lastSectionsRef = this.sections;
438
+ this.lastHasSectionSeparator = hasSectionSeparator;
439
+
440
+ const sections = this.sections ?? [];
441
+ const { entries, headerIndices } = flattenSections(sections, hasSectionSeparator);
442
+ this.flatEntries = entries;
443
+ this.headerIndices = headerIndices;
444
+
445
+ // RN sticks section headers by default only on iOS; Android does not unless asked.
446
+ this.stickyHeaderIndices = resolveStickySectionHeaders(
447
+ this.stickySectionHeadersEnabled,
448
+ headerIndices,
449
+ Platform.OS,
450
+ );
451
+
452
+ dlog(
453
+ `Angular VirtualizedSectionList: ${sections.length} sections flattened to ${entries.length} entries`,
454
+ );
455
+ this.cdr.markForCheck();
456
+ }
457
+
458
+ // The derived flat-stream accessors handed to the inner VirtualizedList. Stable arrow fields so
459
+ // the bindings keep a constant identity; they read the live flatEntries.
460
+ getEntry = (_source: unknown, index: number): ISectionEntry<ItemT> => this.flatEntries[index];
461
+ getEntryCount = (): number => this.flatEntries.length;
462
+ entryKeyExtractor = (entry: ISectionEntry<ItemT>, index: number): string =>
463
+ sectionEntryKey(entry, index, this.keyExtractor);
464
+
465
+ // Adapts each @Output() into the plain callback VirtualizedList's own @Input() still wants,
466
+ // gated on `.observed` so an unlistened event forwards `undefined` — the same "nobody cares"
467
+ // contract the old @Input() callback passthrough had (e.g. onRefresh presence gates whether a
468
+ // RefreshControl is built downstream; see the Vue twin's `listens('onRefresh')` gate).
469
+ get resolvedOnEndReached(): ((info: { distanceFromEnd: number }) => void) | undefined {
470
+ return this.endReached.observed ? info => this.endReached.emit(info) : undefined;
471
+ }
472
+
473
+ get resolvedOnStartReached(): ((info: { distanceFromStart: number }) => void) | undefined {
474
+ return this.startReached.observed ? info => this.startReached.emit(info) : undefined;
475
+ }
476
+
477
+ get resolvedOnRefresh(): (() => void) | undefined {
478
+ return this.refresh.observed ? () => this.refresh.emit() : undefined;
479
+ }
480
+
481
+ get resolvedOnAccessibilityAction(): ((event: ISymbioteEvent) => void) | undefined {
482
+ return this.accessibilityAction.observed
483
+ ? event => this.accessibilityAction.emit(event)
484
+ : undefined;
485
+ }
486
+
487
+ get resolvedOnAccessibilityTap(): ((event: ISymbioteEvent) => void) | undefined {
488
+ return this.accessibilityTap.observed ? event => this.accessibilityTap.emit(event) : undefined;
489
+ }
490
+
491
+ get resolvedOnMagicTap(): ((event: ISymbioteEvent) => void) | undefined {
492
+ return this.magicTap.observed ? event => this.magicTap.emit(event) : undefined;
493
+ }
494
+
495
+ get resolvedOnAccessibilityEscape(): ((event: ISymbioteEvent) => void) | undefined {
496
+ return this.accessibilityEscape.observed
497
+ ? event => this.accessibilityEscape.emit(event)
498
+ : undefined;
499
+ }
500
+
501
+ // --- per-cell-tag dispatch (the inner vListItem context is typed `unknown` under strictTemplates,
502
+ // since VListItemDirective's generic is not bound here; narrow with a runtime guard, no casts). ---
503
+
504
+ private isEntry(value: unknown): value is ISectionEntry<ItemT> {
505
+ return typeof value === 'object' && value !== null && 'kind' in value;
506
+ }
507
+
508
+ entryKind(value: unknown): ISectionEntry<ItemT>['kind'] | 'unknown' {
509
+ return this.isEntry(value) ? value.kind : 'unknown';
510
+ }
511
+
512
+ sectionContextOf(value: unknown): IVSectionContext<ItemT> | undefined {
513
+ if (!this.isEntry(value)) return undefined;
514
+ if (value.kind !== 'header' && value.kind !== 'footer') return undefined;
515
+ return { $implicit: value.section, section: value.section };
516
+ }
517
+
518
+ itemContextOf(value: unknown, separators: ISeparators): IVSectionItemContext<ItemT> | undefined {
519
+ if (!this.isEntry(value) || value.kind !== 'item') return undefined;
520
+ return {
521
+ $implicit: value.item,
522
+ item: value.item,
523
+ index: value.itemIndex,
524
+ section: value.section,
525
+ separators,
526
+ };
527
+ }
528
+
529
+ // The inner list's separator context carries the flattened ENTRY as leading/trailing; unwrap each
530
+ // back to its ItemT (shared unwrapEntryItem) so the app's item separator, typed on ItemT, sees
531
+ // real items (header/footer/section-separator gaps unwrap to undefined). Mirrors the React/Vue
532
+ // entrySeparatorComponent wrap.
533
+ itemSeparatorContextOf(
534
+ highlighted: unknown,
535
+ leadingItem: unknown,
536
+ trailingItem: unknown,
537
+ ): IVListSeparatorContext<ItemT> {
538
+ return {
539
+ $implicit: highlighted === true,
540
+ highlighted: highlighted === true,
541
+ leadingItem: this.isEntry(leadingItem) ? unwrapEntryItem(leadingItem) : undefined,
542
+ trailingItem: this.isEntry(trailingItem) ? unwrapEntryItem(trailingItem) : undefined,
543
+ };
544
+ }
545
+
546
+ // ---- imperative handle (the shared IVirtualizedSectionListHandle surface) ----
547
+
548
+ scrollToLocation(params: {
549
+ sectionIndex: number;
550
+ itemIndex: number;
551
+ viewOffset?: number;
552
+ viewPosition?: number;
553
+ animated?: boolean;
554
+ }): void {
555
+ const flatIndex = scrollLocationToFlatIndex(
556
+ this.headerIndices,
557
+ params.sectionIndex,
558
+ params.itemIndex,
559
+ );
560
+ if (flatIndex === undefined) {
561
+ dlog(
562
+ `Angular VirtualizedSectionList scrollToLocation: section ${params.sectionIndex} out of range`,
563
+ );
564
+ return;
565
+ }
566
+ dlog(
567
+ `Angular VirtualizedSectionList scrollToLocation section=${params.sectionIndex} ` +
568
+ `item=${params.itemIndex} -> flat ${flatIndex}`,
569
+ );
570
+ this.list?.scrollToIndex({
571
+ index: flatIndex,
572
+ viewOffset: params.viewOffset,
573
+ viewPosition: params.viewPosition,
574
+ animated: params.animated,
575
+ });
576
+ }
577
+
578
+ flashScrollIndicators(): void {
579
+ this.list?.flashScrollIndicators();
580
+ }
581
+
582
+ getNativeScrollRef(): IScrollViewHandle | null {
583
+ return this.list?.getNativeScrollRef() ?? null;
584
+ }
585
+
586
+ getScrollableNode(): IScrollViewHandle | null {
587
+ return this.list?.getScrollableNode() ?? null;
588
+ }
589
+
590
+ getScrollResponder(): IScrollViewHandle | null {
591
+ return this.list?.getScrollResponder() ?? null;
592
+ }
593
+
594
+ getScrollNode(): ISymbioteNode | null {
595
+ return this.list?.getScrollNode() ?? null;
596
+ }
597
+
598
+ recordInteraction(): void {
599
+ this.list?.recordInteraction();
600
+ }
601
+ }
@@ -0,0 +1,60 @@
1
+ // Regression test for the anchor/class bug: VirtualizedSectionList is its
2
+ // own ANCHOR_HOST_COMPONENTS entry — a class= on <VirtualizedSectionList> resolves onto its OWN
3
+ // anchor and needs its OWN anchorHostStyle merge (see index.ts's resolvedStyle getter), it is NOT
4
+ // transitively fixed by VirtualizedList's/ScrollView's own fixes further down. Mirrors
5
+ // pressable.test.ts's "resolves a class=" case.
6
+ import '@angular/compiler';
7
+ import { Component } from '@angular/core';
8
+ import { afterEach, beforeEach, describe, expect, it } from 'vitest';
9
+ import { clearGlobalStyles, registerStyles } from '@symbiote-native/engine';
10
+ import { installFabric } from '@symbiote-native/test-utils';
11
+ import type { ISection } from '@symbiote-native/components';
12
+
13
+ import { mount, unmount } from '../../render';
14
+ import { VirtualizedSectionList, VSectionItemDirective } from './index';
15
+
16
+ const ROOT_TAG = 954;
17
+ const fabric = installFabric();
18
+ const tick = (): Promise<void> => new Promise(resolve => setTimeout(resolve, 0));
19
+
20
+ interface IRow {
21
+ id: string;
22
+ }
23
+
24
+ const sections: ISection<IRow>[] = [{ title: 'A', data: [{ id: 'a1' }, { id: 'a2' }] }];
25
+
26
+ @Component({
27
+ selector: 'symbiote-virtualized-section-list-class-host',
28
+ standalone: true,
29
+ imports: [VirtualizedSectionList, VSectionItemDirective],
30
+ template: `
31
+ <VirtualizedSectionList class="card" [sections]="sections">
32
+ <ng-template vSectionItem let-item>
33
+ <symbiote-text [testID]="item.id">{{ item.id }}</symbiote-text>
34
+ </ng-template>
35
+ </VirtualizedSectionList>
36
+ `,
37
+ })
38
+ class VirtualizedSectionListClassHost {
39
+ sections = sections;
40
+ }
41
+
42
+ beforeEach(() => {
43
+ fabric.reset();
44
+ registerStyles({ card: { backgroundColor: 'red' } });
45
+ });
46
+ afterEach(() => {
47
+ unmount(ROOT_TAG);
48
+ clearGlobalStyles();
49
+ });
50
+
51
+ describe('VirtualizedSectionList anchor class= resolution', () => {
52
+ it('resolves a class= on the VirtualizedSectionList use site onto the real committed scroll host', async () => {
53
+ mount(ROOT_TAG, VirtualizedSectionListClassHost);
54
+ await tick();
55
+ await tick();
56
+
57
+ const node = fabric.find(n => n.props.backgroundColor === 'red');
58
+ expect(node, 'a real Fabric node carries the class-derived style').toBeDefined();
59
+ });
60
+ });