@symbiote-native/angular 0.6.2 → 0.8.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 (191) hide show
  1. package/README.md +3 -3
  2. package/bin/symbiote-angular-dev.cjs +20 -19
  3. package/build/angular/anchor-host-registry.js +2 -2
  4. package/build/angular/anchor-host-registry.js.map +1 -1
  5. package/build/angular/bootstrap.js +1 -1
  6. package/build/angular/bootstrap.js.map +1 -1
  7. package/build/angular/components/activity-indicator/index.android.d.ts +2 -1
  8. package/build/angular/components/activity-indicator/index.android.js +12 -4
  9. package/build/angular/components/activity-indicator/index.android.js.map +1 -1
  10. package/build/angular/components/activity-indicator/index.ios.d.ts +2 -1
  11. package/build/angular/components/activity-indicator/index.ios.js +8 -3
  12. package/build/angular/components/activity-indicator/index.ios.js.map +1 -1
  13. package/build/angular/components/activity-indicator/shared.d.ts +1 -1
  14. package/build/angular/components/activity-indicator/shared.js +4 -4
  15. package/build/angular/components/activity-indicator/shared.js.map +1 -1
  16. package/build/angular/components/button.d.ts +4 -2
  17. package/build/angular/components/button.js +41 -22
  18. package/build/angular/components/button.js.map +1 -1
  19. package/build/angular/components/flat-list/index.d.ts +6 -4
  20. package/build/angular/components/flat-list/index.js +80 -50
  21. package/build/angular/components/flat-list/index.js.map +1 -1
  22. package/build/angular/components/image/index.android.d.ts +8 -3
  23. package/build/angular/components/image/index.android.js +42 -10
  24. package/build/angular/components/image/index.android.js.map +1 -1
  25. package/build/angular/components/image/index.ios.d.ts +8 -3
  26. package/build/angular/components/image/index.ios.js +42 -10
  27. package/build/angular/components/image/index.ios.js.map +1 -1
  28. package/build/angular/components/image/shared.d.ts +4 -1
  29. package/build/angular/components/image/shared.js +51 -8
  30. package/build/angular/components/image/shared.js.map +1 -1
  31. package/build/angular/components/image-background.d.ts +2 -1
  32. package/build/angular/components/image-background.js +26 -10
  33. package/build/angular/components/image-background.js.map +1 -1
  34. package/build/angular/components/input-accessory-view/index.d.ts +9 -4
  35. package/build/angular/components/input-accessory-view/index.js +49 -10
  36. package/build/angular/components/input-accessory-view/index.js.map +1 -1
  37. package/build/angular/components/keyboard-avoiding-view/index.d.ts +3 -1
  38. package/build/angular/components/keyboard-avoiding-view/index.js +44 -12
  39. package/build/angular/components/keyboard-avoiding-view/index.js.map +1 -1
  40. package/build/angular/components/modal/index.d.ts +11 -8
  41. package/build/angular/components/modal/index.js +51 -29
  42. package/build/angular/components/modal/index.js.map +1 -1
  43. package/build/angular/components/pressable/index.d.ts +12 -6
  44. package/build/angular/components/pressable/index.js +66 -13
  45. package/build/angular/components/pressable/index.js.map +1 -1
  46. package/build/angular/components/refresh-control/index.d.ts +8 -4
  47. package/build/angular/components/refresh-control/index.js +54 -16
  48. package/build/angular/components/refresh-control/index.js.map +1 -1
  49. package/build/angular/components/safe-area-view/index.d.ts +9 -4
  50. package/build/angular/components/safe-area-view/index.js +48 -9
  51. package/build/angular/components/safe-area-view/index.js.map +1 -1
  52. package/build/angular/components/scroll-view/index.android.d.ts +2 -3
  53. package/build/angular/components/scroll-view/index.android.js +88 -61
  54. package/build/angular/components/scroll-view/index.android.js.map +1 -1
  55. package/build/angular/components/scroll-view/index.ios.d.ts +2 -3
  56. package/build/angular/components/scroll-view/index.ios.js +24 -18
  57. package/build/angular/components/scroll-view/index.ios.js.map +1 -1
  58. package/build/angular/components/scroll-view/projection.d.ts +35 -0
  59. package/build/angular/components/scroll-view/projection.js +279 -53
  60. package/build/angular/components/scroll-view/projection.js.map +1 -1
  61. package/build/angular/components/scroll-view/shared.d.ts +14 -5
  62. package/build/angular/components/scroll-view/shared.js +225 -75
  63. package/build/angular/components/scroll-view/shared.js.map +1 -1
  64. package/build/angular/components/scroll-view/sticky-header.js +21 -22
  65. package/build/angular/components/scroll-view/sticky-header.js.map +1 -1
  66. package/build/angular/components/section-list/index.d.ts +7 -1
  67. package/build/angular/components/section-list/index.js +91 -54
  68. package/build/angular/components/section-list/index.js.map +1 -1
  69. package/build/angular/components/switch/index.android.d.ts +7 -3
  70. package/build/angular/components/switch/index.android.js +52 -10
  71. package/build/angular/components/switch/index.android.js.map +1 -1
  72. package/build/angular/components/switch/index.ios.d.ts +7 -3
  73. package/build/angular/components/switch/index.ios.js +52 -10
  74. package/build/angular/components/switch/index.ios.js.map +1 -1
  75. package/build/angular/components/switch/shared.d.ts +13 -4
  76. package/build/angular/components/switch/shared.js +71 -7
  77. package/build/angular/components/switch/shared.js.map +1 -1
  78. package/build/angular/components/text-input.d.ts +11 -3
  79. package/build/angular/components/text-input.js +68 -8
  80. package/build/angular/components/text-input.js.map +1 -1
  81. package/build/angular/components/touchable/index.d.ts +40 -9
  82. package/build/angular/components/touchable/index.js +303 -67
  83. package/build/angular/components/touchable/index.js.map +1 -1
  84. package/build/angular/components/touchable-native-feedback/index.d.ts +6 -3
  85. package/build/angular/components/touchable-native-feedback/index.js +63 -27
  86. package/build/angular/components/touchable-native-feedback/index.js.map +1 -1
  87. package/build/angular/components/virtualized-list/directives.js +22 -18
  88. package/build/angular/components/virtualized-list/directives.js.map +1 -1
  89. package/build/angular/components/virtualized-list/index.d.ts +15 -4
  90. package/build/angular/components/virtualized-list/index.js +326 -94
  91. package/build/angular/components/virtualized-list/index.js.map +1 -1
  92. package/build/angular/components/virtualized-section-list/directives.js +12 -12
  93. package/build/angular/components/virtualized-section-list/directives.js.map +1 -1
  94. package/build/angular/components/virtualized-section-list/index.d.ts +19 -1
  95. package/build/angular/components/virtualized-section-list/index.js +93 -22
  96. package/build/angular/components/virtualized-section-list/index.js.map +1 -1
  97. package/build/angular/components.d.ts +2 -2
  98. package/build/angular/components.js +5 -0
  99. package/build/angular/components.js.map +1 -1
  100. package/build/angular/create-portal/index.js +34 -36
  101. package/build/angular/create-portal/index.js.map +1 -1
  102. package/build/angular/create-tunnel/index.js +19 -22
  103. package/build/angular/create-tunnel/index.js.map +1 -1
  104. package/build/angular/descriptor-to-angular/index.js +6 -4
  105. package/build/angular/descriptor-to-angular/index.js.map +1 -1
  106. package/build/angular/diagnostics.d.ts +33 -0
  107. package/build/angular/diagnostics.js +82 -0
  108. package/build/angular/diagnostics.js.map +1 -0
  109. package/build/angular/host-instance/index.js +1 -1
  110. package/build/angular/host-instance/index.js.map +1 -1
  111. package/build/angular/index.d.ts +9 -4
  112. package/build/angular/index.js +17 -3
  113. package/build/angular/index.js.map +1 -1
  114. package/build/angular/modules/animated/animated-leaf-binder.d.ts +1 -7
  115. package/build/angular/modules/animated/animated-leaf-binder.js +22 -72
  116. package/build/angular/modules/animated/animated-leaf-binder.js.map +1 -1
  117. package/build/angular/modules/animated/create-animated-component.d.ts +11 -6
  118. package/build/angular/modules/animated/create-animated-component.js +106 -55
  119. package/build/angular/modules/animated/create-animated-component.js.map +1 -1
  120. package/build/angular/modules/status-bar/index.js +4 -4
  121. package/build/angular/modules/status-bar/index.js.map +1 -1
  122. package/build/angular/primitives/index.d.ts +20 -3
  123. package/build/angular/primitives/index.js +84 -48
  124. package/build/angular/primitives/index.js.map +1 -1
  125. package/build/angular/primitives/shared.d.ts +48 -2
  126. package/build/angular/primitives/shared.js +158 -11
  127. package/build/angular/primitives/shared.js.map +1 -1
  128. package/build/angular/render/index.js +69 -38
  129. package/build/angular/render/index.js.map +1 -1
  130. package/build/angular/renderer/index.d.ts +1 -0
  131. package/build/angular/renderer/index.js +37 -2
  132. package/build/angular/renderer/index.js.map +1 -1
  133. package/build/angular/services/color-scheme.service.js +3 -3
  134. package/build/angular/services/color-scheme.service.js.map +1 -1
  135. package/build/angular/services/window-dimensions.service.js +4 -4
  136. package/build/angular/services/window-dimensions.service.js.map +1 -1
  137. package/package.json +10 -8
  138. package/src/anchor-host-registry.ts +2 -2
  139. package/src/bootstrap.ts +4 -1
  140. package/src/components/activity-indicator/index.android.ts +8 -1
  141. package/src/components/activity-indicator/index.ios.ts +4 -0
  142. package/src/components/activity-indicator/shared.ts +32 -13
  143. package/src/components/button.ts +46 -19
  144. package/src/components/flat-list/index.ts +122 -62
  145. package/src/components/image/index.android.ts +58 -7
  146. package/src/components/image/index.ios.ts +58 -7
  147. package/src/components/image/shared.ts +92 -21
  148. package/src/components/image-background.ts +29 -9
  149. package/src/components/input-accessory-view/index.ts +67 -12
  150. package/src/components/keyboard-avoiding-view/index.ts +61 -15
  151. package/src/components/modal/index.ts +72 -33
  152. package/src/components/pressable/index.ts +117 -28
  153. package/src/components/refresh-control/index.ts +74 -18
  154. package/src/components/safe-area-view/index.ts +64 -10
  155. package/src/components/scroll-view/index.android.ts +62 -47
  156. package/src/components/scroll-view/index.ios.ts +19 -17
  157. package/src/components/scroll-view/projection.ts +363 -64
  158. package/src/components/scroll-view/shared.ts +312 -90
  159. package/src/components/scroll-view/sticky-header.ts +36 -24
  160. package/src/components/section-list/index.ts +86 -53
  161. package/src/components/switch/index.android.ts +60 -8
  162. package/src/components/switch/index.ios.ts +60 -8
  163. package/src/components/switch/shared.ts +93 -6
  164. package/src/components/text-input.ts +95 -13
  165. package/src/components/touchable/index.ts +348 -64
  166. package/src/components/touchable-native-feedback/index.ts +94 -31
  167. package/src/components/virtualized-list/directives.ts +19 -5
  168. package/src/components/virtualized-list/index.ts +380 -85
  169. package/src/components/virtualized-section-list/directives.ts +6 -3
  170. package/src/components/virtualized-section-list/index.ts +146 -33
  171. package/src/components.ts +9 -1
  172. package/src/create-portal/index.ts +31 -31
  173. package/src/create-tunnel/index.ts +16 -17
  174. package/src/descriptor-to-angular/index.ts +29 -8
  175. package/src/diagnostics.ts +138 -0
  176. package/src/host-instance/index.ts +9 -3
  177. package/src/index.ts +61 -3
  178. package/src/modules/animated/animated-leaf-binder.ts +28 -77
  179. package/src/modules/animated/create-animated-component.ts +115 -47
  180. package/src/modules/app-registry/index.ts +9 -9
  181. package/src/modules/status-bar/index.ts +13 -4
  182. package/src/primitives/index.ts +47 -2
  183. package/src/primitives/shared.ts +170 -10
  184. package/src/render/index.ts +77 -39
  185. package/src/renderer/index.ts +76 -14
  186. package/src/services/color-scheme.service.ts +3 -1
  187. package/src/services/window-dimensions.service.ts +11 -4
  188. package/build/angular/modules/animated/animated-leaf-binder/index.d.ts +0 -14
  189. package/build/angular/modules/animated/animated-leaf-binder/index.js +0 -91
  190. package/build/angular/modules/animated/animated-leaf-binder/index.js.map +0 -1
  191. package/src/modules/animated/animated-leaf-binder/index.ts +0 -101
@@ -1,4 +1,4 @@
1
- import { EventEmitter } from '@angular/core';
1
+ import { EventEmitter, computed, signal } from '@angular/core';
2
2
  import {
3
3
  imageStatics,
4
4
  renderImage,
@@ -150,15 +150,21 @@ function asNumber(value: unknown): number | undefined {
150
150
  return typeof value === 'number' ? value : undefined;
151
151
  }
152
152
 
153
- function asCrossOrigin(value: unknown): 'anonymous' | 'use-credentials' | undefined {
154
- return value === 'anonymous' || value === 'use-credentials' ? value : undefined;
153
+ function asCrossOrigin(
154
+ value: unknown,
155
+ ): 'anonymous' | 'use-credentials' | undefined {
156
+ return value === 'anonymous' || value === 'use-credentials'
157
+ ? value
158
+ : undefined;
155
159
  }
156
160
 
157
161
  function asStyle(value: unknown): IStyleProp<IViewStyle> | undefined {
158
162
  return typeof value === 'object' && value !== null ? value : undefined;
159
163
  }
160
164
 
161
- export function resolveImageProps(input: Record<string, unknown>): Record<string, unknown> {
165
+ export function resolveImageProps(
166
+ input: Record<string, unknown>,
167
+ ): Record<string, unknown> {
162
168
  const passthrough: IImagePassthroughProps = {
163
169
  resizeMethod: input['resizeMethod'] as IImageProps['resizeMethod'],
164
170
  blurRadius: asNumber(input['blurRadius']),
@@ -170,12 +176,21 @@ export function resolveImageProps(input: Record<string, unknown>): Record<string
170
176
  : undefined,
171
177
  testID: asString(input['testID']),
172
178
  nativeID: asString(input['nativeID']),
173
- accessible: typeof input['accessible'] === 'boolean' ? input['accessible'] : undefined,
179
+ accessible:
180
+ typeof input['accessible'] === 'boolean'
181
+ ? input['accessible']
182
+ : undefined,
174
183
  accessibilityLabel: asString(input['accessibilityLabel']),
175
184
  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'],
185
+ accessibilityRole: input[
186
+ 'accessibilityRole'
187
+ ] as IAccessibilityProps['accessibilityRole'],
188
+ accessibilityState: input[
189
+ 'accessibilityState'
190
+ ] as IAccessibilityProps['accessibilityState'],
191
+ accessibilityValue: input[
192
+ 'accessibilityValue'
193
+ ] as IAccessibilityProps['accessibilityValue'],
179
194
  accessibilityActions: input[
180
195
  'accessibilityActions'
181
196
  ] as IAccessibilityProps['accessibilityActions'],
@@ -213,26 +228,48 @@ export function resolveImageProps(input: Record<string, unknown>): Record<string
213
228
  typeof input['accessibilityShowsLargeContentViewer'] === 'boolean'
214
229
  ? input['accessibilityShowsLargeContentViewer']
215
230
  : undefined,
216
- accessibilityLargeContentTitle: asString(input['accessibilityLargeContentTitle']),
231
+ accessibilityLargeContentTitle: asString(
232
+ input['accessibilityLargeContentTitle'],
233
+ ),
217
234
  role: input['role'] as IAriaProps['role'],
218
235
  'aria-label': asString(input['ariaLabel'] ?? input['aria-label']),
219
- 'aria-labelledby': asString(input['ariaLabelledBy'] ?? input['aria-labelledby']),
236
+ 'aria-labelledby': asString(
237
+ input['ariaLabelledBy'] ?? input['aria-labelledby'],
238
+ ),
220
239
  '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,
240
+ 'aria-hidden':
241
+ typeof input['ariaHidden'] === 'boolean'
242
+ ? input['ariaHidden']
243
+ : undefined,
244
+ 'aria-busy':
245
+ typeof input['ariaBusy'] === 'boolean' ? input['ariaBusy'] : undefined,
223
246
  '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,
247
+ 'aria-disabled':
248
+ typeof input['ariaDisabled'] === 'boolean'
249
+ ? input['ariaDisabled']
250
+ : undefined,
251
+ 'aria-expanded':
252
+ typeof input['ariaExpanded'] === 'boolean'
253
+ ? input['ariaExpanded']
254
+ : undefined,
255
+ 'aria-selected':
256
+ typeof input['ariaSelected'] === 'boolean'
257
+ ? input['ariaSelected']
258
+ : undefined,
259
+ 'aria-modal':
260
+ typeof input['ariaModal'] === 'boolean' ? input['ariaModal'] : undefined,
228
261
  'aria-valuemax': asNumber(input['ariaValueMax']),
229
262
  'aria-valuemin': asNumber(input['ariaValueMin']),
230
263
  'aria-valuenow': asNumber(input['ariaValueNow']),
231
264
  'aria-valuetext': asString(input['ariaValueText']),
232
- onAccessibilityAction: input['onAccessibilityAction'] as IImageProps['onLoad'],
265
+ onAccessibilityAction: input[
266
+ 'onAccessibilityAction'
267
+ ] as IImageProps['onLoad'],
233
268
  onAccessibilityTap: input['onAccessibilityTap'] as IImageProps['onLoad'],
234
269
  onMagicTap: input['onMagicTap'] as IImageProps['onLoad'],
235
- onAccessibilityEscape: input['onAccessibilityEscape'] as IImageProps['onLoad'],
270
+ onAccessibilityEscape: input[
271
+ 'onAccessibilityEscape'
272
+ ] as IImageProps['onLoad'],
236
273
  onLoadStart: input['onLoadStart'] as IImageProps['onLoadStart'],
237
274
  onLoad: input['onLoad'] as IImageProps['onLoad'],
238
275
  onLoadEnd: input['onLoadEnd'] as IImageProps['onLoadEnd'],
@@ -442,8 +479,10 @@ export abstract class ImageBase {
442
479
  accessibilityElementsHidden: this.accessibilityElementsHidden,
443
480
  accessibilityIgnoresInvertColors: this.accessibilityIgnoresInvertColors,
444
481
  accessibilityLanguage: this.accessibilityLanguage,
445
- accessibilityRespondsToUserInteraction: this.accessibilityRespondsToUserInteraction,
446
- accessibilityShowsLargeContentViewer: this.accessibilityShowsLargeContentViewer,
482
+ accessibilityRespondsToUserInteraction:
483
+ this.accessibilityRespondsToUserInteraction,
484
+ accessibilityShowsLargeContentViewer:
485
+ this.accessibilityShowsLargeContentViewer,
447
486
  accessibilityLargeContentTitle: this.accessibilityLargeContentTitle,
448
487
  role: this.role,
449
488
  ariaLabel: this.ariaLabel,
@@ -473,7 +512,39 @@ export abstract class ImageBase {
473
512
  };
474
513
  }
475
514
 
476
- get imageProps(): Record<string, unknown> {
515
+ // Bridges the non-reactive @Input fields into the reactive graph so imageProps below can be a
516
+ // memoized computed(). Signal inputs would make this unnecessary, but `input()` is visible only
517
+ // to the AOT compiler and this package's unit suite runs on JIT - see the
518
+ // `angular-adapter-change-detection` skill, §6. `signal()`/`computed()` are plain runtime APIs
519
+ // and need no compiler support, which is what makes this bridge possible at all.
520
+ //
521
+ // ImageBase is deliberately UNdecorated, so the ngOnChanges that bumps this lives on each
522
+ // platform @Component instead of here.
523
+ //
524
+ // Every dependency of the bag below is an @Input, which Angular always routes through
525
+ // ngOnChanges. State assigned OUTSIDE that path needs its OWN signal - never widen this one to
526
+ // "bump on everything", which silently un-memoizes the bag. The platform anchor style is exactly
527
+ // such a dependency and carries its own signal.
528
+ protected readonly inputsRevision = signal(0);
529
+
530
+ protected bumpInputsRevision(): void {
531
+ this.inputsRevision.update(revision => revision + 1);
532
+ }
533
+
534
+ // MEASURED 2026-08-16: as a getter this rebuilt the bag on every refresh of this view, so
535
+ // `[symbioteHostProps]`'s reference check failed every time and every key was re-pushed through
536
+ // Renderer2 -> routeProp - pure waste when nothing changed. computed() returns the SAME object
537
+ // until a tracked dependency actually changes, and the input setter then skips the whole spread.
538
+ //
539
+ // A computed FIELD cannot be overridden by a subclass accessor the way a getter can, so the
540
+ // platform components override buildImageProps() below and this stays the single memoization
541
+ // point for every platform.
542
+ readonly imageProps = computed<Record<string, unknown>>(() => {
543
+ this.inputsRevision();
544
+ return this.buildImageProps();
545
+ });
546
+
547
+ protected buildImageProps(): Record<string, unknown> {
477
548
  return resolveImageProps(this.imageInputProps);
478
549
  }
479
550
  }
@@ -37,12 +37,19 @@ import {
37
37
  type IViewStyle,
38
38
  } from '@symbiote-native/engine';
39
39
  import { Image } from './image';
40
- import { anchorHostStyle, ViewHost } from '../primitives';
40
+ import {
41
+ anchorHostStyle,
42
+ SymbioteStyleInputDirective,
43
+ ViewHost,
44
+ } from '../primitives';
41
45
 
42
46
  // Mirrors React's IImageBackgroundProps minus children (Angular takes children via <ng-content>):
43
47
  // every forwarding Image prop flows onto the inner image; `style` is the WRAPPER View style and
44
48
  // `imageStyle` the inner image's.
45
- export interface IAngularImageBackgroundProps extends Omit<IImageProps, 'style'> {
49
+ export interface IAngularImageBackgroundProps extends Omit<
50
+ IImageProps,
51
+ 'style'
52
+ > {
46
53
  style?: IStyleProp<IViewStyle>;
47
54
  // A bare string resolves through the shared style registry, like `class` on the wrapper.
48
55
  imageStyle?: IStyleProp<IViewStyle> | string;
@@ -68,6 +75,9 @@ export type IAngularImageBackgroundInputs = Omit<
68
75
  @Component({
69
76
  selector: 'ImageBackground',
70
77
  standalone: true,
78
+ hostDirectives: [
79
+ { directive: SymbioteStyleInputDirective, inputs: ['style'] },
80
+ ],
71
81
  schemas: [CUSTOM_ELEMENTS_SCHEMA],
72
82
  imports: [Image, ViewHost],
73
83
  changeDetection: ChangeDetectionStrategy.OnPush,
@@ -107,10 +117,16 @@ export type IAngularImageBackgroundInputs = Omit<
107
117
  [screenReaderFocusable]="folded.screenReaderFocusable"
108
118
  [accessibilityViewIsModal]="folded.accessibilityViewIsModal"
109
119
  [accessibilityElementsHidden]="folded.accessibilityElementsHidden"
110
- [accessibilityIgnoresInvertColors]="folded.accessibilityIgnoresInvertColors"
120
+ [accessibilityIgnoresInvertColors]="
121
+ folded.accessibilityIgnoresInvertColors
122
+ "
111
123
  [accessibilityLanguage]="folded.accessibilityLanguage"
112
- [accessibilityRespondsToUserInteraction]="folded.accessibilityRespondsToUserInteraction"
113
- [accessibilityShowsLargeContentViewer]="folded.accessibilityShowsLargeContentViewer"
124
+ [accessibilityRespondsToUserInteraction]="
125
+ folded.accessibilityRespondsToUserInteraction
126
+ "
127
+ [accessibilityShowsLargeContentViewer]="
128
+ folded.accessibilityShowsLargeContentViewer
129
+ "
114
130
  [accessibilityLargeContentTitle]="folded.accessibilityLargeContentTitle"
115
131
  (loadStart)="loadStart.emit($event)"
116
132
  (load)="load.emit($event)"
@@ -169,8 +185,10 @@ export class ImageBackground implements IAngularImageBackgroundInputs {
169
185
  @Input() accessibilityValue?: IAccessibilityProps['accessibilityValue'];
170
186
  @Input() accessibilityActions?: IAccessibilityProps['accessibilityActions'];
171
187
  @Input() accessibilityLabelledBy?: string | string[];
172
- @Input() importantForAccessibility?: IAccessibilityProps['importantForAccessibility'];
173
- @Input() accessibilityLiveRegion?: IAccessibilityProps['accessibilityLiveRegion'];
188
+ @Input()
189
+ importantForAccessibility?: IAccessibilityProps['importantForAccessibility'];
190
+ @Input()
191
+ accessibilityLiveRegion?: IAccessibilityProps['accessibilityLiveRegion'];
174
192
  @Input() screenReaderFocusable?: boolean;
175
193
  @Input() accessibilityViewIsModal?: boolean;
176
194
  @Input() accessibilityElementsHidden?: boolean;
@@ -247,8 +265,10 @@ export class ImageBackground implements IAngularImageBackgroundInputs {
247
265
  accessibilityElementsHidden: this.accessibilityElementsHidden,
248
266
  accessibilityIgnoresInvertColors: this.accessibilityIgnoresInvertColors,
249
267
  accessibilityLanguage: this.accessibilityLanguage,
250
- accessibilityRespondsToUserInteraction: this.accessibilityRespondsToUserInteraction,
251
- accessibilityShowsLargeContentViewer: this.accessibilityShowsLargeContentViewer,
268
+ accessibilityRespondsToUserInteraction:
269
+ this.accessibilityRespondsToUserInteraction,
270
+ accessibilityShowsLargeContentViewer:
271
+ this.accessibilityShowsLargeContentViewer,
252
272
  accessibilityLargeContentTitle: this.accessibilityLargeContentTitle,
253
273
  role: this.role,
254
274
  'aria-label': this.ariaLabel,
@@ -11,11 +11,15 @@ import {
11
11
  CUSTOM_ELEMENTS_SCHEMA,
12
12
  ChangeDetectionStrategy,
13
13
  Component,
14
+ computed,
14
15
  ElementRef,
15
16
  EventEmitter,
16
17
  inject,
17
18
  Input,
18
19
  Output,
20
+ signal,
21
+ type DoCheck,
22
+ type OnChanges,
19
23
  } from '@angular/core';
20
24
  import {
21
25
  renderInputAccessoryView,
@@ -34,11 +38,13 @@ import {
34
38
  anchorHostStyle,
35
39
  InputAccessoryViewHost,
36
40
  SymbioteHostPropsDirective,
41
+ SymbioteStyleInputDirective,
37
42
  } from '../../primitives';
38
43
 
39
44
  // Mirrors React's IInputAccessoryViewProps minus children (Angular takes children via
40
45
  // <ng-content>), declared per-adapter over the shared a11y base.
41
- export interface IAngularInputAccessoryViewProps extends IAccessibilityProps, IAriaProps {
46
+ export interface IAngularInputAccessoryViewProps
47
+ extends IAccessibilityProps, IAriaProps {
42
48
  // The id a TextInput's inputAccessoryViewID points at to dock above its keyboard.
43
49
  nativeID?: string;
44
50
  backgroundColor?: string;
@@ -50,18 +56,24 @@ export interface IAngularInputAccessoryViewProps extends IAccessibilityProps, IA
50
56
  // mirroring Pressable's IAngularPressableInputs split.
51
57
  export type IAngularInputAccessoryViewInputs = Omit<
52
58
  IAngularInputAccessoryViewProps,
53
- 'onAccessibilityAction' | 'onAccessibilityTap' | 'onMagicTap' | 'onAccessibilityEscape'
59
+ | 'onAccessibilityAction'
60
+ | 'onAccessibilityTap'
61
+ | 'onMagicTap'
62
+ | 'onAccessibilityEscape'
54
63
  >;
55
64
 
56
65
  @Component({
57
66
  selector: 'InputAccessoryView',
58
67
  standalone: true,
68
+ hostDirectives: [
69
+ { directive: SymbioteStyleInputDirective, inputs: ['style'] },
70
+ ],
59
71
  schemas: [CUSTOM_ELEMENTS_SCHEMA],
60
72
  imports: [InputAccessoryViewHost, SymbioteHostPropsDirective],
61
73
  changeDetection: ChangeDetectionStrategy.OnPush,
62
74
  template: `
63
75
  <symbiote-input-accessory-view
64
- [symbioteHostProps]="hostProps"
76
+ [symbioteHostProps]="hostProps()"
65
77
  (accessibilityAction)="emit(accessibilityAction, $event)"
66
78
  (accessibilityTap)="emit(accessibilityTap, $event)"
67
79
  (magicTap)="emit(magicTap, $event)"
@@ -71,7 +83,9 @@ export type IAngularInputAccessoryViewInputs = Omit<
71
83
  </symbiote-input-accessory-view>
72
84
  `,
73
85
  })
74
- export class InputAccessoryView implements IAngularInputAccessoryViewInputs {
86
+ export class InputAccessoryView
87
+ implements IAngularInputAccessoryViewInputs, OnChanges, DoCheck
88
+ {
75
89
  @Input() nativeID?: string;
76
90
  @Input() backgroundColor?: string;
77
91
  @Input() style?: IStyleProp<IViewStyle>;
@@ -88,8 +102,10 @@ export class InputAccessoryView implements IAngularInputAccessoryViewInputs {
88
102
  @Input() accessibilityValue?: IAccessibilityProps['accessibilityValue'];
89
103
  @Input() accessibilityActions?: IAccessibilityProps['accessibilityActions'];
90
104
  @Input() accessibilityLabelledBy?: string | string[];
91
- @Input() importantForAccessibility?: IAccessibilityProps['importantForAccessibility'];
92
- @Input() accessibilityLiveRegion?: IAccessibilityProps['accessibilityLiveRegion'];
105
+ @Input()
106
+ importantForAccessibility?: IAccessibilityProps['importantForAccessibility'];
107
+ @Input()
108
+ accessibilityLiveRegion?: IAccessibilityProps['accessibilityLiveRegion'];
93
109
  @Input() screenReaderFocusable?: boolean;
94
110
  @Input() accessibilityViewIsModal?: boolean;
95
111
  @Input() accessibilityElementsHidden?: boolean;
@@ -135,16 +151,53 @@ export class InputAccessoryView implements IAngularInputAccessoryViewInputs {
135
151
  });
136
152
  }
137
153
 
154
+ // Bridges the non-reactive fields `hostProps` reads into the reactive graph, so it can memoize.
155
+ // Plain fields read inside a computed() are UNTRACKED - something must signal "a dependency
156
+ // changed" or the bag goes stale. Signal inputs would do this natively, but `input()` is visible
157
+ // only to the AOT compiler and this package's unit suite runs on JIT (see the
158
+ // `angular-adapter-change-detection` skill, §6); `signal()`/`computed()` are plain runtime APIs.
159
+ private readonly hostPropsRevision = signal(0);
160
+ // What the anchor's class-derived style was when the bag was last built (identity, not value).
161
+ private lastAnchorStyle: unknown;
162
+
163
+ ngOnChanges(): void {
164
+ // The single moment Angular has finished writing every changed @Input for this pass.
165
+ this.hostPropsRevision.update(revision => revision + 1);
166
+ }
167
+
168
+ // The anchor's class-derived style is NOT an @Input: `class="..."`/`[ngClass]` at the use site
169
+ // resolves through the renderer's addClass onto this component's anchor host (see
170
+ // anchorHostStyle's doc comment), so it never shows up in SimpleChanges and ngOnChanges alone
171
+ // would leave a later class toggle stranded. ngDoCheck runs at exactly the cadence the old
172
+ // getter was re-read, and bumps only on a real identity change.
173
+ ngDoCheck(): void {
174
+ const anchorStyle = anchorHostStyle(this.elementRef);
175
+ if (anchorStyle === this.lastAnchorStyle) return;
176
+ this.lastAnchorStyle = anchorStyle;
177
+ this.hostPropsRevision.update(revision => revision + 1);
178
+ }
179
+
138
180
  // The anchor's class-derived style goes FIRST, the Descriptor's resolved style SECOND —
139
181
  // flattenStyle's later-wins collapse keeps an explicit [style] winning over its ambient class.
140
- get hostProps(): Record<string, unknown> {
182
+ //
183
+ // A computed(), not a getter: Angular re-reads a template getter on every refresh of this view
184
+ // and `[symbioteHostProps]` compares by REFERENCE, so a freshly rebuilt (but identical) bag
185
+ // re-pushes every key through renderer.setProperty -> the engine's prop routing - and this one
186
+ // re-runs renderInputAccessoryView to build it.
187
+ readonly hostProps = computed<Record<string, unknown>>(() => {
188
+ this.hostPropsRevision();
141
189
  const descriptorProps = this.descriptor.props;
142
- return { ...descriptorProps, style: [anchorHostStyle(this.elementRef), descriptorProps.style] };
143
- }
190
+ return {
191
+ ...descriptorProps,
192
+ style: [anchorHostStyle(this.elementRef), descriptorProps.style],
193
+ };
194
+ });
144
195
 
145
196
  // Typed as the a11y intersection WITH the string index (the bag renderInputAccessoryView spreads
146
197
  // into the host props), so resolveAccessibilityProps's result stays assignable to passthrough.
147
- private accessibilityInputs(): IAccessibilityProps & IAriaProps & Record<string, unknown> {
198
+ private accessibilityInputs(): IAccessibilityProps &
199
+ IAriaProps &
200
+ Record<string, unknown> {
148
201
  return {
149
202
  testID: this.testID,
150
203
  accessible: this.accessible,
@@ -162,8 +215,10 @@ export class InputAccessoryView implements IAngularInputAccessoryViewInputs {
162
215
  accessibilityElementsHidden: this.accessibilityElementsHidden,
163
216
  accessibilityIgnoresInvertColors: this.accessibilityIgnoresInvertColors,
164
217
  accessibilityLanguage: this.accessibilityLanguage,
165
- accessibilityRespondsToUserInteraction: this.accessibilityRespondsToUserInteraction,
166
- accessibilityShowsLargeContentViewer: this.accessibilityShowsLargeContentViewer,
218
+ accessibilityRespondsToUserInteraction:
219
+ this.accessibilityRespondsToUserInteraction,
220
+ accessibilityShowsLargeContentViewer:
221
+ this.accessibilityShowsLargeContentViewer,
167
222
  accessibilityLargeContentTitle: this.accessibilityLargeContentTitle,
168
223
  role: this.role,
169
224
  '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,
@@ -48,14 +50,20 @@ import {
48
50
  type ISymbioteEvent,
49
51
  type IViewStyle,
50
52
  } from '@symbiote-native/engine';
51
- import { anchorHostStyle, SymbioteHostPropsDirective, ViewHost } from '../../primitives';
53
+ import {
54
+ anchorHostStyle,
55
+ SymbioteHostPropsDirective,
56
+ SymbioteStyleInputDirective,
57
+ ViewHost,
58
+ } from '../../primitives';
52
59
 
53
60
  export type { IKeyboardAvoidingBehavior } from '@symbiote-native/components';
54
61
 
55
62
  // Mirrors React's IKeyboardAvoidingViewProps minus children (Angular takes children via
56
63
  // <ng-content>), declared per-adapter over the shared accessibility base since a framework-specific
57
64
  // children field keeps it from being fully shared across adapters.
58
- export interface IAngularKeyboardAvoidingViewProps extends IAccessibilityProps, IAriaProps {
65
+ export interface IAngularKeyboardAvoidingViewProps
66
+ extends IAccessibilityProps, IAriaProps {
59
67
  behavior?: IKeyboardAvoidingBehavior;
60
68
  enabled?: boolean;
61
69
  keyboardVerticalOffset?: number;
@@ -79,6 +87,9 @@ export type IAngularKeyboardAvoidingViewInputs = Omit<
79
87
  @Component({
80
88
  selector: 'KeyboardAvoidingView',
81
89
  standalone: true,
90
+ hostDirectives: [
91
+ { directive: SymbioteStyleInputDirective, inputs: ['style'] },
92
+ ],
82
93
  schemas: [CUSTOM_ELEMENTS_SCHEMA],
83
94
  imports: [ViewHost, SymbioteHostPropsDirective],
84
95
  changeDetection: ChangeDetectionStrategy.OnPush,
@@ -104,7 +115,9 @@ export type IAngularKeyboardAvoidingViewInputs = Omit<
104
115
  </symbiote-view>
105
116
  `,
106
117
  })
107
- export class KeyboardAvoidingView implements IAngularKeyboardAvoidingViewInputs, OnInit, OnDestroy {
118
+ export class KeyboardAvoidingView
119
+ implements IAngularKeyboardAvoidingViewInputs, OnInit, OnDestroy
120
+ {
108
121
  @Input() behavior?: IKeyboardAvoidingBehavior;
109
122
  @Input() enabled?: boolean;
110
123
  @Input() keyboardVerticalOffset?: number;
@@ -129,8 +142,10 @@ export class KeyboardAvoidingView implements IAngularKeyboardAvoidingViewInputs,
129
142
  @Input() accessibilityValue?: IAccessibilityProps['accessibilityValue'];
130
143
  @Input() accessibilityActions?: IAccessibilityProps['accessibilityActions'];
131
144
  @Input() accessibilityLabelledBy?: string | string[];
132
- @Input() importantForAccessibility?: IAccessibilityProps['importantForAccessibility'];
133
- @Input() accessibilityLiveRegion?: IAccessibilityProps['accessibilityLiveRegion'];
145
+ @Input()
146
+ importantForAccessibility?: IAccessibilityProps['importantForAccessibility'];
147
+ @Input()
148
+ accessibilityLiveRegion?: IAccessibilityProps['accessibilityLiveRegion'];
134
149
  @Input() screenReaderFocusable?: boolean;
135
150
  @Input() accessibilityViewIsModal?: boolean;
136
151
  @Input() accessibilityElementsHidden?: boolean;
@@ -162,6 +177,12 @@ export class KeyboardAvoidingView implements IAngularKeyboardAvoidingViewInputs,
162
177
  // keyboard event's inset math (React's frameRef / initialHeightRef, Vue's frame / initialHeight).
163
178
  private frame?: IMeasuredFrame;
164
179
  private initialHeight?: number;
180
+ // The iOS "Prefer Cross-Fade Transitions" setting, read once per mount. Deliberately a plain
181
+ // field and NOT markForCheck'd when the promise lands: it is a device setting that cannot change
182
+ // mid-session, and it feeds only the NEXT keyboard event's math — never the current render — so
183
+ // under zoneless CD there is nothing to repaint at resolve time. The keyboard event that does use
184
+ // it already calls markForCheck itself.
185
+ private prefersCrossFadeTransitions = false;
165
186
  private subscriptions: IEventSubscription[] = [];
166
187
 
167
188
  private readonly changeDetector = inject(ChangeDetectorRef);
@@ -171,11 +192,21 @@ export class KeyboardAvoidingView implements IAngularKeyboardAvoidingViewInputs,
171
192
  private readonly elementRef = inject(ElementRef);
172
193
 
173
194
  ngOnInit(): void {
195
+ // TWO events, picked per host, never three: iOS takes the will* pair so the view rides up with
196
+ // the keyboard animation, Android the did* pair. The change-frame notification is deliberately
197
+ // absent — with an undocked/split/floating iOS keyboard it fires BEFORE the hide, so listening
198
+ // to it applies a frame captured mid-dismissal (RN KeyboardAvoidingView.js's own comment).
199
+ const events = keyboardAvoidingEventNamesFor(Platform.OS);
174
200
  this.subscriptions = [
175
- Keyboard.addListener(KEYBOARD_EVENT.didShow, payload => this.onShow(payload)),
176
- Keyboard.addListener(KEYBOARD_EVENT.didChangeFrame, payload => this.onShow(payload)),
177
- Keyboard.addListener(KEYBOARD_EVENT.didHide, () => this.onHide()),
201
+ Keyboard.addListener(events.show, payload => this.onShow(payload)),
202
+ Keyboard.addListener(events.hide, () => this.onHide()),
178
203
  ];
204
+ // The core wrapper, not AccessibilityInfo directly: the engine's iOS getter REJECTS on a
205
+ // native error (RN parity), and nobody awaits this read, so an unwrapped call would surface
206
+ // as an unhandled rejection. The wrapper answers false on a failed read.
207
+ void readPrefersCrossFadeTransitions().then(enabled => {
208
+ this.prefersCrossFadeTransitions = enabled;
209
+ });
179
210
  }
180
211
 
181
212
  ngOnDestroy(): void {
@@ -186,7 +217,17 @@ export class KeyboardAvoidingView implements IAngularKeyboardAvoidingViewInputs,
186
217
  private onShow(payload: unknown): void {
187
218
  const keyboard = readKeyboardFrame(payload);
188
219
  const offset = this.keyboardVerticalOffset ?? DEFAULT_VERTICAL_OFFSET;
189
- const next = computeInset(this.frame, keyboard, offset);
220
+ const next = computeInset(this.frame, keyboard, offset, {
221
+ // Read off the field at event time, not captured when the subscription was built: `behavior`
222
+ // is an @Input that can change under a long-lived subscription, and a stale one keeps
223
+ // applying the old mode's math (it gates the previous-inset correction below).
224
+ behavior: this.behavior,
225
+ // The inset CURRENTLY applied (RN's this.state.bottom), read off the field at event time so
226
+ // 'height' mode's fixpoint correction sees the live value: that mode SHRINKS the wrapper by
227
+ // the inset, so the next onLayout reports a frame shorter by exactly that much.
228
+ previousInset: this.inset,
229
+ prefersCrossFadeTransitions: this.prefersCrossFadeTransitions,
230
+ });
190
231
  dlog(`KeyboardAvoidingView show -> inset ${next}`);
191
232
  this.inset = next;
192
233
  this.changeDetector.markForCheck();
@@ -205,7 +246,8 @@ export class KeyboardAvoidingView implements IAngularKeyboardAvoidingViewInputs,
205
246
  const measured = readLayoutFrame(event.nativeEvent.layout);
206
247
  if (measured !== undefined) {
207
248
  this.frame = measured;
208
- if (this.initialHeight === undefined) this.initialHeight = measured.height;
249
+ if (this.initialHeight === undefined)
250
+ this.initialHeight = measured.height;
209
251
  }
210
252
  this.layout.emit(event);
211
253
  }
@@ -238,7 +280,9 @@ export class KeyboardAvoidingView implements IAngularKeyboardAvoidingViewInputs,
238
280
  }
239
281
 
240
282
  get innerStyle(): IStyleProp<IViewStyle> | undefined {
241
- return this.resolvedLayout.kind === 'nested' ? this.resolvedLayout.innerStyle : undefined;
283
+ return this.resolvedLayout.kind === 'nested'
284
+ ? this.resolvedLayout.innerStyle
285
+ : undefined;
242
286
  }
243
287
 
244
288
  // The wrapper's full prop bag (style + identity + a11y) folded for `[symbioteHostProps]`,
@@ -280,8 +324,10 @@ export class KeyboardAvoidingView implements IAngularKeyboardAvoidingViewInputs,
280
324
  accessibilityElementsHidden: this.accessibilityElementsHidden,
281
325
  accessibilityIgnoresInvertColors: this.accessibilityIgnoresInvertColors,
282
326
  accessibilityLanguage: this.accessibilityLanguage,
283
- accessibilityRespondsToUserInteraction: this.accessibilityRespondsToUserInteraction,
284
- accessibilityShowsLargeContentViewer: this.accessibilityShowsLargeContentViewer,
327
+ accessibilityRespondsToUserInteraction:
328
+ this.accessibilityRespondsToUserInteraction,
329
+ accessibilityShowsLargeContentViewer:
330
+ this.accessibilityShowsLargeContentViewer,
285
331
  accessibilityLargeContentTitle: this.accessibilityLargeContentTitle,
286
332
  role: this.role,
287
333
  'aria-label': this.ariaLabel,