@symbiote-native/angular 0.3.2 → 0.4.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 (69) hide show
  1. package/babel-register-composed.cjs +119 -0
  2. package/build/angular/components/image/shared.d.ts +4 -4
  3. package/build/angular/components/image/shared.js +1 -3
  4. package/build/angular/components/image/shared.js.map +1 -1
  5. package/build/angular/components/input-accessory-view.d.ts +1 -1
  6. package/build/angular/components/input-accessory-view.js +1 -5
  7. package/build/angular/components/input-accessory-view.js.map +1 -1
  8. package/build/angular/components/keyboard-avoiding-view.js +1 -6
  9. package/build/angular/components/keyboard-avoiding-view.js.map +1 -1
  10. package/build/angular/components/modal.js +1 -6
  11. package/build/angular/components/modal.js.map +1 -1
  12. package/build/angular/components/pressable/index.js +13 -14
  13. package/build/angular/components/pressable/index.js.map +1 -1
  14. package/build/angular/components/refresh-control.js +1 -6
  15. package/build/angular/components/refresh-control.js.map +1 -1
  16. package/build/angular/components/safe-area-view.d.ts +1 -1
  17. package/build/angular/components/safe-area-view.js +1 -5
  18. package/build/angular/components/safe-area-view.js.map +1 -1
  19. package/build/angular/components/text-input.js +1 -6
  20. package/build/angular/components/text-input.js.map +1 -1
  21. package/build/angular/components/touchable-native-feedback.js +1 -6
  22. package/build/angular/components/touchable-native-feedback.js.map +1 -1
  23. package/build/angular/index.d.ts +1 -1
  24. package/build/angular/index.js +1 -1
  25. package/build/angular/index.js.map +1 -1
  26. package/build/angular/modules/animated/animated-leaf-binder.d.ts +14 -0
  27. package/build/angular/modules/animated/animated-leaf-binder.js +91 -0
  28. package/build/angular/modules/animated/animated-leaf-binder.js.map +1 -0
  29. package/build/angular/modules/animated/create-animated-component.d.ts +3 -12
  30. package/build/angular/modules/animated/create-animated-component.js +48 -113
  31. package/build/angular/modules/animated/create-animated-component.js.map +1 -1
  32. package/build/angular/renderer.js +6 -14
  33. package/build/angular/renderer.js.map +1 -1
  34. package/build/components/image/shared.d.ts +4 -4
  35. package/build/components/image/shared.js +1 -3
  36. package/build/components/input-accessory-view.d.ts +1 -1
  37. package/build/components/input-accessory-view.js +1 -5
  38. package/build/components/keyboard-avoiding-view.js +1 -6
  39. package/build/components/modal.js +1 -6
  40. package/build/components/pressable/index.js +13 -14
  41. package/build/components/refresh-control.js +1 -6
  42. package/build/components/safe-area-view.d.ts +1 -1
  43. package/build/components/safe-area-view.js +1 -5
  44. package/build/components/text-input.js +1 -6
  45. package/build/components/touchable-native-feedback.js +1 -6
  46. package/build/index.d.ts +1 -1
  47. package/build/index.js +1 -1
  48. package/build/modules/animated/animated-leaf-binder.d.ts +14 -0
  49. package/build/modules/animated/animated-leaf-binder.js +90 -0
  50. package/build/modules/animated/create-animated-component.d.ts +3 -12
  51. package/build/modules/animated/create-animated-component.js +47 -112
  52. package/build/renderer.js +6 -14
  53. package/package.json +9 -5
  54. package/src/components/image/shared.ts +6 -5
  55. package/src/components/input-accessory-view.ts +6 -7
  56. package/src/components/keyboard-avoiding-view.ts +1 -6
  57. package/src/components/modal.ts +1 -6
  58. package/src/components/pressable/index.ts +17 -15
  59. package/src/components/refresh-control.ts +1 -6
  60. package/src/components/safe-area-view.ts +6 -7
  61. package/src/components/text-input.ts +1 -6
  62. package/src/components/touchable-native-feedback.ts +1 -7
  63. package/src/index.ts +1 -1
  64. package/src/modules/animated/animated-leaf-binder.test.ts +116 -0
  65. package/src/modules/animated/animated-leaf-binder.ts +101 -0
  66. package/src/modules/animated/animated-scroll-view.test.ts +17 -0
  67. package/src/modules/animated/create-animated-component.ts +53 -123
  68. package/src/renderer.test.ts +24 -1
  69. package/src/renderer.ts +6 -14
@@ -52,12 +52,14 @@ var __runInitializers = (this && this.__runInitializers) || function (thisArg, i
52
52
  return useValue ? value : void 0;
53
53
  };
54
54
  import { ChangeDetectionStrategy, Component, CUSTOM_ELEMENTS_SCHEMA, Directive, ElementRef, inject, ViewChild, } from '@angular/core';
55
- import { AnimatedProps, attachNativeEventHandler, dlog, isNativeAnimatedAvailable, isSymbioteNode, reduceProps, readPassthroughStyle, resolveHostNode, whenCommitted, } from '@symbiote-native/engine';
55
+ import { isNativeAnimatedAvailable, isSymbioteNode, reduceProps, readPassthroughStyle, resolveHostNode, } from '@symbiote-native/engine';
56
+ import { selectScrollIntrinsics } from '@symbiote-native/components';
56
57
  import { Image, ScrollView, Text, View } from '../../components';
57
58
  import { anchorHostStyle, ViewHost, TextHost, ImageHost, ScrollViewHost, SymbioteHostPropsDirective, } from '../../primitives';
58
59
  import { FlatList } from '../../components/flat-list';
59
60
  import { IMAGE_INPUTS, IMAGE_OUTPUTS, ImageBase, resolveImageProps, } from '../../components/image/shared';
60
61
  import { SectionList } from '../../components/section-list';
62
+ import { AnimatedLeafBinder } from './animated-leaf-binder';
61
63
  // RN's prop carrying explicit (already-rasterized) values that override the animated prop in
62
64
  // the COMMITTED props (sticky-header passthrough). Named once so the directive input and the
63
65
  // reconcile agree.
@@ -105,12 +107,11 @@ let AnimatedComponentBase = (() => {
105
107
  // The directive sitting on the inner host primitive; its `node` is the committed SymbioteNode
106
108
  // the leaf binds to. Inherited by the decorated subclass (Angular collects base-class queries).
107
109
  hostDirective = __runInitializers(this, _hostDirective_initializers, void 0);
108
- // The leaf currently wired into the value graph. Null until the first reconcile.
109
- attached = (__runInitializers(this, _hostDirective_extraInitializers), null);
110
- // The whenCommitted waiter for the current leaf's node binding, cancelled before the next.
111
- cancelBind;
112
- // Detachers for any Animated.event prop bound natively to the committed node (JS path: none).
113
- eventDetachers = [];
110
+ // The leaf-lifecycle orchestration (build/attach/swap/bind/detach an AnimatedProps leaf) is a
111
+ // Pure Fabrication shared with AnimatedImage, which cannot extend this class (it must extend
112
+ // ImageBase see the file header). `resolveNode` stays a resolver, not a captured node: the
113
+ // committed node only exists after `hostDirective` is populated post-view-init.
114
+ leafBinder = (__runInitializers(this, _hostDirective_extraInitializers), new AnimatedLeafBinder(() => this.hostNode(), this.constructor.name));
114
115
  // Set once the view (and the inner host directive) exists; ngOnChanges before then is a no-op.
115
116
  viewReady = false;
116
117
  // The concrete props for the host element each change-detection pass: animated entries reduced
@@ -134,73 +135,21 @@ let AnimatedComponentBase = (() => {
134
135
  this.reconcile();
135
136
  }
136
137
  ngOnDestroy() {
137
- this.cancelBind?.();
138
- this.cancelBind = undefined;
139
- this.detachEvents();
140
- if (this.attached !== null) {
141
- this.attached.__detach();
142
- this.attached = null;
143
- }
138
+ this.leafBinder.destroy();
144
139
  }
145
- // Build a fresh AnimatedProps leaf from the current props (the React/Vue per-render
146
- // useMemo(rest)), wire it into the graph, and swap the previous one out. Attach the NEW leaf
147
- // BEFORE detaching the OLD one: a shared Value self-detaches (dropping its native node) the
148
- // instant its child count hits zero, so detaching first would kill a running native animation
149
- // on any unrelated re-render (mirrors RN's AnimatedComponent._attachProps). Then bind the
150
- // committed node, go native if wanted, and rebind native events.
140
+ // A non-stable [animatedProps]/[style] input (a fresh object literal in the template) pushes
141
+ // a new reference on every change-detection pass of the HOST view, re-triggering this on every
142
+ // unrelated interaction elsewhere in the app DEBUG=1 turns this into a visible tick count in
143
+ // AnimatedLeafBinder.reconcile, so an unexpectedly high rate for a component nobody is
144
+ // animating is the tell (see AnimatedScrollView's stable-reference fix in examples/angular/App.ts).
151
145
  reconcile() {
152
146
  if (!this.viewReady)
153
147
  return;
154
- // A non-stable [animatedProps]/[style] input (a fresh object literal in the template)
155
- // pushes a new reference on every change-detection pass of the HOST view, re-triggering
156
- // this on every unrelated interaction elsewhere in the app — DEBUG=1 turns this into a
157
- // visible tick count, so an unexpectedly high rate for a component nobody is animating
158
- // is the tell (see AnimatedScrollView's stable-reference fix in examples/angular/App.ts).
159
- dlog(`AnimatedComponentBase reconcile (${this.constructor.name})`);
160
148
  const props = this.mergedProps();
161
149
  const hasPassthroughAnimatedValues = this.passthroughAnimatedPropExplicitValues !== null &&
162
150
  this.passthroughAnimatedPropExplicitValues !== undefined;
163
151
  const wantsNative = hasPassthroughAnimatedValues && isNativeAnimatedAvailable();
164
- const newLeaf = new AnimatedProps(props);
165
- newLeaf.__attach();
166
- if (this.attached !== null && this.attached !== newLeaf)
167
- this.attached.__detach();
168
- this.attached = newLeaf;
169
- this.bindNode(newLeaf, props, wantsNative);
170
- }
171
- // Bind the leaf to the host's Fabric node THROUGH whenCommitted: under Angular's zoneless
172
- // batched change detection the inner host's tag does not exist yet at ngAfterViewInit time
173
- // (the same async-commit gotcha Vue documents), so binding eagerly would no-op. whenCommitted
174
- // runs the action now if already committed, else after the commit that assigns the tag.
175
- bindNode(leaf, props, wantsNative) {
176
- this.cancelBind?.();
177
- this.cancelBind = undefined;
178
- const node = this.hostNode();
179
- if (node === null)
180
- return;
181
- this.cancelBind = whenCommitted(node, () => {
182
- leaf.setNativeView(node);
183
- if (wantsNative)
184
- leaf.__makeNative();
185
- this.attachEvents(node, props);
186
- });
187
- }
188
- // Native-attach any Animated.event prop (e.g. onScroll={Animated.event(…,{useNativeDriver:true})})
189
- // to the committed node. attachNativeEventHandler no-ops (returns undefined) unless the prop is a
190
- // native event handler with a committed tag, so the JS path stays the fallback. Rebound each
191
- // reconcile so a new inline event re-attaches; detached first to avoid leaking the prior binding.
192
- attachEvents(node, props) {
193
- this.detachEvents();
194
- for (const key of Object.keys(props)) {
195
- const attachment = attachNativeEventHandler(node, key, props[key]);
196
- if (attachment !== undefined)
197
- this.eventDetachers.push(attachment.detach);
198
- }
199
- }
200
- detachEvents() {
201
- for (const detach of this.eventDetachers)
202
- detach();
203
- this.eventDetachers = [];
152
+ this.leafBinder.reconcile(props, wantsNative);
204
153
  }
205
154
  // Merge the dedicated `style` input over the generic `animatedProps` bag into one props map —
206
155
  // the equivalent of React/Vue `rest`. `style` wins so `[style]` and `[animatedProps].style`
@@ -333,9 +282,10 @@ let AnimatedImage = (() => {
333
282
  // comment on AnimatedComponentBase.elementRef.
334
283
  elementRef = inject(ElementRef);
335
284
  hostDirective = __runInitializers(this, _hostDirective_initializers, void 0);
336
- attached = (__runInitializers(this, _hostDirective_extraInitializers), null);
337
- cancelBind;
338
- eventDetachers = [];
285
+ // Same Pure Fabrication AnimatedComponentBase holds — see its matching comment. AnimatedImage
286
+ // can't extend AnimatedComponentBase (it must extend ImageBase, see the file header), so it
287
+ // gets the leaf-lifecycle via composition instead of duplicating it.
288
+ leafBinder = (__runInitializers(this, _hostDirective_extraInitializers), new AnimatedLeafBinder(() => this.hostNode(), this.constructor.name));
339
289
  viewReady = false;
340
290
  get animatedImageProps() {
341
291
  const reduced = reduceProps(this.mergedProps());
@@ -356,13 +306,7 @@ let AnimatedImage = (() => {
356
306
  this.reconcile();
357
307
  }
358
308
  ngOnDestroy() {
359
- this.cancelBind?.();
360
- this.cancelBind = undefined;
361
- this.detachEvents();
362
- if (this.attached !== null) {
363
- this.attached.__detach();
364
- this.attached = null;
365
- }
309
+ this.leafBinder.destroy();
366
310
  }
367
311
  reconcile() {
368
312
  if (!this.viewReady)
@@ -371,38 +315,7 @@ let AnimatedImage = (() => {
371
315
  const hasPassthroughAnimatedValues = this.passthroughAnimatedPropExplicitValues !== null &&
372
316
  this.passthroughAnimatedPropExplicitValues !== undefined;
373
317
  const wantsNative = hasPassthroughAnimatedValues && isNativeAnimatedAvailable();
374
- const newLeaf = new AnimatedProps(props);
375
- newLeaf.__attach();
376
- if (this.attached !== null && this.attached !== newLeaf)
377
- this.attached.__detach();
378
- this.attached = newLeaf;
379
- this.bindNode(newLeaf, props, wantsNative);
380
- }
381
- bindNode(leaf, props, wantsNative) {
382
- this.cancelBind?.();
383
- this.cancelBind = undefined;
384
- const node = this.hostNode();
385
- if (node === null)
386
- return;
387
- this.cancelBind = whenCommitted(node, () => {
388
- leaf.setNativeView(node);
389
- if (wantsNative)
390
- leaf.__makeNative();
391
- this.attachEvents(node, props);
392
- });
393
- }
394
- attachEvents(node, props) {
395
- this.detachEvents();
396
- for (const key of Object.keys(props)) {
397
- const attachment = attachNativeEventHandler(node, key, props[key]);
398
- if (attachment !== undefined)
399
- this.eventDetachers.push(attachment.detach);
400
- }
401
- }
402
- detachEvents() {
403
- for (const detach of this.eventDetachers)
404
- detach();
405
- this.eventDetachers = [];
318
+ this.leafBinder.reconcile(props, wantsNative);
406
319
  }
407
320
  mergedProps() {
408
321
  const props = { ...(this.animatedProps ?? {}) };
@@ -429,7 +342,7 @@ let AnimatedScrollView = (() => {
429
342
  inputs: ANIMATED_INPUTS,
430
343
  template: `
431
344
  <symbiote-scroll-view [symbioteHostProps]="reducedProps">
432
- <symbiote-scroll-content [collapsable]="false">
345
+ <symbiote-scroll-content [symbioteHostProps]="contentProps">
433
346
  <ng-content></ng-content>
434
347
  </symbiote-scroll-content>
435
348
  </symbiote-scroll-view>
@@ -454,11 +367,33 @@ let AnimatedScrollView = (() => {
454
367
  // receives touch (swallowed by the outer scroll view; iOS doesn't gate on this). The real
455
368
  // ScrollView component already defaults this (scroll-view/shared.ts), but AnimatedScrollView
456
369
  // talks to the raw primitive directly and has no such defaulting, so it must apply it too.
370
+ //
371
+ // scrollViewBaseStyle (overflow: 'scroll' + the per-axis flexDirection) is the OTHER thing
372
+ // the real ScrollView applies that this bespoke template used to skip entirely — without it,
373
+ // iOS Fabric never clips the scroll view's content to its own frame (Android's native
374
+ // ViewGroup clips regardless of the style prop, which is why this was invisible there). This
375
+ // wrapper has no dedicated `horizontal` input (only the generic `animatedProps`/`style`
376
+ // surface), and its template is hardcoded to the vertical intrinsics
377
+ // (symbiote-scroll-view/symbiote-scroll-content, not the horizontal variants), so
378
+ // selectScrollIntrinsics is always called with isHorizontal=false here — matching what the
379
+ // template can actually render.
457
380
  get reducedProps() {
458
381
  const reduced = super.reducedProps;
459
- return reduced.nestedScrollEnabled === undefined
460
- ? { ...reduced, nestedScrollEnabled: true }
461
- : reduced;
382
+ const { scrollViewBaseStyle } = selectScrollIntrinsics(false, undefined);
383
+ const withScrollBase = {
384
+ ...reduced,
385
+ style: [scrollViewBaseStyle, reduced.style],
386
+ };
387
+ return withScrollBase.nestedScrollEnabled === undefined
388
+ ? { ...withScrollBase, nestedScrollEnabled: true }
389
+ : withScrollBase;
390
+ }
391
+ // The content (inner) view's props: contentStyle from the same intrinsics selection, plus
392
+ // `collapsable: false` (the Android multi-child fix this file's tests already cover) — mirrors
393
+ // the real ScrollView's own `contentProps` getter (scroll-view/shared.ts).
394
+ get contentProps() {
395
+ const { contentStyle } = selectScrollIntrinsics(false, undefined);
396
+ return { style: contentStyle, collapsable: false };
462
397
  }
463
398
  };
464
399
  return AnimatedScrollView = _classThis;
package/build/renderer.js CHANGED
@@ -53,23 +53,15 @@ const ANCHOR_HOST_COMPONENTS = new Set([
53
53
  // Composed Angular components render their real Fabric descriptor tree from the template;
54
54
  // their Angular host element is only a framework bookkeeping node and must not paint. This
55
55
  // is NOT limited to adapter-authored components — ANY custom composed @Component used as a
56
- // plain <Tag> inside another template needs its selector listed here too, adapter or app
57
- // code alike, or Angular's automatic host-element creation for it falls through to a raw
58
- // Fabric createNode call with an unrecognized view name, which paints RN's own "Unimplemented
59
- // component: <Tag>" fallback view instead (a real device-visible bug, not a silent no-op
60
- // discovered when examples/angular's own new demo components hit this).
61
- 'AccessibilityDemo',
56
+ // plain <Tag> inside another template needs its selector listed here too, or Angular's
57
+ // automatic host-element creation for it falls through to a raw Fabric createNode call with
58
+ // an unrecognized view name, which paints RN's own "Unimplemented component: <Tag>" fallback
59
+ // view instead (a real device-visible bug, not a silent no-op). This Set holds only
60
+ // adapter/engine-owned selectors; app code and third-party packages self-register their own
61
+ // composed components through registerComposedComponent instead of being hardcoded here.
62
62
  'ActivityIndicator',
63
- 'AnimatedDemo',
64
- 'AnimatedParityDemo',
65
63
  'Button',
66
64
  'FlatList',
67
- 'NativeModulesDemo',
68
- 'ParityDemo',
69
- 'PlatformColorDemo',
70
- 'RefApiDemo',
71
- 'ResponderDemo',
72
- 'Slider',
73
65
  'AnimatedView',
74
66
  'symbiote-animated-view',
75
67
  'AnimatedText',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@symbiote-native/angular",
3
- "version": "0.3.2",
3
+ "version": "0.4.0",
4
4
  "description": "SymbioteNative's Angular adapter — a Renderer2/RendererFactory2 with DOM-less bootstrap driving real native iOS/Android views through the same engine as the other adapters.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -30,6 +30,7 @@
30
30
  "./metro-css-parser": "./metro-css-parser.cjs",
31
31
  "./metro-config": "./metro-config.cjs",
32
32
  "./babel-linker": "./babel-linker.cjs",
33
+ "./babel-register-composed": "./babel-register-composed.cjs",
33
34
  "./tsconfig.angular.base.json": "./tsconfig.angular.base.json"
34
35
  },
35
36
  "files": [
@@ -39,6 +40,7 @@
39
40
  "metro-css-parser.cjs",
40
41
  "metro-config.cjs",
41
42
  "babel-linker.cjs",
43
+ "babel-register-composed.cjs",
42
44
  "tsconfig.angular.base.json"
43
45
  ],
44
46
  "bin": {
@@ -50,9 +52,9 @@
50
52
  "dependencies": {
51
53
  "@angular/compiler-cli": "^22",
52
54
  "@react-native/typescript-config": "0.86.0",
53
- "@symbiote-native/components": "0.2.2",
54
- "@symbiote-native/engine": "0.1.3",
55
- "@symbiote-native/css-parser": "0.2.1"
55
+ "@symbiote-native/components": "0.2.4",
56
+ "@symbiote-native/css-parser": "0.2.1",
57
+ "@symbiote-native/engine": "0.1.5"
56
58
  },
57
59
  "peerDependencies": {
58
60
  "@angular/core": ">=20",
@@ -61,9 +63,11 @@
61
63
  "devDependencies": {
62
64
  "@angular/compiler": "^22",
63
65
  "@angular/core": "^22",
66
+ "@babel/core": "^7.25.2",
67
+ "@types/babel__core": "^7.20.5",
64
68
  "@types/node": "^26.0.0",
65
69
  "typescript": "~6.0.0",
66
- "@symbiote-native/test-utils": "0.1.3"
70
+ "@symbiote-native/test-utils": "0.1.5"
67
71
  },
68
72
  "scripts": {
69
73
  "ng:build": "ngc -p tsconfig.angular.json",
@@ -9,7 +9,12 @@ import {
9
9
  type IImageSourceProp,
10
10
  type IResizeMode,
11
11
  } from '@symbiote-native/components';
12
- import type { IStyleProp, ISymbioteEvent, IViewStyle } from '@symbiote-native/engine';
12
+ import {
13
+ isSymbioteEvent,
14
+ type IStyleProp,
15
+ type ISymbioteEvent,
16
+ type IViewStyle,
17
+ } from '@symbiote-native/engine';
13
18
 
14
19
  export { setImageSourceResolver } from '@symbiote-native/components';
15
20
  export type {
@@ -153,10 +158,6 @@ function asStyle(value: unknown): IStyleProp<IViewStyle> | undefined {
153
158
  return typeof value === 'object' && value !== null ? value : undefined;
154
159
  }
155
160
 
156
- function isSymbioteEvent(value: unknown): value is ISymbioteEvent {
157
- return typeof value === 'object' && value !== null && Reflect.has(value, 'nativeEvent');
158
- }
159
-
160
161
  export function resolveImageProps(input: Record<string, unknown>): Record<string, unknown> {
161
162
  const passthrough: IImagePassthroughProps = {
162
163
  resizeMethod: input['resizeMethod'] as IImageProps['resizeMethod'],
@@ -21,15 +21,14 @@ import {
21
21
  type IAccessibilityStateValue,
22
22
  type IAriaProps,
23
23
  } from '@symbiote-native/components';
24
- import type { IStyleProp, ISymbioteEvent, IViewStyle } from '@symbiote-native/engine';
24
+ import {
25
+ isSymbioteEvent,
26
+ type IStyleProp,
27
+ type ISymbioteEvent,
28
+ type IViewStyle,
29
+ } from '@symbiote-native/engine';
25
30
  import { anchorHostStyle, InputAccessoryViewHost, SymbioteHostPropsDirective } from '../primitives';
26
31
 
27
- // Angular infers an `(event)` binding's $event as the DOM Event for a custom-schema element,
28
- // so each handler enters as `unknown` and is narrowed here before reaching a typed callback.
29
- function isSymbioteEvent(value: unknown): value is ISymbioteEvent {
30
- return typeof value === 'object' && value !== null && 'nativeEvent' in value;
31
- }
32
-
33
32
  // Mirrors React's IInputAccessoryViewProps minus children (Angular takes children via
34
33
  // <ng-content>), declared per-adapter over the shared a11y base.
35
34
  export interface IAngularInputAccessoryViewProps extends IAccessibilityProps, IAriaProps {
@@ -42,6 +42,7 @@ import {
42
42
  Keyboard,
43
43
  KEYBOARD_EVENT,
44
44
  dlog,
45
+ isSymbioteEvent,
45
46
  type IEventSubscription,
46
47
  type IStyleProp,
47
48
  type ISymbioteEvent,
@@ -51,12 +52,6 @@ import { anchorHostStyle, SymbioteHostPropsDirective, ViewHost } from '../primit
51
52
 
52
53
  export type { IKeyboardAvoidingBehavior } from '@symbiote-native/components';
53
54
 
54
- // Angular infers an `(event)` binding's $event as the DOM Event for a custom-schema element,
55
- // so each handler enters as `unknown` and is narrowed here before reaching a typed callback.
56
- function isSymbioteEvent(value: unknown): value is ISymbioteEvent {
57
- return typeof value === 'object' && value !== null && 'nativeEvent' in value;
58
- }
59
-
60
55
  // Mirrors React's IKeyboardAvoidingViewProps minus children (Angular takes children via
61
56
  // <ng-content>), declared per-adapter over the shared accessibility base since a framework-specific
62
57
  // children field keeps it from being fully shared across adapters.
@@ -39,6 +39,7 @@ import {
39
39
  } from '@symbiote-native/components';
40
40
  import {
41
41
  dlog,
42
+ isSymbioteEvent,
42
43
  type IStyleProp,
43
44
  type ISymbioteEvent,
44
45
  type IViewStyle,
@@ -52,12 +53,6 @@ export type {
52
53
  IModalOrientationChangeEvent,
53
54
  } from '@symbiote-native/components';
54
55
 
55
- // Angular infers an `(event)` binding's $event as the DOM Event for a custom-schema element,
56
- // so each handler enters as `unknown` and is narrowed here before reaching a typed callback.
57
- function isSymbioteEvent(value: unknown): value is ISymbioteEvent {
58
- return typeof value === 'object' && value !== null && 'nativeEvent' in value;
59
- }
60
-
61
56
  const ORIENTATIONS: ReadonlyArray<IModalOrientation> = ['portrait', 'landscape'];
62
57
 
63
58
  function readOrientation(event: ISymbioteEvent): IModalOrientationChangeEvent | undefined {
@@ -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,6 +36,7 @@ import {
36
36
  import {
37
37
  dispatchViewCommand,
38
38
  dlog,
39
+ isSymbioteEvent,
39
40
  isSymbioteNode,
40
41
  type IStyleProp,
41
42
  type ISymbioteEvent,
@@ -47,12 +48,6 @@ import { anchorHostStyle, RefreshControlHost, SymbioteHostPropsDirective } from
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
@@ -22,15 +22,14 @@ 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';
25
+ import {
26
+ isSymbioteEvent,
27
+ type IStyleProp,
28
+ type ISymbioteEvent,
29
+ type IViewStyle,
30
+ } from '@symbiote-native/engine';
26
31
  import { anchorHostStyle, SafeAreaViewHost, SymbioteHostPropsDirective } from '../primitives';
27
32
 
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
- }
33
-
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
36
35
  // slot keeps it from being fully shared across adapters.
@@ -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.
@@ -44,7 +44,7 @@ import {
44
44
  type IPressableAndroidRippleConfig,
45
45
  type IRectOffset,
46
46
  } from '@symbiote-native/components';
47
- import { dlog, type ISymbioteEvent } from '@symbiote-native/engine';
47
+ import { dlog, isSymbioteEvent, type ISymbioteEvent } from '@symbiote-native/engine';
48
48
  import { anchorHostStyle, SymbioteHostPropsDirective, ViewHost } from '../primitives';
49
49
  import { Pressable, type IAngularPressableInputs } from './pressable';
50
50
 
@@ -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,
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';