@symbiote-native/angular 0.7.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 (188) hide show
  1. package/README.md +3 -3
  2. package/babel-lower-host-primitives.cjs +636 -0
  3. package/babel-register-composed.cjs +21 -3
  4. package/build/angular/anchor-host-registry.js +3 -1
  5. package/build/angular/anchor-host-registry.js.map +1 -1
  6. package/build/angular/bootstrap.js +1 -1
  7. package/build/angular/bootstrap.js.map +1 -1
  8. package/build/angular/components/activity-indicator/index.android.js +7 -2
  9. package/build/angular/components/activity-indicator/index.android.js.map +1 -1
  10. package/build/angular/components/activity-indicator/index.ios.js +3 -1
  11. package/build/angular/components/activity-indicator/index.ios.js.map +1 -1
  12. package/build/angular/components/activity-indicator/shared.d.ts +1 -1
  13. package/build/angular/components/activity-indicator/shared.js +1 -1
  14. package/build/angular/components/activity-indicator/shared.js.map +1 -1
  15. package/build/angular/components/button.d.ts +3 -0
  16. package/build/angular/components/button.js +22 -5
  17. package/build/angular/components/button.js.map +1 -1
  18. package/build/angular/components/flat-list/index.d.ts +1 -1
  19. package/build/angular/components/flat-list/index.js +47 -14
  20. package/build/angular/components/flat-list/index.js.map +1 -1
  21. package/build/angular/components/image/index.android.js +4 -10
  22. package/build/angular/components/image/index.android.js.map +1 -1
  23. package/build/angular/components/image/index.ios.js +4 -10
  24. package/build/angular/components/image/index.ios.js.map +1 -1
  25. package/build/angular/components/image/shared.d.ts +4 -4
  26. package/build/angular/components/image/shared.js +51 -33
  27. package/build/angular/components/image/shared.js.map +1 -1
  28. package/build/angular/components/image-background.d.ts +3 -0
  29. package/build/angular/components/image-background.js +32 -9
  30. package/build/angular/components/image-background.js.map +1 -1
  31. package/build/angular/components/input-accessory-view/index.d.ts +2 -1
  32. package/build/angular/components/input-accessory-view/index.js +18 -16
  33. package/build/angular/components/input-accessory-view/index.js.map +1 -1
  34. package/build/angular/components/keyboard-avoiding-view/index.d.ts +3 -1
  35. package/build/angular/components/keyboard-avoiding-view/index.js +56 -19
  36. package/build/angular/components/keyboard-avoiding-view/index.js.map +1 -1
  37. package/build/angular/components/modal/index.d.ts +1 -0
  38. package/build/angular/components/modal/index.js +21 -10
  39. package/build/angular/components/modal/index.js.map +1 -1
  40. package/build/angular/components/pressable/index.d.ts +10 -5
  41. package/build/angular/components/pressable/index.js +61 -16
  42. package/build/angular/components/pressable/index.js.map +1 -1
  43. package/build/angular/components/refresh-control/index.d.ts +2 -1
  44. package/build/angular/components/refresh-control/index.js +13 -9
  45. package/build/angular/components/refresh-control/index.js.map +1 -1
  46. package/build/angular/components/safe-area-view/index.d.ts +3 -1
  47. package/build/angular/components/safe-area-view/index.js +22 -20
  48. package/build/angular/components/safe-area-view/index.js.map +1 -1
  49. package/build/angular/components/scroll-view/index.android.js +44 -14
  50. package/build/angular/components/scroll-view/index.android.js.map +1 -1
  51. package/build/angular/components/scroll-view/index.ios.js +13 -5
  52. package/build/angular/components/scroll-view/index.ios.js.map +1 -1
  53. package/build/angular/components/scroll-view/projection.d.ts +32 -0
  54. package/build/angular/components/scroll-view/projection.js +212 -47
  55. package/build/angular/components/scroll-view/projection.js.map +1 -1
  56. package/build/angular/components/scroll-view/shared.d.ts +2 -0
  57. package/build/angular/components/scroll-view/shared.js +48 -11
  58. package/build/angular/components/scroll-view/shared.js.map +1 -1
  59. package/build/angular/components/scroll-view/sticky-header.js +7 -3
  60. package/build/angular/components/scroll-view/sticky-header.js.map +1 -1
  61. package/build/angular/components/section-list/index.d.ts +9 -1
  62. package/build/angular/components/section-list/index.js +72 -18
  63. package/build/angular/components/section-list/index.js.map +1 -1
  64. package/build/angular/components/switch/index.android.js +23 -9
  65. package/build/angular/components/switch/index.android.js.map +1 -1
  66. package/build/angular/components/switch/index.ios.js +23 -9
  67. package/build/angular/components/switch/index.ios.js.map +1 -1
  68. package/build/angular/components/switch/shared.d.ts +1 -1
  69. package/build/angular/components/switch/shared.js +5 -2
  70. package/build/angular/components/switch/shared.js.map +1 -1
  71. package/build/angular/components/text-input.d.ts +8 -2
  72. package/build/angular/components/text-input.js +79 -34
  73. package/build/angular/components/text-input.js.map +1 -1
  74. package/build/angular/components/touchable/index.d.ts +36 -7
  75. package/build/angular/components/touchable/index.js +280 -72
  76. package/build/angular/components/touchable/index.js.map +1 -1
  77. package/build/angular/components/touchable-native-feedback/index.d.ts +2 -1
  78. package/build/angular/components/touchable-native-feedback/index.js +19 -17
  79. package/build/angular/components/touchable-native-feedback/index.js.map +1 -1
  80. package/build/angular/components/virtualized-list/directives.js +4 -0
  81. package/build/angular/components/virtualized-list/directives.js.map +1 -1
  82. package/build/angular/components/virtualized-list/index.d.ts +8 -2
  83. package/build/angular/components/virtualized-list/index.js +207 -61
  84. package/build/angular/components/virtualized-list/index.js.map +1 -1
  85. package/build/angular/components/virtualized-section-list/directives.js.map +1 -1
  86. package/build/angular/components/virtualized-section-list/index.d.ts +21 -1
  87. package/build/angular/components/virtualized-section-list/index.js +100 -22
  88. package/build/angular/components/virtualized-section-list/index.js.map +1 -1
  89. package/build/angular/components.d.ts +2 -2
  90. package/build/angular/components.js +1 -1
  91. package/build/angular/components.js.map +1 -1
  92. package/build/angular/create-portal/index.d.ts +11 -2
  93. package/build/angular/create-portal/index.js +42 -12
  94. package/build/angular/create-portal/index.js.map +1 -1
  95. package/build/angular/create-tunnel/index.js.map +1 -1
  96. package/build/angular/descriptor-to-angular/index.js +3 -1
  97. package/build/angular/descriptor-to-angular/index.js.map +1 -1
  98. package/build/angular/diagnostics.d.ts +33 -0
  99. package/build/angular/diagnostics.js +82 -0
  100. package/build/angular/diagnostics.js.map +1 -0
  101. package/build/angular/gate-demand.d.ts +37 -0
  102. package/build/angular/gate-demand.js +84 -0
  103. package/build/angular/gate-demand.js.map +1 -0
  104. package/build/angular/host-instance/index.js +1 -1
  105. package/build/angular/host-instance/index.js.map +1 -1
  106. package/build/angular/index.d.ts +6 -2
  107. package/build/angular/index.js +16 -1
  108. package/build/angular/index.js.map +1 -1
  109. package/build/angular/modules/animated/create-animated-component.d.ts +1 -0
  110. package/build/angular/modules/animated/create-animated-component.js +41 -18
  111. package/build/angular/modules/animated/create-animated-component.js.map +1 -1
  112. package/build/angular/modules/app-registry/index.js.map +1 -1
  113. package/build/angular/modules/status-bar/index.js +1 -1
  114. package/build/angular/modules/status-bar/index.js.map +1 -1
  115. package/build/angular/primitives/index.d.ts +31 -2
  116. package/build/angular/primitives/index.js +81 -3
  117. package/build/angular/primitives/index.js.map +1 -1
  118. package/build/angular/primitives/shared.d.ts +9 -1
  119. package/build/angular/primitives/shared.js +49 -3
  120. package/build/angular/primitives/shared.js.map +1 -1
  121. package/build/angular/register.d.ts +1 -0
  122. package/build/angular/register.js +35 -0
  123. package/build/angular/register.js.map +1 -0
  124. package/build/angular/render/index.js +51 -6
  125. package/build/angular/render/index.js.map +1 -1
  126. package/build/angular/renderer/index.d.ts +1 -0
  127. package/build/angular/renderer/index.js +157 -21
  128. package/build/angular/renderer/index.js.map +1 -1
  129. package/build/angular/services/color-scheme.service.js.map +1 -1
  130. package/build/angular/services/window-dimensions.service.js +1 -1
  131. package/build/angular/services/window-dimensions.service.js.map +1 -1
  132. package/build/angular/state-style.d.ts +9 -0
  133. package/build/angular/state-style.js +39 -0
  134. package/build/angular/state-style.js.map +1 -0
  135. package/metro-transformer.cjs +69 -0
  136. package/package.json +27 -8
  137. package/src/anchor-host-registry.ts +3 -1
  138. package/src/bootstrap.ts +4 -1
  139. package/src/components/activity-indicator/index.android.ts +7 -2
  140. package/src/components/activity-indicator/index.ios.ts +3 -1
  141. package/src/components/activity-indicator/shared.ts +32 -13
  142. package/src/components/button.ts +36 -6
  143. package/src/components/flat-list/index.ts +91 -29
  144. package/src/components/image/index.android.ts +9 -6
  145. package/src/components/image/index.ios.ts +9 -6
  146. package/src/components/image/shared.ts +110 -45
  147. package/src/components/image-background.ts +47 -10
  148. package/src/components/input-accessory-view/index.ts +41 -18
  149. package/src/components/keyboard-avoiding-view/index.ts +76 -22
  150. package/src/components/modal/index.ts +39 -12
  151. package/src/components/pressable/index.ts +121 -31
  152. package/src/components/refresh-control/index.ts +36 -13
  153. package/src/components/safe-area-view/index.ts +44 -19
  154. package/src/components/scroll-view/index.android.ts +31 -9
  155. package/src/components/scroll-view/index.ios.ts +17 -4
  156. package/src/components/scroll-view/projection.ts +290 -63
  157. package/src/components/scroll-view/shared.ts +124 -30
  158. package/src/components/scroll-view/sticky-header.ts +25 -8
  159. package/src/components/section-list/index.ts +79 -19
  160. package/src/components/switch/index.android.ts +18 -7
  161. package/src/components/switch/index.ios.ts +18 -7
  162. package/src/components/switch/shared.ts +15 -4
  163. package/src/components/text-input.ts +119 -31
  164. package/src/components/touchable/index.ts +339 -72
  165. package/src/components/touchable-native-feedback/index.ts +50 -18
  166. package/src/components/virtualized-list/directives.ts +19 -5
  167. package/src/components/virtualized-list/index.ts +292 -72
  168. package/src/components/virtualized-section-list/directives.ts +6 -3
  169. package/src/components/virtualized-section-list/index.ts +164 -34
  170. package/src/components.ts +6 -1
  171. package/src/create-portal/index.ts +47 -11
  172. package/src/create-tunnel/index.ts +3 -1
  173. package/src/descriptor-to-angular/index.ts +29 -8
  174. package/src/diagnostics.ts +138 -0
  175. package/src/gate-demand.ts +109 -0
  176. package/src/host-instance/index.ts +9 -3
  177. package/src/index.ts +33 -2
  178. package/src/modules/animated/create-animated-component.ts +67 -19
  179. package/src/modules/app-registry/index.ts +3 -1
  180. package/src/modules/status-bar/index.ts +13 -4
  181. package/src/primitives/index.ts +73 -2
  182. package/src/primitives/shared.ts +70 -9
  183. package/src/register.ts +43 -0
  184. package/src/render/index.ts +59 -7
  185. package/src/renderer/index.ts +209 -37
  186. package/src/services/color-scheme.service.ts +3 -1
  187. package/src/services/window-dimensions.service.ts +11 -4
  188. package/src/state-style.ts +39 -0
@@ -101,7 +101,9 @@ export type IAngularModalInputs = Omit<
101
101
  @Component({
102
102
  selector: 'Modal',
103
103
  standalone: true,
104
- hostDirectives: [{ directive: SymbioteStyleInputDirective, inputs: ['style'] }],
104
+ hostDirectives: [
105
+ { directive: SymbioteStyleInputDirective, inputs: ['style'] },
106
+ ],
105
107
  schemas: [CUSTOM_ELEMENTS_SCHEMA],
106
108
  imports: [ModalHost, ViewHost, SymbioteHostPropsDirective],
107
109
  changeDetection: ChangeDetectionStrategy.OnPush,
@@ -113,10 +115,6 @@ export type IAngularModalInputs = Omit<
113
115
  (dismiss)="dismiss.emit()"
114
116
  (requestClose)="requestClose.emit()"
115
117
  (orientationChange)="emit(orientationChange, $event)"
116
- (accessibilityAction)="emit(accessibilityAction, $event)"
117
- (accessibilityTap)="emit(accessibilityTap, $event)"
118
- (magicTap)="emit(magicTap, $event)"
119
- (accessibilityEscape)="emit(accessibilityEscape, $event)"
120
118
  >
121
119
  <symbiote-view [style]="containerStyle" [collapsable]="false">
122
120
  <ng-content></ng-content>
@@ -154,8 +152,10 @@ export class Modal implements IAngularModalInputs, OnInit, OnChanges, DoCheck {
154
152
  @Input() accessibilityValue?: IAccessibilityProps['accessibilityValue'];
155
153
  @Input() accessibilityActions?: IAccessibilityProps['accessibilityActions'];
156
154
  @Input() accessibilityLabelledBy?: string | string[];
157
- @Input() importantForAccessibility?: IAccessibilityProps['importantForAccessibility'];
158
- @Input() accessibilityLiveRegion?: IAccessibilityProps['accessibilityLiveRegion'];
155
+ @Input()
156
+ importantForAccessibility?: IAccessibilityProps['importantForAccessibility'];
157
+ @Input()
158
+ accessibilityLiveRegion?: IAccessibilityProps['accessibilityLiveRegion'];
159
159
  @Input() screenReaderFocusable?: boolean;
160
160
  @Input() accessibilityViewIsModal?: boolean;
161
161
  @Input() accessibilityElementsHidden?: boolean;
@@ -216,7 +216,10 @@ export class Modal implements IAngularModalInputs, OnInit, OnChanges, DoCheck {
216
216
  // Queue on a microtask so the reducer runs AFTER this CD pass renders with the OLD state —
217
217
  // the keep-alive frame. A synchronous dispatch here would unmount in the same pass.
218
218
  queueMicrotask(() => {
219
- this.state = modalReducer(this.state, isVisible ? { type: 'show' } : { type: 'hide' });
219
+ this.state = modalReducer(
220
+ this.state,
221
+ isVisible ? { type: 'show' } : { type: 'hide' },
222
+ );
220
223
  this.changeDetector.markForCheck();
221
224
  });
222
225
  }
@@ -269,7 +272,14 @@ export class Modal implements IAngularModalInputs, OnInit, OnChanges, DoCheck {
269
272
  readonly hostProps = computed<Record<string, unknown>>(() => {
270
273
  this.hostPropsRevision();
271
274
  const descriptorProps = this.descriptor.props;
272
- return { ...descriptorProps, style: [anchorHostStyle(this.elementRef), descriptorProps.style] };
275
+ return {
276
+ ...descriptorProps,
277
+ style: [anchorHostStyle(this.elementRef), descriptorProps.style],
278
+ onAccessibilityAction: this.eventEmitterHandler(this.accessibilityAction),
279
+ onAccessibilityTap: this.eventEmitterHandler(this.accessibilityTap),
280
+ onMagicTap: this.eventEmitterHandler(this.magicTap),
281
+ onAccessibilityEscape: this.eventEmitterHandler(this.accessibilityEscape),
282
+ };
273
283
  });
274
284
 
275
285
  get containerStyle(): unknown {
@@ -277,13 +287,28 @@ export class Modal implements IAngularModalInputs, OnInit, OnChanges, DoCheck {
277
287
  return typeof container === 'string' ? undefined : container.props.style;
278
288
  }
279
289
 
290
+ // NOT private: `(orientationChange)="emit(...)"` in the template above calls it, and a template
291
+ // can only reach public members. `tsc` does not typecheck Angular templates, so `private` here
292
+ // compiles clean and fails only under `ngc --compilationMode partial`.
280
293
  emit(emitter: EventEmitter<ISymbioteEvent>, event: unknown): void {
281
294
  if (isSymbioteEvent(event)) emitter.emit(event);
282
295
  }
283
296
 
297
+ // The four accessibility events are boolean-GATED Fabric events
298
+ // (`.claude/rules/fabric-boolean-event-gates.md`) — `show`/`dismiss`/`requestClose`/
299
+ // `orientationChange` are NOT (Modal's own MODAL_EVENTS), so those stay unconditional template
300
+ // bindings above. `.observed`-gated, mirroring Pressable's.
301
+ private eventEmitterHandler(
302
+ emitter: EventEmitter<ISymbioteEvent>,
303
+ ): ((event: unknown) => void) | undefined {
304
+ return emitter.observed ? event => this.emit(emitter, event) : undefined;
305
+ }
306
+
284
307
  // Typed as the a11y intersection WITH the string index (the bag renderModal spreads into the
285
308
  // host props), so resolveAccessibilityProps's result stays assignable to passthrough.
286
- private accessibilityInputs(): IAccessibilityProps & IAriaProps & Record<string, unknown> {
309
+ private accessibilityInputs(): IAccessibilityProps &
310
+ IAriaProps &
311
+ Record<string, unknown> {
287
312
  return {
288
313
  testID: this.testID,
289
314
  accessible: this.accessible,
@@ -301,8 +326,10 @@ export class Modal implements IAngularModalInputs, OnInit, OnChanges, DoCheck {
301
326
  accessibilityElementsHidden: this.accessibilityElementsHidden,
302
327
  accessibilityIgnoresInvertColors: this.accessibilityIgnoresInvertColors,
303
328
  accessibilityLanguage: this.accessibilityLanguage,
304
- accessibilityRespondsToUserInteraction: this.accessibilityRespondsToUserInteraction,
305
- accessibilityShowsLargeContentViewer: this.accessibilityShowsLargeContentViewer,
329
+ accessibilityRespondsToUserInteraction:
330
+ this.accessibilityRespondsToUserInteraction,
331
+ accessibilityShowsLargeContentViewer:
332
+ this.accessibilityShowsLargeContentViewer,
306
333
  accessibilityLargeContentTitle: this.accessibilityLargeContentTitle,
307
334
  role: this.role,
308
335
  'aria-label': this.ariaLabel,
@@ -13,11 +13,14 @@ import {
13
13
  ViewChild,
14
14
  type DoCheck,
15
15
  type OnChanges,
16
+ type OnDestroy,
16
17
  } from '@angular/core';
17
18
  import {
18
19
  createPressHandlers,
19
20
  createPressRuntime,
21
+ disposePressRuntime,
20
22
  DEFAULT_DELAY_LONG_PRESS_MS,
23
+ DEFAULT_MIN_PRESS_DURATION_MS,
21
24
  isTerminationAllowed,
22
25
  resolveAccessibilityProps,
23
26
  resolveDisabledAccessibilityState,
@@ -50,14 +53,23 @@ import {
50
53
  SymbioteStyleInputDirective,
51
54
  ViewHost,
52
55
  } from '../../primitives';
56
+ import {
57
+ gateWanted,
58
+ injectGateDemand,
59
+ type IGatedAccessibilityEvent,
60
+ } from '../../gate-demand';
53
61
 
54
- export type { IPressState, IPressableAndroidRippleConfig } from '@symbiote-native/components';
62
+ export type {
63
+ IPressState,
64
+ IPressableAndroidRippleConfig,
65
+ } from '@symbiote-native/components';
55
66
 
56
67
  // The full logical Pressable surface, callback-shaped press/hover events included. Touchable* and
57
68
  // TouchableNativeFeedback still take their OWN onPress/onPressIn/... as @Input() callbacks (that is
58
69
  // a separate, not-yet-converted decision) and derive their public prop types from this - so it keeps
59
70
  // the callback fields even though the Pressable component below no longer implements them directly.
60
- export interface IAngularPressableProps extends IAccessibilityProps, IAriaProps {
71
+ export interface IAngularPressableProps
72
+ extends IAccessibilityProps, IAriaProps {
61
73
  onPress?: IPressHandler;
62
74
  onPressIn?: IPressHandler;
63
75
  onPressOut?: IPressHandler;
@@ -83,7 +95,8 @@ export interface IAngularPressableProps extends IAccessibilityProps, IAriaProps
83
95
  nextFocusLeft?: number;
84
96
  nextFocusRight?: number;
85
97
  nextFocusUp?: number;
86
- style?: IStyleProp<IViewStyle> | ((state: IPressState) => IStyleProp<IViewStyle>);
98
+ style?:
99
+ IStyleProp<IViewStyle> | ((state: IPressState) => IStyleProp<IViewStyle>);
87
100
  }
88
101
 
89
102
  // What the Pressable component itself takes as plain @Input()s: the full surface minus the press/
@@ -105,7 +118,9 @@ export type IAngularPressableInputs = Omit<
105
118
  | 'onAccessibilityEscape'
106
119
  >;
107
120
 
108
- function isStyleFn(value: unknown): value is (state: IPressState) => IStyleProp<IViewStyle> {
121
+ function isStyleFn(
122
+ value: unknown,
123
+ ): value is (state: IPressState) => IStyleProp<IViewStyle> {
109
124
  return typeof value === 'function';
110
125
  }
111
126
 
@@ -116,19 +131,28 @@ function asSymbioteEvent(event: unknown): ISymbioteEvent | undefined {
116
131
  }
117
132
 
118
133
  @Component({
119
- selector: 'Pressable, symbiote-pressable',
134
+ // Single-name selector, matching TextInput's own ('TextInput', text-input.ts) rather than
135
+ // View/Text's dual one. The alternate `symbiote-pressable` spelling was vestigial — nothing in
136
+ // this component's own template ever renders that tag (it renders `symbiote-view`, below), and
137
+ // no app or example code invokes the composed component by that literal tag (verified by grep,
138
+ // 2026-08-31). Its only effect was to force `symbiote-pressable` into
139
+ // `ANCHOR_HOST_COMPONENTS` unconditionally, which collided with the tag's OTHER, load-bearing
140
+ // meaning — the host-behavior registry key (`PRESSABLE_TAG`,
141
+ // core/components/src/behaviors/pressable.ts) a lowered `<Pressable>` must carry to get the
142
+ // press machine. That collision is exactly why `DIAGNOSTIC_LOWERED_PRESSABLE_TAG` existed
143
+ // (renderer/index.ts) — dropping the vestigial spelling here removes the collision at its
144
+ // source, so the escape hatch is retired rather than worked around.
145
+ selector: 'Pressable',
120
146
  standalone: true,
121
- hostDirectives: [{ directive: SymbioteStyleInputDirective, inputs: ['style'] }],
147
+ hostDirectives: [
148
+ { directive: SymbioteStyleInputDirective, inputs: ['style'] },
149
+ ],
122
150
  schemas: [CUSTOM_ELEMENTS_SCHEMA],
123
151
  imports: [ViewHost, SymbioteHostPropsDirective],
124
152
  template: `
125
153
  <symbiote-view
126
154
  #host
127
155
  [symbioteHostProps]="hostProps()"
128
- (accessibilityAction)="emit(accessibilityAction, $event)"
129
- (accessibilityTap)="emit(accessibilityTap, $event)"
130
- (magicTap)="emit(magicTap, $event)"
131
- (accessibilityEscape)="emit(accessibilityEscape, $event)"
132
156
  (press)="handlePress($event)"
133
157
  (pressIn)="handlePressIn($event)"
134
158
  (pressOut)="handlePressOut($event)"
@@ -141,7 +165,9 @@ function asSymbioteEvent(event: unknown): ISymbioteEvent | undefined {
141
165
  `,
142
166
  changeDetection: ChangeDetectionStrategy.OnPush,
143
167
  })
144
- export class Pressable implements IAngularPressableInputs, OnChanges, DoCheck {
168
+ export class Pressable
169
+ implements IAngularPressableInputs, OnChanges, OnDestroy, DoCheck
170
+ {
145
171
  // The press/hover lifecycle as real Angular events: `(press)="onTap($event)"`, not
146
172
  // `[onPress]="onTap"`. createPressHandlers still wants plain IPressHandler callbacks, so
147
173
  // emitterHandler() below adapts each EventEmitter into one - only while something is actually
@@ -154,9 +180,15 @@ export class Pressable implements IAngularPressableInputs, OnChanges, DoCheck {
154
180
  @Output() readonly longPress = new EventEmitter<ISymbioteEvent>();
155
181
  @Output() readonly hoverIn = new EventEmitter<ISymbioteEvent>();
156
182
  @Output() readonly hoverOut = new EventEmitter<ISymbioteEvent>();
157
- // The four accessibility callbacks as real Angular events too, same tradeoff as press/hover
158
- // above: `(accessibilityAction)="emit(accessibilityAction, $event)"`, not
159
- // `[onAccessibilityAction]="onAccessibilityAction"`.
183
+ // The four accessibility callbacks as real Angular events, app-facing. UNLIKE press/hover above,
184
+ // these are NOT bound in the template at all — `accessibilityAction`/`accessibilityTap`/
185
+ // `magicTap`/`accessibilityEscape` are `onX`-gated Fabric events
186
+ // (`.claude/rules/fabric-boolean-event-gates.md`): the engine only emits them when the committed
187
+ // payload carries a FUNCTION at that key, so a `(accessibilityAction)="..."` template binding —
188
+ // always present once written — would light the gate on every instance regardless of whether the
189
+ // app ever subscribes. Routed through `hostProps()` instead (`accessibilityEmitterHandler`
190
+ // below), the same `.observed`-conditional shape `emitterHandler` already uses for press/hover,
191
+ // so the gate lights only when something is actually listening.
160
192
  @Output() readonly accessibilityAction = new EventEmitter<ISymbioteEvent>();
161
193
  @Output() readonly accessibilityTap = new EventEmitter<ISymbioteEvent>();
162
194
  @Output() readonly magicTap = new EventEmitter<ISymbioteEvent>();
@@ -167,6 +199,8 @@ export class Pressable implements IAngularPressableInputs, OnChanges, DoCheck {
167
199
  @Input() hitSlop?: IRectOffset;
168
200
  @Input() pressRetentionOffset?: IRectOffset;
169
201
  @Input() unstable_pressDelay?: number;
202
+ /** @internal Touchable* mirrors RN's Pressability minPressDuration: 0 override. */
203
+ @Input() __minPressDuration = DEFAULT_MIN_PRESS_DURATION_MS;
170
204
  @Input() android_ripple?: IPressableAndroidRippleConfig;
171
205
  @Input() android_disableSound?: boolean;
172
206
  @Input() delayHoverIn?: number;
@@ -186,10 +220,13 @@ export class Pressable implements IAngularPressableInputs, OnChanges, DoCheck {
186
220
  @Input() accessibilityRole?: IAngularPressableProps['accessibilityRole'];
187
221
  @Input() accessibilityState?: IAccessibilityStateValue;
188
222
  @Input() accessibilityValue?: IAngularPressableProps['accessibilityValue'];
189
- @Input() accessibilityActions?: IAngularPressableProps['accessibilityActions'];
223
+ @Input()
224
+ accessibilityActions?: IAngularPressableProps['accessibilityActions'];
190
225
  @Input() accessibilityLabelledBy?: string | string[];
191
- @Input() importantForAccessibility?: IAccessibilityProps['importantForAccessibility'];
192
- @Input() accessibilityLiveRegion?: IAccessibilityProps['accessibilityLiveRegion'];
226
+ @Input()
227
+ importantForAccessibility?: IAccessibilityProps['importantForAccessibility'];
228
+ @Input()
229
+ accessibilityLiveRegion?: IAccessibilityProps['accessibilityLiveRegion'];
193
230
  @Input() screenReaderFocusable?: boolean;
194
231
  @Input() accessibilityViewIsModal?: boolean;
195
232
  @Input() accessibilityElementsHidden?: boolean;
@@ -215,7 +252,8 @@ export class Pressable implements IAngularPressableInputs, OnChanges, DoCheck {
215
252
  @Input() id?: string;
216
253
  @Input() role?: IAngularPressableProps['role'];
217
254
 
218
- @ViewChild('host', { read: ElementRef }) private hostElement?: ElementRef<unknown>;
255
+ @ViewChild('host', { read: ElementRef })
256
+ private hostElement?: ElementRef<unknown>;
219
257
 
220
258
  // INTERNAL mutable state, driven by the press machine rather than by an @Input, so ngOnChanges
221
259
  // can never cover it - it carries its OWN signal, which is what lets the memoized bag below stay
@@ -242,9 +280,14 @@ export class Pressable implements IAngularPressableInputs, OnChanges, DoCheck {
242
280
  const id = setTimeout(callback, ms);
243
281
  return () => clearTimeout(id);
244
282
  },
283
+ now: Date.now,
245
284
  };
246
285
 
247
286
  private readonly changeDetector = inject(ChangeDetectorRef);
287
+ // Null unless an adapter wrapper renders this Pressable in its own template. See
288
+ // `gate-demand.ts`; an app's own `<Pressable>` — including one projected INTO a wrapper — gets
289
+ // null and falls back to its own `.observed`.
290
+ private readonly gateDemand = injectGateDemand();
248
291
  // This component's OWN host - the non-painting anchor `class="..."` at the use site resolves
249
292
  // onto (see anchorHostStyle's doc comment) - NOT `hostElement` above, which targets the real
250
293
  // inner `symbiote-view` one level down.
@@ -276,6 +319,10 @@ export class Pressable implements IAngularPressableInputs, OnChanges, DoCheck {
276
319
  this.anchorStyle.set(anchorHostStyle(this.elementRef));
277
320
  }
278
321
 
322
+ ngOnDestroy(): void {
323
+ disposePressRuntime(this.runtime);
324
+ }
325
+
279
326
  ngOnChanges(): void {
280
327
  // Tells the computed bag below that an @Input moved - plain class fields are not reactive on
281
328
  // their own. Must stay in ngOnChanges: that is the single moment Angular has finished writing
@@ -293,7 +340,10 @@ export class Pressable implements IAngularPressableInputs, OnChanges, DoCheck {
293
340
  accessibilityLabel: this.accessibilityLabel,
294
341
  accessibilityHint: this.accessibilityHint,
295
342
  accessibilityRole: this.accessibilityRole,
296
- accessibilityState: resolveDisabledAccessibilityState(this.accessibilityState, this.disabled),
343
+ accessibilityState: resolveDisabledAccessibilityState(
344
+ this.accessibilityState,
345
+ this.disabled,
346
+ ),
297
347
  accessibilityValue: this.accessibilityValue,
298
348
  accessibilityActions: this.accessibilityActions,
299
349
  accessibilityLabelledBy: this.accessibilityLabelledBy,
@@ -304,8 +354,10 @@ export class Pressable implements IAngularPressableInputs, OnChanges, DoCheck {
304
354
  accessibilityElementsHidden: this.accessibilityElementsHidden,
305
355
  accessibilityIgnoresInvertColors: this.accessibilityIgnoresInvertColors,
306
356
  accessibilityLanguage: this.accessibilityLanguage,
307
- accessibilityRespondsToUserInteraction: this.accessibilityRespondsToUserInteraction,
308
- accessibilityShowsLargeContentViewer: this.accessibilityShowsLargeContentViewer,
357
+ accessibilityRespondsToUserInteraction:
358
+ this.accessibilityRespondsToUserInteraction,
359
+ accessibilityShowsLargeContentViewer:
360
+ this.accessibilityShowsLargeContentViewer,
309
361
  accessibilityLargeContentTitle: this.accessibilityLargeContentTitle,
310
362
  'aria-label': this.ariaLabel,
311
363
  'aria-busy': this.ariaBusy,
@@ -327,7 +379,9 @@ export class Pressable implements IAngularPressableInputs, OnChanges, DoCheck {
327
379
  }
328
380
 
329
381
  get resolvedRippleProps(): Record<string, unknown> | undefined {
330
- return this.android_ripple === undefined ? undefined : rippleProps(this.android_ripple);
382
+ return this.android_ripple === undefined
383
+ ? undefined
384
+ : rippleProps(this.android_ripple);
331
385
  }
332
386
 
333
387
  // Flat bag for `[symbioteHostProps]` (Renderer2.setProperty per key) - see primitives/shared.ts.
@@ -351,33 +405,47 @@ export class Pressable implements IAngularPressableInputs, OnChanges, DoCheck {
351
405
  style: [this.anchorStyle(), this.resolvedStyle],
352
406
  accessible: this.accessible,
353
407
  ...this.foldedAccessibility,
408
+ onAccessibilityAction: this.accessibilityEmitterHandler(
409
+ 'accessibilityAction',
410
+ ),
411
+ onAccessibilityTap: this.accessibilityEmitterHandler('accessibilityTap'),
412
+ onMagicTap: this.accessibilityEmitterHandler('magicTap'),
413
+ onAccessibilityEscape: this.accessibilityEmitterHandler(
414
+ 'accessibilityEscape',
415
+ ),
354
416
  android_disableSound: this.android_disableSound,
355
- nativeBackgroundAndroid: this.resolvedRippleProps?.nativeBackgroundAndroid,
356
- nativeForegroundAndroid: this.resolvedRippleProps?.nativeForegroundAndroid,
417
+ nativeBackgroundAndroid:
418
+ this.resolvedRippleProps?.nativeBackgroundAndroid,
419
+ nativeForegroundAndroid:
420
+ this.resolvedRippleProps?.nativeForegroundAndroid,
357
421
  };
358
422
  });
359
423
 
360
424
  handlePressIn(event: unknown): void {
361
425
  const symbioteEvent = asSymbioteEvent(event);
362
- if (shouldSuppressPress(this.disabled) || symbioteEvent === undefined) return;
426
+ if (shouldSuppressPress(this.disabled) || symbioteEvent === undefined)
427
+ return;
363
428
  this.handlers.handlePressIn(symbioteEvent);
364
429
  }
365
430
 
366
431
  handlePressOut(event: unknown): void {
367
432
  const symbioteEvent = asSymbioteEvent(event);
368
- if (shouldSuppressPress(this.disabled) || symbioteEvent === undefined) return;
433
+ if (shouldSuppressPress(this.disabled) || symbioteEvent === undefined)
434
+ return;
369
435
  this.handlers.handlePressOut(symbioteEvent);
370
436
  }
371
437
 
372
438
  handlePress(event: unknown): void {
373
439
  const symbioteEvent = asSymbioteEvent(event);
374
- if (shouldSuppressPress(this.disabled) || symbioteEvent === undefined) return;
440
+ if (shouldSuppressPress(this.disabled) || symbioteEvent === undefined)
441
+ return;
375
442
  this.handlers.handlePress(symbioteEvent);
376
443
  }
377
444
 
378
445
  handleResponderMove(event: unknown): void {
379
446
  const symbioteEvent = asSymbioteEvent(event);
380
- if (shouldSuppressPress(this.disabled) || symbioteEvent === undefined) return;
447
+ if (shouldSuppressPress(this.disabled) || symbioteEvent === undefined)
448
+ return;
381
449
  this.handlers.handleResponderMove(symbioteEvent);
382
450
  }
383
451
 
@@ -389,7 +457,7 @@ export class Pressable implements IAngularPressableInputs, OnChanges, DoCheck {
389
457
  return isTerminationAllowed(this.cancelable);
390
458
  }
391
459
 
392
- emit(emitter: EventEmitter<ISymbioteEvent>, event: unknown): void {
460
+ private emit(emitter: EventEmitter<ISymbioteEvent>, event: unknown): void {
393
461
  const symbioteEvent = asSymbioteEvent(event);
394
462
  if (symbioteEvent !== undefined) emitter.emit(symbioteEvent);
395
463
  }
@@ -398,17 +466,38 @@ export class Pressable implements IAngularPressableInputs, OnChanges, DoCheck {
398
466
  // is listening anyway, mirroring core's noteHoverNoop but off `.observed` instead of an @Input.
399
467
  private noteHoverNoop(): void {
400
468
  if (this.hoverIn.observed || this.hoverOut.observed) {
401
- dlog('Pressable hover is a no-op on this host (no pointer-enter/leave event)');
469
+ dlog(
470
+ 'Pressable hover is a no-op on this host (no pointer-enter/leave event)',
471
+ );
402
472
  }
403
473
  }
404
474
 
405
475
  // Wraps an @Output() as an IPressHandler only while it has a subscriber, so an unbound
406
476
  // (longPress) still skips arming the timer, matching createPressHandlers' "undefined means
407
477
  // nobody cares" contract.
408
- private emitterHandler(emitter: EventEmitter<ISymbioteEvent>): IPressHandler | undefined {
478
+ private emitterHandler(
479
+ emitter: EventEmitter<ISymbioteEvent>,
480
+ ): IPressHandler | undefined {
409
481
  return emitter.observed ? event => emitter.emit(event) : undefined;
410
482
  }
411
483
 
484
+ // The accessibility twin of emitterHandler, feeding hostProps()'s flat bag instead of
485
+ // createPressHandlers' config — see the accessibilityAction @Output()'s own comment for why
486
+ // these four specifically cannot be plain template `(x)="..."` bindings. `undefined` here means
487
+ // `setEventListener` never sees a function, so the gated prop's flag never lights
488
+ // (`fabric-boolean-event-gates.md`, "The flag follows the VALUE, not the key").
489
+ private accessibilityEmitterHandler(
490
+ name: IGatedAccessibilityEvent,
491
+ ): ((event: unknown) => void) | undefined {
492
+ const emitter = this[name];
493
+ // A WRAPPER's binding is not a subscriber, and `.observed` cannot tell the two apart. When a
494
+ // demand is supplied from above it decides instead — see `gate-demand.ts` for why the channel
495
+ // is DI rather than an `@Input`.
496
+ return gateWanted(this.gateDemand, name, emitter)
497
+ ? event => this.emit(emitter, event)
498
+ : undefined;
499
+ }
500
+
412
501
  private get handlers() {
413
502
  this.noteHoverNoop();
414
503
  const config: IPressMachineConfig = {
@@ -419,6 +508,7 @@ export class Pressable implements IAngularPressableInputs, OnChanges, DoCheck {
419
508
  onLongPress: this.emitterHandler(this.longPress),
420
509
  delayLongPress: this.delayLongPress ?? DEFAULT_DELAY_LONG_PRESS_MS,
421
510
  unstable_pressDelay: this.unstable_pressDelay ?? 0,
511
+ minPressDuration: this.__minPressDuration,
422
512
  hitSlop: this.hitSlop,
423
513
  pressRetentionOffset: this.pressRetentionOffset,
424
514
  };
@@ -60,7 +60,8 @@ type IHostProps = Record<string, unknown>;
60
60
  // via <ng-content>), declared per-adapter over the shared accessibility base since the framework-specific
61
61
  // children slot keeps it from being fully shared across adapters. Read the React reference for the
62
62
  // native prop names.
63
- export interface IAngularRefreshControlProps extends IAccessibilityProps, IAriaProps {
63
+ export interface IAngularRefreshControlProps
64
+ extends IAccessibilityProps, IAriaProps {
64
65
  refreshing: boolean;
65
66
  // RN's onRefresh is `() => void | Promise<void>`, the handler may be async; the promise is
66
67
  // fire-and-forget (native already starts refreshing on the gesture).
@@ -97,7 +98,9 @@ export type IAngularRefreshControlInputs = Omit<
97
98
  @Component({
98
99
  selector: 'RefreshControl',
99
100
  standalone: true,
100
- hostDirectives: [{ directive: SymbioteStyleInputDirective, inputs: ['style'] }],
101
+ hostDirectives: [
102
+ { directive: SymbioteStyleInputDirective, inputs: ['style'] },
103
+ ],
101
104
  schemas: [CUSTOM_ELEMENTS_SCHEMA],
102
105
  imports: [RefreshControlHost, SymbioteHostPropsDirective],
103
106
  changeDetection: ChangeDetectionStrategy.OnPush,
@@ -106,17 +109,18 @@ export type IAngularRefreshControlInputs = Omit<
106
109
  #host
107
110
  [symbioteHostProps]="hostProps()"
108
111
  (refresh)="handleRefresh()"
109
- (accessibilityAction)="emit(accessibilityAction, $event)"
110
- (accessibilityTap)="emit(accessibilityTap, $event)"
111
- (magicTap)="emit(magicTap, $event)"
112
- (accessibilityEscape)="emit(accessibilityEscape, $event)"
113
112
  >
114
113
  <ng-content></ng-content>
115
114
  </symbiote-refresh-control>
116
115
  `,
117
116
  })
118
117
  export class RefreshControl
119
- implements IAngularRefreshControlInputs, OnInit, OnChanges, DoCheck, AfterViewInit
118
+ implements
119
+ IAngularRefreshControlInputs,
120
+ OnInit,
121
+ OnChanges,
122
+ DoCheck,
123
+ AfterViewInit
120
124
  {
121
125
  // Controlled prop the parent owns; required to match the React reference surface.
122
126
  @Input({ required: true }) refreshing!: boolean;
@@ -142,8 +146,10 @@ export class RefreshControl
142
146
  @Input() accessibilityValue?: IAccessibilityProps['accessibilityValue'];
143
147
  @Input() accessibilityActions?: IAccessibilityProps['accessibilityActions'];
144
148
  @Input() accessibilityLabelledBy?: string | string[];
145
- @Input() importantForAccessibility?: IAccessibilityProps['importantForAccessibility'];
146
- @Input() accessibilityLiveRegion?: IAccessibilityProps['accessibilityLiveRegion'];
149
+ @Input()
150
+ importantForAccessibility?: IAccessibilityProps['importantForAccessibility'];
151
+ @Input()
152
+ accessibilityLiveRegion?: IAccessibilityProps['accessibilityLiveRegion'];
147
153
  @Input() screenReaderFocusable?: boolean;
148
154
  @Input() accessibilityViewIsModal?: boolean;
149
155
  @Input() accessibilityElementsHidden?: boolean;
@@ -218,7 +224,9 @@ export class RefreshControl
218
224
  this.lastNativeRefreshing = this.refreshing;
219
225
  return;
220
226
  }
221
- this.syncNativeRefreshing(this.refreshNativeNode ?? this.host?.nativeElement);
227
+ this.syncNativeRefreshing(
228
+ this.refreshNativeNode ?? this.host?.nativeElement,
229
+ );
222
230
  }
223
231
 
224
232
  // Native starts the spinner before JS runs. Mirror RN's RefreshControl controlled-component
@@ -240,10 +248,19 @@ export class RefreshControl
240
248
 
241
249
  // Forward an engine event to the matching @Output(), narrowing the template's untyped $event
242
250
  // first. The accessibility* events arrive on the engine's structural event channel.
243
- emit(emitter: EventEmitter<ISymbioteEvent>, event: unknown): void {
251
+ private emit(emitter: EventEmitter<ISymbioteEvent>, event: unknown): void {
244
252
  if (isSymbioteEvent(event)) emitter.emit(event);
245
253
  }
246
254
 
255
+ // The four accessibility events are boolean-GATED Fabric events
256
+ // (`.claude/rules/fabric-boolean-event-gates.md`): a template binding here lit the gate on every
257
+ // instance whether or not an app ever subscribed. `.observed`-gated, mirroring Pressable's.
258
+ private eventEmitterHandler(
259
+ emitter: EventEmitter<ISymbioteEvent>,
260
+ ): ((event: unknown) => void) | undefined {
261
+ return emitter.observed ? event => this.emit(emitter, event) : undefined;
262
+ }
263
+
247
264
  // The anchor's class-derived style is NOT an @Input: `class="..."`/`[ngClass]` at the use site
248
265
  // resolves through the renderer's addClass onto this component's anchor host (see
249
266
  // anchorHostStyle's doc comment), so it never shows up in SimpleChanges and ngOnChanges alone
@@ -282,6 +299,10 @@ export class RefreshControl
282
299
  nativeID: this.nativeID,
283
300
  accessible: this.accessible,
284
301
  ...this.folded,
302
+ onAccessibilityAction: this.eventEmitterHandler(this.accessibilityAction),
303
+ onAccessibilityTap: this.eventEmitterHandler(this.accessibilityTap),
304
+ onMagicTap: this.eventEmitterHandler(this.magicTap),
305
+ onAccessibilityEscape: this.eventEmitterHandler(this.accessibilityEscape),
285
306
  };
286
307
  });
287
308
 
@@ -303,8 +324,10 @@ export class RefreshControl
303
324
  accessibilityElementsHidden: this.accessibilityElementsHidden,
304
325
  accessibilityIgnoresInvertColors: this.accessibilityIgnoresInvertColors,
305
326
  accessibilityLanguage: this.accessibilityLanguage,
306
- accessibilityRespondsToUserInteraction: this.accessibilityRespondsToUserInteraction,
307
- accessibilityShowsLargeContentViewer: this.accessibilityShowsLargeContentViewer,
327
+ accessibilityRespondsToUserInteraction:
328
+ this.accessibilityRespondsToUserInteraction,
329
+ accessibilityShowsLargeContentViewer:
330
+ this.accessibilityShowsLargeContentViewer,
308
331
  accessibilityLargeContentTitle: this.accessibilityLargeContentTitle,
309
332
  role: this.role,
310
333
  'aria-label': this.ariaLabel,