@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
@@ -1,4 +1,9 @@
1
1
  import { EventEmitter, computed, signal } from '@angular/core';
2
+ import {
3
+ gateWanted,
4
+ injectGateDemand,
5
+ type IGatedAccessibilityEvent,
6
+ } from '../../gate-demand';
2
7
  import {
3
8
  imageStatics,
4
9
  renderImage,
@@ -150,15 +155,29 @@ function asNumber(value: unknown): number | undefined {
150
155
  return typeof value === 'number' ? value : undefined;
151
156
  }
152
157
 
153
- function asCrossOrigin(value: unknown): 'anonymous' | 'use-credentials' | undefined {
154
- return value === 'anonymous' || value === 'use-credentials' ? value : undefined;
158
+ function asCrossOrigin(
159
+ value: unknown,
160
+ ): 'anonymous' | 'use-credentials' | undefined {
161
+ return value === 'anonymous' || value === 'use-credentials'
162
+ ? value
163
+ : undefined;
155
164
  }
156
165
 
157
166
  function asStyle(value: unknown): IStyleProp<IViewStyle> | undefined {
158
167
  return typeof value === 'object' && value !== null ? value : undefined;
159
168
  }
160
169
 
161
- export function resolveImageProps(input: Record<string, unknown>): Record<string, unknown> {
170
+ // Narrows a value out of the untyped animated prop bag. A runtime guard rather than a cast: the
171
+ // bag is built at runtime from merged animated values, so nothing upstream has proven the shape.
172
+ export function isImageEventCallback(
173
+ value: unknown,
174
+ ): value is (event: ISymbioteEvent) => void {
175
+ return typeof value === 'function';
176
+ }
177
+
178
+ export function resolveImageProps(
179
+ input: Record<string, unknown>,
180
+ ): Record<string, unknown> {
162
181
  const passthrough: IImagePassthroughProps = {
163
182
  resizeMethod: input['resizeMethod'] as IImageProps['resizeMethod'],
164
183
  blurRadius: asNumber(input['blurRadius']),
@@ -170,12 +189,21 @@ export function resolveImageProps(input: Record<string, unknown>): Record<string
170
189
  : undefined,
171
190
  testID: asString(input['testID']),
172
191
  nativeID: asString(input['nativeID']),
173
- accessible: typeof input['accessible'] === 'boolean' ? input['accessible'] : undefined,
192
+ accessible:
193
+ typeof input['accessible'] === 'boolean'
194
+ ? input['accessible']
195
+ : undefined,
174
196
  accessibilityLabel: asString(input['accessibilityLabel']),
175
197
  accessibilityHint: asString(input['accessibilityHint']),
176
- accessibilityRole: input['accessibilityRole'] as IAccessibilityProps['accessibilityRole'],
177
- accessibilityState: input['accessibilityState'] as IAccessibilityProps['accessibilityState'],
178
- accessibilityValue: input['accessibilityValue'] as IAccessibilityProps['accessibilityValue'],
198
+ accessibilityRole: input[
199
+ 'accessibilityRole'
200
+ ] as IAccessibilityProps['accessibilityRole'],
201
+ accessibilityState: input[
202
+ 'accessibilityState'
203
+ ] as IAccessibilityProps['accessibilityState'],
204
+ accessibilityValue: input[
205
+ 'accessibilityValue'
206
+ ] as IAccessibilityProps['accessibilityValue'],
179
207
  accessibilityActions: input[
180
208
  'accessibilityActions'
181
209
  ] as IAccessibilityProps['accessibilityActions'],
@@ -213,26 +241,48 @@ export function resolveImageProps(input: Record<string, unknown>): Record<string
213
241
  typeof input['accessibilityShowsLargeContentViewer'] === 'boolean'
214
242
  ? input['accessibilityShowsLargeContentViewer']
215
243
  : undefined,
216
- accessibilityLargeContentTitle: asString(input['accessibilityLargeContentTitle']),
244
+ accessibilityLargeContentTitle: asString(
245
+ input['accessibilityLargeContentTitle'],
246
+ ),
217
247
  role: input['role'] as IAriaProps['role'],
218
248
  'aria-label': asString(input['ariaLabel'] ?? input['aria-label']),
219
- 'aria-labelledby': asString(input['ariaLabelledBy'] ?? input['aria-labelledby']),
249
+ 'aria-labelledby': asString(
250
+ input['ariaLabelledBy'] ?? input['aria-labelledby'],
251
+ ),
220
252
  'aria-live': input['ariaLive'] as IAriaProps['aria-live'],
221
- 'aria-hidden': typeof input['ariaHidden'] === 'boolean' ? input['ariaHidden'] : undefined,
222
- 'aria-busy': typeof input['ariaBusy'] === 'boolean' ? input['ariaBusy'] : undefined,
253
+ 'aria-hidden':
254
+ typeof input['ariaHidden'] === 'boolean'
255
+ ? input['ariaHidden']
256
+ : undefined,
257
+ 'aria-busy':
258
+ typeof input['ariaBusy'] === 'boolean' ? input['ariaBusy'] : undefined,
223
259
  'aria-checked': input['ariaChecked'] as IAriaProps['aria-checked'],
224
- 'aria-disabled': typeof input['ariaDisabled'] === 'boolean' ? input['ariaDisabled'] : undefined,
225
- 'aria-expanded': typeof input['ariaExpanded'] === 'boolean' ? input['ariaExpanded'] : undefined,
226
- 'aria-selected': typeof input['ariaSelected'] === 'boolean' ? input['ariaSelected'] : undefined,
227
- 'aria-modal': typeof input['ariaModal'] === 'boolean' ? input['ariaModal'] : undefined,
260
+ 'aria-disabled':
261
+ typeof input['ariaDisabled'] === 'boolean'
262
+ ? input['ariaDisabled']
263
+ : undefined,
264
+ 'aria-expanded':
265
+ typeof input['ariaExpanded'] === 'boolean'
266
+ ? input['ariaExpanded']
267
+ : undefined,
268
+ 'aria-selected':
269
+ typeof input['ariaSelected'] === 'boolean'
270
+ ? input['ariaSelected']
271
+ : undefined,
272
+ 'aria-modal':
273
+ typeof input['ariaModal'] === 'boolean' ? input['ariaModal'] : undefined,
228
274
  'aria-valuemax': asNumber(input['ariaValueMax']),
229
275
  'aria-valuemin': asNumber(input['ariaValueMin']),
230
276
  'aria-valuenow': asNumber(input['ariaValueNow']),
231
277
  'aria-valuetext': asString(input['ariaValueText']),
232
- onAccessibilityAction: input['onAccessibilityAction'] as IImageProps['onLoad'],
278
+ onAccessibilityAction: input[
279
+ 'onAccessibilityAction'
280
+ ] as IImageProps['onLoad'],
233
281
  onAccessibilityTap: input['onAccessibilityTap'] as IImageProps['onLoad'],
234
282
  onMagicTap: input['onMagicTap'] as IImageProps['onLoad'],
235
- onAccessibilityEscape: input['onAccessibilityEscape'] as IImageProps['onLoad'],
283
+ onAccessibilityEscape: input[
284
+ 'onAccessibilityEscape'
285
+ ] as IImageProps['onLoad'],
236
286
  onLoadStart: input['onLoadStart'] as IImageProps['onLoadStart'],
237
287
  onLoad: input['onLoad'] as IImageProps['onLoad'],
238
288
  onLoadEnd: input['onLoadEnd'] as IImageProps['onLoadEnd'],
@@ -345,28 +395,32 @@ export abstract class ImageBase {
345
395
  readonly progress = new EventEmitter<ISymbioteEvent>();
346
396
  readonly partialLoad = new EventEmitter<ISymbioteEvent>();
347
397
 
348
- handleAccessibilityAction(event: Event): void {
349
- if (!isSymbioteEvent(event)) return;
350
- this.onAccessibilityAction?.(event);
351
- this.accessibilityAction.emit(event);
352
- }
353
-
354
- handleAccessibilityTap(event: Event): void {
355
- if (!isSymbioteEvent(event)) return;
356
- this.onAccessibilityTap?.(event);
357
- this.accessibilityTap.emit(event);
358
- }
359
-
360
- handleMagicTap(event: Event): void {
361
- if (!isSymbioteEvent(event)) return;
362
- this.onMagicTap?.(event);
363
- this.magicTap.emit(event);
364
- }
365
-
366
- handleAccessibilityEscape(event: Event): void {
367
- if (!isSymbioteEvent(event)) return;
368
- this.onAccessibilityEscape?.(event);
369
- this.accessibilityEscape.emit(event);
398
+ // Null unless an adapter wrapper renders this Image in its own template — ImageBackground does.
399
+ // An app's own `<Image>` gets null and answers from its own `.observed`.
400
+ private readonly gateDemand = injectGateDemand();
401
+
402
+ // The four accessibility events are boolean-GATED Fabric events
403
+ // (`.claude/rules/fabric-boolean-event-gates.md`): native fires them only when the committed
404
+ // payload carries a FUNCTION at that key. A template `(accessibilityAction)="..."` binding on the
405
+ // intrinsic host lights the gate unconditionally; this reaches the app two ways — a plain
406
+ // `[onAccessibilityAction]` @Input callback, or the `accessibilityAction` @Output — and the gate
407
+ // must light only when at least one of the two is actually wired. Returned `undefined` means
408
+ // `setEventListener` never sees a function, so the gate stays dark.
409
+ protected gatedAccessibilityHandler(
410
+ name: IGatedAccessibilityEvent,
411
+ callback: ((event: ISymbioteEvent) => void) | undefined,
412
+ ): ((event: Event) => void) | undefined {
413
+ const emitter = this[name];
414
+ // A WRAPPER's binding is not a subscriber, and `.observed` cannot tell the two apart.
415
+ // ImageBackground renders this component and binds all four, so without the demand every
416
+ // Image inside one lit its gates. See `gate-demand.ts`.
417
+ if (!gateWanted(this.gateDemand, name, emitter) && callback === undefined)
418
+ return undefined;
419
+ return event => {
420
+ if (!isSymbioteEvent(event)) return;
421
+ callback?.(event);
422
+ emitter.emit(event);
423
+ };
370
424
  }
371
425
 
372
426
  handleLoadStart(event: Event): void {
@@ -442,8 +496,10 @@ export abstract class ImageBase {
442
496
  accessibilityElementsHidden: this.accessibilityElementsHidden,
443
497
  accessibilityIgnoresInvertColors: this.accessibilityIgnoresInvertColors,
444
498
  accessibilityLanguage: this.accessibilityLanguage,
445
- accessibilityRespondsToUserInteraction: this.accessibilityRespondsToUserInteraction,
446
- accessibilityShowsLargeContentViewer: this.accessibilityShowsLargeContentViewer,
499
+ accessibilityRespondsToUserInteraction:
500
+ this.accessibilityRespondsToUserInteraction,
501
+ accessibilityShowsLargeContentViewer:
502
+ this.accessibilityShowsLargeContentViewer,
447
503
  accessibilityLargeContentTitle: this.accessibilityLargeContentTitle,
448
504
  role: this.role,
449
505
  ariaLabel: this.ariaLabel,
@@ -460,10 +516,19 @@ export abstract class ImageBase {
460
516
  ariaValueMin: this.ariaValueMin,
461
517
  ariaValueNow: this.ariaValueNow,
462
518
  ariaValueText: this.ariaValueText,
463
- onAccessibilityAction: this.onAccessibilityAction,
464
- onAccessibilityTap: this.onAccessibilityTap,
465
- onMagicTap: this.onMagicTap,
466
- onAccessibilityEscape: this.onAccessibilityEscape,
519
+ onAccessibilityAction: this.gatedAccessibilityHandler(
520
+ 'accessibilityAction',
521
+ this.onAccessibilityAction,
522
+ ),
523
+ onAccessibilityTap: this.gatedAccessibilityHandler(
524
+ 'accessibilityTap',
525
+ this.onAccessibilityTap,
526
+ ),
527
+ onMagicTap: this.gatedAccessibilityHandler('magicTap', this.onMagicTap),
528
+ onAccessibilityEscape: this.gatedAccessibilityHandler(
529
+ 'accessibilityEscape',
530
+ this.onAccessibilityEscape,
531
+ ),
467
532
  onLoadStart: this.onLoadStart,
468
533
  onLoad: this.onLoad,
469
534
  onLoadEnd: this.onLoadEnd,
@@ -37,12 +37,26 @@ import {
37
37
  type IViewStyle,
38
38
  } from '@symbiote-native/engine';
39
39
  import { Image } from './image';
40
- import { anchorHostStyle, SymbioteStyleInputDirective, ViewHost } from '../primitives';
40
+ import {
41
+ anchorHostStyle,
42
+ SymbioteStyleInputDirective,
43
+ ViewHost,
44
+ } from '../primitives';
45
+ import {
46
+ gateWanted,
47
+ injectGateDemandAbove,
48
+ provideGateDemand,
49
+ type IGateDemand,
50
+ type IGatedAccessibilityEvent,
51
+ } from '../gate-demand';
41
52
 
42
53
  // Mirrors React's IImageBackgroundProps minus children (Angular takes children via <ng-content>):
43
54
  // every forwarding Image prop flows onto the inner image; `style` is the WRAPPER View style and
44
55
  // `imageStyle` the inner image's.
45
- export interface IAngularImageBackgroundProps extends Omit<IImageProps, 'style'> {
56
+ export interface IAngularImageBackgroundProps extends Omit<
57
+ IImageProps,
58
+ 'style'
59
+ > {
46
60
  style?: IStyleProp<IViewStyle>;
47
61
  // A bare string resolves through the shared style registry, like `class` on the wrapper.
48
62
  imageStyle?: IStyleProp<IViewStyle> | string;
@@ -68,7 +82,10 @@ export type IAngularImageBackgroundInputs = Omit<
68
82
  @Component({
69
83
  selector: 'ImageBackground',
70
84
  standalone: true,
71
- hostDirectives: [{ directive: SymbioteStyleInputDirective, inputs: ['style'] }],
85
+ viewProviders: [provideGateDemand(() => ImageBackground)],
86
+ hostDirectives: [
87
+ { directive: SymbioteStyleInputDirective, inputs: ['style'] },
88
+ ],
72
89
  schemas: [CUSTOM_ELEMENTS_SCHEMA],
73
90
  imports: [Image, ViewHost],
74
91
  changeDetection: ChangeDetectionStrategy.OnPush,
@@ -108,10 +125,16 @@ export type IAngularImageBackgroundInputs = Omit<
108
125
  [screenReaderFocusable]="folded.screenReaderFocusable"
109
126
  [accessibilityViewIsModal]="folded.accessibilityViewIsModal"
110
127
  [accessibilityElementsHidden]="folded.accessibilityElementsHidden"
111
- [accessibilityIgnoresInvertColors]="folded.accessibilityIgnoresInvertColors"
128
+ [accessibilityIgnoresInvertColors]="
129
+ folded.accessibilityIgnoresInvertColors
130
+ "
112
131
  [accessibilityLanguage]="folded.accessibilityLanguage"
113
- [accessibilityRespondsToUserInteraction]="folded.accessibilityRespondsToUserInteraction"
114
- [accessibilityShowsLargeContentViewer]="folded.accessibilityShowsLargeContentViewer"
132
+ [accessibilityRespondsToUserInteraction]="
133
+ folded.accessibilityRespondsToUserInteraction
134
+ "
135
+ [accessibilityShowsLargeContentViewer]="
136
+ folded.accessibilityShowsLargeContentViewer
137
+ "
115
138
  [accessibilityLargeContentTitle]="folded.accessibilityLargeContentTitle"
116
139
  (loadStart)="loadStart.emit($event)"
117
140
  (load)="load.emit($event)"
@@ -135,6 +158,16 @@ export class ImageBackground implements IAngularImageBackgroundInputs {
135
158
  @Output() readonly accessibilityTap = new EventEmitter<ISymbioteEvent>();
136
159
  @Output() readonly magicTap = new EventEmitter<ISymbioteEvent>();
137
160
  @Output() readonly accessibilityEscape = new EventEmitter<ISymbioteEvent>();
161
+
162
+ // This wrapper binds the four gated accessibility events on the component it renders, which
163
+ // Angular forces to be unconditional and which would light that component's gates on every
164
+ // instance. It answers for them instead — see `gate-demand.ts`.
165
+ private readonly gateDemandAbove = injectGateDemandAbove();
166
+
167
+ wantsGate(name: IGatedAccessibilityEvent): boolean {
168
+ return gateWanted(this.gateDemandAbove, name, this[name]);
169
+ }
170
+
138
171
  @Output() readonly loadStart = new EventEmitter<ISymbioteEvent>();
139
172
  @Output() readonly load = new EventEmitter<ISymbioteEvent>();
140
173
  @Output() readonly loadEnd = new EventEmitter<ISymbioteEvent>();
@@ -170,8 +203,10 @@ export class ImageBackground implements IAngularImageBackgroundInputs {
170
203
  @Input() accessibilityValue?: IAccessibilityProps['accessibilityValue'];
171
204
  @Input() accessibilityActions?: IAccessibilityProps['accessibilityActions'];
172
205
  @Input() accessibilityLabelledBy?: string | string[];
173
- @Input() importantForAccessibility?: IAccessibilityProps['importantForAccessibility'];
174
- @Input() accessibilityLiveRegion?: IAccessibilityProps['accessibilityLiveRegion'];
206
+ @Input()
207
+ importantForAccessibility?: IAccessibilityProps['importantForAccessibility'];
208
+ @Input()
209
+ accessibilityLiveRegion?: IAccessibilityProps['accessibilityLiveRegion'];
175
210
  @Input() screenReaderFocusable?: boolean;
176
211
  @Input() accessibilityViewIsModal?: boolean;
177
212
  @Input() accessibilityElementsHidden?: boolean;
@@ -248,8 +283,10 @@ export class ImageBackground implements IAngularImageBackgroundInputs {
248
283
  accessibilityElementsHidden: this.accessibilityElementsHidden,
249
284
  accessibilityIgnoresInvertColors: this.accessibilityIgnoresInvertColors,
250
285
  accessibilityLanguage: this.accessibilityLanguage,
251
- accessibilityRespondsToUserInteraction: this.accessibilityRespondsToUserInteraction,
252
- accessibilityShowsLargeContentViewer: this.accessibilityShowsLargeContentViewer,
286
+ accessibilityRespondsToUserInteraction:
287
+ this.accessibilityRespondsToUserInteraction,
288
+ accessibilityShowsLargeContentViewer:
289
+ this.accessibilityShowsLargeContentViewer,
253
290
  accessibilityLargeContentTitle: this.accessibilityLargeContentTitle,
254
291
  role: this.role,
255
292
  'aria-label': this.ariaLabel,
@@ -43,7 +43,8 @@ import {
43
43
 
44
44
  // Mirrors React's IInputAccessoryViewProps minus children (Angular takes children via
45
45
  // <ng-content>), declared per-adapter over the shared a11y base.
46
- export interface IAngularInputAccessoryViewProps extends IAccessibilityProps, IAriaProps {
46
+ export interface IAngularInputAccessoryViewProps
47
+ extends IAccessibilityProps, IAriaProps {
47
48
  // The id a TextInput's inputAccessoryViewID points at to dock above its keyboard.
48
49
  nativeID?: string;
49
50
  backgroundColor?: string;
@@ -55,29 +56,30 @@ export interface IAngularInputAccessoryViewProps extends IAccessibilityProps, IA
55
56
  // mirroring Pressable's IAngularPressableInputs split.
56
57
  export type IAngularInputAccessoryViewInputs = Omit<
57
58
  IAngularInputAccessoryViewProps,
58
- 'onAccessibilityAction' | 'onAccessibilityTap' | 'onMagicTap' | 'onAccessibilityEscape'
59
+ | 'onAccessibilityAction'
60
+ | 'onAccessibilityTap'
61
+ | 'onMagicTap'
62
+ | 'onAccessibilityEscape'
59
63
  >;
60
64
 
61
65
  @Component({
62
66
  selector: 'InputAccessoryView',
63
67
  standalone: true,
64
- hostDirectives: [{ directive: SymbioteStyleInputDirective, inputs: ['style'] }],
68
+ hostDirectives: [
69
+ { directive: SymbioteStyleInputDirective, inputs: ['style'] },
70
+ ],
65
71
  schemas: [CUSTOM_ELEMENTS_SCHEMA],
66
72
  imports: [InputAccessoryViewHost, SymbioteHostPropsDirective],
67
73
  changeDetection: ChangeDetectionStrategy.OnPush,
68
74
  template: `
69
- <symbiote-input-accessory-view
70
- [symbioteHostProps]="hostProps()"
71
- (accessibilityAction)="emit(accessibilityAction, $event)"
72
- (accessibilityTap)="emit(accessibilityTap, $event)"
73
- (magicTap)="emit(magicTap, $event)"
74
- (accessibilityEscape)="emit(accessibilityEscape, $event)"
75
- >
75
+ <symbiote-input-accessory-view [symbioteHostProps]="hostProps()">
76
76
  <ng-content></ng-content>
77
77
  </symbiote-input-accessory-view>
78
78
  `,
79
79
  })
80
- export class InputAccessoryView implements IAngularInputAccessoryViewInputs, OnChanges, DoCheck {
80
+ export class InputAccessoryView
81
+ implements IAngularInputAccessoryViewInputs, OnChanges, DoCheck
82
+ {
81
83
  @Input() nativeID?: string;
82
84
  @Input() backgroundColor?: string;
83
85
  @Input() style?: IStyleProp<IViewStyle>;
@@ -94,8 +96,10 @@ export class InputAccessoryView implements IAngularInputAccessoryViewInputs, OnC
94
96
  @Input() accessibilityValue?: IAccessibilityProps['accessibilityValue'];
95
97
  @Input() accessibilityActions?: IAccessibilityProps['accessibilityActions'];
96
98
  @Input() accessibilityLabelledBy?: string | string[];
97
- @Input() importantForAccessibility?: IAccessibilityProps['importantForAccessibility'];
98
- @Input() accessibilityLiveRegion?: IAccessibilityProps['accessibilityLiveRegion'];
99
+ @Input()
100
+ importantForAccessibility?: IAccessibilityProps['importantForAccessibility'];
101
+ @Input()
102
+ accessibilityLiveRegion?: IAccessibilityProps['accessibilityLiveRegion'];
99
103
  @Input() screenReaderFocusable?: boolean;
100
104
  @Input() accessibilityViewIsModal?: boolean;
101
105
  @Input() accessibilityElementsHidden?: boolean;
@@ -126,10 +130,18 @@ export class InputAccessoryView implements IAngularInputAccessoryViewInputs, OnC
126
130
  private readonly elementRef = inject(ElementRef);
127
131
 
128
132
  // Forward an engine event to the matching @Output(), narrowing the template's untyped $event.
129
- emit(emitter: EventEmitter<ISymbioteEvent>, event: unknown): void {
133
+ private emit(emitter: EventEmitter<ISymbioteEvent>, event: unknown): void {
130
134
  if (isSymbioteEvent(event)) emitter.emit(event);
131
135
  }
132
136
 
137
+ // The four accessibility events are boolean-GATED Fabric events
138
+ // (`.claude/rules/fabric-boolean-event-gates.md`). `.observed`-gated, mirroring Pressable's.
139
+ private eventEmitterHandler(
140
+ emitter: EventEmitter<ISymbioteEvent>,
141
+ ): ((event: unknown) => void) | undefined {
142
+ return emitter.observed ? event => this.emit(emitter, event) : undefined;
143
+ }
144
+
133
145
  // renderInputAccessoryView owns the host-node assembly (shared with React/Vue); the adapter reads
134
146
  // the resolved host props off the Descriptor it returns.
135
147
  private get descriptor() {
@@ -177,12 +189,21 @@ export class InputAccessoryView implements IAngularInputAccessoryViewInputs, OnC
177
189
  readonly hostProps = computed<Record<string, unknown>>(() => {
178
190
  this.hostPropsRevision();
179
191
  const descriptorProps = this.descriptor.props;
180
- return { ...descriptorProps, style: [anchorHostStyle(this.elementRef), descriptorProps.style] };
192
+ return {
193
+ ...descriptorProps,
194
+ style: [anchorHostStyle(this.elementRef), descriptorProps.style],
195
+ onAccessibilityAction: this.eventEmitterHandler(this.accessibilityAction),
196
+ onAccessibilityTap: this.eventEmitterHandler(this.accessibilityTap),
197
+ onMagicTap: this.eventEmitterHandler(this.magicTap),
198
+ onAccessibilityEscape: this.eventEmitterHandler(this.accessibilityEscape),
199
+ };
181
200
  });
182
201
 
183
202
  // Typed as the a11y intersection WITH the string index (the bag renderInputAccessoryView spreads
184
203
  // into the host props), so resolveAccessibilityProps's result stays assignable to passthrough.
185
- private accessibilityInputs(): IAccessibilityProps & IAriaProps & Record<string, unknown> {
204
+ private accessibilityInputs(): IAccessibilityProps &
205
+ IAriaProps &
206
+ Record<string, unknown> {
186
207
  return {
187
208
  testID: this.testID,
188
209
  accessible: this.accessible,
@@ -200,8 +221,10 @@ export class InputAccessoryView implements IAngularInputAccessoryViewInputs, OnC
200
221
  accessibilityElementsHidden: this.accessibilityElementsHidden,
201
222
  accessibilityIgnoresInvertColors: this.accessibilityIgnoresInvertColors,
202
223
  accessibilityLanguage: this.accessibilityLanguage,
203
- accessibilityRespondsToUserInteraction: this.accessibilityRespondsToUserInteraction,
204
- accessibilityShowsLargeContentViewer: this.accessibilityShowsLargeContentViewer,
224
+ accessibilityRespondsToUserInteraction:
225
+ this.accessibilityRespondsToUserInteraction,
226
+ accessibilityShowsLargeContentViewer:
227
+ this.accessibilityShowsLargeContentViewer,
205
228
  accessibilityLargeContentTitle: this.accessibilityLargeContentTitle,
206
229
  role: this.role,
207
230
  'aria-label': this.ariaLabel,
@@ -2,7 +2,7 @@
2
2
  // keyboard's way as it shows/hides. The inset math + the behavior → style/structure decision
3
3
  // live framework-agnostic in @symbiote-native/components (render-keyboard-avoiding-view), shared verbatim
4
4
  // with React/Vue; Angular supplies only the lifecycle: a plain inset field, ngOnInit subscribes to
5
- // the core Keyboard module (show / changeFrame / hide) and markForCheck pulls the OnPush view (the
5
+ // the core Keyboard module (the host's show / hide pair) and markForCheck pulls the OnPush view (the
6
6
  // Angular twin of React's setState / Vue's reactive ref), ngOnDestroy tears the subscriptions down,
7
7
  // and the wrapper's onLayout measures the frame that feeds the next event's inset. The user
8
8
  // children nest under the wrapper (or, for 'position', an inner View) via <ng-content>. No native
@@ -26,8 +26,10 @@ import {
26
26
  } from '@angular/core';
27
27
  import {
28
28
  computeInset,
29
+ keyboardAvoidingEventNamesFor,
29
30
  readKeyboardFrame,
30
31
  readLayoutFrame,
32
+ readPrefersCrossFadeTransitions,
31
33
  resolveAccessibilityProps,
32
34
  resolveKeyboardAvoidingLayout,
33
35
  DEFAULT_VERTICAL_OFFSET,
@@ -40,7 +42,7 @@ import {
40
42
  } from '@symbiote-native/components';
41
43
  import {
42
44
  Keyboard,
43
- KEYBOARD_EVENT,
45
+ Platform,
44
46
  dlog,
45
47
  isSymbioteEvent,
46
48
  type IEventSubscription,
@@ -60,7 +62,8 @@ export type { IKeyboardAvoidingBehavior } from '@symbiote-native/components';
60
62
  // Mirrors React's IKeyboardAvoidingViewProps minus children (Angular takes children via
61
63
  // <ng-content>), declared per-adapter over the shared accessibility base since a framework-specific
62
64
  // children field keeps it from being fully shared across adapters.
63
- export interface IAngularKeyboardAvoidingViewProps extends IAccessibilityProps, IAriaProps {
65
+ export interface IAngularKeyboardAvoidingViewProps
66
+ extends IAccessibilityProps, IAriaProps {
64
67
  behavior?: IKeyboardAvoidingBehavior;
65
68
  enabled?: boolean;
66
69
  keyboardVerticalOffset?: number;
@@ -84,20 +87,24 @@ export type IAngularKeyboardAvoidingViewInputs = Omit<
84
87
  @Component({
85
88
  selector: 'KeyboardAvoidingView',
86
89
  standalone: true,
87
- hostDirectives: [{ directive: SymbioteStyleInputDirective, inputs: ['style'] }],
90
+ hostDirectives: [
91
+ { directive: SymbioteStyleInputDirective, inputs: ['style'] },
92
+ ],
88
93
  schemas: [CUSTOM_ELEMENTS_SCHEMA],
89
94
  imports: [ViewHost, SymbioteHostPropsDirective],
90
95
  changeDetection: ChangeDetectionStrategy.OnPush,
91
96
  // The wrapper carries a11y + onLayout; 'position' ('nested') pushes the children in an inner
92
97
  // View by `bottom: inset`, the wrapper modes adjust the single wrapper directly. Only one @if
93
98
  // branch is instantiated, so each branch's <ng-content> projects the children unambiguously.
99
+ // `layout` stays a plain, unconditional template binding: unlike the four accessibility events
100
+ // below, this component READS its own onLayout internally (handleLayout measures the frame that
101
+ // feeds the next keyboard event's inset math — see keyboard-avoiding-view-rn-contract.md), so the
102
+ // gate must stay lit regardless of whether the app subscribes to the `layout` @Output(). Gating it
103
+ // on `.observed` would silently break the inset fixpoint correction the moment an app didn't
104
+ // listen to `layout`.
94
105
  template: `
95
106
  <symbiote-view
96
107
  [symbioteHostProps]="hostProps"
97
- (accessibilityAction)="emit(accessibilityAction, $event)"
98
- (accessibilityTap)="emit(accessibilityTap, $event)"
99
- (magicTap)="emit(magicTap, $event)"
100
- (accessibilityEscape)="emit(accessibilityEscape, $event)"
101
108
  (layout)="handleLayout($event)"
102
109
  >
103
110
  @if (isNested) {
@@ -110,7 +117,9 @@ export type IAngularKeyboardAvoidingViewInputs = Omit<
110
117
  </symbiote-view>
111
118
  `,
112
119
  })
113
- export class KeyboardAvoidingView implements IAngularKeyboardAvoidingViewInputs, OnInit, OnDestroy {
120
+ export class KeyboardAvoidingView
121
+ implements IAngularKeyboardAvoidingViewInputs, OnInit, OnDestroy
122
+ {
114
123
  @Input() behavior?: IKeyboardAvoidingBehavior;
115
124
  @Input() enabled?: boolean;
116
125
  @Input() keyboardVerticalOffset?: number;
@@ -135,8 +144,10 @@ export class KeyboardAvoidingView implements IAngularKeyboardAvoidingViewInputs,
135
144
  @Input() accessibilityValue?: IAccessibilityProps['accessibilityValue'];
136
145
  @Input() accessibilityActions?: IAccessibilityProps['accessibilityActions'];
137
146
  @Input() accessibilityLabelledBy?: string | string[];
138
- @Input() importantForAccessibility?: IAccessibilityProps['importantForAccessibility'];
139
- @Input() accessibilityLiveRegion?: IAccessibilityProps['accessibilityLiveRegion'];
147
+ @Input()
148
+ importantForAccessibility?: IAccessibilityProps['importantForAccessibility'];
149
+ @Input()
150
+ accessibilityLiveRegion?: IAccessibilityProps['accessibilityLiveRegion'];
140
151
  @Input() screenReaderFocusable?: boolean;
141
152
  @Input() accessibilityViewIsModal?: boolean;
142
153
  @Input() accessibilityElementsHidden?: boolean;
@@ -168,6 +179,12 @@ export class KeyboardAvoidingView implements IAngularKeyboardAvoidingViewInputs,
168
179
  // keyboard event's inset math (React's frameRef / initialHeightRef, Vue's frame / initialHeight).
169
180
  private frame?: IMeasuredFrame;
170
181
  private initialHeight?: number;
182
+ // The iOS "Prefer Cross-Fade Transitions" setting, read once per mount. Deliberately a plain
183
+ // field and NOT markForCheck'd when the promise lands: it is a device setting that cannot change
184
+ // mid-session, and it feeds only the NEXT keyboard event's math — never the current render — so
185
+ // under zoneless CD there is nothing to repaint at resolve time. The keyboard event that does use
186
+ // it already calls markForCheck itself.
187
+ private prefersCrossFadeTransitions = false;
171
188
  private subscriptions: IEventSubscription[] = [];
172
189
 
173
190
  private readonly changeDetector = inject(ChangeDetectorRef);
@@ -177,11 +194,21 @@ export class KeyboardAvoidingView implements IAngularKeyboardAvoidingViewInputs,
177
194
  private readonly elementRef = inject(ElementRef);
178
195
 
179
196
  ngOnInit(): void {
197
+ // TWO events, picked per host, never three: iOS takes the will* pair so the view rides up with
198
+ // the keyboard animation, Android the did* pair. The change-frame notification is deliberately
199
+ // absent — with an undocked/split/floating iOS keyboard it fires BEFORE the hide, so listening
200
+ // to it applies a frame captured mid-dismissal (RN KeyboardAvoidingView.js's own comment).
201
+ const events = keyboardAvoidingEventNamesFor(Platform.OS);
180
202
  this.subscriptions = [
181
- Keyboard.addListener(KEYBOARD_EVENT.didShow, payload => this.onShow(payload)),
182
- Keyboard.addListener(KEYBOARD_EVENT.didChangeFrame, payload => this.onShow(payload)),
183
- Keyboard.addListener(KEYBOARD_EVENT.didHide, () => this.onHide()),
203
+ Keyboard.addListener(events.show, payload => this.onShow(payload)),
204
+ Keyboard.addListener(events.hide, () => this.onHide()),
184
205
  ];
206
+ // The core wrapper, not AccessibilityInfo directly: the engine's iOS getter REJECTS on a
207
+ // native error (RN parity), and nobody awaits this read, so an unwrapped call would surface
208
+ // as an unhandled rejection. The wrapper answers false on a failed read.
209
+ void readPrefersCrossFadeTransitions().then(enabled => {
210
+ this.prefersCrossFadeTransitions = enabled;
211
+ });
185
212
  }
186
213
 
187
214
  ngOnDestroy(): void {
@@ -192,7 +219,17 @@ export class KeyboardAvoidingView implements IAngularKeyboardAvoidingViewInputs,
192
219
  private onShow(payload: unknown): void {
193
220
  const keyboard = readKeyboardFrame(payload);
194
221
  const offset = this.keyboardVerticalOffset ?? DEFAULT_VERTICAL_OFFSET;
195
- const next = computeInset(this.frame, keyboard, offset);
222
+ const next = computeInset(this.frame, keyboard, offset, {
223
+ // Read off the field at event time, not captured when the subscription was built: `behavior`
224
+ // is an @Input that can change under a long-lived subscription, and a stale one keeps
225
+ // applying the old mode's math (it gates the previous-inset correction below).
226
+ behavior: this.behavior,
227
+ // The inset CURRENTLY applied (RN's this.state.bottom), read off the field at event time so
228
+ // 'height' mode's fixpoint correction sees the live value: that mode SHRINKS the wrapper by
229
+ // the inset, so the next onLayout reports a frame shorter by exactly that much.
230
+ previousInset: this.inset,
231
+ prefersCrossFadeTransitions: this.prefersCrossFadeTransitions,
232
+ });
196
233
  dlog(`KeyboardAvoidingView show -> inset ${next}`);
197
234
  this.inset = next;
198
235
  this.changeDetector.markForCheck();
@@ -211,7 +248,8 @@ export class KeyboardAvoidingView implements IAngularKeyboardAvoidingViewInputs,
211
248
  const measured = readLayoutFrame(event.nativeEvent.layout);
212
249
  if (measured !== undefined) {
213
250
  this.frame = measured;
214
- if (this.initialHeight === undefined) this.initialHeight = measured.height;
251
+ if (this.initialHeight === undefined)
252
+ this.initialHeight = measured.height;
215
253
  }
216
254
  this.layout.emit(event);
217
255
  }
@@ -244,7 +282,9 @@ export class KeyboardAvoidingView implements IAngularKeyboardAvoidingViewInputs,
244
282
  }
245
283
 
246
284
  get innerStyle(): IStyleProp<IViewStyle> | undefined {
247
- return this.resolvedLayout.kind === 'nested' ? this.resolvedLayout.innerStyle : undefined;
285
+ return this.resolvedLayout.kind === 'nested'
286
+ ? this.resolvedLayout.innerStyle
287
+ : undefined;
248
288
  }
249
289
 
250
290
  // The wrapper's full prop bag (style + identity + a11y) folded for `[symbioteHostProps]`,
@@ -258,16 +298,28 @@ export class KeyboardAvoidingView implements IAngularKeyboardAvoidingViewInputs,
258
298
  nativeID: this.nativeID,
259
299
  accessible: this.accessible,
260
300
  ...this.folded,
301
+ onAccessibilityAction: this.eventEmitterHandler(this.accessibilityAction),
302
+ onAccessibilityTap: this.eventEmitterHandler(this.accessibilityTap),
303
+ onMagicTap: this.eventEmitterHandler(this.magicTap),
304
+ onAccessibilityEscape: this.eventEmitterHandler(this.accessibilityEscape),
261
305
  };
262
306
  }
263
307
 
264
308
  // Forward an engine event to the matching @Output(), narrowing the template's untyped $event
265
- // first. The accessibility* Outputs ride the engine's structural event channel (Angular blocks
266
- // [onX] property bindings; events flow through (event) only).
267
- emit(emitter: EventEmitter<ISymbioteEvent>, event: unknown): void {
309
+ // first.
310
+ private emit(emitter: EventEmitter<ISymbioteEvent>, event: unknown): void {
268
311
  if (isSymbioteEvent(event)) emitter.emit(event);
269
312
  }
270
313
 
314
+ // The four accessibility events are boolean-GATED Fabric events
315
+ // (`.claude/rules/fabric-boolean-event-gates.md`). `.observed`-gated, unlike `layout` above which
316
+ // this component needs unconditionally for its own internal frame measurement.
317
+ private eventEmitterHandler(
318
+ emitter: EventEmitter<ISymbioteEvent>,
319
+ ): ((event: unknown) => void) | undefined {
320
+ return emitter.observed ? event => this.emit(emitter, event) : undefined;
321
+ }
322
+
271
323
  // Fold the web aria-*/role aliases into the canonical accessibility* props once per render, so the
272
324
  // host node never sees an aria-* key (native ignores them) — the shared transform every adapter runs.
273
325
  get folded(): Partial<IAngularKeyboardAvoidingViewProps> {
@@ -286,8 +338,10 @@ export class KeyboardAvoidingView implements IAngularKeyboardAvoidingViewInputs,
286
338
  accessibilityElementsHidden: this.accessibilityElementsHidden,
287
339
  accessibilityIgnoresInvertColors: this.accessibilityIgnoresInvertColors,
288
340
  accessibilityLanguage: this.accessibilityLanguage,
289
- accessibilityRespondsToUserInteraction: this.accessibilityRespondsToUserInteraction,
290
- accessibilityShowsLargeContentViewer: this.accessibilityShowsLargeContentViewer,
341
+ accessibilityRespondsToUserInteraction:
342
+ this.accessibilityRespondsToUserInteraction,
343
+ accessibilityShowsLargeContentViewer:
344
+ this.accessibilityShowsLargeContentViewer,
291
345
  accessibilityLargeContentTitle: this.accessibilityLargeContentTitle,
292
346
  role: this.role,
293
347
  'aria-label': this.ariaLabel,