@symbiote-native/angular 0.8.0 → 1.0.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 (121) hide show
  1. package/babel-lower-host-primitives.cjs +636 -0
  2. package/babel-register-composed.cjs +21 -3
  3. package/build/angular/anchor-host-registry.js +3 -1
  4. package/build/angular/anchor-host-registry.js.map +1 -1
  5. package/build/angular/components/button.d.ts +3 -0
  6. package/build/angular/components/button.js +10 -1
  7. package/build/angular/components/button.js.map +1 -1
  8. package/build/angular/components/image/index.android.js +0 -8
  9. package/build/angular/components/image/index.android.js.map +1 -1
  10. package/build/angular/components/image/index.ios.js +0 -8
  11. package/build/angular/components/image/index.ios.js.map +1 -1
  12. package/build/angular/components/image/shared.d.ts +4 -4
  13. package/build/angular/components/image/shared.js +33 -27
  14. package/build/angular/components/image/shared.js.map +1 -1
  15. package/build/angular/components/image-background.d.ts +3 -0
  16. package/build/angular/components/image-background.js +10 -1
  17. package/build/angular/components/image-background.js.map +1 -1
  18. package/build/angular/components/input-accessory-view/index.d.ts +2 -1
  19. package/build/angular/components/input-accessory-view/index.js +11 -14
  20. package/build/angular/components/input-accessory-view/index.js.map +1 -1
  21. package/build/angular/components/keyboard-avoiding-view/index.d.ts +2 -1
  22. package/build/angular/components/keyboard-avoiding-view/index.js +17 -10
  23. package/build/angular/components/keyboard-avoiding-view/index.js.map +1 -1
  24. package/build/angular/components/modal/index.d.ts +1 -0
  25. package/build/angular/components/modal/index.js +14 -8
  26. package/build/angular/components/modal/index.js.map +1 -1
  27. package/build/angular/components/pressable/index.d.ts +9 -4
  28. package/build/angular/components/pressable/index.js +55 -14
  29. package/build/angular/components/pressable/index.js.map +1 -1
  30. package/build/angular/components/refresh-control/index.d.ts +2 -1
  31. package/build/angular/components/refresh-control/index.js +10 -8
  32. package/build/angular/components/refresh-control/index.js.map +1 -1
  33. package/build/angular/components/safe-area-view/index.d.ts +3 -1
  34. package/build/angular/components/safe-area-view/index.js +19 -19
  35. package/build/angular/components/safe-area-view/index.js.map +1 -1
  36. package/build/angular/components/scroll-view/shared.d.ts +2 -0
  37. package/build/angular/components/scroll-view/shared.js +20 -4
  38. package/build/angular/components/scroll-view/shared.js.map +1 -1
  39. package/build/angular/components/section-list/index.d.ts +3 -0
  40. package/build/angular/components/section-list/index.js +10 -1
  41. package/build/angular/components/section-list/index.js.map +1 -1
  42. package/build/angular/components/switch/index.android.js +6 -6
  43. package/build/angular/components/switch/index.android.js.map +1 -1
  44. package/build/angular/components/switch/index.ios.js +6 -6
  45. package/build/angular/components/switch/index.ios.js.map +1 -1
  46. package/build/angular/components/text-input.d.ts +7 -1
  47. package/build/angular/components/text-input.js +54 -29
  48. package/build/angular/components/text-input.js.map +1 -1
  49. package/build/angular/components/touchable/index.d.ts +7 -0
  50. package/build/angular/components/touchable/index.js +34 -3
  51. package/build/angular/components/touchable/index.js.map +1 -1
  52. package/build/angular/components/touchable-native-feedback/index.d.ts +2 -1
  53. package/build/angular/components/touchable-native-feedback/index.js +17 -15
  54. package/build/angular/components/touchable-native-feedback/index.js.map +1 -1
  55. package/build/angular/components/virtualized-list/index.d.ts +3 -0
  56. package/build/angular/components/virtualized-list/index.js +10 -1
  57. package/build/angular/components/virtualized-list/index.js.map +1 -1
  58. package/build/angular/components/virtualized-section-list/index.d.ts +3 -0
  59. package/build/angular/components/virtualized-section-list/index.js +10 -1
  60. package/build/angular/components/virtualized-section-list/index.js.map +1 -1
  61. package/build/angular/components.d.ts +1 -1
  62. package/build/angular/components.js +1 -1
  63. package/build/angular/components.js.map +1 -1
  64. package/build/angular/create-portal/index.d.ts +11 -2
  65. package/build/angular/create-portal/index.js +37 -11
  66. package/build/angular/create-portal/index.js.map +1 -1
  67. package/build/angular/gate-demand.d.ts +37 -0
  68. package/build/angular/gate-demand.js +84 -0
  69. package/build/angular/gate-demand.js.map +1 -0
  70. package/build/angular/index.d.ts +1 -0
  71. package/build/angular/index.js +1 -0
  72. package/build/angular/index.js.map +1 -1
  73. package/build/angular/modules/animated/create-animated-component.d.ts +1 -0
  74. package/build/angular/modules/animated/create-animated-component.js +14 -9
  75. package/build/angular/modules/animated/create-animated-component.js.map +1 -1
  76. package/build/angular/primitives/index.d.ts +13 -1
  77. package/build/angular/primitives/index.js +43 -1
  78. package/build/angular/primitives/index.js.map +1 -1
  79. package/build/angular/register.d.ts +1 -0
  80. package/build/angular/register.js +35 -0
  81. package/build/angular/register.js.map +1 -0
  82. package/build/angular/render/index.js +3 -1
  83. package/build/angular/render/index.js.map +1 -1
  84. package/build/angular/renderer/index.js +120 -19
  85. package/build/angular/renderer/index.js.map +1 -1
  86. package/build/angular/state-style.d.ts +9 -0
  87. package/build/angular/state-style.js +39 -0
  88. package/build/angular/state-style.js.map +1 -0
  89. package/metro-transformer.cjs +69 -0
  90. package/package.json +27 -8
  91. package/src/anchor-host-registry.ts +3 -1
  92. package/src/components/button.ts +17 -0
  93. package/src/components/image/index.android.ts +0 -4
  94. package/src/components/image/index.ios.ts +0 -4
  95. package/src/components/image/shared.ts +52 -26
  96. package/src/components/image-background.ts +18 -0
  97. package/src/components/input-accessory-view/index.ts +14 -8
  98. package/src/components/keyboard-avoiding-view/index.ts +21 -7
  99. package/src/components/modal/index.ts +17 -4
  100. package/src/components/pressable/index.ts +70 -10
  101. package/src/components/refresh-control/index.ts +14 -5
  102. package/src/components/safe-area-view/index.ts +28 -12
  103. package/src/components/scroll-view/shared.ts +31 -4
  104. package/src/components/section-list/index.ts +18 -0
  105. package/src/components/switch/index.android.ts +4 -4
  106. package/src/components/switch/index.ios.ts +4 -4
  107. package/src/components/text-input.ts +73 -17
  108. package/src/components/touchable/index.ts +43 -0
  109. package/src/components/touchable-native-feedback/index.ts +18 -8
  110. package/src/components/virtualized-list/index.ts +18 -0
  111. package/src/components/virtualized-section-list/index.ts +18 -0
  112. package/src/components.ts +2 -0
  113. package/src/create-portal/index.ts +39 -9
  114. package/src/gate-demand.ts +109 -0
  115. package/src/index.ts +2 -0
  116. package/src/modules/animated/create-animated-component.ts +23 -4
  117. package/src/primitives/index.ts +28 -1
  118. package/src/register.ts +43 -0
  119. package/src/render/index.ts +3 -1
  120. package/src/renderer/index.ts +144 -34
  121. package/src/state-style.ts +39 -0
@@ -100,6 +100,13 @@ export {
100
100
  VListHeaderDirective,
101
101
  VListSeparatorDirective,
102
102
  } from '../virtualized-list';
103
+ import {
104
+ gateWanted,
105
+ injectGateDemandAbove,
106
+ provideGateDemand,
107
+ type IGateDemand,
108
+ type IGatedAccessibilityEvent,
109
+ } from '../../gate-demand';
103
110
 
104
111
  // SectionList's public surface is exactly VirtualizedSectionList's (RN layers them one-for-one), so
105
112
  // the prop type is shared verbatim — no SectionList-only field exists.
@@ -123,6 +130,7 @@ export type ISectionListInputs<ItemT> = Omit<
123
130
  @Component({
124
131
  selector: 'SectionList',
125
132
  standalone: true,
133
+ viewProviders: [provideGateDemand(() => SectionList)],
126
134
  hostDirectives: [
127
135
  { directive: SymbioteStyleInputDirective, inputs: ['style'] },
128
136
  ],
@@ -381,6 +389,16 @@ export class SectionList<ItemT = unknown>
381
389
  @Output() readonly accessibilityTap = new EventEmitter<ISymbioteEvent>();
382
390
  @Output() readonly magicTap = new EventEmitter<ISymbioteEvent>();
383
391
  @Output() readonly accessibilityEscape = new EventEmitter<ISymbioteEvent>();
392
+
393
+ // This wrapper binds the four gated accessibility events on the component it renders, which
394
+ // Angular forces to be unconditional and which would light that component's gates on every
395
+ // instance. It answers for them instead — see `gate-demand.ts`.
396
+ private readonly gateDemandAbove = injectGateDemandAbove();
397
+
398
+ wantsGate(name: IGatedAccessibilityEvent): boolean {
399
+ return gateWanted(this.gateDemandAbove, name, this[name]);
400
+ }
401
+
384
402
  @Input() role?: IAriaProps['role'];
385
403
  @Input() ariaLabel?: string;
386
404
  @Input() ariaLabelledBy?: string;
@@ -10,7 +10,7 @@ import {
10
10
  import { NG_VALUE_ACCESSOR } from '@angular/forms';
11
11
  import {
12
12
  anchorHostStyle,
13
- SwitchHost,
13
+ ManagedSwitchHost,
14
14
  SymbioteHostPropsDirective,
15
15
  SymbioteStyleInputDirective,
16
16
  } from '../../primitives';
@@ -24,7 +24,7 @@ export type { ISwitchProps, ISwitchTrackColor } from './shared';
24
24
  { directive: SymbioteStyleInputDirective, inputs: ['style'] },
25
25
  ],
26
26
  schemas: [CUSTOM_ELEMENTS_SCHEMA],
27
- imports: [SwitchHost, SymbioteHostPropsDirective],
27
+ imports: [ManagedSwitchHost, SymbioteHostPropsDirective],
28
28
  providers: [
29
29
  {
30
30
  provide: NG_VALUE_ACCESSOR,
@@ -54,7 +54,7 @@ export type { ISwitchProps, ISwitchTrackColor } from './shared';
54
54
  ],
55
55
  outputs: ['valueChange', 'change'],
56
56
  template: `
57
- <symbiote-switch
57
+ <symbiote-switch-managed
58
58
  #nativeSwitch="symbioteHost"
59
59
  [symbioteHostProps]="hostProps()"
60
60
  (change)="handleChange($event, nativeSwitch)"
@@ -63,7 +63,7 @@ export type { ISwitchProps, ISwitchTrackColor } from './shared';
63
63
  })
64
64
  export class Switch extends SwitchBase implements DoCheck {
65
65
  // This component's OWN host — the non-painting anchor `class="..."` at the use site resolves
66
- // onto (see anchorHostStyle's doc comment) — NOT the <symbiote-switch> leaf one level down.
66
+ // onto (see anchorHostStyle's doc comment) — NOT the <symbiote-switch-managed> leaf one level down.
67
67
  private readonly elementRef = inject(ElementRef);
68
68
 
69
69
  protected readonly platform = {
@@ -10,7 +10,7 @@ import {
10
10
  import { NG_VALUE_ACCESSOR } from '@angular/forms';
11
11
  import {
12
12
  anchorHostStyle,
13
- SwitchHost,
13
+ ManagedSwitchHost,
14
14
  SymbioteHostPropsDirective,
15
15
  SymbioteStyleInputDirective,
16
16
  } from '../../primitives';
@@ -24,7 +24,7 @@ export type { ISwitchProps, ISwitchTrackColor } from './shared';
24
24
  { directive: SymbioteStyleInputDirective, inputs: ['style'] },
25
25
  ],
26
26
  schemas: [CUSTOM_ELEMENTS_SCHEMA],
27
- imports: [SwitchHost, SymbioteHostPropsDirective],
27
+ imports: [ManagedSwitchHost, SymbioteHostPropsDirective],
28
28
  providers: [
29
29
  {
30
30
  provide: NG_VALUE_ACCESSOR,
@@ -54,7 +54,7 @@ export type { ISwitchProps, ISwitchTrackColor } from './shared';
54
54
  ],
55
55
  outputs: ['valueChange', 'change'],
56
56
  template: `
57
- <symbiote-switch
57
+ <symbiote-switch-managed
58
58
  #nativeSwitch="symbioteHost"
59
59
  [symbioteHostProps]="hostProps()"
60
60
  (change)="handleChange($event, nativeSwitch)"
@@ -63,7 +63,7 @@ export type { ISwitchProps, ISwitchTrackColor } from './shared';
63
63
  })
64
64
  export class Switch extends SwitchBase implements DoCheck {
65
65
  // This component's OWN host — the non-painting anchor `class="..."` at the use site resolves
66
- // onto (see anchorHostStyle's doc comment) — NOT the <symbiote-switch> leaf one level down.
66
+ // onto (see anchorHostStyle's doc comment) — NOT the <symbiote-switch-managed> leaf one level down.
67
67
  private readonly elementRef = inject(ElementRef);
68
68
 
69
69
  protected readonly platform = {
@@ -2,7 +2,7 @@
2
2
  // and the controlled-write predicate live in @symbiote-native/components/state, the render (intrinsic
3
3
  // + native-prop mapping) in @symbiote-native/components/view, both shared verbatim with the React and
4
4
  // Vue adapters. Here Angular supplies only the lifecycle: renderTextInput picks ONE of two host
5
- // intrinsics at runtime (symbiote-text-input / symbiote-text-input-multiline) via `@if`/`@else`,
5
+ // intrinsics at runtime (symbiote-text-input-managed / …-multiline-managed) via `@if`/`@else`,
6
6
  // each wired through the shared `SymbioteHostPropsDirective` (`[symbioteHostProps]="hostProps"`,
7
7
  // `../primitives/shared.ts`) so the flat resolved prop bag doesn't need enumerating as individual
8
8
  // `[prop]` bindings; a @ViewChild reading that directive (by its `#host` template ref, across
@@ -73,6 +73,9 @@ import {
73
73
  setInputBlurred,
74
74
  setInputFocused,
75
75
  whenCommitted,
76
+ type IMeasureInWindowOnSuccess,
77
+ type IMeasureLayoutOnSuccess,
78
+ type IMeasureOnSuccess,
76
79
  type IStyleProp,
77
80
  type ISymbioteEvent,
78
81
  type ISymbioteNode,
@@ -80,11 +83,16 @@ import {
80
83
  } from '@symbiote-native/engine';
81
84
  import {
82
85
  anchorHostStyle,
83
- MultilineTextInputHost,
86
+ ManagedMultilineTextInputHost,
84
87
  SymbioteHostPropsDirective,
85
88
  SymbioteStyleInputDirective,
86
- TextInputHost,
89
+ ManagedTextInputHost,
87
90
  } from '../primitives';
91
+ import {
92
+ gateWanted,
93
+ injectGateDemand,
94
+ type IGatedAccessibilityEvent,
95
+ } from '../gate-demand';
88
96
 
89
97
  export type {
90
98
  ITextInputHandle,
@@ -169,7 +177,11 @@ export type IAngularTextInputInputs = Omit<
169
177
  { directive: SymbioteStyleInputDirective, inputs: ['style'] },
170
178
  ],
171
179
  schemas: [CUSTOM_ELEMENTS_SCHEMA],
172
- imports: [SymbioteHostPropsDirective, TextInputHost, MultilineTextInputHost],
180
+ imports: [
181
+ SymbioteHostPropsDirective,
182
+ ManagedTextInputHost,
183
+ ManagedMultilineTextInputHost,
184
+ ],
173
185
  changeDetection: ChangeDetectionStrategy.OnPush,
174
186
  providers: [
175
187
  {
@@ -180,7 +192,7 @@ export type IAngularTextInputInputs = Omit<
180
192
  ],
181
193
  template: `
182
194
  @if (isMultiline) {
183
- <symbiote-text-input-multiline
195
+ <symbiote-text-input-multiline-managed
184
196
  #host
185
197
  [symbioteHostProps]="hostProps"
186
198
  (change)="handleChange($event)"
@@ -191,13 +203,9 @@ export type IAngularTextInputInputs = Omit<
191
203
  (keyPress)="emit(keyPress, $event)"
192
204
  (endEditing)="emit(endEditing, $event)"
193
205
  (contentSizeChange)="emit(contentSizeChange, $event)"
194
- (accessibilityAction)="emit(accessibilityAction, $event)"
195
- (accessibilityTap)="emit(accessibilityTap, $event)"
196
- (magicTap)="emit(magicTap, $event)"
197
- (accessibilityEscape)="emit(accessibilityEscape, $event)"
198
- ></symbiote-text-input-multiline>
206
+ ></symbiote-text-input-multiline-managed>
199
207
  } @else {
200
- <symbiote-text-input
208
+ <symbiote-text-input-managed
201
209
  #host
202
210
  [symbioteHostProps]="hostProps"
203
211
  (change)="handleChange($event)"
@@ -208,11 +216,7 @@ export type IAngularTextInputInputs = Omit<
208
216
  (keyPress)="emit(keyPress, $event)"
209
217
  (endEditing)="emit(endEditing, $event)"
210
218
  (contentSizeChange)="emit(contentSizeChange, $event)"
211
- (accessibilityAction)="emit(accessibilityAction, $event)"
212
- (accessibilityTap)="emit(accessibilityTap, $event)"
213
- (magicTap)="emit(magicTap, $event)"
214
- (accessibilityEscape)="emit(accessibilityEscape, $event)"
215
- ></symbiote-text-input>
219
+ ></symbiote-text-input-managed>
216
220
  }
217
221
  `,
218
222
  })
@@ -282,6 +286,9 @@ export class TextInput
282
286
  @Output() readonly magicTap = new EventEmitter<ISymbioteEvent>();
283
287
  @Output() readonly accessibilityEscape = new EventEmitter<ISymbioteEvent>();
284
288
 
289
+ // Null unless a wrapper renders this TextInput in its own template. See `../gate-demand.ts`.
290
+ private readonly gateDemand = injectGateDemand();
291
+
285
292
  @Input() accessible?: boolean;
286
293
  @Input() accessibilityLabel?: string;
287
294
  @Input() accessibilityHint?: string;
@@ -318,7 +325,7 @@ export class TextInput
318
325
 
319
326
  // This component's OWN host — the non-painting anchor `class="..."` at the use site resolves
320
327
  // onto (see anchorHostStyle's doc comment) — NOT `hostRef` above, which targets the real inner
321
- // symbiote-text-input(-multiline) one level down.
328
+ // symbiote-text-input-managed(-multiline-managed) one level down.
322
329
  private readonly elementRef = inject(ElementRef);
323
330
  // Only setDisabledState() needs this: it mutates `editable` from OUTSIDE Angular's own binding
324
331
  // path (@angular/forms calling in directly), which doesn't itself schedule a tick under zoneless CD.
@@ -458,6 +465,41 @@ export class TextInput
458
465
  if (isSymbioteEvent(event)) emitter.emit(event);
459
466
  }
460
467
 
468
+ // `undefined` means `setEventListener` never sees a function, so the gate stays dark. A demand
469
+ // from above overrides `.observed` for the same reason it does on Pressable: a wrapper's binding
470
+ // is not a subscriber (`../gate-demand.ts`).
471
+ private gatedAccessibilityHandler(
472
+ name: IGatedAccessibilityEvent,
473
+ ): ((event: unknown) => void) | undefined {
474
+ const emitter = this[name];
475
+ return gateWanted(this.gateDemand, name, emitter)
476
+ ? event => this.emit(emitter, event)
477
+ : undefined;
478
+ }
479
+
480
+ // Forwarded from the engine node so a TextInput ref is not poorer than any other host ref — an
481
+ // instance exposing only the five methods below CLOSES the node off (see ITextInputHandle's own
482
+ // comment). No Angular-specific state to add: these four pass straight through.
483
+ measure(callback: IMeasureOnSuccess): void {
484
+ this.hostNode?.measure(callback);
485
+ }
486
+
487
+ measureInWindow(callback: IMeasureInWindowOnSuccess): void {
488
+ this.hostNode?.measureInWindow(callback);
489
+ }
490
+
491
+ measureLayout(
492
+ relativeToNativeNode: ISymbioteNode | number,
493
+ onSuccess: IMeasureLayoutOnSuccess,
494
+ onFail?: () => void,
495
+ ): void {
496
+ this.hostNode?.measureLayout(relativeToNativeNode, onSuccess, onFail);
497
+ }
498
+
499
+ setNativeProps(nativeProps: Record<string, unknown>): void {
500
+ this.hostNode?.setNativeProps(nativeProps);
501
+ }
502
+
461
503
  // The imperative API RN exposes on a TextInput ref; in Angular the component instance IS the
462
504
  // handle (a parent reads it via @ViewChild(TextInput) and calls these). focus/blur drive native
463
505
  // view commands; clear and setSelection reuse setTextAndSelection (the same stale-safe path as a
@@ -531,6 +573,20 @@ export class TextInput
531
573
  return {
532
574
  ...this.foldedAccessibility,
533
575
  accessible: this.accessible,
576
+ // NOT template bindings, unlike every other event on this component's two host tags. These
577
+ // four are `on*`-GATED Fabric events (`.claude/rules/fabric-boolean-event-gates.md`): native
578
+ // fires them only when the committed payload carries a FUNCTION at that key, and a template
579
+ // `(accessibilityAction)="..."` binding is present unconditionally — so it lit all four gates
580
+ // on every TextInput in every app whether or not anything listened. Routed through the flat
581
+ // bag instead, the same shape Pressable uses.
582
+ onAccessibilityAction: this.gatedAccessibilityHandler(
583
+ 'accessibilityAction',
584
+ ),
585
+ onAccessibilityTap: this.gatedAccessibilityHandler('accessibilityTap'),
586
+ onMagicTap: this.gatedAccessibilityHandler('magicTap'),
587
+ onAccessibilityEscape: this.gatedAccessibilityHandler(
588
+ 'accessibilityEscape',
589
+ ),
534
590
  testID: this.testID,
535
591
  nativeID: this.nativeID,
536
592
  placeholder: this.placeholder,
@@ -72,6 +72,13 @@ import {
72
72
  } from '../../primitives';
73
73
  import { Pressable, type IAngularPressableInputs } from '../pressable';
74
74
  import { Animated, AnimatedView } from '../../modules/animated';
75
+ import {
76
+ gateWanted,
77
+ injectGateDemandAbove,
78
+ provideGateDemand,
79
+ type IGateDemand,
80
+ type IGatedAccessibilityEvent,
81
+ } from '../../gate-demand';
75
82
 
76
83
  // The shared field base for all three: the Pressable INPUT surface (minus style, which each
77
84
  // Touchable routes differently, and minus the press/hover events, which each Touchable declares as
@@ -128,6 +135,7 @@ function createTimerScheduler(): ITimerScheduler {
128
135
  @Component({
129
136
  selector: 'TouchableOpacity',
130
137
  standalone: true,
138
+ viewProviders: [provideGateDemand(() => TouchableOpacity)],
131
139
  hostDirectives: [
132
140
  { directive: SymbioteStyleInputDirective, inputs: ['style'] },
133
141
  ],
@@ -136,6 +144,7 @@ function createTimerScheduler(): ITimerScheduler {
136
144
  changeDetection: ChangeDetectionStrategy.OnPush,
137
145
  template: `
138
146
  <Pressable
147
+ [__minPressDuration]="0"
139
148
  (press)="press.emit($event)"
140
149
  (pressIn)="handlePressIn($event)"
141
150
  (pressOut)="handlePressOut($event)"
@@ -224,6 +233,16 @@ export class TouchableOpacity
224
233
  @Output() readonly accessibilityTap = new EventEmitter<ISymbioteEvent>();
225
234
  @Output() readonly magicTap = new EventEmitter<ISymbioteEvent>();
226
235
  @Output() readonly accessibilityEscape = new EventEmitter<ISymbioteEvent>();
236
+
237
+ // This wrapper binds the four gated accessibility events on the component it renders, which
238
+ // Angular forces to be unconditional and which would light that component's gates on every
239
+ // instance. It answers for them instead — see `gate-demand.ts`.
240
+ private readonly gateDemandAbove = injectGateDemandAbove();
241
+
242
+ wantsGate(name: IGatedAccessibilityEvent): boolean {
243
+ return gateWanted(this.gateDemandAbove, name, this[name]);
244
+ }
245
+
227
246
  @Input() activeOpacity?: number;
228
247
  @Input() delayPressIn?: number;
229
248
  @Input() delayPressOut?: number;
@@ -417,6 +436,7 @@ export class TouchableOpacity
417
436
  @Component({
418
437
  selector: 'TouchableHighlight',
419
438
  standalone: true,
439
+ viewProviders: [provideGateDemand(() => TouchableHighlight)],
420
440
  hostDirectives: [
421
441
  { directive: SymbioteStyleInputDirective, inputs: ['style'] },
422
442
  ],
@@ -424,6 +444,7 @@ export class TouchableOpacity
424
444
  changeDetection: ChangeDetectionStrategy.OnPush,
425
445
  template: `
426
446
  <Pressable
447
+ [__minPressDuration]="0"
427
448
  [style]="containerStyle()"
428
449
  (press)="handlePress($event)"
429
450
  (pressIn)="handlePressIn($event)"
@@ -511,6 +532,16 @@ export class TouchableHighlight
511
532
  @Output() readonly accessibilityTap = new EventEmitter<ISymbioteEvent>();
512
533
  @Output() readonly magicTap = new EventEmitter<ISymbioteEvent>();
513
534
  @Output() readonly accessibilityEscape = new EventEmitter<ISymbioteEvent>();
535
+
536
+ // This wrapper binds the four gated accessibility events on the component it renders, which
537
+ // Angular forces to be unconditional and which would light that component's gates on every
538
+ // instance. It answers for them instead — see `gate-demand.ts`.
539
+ private readonly gateDemandAbove = injectGateDemandAbove();
540
+
541
+ wantsGate(name: IGatedAccessibilityEvent): boolean {
542
+ return gateWanted(this.gateDemandAbove, name, this[name]);
543
+ }
544
+
514
545
  // RN's onShowUnderlay / onHideUnderlay — fired on a real transition only, never on a repeat.
515
546
  @Output() readonly showUnderlay = new EventEmitter<void>();
516
547
  @Output() readonly hideUnderlay = new EventEmitter<void>();
@@ -693,6 +724,7 @@ export class TouchableHighlight
693
724
  @Component({
694
725
  selector: 'TouchableWithoutFeedback',
695
726
  standalone: true,
727
+ viewProviders: [provideGateDemand(() => TouchableWithoutFeedback)],
696
728
  hostDirectives: [
697
729
  { directive: SymbioteStyleInputDirective, inputs: ['style'] },
698
730
  ],
@@ -700,6 +732,7 @@ export class TouchableHighlight
700
732
  changeDetection: ChangeDetectionStrategy.OnPush,
701
733
  template: `
702
734
  <Pressable
735
+ [__minPressDuration]="0"
703
736
  [style]="mergedStyle"
704
737
  (press)="press.emit($event)"
705
738
  (pressIn)="handlePressIn($event)"
@@ -791,6 +824,16 @@ export class TouchableWithoutFeedback
791
824
  @Output() readonly accessibilityTap = new EventEmitter<ISymbioteEvent>();
792
825
  @Output() readonly magicTap = new EventEmitter<ISymbioteEvent>();
793
826
  @Output() readonly accessibilityEscape = new EventEmitter<ISymbioteEvent>();
827
+
828
+ // This wrapper binds the four gated accessibility events on the component it renders, which
829
+ // Angular forces to be unconditional and which would light that component's gates on every
830
+ // instance. It answers for them instead — see `gate-demand.ts`.
831
+ private readonly gateDemandAbove = injectGateDemandAbove();
832
+
833
+ wantsGate(name: IGatedAccessibilityEvent): boolean {
834
+ return gateWanted(this.gateDemandAbove, name, this[name]);
835
+ }
836
+
794
837
  @Input() delayPressIn?: number;
795
838
  @Input() delayPressOut?: number;
796
839
  @Input() minPressDuration?: number;
@@ -82,6 +82,7 @@ export type IAngularTouchableNativeFeedbackProps = Omit<
82
82
  changeDetection: ChangeDetectionStrategy.OnPush,
83
83
  template: `
84
84
  <Pressable
85
+ [__minPressDuration]="0"
85
86
  (press)="press.emit($event)"
86
87
  (pressIn)="pressIn.emit($event)"
87
88
  (pressOut)="pressOut.emit($event)"
@@ -100,13 +101,7 @@ export type IAngularTouchableNativeFeedbackProps = Omit<
100
101
  [android_ripple]="android_ripple"
101
102
  [android_disableSound]="android_disableSound"
102
103
  >
103
- <symbiote-view
104
- [symbioteHostProps]="hostProps()"
105
- (accessibilityAction)="emit(accessibilityAction, $event)"
106
- (accessibilityTap)="emit(accessibilityTap, $event)"
107
- (magicTap)="emit(magicTap, $event)"
108
- (accessibilityEscape)="emit(accessibilityEscape, $event)"
109
- >
104
+ <symbiote-view [symbioteHostProps]="hostProps()">
110
105
  <ng-content></ng-content>
111
106
  </symbiote-view>
112
107
  </Pressable>
@@ -262,14 +257,29 @@ export class TouchableNativeFeedback
262
257
  nativeID: this.nativeID,
263
258
  accessible: this.accessible,
264
259
  ...this.folded,
260
+ onAccessibilityAction: this.eventEmitterHandler(this.accessibilityAction),
261
+ onAccessibilityTap: this.eventEmitterHandler(this.accessibilityTap),
262
+ onMagicTap: this.eventEmitterHandler(this.magicTap),
263
+ onAccessibilityEscape: this.eventEmitterHandler(this.accessibilityEscape),
265
264
  };
266
265
  });
267
266
 
268
267
  // Forward an engine event to the matching @Output(), narrowing the template's untyped $event.
269
- emit(emitter: EventEmitter<ISymbioteEvent>, event: unknown): void {
268
+ private emit(emitter: EventEmitter<ISymbioteEvent>, event: unknown): void {
270
269
  if (isSymbioteEvent(event)) emitter.emit(event);
271
270
  }
272
271
 
272
+ // The four accessibility events are boolean-GATED Fabric events
273
+ // (`.claude/rules/fabric-boolean-event-gates.md`): native fires them only when the committed
274
+ // payload carries a FUNCTION at that key, so a template binding here lit the gate on every
275
+ // instance whether or not an app ever subscribed. `.observed`-gated, mirroring Pressable's
276
+ // `accessibilityEmitterHandler`.
277
+ private eventEmitterHandler(
278
+ emitter: EventEmitter<ISymbioteEvent>,
279
+ ): ((event: unknown) => void) | undefined {
280
+ return emitter.observed ? event => this.emit(emitter, event) : undefined;
281
+ }
282
+
273
283
  // Fold the web aria-*/role aliases into the canonical accessibility* props once per render, so the
274
284
  // host never sees an aria-* key (native ignores them), and merge `disabled` into the a11y state
275
285
  // (resolveDisabledAccessibilityState) the same way Pressable does for its own host.
@@ -127,6 +127,13 @@ export {
127
127
  VListItemDirective,
128
128
  VListSeparatorDirective,
129
129
  } from './directives';
130
+ import {
131
+ gateWanted,
132
+ injectGateDemandAbove,
133
+ provideGateDemand,
134
+ type IGateDemand,
135
+ type IGatedAccessibilityEvent,
136
+ } from '../../gate-demand';
130
137
  export type { IVListItemContext, IVListSeparatorContext } from './directives';
131
138
 
132
139
  // The Angular VirtualizedList prop surface. Mirrors React/Vue's IVirtualizedListProps MINUS the
@@ -213,6 +220,7 @@ interface IWindowCell<ItemT> {
213
220
  @Component({
214
221
  selector: 'VirtualizedList',
215
222
  standalone: true,
223
+ viewProviders: [provideGateDemand(() => VirtualizedList)],
216
224
  hostDirectives: [
217
225
  { directive: SymbioteStyleInputDirective, inputs: ['style'] },
218
226
  ],
@@ -381,6 +389,16 @@ export class VirtualizedList<ItemT = unknown>
381
389
  @Output() readonly accessibilityTap = new EventEmitter<ISymbioteEvent>();
382
390
  @Output() readonly magicTap = new EventEmitter<ISymbioteEvent>();
383
391
  @Output() readonly accessibilityEscape = new EventEmitter<ISymbioteEvent>();
392
+
393
+ // This wrapper binds the four gated accessibility events on the component it renders, which
394
+ // Angular forces to be unconditional and which would light that component's gates on every
395
+ // instance. It answers for them instead — see `gate-demand.ts`.
396
+ private readonly gateDemandAbove = injectGateDemandAbove();
397
+
398
+ wantsGate(name: IGatedAccessibilityEvent): boolean {
399
+ return gateWanted(this.gateDemandAbove, name, this[name]);
400
+ }
401
+
384
402
  @Input({ required: true }) data!: unknown;
385
403
  @Input({ required: true }) getItem!: (data: unknown, index: number) => ItemT;
386
404
  @Input({ required: true }) getItemCount!: (data: unknown) => number;
@@ -88,6 +88,13 @@ export {
88
88
  VSectionItemDirective,
89
89
  VSectionSeparatorDirective,
90
90
  } from './directives';
91
+ import {
92
+ gateWanted,
93
+ injectGateDemandAbove,
94
+ provideGateDemand,
95
+ type IGateDemand,
96
+ type IGatedAccessibilityEvent,
97
+ } from '../../gate-demand';
91
98
  export type { IVSectionContext, IVSectionItemContext } from './directives';
92
99
 
93
100
  // The Angular VirtualizedSectionList prop surface. Mirrors React/Vue's IVirtualizedSectionListProps
@@ -160,6 +167,7 @@ export type IVirtualizedSectionListInputs<ItemT> = Omit<
160
167
  @Component({
161
168
  selector: 'VirtualizedSectionList',
162
169
  standalone: true,
170
+ viewProviders: [provideGateDemand(() => VirtualizedSectionList)],
163
171
  hostDirectives: [
164
172
  { directive: SymbioteStyleInputDirective, inputs: ['style'] },
165
173
  ],
@@ -416,6 +424,16 @@ export class VirtualizedSectionList<ItemT = unknown>
416
424
  @Output() readonly accessibilityTap = new EventEmitter<ISymbioteEvent>();
417
425
  @Output() readonly magicTap = new EventEmitter<ISymbioteEvent>();
418
426
  @Output() readonly accessibilityEscape = new EventEmitter<ISymbioteEvent>();
427
+
428
+ // This wrapper binds the four gated accessibility events on the component it renders, which
429
+ // Angular forces to be unconditional and which would light that component's gates on every
430
+ // instance. It answers for them instead — see `gate-demand.ts`.
431
+ private readonly gateDemandAbove = injectGateDemandAbove();
432
+
433
+ wantsGate(name: IGatedAccessibilityEvent): boolean {
434
+ return gateWanted(this.gateDemandAbove, name, this[name]);
435
+ }
436
+
419
437
  @Input() role?: IAriaProps['role'];
420
438
  @Input() ariaLabel?: string;
421
439
  @Input() ariaLabelledBy?: string;
package/src/components.ts CHANGED
@@ -130,6 +130,8 @@ export {
130
130
  HorizontalScrollContentView,
131
131
  TextInputHost,
132
132
  MultilineTextInputHost,
133
+ ManagedTextInputHost,
134
+ ManagedMultilineTextInputHost,
133
135
  SwitchHost,
134
136
  ActivityIndicatorHost,
135
137
  SafeAreaViewHost,
@@ -20,16 +20,23 @@
20
20
  //
21
21
  // The safe, fully-public-API mechanism: create the embedded view DIRECTLY inside a
22
22
  // ViewContainerRef anchored at the destination, so there is nothing to move at all. That
23
- // ViewContainerRef comes from `PortalOutletDirective`, a marker placed on the target host
24
- // (`<View portalOutlet #overlayHost="portalOutlet">`) purely to expose its ViewContainerRef, the
25
- // same export-as-template-variable idiom `#form="ngForm"` uses. This also replaces the
26
- // `isSymbioteNode` runtime guard React/Vue need (there `to` is an arbitrary JS value until
27
- // validated): here `to` is typed as `PortalOutletDirective` the only way to construct one is
28
- // Angular's own template compiler resolving a template reference variable, so `strictTemplates`
29
- // rejects anything else at compile time and there is nothing left to guard at runtime.
23
+ // ViewContainerRef comes from `PortalOutletDirective`, exported to a template variable with the
24
+ // same idiom `#form="ngForm"` uses. This also replaces the `isSymbioteNode` runtime guard
25
+ // React/Vue need (there `to` is an arbitrary JS value until validated): here `to` is typed as
26
+ // `PortalOutletDirective` the only way to construct one is Angular's own template compiler
27
+ // resolving a template reference variable, so `strictTemplates` rejects anything else at compile
28
+ // time and there is nothing left to guard at runtime.
29
+ //
30
+ // THE MARKER GOES ON AN `<ng-container>` INSIDE THE TARGET, never on the target element. A
31
+ // ViewContainerRef anchors AT its host and `createEmbeddedView` inserts after that anchor, so
32
+ // `<View portalOutlet>` delivers a SIBLING where React's `createPortal(node, host)` and Vue's
33
+ // `<Teleport to>` deliver a child. Device-reported 2026-09-02: a card portaled into an absolutely
34
+ // positioned overlay laid out in the scroll flow, since the overlay cannot centre a node that is
35
+ // not in it.
30
36
 
31
37
  import {
32
38
  Directive,
39
+ ElementRef,
33
40
  inject,
34
41
  Input,
35
42
  TemplateRef,
@@ -38,10 +45,19 @@ import {
38
45
  type OnChanges,
39
46
  type OnDestroy,
40
47
  } from '@angular/core';
48
+ import { isAnchor, isSymbioteNode } from '@symbiote-native/engine';
41
49
 
42
50
  /** Marks the destination for `*portal` — place it on whichever already-mounted host should
43
- * paint the portaled content (e.g. a persistent overlay-host View near the app root), then
44
- * export it to a template variable and pass that variable as `*portal`'s target. */
51
+ * paint the portaled content on an `<ng-container>` INSIDE that host, never on the host
52
+ * element itself (see the file header) — then export it to a template variable and pass that
53
+ * variable as `*portal`'s target:
54
+ *
55
+ * ```html
56
+ * <View class="overlay-host">
57
+ * <ng-container portalOutlet #overlayHost="portalOutlet"></ng-container>
58
+ * </View>
59
+ * ```
60
+ */
45
61
  @Directive({
46
62
  selector: '[portalOutlet]',
47
63
  standalone: true,
@@ -49,6 +65,20 @@ import {
49
65
  })
50
66
  export class PortalOutletDirective {
51
67
  readonly viewContainerRef = inject(ViewContainerRef);
68
+
69
+ constructor() {
70
+ // The wrong placement commits a valid tree that simply differs from React's, so nothing
71
+ // downstream can catch it. It has to fail where it is written.
72
+ const host: unknown = inject(ElementRef).nativeElement;
73
+ if (!isSymbioteNode(host) || !isAnchor(host)) {
74
+ throw new Error(
75
+ 'portalOutlet must sit on an <ng-container> inside the target host, not on the host ' +
76
+ 'element: a ViewContainerRef anchored at an element inserts AFTER it, so the ported ' +
77
+ 'content would become the host\'s sibling. Write <View class="…">' +
78
+ '<ng-container portalOutlet #x="portalOutlet"></ng-container></View>.',
79
+ );
80
+ }
81
+ }
52
82
  }
53
83
 
54
84
  /** `*portal="overlayHost"` — renders the host element into whichever `PortalOutletDirective`