@symbiote-native/angular 0.3.3 → 0.5.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 (141) hide show
  1. package/babel-register-composed.cjs +119 -0
  2. package/bin/symbiote-angular-dev.cjs +43 -1
  3. package/build/angular/components/image/shared.d.ts +4 -4
  4. package/build/angular/components/image/shared.js +1 -3
  5. package/build/angular/components/image/shared.js.map +1 -1
  6. package/build/angular/components/{input-accessory-view.d.ts → input-accessory-view/index.d.ts} +1 -1
  7. package/build/angular/components/{input-accessory-view.js → input-accessory-view/index.js} +3 -7
  8. package/build/angular/components/input-accessory-view/index.js.map +1 -0
  9. package/build/angular/components/{keyboard-avoiding-view.js → keyboard-avoiding-view/index.js} +3 -8
  10. package/build/angular/components/keyboard-avoiding-view/index.js.map +1 -0
  11. package/build/angular/components/{modal.js → modal/index.js} +3 -8
  12. package/build/angular/components/modal/index.js.map +1 -0
  13. package/build/angular/components/pressable/index.js +13 -14
  14. package/build/angular/components/pressable/index.js.map +1 -1
  15. package/build/angular/components/{refresh-control.js → refresh-control/index.js} +3 -8
  16. package/build/angular/components/refresh-control/index.js.map +1 -0
  17. package/build/angular/components/{safe-area-view.d.ts → safe-area-view/index.d.ts} +1 -1
  18. package/build/angular/components/{safe-area-view.js → safe-area-view/index.js} +3 -7
  19. package/build/angular/components/safe-area-view/index.js.map +1 -0
  20. package/build/angular/components/text-input.js +1 -6
  21. package/build/angular/components/text-input.js.map +1 -1
  22. package/build/angular/components/{touchable.d.ts → touchable/index.d.ts} +1 -1
  23. package/build/angular/components/{touchable.js → touchable/index.js} +4 -4
  24. package/build/angular/components/touchable/index.js.map +1 -0
  25. package/build/angular/components/{touchable-native-feedback.d.ts → touchable-native-feedback/index.d.ts} +1 -1
  26. package/build/angular/components/{touchable-native-feedback.js → touchable-native-feedback/index.js} +4 -9
  27. package/build/angular/components/touchable-native-feedback/index.js.map +1 -0
  28. package/build/angular/{create-portal.js → create-portal/index.js} +1 -1
  29. package/build/angular/create-portal/index.js.map +1 -0
  30. package/build/angular/{create-tunnel.js → create-tunnel/index.js} +1 -1
  31. package/build/angular/create-tunnel/index.js.map +1 -0
  32. package/build/angular/index.d.ts +1 -1
  33. package/build/angular/index.js +1 -1
  34. package/build/angular/index.js.map +1 -1
  35. package/build/angular/modules/animated/animated-leaf-binder/index.d.ts +14 -0
  36. package/build/angular/modules/animated/animated-leaf-binder/index.js +91 -0
  37. package/build/angular/modules/animated/animated-leaf-binder/index.js.map +1 -0
  38. package/build/angular/modules/animated/animated-leaf-binder.d.ts +14 -0
  39. package/build/angular/modules/animated/animated-leaf-binder.js +91 -0
  40. package/build/angular/modules/animated/animated-leaf-binder.js.map +1 -0
  41. package/build/angular/modules/animated/create-animated-component.d.ts +3 -12
  42. package/build/angular/modules/animated/create-animated-component.js +48 -113
  43. package/build/angular/modules/animated/create-animated-component.js.map +1 -1
  44. package/build/angular/modules/{status-bar.js → status-bar/index.js} +1 -1
  45. package/build/angular/modules/status-bar/index.js.map +1 -0
  46. package/build/angular/{render.js → render/index.js} +15 -4
  47. package/build/angular/render/index.js.map +1 -0
  48. package/build/{renderer.js → angular/renderer/index.js} +17 -18
  49. package/build/angular/renderer/index.js.map +1 -0
  50. package/build/components/image/shared.d.ts +4 -4
  51. package/build/components/image/shared.js +1 -3
  52. package/build/components/{input-accessory-view.d.ts → input-accessory-view/index.d.ts} +1 -1
  53. package/build/components/{input-accessory-view.js → input-accessory-view/index.js} +2 -6
  54. package/build/components/{keyboard-avoiding-view.js → keyboard-avoiding-view/index.js} +2 -7
  55. package/build/components/{modal.js → modal/index.js} +2 -7
  56. package/build/components/pressable/index.js +13 -14
  57. package/build/components/{refresh-control.js → refresh-control/index.js} +2 -7
  58. package/build/components/{safe-area-view.d.ts → safe-area-view/index.d.ts} +1 -1
  59. package/build/components/{safe-area-view.js → safe-area-view/index.js} +2 -6
  60. package/build/components/text-input.js +1 -6
  61. package/build/components/{touchable.d.ts → touchable/index.d.ts} +1 -1
  62. package/build/components/{touchable.js → touchable/index.js} +3 -3
  63. package/build/components/{touchable-native-feedback.d.ts → touchable-native-feedback/index.d.ts} +1 -1
  64. package/build/components/{touchable-native-feedback.js → touchable-native-feedback/index.js} +3 -8
  65. package/build/index.d.ts +1 -1
  66. package/build/index.js +1 -1
  67. package/build/modules/animated/animated-leaf-binder/index.d.ts +14 -0
  68. package/build/modules/animated/animated-leaf-binder/index.js +90 -0
  69. package/build/modules/animated/animated-leaf-binder.d.ts +14 -0
  70. package/build/modules/animated/animated-leaf-binder.js +90 -0
  71. package/build/modules/animated/create-animated-component.d.ts +3 -12
  72. package/build/modules/animated/create-animated-component.js +47 -112
  73. package/build/{render.js → render/index.js} +14 -3
  74. package/build/{angular/renderer.js → renderer/index.js} +16 -19
  75. package/package.json +10 -5
  76. package/src/angular-gaps.test.ts +1 -1
  77. package/src/components/image/shared.ts +6 -5
  78. package/src/components/{input-accessory-view.ts → input-accessory-view/index.ts} +11 -8
  79. package/src/components/{input-accessory-view.test.ts → input-accessory-view/input-accessory-view.test.ts} +2 -2
  80. package/src/components/{keyboard-avoiding-view.ts → keyboard-avoiding-view/index.ts} +2 -7
  81. package/src/components/{keyboard-avoiding-view.test.ts → keyboard-avoiding-view/keyboard-avoiding-view.test.ts} +2 -2
  82. package/src/components/{modal.ts → modal/index.ts} +2 -7
  83. package/src/components/{modal.test.ts → modal/modal.test.ts} +2 -2
  84. package/src/components/pressable/index.ts +17 -15
  85. package/src/components/{refresh-control.ts → refresh-control/index.ts} +2 -7
  86. package/src/components/{refresh-control.test.ts → refresh-control/refresh-control.test.ts} +2 -2
  87. package/src/components/{safe-area-view.ts → safe-area-view/index.ts} +7 -8
  88. package/src/components/{safe-area-view.test.ts → safe-area-view/safe-area-view.test.ts} +2 -2
  89. package/src/components/text-input.ts +1 -6
  90. package/src/components/{touchable.ts → touchable/index.ts} +3 -3
  91. package/src/components/{touchable.test.ts → touchable/touchable.test.ts} +2 -2
  92. package/src/components/{touchable-native-feedback.ts → touchable-native-feedback/index.ts} +3 -9
  93. package/src/components/{touchable-native-feedback.test.ts → touchable-native-feedback/touchable-native-feedback.test.ts} +2 -2
  94. package/src/{create-portal.test.ts → create-portal/create-portal.test.ts} +3 -3
  95. package/src/{create-tunnel.test.ts → create-tunnel/create-tunnel.test.ts} +3 -3
  96. package/src/index.ts +1 -1
  97. package/src/modules/animated/animated-leaf-binder/animated-leaf-binder.test.ts +116 -0
  98. package/src/modules/animated/animated-leaf-binder/index.ts +101 -0
  99. package/src/modules/animated/animated-leaf-binder.test.ts +116 -0
  100. package/src/modules/animated/animated-leaf-binder.ts +101 -0
  101. package/src/modules/animated/animated-scroll-view.test.ts +17 -0
  102. package/src/modules/animated/create-animated-component.ts +53 -123
  103. package/src/modules/{status-bar.test.ts → status-bar/status-bar.test.ts} +2 -2
  104. package/src/{render.ts → render/index.ts} +14 -2
  105. package/src/{render.test.ts → render/render.test.ts} +1 -1
  106. package/src/{renderer.ts → renderer/index.ts} +58 -58
  107. package/src/{renderer.test.ts → renderer/renderer.test.ts} +24 -1
  108. package/build/angular/components/input-accessory-view.js.map +0 -1
  109. package/build/angular/components/keyboard-avoiding-view.js.map +0 -1
  110. package/build/angular/components/modal.js.map +0 -1
  111. package/build/angular/components/refresh-control.js.map +0 -1
  112. package/build/angular/components/safe-area-view.js.map +0 -1
  113. package/build/angular/components/touchable-native-feedback.js.map +0 -1
  114. package/build/angular/components/touchable.js.map +0 -1
  115. package/build/angular/create-portal.js.map +0 -1
  116. package/build/angular/create-tunnel.js.map +0 -1
  117. package/build/angular/modules/status-bar.js.map +0 -1
  118. package/build/angular/render.js.map +0 -1
  119. package/build/angular/renderer.js.map +0 -1
  120. /package/build/angular/components/{keyboard-avoiding-view.d.ts → keyboard-avoiding-view/index.d.ts} +0 -0
  121. /package/build/angular/components/{modal.d.ts → modal/index.d.ts} +0 -0
  122. /package/build/angular/components/{refresh-control.d.ts → refresh-control/index.d.ts} +0 -0
  123. /package/build/angular/{create-portal.d.ts → create-portal/index.d.ts} +0 -0
  124. /package/build/angular/{create-tunnel.d.ts → create-tunnel/index.d.ts} +0 -0
  125. /package/build/angular/modules/{status-bar.d.ts → status-bar/index.d.ts} +0 -0
  126. /package/build/angular/{render.d.ts → render/index.d.ts} +0 -0
  127. /package/build/angular/{renderer.d.ts → renderer/index.d.ts} +0 -0
  128. /package/build/components/{keyboard-avoiding-view.d.ts → keyboard-avoiding-view/index.d.ts} +0 -0
  129. /package/build/components/{modal.d.ts → modal/index.d.ts} +0 -0
  130. /package/build/components/{refresh-control.d.ts → refresh-control/index.d.ts} +0 -0
  131. /package/build/{create-portal.d.ts → create-portal/index.d.ts} +0 -0
  132. /package/build/{create-portal.js → create-portal/index.js} +0 -0
  133. /package/build/{create-tunnel.d.ts → create-tunnel/index.d.ts} +0 -0
  134. /package/build/{create-tunnel.js → create-tunnel/index.js} +0 -0
  135. /package/build/modules/{status-bar.d.ts → status-bar/index.d.ts} +0 -0
  136. /package/build/modules/{status-bar.js → status-bar/index.js} +0 -0
  137. /package/build/{render.d.ts → render/index.d.ts} +0 -0
  138. /package/build/{renderer.d.ts → renderer/index.d.ts} +0 -0
  139. /package/src/{create-portal.ts → create-portal/index.ts} +0 -0
  140. /package/src/{create-tunnel.ts → create-tunnel/index.ts} +0 -0
  141. /package/src/modules/{status-bar.ts → status-bar/index.ts} +0 -0
@@ -14,9 +14,12 @@ import {
14
14
  createPressHandlers,
15
15
  createPressRuntime,
16
16
  DEFAULT_DELAY_LONG_PRESS_MS,
17
+ isTerminationAllowed,
17
18
  resolveAccessibilityProps,
18
19
  resolveDisabledAccessibilityState,
19
20
  rippleProps,
21
+ shouldClaimResponder,
22
+ shouldSuppressPress,
20
23
  type IAccessibilityProps,
21
24
  type IAccessibilityStateValue,
22
25
  type IAriaProps,
@@ -43,7 +46,7 @@ export type { IPressState, IPressableAndroidRippleConfig } from '@symbiote-nativ
43
46
 
44
47
  // The full logical Pressable surface, callback-shaped press/hover events included. Touchable* and
45
48
  // TouchableNativeFeedback still take their OWN onPress/onPressIn/... as @Input() callbacks (that is
46
- // a separate, not-yet-converted decision) and derive their public prop types from this so it keeps
49
+ // a separate, not-yet-converted decision) and derive their public prop types from this - so it keeps
47
50
  // the callback fields even though the Pressable component below no longer implements them directly.
48
51
  export interface IAngularPressableProps extends IAccessibilityProps, IAriaProps {
49
52
  onPress?: IPressHandler;
@@ -76,7 +79,7 @@ export interface IAngularPressableProps extends IAccessibilityProps, IAriaProps
76
79
 
77
80
  // What the Pressable component itself takes as plain @Input()s: the full surface minus the press/
78
81
  // hover events and the four accessibility callbacks, all of which it exposes as real @Output()
79
- // EventEmitters instead (see the class below) the one place in the family where the
82
+ // EventEmitters instead (see the class below) - the one place in the family where the
80
83
  // callback-vs-EventEmitter tradeoff is actually fixed.
81
84
  export type IAngularPressableInputs = Omit<
82
85
  IAngularPressableProps,
@@ -131,7 +134,7 @@ function asSymbioteEvent(event: unknown): ISymbioteEvent | undefined {
131
134
  export class Pressable implements IAngularPressableInputs {
132
135
  // The press/hover lifecycle as real Angular events: `(press)="onTap($event)"`, not
133
136
  // `[onPress]="onTap"`. createPressHandlers still wants plain IPressHandler callbacks, so
134
- // emitterHandler() below adapts each EventEmitter into one only while something is actually
137
+ // emitterHandler() below adapts each EventEmitter into one - only while something is actually
135
138
  // subscribed (`.observed`), so an unbound onLongPress still skips arming the timer, exactly as
136
139
  // the old @Input()-absent case did.
137
140
  @Output() readonly press = new EventEmitter<ISymbioteEvent>();
@@ -223,8 +226,8 @@ export class Pressable implements IAngularPressableInputs {
223
226
  };
224
227
 
225
228
  private readonly changeDetector = inject(ChangeDetectorRef);
226
- // This component's OWN host the non-painting anchor `class="..."` at the use site resolves
227
- // onto (see anchorHostStyle's doc comment) NOT `hostElement` above, which targets the real
229
+ // This component's OWN host - the non-painting anchor `class="..."` at the use site resolves
230
+ // onto (see anchorHostStyle's doc comment) - NOT `hostElement` above, which targets the real
228
231
  // inner `symbiote-view` one level down.
229
232
  private readonly elementRef = inject(ElementRef);
230
233
 
@@ -275,8 +278,7 @@ export class Pressable implements IAngularPressableInputs {
275
278
  return this.android_ripple === undefined ? undefined : rippleProps(this.android_ripple);
276
279
  }
277
280
 
278
- // Assembles the SAME resolved values the template used to bind one-by-one into a single
279
- // flat bag for `[symbioteHostProps]` (Renderer2.setProperty per key) — see primitives/shared.ts.
281
+ // Flat bag for `[symbioteHostProps]` (Renderer2.setProperty per key) - see primitives/shared.ts.
280
282
  get hostProps(): Record<string, unknown> {
281
283
  return {
282
284
  testID: this.testID,
@@ -298,34 +300,34 @@ export class Pressable implements IAngularPressableInputs {
298
300
 
299
301
  handlePressIn(event: unknown): void {
300
302
  const symbioteEvent = asSymbioteEvent(event);
301
- if (this.disabled === true || symbioteEvent === undefined) return;
303
+ if (shouldSuppressPress(this.disabled) || symbioteEvent === undefined) return;
302
304
  this.handlers.handlePressIn(symbioteEvent);
303
305
  }
304
306
 
305
307
  handlePressOut(event: unknown): void {
306
308
  const symbioteEvent = asSymbioteEvent(event);
307
- if (this.disabled === true || symbioteEvent === undefined) return;
309
+ if (shouldSuppressPress(this.disabled) || symbioteEvent === undefined) return;
308
310
  this.handlers.handlePressOut(symbioteEvent);
309
311
  }
310
312
 
311
313
  handlePress(event: unknown): void {
312
314
  const symbioteEvent = asSymbioteEvent(event);
313
- if (this.disabled === true || symbioteEvent === undefined) return;
315
+ if (shouldSuppressPress(this.disabled) || symbioteEvent === undefined) return;
314
316
  this.handlers.handlePress(symbioteEvent);
315
317
  }
316
318
 
317
319
  handleResponderMove(event: unknown): void {
318
320
  const symbioteEvent = asSymbioteEvent(event);
319
- if (this.disabled === true || symbioteEvent === undefined) return;
321
+ if (shouldSuppressPress(this.disabled) || symbioteEvent === undefined) return;
320
322
  this.handlers.handleResponderMove(symbioteEvent);
321
323
  }
322
324
 
323
325
  claimResponder(): boolean {
324
- return this.disabled !== true;
326
+ return shouldClaimResponder(this.disabled);
325
327
  }
326
328
 
327
329
  allowTermination(): boolean {
328
- return this.cancelable !== false;
330
+ return isTerminationAllowed(this.cancelable);
329
331
  }
330
332
 
331
333
  emit(emitter: EventEmitter<ISymbioteEvent>, event: unknown): void {
@@ -342,8 +344,8 @@ export class Pressable implements IAngularPressableInputs {
342
344
  }
343
345
 
344
346
  // Wraps an @Output() as an IPressHandler only while it has a subscriber, so an unbound
345
- // (longPress) still skips arming the timer the same "undefined means nobody cares" contract
346
- // createPressHandlers had with the old @Input() callbacks.
347
+ // (longPress) still skips arming the timer, matching createPressHandlers' "undefined means
348
+ // nobody cares" contract.
347
349
  private emitterHandler(emitter: EventEmitter<ISymbioteEvent>): IPressHandler | undefined {
348
350
  return emitter.observed ? event => emitter.emit(event) : undefined;
349
351
  }
@@ -36,23 +36,18 @@ import {
36
36
  import {
37
37
  dispatchViewCommand,
38
38
  dlog,
39
+ isSymbioteEvent,
39
40
  isSymbioteNode,
40
41
  type IStyleProp,
41
42
  type ISymbioteEvent,
42
43
  type IViewStyle,
43
44
  } from '@symbiote-native/engine';
44
- import { anchorHostStyle, RefreshControlHost, SymbioteHostPropsDirective } from '../primitives';
45
+ import { anchorHostStyle, RefreshControlHost, SymbioteHostPropsDirective } from '../../primitives';
45
46
 
46
47
  // Record<string, unknown> already tolerates `style` holding the [anchorHostStyle, this.style]
47
48
  // array anchorHostStyle's merge produces (see hostProps below) — no widening needed.
48
49
  type IHostProps = Record<string, unknown>;
49
50
 
50
- // Angular infers an `(event)` binding's $event as the DOM Event for a custom-schema element,
51
- // so each handler enters as `unknown` and is narrowed here before reaching a typed callback.
52
- function isSymbioteEvent(value: unknown): value is ISymbioteEvent {
53
- return typeof value === 'object' && value !== null && 'nativeEvent' in value;
54
- }
55
-
56
51
  // Mirrors React's IRefreshControlProps minus children (Angular takes the Android-wrapped scroll view
57
52
  // via <ng-content>), declared per-adapter over the shared accessibility base since the framework-specific
58
53
  // children slot keeps it from being fully shared across adapters. Read the React reference for the
@@ -4,8 +4,8 @@ import { afterEach, beforeEach, describe, expect, it } from 'vitest';
4
4
  import { clearGlobalStyles, registerStyles } from '@symbiote-native/engine';
5
5
  import { installFabric } from '@symbiote-native/test-utils';
6
6
 
7
- import { mount, unmount } from '../render';
8
- import { RefreshControl } from './refresh-control';
7
+ import { mount, unmount } from '../../render';
8
+ import { RefreshControl } from './index';
9
9
 
10
10
  const ROOT_TAG = 914;
11
11
  const fabric = installFabric();
@@ -22,14 +22,13 @@ import {
22
22
  type IAccessibilityStateValue,
23
23
  type IAriaProps,
24
24
  } from '@symbiote-native/components';
25
- import type { IStyleProp, ISymbioteEvent, IViewStyle } from '@symbiote-native/engine';
26
- import { anchorHostStyle, SafeAreaViewHost, SymbioteHostPropsDirective } from '../primitives';
27
-
28
- // Angular infers an `(event)` binding's $event as the DOM Event for a custom-schema element,
29
- // so each handler enters as `unknown` and is narrowed here before reaching a typed callback.
30
- function isSymbioteEvent(value: unknown): value is ISymbioteEvent {
31
- return typeof value === 'object' && value !== null && 'nativeEvent' in value;
32
- }
25
+ import {
26
+ isSymbioteEvent,
27
+ type IStyleProp,
28
+ type ISymbioteEvent,
29
+ type IViewStyle,
30
+ } from '@symbiote-native/engine';
31
+ import { anchorHostStyle, SafeAreaViewHost, SymbioteHostPropsDirective } from '../../primitives';
33
32
 
34
33
  // Mirrors React's ISafeAreaViewProps minus children (Angular takes children via <ng-content>),
35
34
  // declared per-adapter over the shared accessibility base since the framework-specific children
@@ -4,8 +4,8 @@ import { afterEach, beforeEach, describe, expect, it } from 'vitest';
4
4
  import { clearGlobalStyles, registerStyles } from '@symbiote-native/engine';
5
5
  import { installFabric } from '@symbiote-native/test-utils';
6
6
 
7
- import { mount, unmount } from '../render';
8
- import { SafeAreaView } from './safe-area-view';
7
+ import { mount, unmount } from '../../render';
8
+ import { SafeAreaView } from './index';
9
9
 
10
10
  const ROOT_TAG = 910;
11
11
  const fabric = installFabric();
@@ -65,6 +65,7 @@ import {
65
65
  blurTextInput,
66
66
  dispatchViewCommand,
67
67
  dlog,
68
+ isSymbioteEvent,
68
69
  isSymbioteNode,
69
70
  setInputBlurred,
70
71
  setInputFocused,
@@ -83,12 +84,6 @@ import {
83
84
 
84
85
  export type { ITextInputHandle, ITextInputSelection, IInputMode, IEnterKeyHint, ISubmitBehavior };
85
86
 
86
- // Angular infers an (event) binding's $event as the DOM Event for a custom-schema element, so
87
- // each handler enters as `unknown` and is narrowed here before reaching a typed callback.
88
- function isSymbioteEvent(value: unknown): value is ISymbioteEvent {
89
- return typeof value === 'object' && value !== null && 'nativeEvent' in value;
90
- }
91
-
92
87
  // Mirrors React's ITextInputProps minus nothing (TextInput has no children) — declared
93
88
  // per-adapter over the shared accessibility base because Angular's input surface aliases the
94
89
  // aria-* keys to camelCase @Inputs, unlike the plain agnostic fields shared across adapters.
@@ -55,9 +55,9 @@ import {
55
55
  type IStyleProp,
56
56
  type IViewStyle,
57
57
  } from '@symbiote-native/engine';
58
- import { anchorHostStyle, anchorStyleProp } from '../primitives';
59
- import { Pressable, type IAngularPressableInputs } from './pressable';
60
- import { Animated, AnimatedView } from '../modules/animated';
58
+ import { anchorHostStyle, anchorStyleProp } from '../../primitives';
59
+ import { Pressable, type IAngularPressableInputs } from '../pressable';
60
+ import { Animated, AnimatedView } from '../../modules/animated';
61
61
 
62
62
  // The shared field base for all three: the Pressable INPUT surface (minus style, which each
63
63
  // Touchable routes differently, and minus the press/hover events, which each Touchable declares as
@@ -9,8 +9,8 @@ import { afterEach, beforeEach, describe, expect, it } from 'vitest';
9
9
  import { clearGlobalStyles, registerStyles } from '@symbiote-native/engine';
10
10
  import { installFabric } from '@symbiote-native/test-utils';
11
11
 
12
- import { mount, unmount } from '../render';
13
- import { TouchableHighlight, TouchableOpacity, TouchableWithoutFeedback } from './touchable';
12
+ import { mount, unmount } from '../../render';
13
+ import { TouchableHighlight, TouchableOpacity, TouchableWithoutFeedback } from './index';
14
14
 
15
15
  const ROOT_TAG = 940;
16
16
  const fabric = installFabric();
@@ -44,9 +44,9 @@ import {
44
44
  type IPressableAndroidRippleConfig,
45
45
  type IRectOffset,
46
46
  } from '@symbiote-native/components';
47
- import { dlog, type ISymbioteEvent } from '@symbiote-native/engine';
48
- import { anchorHostStyle, SymbioteHostPropsDirective, ViewHost } from '../primitives';
49
- import { Pressable, type IAngularPressableInputs } from './pressable';
47
+ import { dlog, isSymbioteEvent, type ISymbioteEvent } from '@symbiote-native/engine';
48
+ import { anchorHostStyle, SymbioteHostPropsDirective, ViewHost } from '../../primitives';
49
+ import { Pressable, type IAngularPressableInputs } from '../pressable';
50
50
 
51
51
  export type {
52
52
  INativeFeedbackBackground,
@@ -63,12 +63,6 @@ export type IAngularTouchableNativeFeedbackProps = Omit<IAngularPressableInputs,
63
63
  useForeground?: boolean;
64
64
  };
65
65
 
66
- // Angular infers an `(event)` binding's $event as the DOM Event for a custom-schema element,
67
- // so each handler enters as `unknown` and is narrowed here before reaching a typed callback.
68
- function isSymbioteEvent(value: unknown): value is ISymbioteEvent {
69
- return typeof value === 'object' && value !== null && 'nativeEvent' in value;
70
- }
71
-
72
66
  @Component({
73
67
  selector: 'TouchableNativeFeedback',
74
68
  standalone: true,
@@ -10,8 +10,8 @@ import { afterEach, beforeEach, describe, expect, it } from 'vitest';
10
10
  import { clearGlobalStyles, registerStyles } from '@symbiote-native/engine';
11
11
  import { installFabric } from '@symbiote-native/test-utils';
12
12
 
13
- import { mount, unmount } from '../render';
14
- import { TouchableNativeFeedback } from './touchable-native-feedback';
13
+ import { mount, unmount } from '../../render';
14
+ import { TouchableNativeFeedback } from './index';
15
15
 
16
16
  const ROOT_TAG = 941;
17
17
  const fabric = installFabric();
@@ -8,9 +8,9 @@ import { Component, signal } from '@angular/core';
8
8
  import { afterEach, beforeEach, describe, expect, it } from 'vitest';
9
9
  import { installFabric, type IFakeNode } from '@symbiote-native/test-utils';
10
10
 
11
- import { mount, unmount } from './render';
12
- import { ViewHost, TextHost } from './primitives';
13
- import { PortalDirective, PortalOutletDirective } from './create-portal';
11
+ import { mount, unmount } from '../render';
12
+ import { ViewHost, TextHost } from '../primitives';
13
+ import { PortalDirective, PortalOutletDirective } from './index';
14
14
 
15
15
  const ROOT_TAG = 930;
16
16
 
@@ -8,9 +8,9 @@ import { Component } from '@angular/core';
8
8
  import { afterEach, beforeEach, describe, expect, it } from 'vitest';
9
9
  import { installFabric, type IFakeNode } from '@symbiote-native/test-utils';
10
10
 
11
- import { mount, unmount } from './render';
12
- import { ViewHost, TextHost } from './primitives';
13
- import { createTunnel, TunnelInDirective, TunnelOut } from './create-tunnel';
11
+ import { mount, unmount } from '../render';
12
+ import { ViewHost, TextHost } from '../primitives';
13
+ import { createTunnel, TunnelInDirective, TunnelOut } from './index';
14
14
 
15
15
  const SOURCE_TAG = 920;
16
16
  const TARGET_TAG = 921;
package/src/index.ts CHANGED
@@ -135,7 +135,7 @@ export { DescriptorOutlet } from './descriptor-to-angular';
135
135
  // call.
136
136
  export { PortalDirective, PortalOutletDirective } from './create-portal';
137
137
  export { createTunnel, TunnelInDirective, TunnelOut, type ITunnelStore } from './create-tunnel';
138
- export { SymbioteRenderer, SymbioteRendererFactory } from './renderer';
138
+ export { registerComposedComponent, SymbioteRenderer, SymbioteRendererFactory } from './renderer';
139
139
  export { findNodeHandle } from './host-instance';
140
140
  export type { IHostInstance } from './host-instance';
141
141
  export { StatusBar } from './modules/status-bar';
@@ -0,0 +1,116 @@
1
+ // Unit tests for AnimatedLeafBinder in isolation — no Angular involved. Both
2
+ // AnimatedComponentBase and AnimatedImage delegate their leaf-lifecycle (build a leaf,
3
+ // bind it to the committed Fabric node, swap leaves on re-render, tear down) to this Pure
4
+ // Fabrication; these tests exercise the binder directly against a real engine node
5
+ // (SymbioteSurface + the fake Fabric backend), so a break here isolates to the shared
6
+ // orchestration rather than either owning component.
7
+ import { beforeEach, describe, expect, it } from 'vitest';
8
+ import { installFabric } from '@symbiote-native/test-utils';
9
+ import {
10
+ AnimatedValue,
11
+ createElement,
12
+ createSurface,
13
+ type ISymbioteNode,
14
+ } from '@symbiote-native/engine';
15
+ import { AnimatedLeafBinder } from './index';
16
+
17
+ const fabric = installFabric();
18
+ let nextRootTag = 9001;
19
+
20
+ beforeEach(() => fabric.reset());
21
+
22
+ // A node that has already gone through a commit — whenCommitted fires synchronously.
23
+ function committedNode(): ISymbioteNode {
24
+ const surface = createSurface(nextRootTag++);
25
+ const node = createElement('RCTView');
26
+ surface.appendChild(node);
27
+ surface.commit();
28
+ return node;
29
+ }
30
+
31
+ // A node that is attached to a surface but has NOT been committed yet — whenCommitted
32
+ // must defer until the caller commits the surface.
33
+ function uncommittedNode(): { node: ISymbioteNode; commit: () => void } {
34
+ const surface = createSurface(nextRootTag++);
35
+ const node = createElement('RCTView');
36
+ surface.appendChild(node);
37
+ return { node, commit: () => surface.commit() };
38
+ }
39
+
40
+ // `appRoot()` unwraps RN's synthetic box-none AppContainer, which is ALSO an RCTView, so a
41
+ // plain `fabric.find(viewName === 'RCTView')` would match it instead of the node under test.
42
+ // Each test appends exactly one child, so the container's first (only) child is the real node.
43
+ function fakeView(): ReturnType<typeof fabric.find> {
44
+ return fabric.appRoot().children[0];
45
+ }
46
+
47
+ describe('AnimatedLeafBinder', () => {
48
+ it('creates and attaches an AnimatedProps leaf, wiring value changes onto the host node', () => {
49
+ const node = committedNode();
50
+ const binder = new AnimatedLeafBinder(() => node, 'test');
51
+ const opacity = new AnimatedValue(1);
52
+
53
+ // The leaf itself never pushes the INITIAL value — that first paint comes from the
54
+ // owning component's own reduceProps template binding (out of scope here). The leaf
55
+ // only drives value CHANGES after it is bound.
56
+ binder.reconcile({ style: { opacity } }, false);
57
+
58
+ opacity.setValue(0.4);
59
+ expect(fakeView()?.props.opacity).toBe(0.4);
60
+ });
61
+
62
+ it('binds to the host node via whenCommitted, deferring until the node is actually committed', () => {
63
+ const { node, commit } = uncommittedNode();
64
+ const binder = new AnimatedLeafBinder(() => node, 'test');
65
+ const opacity = new AnimatedValue(1);
66
+
67
+ binder.reconcile({ style: { opacity } }, false);
68
+ // Not committed yet: the value graph exists, but no Fabric node to flush onto.
69
+ opacity.setValue(0.5);
70
+ expect(fabric.created).toHaveLength(0);
71
+
72
+ commit();
73
+ // whenCommitted's post-commit retry binds the leaf now; a subsequent change flushes.
74
+ opacity.setValue(0.7);
75
+ expect(fakeView()?.props.opacity).toBe(0.7);
76
+ });
77
+
78
+ it('attaches the new leaf before detaching the old one on a second reconcile', () => {
79
+ const node = committedNode();
80
+ const binder = new AnimatedLeafBinder(() => node, 'test');
81
+ const shared = new AnimatedValue(1);
82
+ const order: string[] = [];
83
+ const originalAddChild = shared.__addChild.bind(shared);
84
+ const originalRemoveChild = shared.__removeChild.bind(shared);
85
+ shared.__addChild = child => {
86
+ order.push('add');
87
+ originalAddChild(child);
88
+ };
89
+ shared.__removeChild = child => {
90
+ order.push('remove');
91
+ originalRemoveChild(child);
92
+ };
93
+
94
+ binder.reconcile({ style: { opacity: shared } }, false);
95
+ order.length = 0; // only care about the SECOND reconcile's swap order
96
+
97
+ binder.reconcile({ style: { opacity: shared } }, false);
98
+
99
+ expect(order).toEqual(['add', 'remove']);
100
+ });
101
+
102
+ it('detaches the leaf and cancels any pending bind on destroy', () => {
103
+ const { node, commit } = uncommittedNode();
104
+ const binder = new AnimatedLeafBinder(() => node, 'test');
105
+ const opacity = new AnimatedValue(1);
106
+
107
+ binder.reconcile({ style: { opacity } }, false);
108
+ binder.destroy();
109
+ commit();
110
+
111
+ // The pending whenCommitted bind was cancelled by destroy, so committing afterward
112
+ // must not retroactively bind the (already-detached) leaf.
113
+ opacity.setValue(0.9);
114
+ expect(fakeView()?.props.opacity).toBeUndefined();
115
+ });
116
+ });
@@ -0,0 +1,101 @@
1
+ // Pure Fabrication (GRASP): the leaf-lifecycle orchestration shared by every Animated*
2
+ // wrapper, extracted out of AnimatedComponentBase so AnimatedImage — which cannot extend
3
+ // AnimatedComponentBase, since it must extend ImageBase for TS's-native image handling —
4
+ // gets the SAME logic via composition instead of copy-pasting it. Framework-thin on
5
+ // purpose: it knows nothing about Angular (@ViewChild, DI, change detection), only the
6
+ // engine's Animated leaf + commit primitives, so it stays testable without any Angular
7
+ // bootstrap. NOTE: react/vue drive this same dance through their own runtime HOC
8
+ // (createAnimatedComponent) rather than a class, so there is no cross-adapter dedup
9
+ // opportunity here yet — this Pure Fabrication is local to the angular adapter.
10
+ import {
11
+ AnimatedProps,
12
+ attachNativeEventHandler,
13
+ dlog,
14
+ whenCommitted,
15
+ type ISymbioteNode,
16
+ } from '@symbiote-native/engine';
17
+
18
+ export class AnimatedLeafBinder {
19
+ // The leaf currently wired into the value graph. Null until the first reconcile.
20
+ private attached: AnimatedProps | null = null;
21
+ // The whenCommitted waiter for the current leaf's node binding, cancelled before the next.
22
+ private cancelBind: (() => void) | undefined;
23
+ // Detachers for any Animated.event prop bound natively to the committed node (JS path: none).
24
+ private eventDetachers: Array<() => void> = [];
25
+
26
+ // `resolveNode` stays a caller-supplied resolver (not a constructor-captured node) so this
27
+ // class never needs to know HOW the host node is found — AnimatedComponentBase and
28
+ // AnimatedImage both resolve it the same way (resolveHostNode + isSymbioteNode over their
29
+ // own @ViewChild), but that's Angular DI state this binder has no business touching.
30
+ // `label` is purely diagnostic (dlog), mirroring the constructor.name the pre-extraction
31
+ // dlog call used.
32
+ constructor(
33
+ private readonly resolveNode: () => ISymbioteNode | null,
34
+ private readonly label: string,
35
+ ) {}
36
+
37
+ // Build a fresh AnimatedProps leaf from the current props (the React/Vue per-render
38
+ // useMemo(rest)), wire it into the graph, and swap the previous one out. Attach the NEW leaf
39
+ // BEFORE detaching the OLD one: a shared Value self-detaches (dropping its native node) the
40
+ // instant its child count hits zero, so detaching first would kill a running native animation
41
+ // on any unrelated re-render (mirrors RN's AnimatedComponent._attachProps). Then bind the
42
+ // committed node, go native if wanted, and rebind native events.
43
+ reconcile(props: Record<string, unknown>, wantsNative: boolean): void {
44
+ dlog(`${this.label} reconcile`);
45
+ const newLeaf = new AnimatedProps(props);
46
+ newLeaf.__attach();
47
+ if (this.attached !== null && this.attached !== newLeaf) this.attached.__detach();
48
+ this.attached = newLeaf;
49
+
50
+ this.bindNode(newLeaf, props, wantsNative);
51
+ }
52
+
53
+ // Bind the leaf to the host's Fabric node THROUGH whenCommitted: under Angular's zoneless
54
+ // batched change detection the inner host's tag does not exist yet at ngAfterViewInit time
55
+ // (the same async-commit gotcha Vue documents), so binding eagerly would no-op. whenCommitted
56
+ // runs the action now if already committed, else after the commit that assigns the tag.
57
+ private bindNode(
58
+ leaf: AnimatedProps,
59
+ props: Record<string, unknown>,
60
+ wantsNative: boolean,
61
+ ): void {
62
+ this.cancelBind?.();
63
+ this.cancelBind = undefined;
64
+ const node = this.resolveNode();
65
+ if (node === null) return;
66
+ this.cancelBind = whenCommitted(node, () => {
67
+ leaf.setNativeView(node);
68
+ if (wantsNative) leaf.__makeNative();
69
+ this.attachEvents(node, props);
70
+ });
71
+ }
72
+
73
+ // Native-attach any Animated.event prop (e.g. onScroll={Animated.event(…,{useNativeDriver:true})})
74
+ // to the committed node. attachNativeEventHandler no-ops (returns undefined) unless the prop is a
75
+ // native event handler with a committed tag, so the JS path stays the fallback. Rebound each
76
+ // reconcile so a new inline event re-attaches; detached first to avoid leaking the prior binding.
77
+ private attachEvents(node: ISymbioteNode, props: Record<string, unknown>): void {
78
+ this.detachEvents();
79
+ for (const key of Object.keys(props)) {
80
+ const attachment = attachNativeEventHandler(node, key, props[key]);
81
+ if (attachment !== undefined) this.eventDetachers.push(attachment.detach);
82
+ }
83
+ }
84
+
85
+ private detachEvents(): void {
86
+ for (const detach of this.eventDetachers) detach();
87
+ this.eventDetachers = [];
88
+ }
89
+
90
+ // Tear down everything: cancel a still-pending bind, drop native event listeners, detach the
91
+ // current leaf from the value graph. Safe to call even if reconcile() was never called.
92
+ destroy(): void {
93
+ this.cancelBind?.();
94
+ this.cancelBind = undefined;
95
+ this.detachEvents();
96
+ if (this.attached !== null) {
97
+ this.attached.__detach();
98
+ this.attached = null;
99
+ }
100
+ }
101
+ }
@@ -0,0 +1,116 @@
1
+ // Unit tests for AnimatedLeafBinder in isolation — no Angular involved. Both
2
+ // AnimatedComponentBase and AnimatedImage delegate their leaf-lifecycle (build a leaf,
3
+ // bind it to the committed Fabric node, swap leaves on re-render, tear down) to this Pure
4
+ // Fabrication; these tests exercise the binder directly against a real engine node
5
+ // (SymbioteSurface + the fake Fabric backend), so a break here isolates to the shared
6
+ // orchestration rather than either owning component.
7
+ import { beforeEach, describe, expect, it } from 'vitest';
8
+ import { installFabric } from '@symbiote-native/test-utils';
9
+ import {
10
+ AnimatedValue,
11
+ createElement,
12
+ createSurface,
13
+ type ISymbioteNode,
14
+ } from '@symbiote-native/engine';
15
+ import { AnimatedLeafBinder } from './animated-leaf-binder';
16
+
17
+ const fabric = installFabric();
18
+ let nextRootTag = 9001;
19
+
20
+ beforeEach(() => fabric.reset());
21
+
22
+ // A node that has already gone through a commit — whenCommitted fires synchronously.
23
+ function committedNode(): ISymbioteNode {
24
+ const surface = createSurface(nextRootTag++);
25
+ const node = createElement('RCTView');
26
+ surface.appendChild(node);
27
+ surface.commit();
28
+ return node;
29
+ }
30
+
31
+ // A node that is attached to a surface but has NOT been committed yet — whenCommitted
32
+ // must defer until the caller commits the surface.
33
+ function uncommittedNode(): { node: ISymbioteNode; commit: () => void } {
34
+ const surface = createSurface(nextRootTag++);
35
+ const node = createElement('RCTView');
36
+ surface.appendChild(node);
37
+ return { node, commit: () => surface.commit() };
38
+ }
39
+
40
+ // `appRoot()` unwraps RN's synthetic box-none AppContainer, which is ALSO an RCTView, so a
41
+ // plain `fabric.find(viewName === 'RCTView')` would match it instead of the node under test.
42
+ // Each test appends exactly one child, so the container's first (only) child is the real node.
43
+ function fakeView(): ReturnType<typeof fabric.find> {
44
+ return fabric.appRoot().children[0];
45
+ }
46
+
47
+ describe('AnimatedLeafBinder', () => {
48
+ it('creates and attaches an AnimatedProps leaf, wiring value changes onto the host node', () => {
49
+ const node = committedNode();
50
+ const binder = new AnimatedLeafBinder(() => node, 'test');
51
+ const opacity = new AnimatedValue(1);
52
+
53
+ // The leaf itself never pushes the INITIAL value — that first paint comes from the
54
+ // owning component's own reduceProps template binding (out of scope here). The leaf
55
+ // only drives value CHANGES after it is bound.
56
+ binder.reconcile({ style: { opacity } }, false);
57
+
58
+ opacity.setValue(0.4);
59
+ expect(fakeView()?.props.opacity).toBe(0.4);
60
+ });
61
+
62
+ it('binds to the host node via whenCommitted, deferring until the node is actually committed', () => {
63
+ const { node, commit } = uncommittedNode();
64
+ const binder = new AnimatedLeafBinder(() => node, 'test');
65
+ const opacity = new AnimatedValue(1);
66
+
67
+ binder.reconcile({ style: { opacity } }, false);
68
+ // Not committed yet: the value graph exists, but no Fabric node to flush onto.
69
+ opacity.setValue(0.5);
70
+ expect(fabric.created).toHaveLength(0);
71
+
72
+ commit();
73
+ // whenCommitted's post-commit retry binds the leaf now; a subsequent change flushes.
74
+ opacity.setValue(0.7);
75
+ expect(fakeView()?.props.opacity).toBe(0.7);
76
+ });
77
+
78
+ it('attaches the new leaf before detaching the old one on a second reconcile', () => {
79
+ const node = committedNode();
80
+ const binder = new AnimatedLeafBinder(() => node, 'test');
81
+ const shared = new AnimatedValue(1);
82
+ const order: string[] = [];
83
+ const originalAddChild = shared.__addChild.bind(shared);
84
+ const originalRemoveChild = shared.__removeChild.bind(shared);
85
+ shared.__addChild = child => {
86
+ order.push('add');
87
+ originalAddChild(child);
88
+ };
89
+ shared.__removeChild = child => {
90
+ order.push('remove');
91
+ originalRemoveChild(child);
92
+ };
93
+
94
+ binder.reconcile({ style: { opacity: shared } }, false);
95
+ order.length = 0; // only care about the SECOND reconcile's swap order
96
+
97
+ binder.reconcile({ style: { opacity: shared } }, false);
98
+
99
+ expect(order).toEqual(['add', 'remove']);
100
+ });
101
+
102
+ it('detaches the leaf and cancels any pending bind on destroy', () => {
103
+ const { node, commit } = uncommittedNode();
104
+ const binder = new AnimatedLeafBinder(() => node, 'test');
105
+ const opacity = new AnimatedValue(1);
106
+
107
+ binder.reconcile({ style: { opacity } }, false);
108
+ binder.destroy();
109
+ commit();
110
+
111
+ // The pending whenCommitted bind was cancelled by destroy, so committing afterward
112
+ // must not retroactively bind the (already-detached) leaf.
113
+ opacity.setValue(0.9);
114
+ expect(fakeView()?.props.opacity).toBeUndefined();
115
+ });
116
+ });