@symbiote-native/angular 0.7.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (188) hide show
  1. package/README.md +3 -3
  2. package/babel-lower-host-primitives.cjs +636 -0
  3. package/babel-register-composed.cjs +21 -3
  4. package/build/angular/anchor-host-registry.js +3 -1
  5. package/build/angular/anchor-host-registry.js.map +1 -1
  6. package/build/angular/bootstrap.js +1 -1
  7. package/build/angular/bootstrap.js.map +1 -1
  8. package/build/angular/components/activity-indicator/index.android.js +7 -2
  9. package/build/angular/components/activity-indicator/index.android.js.map +1 -1
  10. package/build/angular/components/activity-indicator/index.ios.js +3 -1
  11. package/build/angular/components/activity-indicator/index.ios.js.map +1 -1
  12. package/build/angular/components/activity-indicator/shared.d.ts +1 -1
  13. package/build/angular/components/activity-indicator/shared.js +1 -1
  14. package/build/angular/components/activity-indicator/shared.js.map +1 -1
  15. package/build/angular/components/button.d.ts +3 -0
  16. package/build/angular/components/button.js +22 -5
  17. package/build/angular/components/button.js.map +1 -1
  18. package/build/angular/components/flat-list/index.d.ts +1 -1
  19. package/build/angular/components/flat-list/index.js +47 -14
  20. package/build/angular/components/flat-list/index.js.map +1 -1
  21. package/build/angular/components/image/index.android.js +4 -10
  22. package/build/angular/components/image/index.android.js.map +1 -1
  23. package/build/angular/components/image/index.ios.js +4 -10
  24. package/build/angular/components/image/index.ios.js.map +1 -1
  25. package/build/angular/components/image/shared.d.ts +4 -4
  26. package/build/angular/components/image/shared.js +51 -33
  27. package/build/angular/components/image/shared.js.map +1 -1
  28. package/build/angular/components/image-background.d.ts +3 -0
  29. package/build/angular/components/image-background.js +32 -9
  30. package/build/angular/components/image-background.js.map +1 -1
  31. package/build/angular/components/input-accessory-view/index.d.ts +2 -1
  32. package/build/angular/components/input-accessory-view/index.js +18 -16
  33. package/build/angular/components/input-accessory-view/index.js.map +1 -1
  34. package/build/angular/components/keyboard-avoiding-view/index.d.ts +3 -1
  35. package/build/angular/components/keyboard-avoiding-view/index.js +56 -19
  36. package/build/angular/components/keyboard-avoiding-view/index.js.map +1 -1
  37. package/build/angular/components/modal/index.d.ts +1 -0
  38. package/build/angular/components/modal/index.js +21 -10
  39. package/build/angular/components/modal/index.js.map +1 -1
  40. package/build/angular/components/pressable/index.d.ts +10 -5
  41. package/build/angular/components/pressable/index.js +61 -16
  42. package/build/angular/components/pressable/index.js.map +1 -1
  43. package/build/angular/components/refresh-control/index.d.ts +2 -1
  44. package/build/angular/components/refresh-control/index.js +13 -9
  45. package/build/angular/components/refresh-control/index.js.map +1 -1
  46. package/build/angular/components/safe-area-view/index.d.ts +3 -1
  47. package/build/angular/components/safe-area-view/index.js +22 -20
  48. package/build/angular/components/safe-area-view/index.js.map +1 -1
  49. package/build/angular/components/scroll-view/index.android.js +44 -14
  50. package/build/angular/components/scroll-view/index.android.js.map +1 -1
  51. package/build/angular/components/scroll-view/index.ios.js +13 -5
  52. package/build/angular/components/scroll-view/index.ios.js.map +1 -1
  53. package/build/angular/components/scroll-view/projection.d.ts +32 -0
  54. package/build/angular/components/scroll-view/projection.js +212 -47
  55. package/build/angular/components/scroll-view/projection.js.map +1 -1
  56. package/build/angular/components/scroll-view/shared.d.ts +2 -0
  57. package/build/angular/components/scroll-view/shared.js +48 -11
  58. package/build/angular/components/scroll-view/shared.js.map +1 -1
  59. package/build/angular/components/scroll-view/sticky-header.js +7 -3
  60. package/build/angular/components/scroll-view/sticky-header.js.map +1 -1
  61. package/build/angular/components/section-list/index.d.ts +9 -1
  62. package/build/angular/components/section-list/index.js +72 -18
  63. package/build/angular/components/section-list/index.js.map +1 -1
  64. package/build/angular/components/switch/index.android.js +23 -9
  65. package/build/angular/components/switch/index.android.js.map +1 -1
  66. package/build/angular/components/switch/index.ios.js +23 -9
  67. package/build/angular/components/switch/index.ios.js.map +1 -1
  68. package/build/angular/components/switch/shared.d.ts +1 -1
  69. package/build/angular/components/switch/shared.js +5 -2
  70. package/build/angular/components/switch/shared.js.map +1 -1
  71. package/build/angular/components/text-input.d.ts +8 -2
  72. package/build/angular/components/text-input.js +79 -34
  73. package/build/angular/components/text-input.js.map +1 -1
  74. package/build/angular/components/touchable/index.d.ts +36 -7
  75. package/build/angular/components/touchable/index.js +280 -72
  76. package/build/angular/components/touchable/index.js.map +1 -1
  77. package/build/angular/components/touchable-native-feedback/index.d.ts +2 -1
  78. package/build/angular/components/touchable-native-feedback/index.js +19 -17
  79. package/build/angular/components/touchable-native-feedback/index.js.map +1 -1
  80. package/build/angular/components/virtualized-list/directives.js +4 -0
  81. package/build/angular/components/virtualized-list/directives.js.map +1 -1
  82. package/build/angular/components/virtualized-list/index.d.ts +8 -2
  83. package/build/angular/components/virtualized-list/index.js +207 -61
  84. package/build/angular/components/virtualized-list/index.js.map +1 -1
  85. package/build/angular/components/virtualized-section-list/directives.js.map +1 -1
  86. package/build/angular/components/virtualized-section-list/index.d.ts +21 -1
  87. package/build/angular/components/virtualized-section-list/index.js +100 -22
  88. package/build/angular/components/virtualized-section-list/index.js.map +1 -1
  89. package/build/angular/components.d.ts +2 -2
  90. package/build/angular/components.js +1 -1
  91. package/build/angular/components.js.map +1 -1
  92. package/build/angular/create-portal/index.d.ts +11 -2
  93. package/build/angular/create-portal/index.js +42 -12
  94. package/build/angular/create-portal/index.js.map +1 -1
  95. package/build/angular/create-tunnel/index.js.map +1 -1
  96. package/build/angular/descriptor-to-angular/index.js +3 -1
  97. package/build/angular/descriptor-to-angular/index.js.map +1 -1
  98. package/build/angular/diagnostics.d.ts +33 -0
  99. package/build/angular/diagnostics.js +82 -0
  100. package/build/angular/diagnostics.js.map +1 -0
  101. package/build/angular/gate-demand.d.ts +37 -0
  102. package/build/angular/gate-demand.js +84 -0
  103. package/build/angular/gate-demand.js.map +1 -0
  104. package/build/angular/host-instance/index.js +1 -1
  105. package/build/angular/host-instance/index.js.map +1 -1
  106. package/build/angular/index.d.ts +6 -2
  107. package/build/angular/index.js +16 -1
  108. package/build/angular/index.js.map +1 -1
  109. package/build/angular/modules/animated/create-animated-component.d.ts +1 -0
  110. package/build/angular/modules/animated/create-animated-component.js +41 -18
  111. package/build/angular/modules/animated/create-animated-component.js.map +1 -1
  112. package/build/angular/modules/app-registry/index.js.map +1 -1
  113. package/build/angular/modules/status-bar/index.js +1 -1
  114. package/build/angular/modules/status-bar/index.js.map +1 -1
  115. package/build/angular/primitives/index.d.ts +31 -2
  116. package/build/angular/primitives/index.js +81 -3
  117. package/build/angular/primitives/index.js.map +1 -1
  118. package/build/angular/primitives/shared.d.ts +9 -1
  119. package/build/angular/primitives/shared.js +49 -3
  120. package/build/angular/primitives/shared.js.map +1 -1
  121. package/build/angular/register.d.ts +1 -0
  122. package/build/angular/register.js +35 -0
  123. package/build/angular/register.js.map +1 -0
  124. package/build/angular/render/index.js +51 -6
  125. package/build/angular/render/index.js.map +1 -1
  126. package/build/angular/renderer/index.d.ts +1 -0
  127. package/build/angular/renderer/index.js +157 -21
  128. package/build/angular/renderer/index.js.map +1 -1
  129. package/build/angular/services/color-scheme.service.js.map +1 -1
  130. package/build/angular/services/window-dimensions.service.js +1 -1
  131. package/build/angular/services/window-dimensions.service.js.map +1 -1
  132. package/build/angular/state-style.d.ts +9 -0
  133. package/build/angular/state-style.js +39 -0
  134. package/build/angular/state-style.js.map +1 -0
  135. package/metro-transformer.cjs +69 -0
  136. package/package.json +27 -8
  137. package/src/anchor-host-registry.ts +3 -1
  138. package/src/bootstrap.ts +4 -1
  139. package/src/components/activity-indicator/index.android.ts +7 -2
  140. package/src/components/activity-indicator/index.ios.ts +3 -1
  141. package/src/components/activity-indicator/shared.ts +32 -13
  142. package/src/components/button.ts +36 -6
  143. package/src/components/flat-list/index.ts +91 -29
  144. package/src/components/image/index.android.ts +9 -6
  145. package/src/components/image/index.ios.ts +9 -6
  146. package/src/components/image/shared.ts +110 -45
  147. package/src/components/image-background.ts +47 -10
  148. package/src/components/input-accessory-view/index.ts +41 -18
  149. package/src/components/keyboard-avoiding-view/index.ts +76 -22
  150. package/src/components/modal/index.ts +39 -12
  151. package/src/components/pressable/index.ts +121 -31
  152. package/src/components/refresh-control/index.ts +36 -13
  153. package/src/components/safe-area-view/index.ts +44 -19
  154. package/src/components/scroll-view/index.android.ts +31 -9
  155. package/src/components/scroll-view/index.ios.ts +17 -4
  156. package/src/components/scroll-view/projection.ts +290 -63
  157. package/src/components/scroll-view/shared.ts +124 -30
  158. package/src/components/scroll-view/sticky-header.ts +25 -8
  159. package/src/components/section-list/index.ts +79 -19
  160. package/src/components/switch/index.android.ts +18 -7
  161. package/src/components/switch/index.ios.ts +18 -7
  162. package/src/components/switch/shared.ts +15 -4
  163. package/src/components/text-input.ts +119 -31
  164. package/src/components/touchable/index.ts +339 -72
  165. package/src/components/touchable-native-feedback/index.ts +50 -18
  166. package/src/components/virtualized-list/directives.ts +19 -5
  167. package/src/components/virtualized-list/index.ts +292 -72
  168. package/src/components/virtualized-section-list/directives.ts +6 -3
  169. package/src/components/virtualized-section-list/index.ts +164 -34
  170. package/src/components.ts +6 -1
  171. package/src/create-portal/index.ts +47 -11
  172. package/src/create-tunnel/index.ts +3 -1
  173. package/src/descriptor-to-angular/index.ts +29 -8
  174. package/src/diagnostics.ts +138 -0
  175. package/src/gate-demand.ts +109 -0
  176. package/src/host-instance/index.ts +9 -3
  177. package/src/index.ts +33 -2
  178. package/src/modules/animated/create-animated-component.ts +67 -19
  179. package/src/modules/app-registry/index.ts +3 -1
  180. package/src/modules/status-bar/index.ts +13 -4
  181. package/src/primitives/index.ts +73 -2
  182. package/src/primitives/shared.ts +70 -9
  183. package/src/register.ts +43 -0
  184. package/src/render/index.ts +59 -7
  185. package/src/renderer/index.ts +209 -37
  186. package/src/services/color-scheme.service.ts +3 -1
  187. package/src/services/window-dimensions.service.ts +11 -4
  188. package/src/state-style.ts +39 -0
@@ -63,6 +63,11 @@ import {
63
63
  type IViewStyle,
64
64
  } from '@symbiote-native/engine';
65
65
  import { anchorHostStyle, SymbioteHostPropsDirective } from '../../primitives';
66
+ import {
67
+ gateWanted,
68
+ injectGateDemand,
69
+ type IGatedAccessibilityEvent,
70
+ } from '../../gate-demand';
66
71
  import { RefreshControl } from '../refresh-control';
67
72
  import { ScrollViewProjectionController } from './projection';
68
73
 
@@ -164,7 +169,8 @@ export const SCROLL_VIEW_INPUTS = [
164
169
  // (ReactNode children, ReactElement refreshControl) — Angular takes children via <ng-content> and
165
170
  // composes RefreshControl through projection instead. Declared per-adapter over the shared
166
171
  // accessibility base since a framework element/ref field can't live in a shared agnostic type.
167
- export interface IAngularScrollViewProps extends IAccessibilityProps, IAriaProps {
172
+ export interface IAngularScrollViewProps
173
+ extends IAccessibilityProps, IAriaProps {
168
174
  style?: IStyleProp<IViewStyle>;
169
175
  // A bare string resolves through the shared style registry, like `class` on the host node.
170
176
  contentContainerStyle?: IStyleProp<IViewStyle> | string;
@@ -176,7 +182,12 @@ export interface IAngularScrollViewProps extends IAccessibilityProps, IAriaProps
176
182
  bounces?: boolean;
177
183
  decelerationRate?: 'normal' | 'fast' | number;
178
184
  scrollEventThrottle?: number;
179
- contentInset?: { top?: number; left?: number; bottom?: number; right?: number };
185
+ contentInset?: {
186
+ top?: number;
187
+ left?: number;
188
+ bottom?: number;
189
+ right?: number;
190
+ };
180
191
  contentOffset?: { x: number; y: number };
181
192
  removeClippedSubviews?: boolean;
182
193
  snapToInterval?: number;
@@ -204,11 +215,17 @@ export interface IAngularScrollViewProps extends IAccessibilityProps, IAriaProps
204
215
  alwaysBounceHorizontal?: boolean;
205
216
  alwaysBounceVertical?: boolean;
206
217
  centerContent?: boolean;
207
- scrollIndicatorInsets?: { top?: number; left?: number; bottom?: number; right?: number };
218
+ scrollIndicatorInsets?: {
219
+ top?: number;
220
+ left?: number;
221
+ bottom?: number;
222
+ right?: number;
223
+ };
208
224
  indicatorStyle?: 'default' | 'black' | 'white';
209
225
  directionalLockEnabled?: boolean;
210
226
  automaticallyAdjustKeyboardInsets?: boolean;
211
- contentInsetAdjustmentBehavior?: 'automatic' | 'scrollableAxes' | 'never' | 'always';
227
+ contentInsetAdjustmentBehavior?:
228
+ 'automatic' | 'scrollableAxes' | 'never' | 'always';
212
229
  minimumZoomScale?: number;
213
230
  maximumZoomScale?: number;
214
231
  zoomScale?: number;
@@ -357,6 +374,10 @@ export abstract class ScrollViewBase
357
374
  @Output() readonly magicTap = new EventEmitter<ISymbioteEvent>();
358
375
  @Output() readonly accessibilityEscape = new EventEmitter<ISymbioteEvent>();
359
376
 
377
+ // Null unless an adapter wrapper renders this ScrollView in its own template — VirtualizedList
378
+ // does. An app's own `<ScrollView>` gets null and answers from its own `.observed`.
379
+ private readonly gateDemand = injectGateDemand();
380
+
360
381
  testID: string | undefined;
361
382
  nativeID: string | undefined;
362
383
  accessible: boolean | undefined;
@@ -399,7 +420,8 @@ export abstract class ScrollViewBase
399
420
  @ViewChild('host', { read: SymbioteHostPropsDirective })
400
421
  private hostDirective?: SymbioteHostPropsDirective;
401
422
 
402
- @ContentChild(RefreshControl) protected projectedRefreshControl?: RefreshControl;
423
+ @ContentChild(RefreshControl)
424
+ protected projectedRefreshControl?: RefreshControl;
403
425
 
404
426
  // This component's OWN anchor host — where a `class="..."` at the use site resolves (see
405
427
  // anchorHostStyle's doc comment) — NOT `#host` in the platform templates, which targets the real
@@ -459,7 +481,9 @@ export abstract class ScrollViewBase
459
481
  // (commit.ts) falls back to Object.is on a function leaf, so even a structurally-identical bag
460
482
  // reads as "props changed" and forces a real Fabric re-clone that cascades up every ancestor
461
483
  // (see AnimatedComponentBase.reconcile()'s identical warning).
462
- private readonly handleInvertedStickyLayout = (event: ISymbioteEvent): void => {
484
+ private readonly handleInvertedStickyLayout = (
485
+ event: ISymbioteEvent,
486
+ ): void => {
463
487
  const height = readLayoutDimension(event, 'height');
464
488
  // The projection controller is pushed from here rather than picked up on the next prop-bag
465
489
  // read: viewportHeight is internal mutable state, so once `scrollProps` became a memoized
@@ -502,7 +526,8 @@ export abstract class ScrollViewBase
502
526
  // new listener wrapper (and a new AnimatedEvent walking `collectMappedValues`) on every unrelated
503
527
  // change-detection pass while sticky headers are active.
504
528
  private cachedStickyOnScrollSource: IScrollHandler | undefined;
505
- private cachedStickyOnScrollHandler: ((...args: readonly unknown[]) => void) | undefined;
529
+ private cachedStickyOnScrollHandler:
530
+ ((...args: readonly unknown[]) => void) | undefined;
506
531
 
507
532
  get isHorizontal(): boolean {
508
533
  const value = this.horizontal === true;
@@ -516,20 +541,26 @@ export abstract class ScrollViewBase
516
541
  }
517
542
 
518
543
  private get hasStickyHeaders(): boolean {
519
- return this.stickyHeaderIndices !== undefined && this.stickyHeaderIndices.length > 0;
544
+ return (
545
+ this.stickyHeaderIndices !== undefined &&
546
+ this.stickyHeaderIndices.length > 0
547
+ );
520
548
  }
521
549
 
522
550
  // A class-name string resolves through the shared registry before it reaches
523
551
  // selectScrollIntrinsics, which only understands style objects/arrays.
524
- private get resolvedContentContainerStyle(): IStyleProp<IViewStyle> | undefined {
552
+ private get resolvedContentContainerStyle():
553
+ IStyleProp<IViewStyle> | undefined {
525
554
  return typeof this.contentContainerStyle === 'string'
526
555
  ? resolveClassName(this.contentContainerStyle)
527
556
  : this.contentContainerStyle;
528
557
  }
529
558
 
530
559
  private get scrollViewBaseStyle(): IViewStyle {
531
- return selectScrollIntrinsics(this.isHorizontal, this.resolvedContentContainerStyle)
532
- .scrollViewBaseStyle;
560
+ return selectScrollIntrinsics(
561
+ this.isHorizontal,
562
+ this.resolvedContentContainerStyle,
563
+ ).scrollViewBaseStyle;
533
564
  }
534
565
 
535
566
  // Bridges non-reactive @Input fields into the reactive graph, so a computed() can memoize a
@@ -602,10 +633,14 @@ export abstract class ScrollViewBase
602
633
  onMomentumScrollBegin: this.onMomentumScrollBegin,
603
634
  onMomentumScrollEnd: this.onMomentumScrollEnd,
604
635
  onScrollToTop: this.emitterCallback(this.scrollToTop),
605
- onAccessibilityAction: this.emitterCallback(this.accessibilityAction),
606
- onAccessibilityTap: this.emitterCallback(this.accessibilityTap),
607
- onMagicTap: this.emitterCallback(this.magicTap),
608
- onAccessibilityEscape: this.emitterCallback(this.accessibilityEscape),
636
+ onAccessibilityAction: this.gatedAccessibilityCallback(
637
+ 'accessibilityAction',
638
+ ),
639
+ onAccessibilityTap: this.gatedAccessibilityCallback('accessibilityTap'),
640
+ onMagicTap: this.gatedAccessibilityCallback('magicTap'),
641
+ onAccessibilityEscape: this.gatedAccessibilityCallback(
642
+ 'accessibilityEscape',
643
+ ),
609
644
  });
610
645
 
611
646
  // RN defaults nested scrolling ON (ScrollView.js `nestedScrollEnabled ?? true`); Android needs
@@ -623,7 +658,8 @@ export abstract class ScrollViewBase
623
658
  // value each frame (JS jitter). The DECISIONS (which path, throttle defaults, whether to
624
659
  // capture viewport height) are folded out to the shared resolveScrollForwarding; Angular only
625
660
  // EXECUTES them, keeping stable-reference handlers so a fresh closure never forces a re-clone.
626
- const nativeStickyAvailable = this.hasStickyHeaders && isNativeAnimatedAvailable();
661
+ const nativeStickyAvailable =
662
+ this.hasStickyHeaders && isNativeAnimatedAvailable();
627
663
  const forwarding = resolveScrollForwarding({
628
664
  hasStickyHeaders: this.hasStickyHeaders,
629
665
  nativeStickyAvailable,
@@ -725,7 +761,9 @@ export abstract class ScrollViewBase
725
761
  this.projectedRefreshControl?.handleRefresh(nativeNode);
726
762
  }
727
763
 
728
- private refreshControlProps(style?: IStyleProp<IViewStyle>): Record<string, unknown> {
764
+ private refreshControlProps(
765
+ style?: IStyleProp<IViewStyle>,
766
+ ): Record<string, unknown> {
729
767
  const refresh = this.projectedRefreshControl;
730
768
  if (refresh === undefined) return {};
731
769
  return compact({
@@ -765,8 +803,14 @@ export abstract class ScrollViewBase
765
803
  this.isHorizontal,
766
804
  this.resolvedContentContainerStyle,
767
805
  );
768
- const bag: Record<string, unknown> = { style: contentStyle, collapsable: false };
769
- if (this.maintainVisibleContentPosition !== undefined || this.snapToAlignment !== undefined) {
806
+ const bag: Record<string, unknown> = {
807
+ style: contentStyle,
808
+ collapsable: false,
809
+ };
810
+ if (
811
+ this.maintainVisibleContentPosition !== undefined ||
812
+ this.snapToAlignment !== undefined
813
+ ) {
770
814
  bag.collapsableChildren = false;
771
815
  }
772
816
  bag.onLayout = this.handleContentLayout;
@@ -783,7 +827,23 @@ export abstract class ScrollViewBase
783
827
  // subscribes a template's (event) listeners in its CREATION pass, before any input is written and
784
828
  // before this component's own view is first refreshed - so `observed` is already settled at the
785
829
  // first evaluation and cannot flip afterwards without destroying the whole component.
786
- private emitterCallback(emitter: EventEmitter<ISymbioteEvent>): IScrollHandler | undefined {
830
+ // The four GATED accessibility events, which `emitterCallback` cannot answer alone: VirtualizedList
831
+ // renders this component and binds all four, so `.observed` is true on every list in the app
832
+ // whether or not anything listens. A demand from above decides instead — see `gate-demand.ts`.
833
+ //
834
+ // Deliberately NOT applied to the projected RefreshControl's four, one site below: that emitter
835
+ // belongs to a component the APP wrote and projected in, so its `.observed` is honest.
836
+ private gatedAccessibilityCallback(
837
+ name: IGatedAccessibilityEvent,
838
+ ): IScrollHandler | undefined {
839
+ const emitter = this[name];
840
+ if (!gateWanted(this.gateDemand, name, emitter)) return undefined;
841
+ return this.emitterCallback(emitter);
842
+ }
843
+
844
+ private emitterCallback(
845
+ emitter: EventEmitter<ISymbioteEvent>,
846
+ ): IScrollHandler | undefined {
787
847
  if (!emitter.observed) return undefined;
788
848
  let cached = this.emitterCallbackCache.get(emitter);
789
849
  if (cached === undefined) {
@@ -807,7 +867,10 @@ export abstract class ScrollViewBase
807
867
  [{ nativeEvent: { contentOffset: { y: this.scrollAnimatedValue } } }],
808
868
  userOnScroll === undefined
809
869
  ? undefined
810
- : { listener: (...args: readonly unknown[]) => forwardScrollEvent(userOnScroll, args) },
870
+ : {
871
+ listener: (...args: readonly unknown[]) =>
872
+ forwardScrollEvent(userOnScroll, args),
873
+ },
811
874
  );
812
875
  }
813
876
  return this.cachedStickyOnScrollHandler;
@@ -823,7 +886,9 @@ export abstract class ScrollViewBase
823
886
  // The imperative API a parent reaches via @ViewChild(ScrollView). buildScrollViewHandle is the
824
887
  // shared, proven handle (React/Vue use it verbatim); it reads the node through the LAZY getter on
825
888
  // every call, so a command before commit no-ops rather than freezing a null node.
826
- private readonly handle: IScrollViewHandle = buildScrollViewHandle(() => this.hostNode);
889
+ private readonly handle: IScrollViewHandle = buildScrollViewHandle(
890
+ () => this.hostNode,
891
+ );
827
892
 
828
893
  scrollTo(options?: { x?: number; y?: number; animated?: boolean }): void {
829
894
  this.handle.scrollTo(options);
@@ -861,6 +926,20 @@ export abstract class ScrollViewBase
861
926
  // actually moved, so `signal.set`'s own Object.is check makes an unchanged poll dirty nothing.
862
927
  ngDoCheck(): void {
863
928
  this.anchorStyle.set(anchorHostStyle(this.elementRef));
929
+ // The attach can want a node that does not exist yet, and nothing else would ask again.
930
+ // ngOnChanges covers "indices arrived late"; it does NOT cover "indices arrived late AND the host
931
+ // node had not committed yet", which is exactly what a VirtualizedList does - it derives
932
+ // stickyHeaderIndices from the measured window, so they land after this component's
933
+ // ngAfterViewInit and before its host node resolves. Device-diagnosed 2026-08-18 on
934
+ // examples/angular BenchmarkScreen STICKY PATH B, whose log ends on `attachSticky NOT attaching
935
+ // (wantsAttach=true hasNode=false)` - sticky never turned on, while PATH A (literal indices in
936
+ // the template) worked because ngAfterViewInit already saw a node. Retried ONLY from that stuck
937
+ // state, so a settled ScrollView pays nothing per check. Device-verified the same day.
938
+ // Not covered headless: sticky-native-attach.test.ts's late-indices case has its node resolved by
939
+ // the time the indices land, and the deferred-creation ordering under `@if` never rendered the
940
+ // ScrollView at all.
941
+ if (this.stickyAttachedEnabled && this.stickyAttachedNode === null)
942
+ this.attachSticky();
864
943
  }
865
944
 
866
945
  // The projected RefreshControl is a @ContentChild, not an @Input, so ngOnChanges never fires when
@@ -869,7 +948,8 @@ export abstract class ScrollViewBase
869
948
  // hook. Guarded on the flag itself so a check that changed nothing skips the reconcile walk,
870
949
  // which the old getter-driven update could not do.
871
950
  ngAfterContentChecked(): void {
872
- if (this.hasProjectedRefreshControl === this.lastHasProjectedRefreshControl) return;
951
+ if (this.hasProjectedRefreshControl === this.lastHasProjectedRefreshControl)
952
+ return;
873
953
  this.lastHasProjectedRefreshControl = this.hasProjectedRefreshControl;
874
954
  this.updateProjectionController();
875
955
  }
@@ -910,8 +990,13 @@ export abstract class ScrollViewBase
910
990
  // Nothing that decides the attach has changed - return rather than detach and rebind the
911
991
  // native scroll event, which every unrelated @Input change would otherwise churn now that
912
992
  // ngOnChanges drives this too.
913
- if (wantsAttach === this.stickyAttachedEnabled && node === this.stickyAttachedNode) {
914
- dlog('STICKY[sv] attachSticky skipped (nothing about the attach changed)');
993
+ if (
994
+ wantsAttach === this.stickyAttachedEnabled &&
995
+ node === this.stickyAttachedNode
996
+ ) {
997
+ dlog(
998
+ 'STICKY[sv] attachSticky skipped (nothing about the attach changed)',
999
+ );
915
1000
  return;
916
1001
  }
917
1002
 
@@ -931,10 +1016,15 @@ export abstract class ScrollViewBase
931
1016
  );
932
1017
  return;
933
1018
  }
934
- dlog('STICKY[sv] attachSticky waiting for commit to bind the native scroll event');
1019
+ dlog(
1020
+ 'STICKY[sv] attachSticky waiting for commit to bind the native scroll event',
1021
+ );
935
1022
  this.cancelStickyBind = whenCommitted(node, () => {
936
1023
  dlog('STICKY[sv] attachSticky node committed -> attachStickyScroll');
937
- this.detachStickyScroll = attachStickyScroll(node, this.scrollAnimatedValue);
1024
+ this.detachStickyScroll = attachStickyScroll(
1025
+ node,
1026
+ this.scrollAnimatedValue,
1027
+ );
938
1028
  });
939
1029
  }
940
1030
 
@@ -957,7 +1047,9 @@ export abstract class ScrollViewBase
957
1047
 
958
1048
  // Typed as the a11y intersection WITH the string index so resolveAccessibilityProps's result
959
1049
  // stays assignable into the forwarded bag (a genuine narrowing, built at that type — no cast).
960
- private accessibilityInputs(): IAccessibilityProps & IAriaProps & Record<string, unknown> {
1050
+ private accessibilityInputs(): IAccessibilityProps &
1051
+ IAriaProps &
1052
+ Record<string, unknown> {
961
1053
  return {
962
1054
  testID: this.testID,
963
1055
  nativeID: this.nativeID,
@@ -976,8 +1068,10 @@ export abstract class ScrollViewBase
976
1068
  accessibilityElementsHidden: this.accessibilityElementsHidden,
977
1069
  accessibilityIgnoresInvertColors: this.accessibilityIgnoresInvertColors,
978
1070
  accessibilityLanguage: this.accessibilityLanguage,
979
- accessibilityRespondsToUserInteraction: this.accessibilityRespondsToUserInteraction,
980
- accessibilityShowsLargeContentViewer: this.accessibilityShowsLargeContentViewer,
1071
+ accessibilityRespondsToUserInteraction:
1072
+ this.accessibilityRespondsToUserInteraction,
1073
+ accessibilityShowsLargeContentViewer:
1074
+ this.accessibilityShowsLargeContentViewer,
981
1075
  accessibilityLargeContentTitle: this.accessibilityLargeContentTitle,
982
1076
  role: this.role,
983
1077
  'aria-label': this['aria-label'],
@@ -96,7 +96,9 @@ export class ScrollViewStickyHeader
96
96
  // The animated node driving the transform (RN's animatedTranslateY), replaced by the
97
97
  // rebuild-interpolation effect. When the scroll value is native, this interpolation runs on the UI
98
98
  // thread: the smooth pin.
99
- private animatedTranslateY: AnimatedInterpolation = new AnimatedValue(0).interpolate({
99
+ private animatedTranslateY: AnimatedInterpolation = new AnimatedValue(
100
+ 0,
101
+ ).interpolate({
100
102
  inputRange: [-1, 0],
101
103
  outputRange: [0, 0],
102
104
  });
@@ -135,13 +137,17 @@ export class ScrollViewStickyHeader
135
137
  // anchor style first so the fixed transform/zIndex below still wins on conflict.
136
138
  return [
137
139
  anchorHostStyle(this.elementRef),
138
- { transform: [{ translateY: this.animatedTranslateY }], zIndex: STICKY_HEADER_Z_INDEX },
140
+ {
141
+ transform: [{ translateY: this.animatedTranslateY }],
142
+ zIndex: STICKY_HEADER_Z_INDEX,
143
+ },
139
144
  ];
140
145
  }
141
146
 
142
147
  // Stable reference (bound once) so AnimatedView's directive does not re-wire the layout listener
143
148
  // every change-detection pass.
144
- private readonly onHostLayout = (event: ISymbioteEvent): void => this.handleLayout(event);
149
+ private readonly onHostLayout = (event: ISymbioteEvent): void =>
150
+ this.handleLayout(event);
145
151
 
146
152
  // Also a stable reference, not a getter: jsonEqual (commit.ts) can't structurally compare the
147
153
  // embedded onLayout function, so a fresh object here would read as "props changed" on every
@@ -156,7 +162,9 @@ export class ScrollViewStickyHeader
156
162
 
157
163
  // The EXPLICIT debounced translateY overrides the committed transform for hit-testing while
158
164
  // animatedTranslateY does the smooth (native-driven) pin (RN ScrollViewStickyHeader.js).
159
- get passthrough(): { style: { transform: Array<{ translateY: number }> } } | null {
165
+ get passthrough(): {
166
+ style: { transform: Array<{ translateY: number }> };
167
+ } | null {
160
168
  return this.state.translateY !== null
161
169
  ? { style: { transform: [{ translateY: this.state.translateY }] } }
162
170
  : null;
@@ -181,7 +189,10 @@ export class ScrollViewStickyHeader
181
189
  case 'rebuild-interpolation': {
182
190
  // Detach the old listener, build a fresh interpolation onto the shared scroll value, and
183
191
  // wire the settled-value listener (symbiote is always Fabric; RN attaches it only there).
184
- if (this.interpolation !== undefined && this.listenerId !== undefined) {
192
+ if (
193
+ this.interpolation !== undefined &&
194
+ this.listenerId !== undefined
195
+ ) {
185
196
  this.interpolation.removeListener(this.listenerId);
186
197
  this.listenerId = undefined;
187
198
  }
@@ -200,7 +211,8 @@ export class ScrollViewStickyHeader
200
211
  case 'schedule-debounce':
201
212
  // The animated value updates several times per frame; debounce the settled value into the
202
213
  // committed transform so hit detection stays current (a Fabric issue, worse on Android).
203
- if (this.debounceTimer !== undefined) clearTimeout(this.debounceTimer);
214
+ if (this.debounceTimer !== undefined)
215
+ clearTimeout(this.debounceTimer);
204
216
  this.debounceTimer = setTimeout(() => {
205
217
  this.debounceTimer = undefined;
206
218
  this.dispatch({ kind: 'debounce-fired', value: effect.value });
@@ -233,8 +245,13 @@ export class ScrollViewStickyHeader
233
245
  this.changeDetector.markForCheck();
234
246
  }
235
247
 
236
- private readonly animatedValueListener = ({ value }: { value: number | string }): void => {
237
- if (typeof value === 'number') this.dispatch({ kind: 'animated-tick', value });
248
+ private readonly animatedValueListener = ({
249
+ value,
250
+ }: {
251
+ value: number | string;
252
+ }): void => {
253
+ if (typeof value === 'number')
254
+ this.dispatch({ kind: 'animated-tick', value });
238
255
  };
239
256
 
240
257
  private teardown(): void {
@@ -56,7 +56,10 @@ import {
56
56
  type IVListSeparatorContext,
57
57
  } from '../virtualized-list';
58
58
  import { VListOutletDirective } from '../virtualized-list/directives';
59
- import { stableAnchorStyle, SymbioteStyleInputDirective } from '../../primitives';
59
+ import {
60
+ stableAnchorStyle,
61
+ SymbioteStyleInputDirective,
62
+ } from '../../primitives';
60
63
  import {
61
64
  VSectionFooterDirective,
62
65
  VSectionHeaderDirective,
@@ -97,6 +100,13 @@ export {
97
100
  VListHeaderDirective,
98
101
  VListSeparatorDirective,
99
102
  } from '../virtualized-list';
103
+ import {
104
+ gateWanted,
105
+ injectGateDemandAbove,
106
+ provideGateDemand,
107
+ type IGateDemand,
108
+ type IGatedAccessibilityEvent,
109
+ } from '../../gate-demand';
100
110
 
101
111
  // SectionList's public surface is exactly VirtualizedSectionList's (RN layers them one-for-one), so
102
112
  // the prop type is shared verbatim — no SectionList-only field exists.
@@ -120,7 +130,10 @@ export type ISectionListInputs<ItemT> = Omit<
120
130
  @Component({
121
131
  selector: 'SectionList',
122
132
  standalone: true,
123
- hostDirectives: [{ directive: SymbioteStyleInputDirective, inputs: ['style'] }],
133
+ viewProviders: [provideGateDemand(() => SectionList)],
134
+ hostDirectives: [
135
+ { directive: SymbioteStyleInputDirective, inputs: ['style'] },
136
+ ],
124
137
  imports: [
125
138
  VirtualizedSectionList,
126
139
  VSectionItemDirective,
@@ -138,6 +151,7 @@ export type ISectionListInputs<ItemT> = Omit<
138
151
  <VirtualizedSectionList
139
152
  [sections]="sections"
140
153
  [keyExtractor]="keyExtractor"
154
+ [getItemLayout]="getItemLayout"
141
155
  [stickySectionHeadersEnabled]="stickySectionHeadersEnabled"
142
156
  [extraData]="extraData"
143
157
  (endReached)="endReached.emit($event)"
@@ -182,8 +196,12 @@ export type ISectionListInputs<ItemT> = Omit<
182
196
  [accessibilityElementsHidden]="accessibilityElementsHidden"
183
197
  [accessibilityIgnoresInvertColors]="accessibilityIgnoresInvertColors"
184
198
  [accessibilityLanguage]="accessibilityLanguage"
185
- [accessibilityRespondsToUserInteraction]="accessibilityRespondsToUserInteraction"
186
- [accessibilityShowsLargeContentViewer]="accessibilityShowsLargeContentViewer"
199
+ [accessibilityRespondsToUserInteraction]="
200
+ accessibilityRespondsToUserInteraction
201
+ "
202
+ [accessibilityShowsLargeContentViewer]="
203
+ accessibilityShowsLargeContentViewer
204
+ "
187
205
  [accessibilityLargeContentTitle]="accessibilityLargeContentTitle"
188
206
  (accessibilityAction)="accessibilityAction.emit($event)"
189
207
  (accessibilityTap)="accessibilityTap.emit($event)"
@@ -214,7 +232,13 @@ export type ISectionListInputs<ItemT> = Omit<
214
232
  >
215
233
  <ng-container
216
234
  [vListOutlet]="sectionItemDir?.templateRef"
217
- [vListOutletContext]="{ $implicit: item, item, index, section, separators }"
235
+ [vListOutletContext]="{
236
+ $implicit: item,
237
+ item,
238
+ index,
239
+ section,
240
+ separators,
241
+ }"
218
242
  ></ng-container>
219
243
  </ng-template>
220
244
  @if (sectionHeaderDir !== undefined) {
@@ -235,17 +259,23 @@ export type ISectionListInputs<ItemT> = Omit<
235
259
  }
236
260
  @if (sectionSeparatorDir !== undefined) {
237
261
  <ng-template vSectionSeparator>
238
- <ng-container [vListOutlet]="sectionSeparatorDir.templateRef"></ng-container>
262
+ <ng-container
263
+ [vListOutlet]="sectionSeparatorDir.templateRef"
264
+ ></ng-container>
239
265
  </ng-template>
240
266
  }
241
267
  @if (listHeaderDir !== undefined) {
242
268
  <ng-template vListHeader>
243
- <ng-container [vListOutlet]="listHeaderDir.templateRef"></ng-container>
269
+ <ng-container
270
+ [vListOutlet]="listHeaderDir.templateRef"
271
+ ></ng-container>
244
272
  </ng-template>
245
273
  }
246
274
  @if (listFooterDir !== undefined) {
247
275
  <ng-template vListFooter>
248
- <ng-container [vListOutlet]="listFooterDir.templateRef"></ng-container>
276
+ <ng-container
277
+ [vListOutlet]="listFooterDir.templateRef"
278
+ ></ng-container>
249
279
  </ng-template>
250
280
  }
251
281
  @if (listEmptyDir !== undefined) {
@@ -262,7 +292,9 @@ export type ISectionListInputs<ItemT> = Omit<
262
292
  >
263
293
  <ng-container
264
294
  [vListOutlet]="itemSeparatorDir.templateRef"
265
- [vListOutletContext]="itemSeparatorContext(highlighted, leadingItem, trailingItem)"
295
+ [vListOutletContext]="
296
+ itemSeparatorContext(highlighted, leadingItem, trailingItem)
297
+ "
266
298
  ></ng-container>
267
299
  </ng-template>
268
300
  }
@@ -274,11 +306,21 @@ export class SectionList<ItemT = unknown>
274
306
  {
275
307
  @Input({ required: true }) sections!: ReadonlyArray<ISection<ItemT>>;
276
308
  @Input() keyExtractor?: (item: ItemT, index: number) => string;
309
+ // Passed through FLAT (sections array + flat entry index); VirtualizedSectionList owns the
310
+ // sections-vs-entries wrapper - see its entryItemLayout getter.
311
+ @Input() getItemLayout?: (
312
+ data: ReadonlyArray<ISection<ItemT>> | null,
313
+ index: number,
314
+ ) => { length: number; offset: number; index: number };
277
315
  @Input() stickySectionHeadersEnabled?: boolean;
278
316
  @Input() extraData?: unknown;
279
- @Output() readonly endReached = new EventEmitter<{ distanceFromEnd: number }>();
317
+ @Output() readonly endReached = new EventEmitter<{
318
+ distanceFromEnd: number;
319
+ }>();
280
320
  @Input() onEndReachedThreshold?: number;
281
- @Output() readonly startReached = new EventEmitter<{ distanceFromStart: number }>();
321
+ @Output() readonly startReached = new EventEmitter<{
322
+ distanceFromStart: number;
323
+ }>();
282
324
  @Input() onStartReachedThreshold?: number;
283
325
  @Output() readonly refresh = new EventEmitter<void>();
284
326
  @Input() refreshing?: boolean | null;
@@ -331,8 +373,10 @@ export class SectionList<ItemT = unknown>
331
373
  @Input() accessibilityValue?: IAccessibilityProps['accessibilityValue'];
332
374
  @Input() accessibilityActions?: IAccessibilityProps['accessibilityActions'];
333
375
  @Input() accessibilityLabelledBy?: string | string[];
334
- @Input() importantForAccessibility?: IAccessibilityProps['importantForAccessibility'];
335
- @Input() accessibilityLiveRegion?: IAccessibilityProps['accessibilityLiveRegion'];
376
+ @Input()
377
+ importantForAccessibility?: IAccessibilityProps['importantForAccessibility'];
378
+ @Input()
379
+ accessibilityLiveRegion?: IAccessibilityProps['accessibilityLiveRegion'];
336
380
  @Input() screenReaderFocusable?: boolean;
337
381
  @Input() accessibilityViewIsModal?: boolean;
338
382
  @Input() accessibilityElementsHidden?: boolean;
@@ -345,6 +389,16 @@ export class SectionList<ItemT = unknown>
345
389
  @Output() readonly accessibilityTap = new EventEmitter<ISymbioteEvent>();
346
390
  @Output() readonly magicTap = new EventEmitter<ISymbioteEvent>();
347
391
  @Output() readonly accessibilityEscape = new EventEmitter<ISymbioteEvent>();
392
+
393
+ // This wrapper binds the four gated accessibility events on the component it renders, which
394
+ // Angular forces to be unconditional and which would light that component's gates on every
395
+ // instance. It answers for them instead — see `gate-demand.ts`.
396
+ private readonly gateDemandAbove = injectGateDemandAbove();
397
+
398
+ wantsGate(name: IGatedAccessibilityEvent): boolean {
399
+ return gateWanted(this.gateDemandAbove, name, this[name]);
400
+ }
401
+
348
402
  @Input() role?: IAriaProps['role'];
349
403
  @Input() ariaLabel?: string;
350
404
  @Input() ariaLabelledBy?: string;
@@ -363,18 +417,24 @@ export class SectionList<ItemT = unknown>
363
417
 
364
418
  // The app's section/list authoring templates, captured from the app's DIRECT projected content
365
419
  // (single hop — always resolves) and re-stamped onto the inner VirtualizedSectionList above.
366
- @ContentChild(VSectionItemDirective) sectionItemDir?: VSectionItemDirective<ItemT>;
367
- @ContentChild(VSectionHeaderDirective) sectionHeaderDir?: VSectionHeaderDirective<ItemT>;
368
- @ContentChild(VSectionFooterDirective) sectionFooterDir?: VSectionFooterDirective<ItemT>;
369
- @ContentChild(VSectionSeparatorDirective) sectionSeparatorDir?: VSectionSeparatorDirective;
420
+ @ContentChild(VSectionItemDirective)
421
+ sectionItemDir?: VSectionItemDirective<ItemT>;
422
+ @ContentChild(VSectionHeaderDirective)
423
+ sectionHeaderDir?: VSectionHeaderDirective<ItemT>;
424
+ @ContentChild(VSectionFooterDirective)
425
+ sectionFooterDir?: VSectionFooterDirective<ItemT>;
426
+ @ContentChild(VSectionSeparatorDirective)
427
+ sectionSeparatorDir?: VSectionSeparatorDirective;
370
428
  @ContentChild(VListHeaderDirective) listHeaderDir?: VListHeaderDirective;
371
429
  @ContentChild(VListFooterDirective) listFooterDir?: VListFooterDirective;
372
430
  @ContentChild(VListEmptyDirective) listEmptyDir?: VListEmptyDirective;
373
- @ContentChild(VListSeparatorDirective) itemSeparatorDir?: VListSeparatorDirective<ItemT>;
431
+ @ContentChild(VListSeparatorDirective)
432
+ itemSeparatorDir?: VListSeparatorDirective<ItemT>;
374
433
 
375
434
  // The composed inner VirtualizedSectionList. Its instance IS an IVirtualizedSectionListHandle, so
376
435
  // SectionList's handle delegates straight to it. Available from ngAfterViewInit; reads lazily.
377
- @ViewChild(VirtualizedSectionList) private list?: VirtualizedSectionList<ItemT>;
436
+ @ViewChild(VirtualizedSectionList)
437
+ private list?: VirtualizedSectionList<ItemT>;
378
438
 
379
439
  // This component's OWN host — the non-painting anchor `class="..."` at the use site resolves
380
440
  // onto (see anchorHostStyle's doc comment) — NOT `list` above, which targets the real inner
@@ -10,7 +10,7 @@ import {
10
10
  import { NG_VALUE_ACCESSOR } from '@angular/forms';
11
11
  import {
12
12
  anchorHostStyle,
13
- SwitchHost,
13
+ ManagedSwitchHost,
14
14
  SymbioteHostPropsDirective,
15
15
  SymbioteStyleInputDirective,
16
16
  } from '../../primitives';
@@ -20,10 +20,18 @@ export type { ISwitchProps, ISwitchTrackColor } from './shared';
20
20
  @Component({
21
21
  selector: 'Switch',
22
22
  standalone: true,
23
- hostDirectives: [{ directive: SymbioteStyleInputDirective, inputs: ['style'] }],
23
+ hostDirectives: [
24
+ { directive: SymbioteStyleInputDirective, inputs: ['style'] },
25
+ ],
24
26
  schemas: [CUSTOM_ELEMENTS_SCHEMA],
25
- imports: [SwitchHost, SymbioteHostPropsDirective],
26
- providers: [{ provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => Switch), multi: true }],
27
+ imports: [ManagedSwitchHost, SymbioteHostPropsDirective],
28
+ providers: [
29
+ {
30
+ provide: NG_VALUE_ACCESSOR,
31
+ useExisting: forwardRef(() => Switch),
32
+ multi: true,
33
+ },
34
+ ],
27
35
  inputs: [
28
36
  'value',
29
37
  'disabled',
@@ -46,7 +54,7 @@ export type { ISwitchProps, ISwitchTrackColor } from './shared';
46
54
  ],
47
55
  outputs: ['valueChange', 'change'],
48
56
  template: `
49
- <symbiote-switch
57
+ <symbiote-switch-managed
50
58
  #nativeSwitch="symbioteHost"
51
59
  [symbioteHostProps]="hostProps()"
52
60
  (change)="handleChange($event, nativeSwitch)"
@@ -55,12 +63,15 @@ export type { ISwitchProps, ISwitchTrackColor } from './shared';
55
63
  })
56
64
  export class Switch extends SwitchBase implements DoCheck {
57
65
  // This component's OWN host — the non-painting anchor `class="..."` at the use site resolves
58
- // onto (see anchorHostStyle's doc comment) — NOT the <symbiote-switch> leaf one level down.
66
+ // onto (see anchorHostStyle's doc comment) — NOT the <symbiote-switch-managed> leaf one level down.
59
67
  private readonly elementRef = inject(ElementRef);
60
68
 
61
69
  protected readonly platform = {
62
70
  snapBackCommand: 'setNativeValue',
63
- trackColorProps: (value: boolean, trackColor?: { false?: string; true?: string }) => ({
71
+ trackColorProps: (
72
+ value: boolean,
73
+ trackColor?: { false?: string; true?: string },
74
+ ) => ({
64
75
  trackColorForFalse: trackColor?.false,
65
76
  trackColorForTrue: trackColor?.true,
66
77
  trackTintColor: value ? trackColor?.true : trackColor?.false,