@symbiote-native/angular 0.7.0 → 0.8.0

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 (158) hide show
  1. package/README.md +3 -3
  2. package/build/angular/bootstrap.js +1 -1
  3. package/build/angular/bootstrap.js.map +1 -1
  4. package/build/angular/components/activity-indicator/index.android.js +7 -2
  5. package/build/angular/components/activity-indicator/index.android.js.map +1 -1
  6. package/build/angular/components/activity-indicator/index.ios.js +3 -1
  7. package/build/angular/components/activity-indicator/index.ios.js.map +1 -1
  8. package/build/angular/components/activity-indicator/shared.d.ts +1 -1
  9. package/build/angular/components/activity-indicator/shared.js +1 -1
  10. package/build/angular/components/activity-indicator/shared.js.map +1 -1
  11. package/build/angular/components/button.js +13 -5
  12. package/build/angular/components/button.js.map +1 -1
  13. package/build/angular/components/flat-list/index.d.ts +1 -1
  14. package/build/angular/components/flat-list/index.js +47 -14
  15. package/build/angular/components/flat-list/index.js.map +1 -1
  16. package/build/angular/components/image/index.android.js +4 -2
  17. package/build/angular/components/image/index.android.js.map +1 -1
  18. package/build/angular/components/image/index.ios.js +4 -2
  19. package/build/angular/components/image/index.ios.js.map +1 -1
  20. package/build/angular/components/image/shared.js +18 -6
  21. package/build/angular/components/image/shared.js.map +1 -1
  22. package/build/angular/components/image-background.js +22 -8
  23. package/build/angular/components/image-background.js.map +1 -1
  24. package/build/angular/components/input-accessory-view/index.js +7 -2
  25. package/build/angular/components/input-accessory-view/index.js.map +1 -1
  26. package/build/angular/components/keyboard-avoiding-view/index.d.ts +1 -0
  27. package/build/angular/components/keyboard-avoiding-view/index.js +39 -9
  28. package/build/angular/components/keyboard-avoiding-view/index.js.map +1 -1
  29. package/build/angular/components/modal/index.js +7 -2
  30. package/build/angular/components/modal/index.js.map +1 -1
  31. package/build/angular/components/pressable/index.d.ts +1 -1
  32. package/build/angular/components/pressable/index.js +6 -2
  33. package/build/angular/components/pressable/index.js.map +1 -1
  34. package/build/angular/components/refresh-control/index.js +3 -1
  35. package/build/angular/components/refresh-control/index.js.map +1 -1
  36. package/build/angular/components/safe-area-view/index.js +3 -1
  37. package/build/angular/components/safe-area-view/index.js.map +1 -1
  38. package/build/angular/components/scroll-view/index.android.js +44 -14
  39. package/build/angular/components/scroll-view/index.android.js.map +1 -1
  40. package/build/angular/components/scroll-view/index.ios.js +13 -5
  41. package/build/angular/components/scroll-view/index.ios.js.map +1 -1
  42. package/build/angular/components/scroll-view/projection.d.ts +32 -0
  43. package/build/angular/components/scroll-view/projection.js +212 -47
  44. package/build/angular/components/scroll-view/projection.js.map +1 -1
  45. package/build/angular/components/scroll-view/shared.js +28 -7
  46. package/build/angular/components/scroll-view/shared.js.map +1 -1
  47. package/build/angular/components/scroll-view/sticky-header.js +7 -3
  48. package/build/angular/components/scroll-view/sticky-header.js.map +1 -1
  49. package/build/angular/components/section-list/index.d.ts +6 -1
  50. package/build/angular/components/section-list/index.js +63 -18
  51. package/build/angular/components/section-list/index.js.map +1 -1
  52. package/build/angular/components/switch/index.android.js +17 -3
  53. package/build/angular/components/switch/index.android.js.map +1 -1
  54. package/build/angular/components/switch/index.ios.js +17 -3
  55. package/build/angular/components/switch/index.ios.js.map +1 -1
  56. package/build/angular/components/switch/shared.d.ts +1 -1
  57. package/build/angular/components/switch/shared.js +5 -2
  58. package/build/angular/components/switch/shared.js.map +1 -1
  59. package/build/angular/components/text-input.d.ts +1 -1
  60. package/build/angular/components/text-input.js +25 -5
  61. package/build/angular/components/text-input.js.map +1 -1
  62. package/build/angular/components/touchable/index.d.ts +29 -7
  63. package/build/angular/components/touchable/index.js +246 -69
  64. package/build/angular/components/touchable/index.js.map +1 -1
  65. package/build/angular/components/touchable-native-feedback/index.js +2 -2
  66. package/build/angular/components/touchable-native-feedback/index.js.map +1 -1
  67. package/build/angular/components/virtualized-list/directives.js +4 -0
  68. package/build/angular/components/virtualized-list/directives.js.map +1 -1
  69. package/build/angular/components/virtualized-list/index.d.ts +5 -2
  70. package/build/angular/components/virtualized-list/index.js +197 -60
  71. package/build/angular/components/virtualized-list/index.js.map +1 -1
  72. package/build/angular/components/virtualized-section-list/directives.js.map +1 -1
  73. package/build/angular/components/virtualized-section-list/index.d.ts +18 -1
  74. package/build/angular/components/virtualized-section-list/index.js +90 -21
  75. package/build/angular/components/virtualized-section-list/index.js.map +1 -1
  76. package/build/angular/components.d.ts +1 -1
  77. package/build/angular/components.js.map +1 -1
  78. package/build/angular/create-portal/index.js +5 -1
  79. package/build/angular/create-portal/index.js.map +1 -1
  80. package/build/angular/create-tunnel/index.js.map +1 -1
  81. package/build/angular/descriptor-to-angular/index.js +3 -1
  82. package/build/angular/descriptor-to-angular/index.js.map +1 -1
  83. package/build/angular/diagnostics.d.ts +33 -0
  84. package/build/angular/diagnostics.js +82 -0
  85. package/build/angular/diagnostics.js.map +1 -0
  86. package/build/angular/host-instance/index.js +1 -1
  87. package/build/angular/host-instance/index.js.map +1 -1
  88. package/build/angular/index.d.ts +5 -2
  89. package/build/angular/index.js +15 -1
  90. package/build/angular/index.js.map +1 -1
  91. package/build/angular/modules/animated/create-animated-component.js +27 -9
  92. package/build/angular/modules/animated/create-animated-component.js.map +1 -1
  93. package/build/angular/modules/app-registry/index.js.map +1 -1
  94. package/build/angular/modules/status-bar/index.js +1 -1
  95. package/build/angular/modules/status-bar/index.js.map +1 -1
  96. package/build/angular/primitives/index.d.ts +19 -2
  97. package/build/angular/primitives/index.js +39 -3
  98. package/build/angular/primitives/index.js.map +1 -1
  99. package/build/angular/primitives/shared.d.ts +9 -1
  100. package/build/angular/primitives/shared.js +49 -3
  101. package/build/angular/primitives/shared.js.map +1 -1
  102. package/build/angular/render/index.js +48 -5
  103. package/build/angular/render/index.js.map +1 -1
  104. package/build/angular/renderer/index.d.ts +1 -0
  105. package/build/angular/renderer/index.js +37 -2
  106. package/build/angular/renderer/index.js.map +1 -1
  107. package/build/angular/services/color-scheme.service.js.map +1 -1
  108. package/build/angular/services/window-dimensions.service.js +1 -1
  109. package/build/angular/services/window-dimensions.service.js.map +1 -1
  110. package/package.json +5 -5
  111. package/src/bootstrap.ts +4 -1
  112. package/src/components/activity-indicator/index.android.ts +7 -2
  113. package/src/components/activity-indicator/index.ios.ts +3 -1
  114. package/src/components/activity-indicator/shared.ts +32 -13
  115. package/src/components/button.ts +19 -6
  116. package/src/components/flat-list/index.ts +91 -29
  117. package/src/components/image/index.android.ts +9 -2
  118. package/src/components/image/index.ios.ts +9 -2
  119. package/src/components/image/shared.ts +58 -19
  120. package/src/components/image-background.ts +29 -10
  121. package/src/components/input-accessory-view/index.ts +27 -10
  122. package/src/components/keyboard-avoiding-view/index.ts +55 -15
  123. package/src/components/modal/index.ts +22 -8
  124. package/src/components/pressable/index.ts +51 -21
  125. package/src/components/refresh-control/index.ts +22 -8
  126. package/src/components/safe-area-view/index.ts +16 -7
  127. package/src/components/scroll-view/index.android.ts +31 -9
  128. package/src/components/scroll-view/index.ios.ts +17 -4
  129. package/src/components/scroll-view/projection.ts +290 -63
  130. package/src/components/scroll-view/shared.ts +93 -26
  131. package/src/components/scroll-view/sticky-header.ts +25 -8
  132. package/src/components/section-list/index.ts +61 -19
  133. package/src/components/switch/index.android.ts +14 -3
  134. package/src/components/switch/index.ios.ts +14 -3
  135. package/src/components/switch/shared.ts +15 -4
  136. package/src/components/text-input.ts +45 -13
  137. package/src/components/touchable/index.ts +296 -72
  138. package/src/components/touchable-native-feedback/index.ts +32 -10
  139. package/src/components/virtualized-list/directives.ts +19 -5
  140. package/src/components/virtualized-list/index.ts +274 -72
  141. package/src/components/virtualized-section-list/directives.ts +6 -3
  142. package/src/components/virtualized-section-list/index.ts +146 -34
  143. package/src/components.ts +4 -1
  144. package/src/create-portal/index.ts +8 -2
  145. package/src/create-tunnel/index.ts +3 -1
  146. package/src/descriptor-to-angular/index.ts +29 -8
  147. package/src/diagnostics.ts +138 -0
  148. package/src/host-instance/index.ts +9 -3
  149. package/src/index.ts +31 -2
  150. package/src/modules/animated/create-animated-component.ts +44 -15
  151. package/src/modules/app-registry/index.ts +3 -1
  152. package/src/modules/status-bar/index.ts +13 -4
  153. package/src/primitives/index.ts +46 -2
  154. package/src/primitives/shared.ts +70 -9
  155. package/src/render/index.ts +56 -6
  156. package/src/renderer/index.ts +76 -14
  157. package/src/services/color-scheme.service.ts +3 -1
  158. package/src/services/window-dimensions.service.ts +11 -4
@@ -6,9 +6,10 @@
6
6
  // to 1 on press-out, driven from Pressable's onPressIn/onPressOut. The opacity Value is held by
7
7
  // IDENTITY as a plain field (an engine object, never an @Input / reactive wrap) and the
8
8
  // <symbiote-animated-view> leaf commits it every frame.
9
- // TouchableHighlight: paint underlayColor + lower child opacity while pressed, via Pressable's
10
- // style-as-function (Pressable drives the pressed flag through its own responder lifecycle).
11
- // TouchableWithoutFeedback: no visual change, just the press wiring forwarded through Pressable.
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.
12
13
  //
13
14
  // Every Touchable's own press/hover/accessibility events are now real @Output() EventEmitters too,
14
15
  // matching Pressable (which they wrap) — `(press)="handler($event)"`, never `[onPress]="handler"`.
@@ -33,31 +34,42 @@ import {
33
34
  inject,
34
35
  Input,
35
36
  type OnChanges,
37
+ type OnDestroy,
36
38
  Output,
37
39
  signal,
38
40
  } from '@angular/core';
39
41
  import {
42
+ createHighlightUnderlayHandlers,
43
+ createHighlightUnderlayRuntime,
40
44
  createTouchableFeedbackHandlers,
41
45
  createTouchableFeedbackRuntime,
42
- highlightPressedStyle,
46
+ hasTouchablePressHandler,
47
+ resolveHighlightExtraStyles,
48
+ restingOpacityFromStyle,
43
49
  DEFAULT_ACTIVE_OPACITY,
44
- DEFAULT_HIGHLIGHT_CHILD_OPACITY,
45
- DEFAULT_MIN_PRESS_DURATION_MS,
46
- DEFAULT_UNDERLAY_COLOR,
47
- OPACITY_ACTIVE_DURATION_MS,
50
+ OPACITY_ACTIVE_GRANT_DURATION_MS,
48
51
  OPACITY_INACTIVE_DURATION_MS,
49
52
  RESTING_OPACITY,
53
+ TOUCHABLE_MIN_PRESS_DURATION_MS,
50
54
  type IAccessibilityProps,
51
55
  type IAccessibilityStateValue,
52
56
  type IAriaProps,
57
+ type IHighlightUnderlayHandlers,
53
58
  type IPressableAndroidRippleConfig,
54
- type IPressState,
55
59
  type IPressTimingProps,
56
60
  type IRectOffset,
57
61
  type ITouchableFeedbackHandlers,
58
62
  } from '@symbiote-native/components';
59
- import { type ISymbioteEvent, type IStyleProp, type IViewStyle } from '@symbiote-native/engine';
60
- import { anchorHostStyle, anchorStyleProp, SymbioteStyleInputDirective } from '../../primitives';
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';
61
73
  import { Pressable, type IAngularPressableInputs } from '../pressable';
62
74
  import { Animated, AnimatedView } from '../../modules/animated';
63
75
 
@@ -82,17 +94,43 @@ export type IAngularTouchableHighlightProps = IAngularTouchableBaseProps & {
82
94
 
83
95
  export type IAngularTouchableWithoutFeedbackProps = IAngularTouchableBaseProps;
84
96
 
85
- // The real setTimeout the shared feedback machine schedules its deferred activation/deactivation on
86
- // (core/components has no timer globals). Returns a canceller so an early release flushes the timer.
87
- function scheduleTimeout(callback: () => void, ms: number): () => void {
88
- const id = setTimeout(callback, ms);
89
- return () => clearTimeout(id);
97
+ // The real timers both shared machines schedule on core/components carries no timer globals, so
98
+ // scheduling is the adapter's half. Every canceller is retained so ngOnDestroy cancels what is
99
+ // still in flight: a deferred press-out or a held underlay firing after teardown would emit from a
100
+ // destroyed component and write a signal on a destroyed view.
101
+ interface ITimerScheduler {
102
+ schedule: (callback: () => void, ms: number) => () => void;
103
+ cancelAll: () => void;
104
+ }
105
+
106
+ function createTimerScheduler(): ITimerScheduler {
107
+ const pending = new Set<() => void>();
108
+ return {
109
+ schedule(callback: () => void, ms: number): () => void {
110
+ const id = setTimeout(() => {
111
+ pending.delete(cancel);
112
+ callback();
113
+ }, ms);
114
+ const cancel = (): void => {
115
+ clearTimeout(id);
116
+ pending.delete(cancel);
117
+ };
118
+ pending.add(cancel);
119
+ return cancel;
120
+ },
121
+ cancelAll(): void {
122
+ for (const cancel of [...pending]) cancel();
123
+ pending.clear();
124
+ },
125
+ };
90
126
  }
91
127
 
92
128
  @Component({
93
129
  selector: 'TouchableOpacity',
94
130
  standalone: true,
95
- hostDirectives: [{ directive: SymbioteStyleInputDirective, inputs: ['style'] }],
131
+ hostDirectives: [
132
+ { directive: SymbioteStyleInputDirective, inputs: ['style'] },
133
+ ],
96
134
  schemas: [CUSTOM_ELEMENTS_SCHEMA],
97
135
  imports: [Pressable, AnimatedView],
98
136
  changeDetection: ChangeDetectionStrategy.OnPush,
@@ -138,8 +176,12 @@ function scheduleTimeout(callback: () => void, ms: number): () => void {
138
176
  [accessibilityElementsHidden]="accessibilityElementsHidden"
139
177
  [accessibilityIgnoresInvertColors]="accessibilityIgnoresInvertColors"
140
178
  [accessibilityLanguage]="accessibilityLanguage"
141
- [accessibilityRespondsToUserInteraction]="accessibilityRespondsToUserInteraction"
142
- [accessibilityShowsLargeContentViewer]="accessibilityShowsLargeContentViewer"
179
+ [accessibilityRespondsToUserInteraction]="
180
+ accessibilityRespondsToUserInteraction
181
+ "
182
+ [accessibilityShowsLargeContentViewer]="
183
+ accessibilityShowsLargeContentViewer
184
+ "
143
185
  [accessibilityLargeContentTitle]="accessibilityLargeContentTitle"
144
186
  (accessibilityAction)="accessibilityAction.emit($event)"
145
187
  (accessibilityTap)="accessibilityTap.emit($event)"
@@ -168,7 +210,9 @@ function scheduleTimeout(callback: () => void, ms: number): () => void {
168
210
  </Pressable>
169
211
  `,
170
212
  })
171
- export class TouchableOpacity implements IAngularTouchableOpacityProps, DoCheck {
213
+ export class TouchableOpacity
214
+ implements IAngularTouchableOpacityProps, OnChanges, OnDestroy, DoCheck
215
+ {
172
216
  @Output() readonly press = new EventEmitter<ISymbioteEvent>();
173
217
  @Output() readonly pressIn = new EventEmitter<ISymbioteEvent>();
174
218
  @Output() readonly pressOut = new EventEmitter<ISymbioteEvent>();
@@ -211,8 +255,10 @@ export class TouchableOpacity implements IAngularTouchableOpacityProps, DoCheck
211
255
  @Input() accessibilityValue?: IAccessibilityProps['accessibilityValue'];
212
256
  @Input() accessibilityActions?: IAccessibilityProps['accessibilityActions'];
213
257
  @Input() accessibilityLabelledBy?: string | string[];
214
- @Input() importantForAccessibility?: IAccessibilityProps['importantForAccessibility'];
215
- @Input() accessibilityLiveRegion?: IAccessibilityProps['accessibilityLiveRegion'];
258
+ @Input()
259
+ importantForAccessibility?: IAccessibilityProps['importantForAccessibility'];
260
+ @Input()
261
+ accessibilityLiveRegion?: IAccessibilityProps['accessibilityLiveRegion'];
216
262
  @Input() screenReaderFocusable?: boolean;
217
263
  @Input() accessibilityViewIsModal?: boolean;
218
264
  @Input() accessibilityElementsHidden?: boolean;
@@ -238,13 +284,21 @@ export class TouchableOpacity implements IAngularTouchableOpacityProps, DoCheck
238
284
  @Input() id?: string;
239
285
  @Input() role?: IAriaProps['role'];
240
286
 
241
- // One Animated.Value per mount, resting at full opacity. Held by IDENTITY as a plain field (an
242
- // engine object, never an @Input / reactive wrap); the <symbiote-animated-view> leaf rasterizes
243
- // it for the first paint and drives it through setNativeProps every frame.
287
+ // One Animated.Value per mount. Held by IDENTITY as a plain field (an engine object, never an
288
+ // @Input / reactive wrap); the <symbiote-animated-view> leaf rasterizes it for the first paint
289
+ // and drives it through setNativeProps every frame.
290
+ //
291
+ // RN seeds it from _getChildStyleOpacityWithDefault(props.style). Angular writes @Input()s AFTER
292
+ // the field initializers run, so `style` is still undefined here — the real resting value is
293
+ // seeded by the first ngOnChanges below, which still precedes the first template pass.
244
294
  private readonly opacity = new Animated.Value(RESTING_OPACITY);
245
295
  // The shared press-scheduling cell (delayPressIn timer + activation clock), persisted on the
246
296
  // instance; the machine's handlers are rebuilt per event over live @Input()s.
247
297
  private readonly runtime = createTouchableFeedbackRuntime();
298
+ private readonly timers = createTimerScheduler();
299
+ // What the last settle ran for. Undefined until the first ngOnChanges, which is the MOUNT.
300
+ private settled:
301
+ { disabled: boolean | undefined; resting: number } | undefined;
248
302
  // This component's OWN host — the non-painting anchor `class="..."` at the use site resolves
249
303
  // onto (see anchorHostStyle's doc comment) — NOT the <symbiote-animated-view> leaf one level down.
250
304
  private readonly elementRef = inject(ElementRef);
@@ -271,12 +325,46 @@ export class TouchableOpacity implements IAngularTouchableOpacityProps, DoCheck
271
325
  this.anchorStyle.set(anchorHostStyle(this.elementRef));
272
326
  }
273
327
 
328
+ // RN's componentDidUpdate: a changed `disabled` or a changed style opacity re-settles the view,
329
+ // so a Touchable disabled mid-press does not stay stuck at its active opacity. The FIRST call is
330
+ // the mount (ngOnChanges runs before ngOnInit and before the first template pass), so it seeds
331
+ // the Value instead of animating — RN does this on update only, and a fade at mount would animate
332
+ // away from the value the leaf is about to paint. A use site with no bindings gets no ngOnChanges
333
+ // at all, and there the RESTING_OPACITY seed is already the right answer.
334
+ ngOnChanges(): void {
335
+ const settled = { disabled: this.disabled, resting: this.restingOpacity() };
336
+ if (this.settled === undefined) {
337
+ this.opacity.setValue(settled.resting);
338
+ } else if (
339
+ this.settled.disabled !== settled.disabled ||
340
+ this.settled.resting !== settled.resting
341
+ ) {
342
+ this.setOpacityTo(settled.resting, OPACITY_INACTIVE_DURATION_MS);
343
+ }
344
+ this.settled = settled;
345
+ }
346
+
347
+ // RN's componentWillUnmount: stop the animation so a teardown mid-fade leaves no driver running
348
+ // against a node that is gone, and drop any press-out still deferred behind a timer.
349
+ ngOnDestroy(): void {
350
+ this.timers.cancelAll();
351
+ this.opacity.resetAnimation();
352
+ }
353
+
354
+ // RN's _getChildStyleOpacityWithDefault: the fade settles at the opacity the CALLER's style asks
355
+ // for, not at a hard 1 — a Touchable styled `opacity: 0.6` must come back to 0.6.
356
+ private restingOpacity(): number {
357
+ return restingOpacityFromStyle(this.style);
358
+ }
359
+
274
360
  private setOpacityTo(toValue: number, duration: number): void {
275
361
  Animated.timing(this.opacity, {
276
362
  toValue,
277
363
  duration,
278
364
  easing: Animated.Easing.inOut(Animated.Easing.quad),
279
- useNativeDriver: false,
365
+ // RN's own (TouchableOpacity.js:242). opacity is natively drivable, so the fade survives a
366
+ // busy JS thread — which is the entire point of press feedback.
367
+ useNativeDriver: true,
280
368
  }).start();
281
369
  }
282
370
 
@@ -288,8 +376,11 @@ export class TouchableOpacity implements IAngularTouchableOpacityProps, DoCheck
288
376
  {
289
377
  delayPressIn: this.delayPressIn ?? 0,
290
378
  delayPressOut: this.delayPressOut ?? 0,
291
- minPressDuration: this.minPressDuration ?? DEFAULT_MIN_PRESS_DURATION_MS,
292
- schedule: scheduleTimeout,
379
+ // RN's Touchables override Pressability's own 130ms floor with 0; what holds the active
380
+ // visual there is the fade's own duration, not a press-duration floor.
381
+ minPressDuration:
382
+ this.minPressDuration ?? TOUCHABLE_MIN_PRESS_DURATION_MS,
383
+ schedule: this.timers.schedule,
293
384
  now: Date.now,
294
385
  },
295
386
  this.runtime,
@@ -297,12 +388,15 @@ export class TouchableOpacity implements IAngularTouchableOpacityProps, DoCheck
297
388
  activate: (event: ISymbioteEvent): void => {
298
389
  this.setOpacityTo(
299
390
  this.activeOpacity ?? DEFAULT_ACTIVE_OPACITY,
300
- OPACITY_ACTIVE_DURATION_MS,
391
+ OPACITY_ACTIVE_GRANT_DURATION_MS,
301
392
  );
302
393
  this.pressIn.emit(event);
303
394
  },
304
395
  deactivate: (event: ISymbioteEvent): void => {
305
- this.setOpacityTo(RESTING_OPACITY, OPACITY_INACTIVE_DURATION_MS);
396
+ this.setOpacityTo(
397
+ this.restingOpacity(),
398
+ OPACITY_INACTIVE_DURATION_MS,
399
+ );
306
400
  this.pressOut.emit(event);
307
401
  },
308
402
  },
@@ -323,15 +417,17 @@ export class TouchableOpacity implements IAngularTouchableOpacityProps, DoCheck
323
417
  @Component({
324
418
  selector: 'TouchableHighlight',
325
419
  standalone: true,
326
- hostDirectives: [{ directive: SymbioteStyleInputDirective, inputs: ['style'] }],
420
+ hostDirectives: [
421
+ { directive: SymbioteStyleInputDirective, inputs: ['style'] },
422
+ ],
327
423
  imports: [Pressable],
328
424
  changeDetection: ChangeDetectionStrategy.OnPush,
329
425
  template: `
330
426
  <Pressable
331
- [style]="pressedStyle()"
332
- (press)="press.emit($event)"
333
- (pressIn)="pressIn.emit($event)"
334
- (pressOut)="pressOut.emit($event)"
427
+ [style]="containerStyle()"
428
+ (press)="handlePress($event)"
429
+ (pressIn)="handlePressIn($event)"
430
+ (pressOut)="handlePressOut($event)"
335
431
  (pressMove)="pressMove.emit($event)"
336
432
  (longPress)="longPress.emit($event)"
337
433
  (hoverIn)="hoverIn.emit($event)"
@@ -369,8 +465,12 @@ export class TouchableOpacity implements IAngularTouchableOpacityProps, DoCheck
369
465
  [accessibilityElementsHidden]="accessibilityElementsHidden"
370
466
  [accessibilityIgnoresInvertColors]="accessibilityIgnoresInvertColors"
371
467
  [accessibilityLanguage]="accessibilityLanguage"
372
- [accessibilityRespondsToUserInteraction]="accessibilityRespondsToUserInteraction"
373
- [accessibilityShowsLargeContentViewer]="accessibilityShowsLargeContentViewer"
468
+ [accessibilityRespondsToUserInteraction]="
469
+ accessibilityRespondsToUserInteraction
470
+ "
471
+ [accessibilityShowsLargeContentViewer]="
472
+ accessibilityShowsLargeContentViewer
473
+ "
374
474
  [accessibilityLargeContentTitle]="accessibilityLargeContentTitle"
375
475
  (accessibilityAction)="accessibilityAction.emit($event)"
376
476
  (accessibilityTap)="accessibilityTap.emit($event)"
@@ -397,7 +497,9 @@ export class TouchableOpacity implements IAngularTouchableOpacityProps, DoCheck
397
497
  </Pressable>
398
498
  `,
399
499
  })
400
- export class TouchableHighlight implements IAngularTouchableHighlightProps, OnChanges, DoCheck {
500
+ export class TouchableHighlight
501
+ implements IAngularTouchableHighlightProps, OnChanges, OnDestroy, DoCheck
502
+ {
401
503
  @Output() readonly press = new EventEmitter<ISymbioteEvent>();
402
504
  @Output() readonly pressIn = new EventEmitter<ISymbioteEvent>();
403
505
  @Output() readonly pressOut = new EventEmitter<ISymbioteEvent>();
@@ -409,6 +511,9 @@ export class TouchableHighlight implements IAngularTouchableHighlightProps, OnCh
409
511
  @Output() readonly accessibilityTap = new EventEmitter<ISymbioteEvent>();
410
512
  @Output() readonly magicTap = new EventEmitter<ISymbioteEvent>();
411
513
  @Output() readonly accessibilityEscape = new EventEmitter<ISymbioteEvent>();
514
+ // RN's onShowUnderlay / onHideUnderlay — fired on a real transition only, never on a repeat.
515
+ @Output() readonly showUnderlay = new EventEmitter<void>();
516
+ @Output() readonly hideUnderlay = new EventEmitter<void>();
412
517
  @Input() activeOpacity?: number;
413
518
  @Input() underlayColor?: string;
414
519
  @Input() delayPressIn?: number;
@@ -441,8 +546,10 @@ export class TouchableHighlight implements IAngularTouchableHighlightProps, OnCh
441
546
  @Input() accessibilityValue?: IAccessibilityProps['accessibilityValue'];
442
547
  @Input() accessibilityActions?: IAccessibilityProps['accessibilityActions'];
443
548
  @Input() accessibilityLabelledBy?: string | string[];
444
- @Input() importantForAccessibility?: IAccessibilityProps['importantForAccessibility'];
445
- @Input() accessibilityLiveRegion?: IAccessibilityProps['accessibilityLiveRegion'];
549
+ @Input()
550
+ importantForAccessibility?: IAccessibilityProps['importantForAccessibility'];
551
+ @Input()
552
+ accessibilityLiveRegion?: IAccessibilityProps['accessibilityLiveRegion'];
446
553
  @Input() screenReaderFocusable?: boolean;
447
554
  @Input() accessibilityViewIsModal?: boolean;
448
555
  @Input() accessibilityElementsHidden?: boolean;
@@ -472,33 +579,102 @@ export class TouchableHighlight implements IAngularTouchableHighlightProps, OnCh
472
579
  // onto (see anchorHostStyle's doc comment) — NOT the Pressable one level down.
473
580
  private readonly elementRef = inject(ElementRef);
474
581
 
475
- // Pressable accepts a style-as-function and calls it with the live pressed state. Arrow field so
476
- // `this` stays bound when Pressable invokes it; it reads the inputs LIVE at call time. The
477
- // anchor's class-derived style goes first, then the user's explicit style, so an explicit
478
- // [style] still beats the ambient class. When pressed, paint the underlay color + lower the
479
- // child opacity (RN drives this with setState, not Animated, so we mirror that faithfully
480
- // through Pressable's pressed flag, no tween) the overlay always goes last so it wins.
481
- // A computed that RETURNS A NEW ARROW when its dependencies move, not one stable arrow forever.
482
- // The stable-arrow form was silently frozen: Pressable's `style` @Input never reported a change,
483
- // so Pressable's view never refreshed and never re-invoked the arrow - a class or [style]
484
- // toggled after mount never reached the committed view. Reading the anchor signal from inside
485
- // the arrow body instead would have worked by accident, by registering THIS component's signal
486
- // on Pressable's consumer; returning a new reference keeps the dependency where it belongs, in
487
- // an ordinary input change.
488
- readonly pressedStyle = computed<(state: IPressState) => IStyleProp<IViewStyle>>(() => {
582
+ // `shown` is NOT Pressable's `pressed`. RN holds the underlay for delayPressOut past the tap so a
583
+ // press too fast to see still flashes, and the pressed flag is already false by then. A SIGNAL,
584
+ // because the hide fires from a timer outside any Angular listener, so under zoneless CD
585
+ // nothing else would mark this view. A signal write both notifies the change-detection scheduler
586
+ // (markAncestorsForTraversal) and dirties this template's own reactive consumer, which is the one
587
+ // thing honored in Targeted mode; markForCheck from a plain field would not be reachable here.
588
+ private readonly shown = signal(false);
589
+ private readonly underlayRuntime = createHighlightUnderlayRuntime();
590
+ private readonly timers = createTimerScheduler();
591
+
592
+ // The container style Pressable paints.
593
+ //
594
+ // POINT-7 DECISION both halves stay on the CONTAINER, deliberately. RN's _createExtraStyles
595
+ // keeps them apart: `underlay` (the backgroundColor) on the container, `child` (the lowered
596
+ // opacity) cloned onto the single child. Angular has no cloneElement and no way to reach a
597
+ // projected <ng-content> node, so the only path to the child is a permanent wrapper View — which
598
+ // inserts a node into the flex chain and silently re-parents any `flex` a child declares. The
599
+ // fake Fabric runs no Yoga, so no headless test can measure that damage, only assert the extra
600
+ // node exists; shipping an unmeasurable layout change is worse than the visual approximation.
601
+ // Same call, same reason, as the Solid adapter. resolveHighlightExtraStyles keeps the two styles
602
+ // SEPARATE regardless, so a layout-safe fix (which needs a device check, not a test) stays open.
603
+ //
604
+ // Returns a NEW ARRAY whenever a dependency moves: Pressable's `style` @Input then reports an
605
+ // ordinary change. A stable reference was the earlier bug — Pressable's view never refreshed, so
606
+ // a class or [style] toggled after mount never reached the committed view.
607
+ readonly containerStyle = computed<IStyleProp<IViewStyle>>(() => {
489
608
  this.inputsRevision();
490
609
  const base: IStyleProp<IViewStyle> = [this.anchorStyle(), this.style];
491
- const underlayColor = this.underlayColor ?? DEFAULT_UNDERLAY_COLOR;
492
- const childOpacity = this.activeOpacity ?? DEFAULT_HIGHLIGHT_CHILD_OPACITY;
493
- return (state: IPressState): IStyleProp<IViewStyle> =>
494
- highlightPressedStyle(state.pressed, base, underlayColor, childOpacity);
610
+ const extra = resolveHighlightExtraStyles({
611
+ shown: this.shown(),
612
+ hasPressHandler: this.hasPressHandler(),
613
+ underlayColor: this.underlayColor,
614
+ activeOpacity: this.activeOpacity,
615
+ });
616
+ return extra === undefined ? base : [base, extra.underlay, extra.child];
495
617
  });
496
618
 
497
- // `style`, `underlayColor` and `activeOpacity` are read untracked inside that computed, so the
619
+ // RN's _hasPressHandler: a Touchable that handles no press paints no underlay. Angular exposes
620
+ // the press lifecycle as @Output()s, so "a handler is present" is "something is subscribed" — the
621
+ // same `.observed` test Pressable uses to decide whether to arm its long-press timer at all.
622
+ private hasPressHandler(): boolean {
623
+ return hasTouchablePressHandler({
624
+ onPress: this.press.observed ? this.press : undefined,
625
+ onPressIn: this.pressIn.observed ? this.pressIn : undefined,
626
+ onPressOut: this.pressOut.observed ? this.pressOut : undefined,
627
+ onLongPress: this.longPress.observed ? this.longPress : undefined,
628
+ });
629
+ }
630
+
631
+ // Built per event so the machine reads live @Input()s (delayPressOut, the has-handler gate); the
632
+ // runtime persists across calls, which is what lets handlePressOut see the timer handlePress set.
633
+ private underlayHandlers(): IHighlightUnderlayHandlers {
634
+ return createHighlightUnderlayHandlers(
635
+ {
636
+ delayPressOut: this.delayPressOut ?? 0,
637
+ hasPressHandler: this.hasPressHandler(),
638
+ schedule: this.timers.schedule,
639
+ },
640
+ this.underlayRuntime,
641
+ {
642
+ setShown: (shown: boolean): void => this.shown.set(shown),
643
+ onShowUnderlay: (): void => this.showUnderlay.emit(),
644
+ onHideUnderlay: (): void => this.hideUnderlay.emit(),
645
+ },
646
+ );
647
+ }
648
+
649
+ // Visual first, then the caller's own event — RN's order in _createPressabilityConfig.
650
+ // onLongPress is deliberately NOT intercepted (Pressable emits it straight through); it is only
651
+ // READ, by the has-press-handler gate above.
652
+ handlePressIn(event: ISymbioteEvent): void {
653
+ this.underlayHandlers().handlePressIn(event);
654
+ this.pressIn.emit(event);
655
+ }
656
+
657
+ handlePress(event: ISymbioteEvent): void {
658
+ this.underlayHandlers().handlePress(event);
659
+ this.press.emit(event);
660
+ }
661
+
662
+ handlePressOut(event: ISymbioteEvent): void {
663
+ this.underlayHandlers().handlePressOut(event);
664
+ this.pressOut.emit(event);
665
+ }
666
+
667
+ ngOnDestroy(): void {
668
+ this.timers.cancelAll();
669
+ }
670
+
671
+ // `style`, `underlayColor` and `activeOpacity` are read untracked inside containerStyle, so the
498
672
  // bump is what tells it they moved. See the ngDoCheck note below for the anchor half, which
499
673
  // ngOnChanges cannot cover.
500
674
  private readonly inputsRevision = signal(0);
501
- private readonly anchorStyle = signal<IStyleProp<IViewStyle> | undefined>(undefined);
675
+ private readonly anchorStyle = signal<IStyleProp<IViewStyle> | undefined>(
676
+ undefined,
677
+ );
502
678
 
503
679
  ngOnChanges(): void {
504
680
  this.inputsRevision.update(revision => revision + 1);
@@ -517,15 +693,17 @@ export class TouchableHighlight implements IAngularTouchableHighlightProps, OnCh
517
693
  @Component({
518
694
  selector: 'TouchableWithoutFeedback',
519
695
  standalone: true,
520
- hostDirectives: [{ directive: SymbioteStyleInputDirective, inputs: ['style'] }],
696
+ hostDirectives: [
697
+ { directive: SymbioteStyleInputDirective, inputs: ['style'] },
698
+ ],
521
699
  imports: [Pressable],
522
700
  changeDetection: ChangeDetectionStrategy.OnPush,
523
701
  template: `
524
702
  <Pressable
525
703
  [style]="mergedStyle"
526
704
  (press)="press.emit($event)"
527
- (pressIn)="pressIn.emit($event)"
528
- (pressOut)="pressOut.emit($event)"
705
+ (pressIn)="handlePressIn($event)"
706
+ (pressOut)="handlePressOut($event)"
529
707
  (pressMove)="pressMove.emit($event)"
530
708
  (longPress)="longPress.emit($event)"
531
709
  (hoverIn)="hoverIn.emit($event)"
@@ -563,8 +741,12 @@ export class TouchableHighlight implements IAngularTouchableHighlightProps, OnCh
563
741
  [accessibilityElementsHidden]="accessibilityElementsHidden"
564
742
  [accessibilityIgnoresInvertColors]="accessibilityIgnoresInvertColors"
565
743
  [accessibilityLanguage]="accessibilityLanguage"
566
- [accessibilityRespondsToUserInteraction]="accessibilityRespondsToUserInteraction"
567
- [accessibilityShowsLargeContentViewer]="accessibilityShowsLargeContentViewer"
744
+ [accessibilityRespondsToUserInteraction]="
745
+ accessibilityRespondsToUserInteraction
746
+ "
747
+ [accessibilityShowsLargeContentViewer]="
748
+ accessibilityShowsLargeContentViewer
749
+ "
568
750
  [accessibilityLargeContentTitle]="accessibilityLargeContentTitle"
569
751
  (accessibilityAction)="accessibilityAction.emit($event)"
570
752
  (accessibilityTap)="accessibilityTap.emit($event)"
@@ -591,10 +773,13 @@ export class TouchableHighlight implements IAngularTouchableHighlightProps, OnCh
591
773
  </Pressable>
592
774
  `,
593
775
  })
594
- export class TouchableWithoutFeedback implements DoCheck, IAngularTouchableWithoutFeedbackProps {
595
- // delayPressIn/delayPressOut/minPressDuration ride the prop type for surface parity with the
596
- // other two, but TouchableWithoutFeedback has no visual feedback, so nothing schedules off them
597
- // (RN's TouchableWithoutFeedback is the same pure passthrough).
776
+ export class TouchableWithoutFeedback
777
+ implements DoCheck, OnDestroy, IAngularTouchableWithoutFeedbackProps
778
+ {
779
+ // RN's TouchableWithoutFeedback builds a FULL Pressability config with delayPressIn /
780
+ // delayPressOut / minPressDuration: 0 — "without feedback" means no VISUAL, not no timing. So the
781
+ // same shared machine TouchableOpacity uses runs here, with the visual half left empty: the
782
+ // pressIn/pressOut EMIT is what gets deferred and floored.
598
783
  @Output() readonly press = new EventEmitter<ISymbioteEvent>();
599
784
  @Output() readonly pressIn = new EventEmitter<ISymbioteEvent>();
600
785
  @Output() readonly pressOut = new EventEmitter<ISymbioteEvent>();
@@ -636,8 +821,10 @@ export class TouchableWithoutFeedback implements DoCheck, IAngularTouchableWitho
636
821
  @Input() accessibilityValue?: IAccessibilityProps['accessibilityValue'];
637
822
  @Input() accessibilityActions?: IAccessibilityProps['accessibilityActions'];
638
823
  @Input() accessibilityLabelledBy?: string | string[];
639
- @Input() importantForAccessibility?: IAccessibilityProps['importantForAccessibility'];
640
- @Input() accessibilityLiveRegion?: IAccessibilityProps['accessibilityLiveRegion'];
824
+ @Input()
825
+ importantForAccessibility?: IAccessibilityProps['importantForAccessibility'];
826
+ @Input()
827
+ accessibilityLiveRegion?: IAccessibilityProps['accessibilityLiveRegion'];
641
828
  @Input() screenReaderFocusable?: boolean;
642
829
  @Input() accessibilityViewIsModal?: boolean;
643
830
  @Input() accessibilityElementsHidden?: boolean;
@@ -667,12 +854,47 @@ export class TouchableWithoutFeedback implements DoCheck, IAngularTouchableWitho
667
854
  // onto (see anchorHostStyle's doc comment) — NOT the Pressable one level down.
668
855
  private readonly elementRef = inject(ElementRef);
669
856
 
857
+ // The shared press-scheduling cell (delayPressIn timer + activation clock), persisted on the
858
+ // instance; the machine's handlers are rebuilt per event over live @Input()s.
859
+ private readonly runtime = createTouchableFeedbackRuntime();
860
+ private readonly timers = createTimerScheduler();
861
+
670
862
  // The anchor's class-derived style goes first, then the explicit style, so an explicit [style]
671
863
  // still beats the ambient class.
672
864
  get mergedStyle(): IStyleProp<IViewStyle> {
673
865
  return [this.anchorStyle(), this.style];
674
866
  }
675
867
 
868
+ private feedbackHandlers(): ITouchableFeedbackHandlers {
869
+ return createTouchableFeedbackHandlers(
870
+ {
871
+ delayPressIn: this.delayPressIn ?? 0,
872
+ delayPressOut: this.delayPressOut ?? 0,
873
+ minPressDuration:
874
+ this.minPressDuration ?? TOUCHABLE_MIN_PRESS_DURATION_MS,
875
+ schedule: this.timers.schedule,
876
+ now: Date.now,
877
+ },
878
+ this.runtime,
879
+ {
880
+ activate: (event: ISymbioteEvent): void => this.pressIn.emit(event),
881
+ deactivate: (event: ISymbioteEvent): void => this.pressOut.emit(event),
882
+ },
883
+ );
884
+ }
885
+
886
+ handlePressIn(event: ISymbioteEvent): void {
887
+ this.feedbackHandlers().handlePressIn(event);
888
+ }
889
+
890
+ handlePressOut(event: ISymbioteEvent): void {
891
+ this.feedbackHandlers().handlePressOut(event);
892
+ }
893
+
894
+ ngOnDestroy(): void {
895
+ this.timers.cancelAll();
896
+ }
897
+
676
898
  // The anchor's class-derived style is written by the renderer's addClass/removeClass at the USE
677
899
  // SITE - it never appears in SimpleChanges, and nothing about it dirties THIS component's view.
678
900
  // A `class=` present at creation therefore worked, while one toggled later did not: the parent
@@ -681,7 +903,9 @@ export class TouchableWithoutFeedback implements DoCheck, IAngularTouchableWitho
681
903
  // ngDoCheck fixes both halves: ngDoCheck runs during the PARENT's refresh even when this view is
682
904
  // skipped, and the signal write is what then marks this view for refresh. `signal.set`'s own
683
905
  // Object.is makes an unchanged poll a no-op, so there is no loop.
684
- private readonly anchorStyle = signal<IStyleProp<IViewStyle> | undefined>(undefined);
906
+ private readonly anchorStyle = signal<IStyleProp<IViewStyle> | undefined>(
907
+ undefined,
908
+ );
685
909
 
686
910
  ngDoCheck(): void {
687
911
  this.anchorStyle.set(anchorStyleProp<IViewStyle>(this.elementRef));