@symbiote-native/angular 1.0.0 → 2.0.1

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 (248) hide show
  1. package/README.md +13 -10
  2. package/babel-register-composed.cjs +41 -19
  3. package/bin/symbiote-angular-dev.cjs +16 -4
  4. package/build/angular/anchor-host-registry.js +1 -17
  5. package/build/angular/anchor-host-registry.js.map +1 -1
  6. package/build/angular/change-detection-flush.d.ts +31 -0
  7. package/build/angular/change-detection-flush.js +110 -0
  8. package/build/angular/change-detection-flush.js.map +1 -0
  9. package/build/angular/components/activity-indicator-props.d.ts +2 -0
  10. package/build/angular/components/activity-indicator-props.js +2 -0
  11. package/build/angular/components/activity-indicator-props.js.map +1 -0
  12. package/build/angular/components/button-props.d.ts +2 -0
  13. package/build/angular/components/button-props.js +2 -0
  14. package/build/angular/components/button-props.js.map +1 -0
  15. package/build/angular/components/flat-list/index.d.ts +2 -1
  16. package/build/angular/components/flat-list/index.js +22 -10
  17. package/build/angular/components/flat-list/index.js.map +1 -1
  18. package/build/angular/components/image-background-props.d.ts +6 -0
  19. package/build/angular/components/image-background-props.js +2 -0
  20. package/build/angular/components/image-background-props.js.map +1 -0
  21. package/build/angular/components/{image/shared.d.ts → image-shared.d.ts} +1 -1
  22. package/build/angular/components/{image/shared.js → image-shared.js} +7 -2
  23. package/build/angular/components/image-shared.js.map +1 -0
  24. package/build/angular/components/input-accessory-view-props.d.ts +7 -0
  25. package/build/angular/components/input-accessory-view-props.js +9 -0
  26. package/build/angular/components/input-accessory-view-props.js.map +1 -0
  27. package/build/angular/components/keyboard-avoiding-view/index.js +11 -17
  28. package/build/angular/components/keyboard-avoiding-view/index.js.map +1 -1
  29. package/build/angular/components/modal/index.d.ts +1 -0
  30. package/build/angular/components/modal/index.js +19 -12
  31. package/build/angular/components/modal/index.js.map +1 -1
  32. package/build/angular/components/pressable-props.d.ts +32 -0
  33. package/build/angular/components/pressable-props.js +11 -0
  34. package/build/angular/components/pressable-props.js.map +1 -0
  35. package/build/angular/components/refresh-control-props.d.ts +16 -0
  36. package/build/angular/components/refresh-control-props.js +14 -0
  37. package/build/angular/components/refresh-control-props.js.map +1 -0
  38. package/build/angular/components/safe-area-view-props.d.ts +7 -0
  39. package/build/angular/components/safe-area-view-props.js +11 -0
  40. package/build/angular/components/safe-area-view-props.js.map +1 -0
  41. package/build/angular/components/scroll-view-props.d.ts +76 -0
  42. package/build/angular/components/scroll-view-props.js +18 -0
  43. package/build/angular/components/scroll-view-props.js.map +1 -0
  44. package/build/angular/components/text-input-props.d.ts +45 -0
  45. package/build/angular/components/text-input-props.js +18 -0
  46. package/build/angular/components/text-input-props.js.map +1 -0
  47. package/build/angular/components/touchable-native-feedback/touchable-native-feedback-props.d.ts +6 -0
  48. package/build/angular/components/touchable-native-feedback/touchable-native-feedback-props.js +2 -0
  49. package/build/angular/components/touchable-native-feedback/touchable-native-feedback-props.js.map +1 -0
  50. package/build/angular/components/touchable-props.d.ts +14 -0
  51. package/build/angular/components/touchable-props.js +13 -0
  52. package/build/angular/components/touchable-props.js.map +1 -0
  53. package/build/angular/components/touchable-without-feedback/touchable-without-feedback-props.d.ts +3 -0
  54. package/build/angular/components/touchable-without-feedback/touchable-without-feedback-props.js +2 -0
  55. package/build/angular/components/touchable-without-feedback/touchable-without-feedback-props.js.map +1 -0
  56. package/build/angular/components/virtualized-list/index.d.ts +5 -1
  57. package/build/angular/components/virtualized-list/index.js +366 -280
  58. package/build/angular/components/virtualized-list/index.js.map +1 -1
  59. package/build/angular/components.d.ts +20 -26
  60. package/build/angular/components.js +12 -14
  61. package/build/angular/components.js.map +1 -1
  62. package/build/angular/create-portal/index.d.ts +2 -2
  63. package/build/angular/create-portal/index.js +5 -5
  64. package/build/angular/create-tunnel/index.js +1 -1
  65. package/build/angular/element-props.d.ts +53 -0
  66. package/build/angular/element-props.js +2 -0
  67. package/build/angular/element-props.js.map +1 -0
  68. package/build/angular/elements.d.ts +528 -0
  69. package/build/angular/elements.js +1444 -0
  70. package/build/angular/elements.js.map +1 -0
  71. package/build/angular/host-instance/index.js +1 -1
  72. package/build/angular/index.d.ts +4 -2
  73. package/build/angular/index.js +15 -1
  74. package/build/angular/index.js.map +1 -1
  75. package/build/angular/modules/animated/create-animated-component.d.ts +5 -6
  76. package/build/angular/modules/animated/create-animated-component.js +60 -73
  77. package/build/angular/modules/animated/create-animated-component.js.map +1 -1
  78. package/build/angular/modules/animated/index.js +1 -1
  79. package/build/angular/modules/animated/index.js.map +1 -1
  80. package/build/angular/modules/image/index.d.ts +3 -0
  81. package/build/angular/modules/image/index.js +16 -0
  82. package/build/angular/modules/image/index.js.map +1 -0
  83. package/build/angular/primitives/index.d.ts +18 -18
  84. package/build/angular/primitives/index.js +40 -36
  85. package/build/angular/primitives/index.js.map +1 -1
  86. package/build/angular/primitives/shared.d.ts +2 -3
  87. package/build/angular/primitives/shared.js +8 -53
  88. package/build/angular/primitives/shared.js.map +1 -1
  89. package/build/angular/register.d.ts +1 -1
  90. package/build/angular/register.js +12 -31
  91. package/build/angular/register.js.map +1 -1
  92. package/build/angular/render/index.js +3 -3
  93. package/build/angular/render/index.js.map +1 -1
  94. package/build/angular/renderer/index.js +113 -67
  95. package/build/angular/renderer/index.js.map +1 -1
  96. package/build/angular/renderer/value-change.d.ts +2 -0
  97. package/build/angular/renderer/value-change.js +9 -0
  98. package/build/angular/renderer/value-change.js.map +1 -0
  99. package/metro-config.cjs +54 -20
  100. package/metro-css-parser.cjs +2 -1
  101. package/package.json +5 -14
  102. package/src/anchor-host-registry.ts +1 -17
  103. package/src/change-detection-flush.ts +120 -0
  104. package/src/components/activity-indicator-props.ts +21 -0
  105. package/src/components/button-props.ts +26 -0
  106. package/src/components/flat-list/index.ts +12 -4
  107. package/src/components/image-background-props.ts +24 -0
  108. package/src/components/{image/shared.ts → image-shared.ts} +6 -1
  109. package/src/components/input-accessory-view-props.ts +21 -0
  110. package/src/components/keyboard-avoiding-view/index.ts +6 -9
  111. package/src/components/modal/index.ts +15 -7
  112. package/src/components/pressable-props.ts +73 -0
  113. package/src/components/refresh-control-props.ts +43 -0
  114. package/src/components/safe-area-view-props.ts +28 -0
  115. package/src/components/scroll-view-props.ts +109 -0
  116. package/src/components/text-input-props.ts +72 -0
  117. package/src/components/touchable-native-feedback/touchable-native-feedback-props.ts +23 -0
  118. package/src/components/touchable-props.ts +30 -0
  119. package/src/components/touchable-without-feedback/touchable-without-feedback-props.ts +21 -0
  120. package/src/components/virtualized-list/index.ts +239 -156
  121. package/src/components.ts +84 -35
  122. package/src/create-portal/index.ts +5 -5
  123. package/src/create-tunnel/index.ts +1 -1
  124. package/src/diagnostics.ts +1 -1
  125. package/src/element-props.ts +90 -0
  126. package/src/elements.ts +919 -0
  127. package/src/host-instance/index.ts +1 -1
  128. package/src/index.ts +52 -15
  129. package/src/modules/animated/create-animated-component.ts +50 -55
  130. package/src/modules/animated/index.ts +1 -1
  131. package/src/modules/image/index.ts +18 -0
  132. package/src/primitives/index.ts +22 -18
  133. package/src/primitives/shared.ts +11 -44
  134. package/src/register.ts +12 -40
  135. package/src/render/index.ts +3 -3
  136. package/src/renderer/index.ts +122 -73
  137. package/src/renderer/value-change.ts +8 -0
  138. package/babel-lower-host-primitives.cjs +0 -636
  139. package/build/angular/components/activity-indicator/index.android.d.ts +0 -13
  140. package/build/angular/components/activity-indicator/index.android.js +0 -29
  141. package/build/angular/components/activity-indicator/index.android.js.map +0 -1
  142. package/build/angular/components/activity-indicator/index.d.ts +0 -1
  143. package/build/angular/components/activity-indicator/index.ios.d.ts +0 -9
  144. package/build/angular/components/activity-indicator/index.ios.js +0 -26
  145. package/build/angular/components/activity-indicator/index.ios.js.map +0 -1
  146. package/build/angular/components/activity-indicator/index.js +0 -4
  147. package/build/angular/components/activity-indicator/index.js.map +0 -1
  148. package/build/angular/components/activity-indicator/shared.d.ts +0 -67
  149. package/build/angular/components/activity-indicator/shared.js +0 -260
  150. package/build/angular/components/activity-indicator/shared.js.map +0 -1
  151. package/build/angular/components/button.d.ts +0 -68
  152. package/build/angular/components/button.js +0 -360
  153. package/build/angular/components/button.js.map +0 -1
  154. package/build/angular/components/image/index.android.d.ts +0 -15
  155. package/build/angular/components/image/index.android.js +0 -83
  156. package/build/angular/components/image/index.android.js.map +0 -1
  157. package/build/angular/components/image/index.d.ts +0 -1
  158. package/build/angular/components/image/index.ios.d.ts +0 -15
  159. package/build/angular/components/image/index.ios.js +0 -83
  160. package/build/angular/components/image/index.ios.js.map +0 -1
  161. package/build/angular/components/image/index.js +0 -3
  162. package/build/angular/components/image/index.js.map +0 -1
  163. package/build/angular/components/image/shared.js.map +0 -1
  164. package/build/angular/components/image-background.d.ts +0 -85
  165. package/build/angular/components/image-background.js +0 -443
  166. package/build/angular/components/image-background.js.map +0 -1
  167. package/build/angular/components/input-accessory-view/index.d.ts +0 -66
  168. package/build/angular/components/input-accessory-view/index.js +0 -287
  169. package/build/angular/components/input-accessory-view/index.js.map +0 -1
  170. package/build/angular/components/pressable/index.d.ts +0 -134
  171. package/build/angular/components/pressable/index.js +0 -514
  172. package/build/angular/components/pressable/index.js.map +0 -1
  173. package/build/angular/components/refresh-control/index.d.ts +0 -91
  174. package/build/angular/components/refresh-control/index.js +0 -382
  175. package/build/angular/components/refresh-control/index.js.map +0 -1
  176. package/build/angular/components/safe-area-view/index.d.ts +0 -64
  177. package/build/angular/components/safe-area-view/index.js +0 -287
  178. package/build/angular/components/safe-area-view/index.js.map +0 -1
  179. package/build/angular/components/scroll-view/index.android.d.ts +0 -20
  180. package/build/angular/components/scroll-view/index.android.js +0 -262
  181. package/build/angular/components/scroll-view/index.android.js.map +0 -1
  182. package/build/angular/components/scroll-view/index.d.ts +0 -3
  183. package/build/angular/components/scroll-view/index.ios.d.ts +0 -8
  184. package/build/angular/components/scroll-view/index.ios.js +0 -92
  185. package/build/angular/components/scroll-view/index.ios.js.map +0 -1
  186. package/build/angular/components/scroll-view/index.js +0 -6
  187. package/build/angular/components/scroll-view/index.js.map +0 -1
  188. package/build/angular/components/scroll-view/projection.d.ts +0 -67
  189. package/build/angular/components/scroll-view/projection.js +0 -560
  190. package/build/angular/components/scroll-view/projection.js.map +0 -1
  191. package/build/angular/components/scroll-view/shared.d.ts +0 -241
  192. package/build/angular/components/scroll-view/shared.js +0 -848
  193. package/build/angular/components/scroll-view/shared.js.map +0 -1
  194. package/build/angular/components/scroll-view/sticky-header.d.ts +0 -40
  195. package/build/angular/components/scroll-view/sticky-header.js +0 -224
  196. package/build/angular/components/scroll-view/sticky-header.js.map +0 -1
  197. package/build/angular/components/switch/index.android.d.ts +0 -24
  198. package/build/angular/components/switch/index.android.js +0 -112
  199. package/build/angular/components/switch/index.android.js.map +0 -1
  200. package/build/angular/components/switch/index.d.ts +0 -1
  201. package/build/angular/components/switch/index.ios.d.ts +0 -23
  202. package/build/angular/components/switch/index.ios.js +0 -111
  203. package/build/angular/components/switch/index.ios.js.map +0 -1
  204. package/build/angular/components/switch/index.js +0 -4
  205. package/build/angular/components/switch/index.js.map +0 -1
  206. package/build/angular/components/switch/shared.d.ts +0 -49
  207. package/build/angular/components/switch/shared.js +0 -191
  208. package/build/angular/components/switch/shared.js.map +0 -1
  209. package/build/angular/components/text-input.d.ts +0 -167
  210. package/build/angular/components/text-input.js +0 -684
  211. package/build/angular/components/text-input.js.map +0 -1
  212. package/build/angular/components/touchable/index.d.ts +0 -283
  213. package/build/angular/components/touchable/index.js +0 -1453
  214. package/build/angular/components/touchable/index.js.map +0 -1
  215. package/build/angular/components/touchable-native-feedback/index.d.ts +0 -87
  216. package/build/angular/components/touchable-native-feedback/index.js +0 -409
  217. package/build/angular/components/touchable-native-feedback/index.js.map +0 -1
  218. package/build/angular/state-style.d.ts +0 -9
  219. package/build/angular/state-style.js +0 -39
  220. package/build/angular/state-style.js.map +0 -1
  221. package/metro-transformer.cjs +0 -69
  222. package/src/components/activity-indicator/index.android.ts +0 -23
  223. package/src/components/activity-indicator/index.ios.ts +0 -21
  224. package/src/components/activity-indicator/index.ts +0 -4
  225. package/src/components/activity-indicator/shared.ts +0 -225
  226. package/src/components/button.ts +0 -237
  227. package/src/components/image/index.android.ts +0 -95
  228. package/src/components/image/index.ios.ts +0 -95
  229. package/src/components/image/index.ts +0 -3
  230. package/src/components/image-background.ts +0 -308
  231. package/src/components/input-accessory-view/index.ts +0 -246
  232. package/src/components/pressable/index.ts +0 -522
  233. package/src/components/refresh-control/index.ts +0 -349
  234. package/src/components/safe-area-view/index.ts +0 -247
  235. package/src/components/scroll-view/index.android.ts +0 -205
  236. package/src/components/scroll-view/index.ios.ts +0 -81
  237. package/src/components/scroll-view/index.ts +0 -7
  238. package/src/components/scroll-view/projection.ts +0 -713
  239. package/src/components/scroll-view/shared.ts +0 -1093
  240. package/src/components/scroll-view/sticky-header.ts +0 -267
  241. package/src/components/switch/index.android.ts +0 -113
  242. package/src/components/switch/index.ios.ts +0 -112
  243. package/src/components/switch/index.ts +0 -4
  244. package/src/components/switch/shared.ts +0 -284
  245. package/src/components/text-input.ts +0 -669
  246. package/src/components/touchable/index.ts +0 -956
  247. package/src/components/touchable-native-feedback/index.ts +0 -327
  248. package/src/state-style.ts +0 -39
@@ -1,956 +0,0 @@
1
- // The Touchable* family for Angular, all built on Pressable (the Angular twin of the React/Vue
2
- // adapter). The press-timing constants and the deactivation-floor math are shared with every
3
- // adapter (@symbiote-native/components/state/touchable); here Angular owns only the Animated wiring + the
4
- // press scheduling state:
5
- // TouchableOpacity: animate an Animated.Value opacity toward activeOpacity on press-in and back
6
- // to 1 on press-out, driven from Pressable's onPressIn/onPressOut. The opacity Value is held by
7
- // IDENTITY as a plain field (an engine object, never an @Input / reactive wrap) and the
8
- // <symbiote-animated-view> leaf commits it every frame.
9
- // TouchableHighlight: paint underlayColor + lower child opacity while the underlay is SHOWN.
10
- // RN drives that from onPressIn/onPress/onPressOut and holds it past the tap for delayPressOut,
11
- // NOT from Pressable's pressed flag — a tap too fast to see is already un-pressed by then.
12
- // TouchableWithoutFeedback: no visual change, but RN still runs the full press-timing machine.
13
- //
14
- // Every Touchable's own press/hover/accessibility events are now real @Output() EventEmitters too,
15
- // matching Pressable (which they wrap) — `(press)="handler($event)"`, never `[onPress]="handler"`.
16
- // Non-event config (delayLongPress, hitSlop, ...) still rides down as plain @Input() bindings.
17
- // Accessibility EVENTS forward straight through to Pressable's own outputs
18
- // (`(accessibilityAction)="accessibilityAction.emit($event)"`); accessibility STATE folds the web
19
- // aria-*/role aliases and merges `disabled` into the a11y state on Pressable's own host — no re-fold
20
- // here (the a11y host is Pressable's view, mirroring React's `...rest` -> Pressable). No JS-side
21
- // platform branch, so this stays a flat single file, mirroring React/Vue. Each Touchable
22
- // forwards the Angular Pressable surface verbatim; parity is against that surface, exactly as
23
- // React's Touchable parity is against
24
- // the React Pressable surface.
25
-
26
- import {
27
- CUSTOM_ELEMENTS_SCHEMA,
28
- ChangeDetectionStrategy,
29
- Component,
30
- computed,
31
- type DoCheck,
32
- ElementRef,
33
- EventEmitter,
34
- inject,
35
- Input,
36
- type OnChanges,
37
- type OnDestroy,
38
- Output,
39
- signal,
40
- } from '@angular/core';
41
- import {
42
- createHighlightUnderlayHandlers,
43
- createHighlightUnderlayRuntime,
44
- createTouchableFeedbackHandlers,
45
- createTouchableFeedbackRuntime,
46
- hasTouchablePressHandler,
47
- resolveHighlightExtraStyles,
48
- restingOpacityFromStyle,
49
- DEFAULT_ACTIVE_OPACITY,
50
- OPACITY_ACTIVE_GRANT_DURATION_MS,
51
- OPACITY_INACTIVE_DURATION_MS,
52
- RESTING_OPACITY,
53
- TOUCHABLE_MIN_PRESS_DURATION_MS,
54
- type IAccessibilityProps,
55
- type IAccessibilityStateValue,
56
- type IAriaProps,
57
- type IHighlightUnderlayHandlers,
58
- type IPressableAndroidRippleConfig,
59
- type IPressTimingProps,
60
- type IRectOffset,
61
- type ITouchableFeedbackHandlers,
62
- } from '@symbiote-native/components';
63
- import {
64
- type ISymbioteEvent,
65
- type IStyleProp,
66
- type IViewStyle,
67
- } from '@symbiote-native/engine';
68
- import {
69
- anchorHostStyle,
70
- anchorStyleProp,
71
- SymbioteStyleInputDirective,
72
- } from '../../primitives';
73
- import { Pressable, type IAngularPressableInputs } from '../pressable';
74
- import { Animated, AnimatedView } from '../../modules/animated';
75
- import {
76
- gateWanted,
77
- injectGateDemandAbove,
78
- provideGateDemand,
79
- type IGateDemand,
80
- type IGatedAccessibilityEvent,
81
- } from '../../gate-demand';
82
-
83
- // The shared field base for all three: the Pressable INPUT surface (minus style, which each
84
- // Touchable routes differently, and minus the press/hover events, which each Touchable declares as
85
- // its OWN @Output()s below) + RN's press-timing config + the public style. Mirrors React/Vue's
86
- // ITouchableBaseProps. Declared per-adapter over the Angular Pressable surface, since children
87
- // ride <ng-content> here rather than a field, unlike React/Vue's element-returning props.
88
- type IAngularTouchableBaseProps = Omit<IAngularPressableInputs, 'style'> &
89
- IPressTimingProps & {
90
- style?: IStyleProp<IViewStyle>;
91
- };
92
-
93
- export type IAngularTouchableOpacityProps = IAngularTouchableBaseProps & {
94
- activeOpacity?: number;
95
- };
96
-
97
- export type IAngularTouchableHighlightProps = IAngularTouchableBaseProps & {
98
- activeOpacity?: number;
99
- underlayColor?: string;
100
- };
101
-
102
- export type IAngularTouchableWithoutFeedbackProps = IAngularTouchableBaseProps;
103
-
104
- // The real timers both shared machines schedule on — core/components carries no timer globals, so
105
- // scheduling is the adapter's half. Every canceller is retained so ngOnDestroy cancels what is
106
- // still in flight: a deferred press-out or a held underlay firing after teardown would emit from a
107
- // destroyed component and write a signal on a destroyed view.
108
- interface ITimerScheduler {
109
- schedule: (callback: () => void, ms: number) => () => void;
110
- cancelAll: () => void;
111
- }
112
-
113
- function createTimerScheduler(): ITimerScheduler {
114
- const pending = new Set<() => void>();
115
- return {
116
- schedule(callback: () => void, ms: number): () => void {
117
- const id = setTimeout(() => {
118
- pending.delete(cancel);
119
- callback();
120
- }, ms);
121
- const cancel = (): void => {
122
- clearTimeout(id);
123
- pending.delete(cancel);
124
- };
125
- pending.add(cancel);
126
- return cancel;
127
- },
128
- cancelAll(): void {
129
- for (const cancel of [...pending]) cancel();
130
- pending.clear();
131
- },
132
- };
133
- }
134
-
135
- @Component({
136
- selector: 'TouchableOpacity',
137
- standalone: true,
138
- viewProviders: [provideGateDemand(() => TouchableOpacity)],
139
- hostDirectives: [
140
- { directive: SymbioteStyleInputDirective, inputs: ['style'] },
141
- ],
142
- schemas: [CUSTOM_ELEMENTS_SCHEMA],
143
- imports: [Pressable, AnimatedView],
144
- changeDetection: ChangeDetectionStrategy.OnPush,
145
- template: `
146
- <Pressable
147
- [__minPressDuration]="0"
148
- (press)="press.emit($event)"
149
- (pressIn)="handlePressIn($event)"
150
- (pressOut)="handlePressOut($event)"
151
- (pressMove)="pressMove.emit($event)"
152
- (longPress)="longPress.emit($event)"
153
- (hoverIn)="hoverIn.emit($event)"
154
- (hoverOut)="hoverOut.emit($event)"
155
- [delayLongPress]="delayLongPress"
156
- [delayHoverIn]="delayHoverIn"
157
- [delayHoverOut]="delayHoverOut"
158
- [disabled]="disabled"
159
- [cancelable]="cancelable"
160
- [hitSlop]="hitSlop"
161
- [pressRetentionOffset]="pressRetentionOffset"
162
- [unstable_pressDelay]="unstable_pressDelay"
163
- [android_ripple]="android_ripple"
164
- [android_disableSound]="android_disableSound"
165
- [testID]="testID"
166
- [nativeID]="nativeID"
167
- [hasTVPreferredFocus]="hasTVPreferredFocus"
168
- [nextFocusDown]="nextFocusDown"
169
- [nextFocusForward]="nextFocusForward"
170
- [nextFocusLeft]="nextFocusLeft"
171
- [nextFocusRight]="nextFocusRight"
172
- [nextFocusUp]="nextFocusUp"
173
- [accessible]="accessible"
174
- [accessibilityLabel]="accessibilityLabel"
175
- [accessibilityHint]="accessibilityHint"
176
- [accessibilityRole]="accessibilityRole"
177
- [accessibilityState]="accessibilityState"
178
- [accessibilityValue]="accessibilityValue"
179
- [accessibilityActions]="accessibilityActions"
180
- [accessibilityLabelledBy]="accessibilityLabelledBy"
181
- [importantForAccessibility]="importantForAccessibility"
182
- [accessibilityLiveRegion]="accessibilityLiveRegion"
183
- [screenReaderFocusable]="screenReaderFocusable"
184
- [accessibilityViewIsModal]="accessibilityViewIsModal"
185
- [accessibilityElementsHidden]="accessibilityElementsHidden"
186
- [accessibilityIgnoresInvertColors]="accessibilityIgnoresInvertColors"
187
- [accessibilityLanguage]="accessibilityLanguage"
188
- [accessibilityRespondsToUserInteraction]="
189
- accessibilityRespondsToUserInteraction
190
- "
191
- [accessibilityShowsLargeContentViewer]="
192
- accessibilityShowsLargeContentViewer
193
- "
194
- [accessibilityLargeContentTitle]="accessibilityLargeContentTitle"
195
- (accessibilityAction)="accessibilityAction.emit($event)"
196
- (accessibilityTap)="accessibilityTap.emit($event)"
197
- (magicTap)="magicTap.emit($event)"
198
- (accessibilityEscape)="accessibilityEscape.emit($event)"
199
- [ariaLabel]="ariaLabel"
200
- [ariaBusy]="ariaBusy"
201
- [ariaChecked]="ariaChecked"
202
- [ariaDisabled]="ariaDisabled"
203
- [ariaExpanded]="ariaExpanded"
204
- [ariaHidden]="ariaHidden"
205
- [ariaLabelledBy]="ariaLabelledBy"
206
- [ariaLive]="ariaLive"
207
- [ariaSelected]="ariaSelected"
208
- [ariaModal]="ariaModal"
209
- [ariaValueMax]="ariaValueMax"
210
- [ariaValueMin]="ariaValueMin"
211
- [ariaValueNow]="ariaValueNow"
212
- [ariaValueText]="ariaValueText"
213
- [id]="id"
214
- [role]="role"
215
- >
216
- <symbiote-animated-view [style]="animatedStyle">
217
- <ng-content></ng-content>
218
- </symbiote-animated-view>
219
- </Pressable>
220
- `,
221
- })
222
- export class TouchableOpacity
223
- implements IAngularTouchableOpacityProps, OnChanges, OnDestroy, DoCheck
224
- {
225
- @Output() readonly press = new EventEmitter<ISymbioteEvent>();
226
- @Output() readonly pressIn = new EventEmitter<ISymbioteEvent>();
227
- @Output() readonly pressOut = new EventEmitter<ISymbioteEvent>();
228
- @Output() readonly pressMove = new EventEmitter<ISymbioteEvent>();
229
- @Output() readonly longPress = new EventEmitter<ISymbioteEvent>();
230
- @Output() readonly hoverIn = new EventEmitter<ISymbioteEvent>();
231
- @Output() readonly hoverOut = new EventEmitter<ISymbioteEvent>();
232
- @Output() readonly accessibilityAction = new EventEmitter<ISymbioteEvent>();
233
- @Output() readonly accessibilityTap = new EventEmitter<ISymbioteEvent>();
234
- @Output() readonly magicTap = new EventEmitter<ISymbioteEvent>();
235
- @Output() readonly accessibilityEscape = new EventEmitter<ISymbioteEvent>();
236
-
237
- // This wrapper binds the four gated accessibility events on the component it renders, which
238
- // Angular forces to be unconditional and which would light that component's gates on every
239
- // instance. It answers for them instead — see `gate-demand.ts`.
240
- private readonly gateDemandAbove = injectGateDemandAbove();
241
-
242
- wantsGate(name: IGatedAccessibilityEvent): boolean {
243
- return gateWanted(this.gateDemandAbove, name, this[name]);
244
- }
245
-
246
- @Input() activeOpacity?: number;
247
- @Input() delayPressIn?: number;
248
- @Input() delayPressOut?: number;
249
- @Input() minPressDuration?: number;
250
- @Input() style?: IStyleProp<IViewStyle>;
251
- @Input() delayLongPress?: number;
252
- @Input() delayHoverIn?: number;
253
- @Input() delayHoverOut?: number;
254
- @Input() disabled?: boolean;
255
- @Input() cancelable?: boolean;
256
- @Input() hitSlop?: IRectOffset;
257
- @Input() pressRetentionOffset?: IRectOffset;
258
- @Input() unstable_pressDelay?: number;
259
- @Input() android_ripple?: IPressableAndroidRippleConfig;
260
- @Input() android_disableSound?: boolean;
261
- @Input() testID?: string;
262
- @Input() nativeID?: string;
263
- @Input() hasTVPreferredFocus?: boolean;
264
- @Input() nextFocusDown?: number;
265
- @Input() nextFocusForward?: number;
266
- @Input() nextFocusLeft?: number;
267
- @Input() nextFocusRight?: number;
268
- @Input() nextFocusUp?: number;
269
- @Input() accessible?: boolean;
270
- @Input() accessibilityLabel?: string;
271
- @Input() accessibilityHint?: string;
272
- @Input() accessibilityRole?: IAccessibilityProps['accessibilityRole'];
273
- @Input() accessibilityState?: IAccessibilityStateValue;
274
- @Input() accessibilityValue?: IAccessibilityProps['accessibilityValue'];
275
- @Input() accessibilityActions?: IAccessibilityProps['accessibilityActions'];
276
- @Input() accessibilityLabelledBy?: string | string[];
277
- @Input()
278
- importantForAccessibility?: IAccessibilityProps['importantForAccessibility'];
279
- @Input()
280
- accessibilityLiveRegion?: IAccessibilityProps['accessibilityLiveRegion'];
281
- @Input() screenReaderFocusable?: boolean;
282
- @Input() accessibilityViewIsModal?: boolean;
283
- @Input() accessibilityElementsHidden?: boolean;
284
- @Input() accessibilityIgnoresInvertColors?: boolean;
285
- @Input() accessibilityLanguage?: string;
286
- @Input() accessibilityRespondsToUserInteraction?: boolean;
287
- @Input() accessibilityShowsLargeContentViewer?: boolean;
288
- @Input() accessibilityLargeContentTitle?: string;
289
- @Input() ariaLabel?: string;
290
- @Input() ariaBusy?: boolean;
291
- @Input() ariaChecked?: boolean | 'mixed';
292
- @Input() ariaDisabled?: boolean;
293
- @Input() ariaExpanded?: boolean;
294
- @Input() ariaHidden?: boolean;
295
- @Input() ariaLabelledBy?: string;
296
- @Input() ariaLive?: IAriaProps['aria-live'];
297
- @Input() ariaSelected?: boolean;
298
- @Input() ariaModal?: boolean;
299
- @Input() ariaValueMax?: number;
300
- @Input() ariaValueMin?: number;
301
- @Input() ariaValueNow?: number;
302
- @Input() ariaValueText?: string;
303
- @Input() id?: string;
304
- @Input() role?: IAriaProps['role'];
305
-
306
- // One Animated.Value per mount. Held by IDENTITY as a plain field (an engine object, never an
307
- // @Input / reactive wrap); the <symbiote-animated-view> leaf rasterizes it for the first paint
308
- // and drives it through setNativeProps every frame.
309
- //
310
- // RN seeds it from _getChildStyleOpacityWithDefault(props.style). Angular writes @Input()s AFTER
311
- // the field initializers run, so `style` is still undefined here — the real resting value is
312
- // seeded by the first ngOnChanges below, which still precedes the first template pass.
313
- private readonly opacity = new Animated.Value(RESTING_OPACITY);
314
- // The shared press-scheduling cell (delayPressIn timer + activation clock), persisted on the
315
- // instance; the machine's handlers are rebuilt per event over live @Input()s.
316
- private readonly runtime = createTouchableFeedbackRuntime();
317
- private readonly timers = createTimerScheduler();
318
- // What the last settle ran for. Undefined until the first ngOnChanges, which is the MOUNT.
319
- private settled:
320
- { disabled: boolean | undefined; resting: number } | undefined;
321
- // This component's OWN host — the non-painting anchor `class="..."` at the use site resolves
322
- // onto (see anchorHostStyle's doc comment) — NOT the <symbiote-animated-view> leaf one level down.
323
- private readonly elementRef = inject(ElementRef);
324
-
325
- // The (possibly animated) style the leaf reduces: the anchor's class-derived style first, then
326
- // the user's explicit style, then the live opacity layered on top (last wins via the style array
327
- // the commit layer flattens). `unknown` because the array mixes a plain style with an
328
- // AnimatedNode, exactly the bag AnimatedView's `style` input accepts.
329
- get animatedStyle(): unknown {
330
- return [this.anchorStyle(), this.style, { opacity: this.opacity }];
331
- }
332
-
333
- // The anchor's class-derived style is written by the renderer's addClass/removeClass at the USE
334
- // SITE - it never appears in SimpleChanges, and nothing about it dirties THIS component's view.
335
- // A `class=` present at creation therefore worked, while one toggled later did not: the parent
336
- // refreshed, the class landed on the anchor, and this component's own view was never refreshed,
337
- // so whatever merged the anchor style in was never re-evaluated. Polling it into a signal from
338
- // ngDoCheck fixes both halves: ngDoCheck runs during the PARENT's refresh even when this view is
339
- // skipped, and the signal write is what then marks this view for refresh. `signal.set`'s own
340
- // Object.is makes an unchanged poll a no-op, so there is no loop.
341
- private readonly anchorStyle = signal<unknown>(undefined);
342
-
343
- ngDoCheck(): void {
344
- this.anchorStyle.set(anchorHostStyle(this.elementRef));
345
- }
346
-
347
- // RN's componentDidUpdate: a changed `disabled` or a changed style opacity re-settles the view,
348
- // so a Touchable disabled mid-press does not stay stuck at its active opacity. The FIRST call is
349
- // the mount (ngOnChanges runs before ngOnInit and before the first template pass), so it seeds
350
- // the Value instead of animating — RN does this on update only, and a fade at mount would animate
351
- // away from the value the leaf is about to paint. A use site with no bindings gets no ngOnChanges
352
- // at all, and there the RESTING_OPACITY seed is already the right answer.
353
- ngOnChanges(): void {
354
- const settled = { disabled: this.disabled, resting: this.restingOpacity() };
355
- if (this.settled === undefined) {
356
- this.opacity.setValue(settled.resting);
357
- } else if (
358
- this.settled.disabled !== settled.disabled ||
359
- this.settled.resting !== settled.resting
360
- ) {
361
- this.setOpacityTo(settled.resting, OPACITY_INACTIVE_DURATION_MS);
362
- }
363
- this.settled = settled;
364
- }
365
-
366
- // RN's componentWillUnmount: stop the animation so a teardown mid-fade leaves no driver running
367
- // against a node that is gone, and drop any press-out still deferred behind a timer.
368
- ngOnDestroy(): void {
369
- this.timers.cancelAll();
370
- this.opacity.resetAnimation();
371
- }
372
-
373
- // RN's _getChildStyleOpacityWithDefault: the fade settles at the opacity the CALLER's style asks
374
- // for, not at a hard 1 — a Touchable styled `opacity: 0.6` must come back to 0.6.
375
- private restingOpacity(): number {
376
- return restingOpacityFromStyle(this.style);
377
- }
378
-
379
- private setOpacityTo(toValue: number, duration: number): void {
380
- Animated.timing(this.opacity, {
381
- toValue,
382
- duration,
383
- easing: Animated.Easing.inOut(Animated.Easing.quad),
384
- // RN's own (TouchableOpacity.js:242). opacity is natively drivable, so the fade survives a
385
- // busy JS thread — which is the entire point of press feedback.
386
- useNativeDriver: true,
387
- }).start();
388
- }
389
-
390
- // Built per event so the machine reads live @Input()s (delay/opacity); the runtime persists across
391
- // calls. The shared machine owns the scheduling — the adapter supplies only the native seam: the
392
- // Animated opacity fade + the @Output() emit, as activate/deactivate.
393
- private feedbackHandlers(): ITouchableFeedbackHandlers {
394
- return createTouchableFeedbackHandlers(
395
- {
396
- delayPressIn: this.delayPressIn ?? 0,
397
- delayPressOut: this.delayPressOut ?? 0,
398
- // RN's Touchables override Pressability's own 130ms floor with 0; what holds the active
399
- // visual there is the fade's own duration, not a press-duration floor.
400
- minPressDuration:
401
- this.minPressDuration ?? TOUCHABLE_MIN_PRESS_DURATION_MS,
402
- schedule: this.timers.schedule,
403
- now: Date.now,
404
- },
405
- this.runtime,
406
- {
407
- activate: (event: ISymbioteEvent): void => {
408
- this.setOpacityTo(
409
- this.activeOpacity ?? DEFAULT_ACTIVE_OPACITY,
410
- OPACITY_ACTIVE_GRANT_DURATION_MS,
411
- );
412
- this.pressIn.emit(event);
413
- },
414
- deactivate: (event: ISymbioteEvent): void => {
415
- this.setOpacityTo(
416
- this.restingOpacity(),
417
- OPACITY_INACTIVE_DURATION_MS,
418
- );
419
- this.pressOut.emit(event);
420
- },
421
- },
422
- );
423
- }
424
-
425
- // Arrow fields (stable identity for OnPush, `this` intact when Pressable's `(pressIn)` invokes
426
- // them). The delayPressIn defer + minPressDuration/delayPressOut hold live in the shared machine.
427
- handlePressIn = (event: ISymbioteEvent): void => {
428
- this.feedbackHandlers().handlePressIn(event);
429
- };
430
-
431
- handlePressOut = (event: ISymbioteEvent): void => {
432
- this.feedbackHandlers().handlePressOut(event);
433
- };
434
- }
435
-
436
- @Component({
437
- selector: 'TouchableHighlight',
438
- standalone: true,
439
- viewProviders: [provideGateDemand(() => TouchableHighlight)],
440
- hostDirectives: [
441
- { directive: SymbioteStyleInputDirective, inputs: ['style'] },
442
- ],
443
- imports: [Pressable],
444
- changeDetection: ChangeDetectionStrategy.OnPush,
445
- template: `
446
- <Pressable
447
- [__minPressDuration]="0"
448
- [style]="containerStyle()"
449
- (press)="handlePress($event)"
450
- (pressIn)="handlePressIn($event)"
451
- (pressOut)="handlePressOut($event)"
452
- (pressMove)="pressMove.emit($event)"
453
- (longPress)="longPress.emit($event)"
454
- (hoverIn)="hoverIn.emit($event)"
455
- (hoverOut)="hoverOut.emit($event)"
456
- [delayLongPress]="delayLongPress"
457
- [delayHoverIn]="delayHoverIn"
458
- [delayHoverOut]="delayHoverOut"
459
- [disabled]="disabled"
460
- [cancelable]="cancelable"
461
- [hitSlop]="hitSlop"
462
- [pressRetentionOffset]="pressRetentionOffset"
463
- [unstable_pressDelay]="unstable_pressDelay"
464
- [android_ripple]="android_ripple"
465
- [android_disableSound]="android_disableSound"
466
- [testID]="testID"
467
- [nativeID]="nativeID"
468
- [hasTVPreferredFocus]="hasTVPreferredFocus"
469
- [nextFocusDown]="nextFocusDown"
470
- [nextFocusForward]="nextFocusForward"
471
- [nextFocusLeft]="nextFocusLeft"
472
- [nextFocusRight]="nextFocusRight"
473
- [nextFocusUp]="nextFocusUp"
474
- [accessible]="accessible"
475
- [accessibilityLabel]="accessibilityLabel"
476
- [accessibilityHint]="accessibilityHint"
477
- [accessibilityRole]="accessibilityRole"
478
- [accessibilityState]="accessibilityState"
479
- [accessibilityValue]="accessibilityValue"
480
- [accessibilityActions]="accessibilityActions"
481
- [accessibilityLabelledBy]="accessibilityLabelledBy"
482
- [importantForAccessibility]="importantForAccessibility"
483
- [accessibilityLiveRegion]="accessibilityLiveRegion"
484
- [screenReaderFocusable]="screenReaderFocusable"
485
- [accessibilityViewIsModal]="accessibilityViewIsModal"
486
- [accessibilityElementsHidden]="accessibilityElementsHidden"
487
- [accessibilityIgnoresInvertColors]="accessibilityIgnoresInvertColors"
488
- [accessibilityLanguage]="accessibilityLanguage"
489
- [accessibilityRespondsToUserInteraction]="
490
- accessibilityRespondsToUserInteraction
491
- "
492
- [accessibilityShowsLargeContentViewer]="
493
- accessibilityShowsLargeContentViewer
494
- "
495
- [accessibilityLargeContentTitle]="accessibilityLargeContentTitle"
496
- (accessibilityAction)="accessibilityAction.emit($event)"
497
- (accessibilityTap)="accessibilityTap.emit($event)"
498
- (magicTap)="magicTap.emit($event)"
499
- (accessibilityEscape)="accessibilityEscape.emit($event)"
500
- [ariaLabel]="ariaLabel"
501
- [ariaBusy]="ariaBusy"
502
- [ariaChecked]="ariaChecked"
503
- [ariaDisabled]="ariaDisabled"
504
- [ariaExpanded]="ariaExpanded"
505
- [ariaHidden]="ariaHidden"
506
- [ariaLabelledBy]="ariaLabelledBy"
507
- [ariaLive]="ariaLive"
508
- [ariaSelected]="ariaSelected"
509
- [ariaModal]="ariaModal"
510
- [ariaValueMax]="ariaValueMax"
511
- [ariaValueMin]="ariaValueMin"
512
- [ariaValueNow]="ariaValueNow"
513
- [ariaValueText]="ariaValueText"
514
- [id]="id"
515
- [role]="role"
516
- >
517
- <ng-content></ng-content>
518
- </Pressable>
519
- `,
520
- })
521
- export class TouchableHighlight
522
- implements IAngularTouchableHighlightProps, OnChanges, OnDestroy, DoCheck
523
- {
524
- @Output() readonly press = new EventEmitter<ISymbioteEvent>();
525
- @Output() readonly pressIn = new EventEmitter<ISymbioteEvent>();
526
- @Output() readonly pressOut = new EventEmitter<ISymbioteEvent>();
527
- @Output() readonly pressMove = new EventEmitter<ISymbioteEvent>();
528
- @Output() readonly longPress = new EventEmitter<ISymbioteEvent>();
529
- @Output() readonly hoverIn = new EventEmitter<ISymbioteEvent>();
530
- @Output() readonly hoverOut = new EventEmitter<ISymbioteEvent>();
531
- @Output() readonly accessibilityAction = new EventEmitter<ISymbioteEvent>();
532
- @Output() readonly accessibilityTap = new EventEmitter<ISymbioteEvent>();
533
- @Output() readonly magicTap = new EventEmitter<ISymbioteEvent>();
534
- @Output() readonly accessibilityEscape = new EventEmitter<ISymbioteEvent>();
535
-
536
- // This wrapper binds the four gated accessibility events on the component it renders, which
537
- // Angular forces to be unconditional and which would light that component's gates on every
538
- // instance. It answers for them instead — see `gate-demand.ts`.
539
- private readonly gateDemandAbove = injectGateDemandAbove();
540
-
541
- wantsGate(name: IGatedAccessibilityEvent): boolean {
542
- return gateWanted(this.gateDemandAbove, name, this[name]);
543
- }
544
-
545
- // RN's onShowUnderlay / onHideUnderlay — fired on a real transition only, never on a repeat.
546
- @Output() readonly showUnderlay = new EventEmitter<void>();
547
- @Output() readonly hideUnderlay = new EventEmitter<void>();
548
- @Input() activeOpacity?: number;
549
- @Input() underlayColor?: string;
550
- @Input() delayPressIn?: number;
551
- @Input() delayPressOut?: number;
552
- @Input() minPressDuration?: number;
553
- @Input() style?: IStyleProp<IViewStyle>;
554
- @Input() delayLongPress?: number;
555
- @Input() delayHoverIn?: number;
556
- @Input() delayHoverOut?: number;
557
- @Input() disabled?: boolean;
558
- @Input() cancelable?: boolean;
559
- @Input() hitSlop?: IRectOffset;
560
- @Input() pressRetentionOffset?: IRectOffset;
561
- @Input() unstable_pressDelay?: number;
562
- @Input() android_ripple?: IPressableAndroidRippleConfig;
563
- @Input() android_disableSound?: boolean;
564
- @Input() testID?: string;
565
- @Input() nativeID?: string;
566
- @Input() hasTVPreferredFocus?: boolean;
567
- @Input() nextFocusDown?: number;
568
- @Input() nextFocusForward?: number;
569
- @Input() nextFocusLeft?: number;
570
- @Input() nextFocusRight?: number;
571
- @Input() nextFocusUp?: number;
572
- @Input() accessible?: boolean;
573
- @Input() accessibilityLabel?: string;
574
- @Input() accessibilityHint?: string;
575
- @Input() accessibilityRole?: IAccessibilityProps['accessibilityRole'];
576
- @Input() accessibilityState?: IAccessibilityStateValue;
577
- @Input() accessibilityValue?: IAccessibilityProps['accessibilityValue'];
578
- @Input() accessibilityActions?: IAccessibilityProps['accessibilityActions'];
579
- @Input() accessibilityLabelledBy?: string | string[];
580
- @Input()
581
- importantForAccessibility?: IAccessibilityProps['importantForAccessibility'];
582
- @Input()
583
- accessibilityLiveRegion?: IAccessibilityProps['accessibilityLiveRegion'];
584
- @Input() screenReaderFocusable?: boolean;
585
- @Input() accessibilityViewIsModal?: boolean;
586
- @Input() accessibilityElementsHidden?: boolean;
587
- @Input() accessibilityIgnoresInvertColors?: boolean;
588
- @Input() accessibilityLanguage?: string;
589
- @Input() accessibilityRespondsToUserInteraction?: boolean;
590
- @Input() accessibilityShowsLargeContentViewer?: boolean;
591
- @Input() accessibilityLargeContentTitle?: string;
592
- @Input() ariaLabel?: string;
593
- @Input() ariaBusy?: boolean;
594
- @Input() ariaChecked?: boolean | 'mixed';
595
- @Input() ariaDisabled?: boolean;
596
- @Input() ariaExpanded?: boolean;
597
- @Input() ariaHidden?: boolean;
598
- @Input() ariaLabelledBy?: string;
599
- @Input() ariaLive?: IAriaProps['aria-live'];
600
- @Input() ariaSelected?: boolean;
601
- @Input() ariaModal?: boolean;
602
- @Input() ariaValueMax?: number;
603
- @Input() ariaValueMin?: number;
604
- @Input() ariaValueNow?: number;
605
- @Input() ariaValueText?: string;
606
- @Input() id?: string;
607
- @Input() role?: IAriaProps['role'];
608
-
609
- // This component's OWN host — the non-painting anchor `class="..."` at the use site resolves
610
- // onto (see anchorHostStyle's doc comment) — NOT the Pressable one level down.
611
- private readonly elementRef = inject(ElementRef);
612
-
613
- // `shown` is NOT Pressable's `pressed`. RN holds the underlay for delayPressOut past the tap so a
614
- // press too fast to see still flashes, and the pressed flag is already false by then. A SIGNAL,
615
- // because the hide fires from a timer — outside any Angular listener, so under zoneless CD
616
- // nothing else would mark this view. A signal write both notifies the change-detection scheduler
617
- // (markAncestorsForTraversal) and dirties this template's own reactive consumer, which is the one
618
- // thing honored in Targeted mode; markForCheck from a plain field would not be reachable here.
619
- private readonly shown = signal(false);
620
- private readonly underlayRuntime = createHighlightUnderlayRuntime();
621
- private readonly timers = createTimerScheduler();
622
-
623
- // The container style Pressable paints.
624
- //
625
- // POINT-7 DECISION — both halves stay on the CONTAINER, deliberately. RN's _createExtraStyles
626
- // keeps them apart: `underlay` (the backgroundColor) on the container, `child` (the lowered
627
- // opacity) cloned onto the single child. Angular has no cloneElement and no way to reach a
628
- // projected <ng-content> node, so the only path to the child is a permanent wrapper View — which
629
- // inserts a node into the flex chain and silently re-parents any `flex` a child declares. The
630
- // fake Fabric runs no Yoga, so no headless test can measure that damage, only assert the extra
631
- // node exists; shipping an unmeasurable layout change is worse than the visual approximation.
632
- // Same call, same reason, as the Solid adapter. resolveHighlightExtraStyles keeps the two styles
633
- // SEPARATE regardless, so a layout-safe fix (which needs a device check, not a test) stays open.
634
- //
635
- // Returns a NEW ARRAY whenever a dependency moves: Pressable's `style` @Input then reports an
636
- // ordinary change. A stable reference was the earlier bug — Pressable's view never refreshed, so
637
- // a class or [style] toggled after mount never reached the committed view.
638
- readonly containerStyle = computed<IStyleProp<IViewStyle>>(() => {
639
- this.inputsRevision();
640
- const base: IStyleProp<IViewStyle> = [this.anchorStyle(), this.style];
641
- const extra = resolveHighlightExtraStyles({
642
- shown: this.shown(),
643
- hasPressHandler: this.hasPressHandler(),
644
- underlayColor: this.underlayColor,
645
- activeOpacity: this.activeOpacity,
646
- });
647
- return extra === undefined ? base : [base, extra.underlay, extra.child];
648
- });
649
-
650
- // RN's _hasPressHandler: a Touchable that handles no press paints no underlay. Angular exposes
651
- // the press lifecycle as @Output()s, so "a handler is present" is "something is subscribed" — the
652
- // same `.observed` test Pressable uses to decide whether to arm its long-press timer at all.
653
- private hasPressHandler(): boolean {
654
- return hasTouchablePressHandler({
655
- onPress: this.press.observed ? this.press : undefined,
656
- onPressIn: this.pressIn.observed ? this.pressIn : undefined,
657
- onPressOut: this.pressOut.observed ? this.pressOut : undefined,
658
- onLongPress: this.longPress.observed ? this.longPress : undefined,
659
- });
660
- }
661
-
662
- // Built per event so the machine reads live @Input()s (delayPressOut, the has-handler gate); the
663
- // runtime persists across calls, which is what lets handlePressOut see the timer handlePress set.
664
- private underlayHandlers(): IHighlightUnderlayHandlers {
665
- return createHighlightUnderlayHandlers(
666
- {
667
- delayPressOut: this.delayPressOut ?? 0,
668
- hasPressHandler: this.hasPressHandler(),
669
- schedule: this.timers.schedule,
670
- },
671
- this.underlayRuntime,
672
- {
673
- setShown: (shown: boolean): void => this.shown.set(shown),
674
- onShowUnderlay: (): void => this.showUnderlay.emit(),
675
- onHideUnderlay: (): void => this.hideUnderlay.emit(),
676
- },
677
- );
678
- }
679
-
680
- // Visual first, then the caller's own event — RN's order in _createPressabilityConfig.
681
- // onLongPress is deliberately NOT intercepted (Pressable emits it straight through); it is only
682
- // READ, by the has-press-handler gate above.
683
- handlePressIn(event: ISymbioteEvent): void {
684
- this.underlayHandlers().handlePressIn(event);
685
- this.pressIn.emit(event);
686
- }
687
-
688
- handlePress(event: ISymbioteEvent): void {
689
- this.underlayHandlers().handlePress(event);
690
- this.press.emit(event);
691
- }
692
-
693
- handlePressOut(event: ISymbioteEvent): void {
694
- this.underlayHandlers().handlePressOut(event);
695
- this.pressOut.emit(event);
696
- }
697
-
698
- ngOnDestroy(): void {
699
- this.timers.cancelAll();
700
- }
701
-
702
- // `style`, `underlayColor` and `activeOpacity` are read untracked inside containerStyle, so the
703
- // bump is what tells it they moved. See the ngDoCheck note below for the anchor half, which
704
- // ngOnChanges cannot cover.
705
- private readonly inputsRevision = signal(0);
706
- private readonly anchorStyle = signal<IStyleProp<IViewStyle> | undefined>(
707
- undefined,
708
- );
709
-
710
- ngOnChanges(): void {
711
- this.inputsRevision.update(revision => revision + 1);
712
- }
713
-
714
- // The anchor's class-derived style is written by the renderer's addClass/removeClass at the USE
715
- // SITE - it never appears in SimpleChanges, and nothing about it dirties THIS component's view.
716
- // ngDoCheck runs during the PARENT's refresh even when this view is skipped, and the signal
717
- // write is what then marks this view for refresh. `signal.set`'s own Object.is makes an
718
- // unchanged poll a no-op, so there is no loop.
719
- ngDoCheck(): void {
720
- this.anchorStyle.set(anchorStyleProp<IViewStyle>(this.elementRef));
721
- }
722
- }
723
-
724
- @Component({
725
- selector: 'TouchableWithoutFeedback',
726
- standalone: true,
727
- viewProviders: [provideGateDemand(() => TouchableWithoutFeedback)],
728
- hostDirectives: [
729
- { directive: SymbioteStyleInputDirective, inputs: ['style'] },
730
- ],
731
- imports: [Pressable],
732
- changeDetection: ChangeDetectionStrategy.OnPush,
733
- template: `
734
- <Pressable
735
- [__minPressDuration]="0"
736
- [style]="mergedStyle"
737
- (press)="press.emit($event)"
738
- (pressIn)="handlePressIn($event)"
739
- (pressOut)="handlePressOut($event)"
740
- (pressMove)="pressMove.emit($event)"
741
- (longPress)="longPress.emit($event)"
742
- (hoverIn)="hoverIn.emit($event)"
743
- (hoverOut)="hoverOut.emit($event)"
744
- [delayLongPress]="delayLongPress"
745
- [delayHoverIn]="delayHoverIn"
746
- [delayHoverOut]="delayHoverOut"
747
- [disabled]="disabled"
748
- [cancelable]="cancelable"
749
- [hitSlop]="hitSlop"
750
- [pressRetentionOffset]="pressRetentionOffset"
751
- [unstable_pressDelay]="unstable_pressDelay"
752
- [android_ripple]="android_ripple"
753
- [android_disableSound]="android_disableSound"
754
- [testID]="testID"
755
- [nativeID]="nativeID"
756
- [hasTVPreferredFocus]="hasTVPreferredFocus"
757
- [nextFocusDown]="nextFocusDown"
758
- [nextFocusForward]="nextFocusForward"
759
- [nextFocusLeft]="nextFocusLeft"
760
- [nextFocusRight]="nextFocusRight"
761
- [nextFocusUp]="nextFocusUp"
762
- [accessible]="accessible"
763
- [accessibilityLabel]="accessibilityLabel"
764
- [accessibilityHint]="accessibilityHint"
765
- [accessibilityRole]="accessibilityRole"
766
- [accessibilityState]="accessibilityState"
767
- [accessibilityValue]="accessibilityValue"
768
- [accessibilityActions]="accessibilityActions"
769
- [accessibilityLabelledBy]="accessibilityLabelledBy"
770
- [importantForAccessibility]="importantForAccessibility"
771
- [accessibilityLiveRegion]="accessibilityLiveRegion"
772
- [screenReaderFocusable]="screenReaderFocusable"
773
- [accessibilityViewIsModal]="accessibilityViewIsModal"
774
- [accessibilityElementsHidden]="accessibilityElementsHidden"
775
- [accessibilityIgnoresInvertColors]="accessibilityIgnoresInvertColors"
776
- [accessibilityLanguage]="accessibilityLanguage"
777
- [accessibilityRespondsToUserInteraction]="
778
- accessibilityRespondsToUserInteraction
779
- "
780
- [accessibilityShowsLargeContentViewer]="
781
- accessibilityShowsLargeContentViewer
782
- "
783
- [accessibilityLargeContentTitle]="accessibilityLargeContentTitle"
784
- (accessibilityAction)="accessibilityAction.emit($event)"
785
- (accessibilityTap)="accessibilityTap.emit($event)"
786
- (magicTap)="magicTap.emit($event)"
787
- (accessibilityEscape)="accessibilityEscape.emit($event)"
788
- [ariaLabel]="ariaLabel"
789
- [ariaBusy]="ariaBusy"
790
- [ariaChecked]="ariaChecked"
791
- [ariaDisabled]="ariaDisabled"
792
- [ariaExpanded]="ariaExpanded"
793
- [ariaHidden]="ariaHidden"
794
- [ariaLabelledBy]="ariaLabelledBy"
795
- [ariaLive]="ariaLive"
796
- [ariaSelected]="ariaSelected"
797
- [ariaModal]="ariaModal"
798
- [ariaValueMax]="ariaValueMax"
799
- [ariaValueMin]="ariaValueMin"
800
- [ariaValueNow]="ariaValueNow"
801
- [ariaValueText]="ariaValueText"
802
- [id]="id"
803
- [role]="role"
804
- >
805
- <ng-content></ng-content>
806
- </Pressable>
807
- `,
808
- })
809
- export class TouchableWithoutFeedback
810
- implements DoCheck, OnDestroy, IAngularTouchableWithoutFeedbackProps
811
- {
812
- // RN's TouchableWithoutFeedback builds a FULL Pressability config with delayPressIn /
813
- // delayPressOut / minPressDuration: 0 — "without feedback" means no VISUAL, not no timing. So the
814
- // same shared machine TouchableOpacity uses runs here, with the visual half left empty: the
815
- // pressIn/pressOut EMIT is what gets deferred and floored.
816
- @Output() readonly press = new EventEmitter<ISymbioteEvent>();
817
- @Output() readonly pressIn = new EventEmitter<ISymbioteEvent>();
818
- @Output() readonly pressOut = new EventEmitter<ISymbioteEvent>();
819
- @Output() readonly pressMove = new EventEmitter<ISymbioteEvent>();
820
- @Output() readonly longPress = new EventEmitter<ISymbioteEvent>();
821
- @Output() readonly hoverIn = new EventEmitter<ISymbioteEvent>();
822
- @Output() readonly hoverOut = new EventEmitter<ISymbioteEvent>();
823
- @Output() readonly accessibilityAction = new EventEmitter<ISymbioteEvent>();
824
- @Output() readonly accessibilityTap = new EventEmitter<ISymbioteEvent>();
825
- @Output() readonly magicTap = new EventEmitter<ISymbioteEvent>();
826
- @Output() readonly accessibilityEscape = new EventEmitter<ISymbioteEvent>();
827
-
828
- // This wrapper binds the four gated accessibility events on the component it renders, which
829
- // Angular forces to be unconditional and which would light that component's gates on every
830
- // instance. It answers for them instead — see `gate-demand.ts`.
831
- private readonly gateDemandAbove = injectGateDemandAbove();
832
-
833
- wantsGate(name: IGatedAccessibilityEvent): boolean {
834
- return gateWanted(this.gateDemandAbove, name, this[name]);
835
- }
836
-
837
- @Input() delayPressIn?: number;
838
- @Input() delayPressOut?: number;
839
- @Input() minPressDuration?: number;
840
- @Input() style?: IStyleProp<IViewStyle>;
841
- @Input() delayLongPress?: number;
842
- @Input() delayHoverIn?: number;
843
- @Input() delayHoverOut?: number;
844
- @Input() disabled?: boolean;
845
- @Input() cancelable?: boolean;
846
- @Input() hitSlop?: IRectOffset;
847
- @Input() pressRetentionOffset?: IRectOffset;
848
- @Input() unstable_pressDelay?: number;
849
- @Input() android_ripple?: IPressableAndroidRippleConfig;
850
- @Input() android_disableSound?: boolean;
851
- @Input() testID?: string;
852
- @Input() nativeID?: string;
853
- @Input() hasTVPreferredFocus?: boolean;
854
- @Input() nextFocusDown?: number;
855
- @Input() nextFocusForward?: number;
856
- @Input() nextFocusLeft?: number;
857
- @Input() nextFocusRight?: number;
858
- @Input() nextFocusUp?: number;
859
- @Input() accessible?: boolean;
860
- @Input() accessibilityLabel?: string;
861
- @Input() accessibilityHint?: string;
862
- @Input() accessibilityRole?: IAccessibilityProps['accessibilityRole'];
863
- @Input() accessibilityState?: IAccessibilityStateValue;
864
- @Input() accessibilityValue?: IAccessibilityProps['accessibilityValue'];
865
- @Input() accessibilityActions?: IAccessibilityProps['accessibilityActions'];
866
- @Input() accessibilityLabelledBy?: string | string[];
867
- @Input()
868
- importantForAccessibility?: IAccessibilityProps['importantForAccessibility'];
869
- @Input()
870
- accessibilityLiveRegion?: IAccessibilityProps['accessibilityLiveRegion'];
871
- @Input() screenReaderFocusable?: boolean;
872
- @Input() accessibilityViewIsModal?: boolean;
873
- @Input() accessibilityElementsHidden?: boolean;
874
- @Input() accessibilityIgnoresInvertColors?: boolean;
875
- @Input() accessibilityLanguage?: string;
876
- @Input() accessibilityRespondsToUserInteraction?: boolean;
877
- @Input() accessibilityShowsLargeContentViewer?: boolean;
878
- @Input() accessibilityLargeContentTitle?: string;
879
- @Input() ariaLabel?: string;
880
- @Input() ariaBusy?: boolean;
881
- @Input() ariaChecked?: boolean | 'mixed';
882
- @Input() ariaDisabled?: boolean;
883
- @Input() ariaExpanded?: boolean;
884
- @Input() ariaHidden?: boolean;
885
- @Input() ariaLabelledBy?: string;
886
- @Input() ariaLive?: IAriaProps['aria-live'];
887
- @Input() ariaSelected?: boolean;
888
- @Input() ariaModal?: boolean;
889
- @Input() ariaValueMax?: number;
890
- @Input() ariaValueMin?: number;
891
- @Input() ariaValueNow?: number;
892
- @Input() ariaValueText?: string;
893
- @Input() id?: string;
894
- @Input() role?: IAriaProps['role'];
895
-
896
- // This component's OWN host — the non-painting anchor `class="..."` at the use site resolves
897
- // onto (see anchorHostStyle's doc comment) — NOT the Pressable one level down.
898
- private readonly elementRef = inject(ElementRef);
899
-
900
- // The shared press-scheduling cell (delayPressIn timer + activation clock), persisted on the
901
- // instance; the machine's handlers are rebuilt per event over live @Input()s.
902
- private readonly runtime = createTouchableFeedbackRuntime();
903
- private readonly timers = createTimerScheduler();
904
-
905
- // The anchor's class-derived style goes first, then the explicit style, so an explicit [style]
906
- // still beats the ambient class.
907
- get mergedStyle(): IStyleProp<IViewStyle> {
908
- return [this.anchorStyle(), this.style];
909
- }
910
-
911
- private feedbackHandlers(): ITouchableFeedbackHandlers {
912
- return createTouchableFeedbackHandlers(
913
- {
914
- delayPressIn: this.delayPressIn ?? 0,
915
- delayPressOut: this.delayPressOut ?? 0,
916
- minPressDuration:
917
- this.minPressDuration ?? TOUCHABLE_MIN_PRESS_DURATION_MS,
918
- schedule: this.timers.schedule,
919
- now: Date.now,
920
- },
921
- this.runtime,
922
- {
923
- activate: (event: ISymbioteEvent): void => this.pressIn.emit(event),
924
- deactivate: (event: ISymbioteEvent): void => this.pressOut.emit(event),
925
- },
926
- );
927
- }
928
-
929
- handlePressIn(event: ISymbioteEvent): void {
930
- this.feedbackHandlers().handlePressIn(event);
931
- }
932
-
933
- handlePressOut(event: ISymbioteEvent): void {
934
- this.feedbackHandlers().handlePressOut(event);
935
- }
936
-
937
- ngOnDestroy(): void {
938
- this.timers.cancelAll();
939
- }
940
-
941
- // The anchor's class-derived style is written by the renderer's addClass/removeClass at the USE
942
- // SITE - it never appears in SimpleChanges, and nothing about it dirties THIS component's view.
943
- // A `class=` present at creation therefore worked, while one toggled later did not: the parent
944
- // refreshed, the class landed on the anchor, and this component's own view was never refreshed,
945
- // so whatever merged the anchor style in was never re-evaluated. Polling it into a signal from
946
- // ngDoCheck fixes both halves: ngDoCheck runs during the PARENT's refresh even when this view is
947
- // skipped, and the signal write is what then marks this view for refresh. `signal.set`'s own
948
- // Object.is makes an unchanged poll a no-op, so there is no loop.
949
- private readonly anchorStyle = signal<IStyleProp<IViewStyle> | undefined>(
950
- undefined,
951
- );
952
-
953
- ngDoCheck(): void {
954
- this.anchorStyle.set(anchorStyleProp<IViewStyle>(this.elementRef));
955
- }
956
- }