@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,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 { Modal } from './index';
9
+
10
+ const ROOT_TAG = 912;
11
+ const fabric = installFabric();
12
+
13
+ @Component({
14
+ selector: 'symbiote-modal-host',
15
+ standalone: true,
16
+ imports: [Modal],
17
+ template: `
18
+ <Modal [visible]="true" [testID]="'modal'" class="sheet">
19
+ <symbiote-text>Hello</symbiote-text>
20
+ </Modal>
21
+ `,
22
+ })
23
+ class ModalHostFixture {}
24
+
25
+ beforeEach(() => {
26
+ fabric.reset();
27
+ });
28
+ afterEach(() => {
29
+ unmount(ROOT_TAG);
30
+ clearGlobalStyles();
31
+ });
32
+
33
+ describe('Modal', () => {
34
+ it('resolves a class= on the Modal use site onto the real committed view, not the anchor', async () => {
35
+ registerStyles({ sheet: { backgroundColor: 'purple' } });
36
+
37
+ mount(ROOT_TAG, ModalHostFixture);
38
+ await new Promise<void>(resolve => setTimeout(resolve, 0));
39
+
40
+ const node = fabric.find(n => n.props.testID === 'modal');
41
+ expect(node?.props.backgroundColor).toBe('purple');
42
+ });
43
+ });
@@ -0,0 +1,373 @@
1
+ import {
2
+ CUSTOM_ELEMENTS_SCHEMA,
3
+ ChangeDetectionStrategy,
4
+ ChangeDetectorRef,
5
+ Component,
6
+ ElementRef,
7
+ EventEmitter,
8
+ inject,
9
+ Input,
10
+ Output,
11
+ ViewChild,
12
+ } from '@angular/core';
13
+ import {
14
+ createPressHandlers,
15
+ createPressRuntime,
16
+ DEFAULT_DELAY_LONG_PRESS_MS,
17
+ isTerminationAllowed,
18
+ resolveAccessibilityProps,
19
+ resolveDisabledAccessibilityState,
20
+ rippleProps,
21
+ shouldClaimResponder,
22
+ shouldSuppressPress,
23
+ type IAccessibilityProps,
24
+ type IAccessibilityStateValue,
25
+ type IAriaProps,
26
+ type IPressableAndroidRippleConfig,
27
+ type IPressHandler,
28
+ type IPressHost,
29
+ type IPressMachineConfig,
30
+ type IPressRuntime,
31
+ type IPressState,
32
+ type IRectOffset,
33
+ } from '@symbiote-native/components';
34
+ import {
35
+ dlog,
36
+ isSymbioteNode,
37
+ measure,
38
+ type ISymbioteEvent,
39
+ type ISymbioteNode,
40
+ type IStyleProp,
41
+ type IViewStyle,
42
+ } from '@symbiote-native/engine';
43
+ import { anchorHostStyle, SymbioteHostPropsDirective, ViewHost } from '../../primitives';
44
+
45
+ export type { IPressState, IPressableAndroidRippleConfig } from '@symbiote-native/components';
46
+
47
+ // The full logical Pressable surface, callback-shaped press/hover events included. Touchable* and
48
+ // TouchableNativeFeedback still take their OWN onPress/onPressIn/... as @Input() callbacks (that is
49
+ // a separate, not-yet-converted decision) and derive their public prop types from this - so it keeps
50
+ // the callback fields even though the Pressable component below no longer implements them directly.
51
+ export interface IAngularPressableProps extends IAccessibilityProps, IAriaProps {
52
+ onPress?: IPressHandler;
53
+ onPressIn?: IPressHandler;
54
+ onPressOut?: IPressHandler;
55
+ onPressMove?: IPressHandler;
56
+ onLongPress?: IPressHandler;
57
+ delayLongPress?: number;
58
+ disabled?: boolean;
59
+ cancelable?: boolean;
60
+ hitSlop?: IRectOffset;
61
+ pressRetentionOffset?: IRectOffset;
62
+ unstable_pressDelay?: number;
63
+ android_ripple?: IPressableAndroidRippleConfig;
64
+ android_disableSound?: boolean;
65
+ onHoverIn?: IPressHandler;
66
+ onHoverOut?: IPressHandler;
67
+ delayHoverIn?: number;
68
+ delayHoverOut?: number;
69
+ testID?: string;
70
+ nativeID?: string;
71
+ hasTVPreferredFocus?: boolean;
72
+ nextFocusDown?: number;
73
+ nextFocusForward?: number;
74
+ nextFocusLeft?: number;
75
+ nextFocusRight?: number;
76
+ nextFocusUp?: number;
77
+ style?: IStyleProp<IViewStyle> | ((state: IPressState) => IStyleProp<IViewStyle>);
78
+ }
79
+
80
+ // What the Pressable component itself takes as plain @Input()s: the full surface minus the press/
81
+ // hover events and the four accessibility callbacks, all of which it exposes as real @Output()
82
+ // EventEmitters instead (see the class below) - the one place in the family where the
83
+ // callback-vs-EventEmitter tradeoff is actually fixed.
84
+ export type IAngularPressableInputs = Omit<
85
+ IAngularPressableProps,
86
+ | 'onPress'
87
+ | 'onPressIn'
88
+ | 'onPressOut'
89
+ | 'onPressMove'
90
+ | 'onLongPress'
91
+ | 'onHoverIn'
92
+ | 'onHoverOut'
93
+ | 'onAccessibilityAction'
94
+ | 'onAccessibilityTap'
95
+ | 'onMagicTap'
96
+ | 'onAccessibilityEscape'
97
+ >;
98
+
99
+ function isStyleFn(value: unknown): value is (state: IPressState) => IStyleProp<IViewStyle> {
100
+ return typeof value === 'function';
101
+ }
102
+
103
+ function asSymbioteEvent(event: unknown): ISymbioteEvent | undefined {
104
+ return typeof event === 'object' && event !== null && 'nativeEvent' in event
105
+ ? (event as ISymbioteEvent)
106
+ : undefined;
107
+ }
108
+
109
+ @Component({
110
+ selector: 'Pressable, symbiote-pressable',
111
+ standalone: true,
112
+ schemas: [CUSTOM_ELEMENTS_SCHEMA],
113
+ imports: [ViewHost, SymbioteHostPropsDirective],
114
+ template: `
115
+ <symbiote-view
116
+ #host
117
+ [symbioteHostProps]="hostProps"
118
+ (accessibilityAction)="emit(accessibilityAction, $event)"
119
+ (accessibilityTap)="emit(accessibilityTap, $event)"
120
+ (magicTap)="emit(magicTap, $event)"
121
+ (accessibilityEscape)="emit(accessibilityEscape, $event)"
122
+ (press)="handlePress($event)"
123
+ (pressIn)="handlePressIn($event)"
124
+ (pressOut)="handlePressOut($event)"
125
+ (responderMove)="handleResponderMove($event)"
126
+ (startShouldSetResponder)="claimResponder()"
127
+ (responderTerminationRequest)="allowTermination()"
128
+ >
129
+ <ng-content></ng-content>
130
+ </symbiote-view>
131
+ `,
132
+ changeDetection: ChangeDetectionStrategy.OnPush,
133
+ })
134
+ export class Pressable implements IAngularPressableInputs {
135
+ // The press/hover lifecycle as real Angular events: `(press)="onTap($event)"`, not
136
+ // `[onPress]="onTap"`. createPressHandlers still wants plain IPressHandler callbacks, so
137
+ // emitterHandler() below adapts each EventEmitter into one - only while something is actually
138
+ // subscribed (`.observed`), so an unbound onLongPress still skips arming the timer, exactly as
139
+ // the old @Input()-absent case did.
140
+ @Output() readonly press = new EventEmitter<ISymbioteEvent>();
141
+ @Output() readonly pressIn = new EventEmitter<ISymbioteEvent>();
142
+ @Output() readonly pressOut = new EventEmitter<ISymbioteEvent>();
143
+ @Output() readonly pressMove = new EventEmitter<ISymbioteEvent>();
144
+ @Output() readonly longPress = new EventEmitter<ISymbioteEvent>();
145
+ @Output() readonly hoverIn = new EventEmitter<ISymbioteEvent>();
146
+ @Output() readonly hoverOut = new EventEmitter<ISymbioteEvent>();
147
+ // The four accessibility callbacks as real Angular events too, same tradeoff as press/hover
148
+ // above: `(accessibilityAction)="emit(accessibilityAction, $event)"`, not
149
+ // `[onAccessibilityAction]="onAccessibilityAction"`.
150
+ @Output() readonly accessibilityAction = new EventEmitter<ISymbioteEvent>();
151
+ @Output() readonly accessibilityTap = new EventEmitter<ISymbioteEvent>();
152
+ @Output() readonly magicTap = new EventEmitter<ISymbioteEvent>();
153
+ @Output() readonly accessibilityEscape = new EventEmitter<ISymbioteEvent>();
154
+ @Input() delayLongPress?: number;
155
+ @Input() disabled?: boolean;
156
+ @Input() cancelable?: boolean;
157
+ @Input() hitSlop?: IRectOffset;
158
+ @Input() pressRetentionOffset?: IRectOffset;
159
+ @Input() unstable_pressDelay?: number;
160
+ @Input() android_ripple?: IPressableAndroidRippleConfig;
161
+ @Input() android_disableSound?: boolean;
162
+ @Input() delayHoverIn?: number;
163
+ @Input() delayHoverOut?: number;
164
+ @Input() testID?: string;
165
+ @Input() nativeID?: string;
166
+ @Input() hasTVPreferredFocus?: boolean;
167
+ @Input() nextFocusDown?: number;
168
+ @Input() nextFocusForward?: number;
169
+ @Input() nextFocusLeft?: number;
170
+ @Input() nextFocusRight?: number;
171
+ @Input() nextFocusUp?: number;
172
+ @Input() style?: IAngularPressableProps['style'];
173
+ @Input() accessible?: boolean;
174
+ @Input() accessibilityLabel?: string;
175
+ @Input() accessibilityHint?: string;
176
+ @Input() accessibilityRole?: IAngularPressableProps['accessibilityRole'];
177
+ @Input() accessibilityState?: IAccessibilityStateValue;
178
+ @Input() accessibilityValue?: IAngularPressableProps['accessibilityValue'];
179
+ @Input() accessibilityActions?: IAngularPressableProps['accessibilityActions'];
180
+ @Input() accessibilityLabelledBy?: string | string[];
181
+ @Input() importantForAccessibility?: IAccessibilityProps['importantForAccessibility'];
182
+ @Input() accessibilityLiveRegion?: IAccessibilityProps['accessibilityLiveRegion'];
183
+ @Input() screenReaderFocusable?: boolean;
184
+ @Input() accessibilityViewIsModal?: boolean;
185
+ @Input() accessibilityElementsHidden?: boolean;
186
+ @Input() accessibilityIgnoresInvertColors?: boolean;
187
+ @Input() accessibilityLanguage?: string;
188
+ @Input() accessibilityRespondsToUserInteraction?: boolean;
189
+ @Input() accessibilityShowsLargeContentViewer?: boolean;
190
+ @Input() accessibilityLargeContentTitle?: string;
191
+ @Input() ariaLabel?: string;
192
+ @Input() ariaBusy?: boolean;
193
+ @Input() ariaChecked?: boolean | 'mixed';
194
+ @Input() ariaDisabled?: boolean;
195
+ @Input() ariaExpanded?: boolean;
196
+ @Input() ariaHidden?: boolean;
197
+ @Input() ariaLabelledBy?: string;
198
+ @Input() ariaLive?: IAriaProps['aria-live'];
199
+ @Input() ariaSelected?: boolean;
200
+ @Input() ariaModal?: boolean;
201
+ @Input() ariaValueMax?: number;
202
+ @Input() ariaValueMin?: number;
203
+ @Input() ariaValueNow?: number;
204
+ @Input() ariaValueText?: string;
205
+ @Input() id?: string;
206
+ @Input() role?: IAngularPressableProps['role'];
207
+
208
+ @ViewChild('host', { read: ElementRef }) private hostElement?: ElementRef<unknown>;
209
+
210
+ pressed = false;
211
+ private readonly runtime: IPressRuntime = createPressRuntime();
212
+ private readonly host: IPressHost = {
213
+ setPressed: pressed => {
214
+ this.pressed = pressed;
215
+ this.changeDetector.detectChanges();
216
+ },
217
+ getMeasureFn: () => {
218
+ const node = this.hostNode;
219
+ if (node === undefined) return undefined;
220
+ return callback => measure(node, callback);
221
+ },
222
+ schedule: (callback, ms) => {
223
+ const id = setTimeout(callback, ms);
224
+ return () => clearTimeout(id);
225
+ },
226
+ };
227
+
228
+ private readonly changeDetector = inject(ChangeDetectorRef);
229
+ // This component's OWN host - the non-painting anchor `class="..."` at the use site resolves
230
+ // onto (see anchorHostStyle's doc comment) - NOT `hostElement` above, which targets the real
231
+ // inner `symbiote-view` one level down.
232
+ private readonly elementRef = inject(ElementRef);
233
+
234
+ get resolvedStyle(): IStyleProp<IViewStyle> | undefined {
235
+ const state: IPressState = { pressed: this.pressed };
236
+ return isStyleFn(this.style) ? this.style(state) : this.style;
237
+ }
238
+
239
+ get foldedAccessibility(): Partial<IAngularPressableProps> {
240
+ return resolveAccessibilityProps({
241
+ accessibilityLabel: this.accessibilityLabel,
242
+ accessibilityHint: this.accessibilityHint,
243
+ accessibilityRole: this.accessibilityRole,
244
+ accessibilityState: resolveDisabledAccessibilityState(this.accessibilityState, this.disabled),
245
+ accessibilityValue: this.accessibilityValue,
246
+ accessibilityActions: this.accessibilityActions,
247
+ accessibilityLabelledBy: this.accessibilityLabelledBy,
248
+ importantForAccessibility: this.importantForAccessibility,
249
+ accessibilityLiveRegion: this.accessibilityLiveRegion,
250
+ screenReaderFocusable: this.screenReaderFocusable,
251
+ accessibilityViewIsModal: this.accessibilityViewIsModal,
252
+ accessibilityElementsHidden: this.accessibilityElementsHidden,
253
+ accessibilityIgnoresInvertColors: this.accessibilityIgnoresInvertColors,
254
+ accessibilityLanguage: this.accessibilityLanguage,
255
+ accessibilityRespondsToUserInteraction: this.accessibilityRespondsToUserInteraction,
256
+ accessibilityShowsLargeContentViewer: this.accessibilityShowsLargeContentViewer,
257
+ accessibilityLargeContentTitle: this.accessibilityLargeContentTitle,
258
+ 'aria-label': this.ariaLabel,
259
+ 'aria-busy': this.ariaBusy,
260
+ 'aria-checked': this.ariaChecked,
261
+ 'aria-disabled': this.ariaDisabled,
262
+ 'aria-expanded': this.ariaExpanded,
263
+ 'aria-hidden': this.ariaHidden,
264
+ 'aria-labelledby': this.ariaLabelledBy,
265
+ 'aria-live': this.ariaLive,
266
+ 'aria-selected': this.ariaSelected,
267
+ 'aria-modal': this.ariaModal,
268
+ 'aria-valuemax': this.ariaValueMax,
269
+ 'aria-valuemin': this.ariaValueMin,
270
+ 'aria-valuenow': this.ariaValueNow,
271
+ 'aria-valuetext': this.ariaValueText,
272
+ id: this.id,
273
+ role: this.role,
274
+ });
275
+ }
276
+
277
+ get resolvedRippleProps(): Record<string, unknown> | undefined {
278
+ return this.android_ripple === undefined ? undefined : rippleProps(this.android_ripple);
279
+ }
280
+
281
+ // Flat bag for `[symbioteHostProps]` (Renderer2.setProperty per key) - see primitives/shared.ts.
282
+ get hostProps(): Record<string, unknown> {
283
+ return {
284
+ testID: this.testID,
285
+ nativeID: this.nativeID,
286
+ hasTVPreferredFocus: this.hasTVPreferredFocus,
287
+ nextFocusDown: this.nextFocusDown,
288
+ nextFocusForward: this.nextFocusForward,
289
+ nextFocusLeft: this.nextFocusLeft,
290
+ nextFocusRight: this.nextFocusRight,
291
+ nextFocusUp: this.nextFocusUp,
292
+ style: [anchorHostStyle(this.elementRef), this.resolvedStyle],
293
+ accessible: this.accessible,
294
+ ...this.foldedAccessibility,
295
+ android_disableSound: this.android_disableSound,
296
+ nativeBackgroundAndroid: this.resolvedRippleProps?.nativeBackgroundAndroid,
297
+ nativeForegroundAndroid: this.resolvedRippleProps?.nativeForegroundAndroid,
298
+ };
299
+ }
300
+
301
+ handlePressIn(event: unknown): void {
302
+ const symbioteEvent = asSymbioteEvent(event);
303
+ if (shouldSuppressPress(this.disabled) || symbioteEvent === undefined) return;
304
+ this.handlers.handlePressIn(symbioteEvent);
305
+ }
306
+
307
+ handlePressOut(event: unknown): void {
308
+ const symbioteEvent = asSymbioteEvent(event);
309
+ if (shouldSuppressPress(this.disabled) || symbioteEvent === undefined) return;
310
+ this.handlers.handlePressOut(symbioteEvent);
311
+ }
312
+
313
+ handlePress(event: unknown): void {
314
+ const symbioteEvent = asSymbioteEvent(event);
315
+ if (shouldSuppressPress(this.disabled) || symbioteEvent === undefined) return;
316
+ this.handlers.handlePress(symbioteEvent);
317
+ }
318
+
319
+ handleResponderMove(event: unknown): void {
320
+ const symbioteEvent = asSymbioteEvent(event);
321
+ if (shouldSuppressPress(this.disabled) || symbioteEvent === undefined) return;
322
+ this.handlers.handleResponderMove(symbioteEvent);
323
+ }
324
+
325
+ claimResponder(): boolean {
326
+ return shouldClaimResponder(this.disabled);
327
+ }
328
+
329
+ allowTermination(): boolean {
330
+ return isTerminationAllowed(this.cancelable);
331
+ }
332
+
333
+ emit(emitter: EventEmitter<ISymbioteEvent>, event: unknown): void {
334
+ const symbioteEvent = asSymbioteEvent(event);
335
+ if (symbioteEvent !== undefined) emitter.emit(symbioteEvent);
336
+ }
337
+
338
+ // Hover has no event on a touch host (no pointer-enter/leave); this only records that someone
339
+ // is listening anyway, mirroring core's noteHoverNoop but off `.observed` instead of an @Input.
340
+ private noteHoverNoop(): void {
341
+ if (this.hoverIn.observed || this.hoverOut.observed) {
342
+ dlog('Pressable hover is a no-op on this host (no pointer-enter/leave event)');
343
+ }
344
+ }
345
+
346
+ // Wraps an @Output() as an IPressHandler only while it has a subscriber, so an unbound
347
+ // (longPress) still skips arming the timer, matching createPressHandlers' "undefined means
348
+ // nobody cares" contract.
349
+ private emitterHandler(emitter: EventEmitter<ISymbioteEvent>): IPressHandler | undefined {
350
+ return emitter.observed ? event => emitter.emit(event) : undefined;
351
+ }
352
+
353
+ private get handlers() {
354
+ this.noteHoverNoop();
355
+ const config: IPressMachineConfig = {
356
+ onPress: this.emitterHandler(this.press),
357
+ onPressIn: this.emitterHandler(this.pressIn),
358
+ onPressOut: this.emitterHandler(this.pressOut),
359
+ onPressMove: this.emitterHandler(this.pressMove),
360
+ onLongPress: this.emitterHandler(this.longPress),
361
+ delayLongPress: this.delayLongPress ?? DEFAULT_DELAY_LONG_PRESS_MS,
362
+ unstable_pressDelay: this.unstable_pressDelay ?? 0,
363
+ hitSlop: this.hitSlop,
364
+ pressRetentionOffset: this.pressRetentionOffset,
365
+ };
366
+ return createPressHandlers(config, this.runtime, this.host);
367
+ }
368
+
369
+ private get hostNode(): ISymbioteNode | undefined {
370
+ const native = this.hostElement?.nativeElement;
371
+ return isSymbioteNode(native) ? native : undefined;
372
+ }
373
+ }
@@ -0,0 +1,85 @@
1
+ import '@angular/compiler';
2
+ import { Component } from '@angular/core';
3
+ import { afterEach, beforeEach, describe, expect, it, vi } 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 { Pressable } from './index';
9
+
10
+ const ROOT_TAG = 903;
11
+ const fabric = installFabric();
12
+
13
+ let capturedHost: PressableHost | undefined;
14
+
15
+ @Component({
16
+ selector: 'symbiote-pressable-host',
17
+ standalone: true,
18
+ imports: [Pressable],
19
+ template: `
20
+ <Pressable
21
+ [testID]="'pressable'"
22
+ class="card"
23
+ (press)="onPress($event)"
24
+ (pressIn)="onPressIn($event)"
25
+ (pressOut)="onPressOut($event)"
26
+ >
27
+ <symbiote-text>Press me</symbiote-text>
28
+ </Pressable>
29
+ `,
30
+ })
31
+ class PressableHost {
32
+ onPress = vi.fn();
33
+ onPressIn = vi.fn();
34
+ onPressOut = vi.fn();
35
+
36
+ constructor() {
37
+ // Captures the live component instance so the test can drive its signals after mount.
38
+ // eslint-disable-next-line @typescript-eslint/no-this-alias
39
+ capturedHost = this;
40
+ }
41
+ }
42
+
43
+ beforeEach(() => {
44
+ capturedHost = undefined;
45
+ fabric.reset();
46
+ });
47
+ afterEach(() => {
48
+ unmount(ROOT_TAG);
49
+ clearGlobalStyles();
50
+ });
51
+
52
+ describe('Pressable', () => {
53
+ it('synthesizes a press from a touch sequence and fires the lifecycle handlers', async () => {
54
+ mount(ROOT_TAG, PressableHost);
55
+ await new Promise<void>(resolve => setTimeout(resolve, 0));
56
+
57
+ const node = fabric.find(n => n.props.testID === 'pressable');
58
+ expect(node, 'Pressable view committed').toBeDefined();
59
+
60
+ const now = Date.now();
61
+ const touch = { identifier: 1, pageX: 10, pageY: 10, timestamp: now };
62
+ const nativeEvent = { touches: [touch], changedTouches: [touch] };
63
+
64
+ fabric.fireEvent(node?.instanceHandle, 'topTouchStart', nativeEvent);
65
+ await new Promise<void>(resolve => setTimeout(resolve, 0));
66
+
67
+ expect(capturedHost?.onPressIn).toHaveBeenCalledOnce();
68
+
69
+ fabric.fireEvent(node?.instanceHandle, 'topTouchEnd', nativeEvent);
70
+ await new Promise<void>(resolve => setTimeout(resolve, 0));
71
+
72
+ expect(capturedHost?.onPress).toHaveBeenCalledOnce();
73
+ expect(capturedHost?.onPressOut).toHaveBeenCalledOnce();
74
+ });
75
+
76
+ it('resolves a class= on the Pressable use site onto the real committed view, not the anchor', async () => {
77
+ registerStyles({ card: { backgroundColor: 'red' } });
78
+
79
+ mount(ROOT_TAG, PressableHost);
80
+ await new Promise<void>(resolve => setTimeout(resolve, 0));
81
+
82
+ const node = fabric.find(n => n.props.testID === 'pressable');
83
+ expect(node?.props.backgroundColor).toBe('red');
84
+ });
85
+ });