@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,284 @@
1
+ // RefreshControl, the Angular lifecycle half. On iOS this is the PullToRefreshView Fabric node that
2
+ // lives INSIDE a ScrollView (a childless sibling before the content container); on Android it is
3
+ // AndroidSwipeRefreshLayout and WRAPS the scroll view, receiving it through <ng-content>.
4
+ // The Angular twin of the React/Vue RefreshControl. There is no JS-side platform renaming and no
5
+ // shared render fn — every prop forwards straight to the native node, which reads the ones it
6
+ // understands and ignores the rest, so the Android-only and iOS-only families ride down harmlessly
7
+ // on both. So this folds aria/role through the shared resolveAccessibilityProps and maps the native
8
+ // props + a11y + onRefresh straight onto the symbiote-refresh-control host, children via <ng-content>.
9
+ // No platform branch (one composed component both platforms), so this stays a flat single file.
10
+ //
11
+ // `refreshing` is a controlled prop: the parent owns it and pushes it down each commit; native
12
+ // reports the gesture via the direct `topRefresh` event, which the engine routes to the host's
13
+ // `refresh` listener (Angular blocks [onX] property bindings; events flow through (event) only).
14
+
15
+ import {
16
+ CUSTOM_ELEMENTS_SCHEMA,
17
+ ChangeDetectionStrategy,
18
+ Component,
19
+ ElementRef,
20
+ EventEmitter,
21
+ inject,
22
+ Input,
23
+ Output,
24
+ ViewChild,
25
+ type AfterViewInit,
26
+ type OnChanges,
27
+ type OnInit,
28
+ type SimpleChanges,
29
+ } from '@angular/core';
30
+ import {
31
+ resolveAccessibilityProps,
32
+ type IAccessibilityProps,
33
+ type IAccessibilityStateValue,
34
+ type IAriaProps,
35
+ } from '@symbiote-native/components';
36
+ import {
37
+ dispatchViewCommand,
38
+ dlog,
39
+ isSymbioteEvent,
40
+ isSymbioteNode,
41
+ type IStyleProp,
42
+ type ISymbioteEvent,
43
+ type IViewStyle,
44
+ } from '@symbiote-native/engine';
45
+ import { anchorHostStyle, RefreshControlHost, SymbioteHostPropsDirective } from '../../primitives';
46
+
47
+ // Record<string, unknown> already tolerates `style` holding the [anchorHostStyle, this.style]
48
+ // array anchorHostStyle's merge produces (see hostProps below) — no widening needed.
49
+ type IHostProps = Record<string, unknown>;
50
+
51
+ // Mirrors React's IRefreshControlProps minus children (Angular takes the Android-wrapped scroll view
52
+ // via <ng-content>), declared per-adapter over the shared accessibility base since the framework-specific
53
+ // children slot keeps it from being fully shared across adapters. Read the React reference for the
54
+ // native prop names.
55
+ export interface IAngularRefreshControlProps extends IAccessibilityProps, IAriaProps {
56
+ refreshing: boolean;
57
+ // RN's onRefresh is `() => void | Promise<void>`, the handler may be async; the promise is
58
+ // fire-and-forget (native already starts refreshing on the gesture).
59
+ onRefresh?: () => void | Promise<void>;
60
+ tintColor?: string;
61
+ title?: string;
62
+ titleColor?: string;
63
+ progressViewOffset?: number;
64
+ // Android-only spinner styling (RN RefreshControlPropsAndroid): `colors` are the indicator's
65
+ // animated stroke colors, `progressBackgroundColor` the disc behind it, `size` the diameter preset.
66
+ // AndroidSwipeRefreshLayout reads them; PullToRefreshView on iOS ignores unknown props.
67
+ colors?: readonly string[];
68
+ progressBackgroundColor?: string;
69
+ size?: 'default' | 'large';
70
+ // Android-only native prop forwarded to AndroidSwipeRefreshLayout; iOS native never reads it.
71
+ enabled?: boolean;
72
+ // The Android scroll-view wrap injects the layout half of the style onto this host; iOS leaves it
73
+ // unset (the RefreshControl is a childless sibling). Harmless to forward on both.
74
+ style?: IStyleProp<IViewStyle>;
75
+ }
76
+
77
+ // What the RefreshControl component itself takes as plain @Input()s: the full surface minus the
78
+ // refresh/accessibility events, which it exposes as real @Output() EventEmitters instead (mirrors
79
+ // Pressable's IAngularPressableInputs in pressable/index.ts).
80
+ export type IAngularRefreshControlInputs = Omit<
81
+ IAngularRefreshControlProps,
82
+ | 'onRefresh'
83
+ | 'onAccessibilityAction'
84
+ | 'onAccessibilityTap'
85
+ | 'onMagicTap'
86
+ | 'onAccessibilityEscape'
87
+ >;
88
+
89
+ @Component({
90
+ selector: 'RefreshControl',
91
+ standalone: true,
92
+ schemas: [CUSTOM_ELEMENTS_SCHEMA],
93
+ imports: [RefreshControlHost, SymbioteHostPropsDirective],
94
+ changeDetection: ChangeDetectionStrategy.OnPush,
95
+ template: `
96
+ <symbiote-refresh-control
97
+ #host
98
+ [symbioteHostProps]="hostProps"
99
+ (refresh)="handleRefresh()"
100
+ (accessibilityAction)="emit(accessibilityAction, $event)"
101
+ (accessibilityTap)="emit(accessibilityTap, $event)"
102
+ (magicTap)="emit(magicTap, $event)"
103
+ (accessibilityEscape)="emit(accessibilityEscape, $event)"
104
+ >
105
+ <ng-content></ng-content>
106
+ </symbiote-refresh-control>
107
+ `,
108
+ })
109
+ export class RefreshControl
110
+ implements IAngularRefreshControlInputs, OnInit, OnChanges, AfterViewInit
111
+ {
112
+ // Controlled prop the parent owns; required to match the React reference surface.
113
+ @Input({ required: true }) refreshing!: boolean;
114
+ // RN's onRefresh is `() => void | Promise<void>`, so the callback shape allows an async handler;
115
+ // the @Output() itself only signals the gesture (fire-and-forget, native already starts refreshing).
116
+ @Output() readonly refresh = new EventEmitter<void>();
117
+ @Input() tintColor?: string;
118
+ @Input() title?: string;
119
+ @Input() titleColor?: string;
120
+ @Input() progressViewOffset?: number;
121
+ @Input() colors?: readonly string[];
122
+ @Input() progressBackgroundColor?: string;
123
+ @Input() size?: 'default' | 'large';
124
+ @Input() enabled?: boolean;
125
+ @Input() style?: IStyleProp<IViewStyle>;
126
+ @Input() testID?: string;
127
+ @Input() nativeID?: string;
128
+ @Input() accessible?: boolean;
129
+ @Input() accessibilityLabel?: string;
130
+ @Input() accessibilityHint?: string;
131
+ @Input() accessibilityRole?: IAccessibilityProps['accessibilityRole'];
132
+ @Input() accessibilityState?: IAccessibilityStateValue;
133
+ @Input() accessibilityValue?: IAccessibilityProps['accessibilityValue'];
134
+ @Input() accessibilityActions?: IAccessibilityProps['accessibilityActions'];
135
+ @Input() accessibilityLabelledBy?: string | string[];
136
+ @Input() importantForAccessibility?: IAccessibilityProps['importantForAccessibility'];
137
+ @Input() accessibilityLiveRegion?: IAccessibilityProps['accessibilityLiveRegion'];
138
+ @Input() screenReaderFocusable?: boolean;
139
+ @Input() accessibilityViewIsModal?: boolean;
140
+ @Input() accessibilityElementsHidden?: boolean;
141
+ @Input() accessibilityIgnoresInvertColors?: boolean;
142
+ @Input() accessibilityLanguage?: string;
143
+ @Input() accessibilityRespondsToUserInteraction?: boolean;
144
+ @Input() accessibilityShowsLargeContentViewer?: boolean;
145
+ @Input() accessibilityLargeContentTitle?: string;
146
+ @Output() readonly accessibilityAction = new EventEmitter<ISymbioteEvent>();
147
+ @Output() readonly accessibilityTap = new EventEmitter<ISymbioteEvent>();
148
+ @Output() readonly magicTap = new EventEmitter<ISymbioteEvent>();
149
+ @Output() readonly accessibilityEscape = new EventEmitter<ISymbioteEvent>();
150
+ @Input() role?: IAriaProps['role'];
151
+ @Input('aria-label') ariaLabel?: string;
152
+ @Input('aria-labelledby') ariaLabelledBy?: string;
153
+ @Input('aria-live') ariaLive?: IAriaProps['aria-live'];
154
+ @Input('aria-hidden') ariaHidden?: boolean;
155
+ @Input('aria-busy') ariaBusy?: boolean;
156
+ @Input('aria-checked') ariaChecked?: boolean | 'mixed';
157
+ @Input('aria-disabled') ariaDisabled?: boolean;
158
+ @Input('aria-expanded') ariaExpanded?: boolean;
159
+ @Input('aria-selected') ariaSelected?: boolean;
160
+ @Input('aria-modal') ariaModal?: boolean;
161
+ @Input('aria-valuemax') ariaValueMax?: number;
162
+ @Input('aria-valuemin') ariaValueMin?: number;
163
+ @Input('aria-valuenow') ariaValueNow?: number;
164
+ @Input('aria-valuetext') ariaValueText?: string;
165
+
166
+ // The inner symbiote-refresh-control primitive — NOT this component's own anchor host. Used for
167
+ // the imperative dispatchViewCommand calls below.
168
+ @ViewChild('host') private host?: RefreshControlHost;
169
+
170
+ // This component's OWN host — the non-painting anchor `class="..."` at the use site resolves
171
+ // onto (see anchorHostStyle's doc comment) — distinct from `host` above, which targets the real
172
+ // inner `symbiote-refresh-control` primitive one level down.
173
+ private readonly elementRef = inject(ElementRef);
174
+
175
+ private lastNativeRefreshing = false;
176
+ private refreshNativeNode: unknown;
177
+
178
+ ngOnInit(): void {
179
+ dlog('RefreshControl -> PullToRefreshView');
180
+ dlog(`RefreshControl refreshing=${String(this.refreshing)}`);
181
+ if (this.enabled !== undefined)
182
+ dlog(`RefreshControl enabled=${String(this.enabled)} (Android-only)`);
183
+ if (this.refresh.observed) dlog('RefreshControl refresh listener wired');
184
+ }
185
+
186
+ ngAfterViewInit(): void {
187
+ this.lastNativeRefreshing = this.refreshing;
188
+ }
189
+
190
+ ngOnChanges(changes: SimpleChanges): void {
191
+ const refreshing = changes.refreshing;
192
+ if (refreshing === undefined) return;
193
+ if (refreshing.firstChange) {
194
+ this.lastNativeRefreshing = this.refreshing;
195
+ return;
196
+ }
197
+ this.syncNativeRefreshing(this.refreshNativeNode ?? this.host?.nativeElement);
198
+ }
199
+
200
+ // Native starts the spinner before JS runs. Mirror RN's RefreshControl controlled-component
201
+ // handshake: remember that native is refreshing, call the user's callback, then after Angular has
202
+ // had a chance to propagate `[refreshing]`, force native back to the JS value if it stayed false.
203
+ handleRefresh(nativeNode: unknown = this.host?.nativeElement): void {
204
+ this.refreshNativeNode = nativeNode;
205
+ this.lastNativeRefreshing = true;
206
+ this.refresh.emit();
207
+ queueMicrotask(() => this.syncNativeRefreshing(nativeNode));
208
+ }
209
+
210
+ private syncNativeRefreshing(nativeNode: unknown): void {
211
+ if (this.refreshing === this.lastNativeRefreshing) return;
212
+ if (!isSymbioteNode(nativeNode)) return;
213
+ dispatchViewCommand(nativeNode, 'setNativeRefreshing', [this.refreshing]);
214
+ this.lastNativeRefreshing = this.refreshing;
215
+ }
216
+
217
+ // Forward an engine event to the matching @Output(), narrowing the template's untyped $event
218
+ // first. The accessibility* events arrive on the engine's structural event channel.
219
+ emit(emitter: EventEmitter<ISymbioteEvent>, event: unknown): void {
220
+ if (isSymbioteEvent(event)) emitter.emit(event);
221
+ }
222
+
223
+ // The full native + a11y prop bag applied onto the host in one shot via
224
+ // [symbioteHostProps], instead of enumerating each key as its own template binding. The
225
+ // anchor's class-derived style goes FIRST, this component's own explicit `style` @Input SECOND
226
+ // — flattenStyle's later-wins collapse keeps an explicit [style] winning over its ambient class.
227
+ get hostProps(): IHostProps {
228
+ return {
229
+ refreshing: this.refreshing,
230
+ tintColor: this.tintColor,
231
+ title: this.title,
232
+ titleColor: this.titleColor,
233
+ progressViewOffset: this.progressViewOffset,
234
+ colors: this.colors,
235
+ progressBackgroundColor: this.progressBackgroundColor,
236
+ size: this.size,
237
+ enabled: this.enabled,
238
+ style: [anchorHostStyle(this.elementRef), this.style],
239
+ testID: this.testID,
240
+ nativeID: this.nativeID,
241
+ accessible: this.accessible,
242
+ ...this.folded,
243
+ };
244
+ }
245
+
246
+ // Fold the web aria-*/role aliases into the canonical accessibility* props once per render, so the
247
+ // host node never sees an aria-* key (native ignores them) — the shared transform every adapter runs.
248
+ get folded(): Partial<IAngularRefreshControlProps> {
249
+ return resolveAccessibilityProps({
250
+ accessibilityLabel: this.accessibilityLabel,
251
+ accessibilityHint: this.accessibilityHint,
252
+ accessibilityRole: this.accessibilityRole,
253
+ accessibilityState: this.accessibilityState,
254
+ accessibilityValue: this.accessibilityValue,
255
+ accessibilityActions: this.accessibilityActions,
256
+ accessibilityLabelledBy: this.accessibilityLabelledBy,
257
+ importantForAccessibility: this.importantForAccessibility,
258
+ accessibilityLiveRegion: this.accessibilityLiveRegion,
259
+ screenReaderFocusable: this.screenReaderFocusable,
260
+ accessibilityViewIsModal: this.accessibilityViewIsModal,
261
+ accessibilityElementsHidden: this.accessibilityElementsHidden,
262
+ accessibilityIgnoresInvertColors: this.accessibilityIgnoresInvertColors,
263
+ accessibilityLanguage: this.accessibilityLanguage,
264
+ accessibilityRespondsToUserInteraction: this.accessibilityRespondsToUserInteraction,
265
+ accessibilityShowsLargeContentViewer: this.accessibilityShowsLargeContentViewer,
266
+ accessibilityLargeContentTitle: this.accessibilityLargeContentTitle,
267
+ role: this.role,
268
+ 'aria-label': this.ariaLabel,
269
+ 'aria-labelledby': this.ariaLabelledBy,
270
+ 'aria-live': this.ariaLive,
271
+ 'aria-hidden': this.ariaHidden,
272
+ 'aria-busy': this.ariaBusy,
273
+ 'aria-checked': this.ariaChecked,
274
+ 'aria-disabled': this.ariaDisabled,
275
+ 'aria-expanded': this.ariaExpanded,
276
+ 'aria-selected': this.ariaSelected,
277
+ 'aria-modal': this.ariaModal,
278
+ 'aria-valuemax': this.ariaValueMax,
279
+ 'aria-valuemin': this.ariaValueMin,
280
+ 'aria-valuenow': this.ariaValueNow,
281
+ 'aria-valuetext': this.ariaValueText,
282
+ });
283
+ }
284
+ }
@@ -0,0 +1,41 @@
1
+ import '@angular/compiler';
2
+ import { Component } from '@angular/core';
3
+ import { afterEach, beforeEach, describe, expect, it } from 'vitest';
4
+ import { clearGlobalStyles, registerStyles } from '@symbiote-native/engine';
5
+ import { installFabric } from '@symbiote-native/test-utils';
6
+
7
+ import { mount, unmount } from '../../render';
8
+ import { RefreshControl } from './index';
9
+
10
+ const ROOT_TAG = 914;
11
+ const fabric = installFabric();
12
+
13
+ @Component({
14
+ selector: 'symbiote-refresh-control-host',
15
+ standalone: true,
16
+ imports: [RefreshControl],
17
+ template: `
18
+ <RefreshControl [refreshing]="false" [testID]="'refresh'" class="spinner"></RefreshControl>
19
+ `,
20
+ })
21
+ class RefreshControlHostFixture {}
22
+
23
+ beforeEach(() => {
24
+ fabric.reset();
25
+ });
26
+ afterEach(() => {
27
+ unmount(ROOT_TAG);
28
+ clearGlobalStyles();
29
+ });
30
+
31
+ describe('RefreshControl', () => {
32
+ it('resolves a class= on the RefreshControl use site onto the real committed view, not the anchor', async () => {
33
+ registerStyles({ spinner: { backgroundColor: 'green' } });
34
+
35
+ mount(ROOT_TAG, RefreshControlHostFixture);
36
+ await new Promise<void>(resolve => setTimeout(resolve, 0));
37
+
38
+ const node = fabric.find(n => n.props.testID === 'refresh');
39
+ expect(node?.props.backgroundColor).toBe('green');
40
+ });
41
+ });
@@ -0,0 +1,177 @@
1
+ // SafeAreaView, the Angular lifecycle half. A plain view whose native side insets its
2
+ // children to the safe area (notch, rounded corners, system bars). There is no JS-side
3
+ // translation — RN renders the native RCTSafeAreaView and lets the host do the inset math —
4
+ // so this folds aria/role through the shared resolveAccessibilityProps and maps style +
5
+ // a11y + onLayout straight onto the symbiote-safe-area-view host, children via <ng-content>.
6
+ // The Angular twin of the React/Vue SafeAreaView. No platform branch (one Fabric name both
7
+ // platforms), so this stays a flat single file.
8
+
9
+ import {
10
+ CUSTOM_ELEMENTS_SCHEMA,
11
+ ChangeDetectionStrategy,
12
+ Component,
13
+ ElementRef,
14
+ EventEmitter,
15
+ inject,
16
+ Input,
17
+ Output,
18
+ } from '@angular/core';
19
+ import {
20
+ resolveAccessibilityProps,
21
+ type IAccessibilityProps,
22
+ type IAccessibilityStateValue,
23
+ type IAriaProps,
24
+ } from '@symbiote-native/components';
25
+ import {
26
+ isSymbioteEvent,
27
+ type IStyleProp,
28
+ type ISymbioteEvent,
29
+ type IViewStyle,
30
+ } from '@symbiote-native/engine';
31
+ import { anchorHostStyle, SafeAreaViewHost, SymbioteHostPropsDirective } from '../../primitives';
32
+
33
+ // Mirrors React's ISafeAreaViewProps minus children (Angular takes children via <ng-content>),
34
+ // declared per-adapter over the shared accessibility base since the framework-specific children
35
+ // slot keeps it from being fully shared across adapters.
36
+ export interface IAngularSafeAreaViewProps extends IAccessibilityProps, IAriaProps {
37
+ style?: IStyleProp<IViewStyle>;
38
+ onLayout?: (event: ISymbioteEvent) => void;
39
+ }
40
+
41
+ // What the component itself takes as plain @Input()s: the full surface minus onLayout and the
42
+ // accessibility callbacks, which it exposes as real @Output() EventEmitters instead (see the
43
+ // class below), mirroring Pressable's IAngularPressableInputs split.
44
+ export type IAngularSafeAreaViewInputs = Omit<
45
+ IAngularSafeAreaViewProps,
46
+ | 'onLayout'
47
+ | 'onAccessibilityAction'
48
+ | 'onAccessibilityTap'
49
+ | 'onMagicTap'
50
+ | 'onAccessibilityEscape'
51
+ >;
52
+
53
+ @Component({
54
+ selector: 'SafeAreaView',
55
+ standalone: true,
56
+ schemas: [CUSTOM_ELEMENTS_SCHEMA],
57
+ imports: [SafeAreaViewHost, SymbioteHostPropsDirective],
58
+ changeDetection: ChangeDetectionStrategy.OnPush,
59
+ template: `
60
+ <symbiote-safe-area-view
61
+ [symbioteHostProps]="hostProps"
62
+ (accessibilityAction)="emit(accessibilityAction, $event)"
63
+ (accessibilityTap)="emit(accessibilityTap, $event)"
64
+ (magicTap)="emit(magicTap, $event)"
65
+ (accessibilityEscape)="emit(accessibilityEscape, $event)"
66
+ (layout)="emit(layout, $event)"
67
+ >
68
+ <ng-content></ng-content>
69
+ </symbiote-safe-area-view>
70
+ `,
71
+ })
72
+ export class SafeAreaView implements IAngularSafeAreaViewInputs {
73
+ @Input() style?: IStyleProp<IViewStyle>;
74
+ // Real @Output()s, not `[onX]="…"` callbacks. Safe to name `layout` the same as the native
75
+ // `layout` event fired inside this component's own template — the engine's bubble() treats
76
+ // ANCHOR_HOST_COMPONENTS as transparent to listener lookup, so there is no double-fire.
77
+ @Output() readonly layout = new EventEmitter<ISymbioteEvent>();
78
+ @Output() readonly accessibilityAction = new EventEmitter<ISymbioteEvent>();
79
+ @Output() readonly accessibilityTap = new EventEmitter<ISymbioteEvent>();
80
+ @Output() readonly magicTap = new EventEmitter<ISymbioteEvent>();
81
+ @Output() readonly accessibilityEscape = new EventEmitter<ISymbioteEvent>();
82
+ @Input() testID?: string;
83
+ @Input() nativeID?: string;
84
+ @Input() accessible?: boolean;
85
+ @Input() accessibilityLabel?: string;
86
+ @Input() accessibilityHint?: string;
87
+ @Input() accessibilityRole?: IAccessibilityProps['accessibilityRole'];
88
+ @Input() accessibilityState?: IAccessibilityStateValue;
89
+ @Input() accessibilityValue?: IAccessibilityProps['accessibilityValue'];
90
+ @Input() accessibilityActions?: IAccessibilityProps['accessibilityActions'];
91
+ @Input() accessibilityLabelledBy?: string | string[];
92
+ @Input() importantForAccessibility?: IAccessibilityProps['importantForAccessibility'];
93
+ @Input() accessibilityLiveRegion?: IAccessibilityProps['accessibilityLiveRegion'];
94
+ @Input() screenReaderFocusable?: boolean;
95
+ @Input() accessibilityViewIsModal?: boolean;
96
+ @Input() accessibilityElementsHidden?: boolean;
97
+ @Input() accessibilityIgnoresInvertColors?: boolean;
98
+ @Input() accessibilityLanguage?: string;
99
+ @Input() accessibilityRespondsToUserInteraction?: boolean;
100
+ @Input() accessibilityShowsLargeContentViewer?: boolean;
101
+ @Input() accessibilityLargeContentTitle?: string;
102
+ @Input() role?: IAriaProps['role'];
103
+ @Input('aria-label') ariaLabel?: string;
104
+ @Input('aria-labelledby') ariaLabelledBy?: string;
105
+ @Input('aria-live') ariaLive?: IAriaProps['aria-live'];
106
+ @Input('aria-hidden') ariaHidden?: boolean;
107
+ @Input('aria-busy') ariaBusy?: boolean;
108
+ @Input('aria-checked') ariaChecked?: boolean | 'mixed';
109
+ @Input('aria-disabled') ariaDisabled?: boolean;
110
+ @Input('aria-expanded') ariaExpanded?: boolean;
111
+ @Input('aria-selected') ariaSelected?: boolean;
112
+ @Input('aria-modal') ariaModal?: boolean;
113
+ @Input('aria-valuemax') ariaValueMax?: number;
114
+ @Input('aria-valuemin') ariaValueMin?: number;
115
+ @Input('aria-valuenow') ariaValueNow?: number;
116
+ @Input('aria-valuetext') ariaValueText?: string;
117
+
118
+ // This component's OWN host — the non-painting anchor `class="..."` at the use site resolves
119
+ // onto (see anchorHostStyle's doc comment). SafeAreaView has no inner ViewChild, so this is
120
+ // the only ElementRef in the class.
121
+ private readonly elementRef = inject(ElementRef);
122
+
123
+ // Forward an engine event to the matching @Output(), narrowing the template's untyped
124
+ // $event first. layout / accessibility* arrive on the engine's structural event channel
125
+ // (Angular blocks [onX] property bindings; events flow through (event) only).
126
+ emit(emitter: EventEmitter<ISymbioteEvent>, event: unknown): void {
127
+ if (isSymbioteEvent(event)) emitter.emit(event);
128
+ }
129
+
130
+ // Folds style/testID/nativeID/accessible alongside the accessibility*/aria-*/role surface into
131
+ // one bag for [symbioteHostProps] — resolveAccessibilityProps takes the whole props object (not
132
+ // just the accessibility slice) and returns it with aria-*/role folded into accessibility*, so
133
+ // the host node never sees an aria-* key (native ignores them), same transform React's
134
+ // SafeAreaView runs over its whole `rawProps`. The anchor's class-derived style goes FIRST, this
135
+ // component's own explicit `style` @Input SECOND — flattenStyle's later-wins collapse keeps an
136
+ // explicit [style] winning over its ambient class.
137
+ get hostProps(): Record<string, unknown> {
138
+ return resolveAccessibilityProps({
139
+ style: [anchorHostStyle(this.elementRef), this.style],
140
+ testID: this.testID,
141
+ nativeID: this.nativeID,
142
+ accessible: this.accessible,
143
+ accessibilityLabel: this.accessibilityLabel,
144
+ accessibilityHint: this.accessibilityHint,
145
+ accessibilityRole: this.accessibilityRole,
146
+ accessibilityState: this.accessibilityState,
147
+ accessibilityValue: this.accessibilityValue,
148
+ accessibilityActions: this.accessibilityActions,
149
+ accessibilityLabelledBy: this.accessibilityLabelledBy,
150
+ importantForAccessibility: this.importantForAccessibility,
151
+ accessibilityLiveRegion: this.accessibilityLiveRegion,
152
+ screenReaderFocusable: this.screenReaderFocusable,
153
+ accessibilityViewIsModal: this.accessibilityViewIsModal,
154
+ accessibilityElementsHidden: this.accessibilityElementsHidden,
155
+ accessibilityIgnoresInvertColors: this.accessibilityIgnoresInvertColors,
156
+ accessibilityLanguage: this.accessibilityLanguage,
157
+ accessibilityRespondsToUserInteraction: this.accessibilityRespondsToUserInteraction,
158
+ accessibilityShowsLargeContentViewer: this.accessibilityShowsLargeContentViewer,
159
+ accessibilityLargeContentTitle: this.accessibilityLargeContentTitle,
160
+ role: this.role,
161
+ 'aria-label': this.ariaLabel,
162
+ 'aria-labelledby': this.ariaLabelledBy,
163
+ 'aria-live': this.ariaLive,
164
+ 'aria-hidden': this.ariaHidden,
165
+ 'aria-busy': this.ariaBusy,
166
+ 'aria-checked': this.ariaChecked,
167
+ 'aria-disabled': this.ariaDisabled,
168
+ 'aria-expanded': this.ariaExpanded,
169
+ 'aria-selected': this.ariaSelected,
170
+ 'aria-modal': this.ariaModal,
171
+ 'aria-valuemax': this.ariaValueMax,
172
+ 'aria-valuemin': this.ariaValueMin,
173
+ 'aria-valuenow': this.ariaValueNow,
174
+ 'aria-valuetext': this.ariaValueText,
175
+ });
176
+ }
177
+ }
@@ -0,0 +1,43 @@
1
+ import '@angular/compiler';
2
+ import { Component } from '@angular/core';
3
+ import { afterEach, beforeEach, describe, expect, it } from 'vitest';
4
+ import { clearGlobalStyles, registerStyles } from '@symbiote-native/engine';
5
+ import { installFabric } from '@symbiote-native/test-utils';
6
+
7
+ import { mount, unmount } from '../../render';
8
+ import { SafeAreaView } from './index';
9
+
10
+ const ROOT_TAG = 910;
11
+ const fabric = installFabric();
12
+
13
+ @Component({
14
+ selector: 'symbiote-safe-area-view-host',
15
+ standalone: true,
16
+ imports: [SafeAreaView],
17
+ template: `
18
+ <SafeAreaView [testID]="'safe-area'" class="screen">
19
+ <symbiote-text>Hello</symbiote-text>
20
+ </SafeAreaView>
21
+ `,
22
+ })
23
+ class SafeAreaViewHostFixture {}
24
+
25
+ beforeEach(() => {
26
+ fabric.reset();
27
+ });
28
+ afterEach(() => {
29
+ unmount(ROOT_TAG);
30
+ clearGlobalStyles();
31
+ });
32
+
33
+ describe('SafeAreaView', () => {
34
+ it('resolves a class= on the SafeAreaView use site onto the real committed view, not the anchor', async () => {
35
+ registerStyles({ screen: { backgroundColor: 'navy' } });
36
+
37
+ mount(ROOT_TAG, SafeAreaViewHostFixture);
38
+ await new Promise<void>(resolve => setTimeout(resolve, 0));
39
+
40
+ const node = fabric.find(n => n.props.testID === 'safe-area');
41
+ expect(node?.props.backgroundColor).toBe('navy');
42
+ });
43
+ });
@@ -0,0 +1,123 @@
1
+ // Regression test for an Android content-projection bug:
2
+ // ScrollView on Android picks a different native content view per axis (RCTView vs
3
+ // AndroidHorizontalScrollContentView), which used to require a separate <ng-content> per
4
+ // axis x refresh-control combination — and Angular only reliably projects into a component's
5
+ // LAST-declared <ng-content> when 2+ distinct declarations exist. Fixed by routing all four
6
+ // branches through a single shared <ng-template>/<ng-content> via a local template-outlet
7
+ // directive (see index.android.ts). Covers all four static combinations plus a runtime axis
8
+ // flip, which the projection.ts records/reconcile path must survive without dropping children.
9
+ import '@angular/compiler';
10
+ import { Component, CUSTOM_ELEMENTS_SCHEMA, signal } from '@angular/core';
11
+ import { afterEach, beforeEach, describe, expect, it } from 'vitest';
12
+ import { installFabric } from '@symbiote-native/test-utils';
13
+ import { mount, unmount } from '../../render';
14
+ import { RefreshControl } from '../refresh-control';
15
+ import { ScrollView } from './index.android';
16
+
17
+ const ROOT_TAG = 919;
18
+ const fabric = installFabric();
19
+ const tick = (): Promise<void> => new Promise(resolve => setTimeout(resolve, 0));
20
+
21
+ function expectCellsProjected(): void {
22
+ expect(fabric.find(node => node.props.testID === 'a')).toBeDefined();
23
+ expect(fabric.find(node => node.props.testID === 'b')).toBeDefined();
24
+ }
25
+
26
+ class HorizontalNoRefreshApp {}
27
+ Component({
28
+ selector: 'axis-projection-horizontal-no-refresh',
29
+ standalone: true,
30
+ imports: [ScrollView],
31
+ schemas: [CUSTOM_ELEMENTS_SCHEMA],
32
+ template: `
33
+ <ScrollView [horizontal]="true">
34
+ <symbiote-view testID="a"></symbiote-view>
35
+ <symbiote-view testID="b"></symbiote-view>
36
+ </ScrollView>
37
+ `,
38
+ })(HorizontalNoRefreshApp);
39
+
40
+ class HorizontalRefreshApp {
41
+ refresh = (): void => {};
42
+ }
43
+ Component({
44
+ selector: 'axis-projection-horizontal-refresh',
45
+ standalone: true,
46
+ imports: [ScrollView, RefreshControl],
47
+ schemas: [CUSTOM_ELEMENTS_SCHEMA],
48
+ template: `
49
+ <ScrollView [horizontal]="true">
50
+ <RefreshControl [refreshing]="true" (refresh)="refresh()" />
51
+ <symbiote-view testID="a"></symbiote-view>
52
+ <symbiote-view testID="b"></symbiote-view>
53
+ </ScrollView>
54
+ `,
55
+ })(HorizontalRefreshApp);
56
+
57
+ class VerticalNoRefreshApp {}
58
+ Component({
59
+ selector: 'axis-projection-vertical-no-refresh',
60
+ standalone: true,
61
+ imports: [ScrollView],
62
+ schemas: [CUSTOM_ELEMENTS_SCHEMA],
63
+ template: `
64
+ <ScrollView>
65
+ <symbiote-view testID="a"></symbiote-view>
66
+ <symbiote-view testID="b"></symbiote-view>
67
+ </ScrollView>
68
+ `,
69
+ })(VerticalNoRefreshApp);
70
+
71
+ class AxisSwitchApp {
72
+ static readonly horizontal = signal(false);
73
+ get isHorizontal(): boolean {
74
+ return AxisSwitchApp.horizontal();
75
+ }
76
+ }
77
+ Component({
78
+ selector: 'axis-projection-axis-switch',
79
+ standalone: true,
80
+ imports: [ScrollView],
81
+ schemas: [CUSTOM_ELEMENTS_SCHEMA],
82
+ template: `
83
+ <ScrollView [horizontal]="isHorizontal">
84
+ <symbiote-view testID="a"></symbiote-view>
85
+ <symbiote-view testID="b"></symbiote-view>
86
+ </ScrollView>
87
+ `,
88
+ })(AxisSwitchApp);
89
+
90
+ beforeEach(() => fabric.reset());
91
+ afterEach(() => unmount(ROOT_TAG));
92
+
93
+ describe('Android ScrollView projects content across every axis x refresh combination', () => {
94
+ it('horizontal, no refresh control', async () => {
95
+ mount(ROOT_TAG, HorizontalNoRefreshApp);
96
+ await tick();
97
+ expectCellsProjected();
98
+ });
99
+
100
+ it('horizontal, with a projected refresh control', async () => {
101
+ mount(ROOT_TAG, HorizontalRefreshApp);
102
+ await tick();
103
+ expectCellsProjected();
104
+ });
105
+
106
+ it('vertical, no refresh control', async () => {
107
+ mount(ROOT_TAG, VerticalNoRefreshApp);
108
+ await tick();
109
+ expectCellsProjected();
110
+ });
111
+
112
+ it('survives a runtime vertical -> horizontal axis switch', async () => {
113
+ AxisSwitchApp.horizontal.set(false);
114
+ mount(ROOT_TAG, AxisSwitchApp);
115
+ await tick();
116
+ expectCellsProjected();
117
+
118
+ AxisSwitchApp.horizontal.set(true);
119
+ await tick();
120
+ await tick();
121
+ expectCellsProjected();
122
+ });
123
+ });