@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,102 @@
1
+ // Regression coverage for the anchor-vs-real-content class bug (see pressable.test.ts for the
2
+ // full writeup): a `class="..."` written on a composed component's OWN use site always resolves
3
+ // through Angular's addClass/removeClass onto that component's non-painting ANCHOR host, never
4
+ // onto the real committed Fabric view one (or more) levels down. Each Touchable forwards its own
5
+ // anchor's resolved style into whatever it commits, mirroring Pressable's fix.
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
+
12
+ import { mount, unmount } from '../../render';
13
+ import { TouchableHighlight, TouchableOpacity, TouchableWithoutFeedback } from './index';
14
+
15
+ const ROOT_TAG = 940;
16
+ const fabric = installFabric();
17
+
18
+ beforeEach(() => {
19
+ fabric.reset();
20
+ });
21
+ afterEach(() => {
22
+ unmount(ROOT_TAG);
23
+ clearGlobalStyles();
24
+ });
25
+
26
+ @Component({
27
+ selector: 'symbiote-touchable-opacity-host',
28
+ standalone: true,
29
+ imports: [TouchableOpacity],
30
+ template: `
31
+ <TouchableOpacity [testID]="'opacity'" class="card">
32
+ <symbiote-text>Press</symbiote-text>
33
+ </TouchableOpacity>
34
+ `,
35
+ })
36
+ class TouchableOpacityHost {}
37
+
38
+ @Component({
39
+ selector: 'symbiote-touchable-highlight-host',
40
+ standalone: true,
41
+ imports: [TouchableHighlight],
42
+ template: `
43
+ <TouchableHighlight [testID]="'highlight'" class="card">
44
+ <symbiote-text>Press</symbiote-text>
45
+ </TouchableHighlight>
46
+ `,
47
+ })
48
+ class TouchableHighlightHost {}
49
+
50
+ @Component({
51
+ selector: 'symbiote-touchable-without-feedback-host',
52
+ standalone: true,
53
+ imports: [TouchableWithoutFeedback],
54
+ template: `
55
+ <TouchableWithoutFeedback [testID]="'without-feedback'" class="card">
56
+ <symbiote-text>Press</symbiote-text>
57
+ </TouchableWithoutFeedback>
58
+ `,
59
+ })
60
+ class TouchableWithoutFeedbackHost {}
61
+
62
+ describe('TouchableOpacity', () => {
63
+ it('resolves a class= on the TouchableOpacity use site onto the real committed view, not the anchor', async () => {
64
+ registerStyles({ card: { backgroundColor: 'red' } });
65
+
66
+ mount(ROOT_TAG, TouchableOpacityHost);
67
+ await new Promise<void>(resolve => setTimeout(resolve, 0));
68
+
69
+ // TouchableOpacity folds its OWN class-derived style into the inner AnimatedView leaf, not
70
+ // the outer Pressable — mirroring React's TouchableOpacity (see adapters/react/src/components/
71
+ // touchable/index.ts's comment: "className is pulled out here, like style, and applied to the
72
+ // inner AnimatedView... it would land on the outer Pressable instead, which is not what a user
73
+ // expects"), so the testID-carrying outer view is NOT where the resolved style lands.
74
+ expect(fabric.find(n => n.props.testID === 'opacity')).toBeDefined();
75
+ const node = fabric.find(n => n.props.backgroundColor === 'red');
76
+ expect(node, 'a committed node received the class-derived style').toBeDefined();
77
+ });
78
+ });
79
+
80
+ describe('TouchableHighlight', () => {
81
+ it('resolves a class= on the TouchableHighlight use site onto the real committed view, not the anchor', async () => {
82
+ registerStyles({ card: { backgroundColor: 'red' } });
83
+
84
+ mount(ROOT_TAG, TouchableHighlightHost);
85
+ await new Promise<void>(resolve => setTimeout(resolve, 0));
86
+
87
+ const node = fabric.find(n => n.props.testID === 'highlight');
88
+ expect(node?.props.backgroundColor).toBe('red');
89
+ });
90
+ });
91
+
92
+ describe('TouchableWithoutFeedback', () => {
93
+ it('resolves a class= on the TouchableWithoutFeedback use site onto the real committed view, not the anchor', async () => {
94
+ registerStyles({ card: { backgroundColor: 'red' } });
95
+
96
+ mount(ROOT_TAG, TouchableWithoutFeedbackHost);
97
+ await new Promise<void>(resolve => setTimeout(resolve, 0));
98
+
99
+ const node = fabric.find(n => n.props.testID === 'without-feedback');
100
+ expect(node?.props.backgroundColor).toBe('red');
101
+ });
102
+ });
@@ -0,0 +1,254 @@
1
+ // TouchableNativeFeedback: the Angular lifecycle half. Android's ripple / state-drawable
2
+ // touchable, built on Pressable like the rest of the family (the Angular twin of the React/Vue
3
+ // adapter). RN realizes its feedback by cloning the child into an RCTView carrying native ripple
4
+ // props; we instead nest the child under a feedback <symbiote-view> that carries those props
5
+ // (nativeBackgroundAndroid / nativeForegroundAndroid), inside a <Pressable> that owns the press
6
+ // wiring. The native props are read by Android's ReactViewManager; on iOS they are inert props,
7
+ // so the child still renders with working press wiring. The static factories + background mapping
8
+ // are shared in @symbiote-native/components and reused verbatim — Angular only attaches them onto the
9
+ // component class and nests the feedback view. No JS-side platform branch (one Fabric path both
10
+ // platforms), so this stays a flat single file, mirroring React/Vue.
11
+ //
12
+ // This component's own press/hover events are real @Output() EventEmitters too, matching
13
+ // Pressable (which it wraps) — `(press)="press.emit($event)"`, never `[onPress]="onPress"`.
14
+ // Non-event config (delayLongPress, hitSlop, ...) still rides down as plain @Input() bindings. The
15
+ // a11y identity bag + the four a11y events live on the feedback view — the one host intrinsic here —
16
+ // folded through the shared resolveAccessibilityProps and routed through emit() on the (event)
17
+ // channel (Angular blocks [onX] property bindings on host elements; events flow through (event)
18
+ // only). disabled folds into the a11y state via resolveDisabledAccessibilityState, exactly as
19
+ // Pressable does for its own host.
20
+
21
+ import {
22
+ CUSTOM_ELEMENTS_SCHEMA,
23
+ ChangeDetectionStrategy,
24
+ Component,
25
+ ElementRef,
26
+ EventEmitter,
27
+ inject,
28
+ Input,
29
+ Output,
30
+ type OnChanges,
31
+ } from '@angular/core';
32
+ import {
33
+ backgroundProps,
34
+ canUseNativeForeground,
35
+ resolveAccessibilityProps,
36
+ resolveDisabledAccessibilityState,
37
+ rippleBackground,
38
+ selectableBackground,
39
+ selectableBackgroundBorderless,
40
+ type IAccessibilityProps,
41
+ type IAccessibilityStateValue,
42
+ type IAriaProps,
43
+ type INativeFeedbackBackground,
44
+ type IPressableAndroidRippleConfig,
45
+ type IRectOffset,
46
+ } from '@symbiote-native/components';
47
+ import { dlog, isSymbioteEvent, type ISymbioteEvent } from '@symbiote-native/engine';
48
+ import { anchorHostStyle, SymbioteHostPropsDirective, ViewHost } from '../../primitives';
49
+ import { Pressable, type IAngularPressableInputs } from '../pressable';
50
+
51
+ export type {
52
+ INativeFeedbackBackground,
53
+ IThemeAttrBackground,
54
+ IRippleBackground,
55
+ } from '@symbiote-native/components';
56
+
57
+ // Mirrors React's ITouchableNativeFeedbackProps (Omit<IPressableProps, 'style'> + the native
58
+ // feedback config) minus children (Angular takes children via <ng-content>) and minus the press/
59
+ // hover events (declared as this component's OWN @Output()s below). Declared per-adapter over the
60
+ // shared Pressable INPUT surface.
61
+ export type IAngularTouchableNativeFeedbackProps = Omit<IAngularPressableInputs, 'style'> & {
62
+ background?: INativeFeedbackBackground;
63
+ useForeground?: boolean;
64
+ };
65
+
66
+ @Component({
67
+ selector: 'TouchableNativeFeedback',
68
+ standalone: true,
69
+ schemas: [CUSTOM_ELEMENTS_SCHEMA],
70
+ imports: [Pressable, ViewHost, SymbioteHostPropsDirective],
71
+ changeDetection: ChangeDetectionStrategy.OnPush,
72
+ template: `
73
+ <Pressable
74
+ (press)="press.emit($event)"
75
+ (pressIn)="pressIn.emit($event)"
76
+ (pressOut)="pressOut.emit($event)"
77
+ (pressMove)="pressMove.emit($event)"
78
+ (longPress)="longPress.emit($event)"
79
+ (hoverIn)="hoverIn.emit($event)"
80
+ (hoverOut)="hoverOut.emit($event)"
81
+ [delayLongPress]="delayLongPress"
82
+ [delayHoverIn]="delayHoverIn"
83
+ [delayHoverOut]="delayHoverOut"
84
+ [disabled]="disabled"
85
+ [cancelable]="cancelable"
86
+ [hitSlop]="hitSlop"
87
+ [pressRetentionOffset]="pressRetentionOffset"
88
+ [unstable_pressDelay]="unstable_pressDelay"
89
+ [android_ripple]="android_ripple"
90
+ [android_disableSound]="android_disableSound"
91
+ >
92
+ <symbiote-view
93
+ [symbioteHostProps]="hostProps"
94
+ (accessibilityAction)="emit(accessibilityAction, $event)"
95
+ (accessibilityTap)="emit(accessibilityTap, $event)"
96
+ (magicTap)="emit(magicTap, $event)"
97
+ (accessibilityEscape)="emit(accessibilityEscape, $event)"
98
+ >
99
+ <ng-content></ng-content>
100
+ </symbiote-view>
101
+ </Pressable>
102
+ `,
103
+ })
104
+ export class TouchableNativeFeedback implements IAngularTouchableNativeFeedbackProps, OnChanges {
105
+ // The static helpers are pure config-dict producers; they live as members on the component class
106
+ // so callers reach them as `TouchableNativeFeedback.Ripple(...)`, exactly like RN.
107
+ static readonly SelectableBackground = selectableBackground;
108
+ static readonly SelectableBackgroundBorderless = selectableBackgroundBorderless;
109
+ static readonly Ripple = rippleBackground;
110
+ static readonly canUseNativeForeground = canUseNativeForeground;
111
+
112
+ @Output() readonly press = new EventEmitter<ISymbioteEvent>();
113
+ @Output() readonly pressIn = new EventEmitter<ISymbioteEvent>();
114
+ @Output() readonly pressOut = new EventEmitter<ISymbioteEvent>();
115
+ @Output() readonly pressMove = new EventEmitter<ISymbioteEvent>();
116
+ @Output() readonly longPress = new EventEmitter<ISymbioteEvent>();
117
+ @Output() readonly hoverIn = new EventEmitter<ISymbioteEvent>();
118
+ @Output() readonly hoverOut = new EventEmitter<ISymbioteEvent>();
119
+ @Output() readonly accessibilityAction = new EventEmitter<ISymbioteEvent>();
120
+ @Output() readonly accessibilityTap = new EventEmitter<ISymbioteEvent>();
121
+ @Output() readonly magicTap = new EventEmitter<ISymbioteEvent>();
122
+ @Output() readonly accessibilityEscape = new EventEmitter<ISymbioteEvent>();
123
+ @Input() background?: INativeFeedbackBackground;
124
+ @Input() useForeground?: boolean;
125
+ @Input() delayLongPress?: number;
126
+ @Input() delayHoverIn?: number;
127
+ @Input() delayHoverOut?: number;
128
+ @Input() disabled?: boolean;
129
+ @Input() cancelable?: boolean;
130
+ @Input() hitSlop?: IRectOffset;
131
+ @Input() pressRetentionOffset?: IRectOffset;
132
+ @Input() unstable_pressDelay?: number;
133
+ @Input() android_ripple?: IPressableAndroidRippleConfig;
134
+ @Input() android_disableSound?: boolean;
135
+ @Input() testID?: string;
136
+ @Input() nativeID?: string;
137
+ @Input() accessible?: boolean;
138
+ @Input() accessibilityLabel?: string;
139
+ @Input() accessibilityHint?: string;
140
+ @Input() accessibilityRole?: IAccessibilityProps['accessibilityRole'];
141
+ @Input() accessibilityState?: IAccessibilityStateValue;
142
+ @Input() accessibilityValue?: IAccessibilityProps['accessibilityValue'];
143
+ @Input() accessibilityActions?: IAccessibilityProps['accessibilityActions'];
144
+ @Input() accessibilityLabelledBy?: string | string[];
145
+ @Input() importantForAccessibility?: IAccessibilityProps['importantForAccessibility'];
146
+ @Input() accessibilityLiveRegion?: IAccessibilityProps['accessibilityLiveRegion'];
147
+ @Input() screenReaderFocusable?: boolean;
148
+ @Input() accessibilityViewIsModal?: boolean;
149
+ @Input() accessibilityElementsHidden?: boolean;
150
+ @Input() accessibilityIgnoresInvertColors?: boolean;
151
+ @Input() accessibilityLanguage?: string;
152
+ @Input() accessibilityRespondsToUserInteraction?: boolean;
153
+ @Input() accessibilityShowsLargeContentViewer?: boolean;
154
+ @Input() accessibilityLargeContentTitle?: string;
155
+ @Input() role?: IAriaProps['role'];
156
+ @Input('aria-label') ariaLabel?: string;
157
+ @Input('aria-labelledby') ariaLabelledBy?: string;
158
+ @Input('aria-live') ariaLive?: IAriaProps['aria-live'];
159
+ @Input('aria-hidden') ariaHidden?: boolean;
160
+ @Input('aria-busy') ariaBusy?: boolean;
161
+ @Input('aria-checked') ariaChecked?: boolean | 'mixed';
162
+ @Input('aria-disabled') ariaDisabled?: boolean;
163
+ @Input('aria-expanded') ariaExpanded?: boolean;
164
+ @Input('aria-selected') ariaSelected?: boolean;
165
+ @Input('aria-modal') ariaModal?: boolean;
166
+ @Input('aria-valuemax') ariaValueMax?: number;
167
+ @Input('aria-valuemin') ariaValueMin?: number;
168
+ @Input('aria-valuenow') ariaValueNow?: number;
169
+ @Input('aria-valuetext') ariaValueText?: string;
170
+
171
+ // This component's OWN host — the non-painting anchor `class="..."` at the use site resolves
172
+ // onto (see anchorHostStyle's doc comment) — NOT the feedback <symbiote-view> one level down.
173
+ // TouchableNativeFeedback has no explicit `style`/`class`-forwarding @Input() of its own (RN's
174
+ // TouchableNativeFeedback takes none either — style/class always came from the child), so the
175
+ // anchor's class-derived style is the ONLY style source for hostProps.style below.
176
+ private readonly elementRef = inject(ElementRef);
177
+
178
+ ngOnChanges(): void {
179
+ dlog(
180
+ `TouchableNativeFeedback render ${this.resolvedBackground.type} useForeground ${this.useForeground === true}`,
181
+ );
182
+ }
183
+
184
+ // RN defaults a missing background to SelectableBackground() so the touchable always shows
185
+ // feedback; mirror that here.
186
+ private get resolvedBackground(): INativeFeedbackBackground {
187
+ return this.background ?? selectableBackground();
188
+ }
189
+
190
+ // backgroundProps picks the foreground slot only where the platform supports it, else the
191
+ // background slot (canUseNativeForeground) — shared with React/Vue. One side is always undefined.
192
+ get feedback(): Record<string, INativeFeedbackBackground> {
193
+ return backgroundProps(this.resolvedBackground, this.useForeground === true);
194
+ }
195
+
196
+ // Assembles the SAME resolved values the template used to bind one-by-one into a single
197
+ // flat bag for `[symbioteHostProps]` (Renderer2.setProperty per key) — see primitives/shared.ts.
198
+ get hostProps(): Record<string, unknown> {
199
+ return {
200
+ nativeBackgroundAndroid: this.feedback['nativeBackgroundAndroid'],
201
+ nativeForegroundAndroid: this.feedback['nativeForegroundAndroid'],
202
+ style: anchorHostStyle(this.elementRef),
203
+ testID: this.testID,
204
+ nativeID: this.nativeID,
205
+ accessible: this.accessible,
206
+ ...this.folded,
207
+ };
208
+ }
209
+
210
+ // Forward an engine event to the matching @Output(), narrowing the template's untyped $event.
211
+ emit(emitter: EventEmitter<ISymbioteEvent>, event: unknown): void {
212
+ if (isSymbioteEvent(event)) emitter.emit(event);
213
+ }
214
+
215
+ // Fold the web aria-*/role aliases into the canonical accessibility* props once per render, so the
216
+ // host never sees an aria-* key (native ignores them), and merge `disabled` into the a11y state
217
+ // (resolveDisabledAccessibilityState) the same way Pressable does for its own host.
218
+ get folded(): Partial<IAngularTouchableNativeFeedbackProps> {
219
+ return resolveAccessibilityProps({
220
+ accessibilityLabel: this.accessibilityLabel,
221
+ accessibilityHint: this.accessibilityHint,
222
+ accessibilityRole: this.accessibilityRole,
223
+ accessibilityState: resolveDisabledAccessibilityState(this.accessibilityState, this.disabled),
224
+ accessibilityValue: this.accessibilityValue,
225
+ accessibilityActions: this.accessibilityActions,
226
+ accessibilityLabelledBy: this.accessibilityLabelledBy,
227
+ importantForAccessibility: this.importantForAccessibility,
228
+ accessibilityLiveRegion: this.accessibilityLiveRegion,
229
+ screenReaderFocusable: this.screenReaderFocusable,
230
+ accessibilityViewIsModal: this.accessibilityViewIsModal,
231
+ accessibilityElementsHidden: this.accessibilityElementsHidden,
232
+ accessibilityIgnoresInvertColors: this.accessibilityIgnoresInvertColors,
233
+ accessibilityLanguage: this.accessibilityLanguage,
234
+ accessibilityRespondsToUserInteraction: this.accessibilityRespondsToUserInteraction,
235
+ accessibilityShowsLargeContentViewer: this.accessibilityShowsLargeContentViewer,
236
+ accessibilityLargeContentTitle: this.accessibilityLargeContentTitle,
237
+ role: this.role,
238
+ 'aria-label': this.ariaLabel,
239
+ 'aria-labelledby': this.ariaLabelledBy,
240
+ 'aria-live': this.ariaLive,
241
+ 'aria-hidden': this.ariaHidden,
242
+ 'aria-busy': this.ariaBusy,
243
+ 'aria-checked': this.ariaChecked,
244
+ 'aria-disabled': this.ariaDisabled,
245
+ 'aria-expanded': this.ariaExpanded,
246
+ 'aria-selected': this.ariaSelected,
247
+ 'aria-modal': this.ariaModal,
248
+ 'aria-valuemax': this.ariaValueMax,
249
+ 'aria-valuemin': this.ariaValueMin,
250
+ 'aria-valuenow': this.ariaValueNow,
251
+ 'aria-valuetext': this.ariaValueText,
252
+ });
253
+ }
254
+ }
@@ -0,0 +1,49 @@
1
+ // Regression coverage for the anchor-vs-real-content class bug (see pressable.test.ts for the
2
+ // full writeup): a `class="..."` written on TouchableNativeFeedback's OWN use site always resolves
3
+ // through Angular's addClass/removeClass onto its non-painting ANCHOR host, never onto the real
4
+ // committed feedback <symbiote-view> one level down. TouchableNativeFeedback has no explicit
5
+ // `style`/`class`-forwarding @Input() of its own, so the anchor's class-derived style is the
6
+ // ONLY style source hostProps.style forwards.
7
+ import '@angular/compiler';
8
+ import { Component } from '@angular/core';
9
+ import { afterEach, beforeEach, describe, expect, it } from 'vitest';
10
+ import { clearGlobalStyles, registerStyles } from '@symbiote-native/engine';
11
+ import { installFabric } from '@symbiote-native/test-utils';
12
+
13
+ import { mount, unmount } from '../../render';
14
+ import { TouchableNativeFeedback } from './index';
15
+
16
+ const ROOT_TAG = 941;
17
+ const fabric = installFabric();
18
+
19
+ beforeEach(() => {
20
+ fabric.reset();
21
+ });
22
+ afterEach(() => {
23
+ unmount(ROOT_TAG);
24
+ clearGlobalStyles();
25
+ });
26
+
27
+ @Component({
28
+ selector: 'symbiote-touchable-native-feedback-host',
29
+ standalone: true,
30
+ imports: [TouchableNativeFeedback],
31
+ template: `
32
+ <TouchableNativeFeedback [testID]="'native-feedback'" class="card">
33
+ <symbiote-text>Press</symbiote-text>
34
+ </TouchableNativeFeedback>
35
+ `,
36
+ })
37
+ class TouchableNativeFeedbackHost {}
38
+
39
+ describe('TouchableNativeFeedback', () => {
40
+ it('resolves a class= on the TouchableNativeFeedback use site onto the real committed view, not the anchor', async () => {
41
+ registerStyles({ card: { backgroundColor: 'red' } });
42
+
43
+ mount(ROOT_TAG, TouchableNativeFeedbackHost);
44
+ await new Promise<void>(resolve => setTimeout(resolve, 0));
45
+
46
+ const node = fabric.find(n => n.props.testID === 'native-feedback');
47
+ expect(node?.props.backgroundColor).toBe('red');
48
+ });
49
+ });
@@ -0,0 +1,168 @@
1
+ // VirtualizedList authoring directives: the Angular-faithful equivalent of React/Vue
2
+ // `renderItem` / `ListHeaderComponent` / `ListFooterComponent` / `ListEmptyComponent` /
3
+ // `ItemSeparatorComponent`. React/Vue pass an element-returning callback; that does NOT
4
+ // translate to Angular, where per-item content is a TEMPLATE. So the app supplies each
5
+ // slot as an `<ng-template>` carrying one of these structural directives, and the list
6
+ // stamps the windowed slice through them. The agnostic windowing surface stays shared,
7
+ // only the cell-AUTHORING shape is framework-specific.
8
+ //
9
+ // Authoring API (what FlatList / SectionList build on, and what the app imports):
10
+ // <VirtualizedList [data]="rows" [getItem]="getItem" [getItemCount]="count">
11
+ // <ng-template vListItem let-item let-index="index" let-separators="separators"> … </ng-template>
12
+ // <ng-template vListHeader> … </ng-template>
13
+ // <ng-template vListFooter> … </ng-template>
14
+ // <ng-template vListEmpty> … </ng-template>
15
+ // <ng-template vListSeparator let-highlighted="highlighted" let-leadingItem="leadingItem"> … </ng-template>
16
+ // </VirtualizedList>
17
+ //
18
+ // `vListItem` carries a static ngTemplateContextGuard so `let-item` infers as ItemT and
19
+ // `let-index` / `let-separators` are typed — the Angular twin of `renderItem`'s typed info arg.
20
+ //
21
+ // VListOutletDirective is the INTERNAL renderer the list uses to stamp those templates. It is
22
+ // a core-only reimplementation of `@angular/common`'s NgTemplateOutlet (the adapter depends on
23
+ // @angular/core ONLY — see package.json), built on ViewContainerRef / TemplateRef from
24
+ // @angular/core: create on template change, update the embedded view's context in place on a
25
+ // context change (no churn of the cell's own view across windowing recomputes).
26
+
27
+ import {
28
+ Directive,
29
+ Input,
30
+ TemplateRef,
31
+ ViewContainerRef,
32
+ inject,
33
+ type EmbeddedViewRef,
34
+ type OnChanges,
35
+ type OnDestroy,
36
+ type SimpleChanges,
37
+ } from '@angular/core';
38
+ import { dlog } from '@symbiote-native/engine';
39
+ import type { ISeparators } from '@symbiote-native/components';
40
+
41
+ let vListOutletInstanceCounter = 0;
42
+
43
+ // The context a `vListItem` template receives, mirroring RN's renderItem info arg
44
+ // ({ item, index, separators }). `$implicit` is the item, so `let-item` (no key) binds it.
45
+ export interface IVListItemContext<ItemT> {
46
+ $implicit: ItemT;
47
+ index: number;
48
+ separators: ISeparators;
49
+ }
50
+
51
+ // The context a `vListSeparator` template receives, mirroring RN's ItemSeparatorComponent props
52
+ // (the highlight flag the cell toggles + the items flanking the gap). `$implicit` is the highlight
53
+ // flag, so `let-highlighted` binds it.
54
+ export interface IVListSeparatorContext<ItemT> {
55
+ $implicit: boolean;
56
+ highlighted: boolean;
57
+ leadingItem?: ItemT;
58
+ trailingItem?: ItemT;
59
+ // RN lets a row drive arbitrary separator props via separators.updateProps; they ride here.
60
+ [key: string]: unknown;
61
+ }
62
+
63
+ function isRecord(value: unknown): value is Record<string, unknown> {
64
+ return typeof value === 'object' && value !== null;
65
+ }
66
+
67
+ // `<ng-template vListItem>` — the per-cell content. The static guard makes the `let-` bindings
68
+ // typed at the call site (let-item: ItemT, let-index: number, let-separators: ISeparators).
69
+ @Directive({ selector: '[vListItem]', standalone: true })
70
+ export class VListItemDirective<ItemT = unknown> {
71
+ readonly templateRef = inject<TemplateRef<IVListItemContext<ItemT>>>(TemplateRef);
72
+
73
+ static ngTemplateContextGuard<T>(
74
+ _dir: VListItemDirective<T>,
75
+ _ctx: unknown,
76
+ ): _ctx is IVListItemContext<T> {
77
+ return true;
78
+ }
79
+ }
80
+
81
+ // `<ng-template vListHeader>` — rendered once above the cells (RN ListHeaderComponent).
82
+ @Directive({ selector: '[vListHeader]', standalone: true })
83
+ export class VListHeaderDirective {
84
+ readonly templateRef = inject(TemplateRef);
85
+ }
86
+
87
+ // `<ng-template vListFooter>` — rendered once below the cells (RN ListFooterComponent).
88
+ @Directive({ selector: '[vListFooter]', standalone: true })
89
+ export class VListFooterDirective {
90
+ readonly templateRef = inject(TemplateRef);
91
+ }
92
+
93
+ // `<ng-template vListEmpty>` — rendered in place of the cells when the list is empty
94
+ // (RN ListEmptyComponent).
95
+ @Directive({ selector: '[vListEmpty]', standalone: true })
96
+ export class VListEmptyDirective {
97
+ readonly templateRef = inject(TemplateRef);
98
+ }
99
+
100
+ // `<ng-template vListSeparator>` — rendered between cells (RN ItemSeparatorComponent). The guard
101
+ // types the `let-` bindings against the separator props.
102
+ @Directive({ selector: '[vListSeparator]', standalone: true })
103
+ export class VListSeparatorDirective<ItemT = unknown> {
104
+ readonly templateRef = inject<TemplateRef<IVListSeparatorContext<ItemT>>>(TemplateRef);
105
+
106
+ static ngTemplateContextGuard<T>(
107
+ _dir: VListSeparatorDirective<T>,
108
+ _ctx: unknown,
109
+ ): _ctx is IVListSeparatorContext<T> {
110
+ return true;
111
+ }
112
+ }
113
+
114
+ // Core-only NgTemplateOutlet twin. The adapter cannot import @angular/common's NgTemplateOutlet
115
+ // (not a dependency), so the list stamps templates through this: it creates the embedded view when
116
+ // the template changes and updates the live view's context IN PLACE on a context change, so a
117
+ // windowing recompute (a fresh context object every CD) refreshes the cell without tearing it down.
118
+ @Directive({ selector: '[vListOutlet]', standalone: true })
119
+ export class VListOutletDirective<C = unknown> implements OnChanges, OnDestroy {
120
+ @Input({ alias: 'vListOutlet' }) templateRef?: TemplateRef<C>;
121
+ @Input({ alias: 'vListOutletContext' }) context?: C;
122
+
123
+ private viewRef: EmbeddedViewRef<C> | null = null;
124
+ private readonly viewContainer = inject(ViewContainerRef);
125
+ private readonly instanceId = (vListOutletInstanceCounter += 1);
126
+
127
+ ngOnChanges(changes: SimpleChanges): void {
128
+ if (changes['templateRef'] !== undefined) {
129
+ dlog(
130
+ `Angular VListOutlet#${this.instanceId} templateRef CHANGED (was=${changes['templateRef'].previousValue !== undefined} now=${this.templateRef !== undefined}) -> clear + recreate`,
131
+ );
132
+ this.viewContainer.clear();
133
+ this.viewRef =
134
+ this.templateRef === undefined
135
+ ? null
136
+ : this.viewContainer.createEmbeddedView(this.templateRef, this.context);
137
+ return;
138
+ }
139
+ if (this.viewRef !== null && this.context !== undefined) {
140
+ dlog(`Angular VListOutlet#${this.instanceId} context updated, markForCheck`);
141
+ this.updateContext(this.viewRef.context, this.context);
142
+ this.viewRef.markForCheck();
143
+ }
144
+ }
145
+
146
+ ngOnDestroy(): void {
147
+ dlog(`Angular VListOutlet#${this.instanceId} destroyed`);
148
+ this.viewContainer.clear();
149
+ }
150
+
151
+ // Copy the new context's fields onto the live embedded view's context object (whose identity the
152
+ // consumer's `let-` bindings already read), dropping keys that disappeared. The generic `C` can't
153
+ // be index-WRITTEN even once narrowed (TS2862), so the copy runs in a non-generic helper the
154
+ // guarded records flow into — no cast (NgTemplateOutlet does the same copy via `as any`).
155
+ private updateContext(viewContext: C, nextContext: C): void {
156
+ if (!isRecord(viewContext) || !isRecord(nextContext)) return;
157
+ copyContextFields(viewContext, nextContext);
158
+ }
159
+ }
160
+
161
+ function copyContextFields(target: Record<string, unknown>, source: Record<string, unknown>): void {
162
+ for (const key of Object.keys(target)) {
163
+ if (!(key in source)) delete target[key];
164
+ }
165
+ for (const key of Object.keys(source)) {
166
+ target[key] = source[key];
167
+ }
168
+ }