@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.
- package/babel-register-composed.cjs +1 -0
- package/build/angular/accessibility-events.d.ts +12 -0
- package/build/angular/accessibility-events.js +28 -0
- package/build/angular/accessibility-events.js.map +1 -0
- package/build/angular/accessibility-inputs.d.ts +40 -0
- package/build/angular/accessibility-inputs.js +130 -0
- package/build/angular/accessibility-inputs.js.map +1 -0
- package/build/angular/bootstrap.d.ts +3 -1
- package/build/angular/bootstrap.js +4 -1
- package/build/angular/bootstrap.js.map +1 -1
- package/build/angular/components/checkbox-props.d.ts +2 -0
- package/build/angular/components/checkbox-props.js +2 -0
- package/build/angular/components/checkbox-props.js.map +1 -0
- package/build/angular/components/flat-list/flat-list-template.d.ts +1 -0
- package/build/angular/components/flat-list/flat-list-template.js +158 -0
- package/build/angular/components/flat-list/flat-list-template.js.map +1 -0
- package/build/angular/components/flat-list/index.d.ts +6 -61
- package/build/angular/components/flat-list/index.js +26 -463
- package/build/angular/components/flat-list/index.js.map +1 -1
- package/build/angular/components/keyboard-avoiding-view/index.d.ts +4 -40
- package/build/angular/components/keyboard-avoiding-view/index.js +7 -131
- package/build/angular/components/keyboard-avoiding-view/index.js.map +1 -1
- package/build/angular/components/modal/index.d.ts +5 -41
- package/build/angular/components/modal/index.js +8 -133
- package/build/angular/components/modal/index.js.map +1 -1
- package/build/angular/components/section-list/index.d.ts +4 -51
- package/build/angular/components/section-list/index.js +18 -335
- package/build/angular/components/section-list/index.js.map +1 -1
- package/build/angular/components/virtualized-list/cell-registry.d.ts +35 -0
- package/build/angular/components/virtualized-list/cell-registry.js +133 -0
- package/build/angular/components/virtualized-list/cell-registry.js.map +1 -0
- package/build/angular/components/virtualized-list/index.d.ts +18 -253
- package/build/angular/components/virtualized-list/index.js +112 -1178
- package/build/angular/components/virtualized-list/index.js.map +1 -1
- package/build/angular/components/virtualized-list/list-a11y-forward.d.ts +1 -0
- package/build/angular/components/virtualized-list/list-a11y-forward.js +42 -0
- package/build/angular/components/virtualized-list/list-a11y-forward.js.map +1 -0
- package/build/angular/components/virtualized-list/list-bags.d.ts +32 -0
- package/build/angular/components/virtualized-list/list-bags.js +120 -0
- package/build/angular/components/virtualized-list/list-bags.js.map +1 -0
- package/build/angular/components/virtualized-list/list-effects.d.ts +31 -0
- package/build/angular/components/virtualized-list/list-effects.js +83 -0
- package/build/angular/components/virtualized-list/list-effects.js.map +1 -0
- package/build/angular/components/virtualized-list/list-events.d.ts +17 -0
- package/build/angular/components/virtualized-list/list-events.js +34 -0
- package/build/angular/components/virtualized-list/list-events.js.map +1 -0
- package/build/angular/components/virtualized-list/list-handle.d.ts +35 -0
- package/build/angular/components/virtualized-list/list-handle.js +61 -0
- package/build/angular/components/virtualized-list/list-handle.js.map +1 -0
- package/build/angular/components/virtualized-list/list-inputs.d.ts +78 -0
- package/build/angular/components/virtualized-list/list-inputs.js +150 -0
- package/build/angular/components/virtualized-list/list-inputs.js.map +1 -0
- package/build/angular/components/virtualized-list/list-props.d.ts +58 -0
- package/build/angular/components/virtualized-list/list-props.js +3 -0
- package/build/angular/components/virtualized-list/list-props.js.map +1 -0
- package/build/angular/components/virtualized-list/list-styles.d.ts +23 -0
- package/build/angular/components/virtualized-list/list-styles.js +26 -0
- package/build/angular/components/virtualized-list/list-styles.js.map +1 -0
- package/build/angular/components/virtualized-list/list-template.d.ts +1 -0
- package/build/angular/components/virtualized-list/list-template.js +176 -0
- package/build/angular/components/virtualized-list/list-template.js.map +1 -0
- package/build/angular/components/virtualized-section-list/index.d.ts +13 -70
- package/build/angular/components/virtualized-section-list/index.js +22 -455
- package/build/angular/components/virtualized-section-list/index.js.map +1 -1
- package/build/angular/components/virtualized-section-list/section-list-template.d.ts +1 -0
- package/build/angular/components/virtualized-section-list/section-list-template.js +107 -0
- package/build/angular/components/virtualized-section-list/section-list-template.js.map +1 -0
- package/build/angular/components.d.ts +1 -0
- package/build/angular/components.js.map +1 -1
- package/build/angular/descriptor-to-angular/descriptor-host.d.ts +8 -0
- package/build/angular/descriptor-to-angular/descriptor-host.js +38 -0
- package/build/angular/descriptor-to-angular/descriptor-host.js.map +1 -0
- package/build/angular/descriptor-to-angular/index.d.ts +1 -0
- package/build/angular/descriptor-to-angular/index.js +4 -0
- package/build/angular/descriptor-to-angular/index.js.map +1 -1
- package/build/angular/element-base.d.ts +153 -0
- package/build/angular/element-base.js +272 -0
- package/build/angular/element-base.js.map +1 -0
- package/build/angular/elements-controls.d.ts +165 -0
- package/build/angular/elements-controls.js +382 -0
- package/build/angular/elements-controls.js.map +1 -0
- package/build/angular/elements-layout.d.ts +122 -0
- package/build/angular/elements-layout.js +242 -0
- package/build/angular/elements-layout.js.map +1 -0
- package/build/angular/elements-press.d.ts +96 -0
- package/build/angular/elements-press.js +227 -0
- package/build/angular/elements-press.js.map +1 -0
- package/build/angular/elements.d.ts +6 -516
- package/build/angular/elements.js +8 -1091
- package/build/angular/elements.js.map +1 -1
- package/build/angular/finalization-registry.d.ts +4 -0
- package/build/angular/finalization-registry.js +17 -0
- package/build/angular/finalization-registry.js.map +1 -0
- package/build/angular/host-instance/index.d.ts +2 -0
- package/build/angular/host-instance/index.js +4 -4
- package/build/angular/host-instance/index.js.map +1 -1
- package/build/angular/index.d.ts +9 -4
- package/build/angular/index.js +8 -3
- package/build/angular/index.js.map +1 -1
- package/build/angular/native-view-base.d.ts +27 -0
- package/build/angular/native-view-base.js +69 -0
- package/build/angular/native-view-base.js.map +1 -0
- package/build/angular/render/index.d.ts +2 -1
- package/build/angular/render/index.js +46 -73
- package/build/angular/render/index.js.map +1 -1
- package/build/angular/services/connect-watched-signal.d.ts +2 -0
- package/build/angular/services/connect-watched-signal.js +9 -0
- package/build/angular/services/connect-watched-signal.js.map +1 -0
- package/build/angular/services/create-event-value-hook.d.ts +3 -0
- package/build/angular/services/create-event-value-hook.js +17 -0
- package/build/angular/services/create-event-value-hook.js.map +1 -0
- package/build/angular/services/create-resource-hook.d.ts +7 -0
- package/build/angular/services/create-resource-hook.js +20 -0
- package/build/angular/services/create-resource-hook.js.map +1 -0
- package/build/angular/services/index.d.ts +5 -0
- package/build/angular/services/index.js +5 -0
- package/build/angular/services/index.js.map +1 -1
- package/build/angular/services/inject-event.d.ts +6 -0
- package/build/angular/services/inject-event.js +24 -0
- package/build/angular/services/inject-event.js.map +1 -0
- package/build/angular/services/permissions-service-base.d.ts +14 -0
- package/build/angular/services/permissions-service-base.js +38 -0
- package/build/angular/services/permissions-service-base.js.map +1 -0
- package/package.json +6 -6
- package/src/accessibility-events.ts +13 -0
- package/src/accessibility-inputs.ts +89 -0
- package/src/bootstrap.ts +6 -2
- package/src/components/checkbox-props.ts +12 -0
- package/src/components/flat-list/flat-list-template.ts +159 -0
- package/src/components/flat-list/index.ts +30 -307
- package/src/components/keyboard-avoiding-view/index.ts +4 -78
- package/src/components/modal/index.ts +7 -80
- package/src/components/section-list/index.ts +21 -172
- package/src/components/virtualized-list/cell-registry.ts +199 -0
- package/src/components/virtualized-list/index.ts +145 -1181
- package/src/components/virtualized-list/list-a11y-forward.ts +42 -0
- package/src/components/virtualized-list/list-bags.ts +142 -0
- package/src/components/virtualized-list/list-effects.ts +116 -0
- package/src/components/virtualized-list/list-events.ts +31 -0
- package/src/components/virtualized-list/list-handle.ts +92 -0
- package/src/components/virtualized-list/list-inputs.ts +149 -0
- package/src/components/virtualized-list/list-props.ts +81 -0
- package/src/components/virtualized-list/list-styles.ts +62 -0
- package/src/components/virtualized-list/list-template.ts +175 -0
- package/src/components/virtualized-section-list/index.ts +40 -351
- package/src/components/virtualized-section-list/section-list-template.ts +108 -0
- package/src/components.ts +1 -0
- package/src/descriptor-to-angular/descriptor-host.ts +27 -0
- package/src/descriptor-to-angular/index.ts +5 -0
- package/src/element-base.ts +272 -0
- package/src/elements-controls.ts +282 -0
- package/src/elements-layout.ts +141 -0
- package/src/elements-press.ts +145 -0
- package/src/elements.ts +73 -788
- package/src/finalization-registry.ts +18 -0
- package/src/host-instance/index.ts +4 -4
- package/src/index.ts +18 -2
- package/src/native-view-base.ts +86 -0
- package/src/render/index.ts +76 -77
- package/src/services/connect-watched-signal.ts +15 -0
- package/src/services/create-event-value-hook.ts +24 -0
- package/src/services/create-resource-hook.ts +32 -0
- package/src/services/index.ts +5 -0
- package/src/services/inject-event.ts +52 -0
- package/src/services/permissions-service-base.ts +46 -0
|
@@ -1,91 +1,40 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
// so every adapter keeps the same feature surface. Here Angular supplies only the lifecycle: plain fields
|
|
5
|
-
// for scroll offset / viewport / measurement bumps, a once-per-CD metrics + view recompute in
|
|
6
|
-
// ngDoCheck (the Angular twin of React render / Vue `computed` — it owns the controlled
|
|
7
|
-
// committedWindow throttle and assembles the windowed cells before the template bindings are read),
|
|
8
|
-
// and the after-commit work (batch fill, onEndReached/onStartReached, viewability, initialScroll, MVCP)
|
|
9
|
-
// in ngAfterViewChecked. OnPush + ChangeDetectorRef.markForCheck() drives re-render off the native
|
|
10
|
-
// scroll/layout/measure callbacks (they fire outside Angular's own event bindings, so they must
|
|
11
|
-
// mark the view dirty). This is the Angular twin of the React useReducer/useEffect and Vue
|
|
12
|
-
// ref/computed/watch over the same shared functions. It writes the `scroll-view`/
|
|
13
|
-
// `horizontal-scroll-view` TAG directly, exactly as the React/Vue lists drive their scroll tag.
|
|
14
|
-
//
|
|
15
|
-
// Per-item rendering is TEMPLATES, not a callback: React/Vue `renderItem: (info) => element` does
|
|
16
|
-
// not translate to Angular. The app supplies the cell via `<ng-template vListItem>` (and the
|
|
17
|
-
// header/footer/empty/separator slots via vListHeader/vListFooter/vListEmpty/vListSeparator); the
|
|
18
|
-
// list captures them with @ContentChild and stamps the WINDOWED slice through VListOutletDirective
|
|
19
|
-
// (a core-only NgTemplateOutlet twin — @angular/common is not a dependency). See ./directives.ts.
|
|
20
|
-
// Every React/Vue behavior is present (windowing, onEndReached, viewability, headers/footers/empty/
|
|
21
|
-
// separators, refresh, imperative scroll, horizontal, inverted, MVCP); only the cell AUTHORING
|
|
22
|
-
// shape differs, since a callback prop returning an element has no Angular equivalent.
|
|
23
|
-
//
|
|
24
|
-
// Lists have no Descriptor render fn — the cell content is the framework's own children.
|
|
25
|
-
// Cells/spacers are plain view host elements.
|
|
1
|
+
// Angular lifecycle half of VirtualizedList, the windowing engine is shared with React and Vue
|
|
2
|
+
// The bound surface is in ./list-inputs.ts, cells in ./cell-registry.ts, effects in
|
|
3
|
+
// ./list-effects.ts, styles in ./list-styles.ts and the authoring templates in ./directives.ts
|
|
26
4
|
|
|
27
5
|
import {
|
|
28
6
|
CUSTOM_ELEMENTS_SCHEMA,
|
|
29
7
|
ChangeDetectionStrategy,
|
|
30
8
|
ChangeDetectorRef,
|
|
31
9
|
Component,
|
|
32
|
-
ContentChild,
|
|
33
10
|
ElementRef,
|
|
34
|
-
EventEmitter,
|
|
35
|
-
Input,
|
|
36
|
-
Output,
|
|
37
|
-
TemplateRef,
|
|
38
11
|
ViewChild,
|
|
39
|
-
computed,
|
|
40
12
|
inject,
|
|
41
|
-
signal,
|
|
42
13
|
type AfterViewChecked,
|
|
43
14
|
type DoCheck,
|
|
44
|
-
type OnChanges,
|
|
45
15
|
type OnDestroy,
|
|
46
16
|
} from '@angular/core';
|
|
47
17
|
import {
|
|
48
|
-
DEFAULT_INITIAL_NUM_TO_RENDER,
|
|
49
|
-
DEFAULT_MAX_TO_RENDER_PER_BATCH,
|
|
50
|
-
DEFAULT_UPDATE_CELLS_BATCHING_PERIOD,
|
|
51
|
-
DEFAULT_WINDOW_SIZE,
|
|
52
18
|
EMPTY_OFFSET,
|
|
53
19
|
FIRST_INDEX,
|
|
54
|
-
|
|
55
|
-
INVERTED_Y_STYLE,
|
|
20
|
+
LIST_ACTION_KIND,
|
|
56
21
|
buildListPlan,
|
|
22
|
+
buildScrollViewHandle,
|
|
57
23
|
buildViewabilityPairs,
|
|
58
24
|
computeWindow,
|
|
59
25
|
createInitialListState,
|
|
60
|
-
isSeparatorGapInRange,
|
|
61
26
|
listEffectSignature,
|
|
62
|
-
readLayoutLength,
|
|
63
|
-
readLayoutOffset,
|
|
64
|
-
readScrollOffset,
|
|
65
27
|
reduceList,
|
|
66
|
-
resolveAccessibilityProps,
|
|
67
28
|
resolveItemKey,
|
|
68
|
-
type IAccessibilityProps,
|
|
69
|
-
type IAccessibilityStateValue,
|
|
70
|
-
type IAriaProps,
|
|
71
29
|
type IListAction,
|
|
72
|
-
type IListEffect,
|
|
73
30
|
type IListReducerInputs,
|
|
74
31
|
type IListState,
|
|
75
|
-
type ISeparatorProps,
|
|
76
|
-
type ISeparators,
|
|
77
32
|
type IScrollViewHandle,
|
|
78
|
-
type IViewabilityConfig,
|
|
79
|
-
type IViewabilityConfigCallbackPair,
|
|
80
33
|
type IViewableItemsChangedInfo,
|
|
81
|
-
type IVirtualizedListHandle,
|
|
82
|
-
buildScrollViewHandle,
|
|
83
34
|
} from '@symbiote-native/components';
|
|
84
35
|
import {
|
|
85
36
|
dlog,
|
|
86
|
-
flattenStyle,
|
|
87
37
|
isSymbioteNode,
|
|
88
|
-
type IStyleProp,
|
|
89
38
|
type ISymbioteEvent,
|
|
90
39
|
type ISymbioteNode,
|
|
91
40
|
type IViewStyle,
|
|
@@ -97,19 +46,15 @@ import {
|
|
|
97
46
|
SymbioteStyleInputDirective,
|
|
98
47
|
ViewHost,
|
|
99
48
|
} from '../../primitives';
|
|
100
|
-
import {
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
} from './directives';
|
|
110
|
-
|
|
111
|
-
// Re-export the shared list types + the authoring directives so flat-list / section-list keep
|
|
112
|
-
// importing them from '../virtualized-list', exactly as the React/Vue adapters re-export them.
|
|
49
|
+
import { CellRegistry, type IWindowCell } from './cell-registry';
|
|
50
|
+
import { VListOutletDirective } from './directives';
|
|
51
|
+
import { ListEffectRunner } from './list-effects';
|
|
52
|
+
import { VirtualizedListBagsBase } from './list-bags';
|
|
53
|
+
import type { IVirtualizedListInputs } from './list-props';
|
|
54
|
+
import { resolveListStyles } from './list-styles';
|
|
55
|
+
import { VIRTUALIZED_LIST_TEMPLATE } from './list-template';
|
|
56
|
+
|
|
57
|
+
// Re-exported so flat-list and section-list keep importing them from '../virtualized-list'
|
|
113
58
|
export type {
|
|
114
59
|
ICellLayout,
|
|
115
60
|
ISeparators,
|
|
@@ -127,109 +72,15 @@ export {
|
|
|
127
72
|
VListItemDirective,
|
|
128
73
|
VListSeparatorDirective,
|
|
129
74
|
} from './directives';
|
|
130
|
-
import {
|
|
131
|
-
gateWanted,
|
|
132
|
-
injectGateDemandAbove,
|
|
133
|
-
type IGatedAccessibilityEvent,
|
|
134
|
-
} from '../../gate-demand';
|
|
135
75
|
export type { IVListItemContext, IVListSeparatorContext } from './directives';
|
|
136
76
|
|
|
137
|
-
|
|
138
|
-
// element-returning props (renderItem, ListHeader/Footer/Empty Component, ItemSeparatorComponent):
|
|
139
|
-
// those are the per-adapter children/render fields and become `<ng-template>` directives in Angular.
|
|
140
|
-
// Everything agnostic is the SAME surface.
|
|
141
|
-
export interface IVirtualizedListProps<ItemT>
|
|
142
|
-
extends IAccessibilityProps, IAriaProps {
|
|
143
|
-
data: unknown;
|
|
144
|
-
getItem: (data: unknown, index: number) => ItemT;
|
|
145
|
-
getItemCount: (data: unknown) => number;
|
|
146
|
-
keyExtractor?: (item: ItemT, index: number) => string;
|
|
147
|
-
getItemLayout?: (
|
|
148
|
-
data: unknown,
|
|
149
|
-
index: number,
|
|
150
|
-
) => { length: number; offset: number; index: number };
|
|
151
|
-
horizontal?: boolean;
|
|
152
|
-
inverted?: boolean;
|
|
153
|
-
extraData?: unknown;
|
|
154
|
-
onEndReached?: (info: { distanceFromEnd: number }) => void;
|
|
155
|
-
onEndReachedThreshold?: number;
|
|
156
|
-
onStartReached?: (info: { distanceFromStart: number }) => void;
|
|
157
|
-
onStartReachedThreshold?: number;
|
|
158
|
-
onRefresh?: () => void;
|
|
159
|
-
refreshing?: boolean | null;
|
|
160
|
-
progressViewOffset?: number;
|
|
161
|
-
onViewableItemsChanged?: (info: IViewableItemsChangedInfo<ItemT>) => void;
|
|
162
|
-
viewabilityConfig?: IViewabilityConfig;
|
|
163
|
-
viewabilityConfigCallbackPairs?: IViewabilityConfigCallbackPair<ItemT>[];
|
|
164
|
-
onScrollToIndexFailed?: (info: {
|
|
165
|
-
index: number;
|
|
166
|
-
highestMeasuredFrameIndex: number;
|
|
167
|
-
averageItemLength: number;
|
|
168
|
-
}) => void;
|
|
169
|
-
initialNumToRender?: number;
|
|
170
|
-
initialScrollIndex?: number;
|
|
171
|
-
maxToRenderPerBatch?: number;
|
|
172
|
-
updateCellsBatchingPeriod?: number;
|
|
173
|
-
windowSize?: number;
|
|
174
|
-
stickyHeaderIndices?: number[];
|
|
175
|
-
maintainVisibleContentPosition?: {
|
|
176
|
-
minIndexForVisible: number;
|
|
177
|
-
autoscrollToTopThreshold?: number;
|
|
178
|
-
};
|
|
179
|
-
onScroll?: (event: ISymbioteEvent) => void;
|
|
180
|
-
onScrollBeginDrag?: (event: ISymbioteEvent) => void;
|
|
181
|
-
onScrollEndDrag?: (event: ISymbioteEvent) => void;
|
|
182
|
-
onMomentumScrollBegin?: (event: ISymbioteEvent) => void;
|
|
183
|
-
onMomentumScrollEnd?: (event: ISymbioteEvent) => void;
|
|
184
|
-
scrollEventThrottle?: number;
|
|
185
|
-
keyboardShouldPersistTaps?: boolean | 'always' | 'never' | 'handled';
|
|
186
|
-
keyboardDismissMode?: 'none' | 'on-drag' | 'interactive';
|
|
187
|
-
removeClippedSubviews?: boolean;
|
|
188
|
-
nestedScrollEnabled?: boolean;
|
|
189
|
-
style?: IStyleProp<IViewStyle>;
|
|
190
|
-
contentContainerStyle?: IStyleProp<IViewStyle>;
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
// What the VirtualizedList component itself takes as plain @Input()s: the full surface minus the
|
|
194
|
-
// events it exposes as real @Output() EventEmitters instead (see the class below), mirroring how
|
|
195
|
-
// pressable/index.ts derives IAngularPressableInputs from IAngularPressableProps.
|
|
196
|
-
export type IVirtualizedListInputs<ItemT> = Omit<
|
|
197
|
-
IVirtualizedListProps<ItemT>,
|
|
198
|
-
| 'onEndReached'
|
|
199
|
-
| 'onStartReached'
|
|
200
|
-
| 'onRefresh'
|
|
201
|
-
| 'onViewableItemsChanged'
|
|
202
|
-
| 'onScrollToIndexFailed'
|
|
203
|
-
| 'onAccessibilityAction'
|
|
204
|
-
| 'onAccessibilityTap'
|
|
205
|
-
| 'onMagicTap'
|
|
206
|
-
| 'onAccessibilityEscape'
|
|
207
|
-
>;
|
|
77
|
+
type IListPlan = ReturnType<typeof buildListPlan>;
|
|
208
78
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
key: string;
|
|
214
|
-
index: number;
|
|
215
|
-
context: IVListItemContext<ItemT>;
|
|
216
|
-
measure: (event: ISymbioteEvent) => void;
|
|
217
|
-
separatorContext?: IVListSeparatorContext<ItemT>;
|
|
218
|
-
/**
|
|
219
|
-
* Picks the cell's TAG in the template — `sticky-header` pins, `view` does not.
|
|
220
|
-
*
|
|
221
|
-
* It is stable per cell key (an index is a section header or it is not), so the `@if` never
|
|
222
|
-
* swaps an element type under a live cell and nothing remounts as the window slides.
|
|
223
|
-
*/
|
|
224
|
-
isSticky: boolean;
|
|
225
|
-
}
|
|
79
|
+
export type {
|
|
80
|
+
IVirtualizedListInputs,
|
|
81
|
+
IVirtualizedListProps,
|
|
82
|
+
} from './list-props';
|
|
226
83
|
|
|
227
|
-
// The scroll body is identical on both axes; only the outer tag differs (the axis comes from the
|
|
228
|
-
// TAG, never a prop — `<horizontal-scroll-view>` vs `<scroll-view>`, matching every other adapter,
|
|
229
|
-
// `.claude/rules/host-primitive-tier.md`). No shared-outlet primitive exists to de-duplicate this
|
|
230
|
-
// (this package's own NgTemplateOutlet twin is keyed on a per-cell context, not a static block, and
|
|
231
|
-
// ngc's partial evaluator needs the decorator's `template` to be one literal), so the body is
|
|
232
|
-
// duplicated across the two branches — the same shape Svelte's own `{#if horizontal}` takes.
|
|
233
84
|
@Component({
|
|
234
85
|
selector: 'VirtualizedList',
|
|
235
86
|
standalone: true,
|
|
@@ -239,641 +90,71 @@ interface IWindowCell<ItemT> {
|
|
|
239
90
|
schemas: [CUSTOM_ELEMENTS_SCHEMA],
|
|
240
91
|
imports: [SymbioteHostPropsDirective, VListOutletDirective, ViewHost],
|
|
241
92
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
242
|
-
template:
|
|
243
|
-
@if (isHorizontal) {
|
|
244
|
-
<horizontal-scroll-view #scrollHost [symbioteHostProps]="scrollViewBag()">
|
|
245
|
-
@if (shouldRenderRefreshControl) {
|
|
246
|
-
<refresh-control
|
|
247
|
-
[symbioteHostProps]="refreshControlBag()"
|
|
248
|
-
></refresh-control>
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
@if (headerDir !== undefined) {
|
|
252
|
-
<view>
|
|
253
|
-
<ng-container [vListOutlet]="headerDir.templateRef"></ng-container>
|
|
254
|
-
</view>
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
@if (itemCount === 0) {
|
|
258
|
-
@if (emptyDir !== undefined) {
|
|
259
|
-
<view>
|
|
260
|
-
<ng-container [vListOutlet]="emptyDir.templateRef"></ng-container>
|
|
261
|
-
</view>
|
|
262
|
-
}
|
|
263
|
-
} @else {
|
|
264
|
-
@if (leadingSpacerStyle !== null) {
|
|
265
|
-
<view [style]="leadingSpacerStyle"></view>
|
|
266
|
-
}
|
|
267
|
-
@if (forcedStickyCell !== null) {
|
|
268
|
-
<sticky-header
|
|
269
|
-
(layout)="handleCellLayout(forcedStickyCell.measure, $event)"
|
|
270
|
-
[style]="cellStyle"
|
|
271
|
-
>
|
|
272
|
-
<ng-container
|
|
273
|
-
[vListOutlet]="cellTemplate"
|
|
274
|
-
[vListOutletContext]="forcedStickyCell.context"
|
|
275
|
-
></ng-container>
|
|
276
|
-
</sticky-header>
|
|
277
|
-
}
|
|
278
|
-
@if (gapSpacerStyle !== null) {
|
|
279
|
-
<view [style]="gapSpacerStyle"></view>
|
|
280
|
-
}
|
|
281
|
-
@for (cell of windowCells; track cell.key) {
|
|
282
|
-
@if (cell.isSticky) {
|
|
283
|
-
<sticky-header
|
|
284
|
-
(layout)="handleCellLayout(cell.measure, $event)"
|
|
285
|
-
[style]="cellStyle"
|
|
286
|
-
>
|
|
287
|
-
<ng-container
|
|
288
|
-
[vListOutlet]="cellTemplate"
|
|
289
|
-
[vListOutletContext]="cell.context"
|
|
290
|
-
></ng-container>
|
|
291
|
-
@if (cell.separatorContext !== undefined) {
|
|
292
|
-
<ng-container
|
|
293
|
-
[vListOutlet]="separatorTemplate"
|
|
294
|
-
[vListOutletContext]="cell.separatorContext"
|
|
295
|
-
></ng-container>
|
|
296
|
-
}
|
|
297
|
-
</sticky-header>
|
|
298
|
-
} @else {
|
|
299
|
-
<view
|
|
300
|
-
(layout)="handleCellLayout(cell.measure, $event)"
|
|
301
|
-
[style]="cellStyle"
|
|
302
|
-
>
|
|
303
|
-
<ng-container
|
|
304
|
-
[vListOutlet]="cellTemplate"
|
|
305
|
-
[vListOutletContext]="cell.context"
|
|
306
|
-
></ng-container>
|
|
307
|
-
@if (cell.separatorContext !== undefined) {
|
|
308
|
-
<ng-container
|
|
309
|
-
[vListOutlet]="separatorTemplate"
|
|
310
|
-
[vListOutletContext]="cell.separatorContext"
|
|
311
|
-
></ng-container>
|
|
312
|
-
}
|
|
313
|
-
</view>
|
|
314
|
-
}
|
|
315
|
-
}
|
|
316
|
-
@if (trailingSpacerStyle !== null) {
|
|
317
|
-
<view [style]="trailingSpacerStyle"></view>
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
@if (footerDir !== undefined) {
|
|
322
|
-
<view>
|
|
323
|
-
<ng-container [vListOutlet]="footerDir.templateRef"></ng-container>
|
|
324
|
-
</view>
|
|
325
|
-
}
|
|
326
|
-
</horizontal-scroll-view>
|
|
327
|
-
} @else {
|
|
328
|
-
<scroll-view #scrollHost [symbioteHostProps]="scrollViewBag()">
|
|
329
|
-
@if (shouldRenderRefreshControl) {
|
|
330
|
-
<refresh-control
|
|
331
|
-
[symbioteHostProps]="refreshControlBag()"
|
|
332
|
-
></refresh-control>
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
@if (headerDir !== undefined) {
|
|
336
|
-
<view>
|
|
337
|
-
<ng-container [vListOutlet]="headerDir.templateRef"></ng-container>
|
|
338
|
-
</view>
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
@if (itemCount === 0) {
|
|
342
|
-
@if (emptyDir !== undefined) {
|
|
343
|
-
<view>
|
|
344
|
-
<ng-container [vListOutlet]="emptyDir.templateRef"></ng-container>
|
|
345
|
-
</view>
|
|
346
|
-
}
|
|
347
|
-
} @else {
|
|
348
|
-
@if (leadingSpacerStyle !== null) {
|
|
349
|
-
<view [style]="leadingSpacerStyle"></view>
|
|
350
|
-
}
|
|
351
|
-
@if (forcedStickyCell !== null) {
|
|
352
|
-
<sticky-header
|
|
353
|
-
(layout)="handleCellLayout(forcedStickyCell.measure, $event)"
|
|
354
|
-
[style]="cellStyle"
|
|
355
|
-
>
|
|
356
|
-
<ng-container
|
|
357
|
-
[vListOutlet]="cellTemplate"
|
|
358
|
-
[vListOutletContext]="forcedStickyCell.context"
|
|
359
|
-
></ng-container>
|
|
360
|
-
</sticky-header>
|
|
361
|
-
}
|
|
362
|
-
@if (gapSpacerStyle !== null) {
|
|
363
|
-
<view [style]="gapSpacerStyle"></view>
|
|
364
|
-
}
|
|
365
|
-
<!-- Separator sits INSIDE the measuring view, matching RN's cell renderer
|
|
366
|
-
(VirtualizedListCellRenderer.js:218-221) — as a sibling it'd add an extra flex gap.
|
|
367
|
-
See list-geometry-feedback-loop.md. -->
|
|
368
|
-
@for (cell of windowCells; track cell.key) {
|
|
369
|
-
@if (cell.isSticky) {
|
|
370
|
-
<sticky-header
|
|
371
|
-
(layout)="handleCellLayout(cell.measure, $event)"
|
|
372
|
-
[style]="cellStyle"
|
|
373
|
-
>
|
|
374
|
-
<ng-container
|
|
375
|
-
[vListOutlet]="cellTemplate"
|
|
376
|
-
[vListOutletContext]="cell.context"
|
|
377
|
-
></ng-container>
|
|
378
|
-
@if (cell.separatorContext !== undefined) {
|
|
379
|
-
<ng-container
|
|
380
|
-
[vListOutlet]="separatorTemplate"
|
|
381
|
-
[vListOutletContext]="cell.separatorContext"
|
|
382
|
-
></ng-container>
|
|
383
|
-
}
|
|
384
|
-
</sticky-header>
|
|
385
|
-
} @else {
|
|
386
|
-
<view
|
|
387
|
-
(layout)="handleCellLayout(cell.measure, $event)"
|
|
388
|
-
[style]="cellStyle"
|
|
389
|
-
>
|
|
390
|
-
<ng-container
|
|
391
|
-
[vListOutlet]="cellTemplate"
|
|
392
|
-
[vListOutletContext]="cell.context"
|
|
393
|
-
></ng-container>
|
|
394
|
-
@if (cell.separatorContext !== undefined) {
|
|
395
|
-
<ng-container
|
|
396
|
-
[vListOutlet]="separatorTemplate"
|
|
397
|
-
[vListOutletContext]="cell.separatorContext"
|
|
398
|
-
></ng-container>
|
|
399
|
-
}
|
|
400
|
-
</view>
|
|
401
|
-
}
|
|
402
|
-
}
|
|
403
|
-
@if (trailingSpacerStyle !== null) {
|
|
404
|
-
<view [style]="trailingSpacerStyle"></view>
|
|
405
|
-
}
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
@if (footerDir !== undefined) {
|
|
409
|
-
<view>
|
|
410
|
-
<ng-container [vListOutlet]="footerDir.templateRef"></ng-container>
|
|
411
|
-
</view>
|
|
412
|
-
}
|
|
413
|
-
</scroll-view>
|
|
414
|
-
}
|
|
415
|
-
`,
|
|
93
|
+
template: VIRTUALIZED_LIST_TEMPLATE,
|
|
416
94
|
})
|
|
417
95
|
export class VirtualizedList<ItemT = unknown>
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
IVirtualizedListHandle,
|
|
421
|
-
DoCheck,
|
|
422
|
-
AfterViewChecked,
|
|
423
|
-
OnChanges,
|
|
424
|
-
OnDestroy
|
|
96
|
+
extends VirtualizedListBagsBase<ItemT>
|
|
97
|
+
implements IVirtualizedListInputs<ItemT>, DoCheck, AfterViewChecked, OnDestroy
|
|
425
98
|
{
|
|
426
|
-
//
|
|
427
|
-
// `[onEndReached]="…"`. See handleRefresh/accessibility*Tick below for how the still-callback-
|
|
428
|
-
// shaped scroll-view/refresh-control bag keys are fed from these.
|
|
429
|
-
@Output() readonly endReached = new EventEmitter<{
|
|
430
|
-
distanceFromEnd: number;
|
|
431
|
-
}>();
|
|
432
|
-
@Output() readonly startReached = new EventEmitter<{
|
|
433
|
-
distanceFromStart: number;
|
|
434
|
-
}>();
|
|
435
|
-
@Output() readonly refresh = new EventEmitter<void>();
|
|
436
|
-
@Output() readonly viewableItemsChanged = new EventEmitter<
|
|
437
|
-
IViewableItemsChangedInfo<ItemT>
|
|
438
|
-
>();
|
|
439
|
-
@Output() readonly scrollToIndexFailed = new EventEmitter<{
|
|
440
|
-
index: number;
|
|
441
|
-
highestMeasuredFrameIndex: number;
|
|
442
|
-
averageItemLength: number;
|
|
443
|
-
}>();
|
|
444
|
-
@Output() readonly accessibilityAction = new EventEmitter<ISymbioteEvent>();
|
|
445
|
-
@Output() readonly accessibilityTap = new EventEmitter<ISymbioteEvent>();
|
|
446
|
-
@Output() readonly magicTap = new EventEmitter<ISymbioteEvent>();
|
|
447
|
-
@Output() readonly accessibilityEscape = new EventEmitter<ISymbioteEvent>();
|
|
448
|
-
|
|
449
|
-
// This wrapper binds the four gated accessibility events on the component it renders, which
|
|
450
|
-
// Angular forces to be unconditional and which would light that component's gates on every
|
|
451
|
-
// instance. It answers for them instead — see `gate-demand.ts`.
|
|
452
|
-
private readonly gateDemandAbove = injectGateDemandAbove();
|
|
453
|
-
|
|
454
|
-
wantsGate(name: IGatedAccessibilityEvent): boolean {
|
|
455
|
-
return gateWanted(this.gateDemandAbove, name, this[name]);
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
@Input({ required: true }) data!: unknown;
|
|
459
|
-
@Input({ required: true }) getItem!: (data: unknown, index: number) => ItemT;
|
|
460
|
-
@Input({ required: true }) getItemCount!: (data: unknown) => number;
|
|
461
|
-
@Input() keyExtractor?: (item: ItemT, index: number) => string;
|
|
462
|
-
@Input() getItemLayout?: (
|
|
463
|
-
data: unknown,
|
|
464
|
-
index: number,
|
|
465
|
-
) => { length: number; offset: number; index: number };
|
|
466
|
-
@Input() horizontal?: boolean;
|
|
467
|
-
@Input() inverted?: boolean;
|
|
468
|
-
@Input() extraData?: unknown;
|
|
469
|
-
@Input() onEndReachedThreshold?: number;
|
|
470
|
-
@Input() onStartReachedThreshold?: number;
|
|
471
|
-
@Input() refreshing?: boolean | null;
|
|
472
|
-
@Input() progressViewOffset?: number;
|
|
473
|
-
// Set explicitly by a wrapper (FlatList/VirtualizedSectionList) that ALWAYS binds
|
|
474
|
-
// `(refresh)="refresh.emit()"` on this component to re-forward the event outward — that binding
|
|
475
|
-
// itself makes `this.refresh.observed` permanently true (Angular subscribes unconditionally the
|
|
476
|
-
// moment a template writes `(refresh)="…"`, regardless of what the handler does), so `.observed`
|
|
477
|
-
// can no longer tell "the app actually wants pull-to-refresh" from "a wrapper is just forwarding
|
|
478
|
-
// the event". A wrapper passes its OWN public `refresh.observed` here instead; direct usage
|
|
479
|
-
// (no wrapper) falls back to this component's own `.observed`, unchanged from before.
|
|
480
|
-
@Input() refreshRequested?: boolean;
|
|
481
|
-
@Input() viewabilityConfig?: IViewabilityConfig;
|
|
482
|
-
@Input()
|
|
483
|
-
viewabilityConfigCallbackPairs?: IViewabilityConfigCallbackPair<ItemT>[];
|
|
484
|
-
@Input() initialNumToRender?: number;
|
|
485
|
-
@Input() initialScrollIndex?: number;
|
|
486
|
-
@Input() maxToRenderPerBatch?: number;
|
|
487
|
-
@Input() updateCellsBatchingPeriod?: number;
|
|
488
|
-
@Input() windowSize?: number;
|
|
489
|
-
@Input() stickyHeaderIndices?: number[];
|
|
490
|
-
@Input() maintainVisibleContentPosition?: {
|
|
491
|
-
minIndexForVisible: number;
|
|
492
|
-
autoscrollToTopThreshold?: number;
|
|
493
|
-
};
|
|
494
|
-
@Input() onScroll?: (event: ISymbioteEvent) => void;
|
|
495
|
-
@Input() onScrollBeginDrag?: (event: ISymbioteEvent) => void;
|
|
496
|
-
@Input() onScrollEndDrag?: (event: ISymbioteEvent) => void;
|
|
497
|
-
@Input() onMomentumScrollBegin?: (event: ISymbioteEvent) => void;
|
|
498
|
-
@Input() onMomentumScrollEnd?: (event: ISymbioteEvent) => void;
|
|
499
|
-
@Input() scrollEventThrottle?: number;
|
|
500
|
-
@Input() keyboardShouldPersistTaps?: boolean | 'always' | 'never' | 'handled';
|
|
501
|
-
@Input() keyboardDismissMode?: 'none' | 'on-drag' | 'interactive';
|
|
502
|
-
@Input() removeClippedSubviews?: boolean;
|
|
503
|
-
@Input() nestedScrollEnabled?: boolean;
|
|
504
|
-
@Input() style?: IStyleProp<IViewStyle>;
|
|
505
|
-
@Input() contentContainerStyle?: IStyleProp<IViewStyle>;
|
|
506
|
-
@Input() testID?: string;
|
|
507
|
-
@Input() nativeID?: string;
|
|
508
|
-
@Input() accessible?: boolean;
|
|
509
|
-
@Input() accessibilityLabel?: string;
|
|
510
|
-
@Input() accessibilityHint?: string;
|
|
511
|
-
@Input() accessibilityRole?: IAccessibilityProps['accessibilityRole'];
|
|
512
|
-
@Input() accessibilityState?: IAccessibilityStateValue;
|
|
513
|
-
@Input() accessibilityValue?: IAccessibilityProps['accessibilityValue'];
|
|
514
|
-
@Input() accessibilityActions?: IAccessibilityProps['accessibilityActions'];
|
|
515
|
-
@Input() accessibilityLabelledBy?: string | string[];
|
|
516
|
-
@Input()
|
|
517
|
-
importantForAccessibility?: IAccessibilityProps['importantForAccessibility'];
|
|
518
|
-
@Input()
|
|
519
|
-
accessibilityLiveRegion?: IAccessibilityProps['accessibilityLiveRegion'];
|
|
520
|
-
@Input() screenReaderFocusable?: boolean;
|
|
521
|
-
@Input() accessibilityViewIsModal?: boolean;
|
|
522
|
-
@Input() accessibilityElementsHidden?: boolean;
|
|
523
|
-
@Input() accessibilityIgnoresInvertColors?: boolean;
|
|
524
|
-
@Input() accessibilityLanguage?: string;
|
|
525
|
-
@Input() accessibilityRespondsToUserInteraction?: boolean;
|
|
526
|
-
@Input() accessibilityShowsLargeContentViewer?: boolean;
|
|
527
|
-
@Input() accessibilityLargeContentTitle?: string;
|
|
528
|
-
@Input() role?: IAriaProps['role'];
|
|
529
|
-
@Input() ariaLabel?: string;
|
|
530
|
-
@Input() ariaLabelledBy?: string;
|
|
531
|
-
@Input() ariaLive?: IAriaProps['aria-live'];
|
|
532
|
-
@Input() ariaHidden?: boolean;
|
|
533
|
-
@Input() ariaBusy?: boolean;
|
|
534
|
-
@Input() ariaChecked?: boolean | 'mixed';
|
|
535
|
-
@Input() ariaDisabled?: boolean;
|
|
536
|
-
@Input() ariaExpanded?: boolean;
|
|
537
|
-
@Input() ariaSelected?: boolean;
|
|
538
|
-
@Input() ariaModal?: boolean;
|
|
539
|
-
@Input() ariaValueMax?: number;
|
|
540
|
-
@Input() ariaValueMin?: number;
|
|
541
|
-
@Input() ariaValueNow?: number;
|
|
542
|
-
@Input() ariaValueText?: string;
|
|
543
|
-
|
|
544
|
-
// The cell + slot templates the app authors (the Angular twin of renderItem / ListHeaderComponent
|
|
545
|
-
// / ListFooterComponent / ListEmptyComponent / ItemSeparatorComponent), captured from projected
|
|
546
|
-
// <ng-template> content. Resolved by ngAfterContentInit, read each ngDoCheck (from the 2nd CD on).
|
|
547
|
-
@ContentChild(VListItemDirective) itemDir?: VListItemDirective<ItemT>;
|
|
548
|
-
@ContentChild(VListHeaderDirective) headerDir?: VListHeaderDirective;
|
|
549
|
-
@ContentChild(VListFooterDirective) footerDir?: VListFooterDirective;
|
|
550
|
-
@ContentChild(VListEmptyDirective) emptyDir?: VListEmptyDirective;
|
|
551
|
-
@ContentChild(VListSeparatorDirective)
|
|
552
|
-
separatorDir?: VListSeparatorDirective<ItemT>;
|
|
553
|
-
|
|
554
|
-
// A wrapping list (FlatList) hands its app's cell/separator templates straight in, so a cell is
|
|
555
|
-
// one outlet deep instead of a wrapper outlet around the app's own; each wins over projection.
|
|
556
|
-
@Input() itemTemplate?: TemplateRef<IVListItemContext<ItemT>>;
|
|
557
|
-
@Input() itemSeparatorTemplate?: TemplateRef<IVListSeparatorContext<ItemT>>;
|
|
558
|
-
|
|
559
|
-
get cellTemplate(): TemplateRef<IVListItemContext<ItemT>> | undefined {
|
|
560
|
-
return this.itemTemplate ?? this.itemDir?.templateRef;
|
|
561
|
-
}
|
|
562
|
-
|
|
563
|
-
get separatorTemplate():
|
|
564
|
-
TemplateRef<IVListSeparatorContext<ItemT>> | undefined {
|
|
565
|
-
return this.itemSeparatorTemplate ?? this.separatorDir?.templateRef;
|
|
566
|
-
}
|
|
567
|
-
|
|
568
|
-
// The inner scroll TAG's own engine node — a template ref on a bare intrinsic hands back the
|
|
569
|
-
// host node directly (`isSymbioteNode(elementRef.nativeElement)`, matching `anchorHostStyle`'s
|
|
570
|
-
// own reader below), so there is no wrapper component left to read a handle off. Present from
|
|
571
|
-
// whichever branch (`scroll-view` / `horizontal-scroll-view`) is currently mounted, once the
|
|
572
|
-
// template has run at least one CD pass.
|
|
99
|
+
// A template ref on a bare intrinsic hands back the host node directly
|
|
573
100
|
@ViewChild('scrollHost', { read: ElementRef })
|
|
574
101
|
private scrollHostRef?: ElementRef<unknown>;
|
|
575
102
|
|
|
576
|
-
|
|
103
|
+
protected get scrollNode(): ISymbioteNode | null {
|
|
577
104
|
const node = this.scrollHostRef?.nativeElement;
|
|
578
105
|
return isSymbioteNode(node) ? node : null;
|
|
579
106
|
}
|
|
580
107
|
|
|
581
|
-
|
|
582
|
-
// commit — the same function every other adapter feeds from its own host-instance accessor.
|
|
583
|
-
private readonly scrollHandle: IScrollViewHandle = buildScrollViewHandle(
|
|
108
|
+
protected readonly scrollHandle: IScrollViewHandle = buildScrollViewHandle(
|
|
584
109
|
() => this.scrollNode,
|
|
585
110
|
);
|
|
586
111
|
|
|
587
|
-
//
|
|
112
|
+
// Template-bound view state, assembled by `recomputeView` in `ngDoCheck`
|
|
588
113
|
itemCount = EMPTY_OFFSET;
|
|
589
114
|
windowCells: IWindowCell<ItemT>[] = [];
|
|
590
|
-
// The nearest sticky
|
|
591
|
-
// Angular twin of RN's _ensureClosestStickyHeader. Rendered ahead of windowCells, right after the
|
|
592
|
-
// leading spacer, with gapSpacerStyle filling the gap to the window's own first cell — see
|
|
593
|
-
// buildListPlan's doc comment for the exact child order this assumes.
|
|
115
|
+
// The nearest sticky cell mounted outside the window, `gapSpacerStyle` fills up to the window
|
|
594
116
|
forcedStickyCell: IWindowCell<ItemT> | null = null;
|
|
595
117
|
leadingSpacerStyle: IViewStyle | null = null;
|
|
596
118
|
gapSpacerStyle: IViewStyle | null = null;
|
|
597
119
|
trailingSpacerStyle: IViewStyle | null = null;
|
|
598
120
|
cellStyle: IViewStyle | undefined = undefined;
|
|
599
|
-
// Bound to the template's `[style]="resolvedStyle"`, which Angular compiles to the built-in
|
|
600
|
-
// ɵɵstyleMap instruction — it only understands a flat object, never an array (RN's own
|
|
601
|
-
// `style={[a, b]}` composition idiom crashes deep inside Angular's styling engine), so this
|
|
602
|
-
// is always pre-flattened via the engine's own flattenStyle, never left as an array.
|
|
603
|
-
resolvedStyle: IViewStyle | undefined = undefined;
|
|
604
|
-
resolvedContentContainerStyle: IStyleProp<IViewStyle> | undefined = undefined;
|
|
605
|
-
resolvedMaintainVisibleContentPosition:
|
|
606
|
-
| { minIndexForVisible: number; autoscrollToTopThreshold?: number }
|
|
607
|
-
| undefined = undefined;
|
|
608
|
-
// The offset we are imperatively driving native to before the scroll handle attaches (rides down
|
|
609
|
-
// as contentOffset). A fresh object identity each push so the commit re-applies a repeated value.
|
|
610
|
-
commandedOffset: { x: number; y: number } | undefined = undefined;
|
|
611
121
|
|
|
612
|
-
//
|
|
613
|
-
// measured lengths, committed window, edge/viewability dedup, MVCP anchor — all in IListState,
|
|
614
|
-
// driven by the shared reduceList. `renderVersion` bumps whenever a transition changes render
|
|
615
|
-
// state, so ngDoCheck's recompute-dedup can detect it (listState is a plain object, not tracked).
|
|
122
|
+
// `renderVersion` bumps on every transition that changes render state, for the recompute dedup
|
|
616
123
|
private readonly listState: IListState<ItemT> =
|
|
617
124
|
createInitialListState<ItemT>();
|
|
618
125
|
private renderVersion = EMPTY_OFFSET;
|
|
619
126
|
|
|
620
|
-
private readonly
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
// Everything a cell's CONTENT can depend on that is not its own item identity. RN's contract for
|
|
638
|
-
// "re-render the cells, the data object itself did not change" is extraData; without it here an
|
|
639
|
-
// extraData flip would reuse every context and repaint nothing.
|
|
640
|
-
private cellContextEpoch: unknown[] | undefined = undefined;
|
|
641
|
-
private readonly separatorOverrides = new Map<
|
|
642
|
-
number,
|
|
643
|
-
Partial<ISeparatorProps<unknown>>
|
|
644
|
-
>();
|
|
645
|
-
// The adapter-owned debounce (minimumViewTime) and incremental-fill timers; the reducer only
|
|
646
|
-
// hands back a delay.
|
|
647
|
-
private viewableTimer: ReturnType<typeof setTimeout> | null = null;
|
|
648
|
-
private batchTimer: ReturnType<typeof setTimeout> | null = null;
|
|
649
|
-
// Dedupes the after-commit effects: they run only when the windowing signature changed.
|
|
127
|
+
private readonly cells = new CellRegistry<ItemT>({
|
|
128
|
+
itemAt: index => this.getItem(this.data, index),
|
|
129
|
+
itemCount: () => this.listState.metrics.count,
|
|
130
|
+
isHorizontal: () => this.isHorizontal,
|
|
131
|
+
markForCheck: () => this.cdr.markForCheck(),
|
|
132
|
+
measured: (index, length, offset) =>
|
|
133
|
+
this.dispatch({ kind: LIST_ACTION_KIND.measure, index, length, offset }),
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
private readonly effects = new ListEffectRunner<ItemT>({
|
|
137
|
+
scrollToPixel: (offset, animated) => this.scrollToPixel(offset, animated),
|
|
138
|
+
dispatch: action => this.dispatch(action),
|
|
139
|
+
endReached: this.endReached,
|
|
140
|
+
startReached: this.startReached,
|
|
141
|
+
scrollToIndexFailed: this.scrollToIndexFailed,
|
|
142
|
+
});
|
|
143
|
+
|
|
650
144
|
private lastEffectSignature = '';
|
|
651
|
-
//
|
|
652
|
-
// windowCells (and each cell's `separators` handle — fresh closures every call) on EVERY CD pass,
|
|
653
|
-
// including ones triggered by something else entirely in the app. A fresh context object flows
|
|
654
|
-
// into VListOutletDirective, which (correctly) sees its `context` @Input() change and calls
|
|
655
|
-
// `viewRef.markForCheck()` — which reschedules ANOTHER change-detection tick via the zoneless
|
|
656
|
-
// scheduler (adapters/angular/src/render.ts). Recomputing unconditionally therefore free-runs
|
|
657
|
-
// forever the moment a list actually has cells to stamp (a real, previously-dormant bug only
|
|
658
|
-
// exposed once flat-list.test.ts's projection fix let cells render for the first time — see that
|
|
659
|
-
// test's comment). Guarding recompute behind "did anything relevant actually change" breaks the
|
|
660
|
-
// cycle: unrelated CD passes reuse the same windowCells/context identities, so
|
|
661
|
-
// VListOutletDirective's ngOnChanges sees no change and never reschedules.
|
|
145
|
+
// Without this dedup a rebuilt cell context reschedules another tick and the list free-runs
|
|
662
146
|
private lastRecompute: unknown[] | undefined = undefined;
|
|
663
147
|
|
|
664
148
|
private readonly cdr = inject(ChangeDetectorRef);
|
|
665
|
-
// This component's
|
|
666
|
-
// onto (see anchorHostStyle's doc comment) — NOT `scrollHostRef` above, which targets the real
|
|
667
|
-
// inner `<scroll-view>`/`<horizontal-scroll-view>` tag one level down.
|
|
149
|
+
// This component's own host, the anchor that `class="..."` at the use site resolves onto
|
|
668
150
|
private readonly elementRef = inject(ElementRef);
|
|
669
151
|
|
|
670
|
-
// Bridges the non-reactive @Input fields into the reactive graph so `foldedAccessibility` below
|
|
671
|
-
// can memoize a bag derived from them (read its own comment for why signal inputs are not an
|
|
672
|
-
// option while this package's unit suite runs on JIT).
|
|
673
|
-
//
|
|
674
|
-
// ONLY safe for a bag whose every dependency is an @Input. This list's window machinery
|
|
675
|
-
// (listState, renderVersion, windowCells, the measured metrics) is driven from scroll/layout
|
|
676
|
-
// callbacks that never touch ngOnChanges, so nothing derived from it may ride this signal - it
|
|
677
|
-
// would memoize a window that stops re-computing mid-scroll. Make that state its own signal
|
|
678
|
-
// instead of widening this one.
|
|
679
|
-
private readonly inputsRevision = signal(0);
|
|
680
|
-
|
|
681
|
-
// MEASURED (while ScrollView was still a wrapper): the template fed 20 separate inputs off this
|
|
682
|
-
// one bag, and Angular does not cache a getter across binding expressions - as a getter it
|
|
683
|
-
// rebuilt the object 20 times per refresh, and every consumer saw a fresh reference, so all 20
|
|
684
|
-
// reported "changed" and wrote through. A computed evaluates once and hands back the SAME object
|
|
685
|
-
// until an input actually changes. Every dependency below is an @Input, which is what makes
|
|
686
|
-
// inputsRevision a complete dependency set here — `scrollViewBag()` reuses this result rather
|
|
687
|
-
// than re-running the fold.
|
|
688
|
-
readonly foldedAccessibility = computed<
|
|
689
|
-
IAccessibilityProps & IAriaProps & Record<string, unknown>
|
|
690
|
-
>(() => {
|
|
691
|
-
this.inputsRevision();
|
|
692
|
-
return resolveAccessibilityProps({
|
|
693
|
-
testID: this.testID,
|
|
694
|
-
nativeID: this.nativeID,
|
|
695
|
-
accessible: this.accessible,
|
|
696
|
-
accessibilityLabel: this.accessibilityLabel,
|
|
697
|
-
accessibilityHint: this.accessibilityHint,
|
|
698
|
-
accessibilityRole: this.accessibilityRole,
|
|
699
|
-
accessibilityState: this.accessibilityState,
|
|
700
|
-
accessibilityValue: this.accessibilityValue,
|
|
701
|
-
accessibilityActions: this.accessibilityActions,
|
|
702
|
-
accessibilityLabelledBy: this.accessibilityLabelledBy,
|
|
703
|
-
importantForAccessibility: this.importantForAccessibility,
|
|
704
|
-
accessibilityLiveRegion: this.accessibilityLiveRegion,
|
|
705
|
-
screenReaderFocusable: this.screenReaderFocusable,
|
|
706
|
-
accessibilityViewIsModal: this.accessibilityViewIsModal,
|
|
707
|
-
accessibilityElementsHidden: this.accessibilityElementsHidden,
|
|
708
|
-
accessibilityIgnoresInvertColors: this.accessibilityIgnoresInvertColors,
|
|
709
|
-
accessibilityLanguage: this.accessibilityLanguage,
|
|
710
|
-
accessibilityRespondsToUserInteraction:
|
|
711
|
-
this.accessibilityRespondsToUserInteraction,
|
|
712
|
-
accessibilityShowsLargeContentViewer:
|
|
713
|
-
this.accessibilityShowsLargeContentViewer,
|
|
714
|
-
accessibilityLargeContentTitle: this.accessibilityLargeContentTitle,
|
|
715
|
-
role: this.role,
|
|
716
|
-
'aria-label': this.ariaLabel,
|
|
717
|
-
'aria-labelledby': this.ariaLabelledBy,
|
|
718
|
-
'aria-live': this.ariaLive,
|
|
719
|
-
'aria-hidden': this.ariaHidden,
|
|
720
|
-
'aria-busy': this.ariaBusy,
|
|
721
|
-
'aria-checked': this.ariaChecked,
|
|
722
|
-
'aria-disabled': this.ariaDisabled,
|
|
723
|
-
'aria-expanded': this.ariaExpanded,
|
|
724
|
-
'aria-selected': this.ariaSelected,
|
|
725
|
-
'aria-modal': this.ariaModal,
|
|
726
|
-
'aria-valuemax': this.ariaValueMax,
|
|
727
|
-
'aria-valuemin': this.ariaValueMin,
|
|
728
|
-
'aria-valuenow': this.ariaValueNow,
|
|
729
|
-
'aria-valuetext': this.ariaValueText,
|
|
730
|
-
});
|
|
731
|
-
});
|
|
732
|
-
|
|
733
|
-
get shouldRenderRefreshControl(): boolean {
|
|
734
|
-
return this.refreshRequested ?? this.refresh.observed;
|
|
735
|
-
}
|
|
736
|
-
|
|
737
|
-
// The flat bag fed to the scroll tag via `[symbioteHostProps]`. A plain method, not `computed()`:
|
|
738
|
-
// most of these fields (resolvedStyle, renderedStickyIndices, commandedOffset, …) are recomputed
|
|
739
|
-
// in ngDoCheck/dispatch as ordinary fields, not signals, so a computed() memoized only against
|
|
740
|
-
// `inputsRevision` would go stale the moment one of them changed outside an @Input() write. The
|
|
741
|
-
// directive itself diffs per key before writing to the engine (`primitives/shared.ts`), so a
|
|
742
|
-
// fresh object every CD pass costs nothing beyond the literal allocation.
|
|
743
|
-
scrollViewBag(): Record<string, unknown> {
|
|
744
|
-
return {
|
|
745
|
-
...this.foldedAccessibility(),
|
|
746
|
-
style: this.resolvedStyle,
|
|
747
|
-
contentContainerStyle: this.resolvedContentContainerStyle,
|
|
748
|
-
onScroll: this.onScrollTick,
|
|
749
|
-
onLayout: this.onLayoutTick,
|
|
750
|
-
onScrollBeginDrag: this.onScrollBeginDrag,
|
|
751
|
-
onScrollEndDrag: this.onScrollEndDrag,
|
|
752
|
-
onMomentumScrollBegin: this.onMomentumScrollBegin,
|
|
753
|
-
onMomentumScrollEnd: this.onMomentumScrollEnd,
|
|
754
|
-
scrollEventThrottle: this.scrollEventThrottle,
|
|
755
|
-
keyboardShouldPersistTaps: this.keyboardShouldPersistTaps,
|
|
756
|
-
keyboardDismissMode: this.keyboardDismissMode,
|
|
757
|
-
// RN's VirtualizedList spreads its props onto the ScrollView, this one included.
|
|
758
|
-
removeClippedSubviews: this.removeClippedSubviews,
|
|
759
|
-
nestedScrollEnabled: this.nestedScrollEnabled,
|
|
760
|
-
// VirtualizedList.js:1111 — Android moves the scrollbar back after the `scale: -1` flip.
|
|
761
|
-
isInvertedVirtualizedList: this.isInverted ? true : undefined,
|
|
762
|
-
contentOffset: this.commandedOffset,
|
|
763
|
-
// `stickyHeaderIndices` is deliberately NOT forwarded — it numbers the scroll view's PAINT
|
|
764
|
-
// children, and a windowed list paints a header, a spacer and a slice, so the positions move
|
|
765
|
-
// every time the window slides and the behavior re-wraps a different child each pass. The
|
|
766
|
-
// cells carry the `sticky-header` TAG instead, which pins by DOCUMENT order.
|
|
767
|
-
maintainVisibleContentPosition:
|
|
768
|
-
this.resolvedMaintainVisibleContentPosition,
|
|
769
|
-
// The four boolean-gated Fabric events (`.claude/rules/fabric-boolean-event-gates.md`): the
|
|
770
|
-
// TAG has no `.observed` of its own, so `wantsGate` (own subscriber, or the demand from
|
|
771
|
-
// whatever wraps this list) is what decides whether the flag reaches Fabric at all.
|
|
772
|
-
onAccessibilityAction: this.wantsGate('accessibilityAction')
|
|
773
|
-
? this.accessibilityActionTick
|
|
774
|
-
: undefined,
|
|
775
|
-
onAccessibilityTap: this.wantsGate('accessibilityTap')
|
|
776
|
-
? this.accessibilityTapTick
|
|
777
|
-
: undefined,
|
|
778
|
-
onMagicTap: this.wantsGate('magicTap') ? this.magicTapTick : undefined,
|
|
779
|
-
onAccessibilityEscape: this.wantsGate('accessibilityEscape')
|
|
780
|
-
? this.accessibilityEscapeTick
|
|
781
|
-
: undefined,
|
|
782
|
-
};
|
|
783
|
-
}
|
|
784
|
-
|
|
785
|
-
// The `refresh-control` tag's own bag — an ordinary first child, re-parented per platform by the
|
|
786
|
-
// scroll-view behavior (`../refresh-control-props.ts`). `onRefresh` is a plain listener prop, not
|
|
787
|
-
// a gated one: RefreshControl's `refresh` is unaffected by `fabric-boolean-event-gates.md`.
|
|
788
|
-
refreshControlBag(): Record<string, unknown> {
|
|
789
|
-
return {
|
|
790
|
-
refreshing: this.refreshing ?? false,
|
|
791
|
-
progressViewOffset: this.progressViewOffset,
|
|
792
|
-
onRefresh: this.handleRefresh,
|
|
793
|
-
};
|
|
794
|
-
}
|
|
795
|
-
|
|
796
|
-
get isHorizontal(): boolean {
|
|
797
|
-
return this.horizontal === true;
|
|
798
|
-
}
|
|
799
|
-
private get isInverted(): boolean {
|
|
800
|
-
return this.inverted === true;
|
|
801
|
-
}
|
|
802
|
-
private get windowSizeValue(): number {
|
|
803
|
-
return this.windowSize ?? DEFAULT_WINDOW_SIZE;
|
|
804
|
-
}
|
|
805
|
-
private get initialNumToRenderValue(): number {
|
|
806
|
-
return this.initialNumToRender ?? DEFAULT_INITIAL_NUM_TO_RENDER;
|
|
807
|
-
}
|
|
808
|
-
private get maxToRenderPerBatchValue(): number {
|
|
809
|
-
return this.maxToRenderPerBatch ?? DEFAULT_MAX_TO_RENDER_PER_BATCH;
|
|
810
|
-
}
|
|
811
|
-
private get updateCellsBatchingPeriodValue(): number {
|
|
812
|
-
return (
|
|
813
|
-
this.updateCellsBatchingPeriod ?? DEFAULT_UPDATE_CELLS_BATCHING_PERIOD
|
|
814
|
-
);
|
|
815
|
-
}
|
|
816
|
-
private get onEndReachedThresholdValue(): number | undefined {
|
|
817
|
-
return this.onEndReachedThreshold;
|
|
818
|
-
}
|
|
819
|
-
private get onStartReachedThresholdValue(): number | undefined {
|
|
820
|
-
return this.onStartReachedThreshold;
|
|
821
|
-
}
|
|
822
|
-
|
|
823
|
-
// onScroll stays a plain callback bag key (an Animated.event(...) target must be able to flow
|
|
824
|
-
// through it, which an @Output() can't carry), so this is a stable arrow field read straight into
|
|
825
|
-
// scrollViewBag()'s `onScroll`. onLayout is unconditional too — this list always needs its own
|
|
826
|
-
// layout for windowing, regardless of whether an app subscribes to anything.
|
|
827
|
-
onScrollTick = (event: ISymbioteEvent): void => {
|
|
828
|
-
countAngular('scrollTicks');
|
|
829
|
-
const offset = readScrollOffset(event, this.isHorizontal);
|
|
830
|
-
if (offset === undefined) return;
|
|
831
|
-
dlog(`Angular VirtualizedList onScroll offset=${offset}`);
|
|
832
|
-
// A real native scroll supersedes any pending commanded offset.
|
|
833
|
-
this.commandedOffset = undefined;
|
|
834
|
-
this.dispatch({ kind: 'scroll', offset });
|
|
835
|
-
// Compose, don't clobber: internal windowing ran first, now the user's onScroll.
|
|
836
|
-
this.onScroll?.(event);
|
|
837
|
-
};
|
|
838
|
-
|
|
839
|
-
onLayoutTick = (event: ISymbioteEvent): void => {
|
|
840
|
-
const length = readLayoutLength(event, this.isHorizontal);
|
|
841
|
-
if (length === undefined) return;
|
|
842
|
-
dlog(`Angular VirtualizedList onLayout viewport=${length}`);
|
|
843
|
-
this.dispatch({ kind: 'layout', length });
|
|
844
|
-
};
|
|
845
|
-
|
|
846
|
-
// RefreshControl's refresh and the scroll tag's accessibility events reach here as plain
|
|
847
|
-
// listener-prop values in scrollViewBag()/refreshControlBag() — these fields just adapt
|
|
848
|
-
// VirtualizedList's own @Output() into a plain re-emit callback.
|
|
849
|
-
handleRefresh = (): void => {
|
|
850
|
-
this.refresh.emit();
|
|
851
|
-
};
|
|
852
|
-
|
|
853
|
-
accessibilityActionTick = (event: ISymbioteEvent): void => {
|
|
854
|
-
this.accessibilityAction.emit(event);
|
|
855
|
-
};
|
|
856
|
-
|
|
857
|
-
accessibilityTapTick = (event: ISymbioteEvent): void => {
|
|
858
|
-
this.accessibilityTap.emit(event);
|
|
859
|
-
};
|
|
860
|
-
|
|
861
|
-
magicTapTick = (event: ISymbioteEvent): void => {
|
|
862
|
-
this.magicTap.emit(event);
|
|
863
|
-
};
|
|
864
|
-
|
|
865
|
-
accessibilityEscapeTick = (event: ISymbioteEvent): void => {
|
|
866
|
-
this.accessibilityEscape.emit(event);
|
|
867
|
-
};
|
|
868
|
-
|
|
869
152
|
private keyFor = (index: number): string => {
|
|
870
153
|
const item = this.getItem(this.data, index);
|
|
871
154
|
return resolveItemKey(item, index, this.keyExtractor);
|
|
872
155
|
};
|
|
873
156
|
|
|
874
|
-
//
|
|
875
|
-
// `.observed` (Angular's "is anyone bound to this @Output()"), so the reducer emits only when the
|
|
876
|
-
// consumer listens — the same on-demand gating the prop-callback era had.
|
|
157
|
+
// Edge and viewability listeners map to `.observed`, so the reducer emits only if listened to
|
|
877
158
|
private buildInputs(): IListReducerInputs<ItemT> {
|
|
878
159
|
return {
|
|
879
160
|
data: this.data,
|
|
@@ -886,8 +167,8 @@ export class VirtualizedList<ItemT = unknown>
|
|
|
886
167
|
initialNumToRender: this.initialNumToRenderValue,
|
|
887
168
|
maxToRenderPerBatch: this.maxToRenderPerBatchValue,
|
|
888
169
|
updateCellsBatchingPeriod: this.updateCellsBatchingPeriodValue,
|
|
889
|
-
onEndReachedThreshold: this.
|
|
890
|
-
onStartReachedThreshold: this.
|
|
170
|
+
onEndReachedThreshold: this.onEndReachedThreshold,
|
|
171
|
+
onStartReachedThreshold: this.onStartReachedThreshold,
|
|
891
172
|
onEndReachedActive: this.endReached.observed,
|
|
892
173
|
onStartReachedActive: this.startReached.observed,
|
|
893
174
|
viewabilityPairs: buildViewabilityPairs(
|
|
@@ -903,12 +184,11 @@ export class VirtualizedList<ItemT = unknown>
|
|
|
903
184
|
};
|
|
904
185
|
}
|
|
905
186
|
|
|
906
|
-
//
|
|
907
|
-
|
|
908
|
-
private dispatch(action: IListAction<ItemT>): void {
|
|
187
|
+
// Native callbacks fire outside Angular's bindings, so a changed state must mark the view dirty
|
|
188
|
+
protected dispatch(action: IListAction<ItemT>): void {
|
|
909
189
|
const inputs = this.buildInputs();
|
|
910
190
|
const result = reduceList(this.listState, action, inputs);
|
|
911
|
-
this.
|
|
191
|
+
this.effects.run(result.effects, inputs);
|
|
912
192
|
if (!result.changed) return;
|
|
913
193
|
this.renderVersion += 1;
|
|
914
194
|
if (this.isWindowSettled(action)) return;
|
|
@@ -916,29 +196,13 @@ export class VirtualizedList<ItemT = unknown>
|
|
|
916
196
|
this.cdr.markForCheck();
|
|
917
197
|
}
|
|
918
198
|
|
|
919
|
-
//
|
|
920
|
-
//
|
|
921
|
-
// TEMPLATE reads none of those; it reads the window. Marking on the offset repainted the whole
|
|
922
|
-
// ancestor chain 60 times a second on a sticky screen for a window that had not moved.
|
|
923
|
-
//
|
|
924
|
-
// So the question here is "would a render leave the window exactly where it is", and it is asked
|
|
925
|
-
// of the state as it is NOW. It must NOT be answered by comparing the last-rendered window with
|
|
926
|
-
// the one before it: that window was derived from the PREVIOUS scroll offset, so once two
|
|
927
|
-
// consecutive derives agree the comparison reads "settled" for every offset that ever follows -
|
|
928
|
-
// no mark, no CD, no ngDoCheck, no refresh-metrics, so the window can never move again and the
|
|
929
|
-
// list goes permanently deaf mid-scroll (device: examples/angular Benchmark sticky path B froze
|
|
930
|
-
// at exactly the window the first viewport asked for, blank below it).
|
|
931
|
-
//
|
|
932
|
-
// computeWindow re-run over the CACHED offset table answers it directly. Two scans, no
|
|
933
|
-
// allocation - buildOffsets, the O(count) allocating half, still runs once per render only. It is
|
|
934
|
-
// the same function the render will use, so this cannot drift away from the window it predicts.
|
|
199
|
+
// Asks whether a render would leave the window where it is, of the state as it is now
|
|
200
|
+
// Comparing the last two rendered windows would read settled forever and the list goes deaf
|
|
935
201
|
private isWindowSettled(action: IListAction<ItemT>): boolean {
|
|
936
|
-
// A measurement rewrites the
|
|
937
|
-
|
|
938
|
-
if (action.kind === 'measure') return false;
|
|
202
|
+
// A measurement rewrites the offset table the prediction reads
|
|
203
|
+
if (action.kind === LIST_ACTION_KIND.measure) return false;
|
|
939
204
|
const m = this.listState.metrics;
|
|
940
|
-
// Mid-fill
|
|
941
|
-
// only a render advances it. Skipping the mark here is what stalls the incremental fill.
|
|
205
|
+
// Mid-fill the window climbs one batch step per render, only a render advances it
|
|
942
206
|
if (m.first !== m.target.first || m.last !== m.target.last) return false;
|
|
943
207
|
const next = computeWindow(
|
|
944
208
|
m.count,
|
|
@@ -959,83 +223,7 @@ export class VirtualizedList<ItemT = unknown>
|
|
|
959
223
|
return isSettled;
|
|
960
224
|
}
|
|
961
225
|
|
|
962
|
-
|
|
963
|
-
effects: IListEffect<ItemT>[],
|
|
964
|
-
inputs: IListReducerInputs<ItemT>,
|
|
965
|
-
): void {
|
|
966
|
-
for (const effect of effects) {
|
|
967
|
-
switch (effect.kind) {
|
|
968
|
-
case 'scroll-to':
|
|
969
|
-
this.scrollToPixel(effect.offset, effect.animated);
|
|
970
|
-
break;
|
|
971
|
-
case 'fire-end-reached':
|
|
972
|
-
this.endReached.emit({ distanceFromEnd: effect.distanceFromEnd });
|
|
973
|
-
break;
|
|
974
|
-
case 'fire-start-reached':
|
|
975
|
-
this.startReached.emit({
|
|
976
|
-
distanceFromStart: effect.distanceFromStart,
|
|
977
|
-
});
|
|
978
|
-
break;
|
|
979
|
-
case 'fire-scroll-to-index-failed':
|
|
980
|
-
this.scrollToIndexFailed.emit({
|
|
981
|
-
index: effect.index,
|
|
982
|
-
highestMeasuredFrameIndex: effect.highestMeasuredFrameIndex,
|
|
983
|
-
averageItemLength: effect.averageItemLength,
|
|
984
|
-
});
|
|
985
|
-
break;
|
|
986
|
-
case 'schedule-refill': {
|
|
987
|
-
if (this.batchTimer !== null) clearTimeout(this.batchTimer);
|
|
988
|
-
this.batchTimer = setTimeout(() => {
|
|
989
|
-
this.batchTimer = null;
|
|
990
|
-
this.dispatch({ kind: 'batch-tick' });
|
|
991
|
-
}, effect.delay);
|
|
992
|
-
break;
|
|
993
|
-
}
|
|
994
|
-
case 'fire-viewable': {
|
|
995
|
-
const pairs = inputs.viewabilityPairs;
|
|
996
|
-
const info = effect.info;
|
|
997
|
-
const map = effect.map;
|
|
998
|
-
const fire = (): void => {
|
|
999
|
-
for (const pair of pairs) {
|
|
1000
|
-
pair.onViewableItemsChanged({
|
|
1001
|
-
...info,
|
|
1002
|
-
viewabilityConfig: pair.viewabilityConfig,
|
|
1003
|
-
});
|
|
1004
|
-
}
|
|
1005
|
-
this.dispatch({ kind: 'viewable-fired', map });
|
|
1006
|
-
};
|
|
1007
|
-
if (this.viewableTimer !== null) {
|
|
1008
|
-
clearTimeout(this.viewableTimer);
|
|
1009
|
-
this.viewableTimer = null;
|
|
1010
|
-
}
|
|
1011
|
-
if (effect.delay > EMPTY_OFFSET) {
|
|
1012
|
-
this.viewableTimer = setTimeout(() => {
|
|
1013
|
-
this.viewableTimer = null;
|
|
1014
|
-
fire();
|
|
1015
|
-
}, effect.delay);
|
|
1016
|
-
} else {
|
|
1017
|
-
fire();
|
|
1018
|
-
}
|
|
1019
|
-
break;
|
|
1020
|
-
}
|
|
1021
|
-
}
|
|
1022
|
-
}
|
|
1023
|
-
}
|
|
1024
|
-
|
|
1025
|
-
// foldedAccessibility reads plain @Input fields, which are NOT reactive on their own - this bump
|
|
1026
|
-
// is what tells that computed an input changed. It must stay in ngOnChanges: that is the single
|
|
1027
|
-
// moment Angular has finished writing every changed input for this pass, and it runs before
|
|
1028
|
-
// ngDoCheck below, so the recompute already sees the current values.
|
|
1029
|
-
ngOnChanges(): void {
|
|
1030
|
-
this.inputsRevision.update(revision => revision + 1);
|
|
1031
|
-
}
|
|
1032
|
-
|
|
1033
|
-
// Once-per-CD recompute, BEFORE the template bindings are read (so the freshly computed
|
|
1034
|
-
// template-bound fields render this pass — computing them in ngAfterContentChecked instead would
|
|
1035
|
-
// trip ExpressionChangedAfterItHasBeenChecked). The dedup guard runs refresh-metrics (which owns
|
|
1036
|
-
// the controlled committedWindow throttle) only when something relevant changed, so the window
|
|
1037
|
-
// grows exactly one step per meaningful CD. The projected templates (ContentChild) are resolved
|
|
1038
|
-
// from the second CD on; the first paint (count/viewport still 0) corrects on the layout-driven CD.
|
|
226
|
+
// Runs before the bindings are read, a later hook would trip the changed-after-checked guard
|
|
1039
227
|
ngDoCheck(): void {
|
|
1040
228
|
countAngular('listChecks');
|
|
1041
229
|
const recomputeInputs: unknown[] = [
|
|
@@ -1051,12 +239,9 @@ export class VirtualizedList<ItemT = unknown>
|
|
|
1051
239
|
this.stickyHeaderIndices,
|
|
1052
240
|
this.maintainVisibleContentPosition,
|
|
1053
241
|
this.style,
|
|
1054
|
-
// `
|
|
1055
|
-
// addClass/removeClass, never as an @Input - so without it here no entry moves on a class
|
|
1056
|
-
// toggle and the gate skips the recompute forever. Reference-stable, so the dedup holds.
|
|
242
|
+
// Arrives through `addClass` and `removeClass`, never as an input
|
|
1057
243
|
anchorHostStyle(this.elementRef),
|
|
1058
244
|
this.contentContainerStyle,
|
|
1059
|
-
// Folds scroll / layout / measure / batch-tick: dispatch bumps renderVersion on any change.
|
|
1060
245
|
this.renderVersion,
|
|
1061
246
|
this.headerDir !== undefined,
|
|
1062
247
|
this.footerDir !== undefined,
|
|
@@ -1072,9 +257,12 @@ export class VirtualizedList<ItemT = unknown>
|
|
|
1072
257
|
) {
|
|
1073
258
|
return;
|
|
1074
259
|
}
|
|
1075
|
-
// The single derive-per-CD: recompute the window off the current state before the view reads it.
|
|
1076
260
|
countAngular('listRecomputes');
|
|
1077
|
-
reduceList(
|
|
261
|
+
reduceList(
|
|
262
|
+
this.listState,
|
|
263
|
+
{ kind: LIST_ACTION_KIND.refreshMetrics },
|
|
264
|
+
this.buildInputs(),
|
|
265
|
+
);
|
|
1078
266
|
this.recomputeView();
|
|
1079
267
|
}
|
|
1080
268
|
|
|
@@ -1083,82 +271,36 @@ export class VirtualizedList<ItemT = unknown>
|
|
|
1083
271
|
if (signature === this.lastEffectSignature) return;
|
|
1084
272
|
this.lastEffectSignature = signature;
|
|
1085
273
|
const inputs = this.buildInputs();
|
|
1086
|
-
const result = reduceList(
|
|
1087
|
-
|
|
274
|
+
const result = reduceList(
|
|
275
|
+
this.listState,
|
|
276
|
+
{ kind: LIST_ACTION_KIND.commit },
|
|
277
|
+
inputs,
|
|
278
|
+
);
|
|
279
|
+
this.effects.run(result.effects, inputs);
|
|
1088
280
|
}
|
|
1089
281
|
|
|
1090
282
|
ngOnDestroy(): void {
|
|
1091
|
-
|
|
1092
|
-
if (this.batchTimer !== null) clearTimeout(this.batchTimer);
|
|
283
|
+
this.effects.dispose();
|
|
1093
284
|
}
|
|
1094
285
|
|
|
1095
286
|
private recomputeView(): void {
|
|
1096
287
|
const m = this.listState.metrics;
|
|
1097
288
|
this.itemCount = m.count;
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
const stickySet =
|
|
1101
|
-
this.stickyHeaderIndices !== undefined
|
|
1102
|
-
? new Set(this.stickyHeaderIndices)
|
|
1103
|
-
: undefined;
|
|
1104
|
-
|
|
1105
|
-
this.resolvedContentContainerStyle = this.isHorizontal
|
|
1106
|
-
? [this.contentContainerStyle, { width: m.total }]
|
|
1107
|
-
: this.contentContainerStyle;
|
|
1108
|
-
this.resolvedStyle = flattenStyle([
|
|
1109
|
-
anchorHostStyle(this.elementRef),
|
|
1110
|
-
// VirtualizedList.js: `[inversionStyle, style]` — the app's style can override the flip.
|
|
1111
|
-
this.isInverted
|
|
1112
|
-
? [this.isHorizontal ? INVERTED_X_STYLE : INVERTED_Y_STYLE, this.style]
|
|
1113
|
-
: this.style,
|
|
1114
|
-
]);
|
|
1115
|
-
this.cellStyle = this.isInverted
|
|
1116
|
-
? this.isHorizontal
|
|
1117
|
-
? INVERTED_X_STYLE
|
|
1118
|
-
: INVERTED_Y_STYLE
|
|
1119
|
-
: undefined;
|
|
1120
|
-
this.resolvedMaintainVisibleContentPosition =
|
|
1121
|
-
this.maintainVisibleContentPosition === undefined
|
|
1122
|
-
? undefined
|
|
1123
|
-
: {
|
|
1124
|
-
...this.maintainVisibleContentPosition,
|
|
1125
|
-
minIndexForVisible:
|
|
1126
|
-
this.maintainVisibleContentPosition.minIndexForVisible +
|
|
1127
|
-
(hasHeader ? 1 : 0),
|
|
1128
|
-
};
|
|
1129
|
-
|
|
1130
|
-
// A change in anything a cell's content reads other than its own item invalidates every cached
|
|
1131
|
-
// context: identity reuse is only sound while the reused object still describes the cell.
|
|
1132
|
-
const epoch: unknown[] = [
|
|
289
|
+
this.applyStyles(m.total);
|
|
290
|
+
this.cells.beginPass([
|
|
1133
291
|
this.data,
|
|
1134
292
|
this.extraData,
|
|
1135
293
|
this.getItem,
|
|
1136
294
|
this.keyExtractor,
|
|
1137
|
-
];
|
|
1138
|
-
const previousEpoch = this.cellContextEpoch;
|
|
1139
|
-
this.cellContextEpoch = epoch;
|
|
1140
|
-
if (
|
|
1141
|
-
previousEpoch === undefined ||
|
|
1142
|
-
previousEpoch.length !== epoch.length ||
|
|
1143
|
-
!previousEpoch.every((value, index) => value === epoch[index])
|
|
1144
|
-
) {
|
|
1145
|
-
this.cellContexts.clear();
|
|
1146
|
-
}
|
|
1147
|
-
|
|
295
|
+
]);
|
|
1148
296
|
if (m.count === FIRST_INDEX) {
|
|
1149
|
-
this.
|
|
1150
|
-
this.nextCellContexts.clear();
|
|
1151
|
-
this.windowCells = [];
|
|
1152
|
-
this.forcedStickyCell = null;
|
|
1153
|
-
this.leadingSpacerStyle = null;
|
|
1154
|
-
this.gapSpacerStyle = null;
|
|
1155
|
-
this.trailingSpacerStyle = null;
|
|
1156
|
-
dlog(
|
|
1157
|
-
`Angular VirtualizedList empty (viewport=${this.listState.viewportLength})`,
|
|
1158
|
-
);
|
|
297
|
+
this.showEmpty();
|
|
1159
298
|
return;
|
|
1160
299
|
}
|
|
1161
|
-
|
|
300
|
+
const stickySet =
|
|
301
|
+
this.stickyHeaderIndices !== undefined
|
|
302
|
+
? new Set(this.stickyHeaderIndices)
|
|
303
|
+
: undefined;
|
|
1162
304
|
const plan = buildListPlan({
|
|
1163
305
|
count: m.count,
|
|
1164
306
|
first: m.first,
|
|
@@ -1168,161 +310,88 @@ export class VirtualizedList<ItemT = unknown>
|
|
|
1168
310
|
total: m.total,
|
|
1169
311
|
keyFor: this.keyFor,
|
|
1170
312
|
stickyIndices: stickySet,
|
|
1171
|
-
hasHeader,
|
|
313
|
+
hasHeader: this.headerDir !== undefined,
|
|
1172
314
|
});
|
|
1173
|
-
this.
|
|
1174
|
-
|
|
1175
|
-
? this.spacerStyle(plan.leadingExtent)
|
|
1176
|
-
: null;
|
|
1177
|
-
this.gapSpacerStyle =
|
|
1178
|
-
plan.gapExtent > EMPTY_OFFSET ? this.spacerStyle(plan.gapExtent) : null;
|
|
1179
|
-
this.trailingSpacerStyle =
|
|
1180
|
-
plan.trailingExtent > EMPTY_OFFSET
|
|
1181
|
-
? this.spacerStyle(plan.trailingExtent)
|
|
1182
|
-
: null;
|
|
1183
|
-
// `plan.stickyChildPositions` is deliberately NOT read — it was the input to the index form.
|
|
1184
|
-
dlog(
|
|
1185
|
-
`STICKY[list] stickySet=${stickySet === undefined ? 'undefined' : JSON.stringify([...stickySet])} ` +
|
|
1186
|
-
`childPositions=${JSON.stringify(plan.stickyChildPositions)} ` +
|
|
1187
|
-
`forcedStickyCell=${plan.forcedStickyCell === undefined ? 'none' : plan.forcedStickyCell.index} ` +
|
|
1188
|
-
`window=[${m.first},${m.last}] hasHeader=${this.headerDir !== undefined}`,
|
|
1189
|
-
);
|
|
1190
|
-
|
|
1191
|
-
this.forcedStickyCell =
|
|
1192
|
-
plan.forcedStickyCell !== undefined
|
|
1193
|
-
? this.buildWindowCell(
|
|
1194
|
-
plan.forcedStickyCell.index,
|
|
1195
|
-
plan.forcedStickyCell.key,
|
|
1196
|
-
false,
|
|
1197
|
-
// Sticky by construction — `forcedStickyCell` exists only for an index in `stickySet`.
|
|
1198
|
-
true,
|
|
1199
|
-
)
|
|
1200
|
-
: null;
|
|
1201
|
-
|
|
1202
|
-
const cells: IWindowCell<ItemT>[] = [];
|
|
1203
|
-
for (const planned of plan.cells) {
|
|
1204
|
-
// RN gates the separator on the last index of the DATA, not the WINDOW
|
|
1205
|
-
// (VirtualizedList.js:793 `getItemCount(data) - 1`). The separator lives INSIDE the
|
|
1206
|
-
// measuring wrapper, so gating on the window would shift a cell's height as it slides past.
|
|
1207
|
-
const includeSeparator = hasSeparators && planned.index < m.count - 1;
|
|
1208
|
-
cells.push(
|
|
1209
|
-
this.buildWindowCell(
|
|
1210
|
-
planned.index,
|
|
1211
|
-
planned.key,
|
|
1212
|
-
includeSeparator,
|
|
1213
|
-
stickySet?.has(planned.index) === true,
|
|
1214
|
-
),
|
|
1215
|
-
);
|
|
1216
|
-
}
|
|
1217
|
-
this.windowCells = cells;
|
|
1218
|
-
// Swap, don't prune: a cell that left the window is simply absent from the pass that just ran,
|
|
1219
|
-
// so the old map is the garbage and the new one is exactly the live set.
|
|
1220
|
-
this.cellContexts = this.nextCellContexts;
|
|
1221
|
-
this.nextCellContexts = new Map<string, IVListItemContext<ItemT>>();
|
|
1222
|
-
|
|
315
|
+
this.applySpacers(plan);
|
|
316
|
+
this.buildCells(plan, stickySet);
|
|
1223
317
|
dlog(
|
|
1224
318
|
`Angular VirtualizedList window [${m.first}, ${m.last}] of ${m.count} ` +
|
|
1225
|
-
`(offset=${this.listState.scrollOffset}, viewport=${this.listState.viewportLength}, rendered=${
|
|
319
|
+
`(offset=${this.listState.scrollOffset}, viewport=${this.listState.viewportLength}, rendered=${this.windowCells.length})`,
|
|
1226
320
|
);
|
|
1227
321
|
}
|
|
1228
322
|
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
: undefined,
|
|
1246
|
-
isSticky,
|
|
1247
|
-
};
|
|
323
|
+
private applyStyles(total: number): void {
|
|
324
|
+
const styles = resolveListStyles({
|
|
325
|
+
isHorizontal: this.isHorizontal,
|
|
326
|
+
isInverted: this.isInverted,
|
|
327
|
+
total,
|
|
328
|
+
hasHeader: this.headerDir !== undefined,
|
|
329
|
+
style: this.style,
|
|
330
|
+
contentContainerStyle: this.contentContainerStyle,
|
|
331
|
+
anchorStyle: anchorHostStyle(this.elementRef),
|
|
332
|
+
maintainVisibleContentPosition: this.maintainVisibleContentPosition,
|
|
333
|
+
});
|
|
334
|
+
this.resolvedStyle = styles.style;
|
|
335
|
+
this.resolvedContentContainerStyle = styles.contentContainerStyle;
|
|
336
|
+
this.cellStyle = styles.cellStyle;
|
|
337
|
+
this.resolvedMaintainVisibleContentPosition =
|
|
338
|
+
styles.maintainVisibleContentPosition;
|
|
1248
339
|
}
|
|
1249
340
|
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
cached.index === index
|
|
1261
|
-
) {
|
|
1262
|
-
this.nextCellContexts.set(key, cached);
|
|
1263
|
-
return cached;
|
|
1264
|
-
}
|
|
1265
|
-
const context: IVListItemContext<ItemT> = {
|
|
1266
|
-
$implicit: item,
|
|
1267
|
-
index,
|
|
1268
|
-
separators: this.makeSeparators(index),
|
|
1269
|
-
};
|
|
1270
|
-
this.nextCellContexts.set(key, context);
|
|
1271
|
-
return context;
|
|
341
|
+
private showEmpty(): void {
|
|
342
|
+
this.cells.clear();
|
|
343
|
+
this.windowCells = [];
|
|
344
|
+
this.forcedStickyCell = null;
|
|
345
|
+
this.leadingSpacerStyle = null;
|
|
346
|
+
this.gapSpacerStyle = null;
|
|
347
|
+
this.trailingSpacerStyle = null;
|
|
348
|
+
dlog(
|
|
349
|
+
`Angular VirtualizedList empty (viewport=${this.listState.viewportLength})`,
|
|
350
|
+
);
|
|
1272
351
|
}
|
|
1273
352
|
|
|
1274
|
-
private
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
const overrides = this.separatorOverrides.get(index);
|
|
1279
|
-
const highlighted = overrides?.highlighted ?? false;
|
|
1280
|
-
const context: IVListSeparatorContext<ItemT> = {
|
|
1281
|
-
$implicit: highlighted,
|
|
1282
|
-
highlighted,
|
|
1283
|
-
leadingItem: item,
|
|
1284
|
-
trailingItem: this.getItem(this.data, index + 1),
|
|
1285
|
-
};
|
|
1286
|
-
// Fold any custom updateProps keys through the context's index signature (RN lets a row drive
|
|
1287
|
-
// arbitrary separator props); the typed fields above stay authoritative.
|
|
1288
|
-
if (overrides !== undefined) {
|
|
1289
|
-
for (const key of Object.keys(overrides)) {
|
|
1290
|
-
if (
|
|
1291
|
-
key === 'highlighted' ||
|
|
1292
|
-
key === 'leadingItem' ||
|
|
1293
|
-
key === 'trailingItem'
|
|
1294
|
-
)
|
|
1295
|
-
continue;
|
|
1296
|
-
context[key] = overrides[key];
|
|
1297
|
-
}
|
|
1298
|
-
}
|
|
1299
|
-
return context;
|
|
353
|
+
private applySpacers(plan: IListPlan): void {
|
|
354
|
+
this.leadingSpacerStyle = this.spacerStyle(plan.leadingExtent);
|
|
355
|
+
this.gapSpacerStyle = this.spacerStyle(plan.gapExtent);
|
|
356
|
+
this.trailingSpacerStyle = this.spacerStyle(plan.trailingExtent);
|
|
1300
357
|
}
|
|
1301
358
|
|
|
1302
|
-
private spacerStyle(extent: number): IViewStyle {
|
|
359
|
+
private spacerStyle(extent: number): IViewStyle | null {
|
|
360
|
+
if (extent <= EMPTY_OFFSET) return null;
|
|
1303
361
|
return this.isHorizontal ? { width: extent } : { height: extent };
|
|
1304
362
|
}
|
|
1305
363
|
|
|
1306
|
-
private
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
`
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
364
|
+
private buildCells(
|
|
365
|
+
plan: IListPlan,
|
|
366
|
+
stickySet: Set<number> | undefined,
|
|
367
|
+
): void {
|
|
368
|
+
const count = this.listState.metrics.count;
|
|
369
|
+
const forced = plan.forcedStickyCell;
|
|
370
|
+
dlog(
|
|
371
|
+
`STICKY[list] stickySet=${stickySet === undefined ? 'undefined' : JSON.stringify([...stickySet])} ` +
|
|
372
|
+
`childPositions=${JSON.stringify(plan.stickyChildPositions)} ` +
|
|
373
|
+
`forcedStickyCell=${forced === undefined ? 'none' : forced.index}`,
|
|
374
|
+
);
|
|
375
|
+
// Sticky by construction: a forced cell exists only for an index in `stickySet`
|
|
376
|
+
this.forcedStickyCell =
|
|
377
|
+
forced !== undefined
|
|
378
|
+
? this.cells.windowCell(forced.index, forced.key, false, true)
|
|
379
|
+
: null;
|
|
380
|
+
const hasSeparators = this.separatorTemplate !== undefined;
|
|
381
|
+
// The separator gates on the last index of the data, not of the window, or a cell's height
|
|
382
|
+
// would shift as it slides past
|
|
383
|
+
this.windowCells = plan.cells.map(planned =>
|
|
384
|
+
this.cells.windowCell(
|
|
385
|
+
planned.index,
|
|
386
|
+
planned.key,
|
|
387
|
+
hasSeparators && planned.index < count - 1,
|
|
388
|
+
stickySet?.has(planned.index) === true,
|
|
389
|
+
),
|
|
390
|
+
);
|
|
391
|
+
this.cells.endPass();
|
|
1322
392
|
}
|
|
1323
393
|
|
|
1324
|
-
//
|
|
1325
|
-
// [onLayout] property binding (NG8002). $event arrives untyped, narrowed before reaching measure.
|
|
394
|
+
// Angular forbids an `[onLayout]` binding, so the event arrives untyped and is narrowed here
|
|
1326
395
|
handleCellLayout(
|
|
1327
396
|
measure: (event: ISymbioteEvent) => void,
|
|
1328
397
|
event: unknown,
|
|
@@ -1336,111 +405,6 @@ export class VirtualizedList<ItemT = unknown>
|
|
|
1336
405
|
);
|
|
1337
406
|
}
|
|
1338
407
|
|
|
1339
|
-
private mergeSeparator(
|
|
1340
|
-
gapIndex: number,
|
|
1341
|
-
patch: Partial<ISeparatorProps<unknown>>,
|
|
1342
|
-
): void {
|
|
1343
|
-
const count = this.listState.metrics.count;
|
|
1344
|
-
if (!isSeparatorGapInRange(gapIndex, count)) return;
|
|
1345
|
-
this.separatorOverrides.set(gapIndex, {
|
|
1346
|
-
...this.separatorOverrides.get(gapIndex),
|
|
1347
|
-
...patch,
|
|
1348
|
-
});
|
|
1349
|
-
this.cdr.markForCheck();
|
|
1350
|
-
}
|
|
1351
|
-
|
|
1352
|
-
private makeSeparators(index: number): ISeparators {
|
|
1353
|
-
return {
|
|
1354
|
-
highlight: (): void => {
|
|
1355
|
-
dlog(`Angular VirtualizedList separator highlight cell=${index}`);
|
|
1356
|
-
this.mergeSeparator(index - 1, { highlighted: true });
|
|
1357
|
-
this.mergeSeparator(index, { highlighted: true });
|
|
1358
|
-
},
|
|
1359
|
-
unhighlight: (): void => {
|
|
1360
|
-
dlog(`Angular VirtualizedList separator unhighlight cell=${index}`);
|
|
1361
|
-
this.mergeSeparator(index - 1, { highlighted: false });
|
|
1362
|
-
this.mergeSeparator(index, { highlighted: false });
|
|
1363
|
-
},
|
|
1364
|
-
updateProps: (
|
|
1365
|
-
select: 'leading' | 'trailing',
|
|
1366
|
-
newProps: Record<string, unknown>,
|
|
1367
|
-
): void => {
|
|
1368
|
-
this.mergeSeparator(select === 'leading' ? index - 1 : index, newProps);
|
|
1369
|
-
},
|
|
1370
|
-
};
|
|
1371
|
-
}
|
|
1372
|
-
|
|
1373
|
-
// ---- imperative handle (the shared IVirtualizedListHandle surface) ----
|
|
1374
|
-
// Each scroll resolves to an offset (or a scroll-to-index failure) inside the reducer, then rides
|
|
1375
|
-
// the scroll-to effect through scrollToPixel.
|
|
1376
|
-
|
|
1377
|
-
scrollToOffset(params: { offset: number; animated?: boolean }): void {
|
|
1378
|
-
this.dispatch({
|
|
1379
|
-
kind: 'scroll-to-offset',
|
|
1380
|
-
offset: params.offset,
|
|
1381
|
-
animated: params.animated ?? true,
|
|
1382
|
-
});
|
|
1383
|
-
}
|
|
1384
|
-
|
|
1385
|
-
scrollToIndex(params: {
|
|
1386
|
-
index: number;
|
|
1387
|
-
animated?: boolean;
|
|
1388
|
-
viewOffset?: number;
|
|
1389
|
-
viewPosition?: number;
|
|
1390
|
-
}): void {
|
|
1391
|
-
this.dispatch({
|
|
1392
|
-
kind: 'scroll-to-index',
|
|
1393
|
-
index: params.index,
|
|
1394
|
-
animated: params.animated ?? true,
|
|
1395
|
-
viewPosition: params.viewPosition ?? FIRST_INDEX,
|
|
1396
|
-
viewOffset: params.viewOffset ?? EMPTY_OFFSET,
|
|
1397
|
-
});
|
|
1398
|
-
}
|
|
1399
|
-
|
|
1400
|
-
scrollToItem(params: {
|
|
1401
|
-
item: unknown;
|
|
1402
|
-
animated?: boolean;
|
|
1403
|
-
viewPosition?: number;
|
|
1404
|
-
}): void {
|
|
1405
|
-
this.dispatch({
|
|
1406
|
-
kind: 'scroll-to-item',
|
|
1407
|
-
item: params.item,
|
|
1408
|
-
animated: params.animated ?? true,
|
|
1409
|
-
viewPosition: params.viewPosition ?? FIRST_INDEX,
|
|
1410
|
-
});
|
|
1411
|
-
}
|
|
1412
|
-
|
|
1413
|
-
scrollToEnd(params?: { animated?: boolean }): void {
|
|
1414
|
-
this.dispatch({
|
|
1415
|
-
kind: 'scroll-to-end',
|
|
1416
|
-
animated: params?.animated ?? true,
|
|
1417
|
-
});
|
|
1418
|
-
}
|
|
1419
|
-
|
|
1420
|
-
flashScrollIndicators(): void {
|
|
1421
|
-
this.scrollHandle.flashScrollIndicators();
|
|
1422
|
-
}
|
|
1423
|
-
|
|
1424
|
-
getNativeScrollRef(): IScrollViewHandle | null {
|
|
1425
|
-
return this.scrollNode !== null ? this.scrollHandle : null;
|
|
1426
|
-
}
|
|
1427
|
-
|
|
1428
|
-
getScrollableNode(): IScrollViewHandle | null {
|
|
1429
|
-
return this.scrollNode !== null ? this.scrollHandle : null;
|
|
1430
|
-
}
|
|
1431
|
-
|
|
1432
|
-
getScrollResponder(): IScrollViewHandle | null {
|
|
1433
|
-
return this.scrollNode !== null ? this.scrollHandle : null;
|
|
1434
|
-
}
|
|
1435
|
-
|
|
1436
|
-
getScrollNode(): ISymbioteNode | null {
|
|
1437
|
-
return this.scrollHandle.getScrollNode();
|
|
1438
|
-
}
|
|
1439
|
-
|
|
1440
|
-
recordInteraction(): void {
|
|
1441
|
-
this.dispatch({ kind: 'record-interaction' });
|
|
1442
|
-
}
|
|
1443
|
-
|
|
1444
408
|
private scrollToPixel(offset: number, animated: boolean): void {
|
|
1445
409
|
const clamped = Math.max(EMPTY_OFFSET, offset);
|
|
1446
410
|
const target = this.isHorizontal
|