@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
@@ -13,11 +13,14 @@ import {
13
13
  ChangeDetectionStrategy,
14
14
  ChangeDetectorRef,
15
15
  Component,
16
+ computed,
16
17
  ElementRef,
17
18
  EventEmitter,
18
19
  inject,
19
20
  Input,
20
21
  Output,
22
+ signal,
23
+ type DoCheck,
21
24
  type OnChanges,
22
25
  type OnInit,
23
26
  type SimpleChanges,
@@ -33,7 +36,6 @@ import {
33
36
  type IAriaProps,
34
37
  type IModalAnimationType,
35
38
  type IModalOrientation,
36
- type IModalOrientationChangeEvent,
37
39
  type IModalPresentationStyle,
38
40
  type IModalState,
39
41
  } from '@symbiote-native/components';
@@ -44,7 +46,13 @@ import {
44
46
  type ISymbioteEvent,
45
47
  type IViewStyle,
46
48
  } from '@symbiote-native/engine';
47
- import { anchorHostStyle, ModalHost, SymbioteHostPropsDirective, ViewHost } from '../../primitives';
49
+ import {
50
+ anchorHostStyle,
51
+ ModalHost,
52
+ SymbioteHostPropsDirective,
53
+ SymbioteStyleInputDirective,
54
+ ViewHost,
55
+ } from '../../primitives';
48
56
 
49
57
  export type {
50
58
  IModalAnimationType,
@@ -53,17 +61,6 @@ export type {
53
61
  IModalOrientationChangeEvent,
54
62
  } from '@symbiote-native/components';
55
63
 
56
- const ORIENTATIONS: ReadonlyArray<IModalOrientation> = ['portrait', 'landscape'];
57
-
58
- function readOrientation(event: ISymbioteEvent): IModalOrientationChangeEvent | undefined {
59
- const native = event.nativeEvent;
60
- if (typeof native !== 'object' || native === null || !('orientation' in native)) return undefined;
61
- const orientation = native.orientation;
62
- return ORIENTATIONS.some(value => value === orientation)
63
- ? { orientation: orientation === 'landscape' ? 'landscape' : 'portrait' }
64
- : undefined;
65
- }
66
-
67
64
  // Mirrors React's IModalProps minus children (Angular takes children via <ng-content>).
68
65
  export interface IAngularModalProps extends IAccessibilityProps, IAriaProps {
69
66
  visible?: boolean;
@@ -79,7 +76,9 @@ export interface IAngularModalProps extends IAccessibilityProps, IAriaProps {
79
76
  onShow?: () => void;
80
77
  onDismiss?: () => void;
81
78
  onRequestClose?: () => void;
82
- onOrientationChange?: (event: IModalOrientationChangeEvent) => void;
79
+ // The engine hands every listener the ISymbioteEvent wrapper, so the orientation is read at
80
+ // event.nativeEvent.orientation (IModalOrientationChangeEvent describes that payload).
81
+ onOrientationChange?: (event: ISymbioteEvent) => void;
83
82
  style?: IStyleProp<IViewStyle>;
84
83
  }
85
84
 
@@ -102,17 +101,20 @@ export type IAngularModalInputs = Omit<
102
101
  @Component({
103
102
  selector: 'Modal',
104
103
  standalone: true,
104
+ hostDirectives: [
105
+ { directive: SymbioteStyleInputDirective, inputs: ['style'] },
106
+ ],
105
107
  schemas: [CUSTOM_ELEMENTS_SCHEMA],
106
108
  imports: [ModalHost, ViewHost, SymbioteHostPropsDirective],
107
109
  changeDetection: ChangeDetectionStrategy.OnPush,
108
110
  template: `
109
111
  @if (shouldRender) {
110
112
  <symbiote-modal
111
- [symbioteHostProps]="hostProps"
113
+ [symbioteHostProps]="hostProps()"
112
114
  (show)="show.emit()"
113
115
  (dismiss)="dismiss.emit()"
114
116
  (requestClose)="requestClose.emit()"
115
- (orientationChange)="emitOrientation($event)"
117
+ (orientationChange)="emit(orientationChange, $event)"
116
118
  (accessibilityAction)="emit(accessibilityAction, $event)"
117
119
  (accessibilityTap)="emit(accessibilityTap, $event)"
118
120
  (magicTap)="emit(magicTap, $event)"
@@ -125,11 +127,11 @@ export type IAngularModalInputs = Omit<
125
127
  }
126
128
  `,
127
129
  })
128
- export class Modal implements IAngularModalInputs, OnInit, OnChanges {
130
+ export class Modal implements IAngularModalInputs, OnInit, OnChanges, DoCheck {
129
131
  @Output() readonly show = new EventEmitter<void>();
130
132
  @Output() readonly dismiss = new EventEmitter<void>();
131
133
  @Output() readonly requestClose = new EventEmitter<void>();
132
- @Output() readonly orientationChange = new EventEmitter<IModalOrientationChangeEvent>();
134
+ @Output() readonly orientationChange = new EventEmitter<ISymbioteEvent>();
133
135
  @Output() readonly accessibilityAction = new EventEmitter<ISymbioteEvent>();
134
136
  @Output() readonly accessibilityTap = new EventEmitter<ISymbioteEvent>();
135
137
  @Output() readonly magicTap = new EventEmitter<ISymbioteEvent>();
@@ -154,8 +156,10 @@ export class Modal implements IAngularModalInputs, OnInit, OnChanges {
154
156
  @Input() accessibilityValue?: IAccessibilityProps['accessibilityValue'];
155
157
  @Input() accessibilityActions?: IAccessibilityProps['accessibilityActions'];
156
158
  @Input() accessibilityLabelledBy?: string | string[];
157
- @Input() importantForAccessibility?: IAccessibilityProps['importantForAccessibility'];
158
- @Input() accessibilityLiveRegion?: IAccessibilityProps['accessibilityLiveRegion'];
159
+ @Input()
160
+ importantForAccessibility?: IAccessibilityProps['importantForAccessibility'];
161
+ @Input()
162
+ accessibilityLiveRegion?: IAccessibilityProps['accessibilityLiveRegion'];
159
163
  @Input() screenReaderFocusable?: boolean;
160
164
  @Input() accessibilityViewIsModal?: boolean;
161
165
  @Input() accessibilityElementsHidden?: boolean;
@@ -195,7 +199,20 @@ export class Modal implements IAngularModalInputs, OnInit, OnChanges {
195
199
  this.state = createInitialModalState(this.visible === true);
196
200
  }
197
201
 
202
+ // Bridges the non-reactive @Input fields `hostProps` reads into the reactive graph, so it can
203
+ // memoize. Plain fields read inside a computed() are UNTRACKED - something must signal "a
204
+ // dependency changed" or the bag goes stale. Signal inputs would do this natively, but `input()`
205
+ // is visible only to the AOT compiler and this package's unit suite runs on JIT (see the
206
+ // `angular-adapter-change-detection` skill, §6); `signal()`/`computed()` are plain runtime APIs.
207
+ // The keep-alive `state` is deliberately NOT a dependency: it gates `shouldRender`, never the bag.
208
+ private readonly hostPropsRevision = signal(0);
209
+ // What the anchor's class-derived style was when the bag was last built (identity, not value).
210
+ private lastAnchorStyle: unknown;
211
+
198
212
  ngOnChanges(changes: SimpleChanges): void {
213
+ // Before the keep-alive early-return below: this is the single moment Angular has finished
214
+ // writing every changed @Input, and `hostProps` depends on far more than `visible`.
215
+ this.hostPropsRevision.update(revision => revision + 1);
199
216
  const visibleChange = changes.visible;
200
217
  // First change is reflected by the ngOnInit seed; only later toggles drive the keep-alive.
201
218
  if (visibleChange === undefined || visibleChange.firstChange) return;
@@ -203,7 +220,10 @@ export class Modal implements IAngularModalInputs, OnInit, OnChanges {
203
220
  // Queue on a microtask so the reducer runs AFTER this CD pass renders with the OLD state —
204
221
  // the keep-alive frame. A synchronous dispatch here would unmount in the same pass.
205
222
  queueMicrotask(() => {
206
- this.state = modalReducer(this.state, isVisible ? { type: 'show' } : { type: 'hide' });
223
+ this.state = modalReducer(
224
+ this.state,
225
+ isVisible ? { type: 'show' } : { type: 'hide' },
226
+ );
207
227
  this.changeDetector.markForCheck();
208
228
  });
209
229
  }
@@ -233,13 +253,34 @@ export class Modal implements IAngularModalInputs, OnInit, OnChanges {
233
253
  });
234
254
  }
235
255
 
256
+ // The anchor's class-derived style is NOT an @Input: `class="..."`/`[ngClass]` at the use site
257
+ // resolves through the renderer's addClass onto this component's anchor host (see
258
+ // anchorHostStyle's doc comment), so it never shows up in SimpleChanges and ngOnChanges alone
259
+ // would leave a later class toggle stranded. ngDoCheck runs at exactly the cadence the old
260
+ // getter was re-read, and bumps only on a real identity change.
261
+ ngDoCheck(): void {
262
+ const anchorStyle = anchorHostStyle(this.elementRef);
263
+ if (anchorStyle === this.lastAnchorStyle) return;
264
+ this.lastAnchorStyle = anchorStyle;
265
+ this.hostPropsRevision.update(revision => revision + 1);
266
+ }
267
+
236
268
  // renderModal's own MODAL_HOST_STYLE (position:'absolute') is the outer symbiote-modal node's
237
269
  // style; the anchor's class-derived style goes FIRST, that resolved style SECOND —
238
270
  // flattenStyle's later-wins collapse keeps the modal's own style winning over its ambient class.
239
- get hostProps(): Record<string, unknown> {
271
+ //
272
+ // A computed(), not a getter: Angular re-reads a template getter on every refresh of this view
273
+ // and `[symbioteHostProps]` compares by REFERENCE, so a freshly rebuilt (but identical) bag
274
+ // re-pushes every key through renderer.setProperty -> the engine's prop routing - and this one
275
+ // re-runs renderModal to build it.
276
+ readonly hostProps = computed<Record<string, unknown>>(() => {
277
+ this.hostPropsRevision();
240
278
  const descriptorProps = this.descriptor.props;
241
- return { ...descriptorProps, style: [anchorHostStyle(this.elementRef), descriptorProps.style] };
242
- }
279
+ return {
280
+ ...descriptorProps,
281
+ style: [anchorHostStyle(this.elementRef), descriptorProps.style],
282
+ };
283
+ });
243
284
 
244
285
  get containerStyle(): unknown {
245
286
  const container = this.descriptor.children[0];
@@ -250,15 +291,11 @@ export class Modal implements IAngularModalInputs, OnInit, OnChanges {
250
291
  if (isSymbioteEvent(event)) emitter.emit(event);
251
292
  }
252
293
 
253
- emitOrientation(event: unknown): void {
254
- if (!isSymbioteEvent(event)) return;
255
- const change = readOrientation(event);
256
- if (change !== undefined) this.orientationChange.emit(change);
257
- }
258
-
259
294
  // Typed as the a11y intersection WITH the string index (the bag renderModal spreads into the
260
295
  // host props), so resolveAccessibilityProps's result stays assignable to passthrough.
261
- private accessibilityInputs(): IAccessibilityProps & IAriaProps & Record<string, unknown> {
296
+ private accessibilityInputs(): IAccessibilityProps &
297
+ IAriaProps &
298
+ Record<string, unknown> {
262
299
  return {
263
300
  testID: this.testID,
264
301
  accessible: this.accessible,
@@ -276,8 +313,10 @@ export class Modal implements IAngularModalInputs, OnInit, OnChanges {
276
313
  accessibilityElementsHidden: this.accessibilityElementsHidden,
277
314
  accessibilityIgnoresInvertColors: this.accessibilityIgnoresInvertColors,
278
315
  accessibilityLanguage: this.accessibilityLanguage,
279
- accessibilityRespondsToUserInteraction: this.accessibilityRespondsToUserInteraction,
280
- accessibilityShowsLargeContentViewer: this.accessibilityShowsLargeContentViewer,
316
+ accessibilityRespondsToUserInteraction:
317
+ this.accessibilityRespondsToUserInteraction,
318
+ accessibilityShowsLargeContentViewer:
319
+ this.accessibilityShowsLargeContentViewer,
281
320
  accessibilityLargeContentTitle: this.accessibilityLargeContentTitle,
282
321
  role: this.role,
283
322
  'aria-label': this.ariaLabel,
@@ -5,10 +5,14 @@ import {
5
5
  Component,
6
6
  ElementRef,
7
7
  EventEmitter,
8
+ computed,
8
9
  inject,
9
10
  Input,
10
11
  Output,
12
+ signal,
11
13
  ViewChild,
14
+ type DoCheck,
15
+ type OnChanges,
12
16
  } from '@angular/core';
13
17
  import {
14
18
  createPressHandlers,
@@ -40,15 +44,24 @@ import {
40
44
  type IStyleProp,
41
45
  type IViewStyle,
42
46
  } from '@symbiote-native/engine';
43
- import { anchorHostStyle, SymbioteHostPropsDirective, ViewHost } from '../../primitives';
47
+ import {
48
+ anchorHostStyle,
49
+ SymbioteHostPropsDirective,
50
+ SymbioteStyleInputDirective,
51
+ ViewHost,
52
+ } from '../../primitives';
44
53
 
45
- export type { IPressState, IPressableAndroidRippleConfig } from '@symbiote-native/components';
54
+ export type {
55
+ IPressState,
56
+ IPressableAndroidRippleConfig,
57
+ } from '@symbiote-native/components';
46
58
 
47
59
  // The full logical Pressable surface, callback-shaped press/hover events included. Touchable* and
48
60
  // TouchableNativeFeedback still take their OWN onPress/onPressIn/... as @Input() callbacks (that is
49
61
  // a separate, not-yet-converted decision) and derive their public prop types from this - so it keeps
50
62
  // the callback fields even though the Pressable component below no longer implements them directly.
51
- export interface IAngularPressableProps extends IAccessibilityProps, IAriaProps {
63
+ export interface IAngularPressableProps
64
+ extends IAccessibilityProps, IAriaProps {
52
65
  onPress?: IPressHandler;
53
66
  onPressIn?: IPressHandler;
54
67
  onPressOut?: IPressHandler;
@@ -74,7 +87,8 @@ export interface IAngularPressableProps extends IAccessibilityProps, IAriaProps
74
87
  nextFocusLeft?: number;
75
88
  nextFocusRight?: number;
76
89
  nextFocusUp?: number;
77
- style?: IStyleProp<IViewStyle> | ((state: IPressState) => IStyleProp<IViewStyle>);
90
+ style?:
91
+ IStyleProp<IViewStyle> | ((state: IPressState) => IStyleProp<IViewStyle>);
78
92
  }
79
93
 
80
94
  // What the Pressable component itself takes as plain @Input()s: the full surface minus the press/
@@ -96,7 +110,9 @@ export type IAngularPressableInputs = Omit<
96
110
  | 'onAccessibilityEscape'
97
111
  >;
98
112
 
99
- function isStyleFn(value: unknown): value is (state: IPressState) => IStyleProp<IViewStyle> {
113
+ function isStyleFn(
114
+ value: unknown,
115
+ ): value is (state: IPressState) => IStyleProp<IViewStyle> {
100
116
  return typeof value === 'function';
101
117
  }
102
118
 
@@ -109,12 +125,15 @@ function asSymbioteEvent(event: unknown): ISymbioteEvent | undefined {
109
125
  @Component({
110
126
  selector: 'Pressable, symbiote-pressable',
111
127
  standalone: true,
128
+ hostDirectives: [
129
+ { directive: SymbioteStyleInputDirective, inputs: ['style'] },
130
+ ],
112
131
  schemas: [CUSTOM_ELEMENTS_SCHEMA],
113
132
  imports: [ViewHost, SymbioteHostPropsDirective],
114
133
  template: `
115
134
  <symbiote-view
116
135
  #host
117
- [symbioteHostProps]="hostProps"
136
+ [symbioteHostProps]="hostProps()"
118
137
  (accessibilityAction)="emit(accessibilityAction, $event)"
119
138
  (accessibilityTap)="emit(accessibilityTap, $event)"
120
139
  (magicTap)="emit(magicTap, $event)"
@@ -131,7 +150,7 @@ function asSymbioteEvent(event: unknown): ISymbioteEvent | undefined {
131
150
  `,
132
151
  changeDetection: ChangeDetectionStrategy.OnPush,
133
152
  })
134
- export class Pressable implements IAngularPressableInputs {
153
+ export class Pressable implements IAngularPressableInputs, OnChanges, DoCheck {
135
154
  // The press/hover lifecycle as real Angular events: `(press)="onTap($event)"`, not
136
155
  // `[onPress]="onTap"`. createPressHandlers still wants plain IPressHandler callbacks, so
137
156
  // emitterHandler() below adapts each EventEmitter into one - only while something is actually
@@ -176,10 +195,13 @@ export class Pressable implements IAngularPressableInputs {
176
195
  @Input() accessibilityRole?: IAngularPressableProps['accessibilityRole'];
177
196
  @Input() accessibilityState?: IAccessibilityStateValue;
178
197
  @Input() accessibilityValue?: IAngularPressableProps['accessibilityValue'];
179
- @Input() accessibilityActions?: IAngularPressableProps['accessibilityActions'];
198
+ @Input()
199
+ accessibilityActions?: IAngularPressableProps['accessibilityActions'];
180
200
  @Input() accessibilityLabelledBy?: string | string[];
181
- @Input() importantForAccessibility?: IAccessibilityProps['importantForAccessibility'];
182
- @Input() accessibilityLiveRegion?: IAccessibilityProps['accessibilityLiveRegion'];
201
+ @Input()
202
+ importantForAccessibility?: IAccessibilityProps['importantForAccessibility'];
203
+ @Input()
204
+ accessibilityLiveRegion?: IAccessibilityProps['accessibilityLiveRegion'];
183
205
  @Input() screenReaderFocusable?: boolean;
184
206
  @Input() accessibilityViewIsModal?: boolean;
185
207
  @Input() accessibilityElementsHidden?: boolean;
@@ -205,13 +227,23 @@ export class Pressable implements IAngularPressableInputs {
205
227
  @Input() id?: string;
206
228
  @Input() role?: IAngularPressableProps['role'];
207
229
 
208
- @ViewChild('host', { read: ElementRef }) private hostElement?: ElementRef<unknown>;
230
+ @ViewChild('host', { read: ElementRef })
231
+ private hostElement?: ElementRef<unknown>;
232
+
233
+ // INTERNAL mutable state, driven by the press machine rather than by an @Input, so ngOnChanges
234
+ // can never cover it - it carries its OWN signal, which is what lets the memoized bag below stay
235
+ // correct across a press. Exposed as a plain boolean getter so a use site's `#ref.pressed` keeps
236
+ // reading a boolean and the public surface is unchanged.
237
+ private readonly pressedState = signal(false);
238
+
239
+ get pressed(): boolean {
240
+ return this.pressedState();
241
+ }
209
242
 
210
- pressed = false;
211
243
  private readonly runtime: IPressRuntime = createPressRuntime();
212
244
  private readonly host: IPressHost = {
213
245
  setPressed: pressed => {
214
- this.pressed = pressed;
246
+ this.pressedState.set(pressed);
215
247
  this.changeDetector.detectChanges();
216
248
  },
217
249
  getMeasureFn: () => {
@@ -231,6 +263,39 @@ export class Pressable implements IAngularPressableInputs {
231
263
  // inner `symbiote-view` one level down.
232
264
  private readonly elementRef = inject(ElementRef);
233
265
 
266
+ // Bridges the non-reactive @Input fields above into the reactive graph, so the computed() bag
267
+ // below can memoize a value derived from them. Signal inputs would make this unnecessary, but
268
+ // `input()` is visible only to the AOT compiler and this package's unit suite runs on JIT - see
269
+ // the `angular-adapter-change-detection` skill, §6. Mirrors ScrollView's identical bridge
270
+ // (components/scroll-view/shared.ts), including its constraint: ONLY safe for a bag whose every
271
+ // dependency is an @Input or carries its own signal, never for plain internal mutable state.
272
+ private readonly inputsRevision = signal(0);
273
+
274
+ // The anchor's class-derived style is the one hostProps dependency that is NOT an @Input:
275
+ // `class="..."`/`[class.x]`/`[ngClass]` at the use site resolves through the renderer's
276
+ // addClass/removeClass onto the anchor node (anchorHostStyle's doc comment), so it never appears
277
+ // in SimpleChanges and ngOnChanges cannot cover it. The engine's commitClassStyle allocates a
278
+ // fresh [classStyle, explicitStyle] array on exactly the passes where a class token actually
279
+ // moved, so signal.set's own Object.is check makes an unchanged poll a no-op that dirties nothing.
280
+ private readonly anchorStyle = signal<unknown>(undefined);
281
+
282
+ // ngDoCheck is the only hook that observes the anchor AFTER the parent wrote it. Angular flushes
283
+ // a node's pre-order hooks at the next `advance` PAST it (hooks.ts callHooks runs "until that
284
+ // node index exclusive") or in refreshView's post-template flush - both strictly after the
285
+ // parent's classProp on this element, and strictly before this component's own view refreshes.
286
+ // Angular also nulls the active consumer around lifecycle hooks (render3/hooks.ts), so this
287
+ // write registers no dependency on the caller's view and cannot throw NG0600.
288
+ ngDoCheck(): void {
289
+ this.anchorStyle.set(anchorHostStyle(this.elementRef));
290
+ }
291
+
292
+ ngOnChanges(): void {
293
+ // Tells the computed bag below that an @Input moved - plain class fields are not reactive on
294
+ // their own. Must stay in ngOnChanges: that is the single moment Angular has finished writing
295
+ // every changed input for this pass.
296
+ this.inputsRevision.update(revision => revision + 1);
297
+ }
298
+
234
299
  get resolvedStyle(): IStyleProp<IViewStyle> | undefined {
235
300
  const state: IPressState = { pressed: this.pressed };
236
301
  return isStyleFn(this.style) ? this.style(state) : this.style;
@@ -241,7 +306,10 @@ export class Pressable implements IAngularPressableInputs {
241
306
  accessibilityLabel: this.accessibilityLabel,
242
307
  accessibilityHint: this.accessibilityHint,
243
308
  accessibilityRole: this.accessibilityRole,
244
- accessibilityState: resolveDisabledAccessibilityState(this.accessibilityState, this.disabled),
309
+ accessibilityState: resolveDisabledAccessibilityState(
310
+ this.accessibilityState,
311
+ this.disabled,
312
+ ),
245
313
  accessibilityValue: this.accessibilityValue,
246
314
  accessibilityActions: this.accessibilityActions,
247
315
  accessibilityLabelledBy: this.accessibilityLabelledBy,
@@ -252,8 +320,10 @@ export class Pressable implements IAngularPressableInputs {
252
320
  accessibilityElementsHidden: this.accessibilityElementsHidden,
253
321
  accessibilityIgnoresInvertColors: this.accessibilityIgnoresInvertColors,
254
322
  accessibilityLanguage: this.accessibilityLanguage,
255
- accessibilityRespondsToUserInteraction: this.accessibilityRespondsToUserInteraction,
256
- accessibilityShowsLargeContentViewer: this.accessibilityShowsLargeContentViewer,
323
+ accessibilityRespondsToUserInteraction:
324
+ this.accessibilityRespondsToUserInteraction,
325
+ accessibilityShowsLargeContentViewer:
326
+ this.accessibilityShowsLargeContentViewer,
257
327
  accessibilityLargeContentTitle: this.accessibilityLargeContentTitle,
258
328
  'aria-label': this.ariaLabel,
259
329
  'aria-busy': this.ariaBusy,
@@ -275,11 +345,20 @@ export class Pressable implements IAngularPressableInputs {
275
345
  }
276
346
 
277
347
  get resolvedRippleProps(): Record<string, unknown> | undefined {
278
- return this.android_ripple === undefined ? undefined : rippleProps(this.android_ripple);
348
+ return this.android_ripple === undefined
349
+ ? undefined
350
+ : rippleProps(this.android_ripple);
279
351
  }
280
352
 
281
353
  // Flat bag for `[symbioteHostProps]` (Renderer2.setProperty per key) - see primitives/shared.ts.
282
- get hostProps(): Record<string, unknown> {
354
+ //
355
+ // MEASURED 2026-08-16: as a getter this rebuilt the bag on every refresh of this view, and a
356
+ // press dirties this very view (SymbioteHostPropsDirective wraps each flat-bag onX with
357
+ // markForCheck, and setPressed calls detectChanges outright), so `[symbioteHostProps]`'s
358
+ // reference check failed every time and re-pushed every key through Renderer2.setProperty ->
359
+ // routeProp for an identical bag. Memoized, an unchanged bag costs one reference compare.
360
+ readonly hostProps = computed<Record<string, unknown>>(() => {
361
+ this.inputsRevision();
283
362
  return {
284
363
  testID: this.testID,
285
364
  nativeID: this.nativeID,
@@ -289,36 +368,42 @@ export class Pressable implements IAngularPressableInputs {
289
368
  nextFocusLeft: this.nextFocusLeft,
290
369
  nextFocusRight: this.nextFocusRight,
291
370
  nextFocusUp: this.nextFocusUp,
292
- style: [anchorHostStyle(this.elementRef), this.resolvedStyle],
371
+ style: [this.anchorStyle(), this.resolvedStyle],
293
372
  accessible: this.accessible,
294
373
  ...this.foldedAccessibility,
295
374
  android_disableSound: this.android_disableSound,
296
- nativeBackgroundAndroid: this.resolvedRippleProps?.nativeBackgroundAndroid,
297
- nativeForegroundAndroid: this.resolvedRippleProps?.nativeForegroundAndroid,
375
+ nativeBackgroundAndroid:
376
+ this.resolvedRippleProps?.nativeBackgroundAndroid,
377
+ nativeForegroundAndroid:
378
+ this.resolvedRippleProps?.nativeForegroundAndroid,
298
379
  };
299
- }
380
+ });
300
381
 
301
382
  handlePressIn(event: unknown): void {
302
383
  const symbioteEvent = asSymbioteEvent(event);
303
- if (shouldSuppressPress(this.disabled) || symbioteEvent === undefined) return;
384
+ if (shouldSuppressPress(this.disabled) || symbioteEvent === undefined)
385
+ return;
304
386
  this.handlers.handlePressIn(symbioteEvent);
305
387
  }
306
388
 
307
389
  handlePressOut(event: unknown): void {
308
390
  const symbioteEvent = asSymbioteEvent(event);
309
- if (shouldSuppressPress(this.disabled) || symbioteEvent === undefined) return;
391
+ if (shouldSuppressPress(this.disabled) || symbioteEvent === undefined)
392
+ return;
310
393
  this.handlers.handlePressOut(symbioteEvent);
311
394
  }
312
395
 
313
396
  handlePress(event: unknown): void {
314
397
  const symbioteEvent = asSymbioteEvent(event);
315
- if (shouldSuppressPress(this.disabled) || symbioteEvent === undefined) return;
398
+ if (shouldSuppressPress(this.disabled) || symbioteEvent === undefined)
399
+ return;
316
400
  this.handlers.handlePress(symbioteEvent);
317
401
  }
318
402
 
319
403
  handleResponderMove(event: unknown): void {
320
404
  const symbioteEvent = asSymbioteEvent(event);
321
- if (shouldSuppressPress(this.disabled) || symbioteEvent === undefined) return;
405
+ if (shouldSuppressPress(this.disabled) || symbioteEvent === undefined)
406
+ return;
322
407
  this.handlers.handleResponderMove(symbioteEvent);
323
408
  }
324
409
 
@@ -339,14 +424,18 @@ export class Pressable implements IAngularPressableInputs {
339
424
  // is listening anyway, mirroring core's noteHoverNoop but off `.observed` instead of an @Input.
340
425
  private noteHoverNoop(): void {
341
426
  if (this.hoverIn.observed || this.hoverOut.observed) {
342
- dlog('Pressable hover is a no-op on this host (no pointer-enter/leave event)');
427
+ dlog(
428
+ 'Pressable hover is a no-op on this host (no pointer-enter/leave event)',
429
+ );
343
430
  }
344
431
  }
345
432
 
346
433
  // Wraps an @Output() as an IPressHandler only while it has a subscriber, so an unbound
347
434
  // (longPress) still skips arming the timer, matching createPressHandlers' "undefined means
348
435
  // nobody cares" contract.
349
- private emitterHandler(emitter: EventEmitter<ISymbioteEvent>): IPressHandler | undefined {
436
+ private emitterHandler(
437
+ emitter: EventEmitter<ISymbioteEvent>,
438
+ ): IPressHandler | undefined {
350
439
  return emitter.observed ? event => emitter.emit(event) : undefined;
351
440
  }
352
441