@symbiote-native/angular 3.1.3 → 3.3.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 (165) hide show
  1. package/babel-register-composed.cjs +1 -0
  2. package/build/angular/accessibility-events.d.ts +12 -0
  3. package/build/angular/accessibility-events.js +28 -0
  4. package/build/angular/accessibility-events.js.map +1 -0
  5. package/build/angular/accessibility-inputs.d.ts +40 -0
  6. package/build/angular/accessibility-inputs.js +130 -0
  7. package/build/angular/accessibility-inputs.js.map +1 -0
  8. package/build/angular/bootstrap.d.ts +3 -1
  9. package/build/angular/bootstrap.js +4 -1
  10. package/build/angular/bootstrap.js.map +1 -1
  11. package/build/angular/components/checkbox-props.d.ts +2 -0
  12. package/build/angular/components/checkbox-props.js +2 -0
  13. package/build/angular/components/checkbox-props.js.map +1 -0
  14. package/build/angular/components/flat-list/flat-list-template.d.ts +1 -0
  15. package/build/angular/components/flat-list/flat-list-template.js +158 -0
  16. package/build/angular/components/flat-list/flat-list-template.js.map +1 -0
  17. package/build/angular/components/flat-list/index.d.ts +6 -61
  18. package/build/angular/components/flat-list/index.js +26 -463
  19. package/build/angular/components/flat-list/index.js.map +1 -1
  20. package/build/angular/components/keyboard-avoiding-view/index.d.ts +4 -40
  21. package/build/angular/components/keyboard-avoiding-view/index.js +7 -131
  22. package/build/angular/components/keyboard-avoiding-view/index.js.map +1 -1
  23. package/build/angular/components/modal/index.d.ts +5 -41
  24. package/build/angular/components/modal/index.js +8 -133
  25. package/build/angular/components/modal/index.js.map +1 -1
  26. package/build/angular/components/section-list/index.d.ts +4 -51
  27. package/build/angular/components/section-list/index.js +18 -335
  28. package/build/angular/components/section-list/index.js.map +1 -1
  29. package/build/angular/components/virtualized-list/cell-registry.d.ts +35 -0
  30. package/build/angular/components/virtualized-list/cell-registry.js +133 -0
  31. package/build/angular/components/virtualized-list/cell-registry.js.map +1 -0
  32. package/build/angular/components/virtualized-list/index.d.ts +18 -253
  33. package/build/angular/components/virtualized-list/index.js +112 -1178
  34. package/build/angular/components/virtualized-list/index.js.map +1 -1
  35. package/build/angular/components/virtualized-list/list-a11y-forward.d.ts +1 -0
  36. package/build/angular/components/virtualized-list/list-a11y-forward.js +42 -0
  37. package/build/angular/components/virtualized-list/list-a11y-forward.js.map +1 -0
  38. package/build/angular/components/virtualized-list/list-bags.d.ts +32 -0
  39. package/build/angular/components/virtualized-list/list-bags.js +120 -0
  40. package/build/angular/components/virtualized-list/list-bags.js.map +1 -0
  41. package/build/angular/components/virtualized-list/list-effects.d.ts +31 -0
  42. package/build/angular/components/virtualized-list/list-effects.js +83 -0
  43. package/build/angular/components/virtualized-list/list-effects.js.map +1 -0
  44. package/build/angular/components/virtualized-list/list-events.d.ts +17 -0
  45. package/build/angular/components/virtualized-list/list-events.js +34 -0
  46. package/build/angular/components/virtualized-list/list-events.js.map +1 -0
  47. package/build/angular/components/virtualized-list/list-handle.d.ts +35 -0
  48. package/build/angular/components/virtualized-list/list-handle.js +61 -0
  49. package/build/angular/components/virtualized-list/list-handle.js.map +1 -0
  50. package/build/angular/components/virtualized-list/list-inputs.d.ts +78 -0
  51. package/build/angular/components/virtualized-list/list-inputs.js +150 -0
  52. package/build/angular/components/virtualized-list/list-inputs.js.map +1 -0
  53. package/build/angular/components/virtualized-list/list-props.d.ts +58 -0
  54. package/build/angular/components/virtualized-list/list-props.js +3 -0
  55. package/build/angular/components/virtualized-list/list-props.js.map +1 -0
  56. package/build/angular/components/virtualized-list/list-styles.d.ts +23 -0
  57. package/build/angular/components/virtualized-list/list-styles.js +26 -0
  58. package/build/angular/components/virtualized-list/list-styles.js.map +1 -0
  59. package/build/angular/components/virtualized-list/list-template.d.ts +1 -0
  60. package/build/angular/components/virtualized-list/list-template.js +176 -0
  61. package/build/angular/components/virtualized-list/list-template.js.map +1 -0
  62. package/build/angular/components/virtualized-section-list/index.d.ts +13 -70
  63. package/build/angular/components/virtualized-section-list/index.js +22 -455
  64. package/build/angular/components/virtualized-section-list/index.js.map +1 -1
  65. package/build/angular/components/virtualized-section-list/section-list-template.d.ts +1 -0
  66. package/build/angular/components/virtualized-section-list/section-list-template.js +107 -0
  67. package/build/angular/components/virtualized-section-list/section-list-template.js.map +1 -0
  68. package/build/angular/components.d.ts +1 -0
  69. package/build/angular/components.js.map +1 -1
  70. package/build/angular/descriptor-to-angular/descriptor-host.d.ts +8 -0
  71. package/build/angular/descriptor-to-angular/descriptor-host.js +38 -0
  72. package/build/angular/descriptor-to-angular/descriptor-host.js.map +1 -0
  73. package/build/angular/descriptor-to-angular/index.d.ts +1 -0
  74. package/build/angular/descriptor-to-angular/index.js +4 -0
  75. package/build/angular/descriptor-to-angular/index.js.map +1 -1
  76. package/build/angular/element-base.d.ts +153 -0
  77. package/build/angular/element-base.js +272 -0
  78. package/build/angular/element-base.js.map +1 -0
  79. package/build/angular/elements-controls.d.ts +165 -0
  80. package/build/angular/elements-controls.js +382 -0
  81. package/build/angular/elements-controls.js.map +1 -0
  82. package/build/angular/elements-layout.d.ts +122 -0
  83. package/build/angular/elements-layout.js +242 -0
  84. package/build/angular/elements-layout.js.map +1 -0
  85. package/build/angular/elements-press.d.ts +96 -0
  86. package/build/angular/elements-press.js +227 -0
  87. package/build/angular/elements-press.js.map +1 -0
  88. package/build/angular/elements.d.ts +6 -516
  89. package/build/angular/elements.js +8 -1091
  90. package/build/angular/elements.js.map +1 -1
  91. package/build/angular/finalization-registry.d.ts +4 -0
  92. package/build/angular/finalization-registry.js +17 -0
  93. package/build/angular/finalization-registry.js.map +1 -0
  94. package/build/angular/host-instance/index.d.ts +2 -0
  95. package/build/angular/host-instance/index.js +4 -4
  96. package/build/angular/host-instance/index.js.map +1 -1
  97. package/build/angular/index.d.ts +9 -4
  98. package/build/angular/index.js +8 -3
  99. package/build/angular/index.js.map +1 -1
  100. package/build/angular/native-view-base.d.ts +27 -0
  101. package/build/angular/native-view-base.js +69 -0
  102. package/build/angular/native-view-base.js.map +1 -0
  103. package/build/angular/render/index.d.ts +2 -1
  104. package/build/angular/render/index.js +46 -73
  105. package/build/angular/render/index.js.map +1 -1
  106. package/build/angular/services/connect-watched-signal.d.ts +2 -0
  107. package/build/angular/services/connect-watched-signal.js +9 -0
  108. package/build/angular/services/connect-watched-signal.js.map +1 -0
  109. package/build/angular/services/create-event-value-hook.d.ts +3 -0
  110. package/build/angular/services/create-event-value-hook.js +17 -0
  111. package/build/angular/services/create-event-value-hook.js.map +1 -0
  112. package/build/angular/services/create-resource-hook.d.ts +7 -0
  113. package/build/angular/services/create-resource-hook.js +20 -0
  114. package/build/angular/services/create-resource-hook.js.map +1 -0
  115. package/build/angular/services/index.d.ts +5 -0
  116. package/build/angular/services/index.js +5 -0
  117. package/build/angular/services/index.js.map +1 -1
  118. package/build/angular/services/inject-event.d.ts +6 -0
  119. package/build/angular/services/inject-event.js +24 -0
  120. package/build/angular/services/inject-event.js.map +1 -0
  121. package/build/angular/services/permissions-service-base.d.ts +14 -0
  122. package/build/angular/services/permissions-service-base.js +38 -0
  123. package/build/angular/services/permissions-service-base.js.map +1 -0
  124. package/package.json +6 -6
  125. package/src/accessibility-events.ts +13 -0
  126. package/src/accessibility-inputs.ts +89 -0
  127. package/src/bootstrap.ts +6 -2
  128. package/src/components/checkbox-props.ts +12 -0
  129. package/src/components/flat-list/flat-list-template.ts +159 -0
  130. package/src/components/flat-list/index.ts +30 -307
  131. package/src/components/keyboard-avoiding-view/index.ts +4 -78
  132. package/src/components/modal/index.ts +7 -80
  133. package/src/components/section-list/index.ts +21 -172
  134. package/src/components/virtualized-list/cell-registry.ts +199 -0
  135. package/src/components/virtualized-list/index.ts +145 -1181
  136. package/src/components/virtualized-list/list-a11y-forward.ts +42 -0
  137. package/src/components/virtualized-list/list-bags.ts +142 -0
  138. package/src/components/virtualized-list/list-effects.ts +116 -0
  139. package/src/components/virtualized-list/list-events.ts +31 -0
  140. package/src/components/virtualized-list/list-handle.ts +92 -0
  141. package/src/components/virtualized-list/list-inputs.ts +149 -0
  142. package/src/components/virtualized-list/list-props.ts +81 -0
  143. package/src/components/virtualized-list/list-styles.ts +62 -0
  144. package/src/components/virtualized-list/list-template.ts +175 -0
  145. package/src/components/virtualized-section-list/index.ts +40 -351
  146. package/src/components/virtualized-section-list/section-list-template.ts +108 -0
  147. package/src/components.ts +1 -0
  148. package/src/descriptor-to-angular/descriptor-host.ts +27 -0
  149. package/src/descriptor-to-angular/index.ts +5 -0
  150. package/src/element-base.ts +272 -0
  151. package/src/elements-controls.ts +282 -0
  152. package/src/elements-layout.ts +141 -0
  153. package/src/elements-press.ts +145 -0
  154. package/src/elements.ts +73 -788
  155. package/src/finalization-registry.ts +18 -0
  156. package/src/host-instance/index.ts +4 -4
  157. package/src/index.ts +18 -2
  158. package/src/native-view-base.ts +86 -0
  159. package/src/render/index.ts +76 -77
  160. package/src/services/connect-watched-signal.ts +15 -0
  161. package/src/services/create-event-value-hook.ts +24 -0
  162. package/src/services/create-resource-hook.ts +32 -0
  163. package/src/services/index.ts +5 -0
  164. package/src/services/inject-event.ts +52 -0
  165. package/src/services/permissions-service-base.ts +46 -0
@@ -0,0 +1,42 @@
1
+ // Forwards every accessibility input and gated event of a wrapper list to the list it renders
2
+ // Angular has no attribute spread, and a template must be one static string
3
+
4
+ export const LIST_ACCESSIBILITY_FORWARD = `
5
+ [accessible]="accessible"
6
+ [accessibilityLabel]="accessibilityLabel"
7
+ [accessibilityHint]="accessibilityHint"
8
+ [accessibilityRole]="accessibilityRole"
9
+ [accessibilityState]="accessibilityState"
10
+ [accessibilityValue]="accessibilityValue"
11
+ [accessibilityActions]="accessibilityActions"
12
+ [accessibilityLabelledBy]="accessibilityLabelledBy"
13
+ [importantForAccessibility]="importantForAccessibility"
14
+ [accessibilityLiveRegion]="accessibilityLiveRegion"
15
+ [screenReaderFocusable]="screenReaderFocusable"
16
+ [accessibilityViewIsModal]="accessibilityViewIsModal"
17
+ [accessibilityElementsHidden]="accessibilityElementsHidden"
18
+ [accessibilityIgnoresInvertColors]="accessibilityIgnoresInvertColors"
19
+ [accessibilityLanguage]="accessibilityLanguage"
20
+ [accessibilityRespondsToUserInteraction]="accessibilityRespondsToUserInteraction"
21
+ [accessibilityShowsLargeContentViewer]="accessibilityShowsLargeContentViewer"
22
+ [accessibilityLargeContentTitle]="accessibilityLargeContentTitle"
23
+ (accessibilityAction)="accessibilityAction.emit($event)"
24
+ (accessibilityTap)="accessibilityTap.emit($event)"
25
+ (magicTap)="magicTap.emit($event)"
26
+ (accessibilityEscape)="accessibilityEscape.emit($event)"
27
+ [role]="role"
28
+ [aria-label]="ariaLabel"
29
+ [aria-labelledby]="ariaLabelledBy"
30
+ [aria-live]="ariaLive"
31
+ [aria-hidden]="ariaHidden"
32
+ [aria-busy]="ariaBusy"
33
+ [aria-checked]="ariaChecked"
34
+ [aria-disabled]="ariaDisabled"
35
+ [aria-expanded]="ariaExpanded"
36
+ [aria-selected]="ariaSelected"
37
+ [aria-modal]="ariaModal"
38
+ [aria-valuemax]="ariaValueMax"
39
+ [aria-valuemin]="ariaValueMin"
40
+ [aria-valuenow]="ariaValueNow"
41
+ [aria-valuetext]="ariaValueText"
42
+ `;
@@ -0,0 +1,142 @@
1
+ // The prop bags the list writes onto its scroll tag and refresh control, with the native callbacks
2
+ // that feed them, kept apart from the windowing lifecycle of the component
3
+
4
+ import { Directive, computed, signal, type OnChanges } from '@angular/core';
5
+ import {
6
+ LIST_ACTION_KIND,
7
+ readLayoutLength,
8
+ readScrollOffset,
9
+ resolveAccessibilityProps,
10
+ type IAccessibilityProps,
11
+ type IAriaProps,
12
+ } from '@symbiote-native/components';
13
+ import {
14
+ dlog,
15
+ type IStyleProp,
16
+ type ISymbioteEvent,
17
+ type IViewStyle,
18
+ } from '@symbiote-native/engine';
19
+ import { countAngular } from '../../diagnostics';
20
+ import { VirtualizedListHandleBase } from './list-handle';
21
+
22
+ @Directive()
23
+ export abstract class VirtualizedListBagsBase<ItemT>
24
+ extends VirtualizedListHandleBase<ItemT>
25
+ implements OnChanges
26
+ {
27
+ resolvedStyle: IViewStyle | undefined = undefined;
28
+ resolvedContentContainerStyle: IStyleProp<IViewStyle> | undefined = undefined;
29
+ resolvedMaintainVisibleContentPosition:
30
+ | { minIndexForVisible: number; autoscrollToTopThreshold?: number }
31
+ | undefined = undefined;
32
+ // A fresh object per push, so the commit re-applies a repeated offset
33
+ commandedOffset: { x: number; y: number } | undefined = undefined;
34
+
35
+ // Only inputs may feed this signal, window state changes skip `ngOnChanges`
36
+ private readonly inputsRevision = signal(0);
37
+
38
+ // A computed hands back one object until an input changes, a getter rebuilt it per binding
39
+ readonly foldedAccessibility = computed<
40
+ IAccessibilityProps & IAriaProps & Record<string, unknown>
41
+ >(() => {
42
+ this.inputsRevision();
43
+ return resolveAccessibilityProps({
44
+ testID: this.testID,
45
+ nativeID: this.nativeID,
46
+ ...this.accessibilityInputProps(),
47
+ });
48
+ });
49
+
50
+ // The one moment every changed input is written, before `ngDoCheck`
51
+ ngOnChanges(): void {
52
+ this.inputsRevision.update(revision => revision + 1);
53
+ }
54
+
55
+ get shouldRenderRefreshControl(): boolean {
56
+ return this.refreshRequested ?? this.refresh.observed;
57
+ }
58
+
59
+ // A method, not a computed: most fields are plain and the directive diffs per key anyway
60
+ scrollViewBag(): Record<string, unknown> {
61
+ return {
62
+ ...this.foldedAccessibility(),
63
+ style: this.resolvedStyle,
64
+ contentContainerStyle: this.resolvedContentContainerStyle,
65
+ onScroll: this.onScrollTick,
66
+ onLayout: this.onLayoutTick,
67
+ onScrollBeginDrag: this.onScrollBeginDrag,
68
+ onScrollEndDrag: this.onScrollEndDrag,
69
+ onMomentumScrollBegin: this.onMomentumScrollBegin,
70
+ onMomentumScrollEnd: this.onMomentumScrollEnd,
71
+ scrollEventThrottle: this.scrollEventThrottle,
72
+ keyboardShouldPersistTaps: this.keyboardShouldPersistTaps,
73
+ keyboardDismissMode: this.keyboardDismissMode,
74
+ removeClippedSubviews: this.removeClippedSubviews,
75
+ nestedScrollEnabled: this.nestedScrollEnabled,
76
+ // Android moves the scrollbar back after the `scale: -1` flip
77
+ isInvertedVirtualizedList: this.isInverted ? true : undefined,
78
+ contentOffset: this.commandedOffset,
79
+ // `stickyHeaderIndices` is not forwarded, the cells carry the `sticky-header` tag instead
80
+ maintainVisibleContentPosition:
81
+ this.resolvedMaintainVisibleContentPosition,
82
+ // The tag has no `.observed`, so `wantsGate` decides whether the flag reaches Fabric
83
+ onAccessibilityAction: this.wantsGate('accessibilityAction')
84
+ ? this.accessibilityActionTick
85
+ : undefined,
86
+ onAccessibilityTap: this.wantsGate('accessibilityTap')
87
+ ? this.accessibilityTapTick
88
+ : undefined,
89
+ onMagicTap: this.wantsGate('magicTap') ? this.magicTapTick : undefined,
90
+ onAccessibilityEscape: this.wantsGate('accessibilityEscape')
91
+ ? this.accessibilityEscapeTick
92
+ : undefined,
93
+ };
94
+ }
95
+
96
+ refreshControlBag(): Record<string, unknown> {
97
+ return {
98
+ refreshing: this.refreshing ?? false,
99
+ progressViewOffset: this.progressViewOffset,
100
+ onRefresh: this.handleRefresh,
101
+ };
102
+ }
103
+
104
+ // `onScroll` stays a callback so an `Animated.event` target can flow through it
105
+ onScrollTick = (event: ISymbioteEvent): void => {
106
+ countAngular('scrollTicks');
107
+ const offset = readScrollOffset(event, this.isHorizontal);
108
+ if (offset === undefined) return;
109
+ dlog(`Angular VirtualizedList onScroll offset=${offset}`);
110
+ // A real native scroll supersedes any pending commanded offset
111
+ this.commandedOffset = undefined;
112
+ this.dispatch({ kind: LIST_ACTION_KIND.scroll, offset });
113
+ this.onScroll?.(event);
114
+ };
115
+
116
+ onLayoutTick = (event: ISymbioteEvent): void => {
117
+ const length = readLayoutLength(event, this.isHorizontal);
118
+ if (length === undefined) return;
119
+ dlog(`Angular VirtualizedList onLayout viewport=${length}`);
120
+ this.dispatch({ kind: LIST_ACTION_KIND.layout, length });
121
+ };
122
+
123
+ handleRefresh = (): void => {
124
+ this.refresh.emit();
125
+ };
126
+
127
+ accessibilityActionTick = (event: ISymbioteEvent): void => {
128
+ this.accessibilityAction.emit(event);
129
+ };
130
+
131
+ accessibilityTapTick = (event: ISymbioteEvent): void => {
132
+ this.accessibilityTap.emit(event);
133
+ };
134
+
135
+ magicTapTick = (event: ISymbioteEvent): void => {
136
+ this.magicTap.emit(event);
137
+ };
138
+
139
+ accessibilityEscapeTick = (event: ISymbioteEvent): void => {
140
+ this.accessibilityEscape.emit(event);
141
+ };
142
+ }
@@ -0,0 +1,116 @@
1
+ // Runs the effects the shared list reducer hands back: scrolls, edge events and the two timers the
2
+ // reducer only knows a delay for (the incremental fill and the viewability debounce)
3
+
4
+ import {
5
+ EMPTY_OFFSET,
6
+ LIST_ACTION_KIND,
7
+ LIST_EFFECT_KIND,
8
+ type IListAction,
9
+ type IListEffect,
10
+ type IListReducerInputs,
11
+ } from '@symbiote-native/components';
12
+
13
+ type IEmitter<TValue> = { emit: (value: TValue) => void };
14
+
15
+ export type IEffectHost<ItemT> = {
16
+ scrollToPixel: (offset: number, animated: boolean) => void;
17
+ dispatch: (action: IListAction<ItemT>) => void;
18
+ endReached: IEmitter<{ distanceFromEnd: number }>;
19
+ startReached: IEmitter<{ distanceFromStart: number }>;
20
+ scrollToIndexFailed: IEmitter<{
21
+ index: number;
22
+ highestMeasuredFrameIndex: number;
23
+ averageItemLength: number;
24
+ }>;
25
+ };
26
+
27
+ type IViewableEffect<ItemT> = Extract<
28
+ IListEffect<ItemT>,
29
+ { kind: typeof LIST_EFFECT_KIND.fireViewable }
30
+ >;
31
+
32
+ export class ListEffectRunner<ItemT> {
33
+ private viewableTimer: ReturnType<typeof setTimeout> | null = null;
34
+ private batchTimer: ReturnType<typeof setTimeout> | null = null;
35
+
36
+ constructor(private readonly host: IEffectHost<ItemT>) {}
37
+
38
+ run(effects: IListEffect<ItemT>[], inputs: IListReducerInputs<ItemT>): void {
39
+ for (const effect of effects) this.runOne(effect, inputs);
40
+ }
41
+
42
+ dispose(): void {
43
+ if (this.viewableTimer !== null) clearTimeout(this.viewableTimer);
44
+ if (this.batchTimer !== null) clearTimeout(this.batchTimer);
45
+ }
46
+
47
+ private runOne(
48
+ effect: IListEffect<ItemT>,
49
+ inputs: IListReducerInputs<ItemT>,
50
+ ): void {
51
+ switch (effect.kind) {
52
+ case LIST_EFFECT_KIND.scrollTo:
53
+ this.host.scrollToPixel(effect.offset, effect.animated);
54
+ break;
55
+ case LIST_EFFECT_KIND.fireEndReached:
56
+ this.host.endReached.emit({ distanceFromEnd: effect.distanceFromEnd });
57
+ break;
58
+ case LIST_EFFECT_KIND.fireStartReached:
59
+ this.host.startReached.emit({
60
+ distanceFromStart: effect.distanceFromStart,
61
+ });
62
+ break;
63
+ case LIST_EFFECT_KIND.fireScrollToIndexFailed:
64
+ this.host.scrollToIndexFailed.emit({
65
+ index: effect.index,
66
+ highestMeasuredFrameIndex: effect.highestMeasuredFrameIndex,
67
+ averageItemLength: effect.averageItemLength,
68
+ });
69
+ break;
70
+ case LIST_EFFECT_KIND.scheduleRefill:
71
+ this.scheduleRefill(effect.delay);
72
+ break;
73
+ case LIST_EFFECT_KIND.fireViewable:
74
+ this.fireViewable(effect, inputs);
75
+ break;
76
+ }
77
+ }
78
+
79
+ private scheduleRefill(delay: number): void {
80
+ if (this.batchTimer !== null) clearTimeout(this.batchTimer);
81
+ this.batchTimer = setTimeout(() => {
82
+ this.batchTimer = null;
83
+ this.host.dispatch({ kind: LIST_ACTION_KIND.batchTick });
84
+ }, delay);
85
+ }
86
+
87
+ private fireViewable(
88
+ effect: IViewableEffect<ItemT>,
89
+ inputs: IListReducerInputs<ItemT>,
90
+ ): void {
91
+ const fire = (): void => {
92
+ for (const pair of inputs.viewabilityPairs) {
93
+ pair.onViewableItemsChanged({
94
+ ...effect.info,
95
+ viewabilityConfig: pair.viewabilityConfig,
96
+ });
97
+ }
98
+ this.host.dispatch({
99
+ kind: LIST_ACTION_KIND.viewableFired,
100
+ map: effect.map,
101
+ });
102
+ };
103
+ if (this.viewableTimer !== null) {
104
+ clearTimeout(this.viewableTimer);
105
+ this.viewableTimer = null;
106
+ }
107
+ if (effect.delay <= EMPTY_OFFSET) {
108
+ fire();
109
+ return;
110
+ }
111
+ this.viewableTimer = setTimeout(() => {
112
+ this.viewableTimer = null;
113
+ fire();
114
+ }, effect.delay);
115
+ }
116
+ }
@@ -0,0 +1,31 @@
1
+ // The events every list component exposes, from `VirtualizedList` up to `SectionList`, so the
2
+ // wrappers stop redeclaring them
3
+
4
+ import { Directive, EventEmitter, Output } from '@angular/core';
5
+ import { AccessibilityEventsBase } from '../../accessibility-events';
6
+ import {
7
+ gateWanted,
8
+ injectGateDemandAbove,
9
+ type IGatedAccessibilityEvent,
10
+ } from '../../gate-demand';
11
+
12
+ @Directive()
13
+ export abstract class ListEventsBase extends AccessibilityEventsBase {
14
+ // The edge and refresh events are real Angular events: `(endReached)`, not `[onEndReached]`
15
+ @Output() readonly endReached = new EventEmitter<{
16
+ distanceFromEnd: number;
17
+ }>();
18
+ @Output() readonly startReached = new EventEmitter<{
19
+ distanceFromStart: number;
20
+ }>();
21
+ @Output() readonly refresh = new EventEmitter<void>();
22
+
23
+ // A list binds the four gated accessibility events on what it renders, which Angular forces to be
24
+ // unconditional and which would light that component's gates on every instance. It answers for
25
+ // them instead, see `gate-demand.ts`
26
+ private readonly gateDemandAbove = injectGateDemandAbove();
27
+
28
+ wantsGate(name: IGatedAccessibilityEvent): boolean {
29
+ return gateWanted(this.gateDemandAbove, name, this[name]);
30
+ }
31
+ }
@@ -0,0 +1,92 @@
1
+ // The imperative scroll surface of the list, kept apart so the component stays on windowing
2
+
3
+ import { Directive } from '@angular/core';
4
+ import {
5
+ EMPTY_OFFSET,
6
+ FIRST_INDEX,
7
+ LIST_ACTION_KIND,
8
+ type IListAction,
9
+ type IScrollViewHandle,
10
+ type IVirtualizedListHandle,
11
+ } from '@symbiote-native/components';
12
+ import type { ISymbioteNode } from '@symbiote-native/engine';
13
+ import { VirtualizedListInputs } from './list-inputs';
14
+
15
+ @Directive()
16
+ export abstract class VirtualizedListHandleBase<ItemT>
17
+ extends VirtualizedListInputs<ItemT>
18
+ implements IVirtualizedListHandle
19
+ {
20
+ protected abstract readonly scrollHandle: IScrollViewHandle;
21
+
22
+ protected abstract get scrollNode(): ISymbioteNode | null;
23
+
24
+ protected abstract dispatch(action: IListAction<ItemT>): void;
25
+
26
+ scrollToOffset(params: { offset: number; animated?: boolean }): void {
27
+ this.dispatch({
28
+ kind: LIST_ACTION_KIND.scrollToOffset,
29
+ offset: params.offset,
30
+ animated: params.animated ?? true,
31
+ });
32
+ }
33
+
34
+ scrollToIndex(params: {
35
+ index: number;
36
+ animated?: boolean;
37
+ viewOffset?: number;
38
+ viewPosition?: number;
39
+ }): void {
40
+ this.dispatch({
41
+ kind: LIST_ACTION_KIND.scrollToIndex,
42
+ index: params.index,
43
+ animated: params.animated ?? true,
44
+ viewPosition: params.viewPosition ?? FIRST_INDEX,
45
+ viewOffset: params.viewOffset ?? EMPTY_OFFSET,
46
+ });
47
+ }
48
+
49
+ scrollToItem(params: {
50
+ item: unknown;
51
+ animated?: boolean;
52
+ viewPosition?: number;
53
+ }): void {
54
+ this.dispatch({
55
+ kind: LIST_ACTION_KIND.scrollToItem,
56
+ item: params.item,
57
+ animated: params.animated ?? true,
58
+ viewPosition: params.viewPosition ?? FIRST_INDEX,
59
+ });
60
+ }
61
+
62
+ scrollToEnd(params?: { animated?: boolean }): void {
63
+ this.dispatch({
64
+ kind: LIST_ACTION_KIND.scrollToEnd,
65
+ animated: params?.animated ?? true,
66
+ });
67
+ }
68
+
69
+ flashScrollIndicators(): void {
70
+ this.scrollHandle.flashScrollIndicators();
71
+ }
72
+
73
+ getNativeScrollRef(): IScrollViewHandle | null {
74
+ return this.scrollNode !== null ? this.scrollHandle : null;
75
+ }
76
+
77
+ getScrollableNode(): IScrollViewHandle | null {
78
+ return this.scrollNode !== null ? this.scrollHandle : null;
79
+ }
80
+
81
+ getScrollResponder(): IScrollViewHandle | null {
82
+ return this.scrollNode !== null ? this.scrollHandle : null;
83
+ }
84
+
85
+ getScrollNode(): ISymbioteNode | null {
86
+ return this.scrollHandle.getScrollNode();
87
+ }
88
+
89
+ recordInteraction(): void {
90
+ this.dispatch({ kind: LIST_ACTION_KIND.recordInteraction });
91
+ }
92
+ }
@@ -0,0 +1,149 @@
1
+ // The list surface Angular binds statically: inputs, outputs and projected templates. Kept apart
2
+ // from the components so the windowing lifecycle reads on its own
3
+
4
+ import {
5
+ ContentChild,
6
+ Directive,
7
+ EventEmitter,
8
+ Input,
9
+ Output,
10
+ TemplateRef,
11
+ } from '@angular/core';
12
+ import {
13
+ DEFAULT_INITIAL_NUM_TO_RENDER,
14
+ DEFAULT_MAX_TO_RENDER_PER_BATCH,
15
+ DEFAULT_UPDATE_CELLS_BATCHING_PERIOD,
16
+ DEFAULT_WINDOW_SIZE,
17
+ type IViewabilityConfig,
18
+ type IViewabilityConfigCallbackPair,
19
+ type IViewableItemsChangedInfo,
20
+ } from '@symbiote-native/components';
21
+ import type {
22
+ IStyleProp,
23
+ ISymbioteEvent,
24
+ IViewStyle,
25
+ } from '@symbiote-native/engine';
26
+ import {
27
+ VListEmptyDirective,
28
+ VListFooterDirective,
29
+ VListHeaderDirective,
30
+ VListItemDirective,
31
+ VListSeparatorDirective,
32
+ type IVListItemContext,
33
+ type IVListSeparatorContext,
34
+ } from './directives';
35
+ import { ListEventsBase } from './list-events';
36
+
37
+ // What `VirtualizedList` and `FlatList` share: the windowing inputs and the slot templates
38
+ @Directive()
39
+ export abstract class ListInputsBase<ItemT> extends ListEventsBase {
40
+ @Output() readonly viewableItemsChanged = new EventEmitter<
41
+ IViewableItemsChangedInfo<ItemT>
42
+ >();
43
+ @Output() readonly scrollToIndexFailed = new EventEmitter<{
44
+ index: number;
45
+ highestMeasuredFrameIndex: number;
46
+ averageItemLength: number;
47
+ }>();
48
+
49
+ @Input() keyExtractor?: (item: ItemT, index: number) => string;
50
+ @Input() getItemLayout?: (
51
+ data: unknown,
52
+ index: number,
53
+ ) => { length: number; offset: number; index: number };
54
+ @Input() horizontal?: boolean;
55
+ @Input() inverted?: boolean;
56
+ @Input() extraData?: unknown;
57
+ @Input() onEndReachedThreshold?: number;
58
+ @Input() onStartReachedThreshold?: number;
59
+ @Input() refreshing?: boolean | null;
60
+ @Input() progressViewOffset?: number;
61
+ @Input() viewabilityConfig?: IViewabilityConfig;
62
+ @Input()
63
+ viewabilityConfigCallbackPairs?: IViewabilityConfigCallbackPair<ItemT>[];
64
+ @Input() initialNumToRender?: number;
65
+ @Input() initialScrollIndex?: number;
66
+ @Input() maxToRenderPerBatch?: number;
67
+ @Input() updateCellsBatchingPeriod?: number;
68
+ @Input() windowSize?: number;
69
+ @Input() stickyHeaderIndices?: number[];
70
+ @Input() maintainVisibleContentPosition?: {
71
+ minIndexForVisible: number;
72
+ autoscrollToTopThreshold?: number;
73
+ };
74
+ @Input() onScroll?: (event: ISymbioteEvent) => void;
75
+ @Input() onScrollBeginDrag?: (event: ISymbioteEvent) => void;
76
+ @Input() onScrollEndDrag?: (event: ISymbioteEvent) => void;
77
+ @Input() onMomentumScrollBegin?: (event: ISymbioteEvent) => void;
78
+ @Input() onMomentumScrollEnd?: (event: ISymbioteEvent) => void;
79
+ @Input() scrollEventThrottle?: number;
80
+ @Input() keyboardShouldPersistTaps?: boolean | 'always' | 'never' | 'handled';
81
+ @Input() keyboardDismissMode?: 'none' | 'on-drag' | 'interactive';
82
+ @Input() removeClippedSubviews?: boolean;
83
+ @Input() nestedScrollEnabled?: boolean;
84
+ @Input() style?: IStyleProp<IViewStyle>;
85
+ @Input() contentContainerStyle?: IStyleProp<IViewStyle>;
86
+ @Input() testID?: string;
87
+ @Input() nativeID?: string;
88
+
89
+ // The cell and slot templates the app authors, captured from projected <ng-template> content
90
+ @ContentChild(VListItemDirective) itemDir?: VListItemDirective<ItemT>;
91
+ @ContentChild(VListHeaderDirective) headerDir?: VListHeaderDirective;
92
+ @ContentChild(VListFooterDirective) footerDir?: VListFooterDirective;
93
+ @ContentChild(VListEmptyDirective) emptyDir?: VListEmptyDirective;
94
+ @ContentChild(VListSeparatorDirective)
95
+ separatorDir?: VListSeparatorDirective<ItemT>;
96
+
97
+ get isHorizontal(): boolean {
98
+ return this.horizontal === true;
99
+ }
100
+
101
+ protected get isInverted(): boolean {
102
+ return this.inverted === true;
103
+ }
104
+
105
+ protected get windowSizeValue(): number {
106
+ return this.windowSize ?? DEFAULT_WINDOW_SIZE;
107
+ }
108
+
109
+ protected get initialNumToRenderValue(): number {
110
+ return this.initialNumToRender ?? DEFAULT_INITIAL_NUM_TO_RENDER;
111
+ }
112
+
113
+ protected get maxToRenderPerBatchValue(): number {
114
+ return this.maxToRenderPerBatch ?? DEFAULT_MAX_TO_RENDER_PER_BATCH;
115
+ }
116
+
117
+ protected get updateCellsBatchingPeriodValue(): number {
118
+ return (
119
+ this.updateCellsBatchingPeriod ?? DEFAULT_UPDATE_CELLS_BATCHING_PERIOD
120
+ );
121
+ }
122
+ }
123
+
124
+ @Directive()
125
+ export abstract class VirtualizedListInputs<
126
+ ItemT,
127
+ > extends ListInputsBase<ItemT> {
128
+ @Input({ required: true }) data!: unknown;
129
+ @Input({ required: true }) getItem!: (data: unknown, index: number) => ItemT;
130
+ @Input({ required: true }) getItemCount!: (data: unknown) => number;
131
+ // Set by a wrapper (`FlatList`, `VirtualizedSectionList`) that always binds `(refresh)` here to
132
+ // re-forward the event, which keeps `refresh.observed` permanently true. The wrapper passes its
133
+ // own public `refresh.observed` instead, direct usage falls back to this component's own
134
+ @Input() refreshRequested?: boolean;
135
+
136
+ // A wrapping list hands its app's cell and separator templates straight in, so a cell is one
137
+ // outlet deep instead of a wrapper outlet around the app's own. Each wins over projection
138
+ @Input() itemTemplate?: TemplateRef<IVListItemContext<ItemT>>;
139
+ @Input() itemSeparatorTemplate?: TemplateRef<IVListSeparatorContext<ItemT>>;
140
+
141
+ get cellTemplate(): TemplateRef<IVListItemContext<ItemT>> | undefined {
142
+ return this.itemTemplate ?? this.itemDir?.templateRef;
143
+ }
144
+
145
+ get separatorTemplate():
146
+ TemplateRef<IVListSeparatorContext<ItemT>> | undefined {
147
+ return this.itemSeparatorTemplate ?? this.separatorDir?.templateRef;
148
+ }
149
+ }
@@ -0,0 +1,81 @@
1
+ // The public prop surface of the Angular VirtualizedList, kept apart from the component class
2
+
3
+ import type {
4
+ IAccessibilityProps,
5
+ IAriaProps,
6
+ IViewabilityConfig,
7
+ IViewabilityConfigCallbackPair,
8
+ IViewableItemsChangedInfo,
9
+ } from '@symbiote-native/components';
10
+ import type {
11
+ IStyleProp,
12
+ ISymbioteEvent,
13
+ IViewStyle,
14
+ } from '@symbiote-native/engine';
15
+
16
+ // The React and Vue surface minus element-returning props, which are `<ng-template>` here
17
+ export interface IVirtualizedListProps<ItemT>
18
+ extends IAccessibilityProps, IAriaProps {
19
+ data: unknown;
20
+ getItem: (data: unknown, index: number) => ItemT;
21
+ getItemCount: (data: unknown) => number;
22
+ keyExtractor?: (item: ItemT, index: number) => string;
23
+ getItemLayout?: (
24
+ data: unknown,
25
+ index: number,
26
+ ) => { length: number; offset: number; index: number };
27
+ horizontal?: boolean;
28
+ inverted?: boolean;
29
+ extraData?: unknown;
30
+ onEndReached?: (info: { distanceFromEnd: number }) => void;
31
+ onEndReachedThreshold?: number;
32
+ onStartReached?: (info: { distanceFromStart: number }) => void;
33
+ onStartReachedThreshold?: number;
34
+ onRefresh?: () => void;
35
+ refreshing?: boolean | null;
36
+ progressViewOffset?: number;
37
+ onViewableItemsChanged?: (info: IViewableItemsChangedInfo<ItemT>) => void;
38
+ viewabilityConfig?: IViewabilityConfig;
39
+ viewabilityConfigCallbackPairs?: IViewabilityConfigCallbackPair<ItemT>[];
40
+ onScrollToIndexFailed?: (info: {
41
+ index: number;
42
+ highestMeasuredFrameIndex: number;
43
+ averageItemLength: number;
44
+ }) => void;
45
+ initialNumToRender?: number;
46
+ initialScrollIndex?: number;
47
+ maxToRenderPerBatch?: number;
48
+ updateCellsBatchingPeriod?: number;
49
+ windowSize?: number;
50
+ stickyHeaderIndices?: number[];
51
+ maintainVisibleContentPosition?: {
52
+ minIndexForVisible: number;
53
+ autoscrollToTopThreshold?: number;
54
+ };
55
+ onScroll?: (event: ISymbioteEvent) => void;
56
+ onScrollBeginDrag?: (event: ISymbioteEvent) => void;
57
+ onScrollEndDrag?: (event: ISymbioteEvent) => void;
58
+ onMomentumScrollBegin?: (event: ISymbioteEvent) => void;
59
+ onMomentumScrollEnd?: (event: ISymbioteEvent) => void;
60
+ scrollEventThrottle?: number;
61
+ keyboardShouldPersistTaps?: boolean | 'always' | 'never' | 'handled';
62
+ keyboardDismissMode?: 'none' | 'on-drag' | 'interactive';
63
+ removeClippedSubviews?: boolean;
64
+ nestedScrollEnabled?: boolean;
65
+ style?: IStyleProp<IViewStyle>;
66
+ contentContainerStyle?: IStyleProp<IViewStyle>;
67
+ }
68
+
69
+ // The plain inputs: the full surface minus the events exposed as real outputs
70
+ export type IVirtualizedListInputs<ItemT> = Omit<
71
+ IVirtualizedListProps<ItemT>,
72
+ | 'onEndReached'
73
+ | 'onStartReached'
74
+ | 'onRefresh'
75
+ | 'onViewableItemsChanged'
76
+ | 'onScrollToIndexFailed'
77
+ | 'onAccessibilityAction'
78
+ | 'onAccessibilityTap'
79
+ | 'onMagicTap'
80
+ | 'onAccessibilityEscape'
81
+ >;