@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,12 +1,12 @@
1
1
  // RefreshControl, the Angular lifecycle half. On iOS this is the PullToRefreshView Fabric node that
2
2
  // lives INSIDE a ScrollView (a childless sibling before the content container); on Android it is
3
- // AndroidSwipeRefreshLayout and WRAPS the scroll view, receiving it through <ng-content>.
4
- // The Angular twin of the React/Vue RefreshControl. There is no JS-side platform renaming and no
5
- // shared render fn every prop forwards straight to the native node, which reads the ones it
6
- // understands and ignores the rest, so the Android-only and iOS-only families ride down harmlessly
7
- // on both. So this folds aria/role through the shared resolveAccessibilityProps and maps the native
8
- // props + a11y + onRefresh straight onto the symbiote-refresh-control host, children via <ng-content>.
9
- // No platform branch (one composed component both platforms), so this stays a flat single file.
3
+ // AndroidSwipeRefreshLayout and WRAPS the scroll view, receiving it through <ng-content>. There is
4
+ // no JS-side platform renaming and no shared render fn every prop forwards straight to the
5
+ // native node, which reads what it understands and ignores the rest, so Android-only and iOS-only
6
+ // prop families ride down harmlessly on both. This folds aria/role through the shared
7
+ // resolveAccessibilityProps and maps the native props + a11y + onRefresh onto the
8
+ // symbiote-refresh-control host, children via <ng-content>. One composed component covers both
9
+ // platforms, so this stays a flat single file.
10
10
  //
11
11
  // `refreshing` is a controlled prop: the parent owns it and pushes it down each commit; native
12
12
  // reports the gesture via the direct `topRefresh` event, which the engine routes to the host's
@@ -16,13 +16,16 @@ import {
16
16
  CUSTOM_ELEMENTS_SCHEMA,
17
17
  ChangeDetectionStrategy,
18
18
  Component,
19
+ computed,
19
20
  ElementRef,
20
21
  EventEmitter,
21
22
  inject,
22
23
  Input,
23
24
  Output,
25
+ signal,
24
26
  ViewChild,
25
27
  type AfterViewInit,
28
+ type DoCheck,
26
29
  type OnChanges,
27
30
  type OnInit,
28
31
  type SimpleChanges,
@@ -42,7 +45,12 @@ import {
42
45
  type ISymbioteEvent,
43
46
  type IViewStyle,
44
47
  } from '@symbiote-native/engine';
45
- import { anchorHostStyle, RefreshControlHost, SymbioteHostPropsDirective } from '../../primitives';
48
+ import {
49
+ anchorHostStyle,
50
+ RefreshControlHost,
51
+ SymbioteHostPropsDirective,
52
+ SymbioteStyleInputDirective,
53
+ } from '../../primitives';
46
54
 
47
55
  // Record<string, unknown> already tolerates `style` holding the [anchorHostStyle, this.style]
48
56
  // array anchorHostStyle's merge produces (see hostProps below) — no widening needed.
@@ -52,7 +60,8 @@ type IHostProps = Record<string, unknown>;
52
60
  // via <ng-content>), declared per-adapter over the shared accessibility base since the framework-specific
53
61
  // children slot keeps it from being fully shared across adapters. Read the React reference for the
54
62
  // native prop names.
55
- export interface IAngularRefreshControlProps extends IAccessibilityProps, IAriaProps {
63
+ export interface IAngularRefreshControlProps
64
+ extends IAccessibilityProps, IAriaProps {
56
65
  refreshing: boolean;
57
66
  // RN's onRefresh is `() => void | Promise<void>`, the handler may be async; the promise is
58
67
  // fire-and-forget (native already starts refreshing on the gesture).
@@ -89,13 +98,16 @@ export type IAngularRefreshControlInputs = Omit<
89
98
  @Component({
90
99
  selector: 'RefreshControl',
91
100
  standalone: true,
101
+ hostDirectives: [
102
+ { directive: SymbioteStyleInputDirective, inputs: ['style'] },
103
+ ],
92
104
  schemas: [CUSTOM_ELEMENTS_SCHEMA],
93
105
  imports: [RefreshControlHost, SymbioteHostPropsDirective],
94
106
  changeDetection: ChangeDetectionStrategy.OnPush,
95
107
  template: `
96
108
  <symbiote-refresh-control
97
109
  #host
98
- [symbioteHostProps]="hostProps"
110
+ [symbioteHostProps]="hostProps()"
99
111
  (refresh)="handleRefresh()"
100
112
  (accessibilityAction)="emit(accessibilityAction, $event)"
101
113
  (accessibilityTap)="emit(accessibilityTap, $event)"
@@ -107,7 +119,12 @@ export type IAngularRefreshControlInputs = Omit<
107
119
  `,
108
120
  })
109
121
  export class RefreshControl
110
- implements IAngularRefreshControlInputs, OnInit, OnChanges, AfterViewInit
122
+ implements
123
+ IAngularRefreshControlInputs,
124
+ OnInit,
125
+ OnChanges,
126
+ DoCheck,
127
+ AfterViewInit
111
128
  {
112
129
  // Controlled prop the parent owns; required to match the React reference surface.
113
130
  @Input({ required: true }) refreshing!: boolean;
@@ -133,8 +150,10 @@ export class RefreshControl
133
150
  @Input() accessibilityValue?: IAccessibilityProps['accessibilityValue'];
134
151
  @Input() accessibilityActions?: IAccessibilityProps['accessibilityActions'];
135
152
  @Input() accessibilityLabelledBy?: string | string[];
136
- @Input() importantForAccessibility?: IAccessibilityProps['importantForAccessibility'];
137
- @Input() accessibilityLiveRegion?: IAccessibilityProps['accessibilityLiveRegion'];
153
+ @Input()
154
+ importantForAccessibility?: IAccessibilityProps['importantForAccessibility'];
155
+ @Input()
156
+ accessibilityLiveRegion?: IAccessibilityProps['accessibilityLiveRegion'];
138
157
  @Input() screenReaderFocusable?: boolean;
139
158
  @Input() accessibilityViewIsModal?: boolean;
140
159
  @Input() accessibilityElementsHidden?: boolean;
@@ -175,6 +194,18 @@ export class RefreshControl
175
194
  private lastNativeRefreshing = false;
176
195
  private refreshNativeNode: unknown;
177
196
 
197
+ // Bridges the non-reactive @Input fields `hostProps` reads into the reactive graph, so it can
198
+ // memoize. Plain fields read inside a computed() are UNTRACKED - something must signal "a
199
+ // dependency changed" or the bag goes stale. Signal inputs would do this natively, but `input()`
200
+ // is visible only to the AOT compiler and this package's unit suite runs on JIT (see the
201
+ // `angular-adapter-change-detection` skill, §6); `signal()`/`computed()` are plain runtime APIs.
202
+ // Safe here because `refreshing` is a CONTROLLED @Input - the parent owns it, nothing in this
203
+ // class assigns it. The internal `lastNativeRefreshing` / `refreshNativeNode` fields drive the
204
+ // native handshake only; neither is read by the bag, so neither may bump this.
205
+ private readonly hostPropsRevision = signal(0);
206
+ // What the anchor's class-derived style was when the bag was last built (identity, not value).
207
+ private lastAnchorStyle: unknown;
208
+
178
209
  ngOnInit(): void {
179
210
  dlog('RefreshControl -> PullToRefreshView');
180
211
  dlog(`RefreshControl refreshing=${String(this.refreshing)}`);
@@ -188,13 +219,18 @@ export class RefreshControl
188
219
  }
189
220
 
190
221
  ngOnChanges(changes: SimpleChanges): void {
222
+ // Before the `refreshing`-only early-return below: this is the single moment Angular has
223
+ // finished writing every changed @Input, and `hostProps` depends on the whole input surface.
224
+ this.hostPropsRevision.update(revision => revision + 1);
191
225
  const refreshing = changes.refreshing;
192
226
  if (refreshing === undefined) return;
193
227
  if (refreshing.firstChange) {
194
228
  this.lastNativeRefreshing = this.refreshing;
195
229
  return;
196
230
  }
197
- this.syncNativeRefreshing(this.refreshNativeNode ?? this.host?.nativeElement);
231
+ this.syncNativeRefreshing(
232
+ this.refreshNativeNode ?? this.host?.nativeElement,
233
+ );
198
234
  }
199
235
 
200
236
  // Native starts the spinner before JS runs. Mirror RN's RefreshControl controlled-component
@@ -220,11 +256,29 @@ export class RefreshControl
220
256
  if (isSymbioteEvent(event)) emitter.emit(event);
221
257
  }
222
258
 
259
+ // The anchor's class-derived style is NOT an @Input: `class="..."`/`[ngClass]` at the use site
260
+ // resolves through the renderer's addClass onto this component's anchor host (see
261
+ // anchorHostStyle's doc comment), so it never shows up in SimpleChanges and ngOnChanges alone
262
+ // would leave a later class toggle stranded. ngDoCheck runs at exactly the cadence the old
263
+ // getter was re-read, and bumps only on a real identity change.
264
+ ngDoCheck(): void {
265
+ const anchorStyle = anchorHostStyle(this.elementRef);
266
+ if (anchorStyle === this.lastAnchorStyle) return;
267
+ this.lastAnchorStyle = anchorStyle;
268
+ this.hostPropsRevision.update(revision => revision + 1);
269
+ }
270
+
223
271
  // The full native + a11y prop bag applied onto the host in one shot via
224
272
  // [symbioteHostProps], instead of enumerating each key as its own template binding. The
225
273
  // anchor's class-derived style goes FIRST, this component's own explicit `style` @Input SECOND
226
274
  // — flattenStyle's later-wins collapse keeps an explicit [style] winning over its ambient class.
227
- get hostProps(): IHostProps {
275
+ //
276
+ // A computed(), not a getter: Angular re-reads a template getter on every refresh of this view
277
+ // and `[symbioteHostProps]` compares by REFERENCE, so a freshly rebuilt (but identical) bag
278
+ // re-pushes every key through renderer.setProperty -> the engine's prop routing. A pull gesture
279
+ // dirties this view through its own `(refresh)` binding, which is exactly when that waste hit.
280
+ readonly hostProps = computed<IHostProps>(() => {
281
+ this.hostPropsRevision();
228
282
  return {
229
283
  refreshing: this.refreshing,
230
284
  tintColor: this.tintColor,
@@ -241,7 +295,7 @@ export class RefreshControl
241
295
  accessible: this.accessible,
242
296
  ...this.folded,
243
297
  };
244
- }
298
+ });
245
299
 
246
300
  // Fold the web aria-*/role aliases into the canonical accessibility* props once per render, so the
247
301
  // host node never sees an aria-* key (native ignores them) — the shared transform every adapter runs.
@@ -261,8 +315,10 @@ export class RefreshControl
261
315
  accessibilityElementsHidden: this.accessibilityElementsHidden,
262
316
  accessibilityIgnoresInvertColors: this.accessibilityIgnoresInvertColors,
263
317
  accessibilityLanguage: this.accessibilityLanguage,
264
- accessibilityRespondsToUserInteraction: this.accessibilityRespondsToUserInteraction,
265
- accessibilityShowsLargeContentViewer: this.accessibilityShowsLargeContentViewer,
318
+ accessibilityRespondsToUserInteraction:
319
+ this.accessibilityRespondsToUserInteraction,
320
+ accessibilityShowsLargeContentViewer:
321
+ this.accessibilityShowsLargeContentViewer,
266
322
  accessibilityLargeContentTitle: this.accessibilityLargeContentTitle,
267
323
  role: this.role,
268
324
  'aria-label': this.ariaLabel,
@@ -10,11 +10,15 @@ import {
10
10
  CUSTOM_ELEMENTS_SCHEMA,
11
11
  ChangeDetectionStrategy,
12
12
  Component,
13
+ computed,
13
14
  ElementRef,
14
15
  EventEmitter,
15
16
  inject,
16
17
  Input,
17
18
  Output,
19
+ signal,
20
+ type DoCheck,
21
+ type OnChanges,
18
22
  } from '@angular/core';
19
23
  import {
20
24
  resolveAccessibilityProps,
@@ -28,12 +32,18 @@ import {
28
32
  type ISymbioteEvent,
29
33
  type IViewStyle,
30
34
  } from '@symbiote-native/engine';
31
- import { anchorHostStyle, SafeAreaViewHost, SymbioteHostPropsDirective } from '../../primitives';
35
+ import {
36
+ anchorHostStyle,
37
+ SafeAreaViewHost,
38
+ SymbioteHostPropsDirective,
39
+ SymbioteStyleInputDirective,
40
+ } from '../../primitives';
32
41
 
33
42
  // Mirrors React's ISafeAreaViewProps minus children (Angular takes children via <ng-content>),
34
43
  // declared per-adapter over the shared accessibility base since the framework-specific children
35
44
  // slot keeps it from being fully shared across adapters.
36
- export interface IAngularSafeAreaViewProps extends IAccessibilityProps, IAriaProps {
45
+ export interface IAngularSafeAreaViewProps
46
+ extends IAccessibilityProps, IAriaProps {
37
47
  style?: IStyleProp<IViewStyle>;
38
48
  onLayout?: (event: ISymbioteEvent) => void;
39
49
  }
@@ -53,12 +63,15 @@ export type IAngularSafeAreaViewInputs = Omit<
53
63
  @Component({
54
64
  selector: 'SafeAreaView',
55
65
  standalone: true,
66
+ hostDirectives: [
67
+ { directive: SymbioteStyleInputDirective, inputs: ['style'] },
68
+ ],
56
69
  schemas: [CUSTOM_ELEMENTS_SCHEMA],
57
70
  imports: [SafeAreaViewHost, SymbioteHostPropsDirective],
58
71
  changeDetection: ChangeDetectionStrategy.OnPush,
59
72
  template: `
60
73
  <symbiote-safe-area-view
61
- [symbioteHostProps]="hostProps"
74
+ [symbioteHostProps]="hostProps()"
62
75
  (accessibilityAction)="emit(accessibilityAction, $event)"
63
76
  (accessibilityTap)="emit(accessibilityTap, $event)"
64
77
  (magicTap)="emit(magicTap, $event)"
@@ -69,7 +82,9 @@ export type IAngularSafeAreaViewInputs = Omit<
69
82
  </symbiote-safe-area-view>
70
83
  `,
71
84
  })
72
- export class SafeAreaView implements IAngularSafeAreaViewInputs {
85
+ export class SafeAreaView
86
+ implements IAngularSafeAreaViewInputs, OnChanges, DoCheck
87
+ {
73
88
  @Input() style?: IStyleProp<IViewStyle>;
74
89
  // Real @Output()s, not `[onX]="…"` callbacks. Safe to name `layout` the same as the native
75
90
  // `layout` event fired inside this component's own template — the engine's bubble() treats
@@ -89,8 +104,10 @@ export class SafeAreaView implements IAngularSafeAreaViewInputs {
89
104
  @Input() accessibilityValue?: IAccessibilityProps['accessibilityValue'];
90
105
  @Input() accessibilityActions?: IAccessibilityProps['accessibilityActions'];
91
106
  @Input() accessibilityLabelledBy?: string | string[];
92
- @Input() importantForAccessibility?: IAccessibilityProps['importantForAccessibility'];
93
- @Input() accessibilityLiveRegion?: IAccessibilityProps['accessibilityLiveRegion'];
107
+ @Input()
108
+ importantForAccessibility?: IAccessibilityProps['importantForAccessibility'];
109
+ @Input()
110
+ accessibilityLiveRegion?: IAccessibilityProps['accessibilityLiveRegion'];
94
111
  @Input() screenReaderFocusable?: boolean;
95
112
  @Input() accessibilityViewIsModal?: boolean;
96
113
  @Input() accessibilityElementsHidden?: boolean;
@@ -127,6 +144,35 @@ export class SafeAreaView implements IAngularSafeAreaViewInputs {
127
144
  if (isSymbioteEvent(event)) emitter.emit(event);
128
145
  }
129
146
 
147
+ // Bridges the non-reactive fields `hostProps` reads into the reactive graph, so it can memoize.
148
+ // Plain fields read inside a computed() are UNTRACKED - something must signal "a dependency
149
+ // changed" or the bag goes stale. Signal inputs would do this natively, but `input()` is visible
150
+ // only to the AOT compiler and this package's unit suite runs on JIT (see the
151
+ // `angular-adapter-change-detection` skill, §6); `signal()`/`computed()` are plain runtime APIs.
152
+ // TWO bump sites, both required - see ngOnChanges and ngDoCheck below.
153
+ private readonly hostPropsRevision = signal(0);
154
+ // What the anchor's class-derived style was when the bag was last built (identity, not value).
155
+ private lastAnchorStyle: unknown;
156
+
157
+ ngOnChanges(): void {
158
+ // The single moment Angular has finished writing every changed @Input for this pass.
159
+ this.hostPropsRevision.update(revision => revision + 1);
160
+ }
161
+
162
+ // The anchor's class-derived style is NOT an @Input: `class="..."`/`[ngClass]` at the use site
163
+ // resolves through the renderer's addClass onto this component's anchor host (see
164
+ // anchorHostStyle's doc comment), so it never shows up in SimpleChanges - ngOnChanges alone
165
+ // would strand a class toggled after mount (regression-covered by safe-area-view.test.ts's
166
+ // class-toggle case, which fails the moment this hook stops bumping). ngDoCheck runs at exactly
167
+ // the cadence the old getter was re-read, and bumps only on a real identity change, so an
168
+ // unchanged anchor still costs nothing.
169
+ ngDoCheck(): void {
170
+ const anchorStyle = anchorHostStyle(this.elementRef);
171
+ if (anchorStyle === this.lastAnchorStyle) return;
172
+ this.lastAnchorStyle = anchorStyle;
173
+ this.hostPropsRevision.update(revision => revision + 1);
174
+ }
175
+
130
176
  // Folds style/testID/nativeID/accessible alongside the accessibility*/aria-*/role surface into
131
177
  // one bag for [symbioteHostProps] — resolveAccessibilityProps takes the whole props object (not
132
178
  // just the accessibility slice) and returns it with aria-*/role folded into accessibility*, so
@@ -134,7 +180,13 @@ export class SafeAreaView implements IAngularSafeAreaViewInputs {
134
180
  // SafeAreaView runs over its whole `rawProps`. The anchor's class-derived style goes FIRST, this
135
181
  // component's own explicit `style` @Input SECOND — flattenStyle's later-wins collapse keeps an
136
182
  // explicit [style] winning over its ambient class.
137
- get hostProps(): Record<string, unknown> {
183
+ //
184
+ // A computed(), not a getter: Angular re-reads a template getter on every refresh of this view
185
+ // and `[symbioteHostProps]` compares by REFERENCE, so a freshly rebuilt (but identical) bag
186
+ // re-pushes every key through renderer.setProperty -> the engine's prop routing. Memoized, an
187
+ // a11y/layout event that only dirties this view costs nothing.
188
+ readonly hostProps = computed<Record<string, unknown>>(() => {
189
+ this.hostPropsRevision();
138
190
  return resolveAccessibilityProps({
139
191
  style: [anchorHostStyle(this.elementRef), this.style],
140
192
  testID: this.testID,
@@ -154,8 +206,10 @@ export class SafeAreaView implements IAngularSafeAreaViewInputs {
154
206
  accessibilityElementsHidden: this.accessibilityElementsHidden,
155
207
  accessibilityIgnoresInvertColors: this.accessibilityIgnoresInvertColors,
156
208
  accessibilityLanguage: this.accessibilityLanguage,
157
- accessibilityRespondsToUserInteraction: this.accessibilityRespondsToUserInteraction,
158
- accessibilityShowsLargeContentViewer: this.accessibilityShowsLargeContentViewer,
209
+ accessibilityRespondsToUserInteraction:
210
+ this.accessibilityRespondsToUserInteraction,
211
+ accessibilityShowsLargeContentViewer:
212
+ this.accessibilityShowsLargeContentViewer,
159
213
  accessibilityLargeContentTitle: this.accessibilityLargeContentTitle,
160
214
  role: this.role,
161
215
  'aria-label': this.ariaLabel,
@@ -173,5 +227,5 @@ export class SafeAreaView implements IAngularSafeAreaViewInputs {
173
227
  'aria-valuenow': this.ariaValueNow,
174
228
  'aria-valuetext': this.ariaValueText,
175
229
  });
176
- }
230
+ });
177
231
  }
@@ -1,23 +1,22 @@
1
- // ScrollView on Android. An Android ScrollView accepts only ONE child, so a
2
- // RefreshControl can't be a sibling of the content the way iOS allows ("addViewAt: failed to insert
3
- // view ... at index 1"). RN solves this by having the RefreshControl (AndroidSwipeRefreshLayout)
4
- // WRAP the scroll view (cloneElement(refreshControl, {style}, scrollView)). Angular mirrors that
5
- // shape by querying a projected <RefreshControl>, re-rendering its native prop surface as the outer
6
- // wrapper, and projecting every non-RefreshControl child into the inner scroll content.
1
+ // ScrollView on Android. An Android ScrollView accepts only ONE child, so a RefreshControl can't
2
+ // be a sibling of the content the way iOS allows ("addViewAt: failed to insert view ... at index
3
+ // 1"). RN solves this by having the RefreshControl (AndroidSwipeRefreshLayout) WRAP the scroll
4
+ // view (cloneElement(refreshControl, {style}, scrollView)). Angular mirrors that shape by querying
5
+ // a projected <RefreshControl>, re-rendering its native prop surface as the outer wrapper, and
6
+ // projecting every non-RefreshControl child into the inner scroll content.
7
7
  //
8
- // Android genuinely needs a different Fabric view per
9
- // axis for BOTH the outer scroll container (RCTScrollView is vertical-only; AndroidHorizontalScrollView
10
- // is a dedicated ViewManager) AND the inner content view (vertical content is a plain Android View,
11
- // horizontal content is AndroidHorizontalScrollContentView, which carries its own ShadowNode::
12
- // layout() override). So the content tag differs by axis but Angular only reliably projects into a
13
- // component's LAST-declared <ng-content> when 2+ DISTINCT declarations exist (angular/angular#22972),
14
- // which an earlier 4-call-site template (one <ng-content> per axis x refresh-control branch) tripped.
15
- // Fix: exactly ONE `<ng-content>` textually, inside a single top-level `<ng-template>`, re-instantiated
16
- // into whichever branch is active via a local template-outlet directive (SymbioteTemplateOutletDirective
17
- // below, `@angular/common`-free per this adapter's dependency policy — see package.json). Only one
18
- // branch is ever live at a time, and the `<ng-content>` DECLARATION COUNT stays at one, so the "last
19
- // one wins" limitation never triggers. Covered by `android-scroll-view-axis-projection.test.ts` (all
20
- // four axis x refresh combos, plus a runtime axis switch) and `flat-list-scroll-containment.test.ts`.
8
+ // Android needs a different Fabric view per axis for both the outer scroll container
9
+ // (RCTScrollView is vertical-only; AndroidHorizontalScrollView is a dedicated ViewManager) and the
10
+ // inner content view (horizontal content is AndroidHorizontalScrollContentView, with its own
11
+ // ShadowNode::layout() override) but Angular only reliably projects into a component's
12
+ // LAST-declared <ng-content> when 2+ DISTINCT declarations exist (angular/angular#22972), which an
13
+ // earlier 4-call-site template (one <ng-content> per axis x refresh-control branch) tripped. Fix:
14
+ // exactly ONE `<ng-content>` textually, inside a single top-level `<ng-template>`, re-instantiated
15
+ // into whichever branch is active via a local template-outlet directive
16
+ // (SymbioteTemplateOutletDirective below, `@angular/common`-free per this adapter's dependency
17
+ // policy — see package.json). Only one branch is ever live, so the declaration count stays at one
18
+ // and the "last one wins" limitation never triggers. Covered by
19
+ // `android-scroll-view-axis-projection.test.ts` and `flat-list-scroll-containment.test.ts`.
21
20
  //
22
21
  // Metro picks this on an Android host; no Platform.OS read.
23
22
 
@@ -26,7 +25,6 @@ import {
26
25
  ChangeDetectionStrategy,
27
26
  Component,
28
27
  Directive,
29
- ElementRef,
30
28
  Input,
31
29
  TemplateRef,
32
30
  ViewContainerRef,
@@ -37,15 +35,19 @@ import {
37
35
  } from '@angular/core';
38
36
  import type { IStyleProp, IViewStyle } from '@symbiote-native/engine';
39
37
  import {
40
- anchorHostStyle,
41
- HorizontalScrollView,
42
38
  HorizontalScrollContentView,
39
+ HorizontalScrollView,
43
40
  RefreshControlHost,
44
41
  ScrollContentView,
45
42
  ScrollViewHost,
46
43
  SymbioteHostPropsDirective,
44
+ SymbioteStyleInputDirective,
47
45
  } from '../../primitives';
48
- import { ScrollViewBase, ScrollViewProjectionDirective, SCROLL_VIEW_INPUTS } from './shared';
46
+ import {
47
+ ScrollViewBase,
48
+ ScrollViewProjectionDirective,
49
+ SCROLL_VIEW_INPUTS,
50
+ } from './shared';
49
51
  export type { IAngularScrollViewProps, IScrollViewHandle } from './shared';
50
52
 
51
53
  // Minimal local twin of `@angular/common`'s NgTemplateOutlet (this adapter deliberately has no
@@ -68,7 +70,9 @@ export class SymbioteTemplateOutletDirective implements OnChanges, OnDestroy {
68
70
  this.viewRef = undefined;
69
71
  }
70
72
  if (this.symbioteTemplateOutlet !== null) {
71
- this.viewRef = this.viewContainerRef.createEmbeddedView(this.symbioteTemplateOutlet);
73
+ this.viewRef = this.viewContainerRef.createEmbeddedView(
74
+ this.symbioteTemplateOutlet,
75
+ );
72
76
  }
73
77
  }
74
78
 
@@ -89,6 +93,9 @@ function asStyle(value: unknown): IStyleProp<IViewStyle> | undefined {
89
93
  @Component({
90
94
  selector: 'ScrollView',
91
95
  standalone: true,
96
+ hostDirectives: [
97
+ { directive: SymbioteStyleInputDirective, inputs: ['style'] },
98
+ ],
92
99
  schemas: [CUSTOM_ELEMENTS_SCHEMA],
93
100
  imports: [
94
101
  ScrollViewHost,
@@ -115,23 +122,30 @@ function asStyle(value: unknown): IStyleProp<IViewStyle> | undefined {
115
122
  >
116
123
  <symbiote-horizontal-scroll-view
117
124
  #host="symbioteHost"
118
- [symbioteHostProps]="androidWrappedScrollProps"
125
+ [symbioteHostProps]="androidWrappedScrollProps()"
119
126
  >
120
127
  <symbiote-horizontal-scroll-content
121
- [symbioteHostProps]="contentProps"
128
+ [symbioteHostProps]="contentProps()"
122
129
  [symbioteScrollViewProjection]="projectionController"
123
130
  >
124
- <ng-container [symbioteTemplateOutlet]="sharedContent"></ng-container>
131
+ <ng-container
132
+ [symbioteTemplateOutlet]="sharedContent"
133
+ ></ng-container>
125
134
  </symbiote-horizontal-scroll-content>
126
135
  </symbiote-horizontal-scroll-view>
127
136
  </symbiote-refresh-control>
128
137
  } @else {
129
- <symbiote-horizontal-scroll-view #host="symbioteHost" [symbioteHostProps]="scrollProps">
138
+ <symbiote-horizontal-scroll-view
139
+ #host="symbioteHost"
140
+ [symbioteHostProps]="scrollProps()"
141
+ >
130
142
  <symbiote-horizontal-scroll-content
131
- [symbioteHostProps]="contentProps"
143
+ [symbioteHostProps]="contentProps()"
132
144
  [symbioteScrollViewProjection]="projectionController"
133
145
  >
134
- <ng-container [symbioteTemplateOutlet]="sharedContent"></ng-container>
146
+ <ng-container
147
+ [symbioteTemplateOutlet]="sharedContent"
148
+ ></ng-container>
135
149
  </symbiote-horizontal-scroll-content>
136
150
  </symbiote-horizontal-scroll-view>
137
151
  }
@@ -144,23 +158,30 @@ function asStyle(value: unknown): IStyleProp<IViewStyle> | undefined {
144
158
  >
145
159
  <symbiote-scroll-view
146
160
  #host="symbioteHost"
147
- [symbioteHostProps]="androidWrappedScrollProps"
161
+ [symbioteHostProps]="androidWrappedScrollProps()"
148
162
  >
149
163
  <symbiote-scroll-content
150
- [symbioteHostProps]="contentProps"
164
+ [symbioteHostProps]="contentProps()"
151
165
  [symbioteScrollViewProjection]="projectionController"
152
166
  >
153
- <ng-container [symbioteTemplateOutlet]="sharedContent"></ng-container>
167
+ <ng-container
168
+ [symbioteTemplateOutlet]="sharedContent"
169
+ ></ng-container>
154
170
  </symbiote-scroll-content>
155
171
  </symbiote-scroll-view>
156
172
  </symbiote-refresh-control>
157
173
  } @else {
158
- <symbiote-scroll-view #host="symbioteHost" [symbioteHostProps]="scrollProps">
174
+ <symbiote-scroll-view
175
+ #host="symbioteHost"
176
+ [symbioteHostProps]="scrollProps()"
177
+ >
159
178
  <symbiote-scroll-content
160
- [symbioteHostProps]="contentProps"
179
+ [symbioteHostProps]="contentProps()"
161
180
  [symbioteScrollViewProjection]="projectionController"
162
181
  >
163
- <ng-container [symbioteTemplateOutlet]="sharedContent"></ng-container>
182
+ <ng-container
183
+ [symbioteTemplateOutlet]="sharedContent"
184
+ ></ng-container>
164
185
  </symbiote-scroll-content>
165
186
  </symbiote-scroll-view>
166
187
  }
@@ -168,23 +189,17 @@ function asStyle(value: unknown): IStyleProp<IViewStyle> | undefined {
168
189
  `,
169
190
  })
170
191
  export class ScrollView extends ScrollViewBase {
171
- // This component's OWN host — the non-painting anchor `class="..."` at the use site resolves
172
- // onto (see anchorHostStyle's doc comment) — NOT `#host` in the template above, which targets
173
- // the real inner `symbiote-scroll-view`/`symbiote-horizontal-scroll-view` one level down.
174
- private readonly elementRef = inject(ElementRef);
175
-
176
- override get scrollProps(): Record<string, unknown> {
177
- const props = super.scrollProps;
178
- return { ...props, style: [anchorHostStyle(this.elementRef), props.style] };
179
- }
180
-
181
192
  // Feeds the anchor's class-derived style INTO the base class's own `splitLayoutProps` call
182
193
  // (shared.ts's `androidWrappedScrollProps`/`androidRefreshControlProps`), not tacked on after —
183
194
  // a class-only layout style (flex/height/gap/…) must be visible to the split itself, or the
184
195
  // Android outer refresh-control wrapper never receives its share and collapses to zero size
185
196
  // (a real device bug: the whole ScrollView renders nothing). See layoutSplitStyle's doc comment
186
197
  // in shared.ts and the identical fix already shipped in the Vue adapter's scroll-view.
198
+ //
199
+ // Reads the base's `anchorStyle` SIGNAL rather than calling anchorHostStyle() directly: this
200
+ // getter is evaluated inside the `androidWrappedScrollProps` computed, where a plain read would
201
+ // be untracked and leave the memoized bag holding a stale class.
187
202
  protected override get layoutSplitStyle(): IStyleProp<IViewStyle> {
188
- return [asStyle(anchorHostStyle(this.elementRef)), this.style];
203
+ return [asStyle(this.anchorStyle()), this.style];
189
204
  }
190
205
  }
@@ -20,17 +20,19 @@ import {
20
20
  CUSTOM_ELEMENTS_SCHEMA,
21
21
  ChangeDetectionStrategy,
22
22
  Component,
23
- ElementRef,
24
- inject,
25
23
  } from '@angular/core';
26
24
  import {
27
- anchorHostStyle,
28
25
  RefreshControlHost,
29
26
  ScrollContentView,
30
27
  ScrollViewHost,
31
28
  SymbioteHostPropsDirective,
29
+ SymbioteStyleInputDirective,
32
30
  } from '../../primitives';
33
- import { ScrollViewBase, ScrollViewProjectionDirective, SCROLL_VIEW_INPUTS } from './shared';
31
+ import {
32
+ ScrollViewBase,
33
+ ScrollViewProjectionDirective,
34
+ SCROLL_VIEW_INPUTS,
35
+ } from './shared';
34
36
  export type { IAngularScrollViewProps, IScrollViewHandle } from './shared';
35
37
 
36
38
  // The symbiote-* host elements are imported as standalone components; the props directive is
@@ -38,6 +40,9 @@ export type { IAngularScrollViewProps, IScrollViewHandle } from './shared';
38
40
  @Component({
39
41
  selector: 'ScrollView',
40
42
  standalone: true,
43
+ hostDirectives: [
44
+ { directive: SymbioteStyleInputDirective, inputs: ['style'] },
45
+ ],
41
46
  schemas: [CUSTOM_ELEMENTS_SCHEMA],
42
47
  imports: [
43
48
  ScrollViewHost,
@@ -49,7 +54,10 @@ export type { IAngularScrollViewProps, IScrollViewHandle } from './shared';
49
54
  changeDetection: ChangeDetectionStrategy.OnPush,
50
55
  inputs: SCROLL_VIEW_INPUTS,
51
56
  template: `
52
- <symbiote-scroll-view #host="symbioteHost" [symbioteHostProps]="scrollProps">
57
+ <symbiote-scroll-view
58
+ #host="symbioteHost"
59
+ [symbioteHostProps]="scrollProps()"
60
+ >
53
61
  @if (hasProjectedRefreshControl) {
54
62
  <symbiote-refresh-control
55
63
  #refreshHost="symbioteHost"
@@ -58,7 +66,7 @@ export type { IAngularScrollViewProps, IScrollViewHandle } from './shared';
58
66
  ></symbiote-refresh-control>
59
67
  }
60
68
  <symbiote-scroll-content
61
- [symbioteHostProps]="contentProps"
69
+ [symbioteHostProps]="contentProps()"
62
70
  [symbioteScrollViewProjection]="projectionController"
63
71
  >
64
72
  <ng-content></ng-content>
@@ -66,14 +74,8 @@ export type { IAngularScrollViewProps, IScrollViewHandle } from './shared';
66
74
  </symbiote-scroll-view>
67
75
  `,
68
76
  })
69
- export class ScrollView extends ScrollViewBase {
70
- // This component's OWN host the non-painting anchor `class="..."` at the use site resolves
71
- // onto (see anchorHostStyle's doc comment) NOT `#host` in the template above, which targets
72
- // the real inner `symbiote-scroll-view` one level down.
73
- private readonly elementRef = inject(ElementRef);
74
-
75
- override get scrollProps(): Record<string, unknown> {
76
- const props = super.scrollProps;
77
- return { ...props, style: [anchorHostStyle(this.elementRef), props.style] };
78
- }
79
- }
77
+ // Behavior lives entirely in ScrollViewBase; this class exists for the decorator + iOS template.
78
+ // The anchor-style merge that used to be a `scrollProps` override here moved into the base's
79
+ // `scrollProps` computed - a subclass getter cannot override a field, and both platforms merged it
80
+ // identically anyway (see that computed's comment).
81
+ export class ScrollView extends ScrollViewBase {}