@symbiote-native/angular 3.1.2 → 3.2.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 (121) hide show
  1. package/build/angular/accessibility-events.d.ts +12 -0
  2. package/build/angular/accessibility-events.js +28 -0
  3. package/build/angular/accessibility-events.js.map +1 -0
  4. package/build/angular/accessibility-inputs.d.ts +40 -0
  5. package/build/angular/accessibility-inputs.js +130 -0
  6. package/build/angular/accessibility-inputs.js.map +1 -0
  7. package/build/angular/bootstrap.d.ts +3 -1
  8. package/build/angular/bootstrap.js +4 -1
  9. package/build/angular/bootstrap.js.map +1 -1
  10. package/build/angular/components/flat-list/flat-list-template.d.ts +1 -0
  11. package/build/angular/components/flat-list/flat-list-template.js +158 -0
  12. package/build/angular/components/flat-list/flat-list-template.js.map +1 -0
  13. package/build/angular/components/flat-list/index.d.ts +6 -61
  14. package/build/angular/components/flat-list/index.js +26 -463
  15. package/build/angular/components/flat-list/index.js.map +1 -1
  16. package/build/angular/components/keyboard-avoiding-view/index.d.ts +4 -40
  17. package/build/angular/components/keyboard-avoiding-view/index.js +7 -131
  18. package/build/angular/components/keyboard-avoiding-view/index.js.map +1 -1
  19. package/build/angular/components/modal/index.d.ts +5 -41
  20. package/build/angular/components/modal/index.js +8 -133
  21. package/build/angular/components/modal/index.js.map +1 -1
  22. package/build/angular/components/section-list/index.d.ts +4 -51
  23. package/build/angular/components/section-list/index.js +18 -335
  24. package/build/angular/components/section-list/index.js.map +1 -1
  25. package/build/angular/components/virtualized-list/cell-registry.d.ts +35 -0
  26. package/build/angular/components/virtualized-list/cell-registry.js +133 -0
  27. package/build/angular/components/virtualized-list/cell-registry.js.map +1 -0
  28. package/build/angular/components/virtualized-list/index.d.ts +18 -253
  29. package/build/angular/components/virtualized-list/index.js +112 -1178
  30. package/build/angular/components/virtualized-list/index.js.map +1 -1
  31. package/build/angular/components/virtualized-list/list-a11y-forward.d.ts +1 -0
  32. package/build/angular/components/virtualized-list/list-a11y-forward.js +42 -0
  33. package/build/angular/components/virtualized-list/list-a11y-forward.js.map +1 -0
  34. package/build/angular/components/virtualized-list/list-bags.d.ts +32 -0
  35. package/build/angular/components/virtualized-list/list-bags.js +120 -0
  36. package/build/angular/components/virtualized-list/list-bags.js.map +1 -0
  37. package/build/angular/components/virtualized-list/list-effects.d.ts +31 -0
  38. package/build/angular/components/virtualized-list/list-effects.js +83 -0
  39. package/build/angular/components/virtualized-list/list-effects.js.map +1 -0
  40. package/build/angular/components/virtualized-list/list-events.d.ts +17 -0
  41. package/build/angular/components/virtualized-list/list-events.js +34 -0
  42. package/build/angular/components/virtualized-list/list-events.js.map +1 -0
  43. package/build/angular/components/virtualized-list/list-handle.d.ts +35 -0
  44. package/build/angular/components/virtualized-list/list-handle.js +61 -0
  45. package/build/angular/components/virtualized-list/list-handle.js.map +1 -0
  46. package/build/angular/components/virtualized-list/list-inputs.d.ts +78 -0
  47. package/build/angular/components/virtualized-list/list-inputs.js +150 -0
  48. package/build/angular/components/virtualized-list/list-inputs.js.map +1 -0
  49. package/build/angular/components/virtualized-list/list-props.d.ts +58 -0
  50. package/build/angular/components/virtualized-list/list-props.js +3 -0
  51. package/build/angular/components/virtualized-list/list-props.js.map +1 -0
  52. package/build/angular/components/virtualized-list/list-styles.d.ts +23 -0
  53. package/build/angular/components/virtualized-list/list-styles.js +26 -0
  54. package/build/angular/components/virtualized-list/list-styles.js.map +1 -0
  55. package/build/angular/components/virtualized-list/list-template.d.ts +1 -0
  56. package/build/angular/components/virtualized-list/list-template.js +176 -0
  57. package/build/angular/components/virtualized-list/list-template.js.map +1 -0
  58. package/build/angular/components/virtualized-section-list/index.d.ts +13 -70
  59. package/build/angular/components/virtualized-section-list/index.js +22 -455
  60. package/build/angular/components/virtualized-section-list/index.js.map +1 -1
  61. package/build/angular/components/virtualized-section-list/section-list-template.d.ts +1 -0
  62. package/build/angular/components/virtualized-section-list/section-list-template.js +107 -0
  63. package/build/angular/components/virtualized-section-list/section-list-template.js.map +1 -0
  64. package/build/angular/finalization-registry.d.ts +4 -0
  65. package/build/angular/finalization-registry.js +17 -0
  66. package/build/angular/finalization-registry.js.map +1 -0
  67. package/build/angular/index.d.ts +5 -1
  68. package/build/angular/index.js +5 -1
  69. package/build/angular/index.js.map +1 -1
  70. package/build/angular/native-view-base.d.ts +19 -0
  71. package/build/angular/native-view-base.js +48 -0
  72. package/build/angular/native-view-base.js.map +1 -0
  73. package/build/angular/render/index.d.ts +2 -1
  74. package/build/angular/render/index.js +46 -73
  75. package/build/angular/render/index.js.map +1 -1
  76. package/build/angular/services/connect-watched-signal.d.ts +2 -0
  77. package/build/angular/services/connect-watched-signal.js +9 -0
  78. package/build/angular/services/connect-watched-signal.js.map +1 -0
  79. package/build/angular/services/create-event-value-hook.d.ts +3 -0
  80. package/build/angular/services/create-event-value-hook.js +17 -0
  81. package/build/angular/services/create-event-value-hook.js.map +1 -0
  82. package/build/angular/services/create-resource-hook.d.ts +7 -0
  83. package/build/angular/services/create-resource-hook.js +20 -0
  84. package/build/angular/services/create-resource-hook.js.map +1 -0
  85. package/build/angular/services/index.d.ts +4 -0
  86. package/build/angular/services/index.js +4 -0
  87. package/build/angular/services/index.js.map +1 -1
  88. package/build/angular/services/permissions-service-base.d.ts +14 -0
  89. package/build/angular/services/permissions-service-base.js +38 -0
  90. package/build/angular/services/permissions-service-base.js.map +1 -0
  91. package/package.json +6 -6
  92. package/src/accessibility-events.ts +13 -0
  93. package/src/accessibility-inputs.ts +89 -0
  94. package/src/bootstrap.ts +6 -2
  95. package/src/components/flat-list/flat-list-template.ts +159 -0
  96. package/src/components/flat-list/index.ts +30 -307
  97. package/src/components/keyboard-avoiding-view/index.ts +4 -78
  98. package/src/components/modal/index.ts +7 -80
  99. package/src/components/section-list/index.ts +21 -172
  100. package/src/components/virtualized-list/cell-registry.ts +199 -0
  101. package/src/components/virtualized-list/index.ts +145 -1181
  102. package/src/components/virtualized-list/list-a11y-forward.ts +42 -0
  103. package/src/components/virtualized-list/list-bags.ts +142 -0
  104. package/src/components/virtualized-list/list-effects.ts +116 -0
  105. package/src/components/virtualized-list/list-events.ts +31 -0
  106. package/src/components/virtualized-list/list-handle.ts +92 -0
  107. package/src/components/virtualized-list/list-inputs.ts +149 -0
  108. package/src/components/virtualized-list/list-props.ts +81 -0
  109. package/src/components/virtualized-list/list-styles.ts +62 -0
  110. package/src/components/virtualized-list/list-template.ts +175 -0
  111. package/src/components/virtualized-section-list/index.ts +40 -351
  112. package/src/components/virtualized-section-list/section-list-template.ts +108 -0
  113. package/src/finalization-registry.ts +18 -0
  114. package/src/index.ts +12 -1
  115. package/src/native-view-base.ts +51 -0
  116. package/src/render/index.ts +76 -77
  117. package/src/services/connect-watched-signal.ts +15 -0
  118. package/src/services/create-event-value-hook.ts +24 -0
  119. package/src/services/create-resource-hook.ts +32 -0
  120. package/src/services/index.ts +4 -0
  121. package/src/services/permissions-service-base.ts +46 -0
@@ -32,7 +32,6 @@ import {
32
32
  resolveAccessibilityProps,
33
33
  shouldRenderModal,
34
34
  type IAccessibilityProps,
35
- type IAccessibilityStateValue,
36
35
  type IAriaProps,
37
36
  type IModalAnimationType,
38
37
  type IModalOrientation,
@@ -47,6 +46,7 @@ import {
47
46
  type ISymbioteEvent,
48
47
  type IViewStyle,
49
48
  } from '@symbiote-native/engine';
49
+ import { AccessibilityEventsBase } from '../../accessibility-events';
50
50
  import {
51
51
  anchorHostStyle,
52
52
  ModalHost,
@@ -124,15 +124,13 @@ export type IAngularModalInputs = Omit<
124
124
  }
125
125
  `,
126
126
  })
127
- export class Modal implements IAngularModalInputs, OnInit, OnChanges, DoCheck {
127
+ export class Modal
128
+ extends AccessibilityEventsBase
129
+ implements IAngularModalInputs, OnInit, OnChanges, DoCheck
130
+ {
128
131
  @Output() readonly show = new EventEmitter<void>();
129
132
  @Output() readonly dismiss = new EventEmitter<void>();
130
133
  @Output() readonly requestClose = new EventEmitter<void>();
131
- @Output() readonly orientationChange = new EventEmitter<ISymbioteEvent>();
132
- @Output() readonly accessibilityAction = new EventEmitter<ISymbioteEvent>();
133
- @Output() readonly accessibilityTap = new EventEmitter<ISymbioteEvent>();
134
- @Output() readonly magicTap = new EventEmitter<ISymbioteEvent>();
135
- @Output() readonly accessibilityEscape = new EventEmitter<ISymbioteEvent>();
136
134
  @Input() visible?: boolean;
137
135
  @Input() transparent?: boolean;
138
136
  @Input() backdropColor?: string;
@@ -145,41 +143,7 @@ export class Modal implements IAngularModalInputs, OnInit, OnChanges, DoCheck {
145
143
  @Input() allowSwipeDismissal?: boolean;
146
144
  @Input() style?: IStyleProp<IViewStyle>;
147
145
  @Input() testID?: string;
148
- @Input() accessible?: boolean;
149
- @Input() accessibilityLabel?: string;
150
- @Input() accessibilityHint?: string;
151
- @Input() accessibilityRole?: IAccessibilityProps['accessibilityRole'];
152
- @Input() accessibilityState?: IAccessibilityStateValue;
153
- @Input() accessibilityValue?: IAccessibilityProps['accessibilityValue'];
154
- @Input() accessibilityActions?: IAccessibilityProps['accessibilityActions'];
155
- @Input() accessibilityLabelledBy?: string | string[];
156
- @Input()
157
- importantForAccessibility?: IAccessibilityProps['importantForAccessibility'];
158
- @Input()
159
- accessibilityLiveRegion?: IAccessibilityProps['accessibilityLiveRegion'];
160
- @Input() screenReaderFocusable?: boolean;
161
- @Input() accessibilityViewIsModal?: boolean;
162
- @Input() accessibilityElementsHidden?: boolean;
163
- @Input() accessibilityIgnoresInvertColors?: boolean;
164
- @Input() accessibilityLanguage?: string;
165
- @Input() accessibilityRespondsToUserInteraction?: boolean;
166
- @Input() accessibilityShowsLargeContentViewer?: boolean;
167
- @Input() accessibilityLargeContentTitle?: string;
168
- @Input() role?: IAriaProps['role'];
169
- @Input('aria-label') ariaLabel?: string;
170
- @Input('aria-labelledby') ariaLabelledBy?: string;
171
- @Input('aria-live') ariaLive?: IAriaProps['aria-live'];
172
- @Input('aria-hidden') ariaHidden?: boolean;
173
- @Input('aria-busy') ariaBusy?: boolean;
174
- @Input('aria-checked') ariaChecked?: boolean | 'mixed';
175
- @Input('aria-disabled') ariaDisabled?: boolean;
176
- @Input('aria-expanded') ariaExpanded?: boolean;
177
- @Input('aria-selected') ariaSelected?: boolean;
178
- @Input('aria-modal') ariaModal?: boolean;
179
- @Input('aria-valuemax') ariaValueMax?: number;
180
- @Input('aria-valuemin') ariaValueMin?: number;
181
- @Input('aria-valuenow') ariaValueNow?: number;
182
- @Input('aria-valuetext') ariaValueText?: string;
146
+ @Output() readonly orientationChange = new EventEmitter<ISymbioteEvent>();
183
147
 
184
148
  // The iOS keep-alive (state/modal.ts): after visible→hidden the node stays until the native
185
149
  // onDismiss (`handleDismiss`) drops it.
@@ -323,43 +287,6 @@ export class Modal implements IAngularModalInputs, OnInit, OnChanges, DoCheck {
323
287
  private accessibilityInputs(): IAccessibilityProps &
324
288
  IAriaProps &
325
289
  Record<string, unknown> {
326
- return {
327
- testID: this.testID,
328
- accessible: this.accessible,
329
- accessibilityLabel: this.accessibilityLabel,
330
- accessibilityHint: this.accessibilityHint,
331
- accessibilityRole: this.accessibilityRole,
332
- accessibilityState: this.accessibilityState,
333
- accessibilityValue: this.accessibilityValue,
334
- accessibilityActions: this.accessibilityActions,
335
- accessibilityLabelledBy: this.accessibilityLabelledBy,
336
- importantForAccessibility: this.importantForAccessibility,
337
- accessibilityLiveRegion: this.accessibilityLiveRegion,
338
- screenReaderFocusable: this.screenReaderFocusable,
339
- accessibilityViewIsModal: this.accessibilityViewIsModal,
340
- accessibilityElementsHidden: this.accessibilityElementsHidden,
341
- accessibilityIgnoresInvertColors: this.accessibilityIgnoresInvertColors,
342
- accessibilityLanguage: this.accessibilityLanguage,
343
- accessibilityRespondsToUserInteraction:
344
- this.accessibilityRespondsToUserInteraction,
345
- accessibilityShowsLargeContentViewer:
346
- this.accessibilityShowsLargeContentViewer,
347
- accessibilityLargeContentTitle: this.accessibilityLargeContentTitle,
348
- role: this.role,
349
- 'aria-label': this.ariaLabel,
350
- 'aria-labelledby': this.ariaLabelledBy,
351
- 'aria-live': this.ariaLive,
352
- 'aria-hidden': this.ariaHidden,
353
- 'aria-busy': this.ariaBusy,
354
- 'aria-checked': this.ariaChecked,
355
- 'aria-disabled': this.ariaDisabled,
356
- 'aria-expanded': this.ariaExpanded,
357
- 'aria-selected': this.ariaSelected,
358
- 'aria-modal': this.ariaModal,
359
- 'aria-valuemax': this.ariaValueMax,
360
- 'aria-valuemin': this.ariaValueMin,
361
- 'aria-valuenow': this.ariaValueNow,
362
- 'aria-valuetext': this.ariaValueText,
363
- };
290
+ return { testID: this.testID, ...this.accessibilityInputProps() };
364
291
  }
365
292
  }
@@ -1,43 +1,17 @@
1
- // SectionList, the Angular public list-of-sections component. A thin convenience surface over
2
- // VirtualizedSectionList, mirroring RN's layering (SectionList -> VirtualizedSectionList ->
3
- // VirtualizedList). All section-flattening / windowing / sticky-header / imperative-scroll logic
4
- // lives below; this layer re-exposes the same surface under the SectionList name and delegates
5
- // the handle to the inner VirtualizedSectionList.
6
- //
7
- // SectionList is a PURE FORWARDER: its public prop surface IS VirtualizedSectionList's (RN layers
8
- // them one-for-one), and the SectionList-flavour defaults (stickySectionHeadersEnabled per
9
- // Platform.OS, the section entry keyExtractor) already live INSIDE VirtualizedSectionList — double-
10
- // applying them here would fight it. Angular has no attrs-spread (unlike Vue's $attrs), so every
11
- // prop is forwarded as an explicit @Input binding, except the seven list-lifecycle events
12
- // (endReached/startReached/refresh/accessibilityAction/accessibilityTap/magicTap/
13
- // accessibilityEscape), which are real @Output() EventEmitters re-emitted via a listener binding.
14
- //
15
- // TEMPLATE FORWARDING — RE-STAMP, the same pattern FlatList's single-column branch uses (see
16
- // flat-list/index.ts). A bare `<ng-content></ng-content>` passthrough does NOT let
17
- // VirtualizedSectionList's own @ContentChild resolve directives across a SECOND projection hop —
18
- // Angular's content queries resolve only against what was projected directly onto the querying
19
- // component's OWN tag. So SectionList captures the app's `<ng-template vSectionItem>` /
20
- // vSectionHeader / vSectionFooter / vSectionSeparator / vListHeader / vListFooter / vListEmpty /
21
- // vListSeparator with its OWN @ContentChild (a single, direct hop — always resolves) and
22
- // re-authors equivalent `<ng-template>`s on `<VirtualizedSectionList>`, forwarding the captured
23
- // templateRef + context through VListOutletDirective — mirroring how VirtualizedSectionList itself
24
- // re-stamps its own captured directives onto its inner VirtualizedList.
1
+ // SectionList, a pure forwarder over VirtualizedSectionList, mirroring RN's layering
2
+ // A content query does not resolve across a second `<ng-content>` hop, so the app's templates are
3
+ // captured here and re-stamped onto the inner list
25
4
 
26
5
  import {
27
6
  ChangeDetectionStrategy,
28
7
  Component,
29
8
  ContentChild,
30
9
  ElementRef,
31
- EventEmitter,
32
10
  Input,
33
- Output,
34
11
  ViewChild,
35
12
  inject,
36
13
  } from '@angular/core';
37
14
  import type {
38
- IAccessibilityProps,
39
- IAccessibilityStateValue,
40
- IAriaProps,
41
15
  IScrollViewHandle,
42
16
  ISection,
43
17
  IVirtualizedSectionListHandle,
@@ -56,6 +30,9 @@ import {
56
30
  type IVListSeparatorContext,
57
31
  } from '../virtualized-list';
58
32
  import { VListOutletDirective } from '../virtualized-list/directives';
33
+ import { LIST_ACCESSIBILITY_FORWARD } from '../virtualized-list/list-a11y-forward';
34
+ import { ListEventsBase } from '../virtualized-list/list-events';
35
+ import { provideGateDemand } from '../../gate-demand';
59
36
  import {
60
37
  stableAnchorStyle,
61
38
  SymbioteStyleInputDirective,
@@ -69,25 +46,16 @@ import {
69
46
  type IVirtualizedSectionListProps,
70
47
  } from '../virtualized-section-list';
71
48
 
72
- // Angular cannot preserve VListSeparatorDirective<ItemT>'s type parameter across this re-stamp
73
- // reuse (the directive is matched structurally in SectionList's own template with no explicit type
74
- // argument to pin ItemT to, so `let-leadingItem`/`let-trailingItem` arrive typed `unknown`) — even
75
- // though VirtualizedSectionList's own unwrap always supplies a real ItemT value. The narrowest
76
- // possible I/O-boundary cast for that gap; not a general-purpose unknown-to-T narrowing (mirrors
77
- // flat-list/index.ts's identical asItem helper).
49
+ // The separator context arrives typed `unknown`, the directive is matched structurally in the
50
+ // template with no type argument to pin the item type. The narrowest cast for that gap
78
51
  function asItem<ItemT>(value: unknown): ItemT | undefined {
79
52
  return value as ItemT | undefined;
80
53
  }
81
54
 
82
- // Re-export the shared section type + the imperative handle so app code (and any later layer) imports
83
- // them from SectionList, mirroring how the Vue twin re-exposes ISection + ISectionListHandle.
84
55
  export type { ISection } from '@symbiote-native/components';
85
56
  export type ISectionListHandle = IVirtualizedSectionListHandle;
86
57
 
87
- // Re-export the section authoring directives + the list-level slot directives so app code importing
88
- // from '@symbiote-native/angular' gets the full `<ng-template vSection*>` / `vList*` authoring surface
89
- // alongside SectionList (mirrors how virtualized-section-list/index re-exports the section ones and
90
- // FlatList re-exports the list-level ones).
58
+ // Re-exported so app code gets the whole `vSection*` and `vList*` authoring surface from here
91
59
  export {
92
60
  VSectionFooterDirective,
93
61
  VSectionHeaderDirective,
@@ -100,21 +68,11 @@ export {
100
68
  VListHeaderDirective,
101
69
  VListSeparatorDirective,
102
70
  } from '../virtualized-list';
103
- import {
104
- gateWanted,
105
- injectGateDemandAbove,
106
- provideGateDemand,
107
- type IGatedAccessibilityEvent,
108
- } from '../../gate-demand';
109
71
 
110
- // SectionList's public surface is exactly VirtualizedSectionList's (RN layers them one-for-one), so
111
- // the prop type is shared verbatim — no SectionList-only field exists.
72
+ // RN layers the two one-for-one, so the prop type is shared verbatim
112
73
  export type ISectionListProps<ItemT> = IVirtualizedSectionListProps<ItemT>;
113
74
 
114
- // What the SectionList component itself takes as plain @Input()s: the full surface minus the
115
- // list-lifecycle events, which it exposes as real @Output() EventEmitters instead (see the class
116
- // below) — mirrors IAngularPressableInputs in pressable/index.ts and VirtualizedSectionList's own
117
- // IVirtualizedSectionListInputs.
75
+ // The plain inputs: the full surface minus the events exposed as real outputs
118
76
  export type ISectionListInputs<ItemT> = Omit<
119
77
  ISectionListProps<ItemT>,
120
78
  | 'onEndReached'
@@ -182,47 +140,7 @@ export type ISectionListInputs<ItemT> = Omit<
182
140
  [contentContainerStyle]="contentContainerStyle"
183
141
  [testID]="testID"
184
142
  [nativeID]="nativeID"
185
- [accessible]="accessible"
186
- [accessibilityLabel]="accessibilityLabel"
187
- [accessibilityHint]="accessibilityHint"
188
- [accessibilityRole]="accessibilityRole"
189
- [accessibilityState]="accessibilityState"
190
- [accessibilityValue]="accessibilityValue"
191
- [accessibilityActions]="accessibilityActions"
192
- [accessibilityLabelledBy]="accessibilityLabelledBy"
193
- [importantForAccessibility]="importantForAccessibility"
194
- [accessibilityLiveRegion]="accessibilityLiveRegion"
195
- [screenReaderFocusable]="screenReaderFocusable"
196
- [accessibilityViewIsModal]="accessibilityViewIsModal"
197
- [accessibilityElementsHidden]="accessibilityElementsHidden"
198
- [accessibilityIgnoresInvertColors]="accessibilityIgnoresInvertColors"
199
- [accessibilityLanguage]="accessibilityLanguage"
200
- [accessibilityRespondsToUserInteraction]="
201
- accessibilityRespondsToUserInteraction
202
- "
203
- [accessibilityShowsLargeContentViewer]="
204
- accessibilityShowsLargeContentViewer
205
- "
206
- [accessibilityLargeContentTitle]="accessibilityLargeContentTitle"
207
- (accessibilityAction)="accessibilityAction.emit($event)"
208
- (accessibilityTap)="accessibilityTap.emit($event)"
209
- (magicTap)="magicTap.emit($event)"
210
- (accessibilityEscape)="accessibilityEscape.emit($event)"
211
- [role]="role"
212
- [ariaLabel]="ariaLabel"
213
- [ariaLabelledBy]="ariaLabelledBy"
214
- [ariaLive]="ariaLive"
215
- [ariaHidden]="ariaHidden"
216
- [ariaBusy]="ariaBusy"
217
- [ariaChecked]="ariaChecked"
218
- [ariaDisabled]="ariaDisabled"
219
- [ariaExpanded]="ariaExpanded"
220
- [ariaSelected]="ariaSelected"
221
- [ariaModal]="ariaModal"
222
- [ariaValueMax]="ariaValueMax"
223
- [ariaValueMin]="ariaValueMin"
224
- [ariaValueNow]="ariaValueNow"
225
- [ariaValueText]="ariaValueText"
143
+ ${LIST_ACCESSIBILITY_FORWARD}
226
144
  >
227
145
  <ng-template
228
146
  vSectionItem
@@ -303,27 +221,20 @@ export type ISectionListInputs<ItemT> = Omit<
303
221
  `,
304
222
  })
305
223
  export class SectionList<ItemT = unknown>
224
+ extends ListEventsBase
306
225
  implements ISectionListInputs<ItemT>, IVirtualizedSectionListHandle
307
226
  {
308
227
  @Input({ required: true }) sections!: ReadonlyArray<ISection<ItemT>>;
309
228
  @Input() keyExtractor?: (item: ItemT, index: number) => string;
310
- // Passed through FLAT (sections array + flat entry index); VirtualizedSectionList owns the
311
- // sections-vs-entries wrapper - see its entryItemLayout getter.
229
+ // Passed through flat, `VirtualizedSectionList` owns the sections-versus-entries wrapper
312
230
  @Input() getItemLayout?: (
313
231
  data: ReadonlyArray<ISection<ItemT>> | null,
314
232
  index: number,
315
233
  ) => { length: number; offset: number; index: number };
316
234
  @Input() stickySectionHeadersEnabled?: boolean;
317
235
  @Input() extraData?: unknown;
318
- @Output() readonly endReached = new EventEmitter<{
319
- distanceFromEnd: number;
320
- }>();
321
236
  @Input() onEndReachedThreshold?: number;
322
- @Output() readonly startReached = new EventEmitter<{
323
- distanceFromStart: number;
324
- }>();
325
237
  @Input() onStartReachedThreshold?: number;
326
- @Output() readonly refresh = new EventEmitter<void>();
327
238
  @Input() refreshing?: boolean | null;
328
239
  @Input() progressViewOffset?: number;
329
240
  @Input() initialNumToRender?: number;
@@ -351,14 +262,8 @@ export class SectionList<ItemT = unknown>
351
262
  @Input() testID?: string;
352
263
  @Input() nativeID?: string;
353
264
 
354
- // Bound to `[style]="resolvedStyle"`, which Angular compiles to ɵɵstyleMap — it only
355
- // understands a flat object, never an array (RN's `style={[a, b]}` idiom crashes deep inside
356
- // Angular's styling engine), so this flattens `style` first, merging in this component's OWN
357
- // anchor's class-derived style via anchorHostStyle (`elementRef` here is SectionList's own
358
- // host, not `list`'s inner VirtualizedSectionList). A plain `flattenStyle([...])` would
359
- // allocate a fresh object on every CD check, defeating VirtualizedList's `ngDoCheck` dedup gate
360
- // and free-running change detection forever — the bug `stableAnchorStyle` exists to prevent
361
- // (see its doc comment). `cachedResolvedStyle` is the getter's own persisted "previous" value.
265
+ // A flat object bound to `[style]`, merged with this host's own class-derived style. A fresh
266
+ // object per read would defeat the inner list's dedup gate, `stableAnchorStyle` keeps it steady
362
267
  private cachedResolvedStyle: Record<string, unknown> | undefined;
363
268
  get resolvedStyle(): IViewStyle {
364
269
  this.cachedResolvedStyle = stableAnchorStyle(
@@ -368,58 +273,8 @@ export class SectionList<ItemT = unknown>
368
273
  );
369
274
  return this.cachedResolvedStyle;
370
275
  }
371
- @Input() accessible?: boolean;
372
- @Input() accessibilityLabel?: string;
373
- @Input() accessibilityHint?: string;
374
- @Input() accessibilityRole?: IAccessibilityProps['accessibilityRole'];
375
- @Input() accessibilityState?: IAccessibilityStateValue;
376
- @Input() accessibilityValue?: IAccessibilityProps['accessibilityValue'];
377
- @Input() accessibilityActions?: IAccessibilityProps['accessibilityActions'];
378
- @Input() accessibilityLabelledBy?: string | string[];
379
- @Input()
380
- importantForAccessibility?: IAccessibilityProps['importantForAccessibility'];
381
- @Input()
382
- accessibilityLiveRegion?: IAccessibilityProps['accessibilityLiveRegion'];
383
- @Input() screenReaderFocusable?: boolean;
384
- @Input() accessibilityViewIsModal?: boolean;
385
- @Input() accessibilityElementsHidden?: boolean;
386
- @Input() accessibilityIgnoresInvertColors?: boolean;
387
- @Input() accessibilityLanguage?: string;
388
- @Input() accessibilityRespondsToUserInteraction?: boolean;
389
- @Input() accessibilityShowsLargeContentViewer?: boolean;
390
- @Input() accessibilityLargeContentTitle?: string;
391
- @Output() readonly accessibilityAction = new EventEmitter<ISymbioteEvent>();
392
- @Output() readonly accessibilityTap = new EventEmitter<ISymbioteEvent>();
393
- @Output() readonly magicTap = new EventEmitter<ISymbioteEvent>();
394
- @Output() readonly accessibilityEscape = new EventEmitter<ISymbioteEvent>();
395
-
396
- // This wrapper binds the four gated accessibility events on the component it renders, which
397
- // Angular forces to be unconditional and which would light that component's gates on every
398
- // instance. It answers for them instead — see `gate-demand.ts`.
399
- private readonly gateDemandAbove = injectGateDemandAbove();
400
-
401
- wantsGate(name: IGatedAccessibilityEvent): boolean {
402
- return gateWanted(this.gateDemandAbove, name, this[name]);
403
- }
404
-
405
- @Input() role?: IAriaProps['role'];
406
- @Input() ariaLabel?: string;
407
- @Input() ariaLabelledBy?: string;
408
- @Input() ariaLive?: IAriaProps['aria-live'];
409
- @Input() ariaHidden?: boolean;
410
- @Input() ariaBusy?: boolean;
411
- @Input() ariaChecked?: boolean | 'mixed';
412
- @Input() ariaDisabled?: boolean;
413
- @Input() ariaExpanded?: boolean;
414
- @Input() ariaSelected?: boolean;
415
- @Input() ariaModal?: boolean;
416
- @Input() ariaValueMax?: number;
417
- @Input() ariaValueMin?: number;
418
- @Input() ariaValueNow?: number;
419
- @Input() ariaValueText?: string;
420
276
 
421
- // The app's section/list authoring templates, captured from the app's DIRECT projected content
422
- // (single hop — always resolves) and re-stamped onto the inner VirtualizedSectionList above.
277
+ // The app's templates, captured from directly projected content and re-stamped above
423
278
  @ContentChild(VSectionItemDirective)
424
279
  sectionItemDir?: VSectionItemDirective<ItemT>;
425
280
  @ContentChild(VSectionHeaderDirective)
@@ -434,19 +289,15 @@ export class SectionList<ItemT = unknown>
434
289
  @ContentChild(VListSeparatorDirective)
435
290
  itemSeparatorDir?: VListSeparatorDirective<ItemT>;
436
291
 
437
- // The composed inner VirtualizedSectionList. Its instance IS an IVirtualizedSectionListHandle, so
438
- // SectionList's handle delegates straight to it. Available from ngAfterViewInit; reads lazily.
292
+ // The inner list, whose instance is the scroll handle, reads lazily
439
293
  @ViewChild(VirtualizedSectionList)
440
294
  private list?: VirtualizedSectionList<ItemT>;
441
295
 
442
- // This component's OWN host — the non-painting anchor `class="..."` at the use site resolves
443
- // onto (see anchorHostStyle's doc comment) — NOT `list` above, which targets the real inner
444
- // `<VirtualizedSectionList>` one level down (itself its own separate anchor host).
296
+ // This component's own host, the anchor `class="..."` resolves onto, not the inner list's
445
297
  private readonly elementRef = inject(ElementRef);
446
298
 
447
- // The item separator context: leadingItem/trailingItem arrive `unknown` from the template's `let`
448
- // bindings (see the asItem boundary comment above), already-unwrapped real items (no envelope to
449
- // narrow through, unlike a section header/footer entry).
299
+ // The leading and trailing items arrive `unknown` from the template's `let` bindings, already
300
+ // unwrapped to real items
450
301
  itemSeparatorContext(
451
302
  highlighted: unknown,
452
303
  leadingItem: unknown,
@@ -461,8 +312,6 @@ export class SectionList<ItemT = unknown>
461
312
  };
462
313
  }
463
314
 
464
- // ---- imperative handle (the shared IVirtualizedSectionListHandle surface) — delegates down ----
465
-
466
315
  scrollToLocation(params: {
467
316
  sectionIndex: number;
468
317
  itemIndex: number;
@@ -0,0 +1,199 @@
1
+ // Everything a windowed list keeps per cell between passes: the context objects (reused by
2
+ // identity), the measure closures, and the separator overrides a row drives through `separators`
3
+
4
+ import {
5
+ isSeparatorGapInRange,
6
+ readLayoutLength,
7
+ readLayoutOffset,
8
+ type ISeparatorProps,
9
+ type ISeparators,
10
+ } from '@symbiote-native/components';
11
+ import { dlog, type ISymbioteEvent } from '@symbiote-native/engine';
12
+ import type { IVListItemContext, IVListSeparatorContext } from './directives';
13
+
14
+ // One in-window cell, assembled per pass and stamped by the template's `@for`
15
+ export interface IWindowCell<ItemT> {
16
+ key: string;
17
+ index: number;
18
+ context: IVListItemContext<ItemT>;
19
+ measure: (event: ISymbioteEvent) => void;
20
+ separatorContext?: IVListSeparatorContext<ItemT>;
21
+ // Picks the cell's TAG in the template: `sticky-header` pins, `view` does not. Stable per cell
22
+ // key, so the `@if` never swaps an element type under a live cell as the window slides
23
+ isSticky: boolean;
24
+ }
25
+
26
+ export type ICellHost<ItemT> = {
27
+ itemAt: (index: number) => ItemT;
28
+ itemCount: () => number;
29
+ isHorizontal: () => boolean;
30
+ markForCheck: () => void;
31
+ measured: (index: number, length: number, offset: number | undefined) => void;
32
+ };
33
+
34
+ // The gap a cell's `updateProps` side addresses, relative to the cell's own index
35
+ const SEPARATOR_GAP_OFFSET = { leading: -1, trailing: 0 } as const;
36
+
37
+ export class CellRegistry<ItemT> {
38
+ // A cell's context is the only thing its stamped view reads, and the outlet refreshes that view
39
+ // whenever the object's identity changes. Handing back the same object for a cell whose item and
40
+ // index did not move keeps a window slide from refreshing every cell in it
41
+ private contexts = new Map<string, IVListItemContext<ItemT>>();
42
+ private nextContexts = new Map<string, IVListItemContext<ItemT>>();
43
+ // What a cell's content can depend on besides its own item: an `extraData` flip has to drop
44
+ // every reused context or nothing would repaint
45
+ private epoch: unknown[] | undefined = undefined;
46
+ private readonly separatorOverrides = new Map<
47
+ number,
48
+ Partial<ISeparatorProps<unknown>>
49
+ >();
50
+ private readonly measures = new Map<
51
+ number,
52
+ (event: ISymbioteEvent) => void
53
+ >();
54
+
55
+ constructor(private readonly host: ICellHost<ItemT>) {}
56
+
57
+ beginPass(epoch: unknown[]): void {
58
+ const previous = this.epoch;
59
+ this.epoch = epoch;
60
+ if (
61
+ previous === undefined ||
62
+ previous.length !== epoch.length ||
63
+ !previous.every((value, index) => value === epoch[index])
64
+ ) {
65
+ this.contexts.clear();
66
+ }
67
+ }
68
+
69
+ clear(): void {
70
+ this.contexts.clear();
71
+ this.nextContexts.clear();
72
+ }
73
+
74
+ // A cell that left the window is absent from the pass that just ran, so the old map is the
75
+ // garbage and the new one is exactly the live set
76
+ endPass(): void {
77
+ this.contexts = this.nextContexts;
78
+ this.nextContexts = new Map<string, IVListItemContext<ItemT>>();
79
+ }
80
+
81
+ windowCell(
82
+ index: number,
83
+ key: string,
84
+ includeSeparator: boolean,
85
+ isSticky: boolean,
86
+ ): IWindowCell<ItemT> {
87
+ const item = this.host.itemAt(index);
88
+ return {
89
+ key,
90
+ index,
91
+ context: this.contextFor(key, index, item),
92
+ measure: this.measureFor(index),
93
+ separatorContext: includeSeparator
94
+ ? this.separatorContextFor(index, item)
95
+ : undefined,
96
+ isSticky,
97
+ };
98
+ }
99
+
100
+ private contextFor(
101
+ key: string,
102
+ index: number,
103
+ item: ItemT,
104
+ ): IVListItemContext<ItemT> {
105
+ const cached = this.contexts.get(key);
106
+ if (
107
+ cached !== undefined &&
108
+ cached.$implicit === item &&
109
+ cached.index === index
110
+ ) {
111
+ this.nextContexts.set(key, cached);
112
+ return cached;
113
+ }
114
+ const context: IVListItemContext<ItemT> = {
115
+ $implicit: item,
116
+ index,
117
+ separators: this.makeSeparators(index),
118
+ };
119
+ this.nextContexts.set(key, context);
120
+ return context;
121
+ }
122
+
123
+ private separatorContextFor(
124
+ index: number,
125
+ item: ItemT,
126
+ ): IVListSeparatorContext<ItemT> {
127
+ const overrides = this.separatorOverrides.get(index);
128
+ const highlighted = overrides?.highlighted ?? false;
129
+ const context: IVListSeparatorContext<ItemT> = {
130
+ $implicit: highlighted,
131
+ highlighted,
132
+ leadingItem: item,
133
+ trailingItem: this.host.itemAt(index + 1),
134
+ };
135
+ // Custom `updateProps` keys ride the index signature, typed fields stay authoritative
136
+ if (overrides !== undefined) {
137
+ for (const key of Object.keys(overrides)) {
138
+ if (
139
+ key === 'highlighted' ||
140
+ key === 'leadingItem' ||
141
+ key === 'trailingItem'
142
+ )
143
+ continue;
144
+ context[key] = overrides[key];
145
+ }
146
+ }
147
+ return context;
148
+ }
149
+
150
+ private measureFor(index: number): (event: ISymbioteEvent) => void {
151
+ const existing = this.measures.get(index);
152
+ if (existing !== undefined) return existing;
153
+ const measure = (event: ISymbioteEvent): void => {
154
+ const horizontal = this.host.isHorizontal();
155
+ const length = readLayoutLength(event, horizontal);
156
+ if (length === undefined) return;
157
+ const offset = readLayoutOffset(event, horizontal);
158
+ dlog(
159
+ `Angular VirtualizedList cell ${index} measured length=${length} offset=${offset ?? 'none'}`,
160
+ );
161
+ this.host.measured(index, length, offset);
162
+ };
163
+ this.measures.set(index, measure);
164
+ return measure;
165
+ }
166
+
167
+ private mergeSeparator(
168
+ gapIndex: number,
169
+ patch: Partial<ISeparatorProps<unknown>>,
170
+ ): void {
171
+ if (!isSeparatorGapInRange(gapIndex, this.host.itemCount())) return;
172
+ this.separatorOverrides.set(gapIndex, {
173
+ ...this.separatorOverrides.get(gapIndex),
174
+ ...patch,
175
+ });
176
+ this.host.markForCheck();
177
+ }
178
+
179
+ private makeSeparators(index: number): ISeparators {
180
+ return {
181
+ highlight: (): void => {
182
+ dlog(`Angular VirtualizedList separator highlight cell=${index}`);
183
+ this.mergeSeparator(index - 1, { highlighted: true });
184
+ this.mergeSeparator(index, { highlighted: true });
185
+ },
186
+ unhighlight: (): void => {
187
+ dlog(`Angular VirtualizedList separator unhighlight cell=${index}`);
188
+ this.mergeSeparator(index - 1, { highlighted: false });
189
+ this.mergeSeparator(index, { highlighted: false });
190
+ },
191
+ updateProps: (
192
+ select: keyof typeof SEPARATOR_GAP_OFFSET,
193
+ newProps: Record<string, unknown>,
194
+ ): void => {
195
+ this.mergeSeparator(index + SEPARATOR_GAP_OFFSET[select], newProps);
196
+ },
197
+ };
198
+ }
199
+ }