@symbiote-native/react 0.1.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/LICENSE +21 -0
- package/README.md +149 -0
- package/build/components/activity-indicator/index.android.d.ts +2 -0
- package/build/components/activity-indicator/index.android.js +13 -0
- package/build/components/activity-indicator/index.d.ts +1 -0
- package/build/components/activity-indicator/index.ios.d.ts +2 -0
- package/build/components/activity-indicator/index.ios.js +10 -0
- package/build/components/activity-indicator/index.js +4 -0
- package/build/components/activity-indicator/shared.d.ts +7 -0
- package/build/components/activity-indicator/shared.js +20 -0
- package/build/components/button.d.ts +6 -0
- package/build/components/button.js +42 -0
- package/build/components/drawer-layout-android/index.android.d.ts +3 -0
- package/build/components/drawer-layout-android/index.android.js +93 -0
- package/build/components/drawer-layout-android/index.d.ts +3 -0
- package/build/components/drawer-layout-android/index.js +19 -0
- package/build/components/drawer-layout-android/shared.d.ts +19 -0
- package/build/components/drawer-layout-android/shared.js +7 -0
- package/build/components/flat-list/index.d.ts +73 -0
- package/build/components/flat-list/index.js +86 -0
- package/build/components/image/index.d.ts +9 -0
- package/build/components/image/index.js +28 -0
- package/build/components/image-background/index.d.ts +10 -0
- package/build/components/image-background/index.js +46 -0
- package/build/components/input-accessory-view/index.d.ts +11 -0
- package/build/components/input-accessory-view/index.js +14 -0
- package/build/components/keyboard-avoiding-view/index.d.ts +17 -0
- package/build/components/keyboard-avoiding-view/index.js +85 -0
- package/build/components/modal/index.d.ts +24 -0
- package/build/components/modal/index.js +56 -0
- package/build/components/pressable/index.d.ts +30 -0
- package/build/components/pressable/index.js +100 -0
- package/build/components/refresh-control/index.d.ts +17 -0
- package/build/components/refresh-control/index.js +21 -0
- package/build/components/safe-area-view/index.d.ts +11 -0
- package/build/components/safe-area-view/index.js +18 -0
- package/build/components/scroll-view/index.android.d.ts +3 -0
- package/build/components/scroll-view/index.android.js +53 -0
- package/build/components/scroll-view/index.d.ts +1 -0
- package/build/components/scroll-view/index.ios.d.ts +3 -0
- package/build/components/scroll-view/index.ios.js +27 -0
- package/build/components/scroll-view/index.js +4 -0
- package/build/components/scroll-view/shared.d.ts +98 -0
- package/build/components/scroll-view/shared.js +195 -0
- package/build/components/scroll-view/sticky-header.d.ts +9 -0
- package/build/components/scroll-view/sticky-header.js +161 -0
- package/build/components/section-list/index.d.ts +8 -0
- package/build/components/section-list/index.js +12 -0
- package/build/components/switch/index.android.d.ts +2 -0
- package/build/components/switch/index.android.js +12 -0
- package/build/components/switch/index.d.ts +1 -0
- package/build/components/switch/index.ios.d.ts +2 -0
- package/build/components/switch/index.ios.js +10 -0
- package/build/components/switch/index.js +6 -0
- package/build/components/switch/shared.d.ts +10 -0
- package/build/components/switch/shared.js +59 -0
- package/build/components/text-input/index.d.ts +50 -0
- package/build/components/text-input/index.js +167 -0
- package/build/components/touchable/index.d.ts +20 -0
- package/build/components/touchable/index.js +95 -0
- package/build/components/touchable-native-feedback/index.d.ts +18 -0
- package/build/components/touchable-native-feedback/index.js +32 -0
- package/build/components/virtualized-list/index.d.ts +73 -0
- package/build/components/virtualized-list/index.js +552 -0
- package/build/components/virtualized-section-list/index.d.ts +66 -0
- package/build/components/virtualized-section-list/index.js +99 -0
- package/build/components.d.ts +54 -0
- package/build/components.js +17 -0
- package/build/create-portal.d.ts +4 -0
- package/build/create-portal.js +23 -0
- package/build/create-tunnel.d.ts +12 -0
- package/build/create-tunnel.js +60 -0
- package/build/descriptor-to-react/index.d.ts +3 -0
- package/build/descriptor-to-react/index.js +15 -0
- package/build/hooks/use-color-scheme.d.ts +2 -0
- package/build/hooks/use-color-scheme.js +13 -0
- package/build/hooks/use-window-dimensions.d.ts +2 -0
- package/build/hooks/use-window-dimensions.js +30 -0
- package/build/host-config.d.ts +7 -0
- package/build/host-config.js +169 -0
- package/build/host-instance.d.ts +3 -0
- package/build/host-instance.js +20 -0
- package/build/index.d.ts +98 -0
- package/build/index.js +85 -0
- package/build/modules/accessibility-info/index.android.d.ts +2 -0
- package/build/modules/accessibility-info/index.android.js +4 -0
- package/build/modules/accessibility-info/index.d.ts +1 -0
- package/build/modules/accessibility-info/index.ios.d.ts +2 -0
- package/build/modules/accessibility-info/index.ios.js +3 -0
- package/build/modules/accessibility-info/index.js +5 -0
- package/build/modules/accessibility-info/shared.d.ts +1 -0
- package/build/modules/accessibility-info/shared.js +1 -0
- package/build/modules/action-sheet-ios.d.ts +2 -0
- package/build/modules/action-sheet-ios.js +4 -0
- package/build/modules/alert.d.ts +2 -0
- package/build/modules/alert.js +5 -0
- package/build/modules/animated/create-animated-component.d.ts +12 -0
- package/build/modules/animated/create-animated-component.js +125 -0
- package/build/modules/animated/index.d.ts +52 -0
- package/build/modules/animated/index.js +84 -0
- package/build/modules/app-registry/index.d.ts +7 -0
- package/build/modules/app-registry/index.js +26 -0
- package/build/modules/app-state.d.ts +2 -0
- package/build/modules/app-state.js +3 -0
- package/build/modules/appearance.d.ts +2 -0
- package/build/modules/appearance.js +3 -0
- package/build/modules/back-handler.d.ts +2 -0
- package/build/modules/back-handler.js +3 -0
- package/build/modules/dimensions.d.ts +2 -0
- package/build/modules/dimensions.js +3 -0
- package/build/modules/i18n-manager.d.ts +2 -0
- package/build/modules/i18n-manager.js +4 -0
- package/build/modules/keyboard.d.ts +2 -0
- package/build/modules/keyboard.js +3 -0
- package/build/modules/layout-animation.d.ts +2 -0
- package/build/modules/layout-animation.js +3 -0
- package/build/modules/linking.d.ts +2 -0
- package/build/modules/linking.js +4 -0
- package/build/modules/permissions-android.d.ts +2 -0
- package/build/modules/permissions-android.js +3 -0
- package/build/modules/settings.d.ts +1 -0
- package/build/modules/settings.js +4 -0
- package/build/modules/share.d.ts +2 -0
- package/build/modules/share.js +4 -0
- package/build/modules/status-bar/index.android.d.ts +3 -0
- package/build/modules/status-bar/index.android.js +89 -0
- package/build/modules/status-bar/index.d.ts +1 -0
- package/build/modules/status-bar/index.ios.d.ts +3 -0
- package/build/modules/status-bar/index.ios.js +64 -0
- package/build/modules/status-bar/index.js +4 -0
- package/build/modules/status-bar/shared.d.ts +6 -0
- package/build/modules/status-bar/shared.js +6 -0
- package/build/modules/text-input-state.d.ts +1 -0
- package/build/modules/text-input-state.js +3 -0
- package/build/modules/toast-android.d.ts +1 -0
- package/build/modules/toast-android.js +4 -0
- package/build/modules/vibration.d.ts +1 -0
- package/build/modules/vibration.js +4 -0
- package/build/reconciler-constants.d.ts +7 -0
- package/build/reconciler-constants.js +12 -0
- package/build/render.d.ts +7 -0
- package/build/render.js +60 -0
- package/build/utils/responder-props.d.ts +1 -0
- package/build/utils/responder-props.js +1 -0
- package/build/utils/styles.d.ts +1 -0
- package/build/utils/styles.js +1 -0
- package/metro-css-parser.cjs +11 -0
- package/package.json +53 -0
|
@@ -0,0 +1,552 @@
|
|
|
1
|
+
// VirtualizedList: real windowing over the existing ScrollView. Only the cells
|
|
2
|
+
// whose computed offset falls inside the visible window (plus a leading/trailing
|
|
3
|
+
// buffer) are rendered; everything above and below is collapsed into two spacer
|
|
4
|
+
// Views whose sizes sum to the off-screen extent, so the scroll thumb and total
|
|
5
|
+
// content size stay correct without mounting all N rows.
|
|
6
|
+
//
|
|
7
|
+
// The windowing engine (offset table, window compute, batch throttle, viewability,
|
|
8
|
+
// edge-reached, the child PLAN, the imperative-handle surface) lives in
|
|
9
|
+
// @symbiote-native/components/state, shared verbatim with the Vue adapter — a windowing or
|
|
10
|
+
// viewability bug is fixed once for all adapters (<adapters_reach_full_feature_parity>).
|
|
11
|
+
// React supplies only its lifecycle (state/refs/effects), the imperative-handle wiring,
|
|
12
|
+
// and the per-cell element creation (createElement). Lists have no Descriptor render fn
|
|
13
|
+
// (the cell content is React's own children); see core/components/.docs-note-lists.md.
|
|
14
|
+
//
|
|
15
|
+
// Imperative scrolling (scrollToIndex / scrollToOffset / scrollToItem / scrollToEnd)
|
|
16
|
+
// resolves to an offset and rides the ScrollView's native scrollTo command via its handle
|
|
17
|
+
// ref, animated by default. The `contentOffset` prop is only a fallback for the pre-mount
|
|
18
|
+
// window (handle not yet attached).
|
|
19
|
+
import { createElement, useCallback, useEffect, useImperativeHandle, useLayoutEffect, useMemo, useRef, useState, } from 'react';
|
|
20
|
+
import { dlog } from '@symbiote-native/engine';
|
|
21
|
+
import { DEFAULT_END_REACHED_THRESHOLD, DEFAULT_INITIAL_NUM_TO_RENDER, DEFAULT_MAX_TO_RENDER_PER_BATCH, DEFAULT_START_REACHED_THRESHOLD, DEFAULT_UPDATE_CELLS_BATCHING_PERIOD, DEFAULT_WINDOW_SIZE, EMPTY_OFFSET, FIRST_INDEX, INVERTED_X_STYLE, INVERTED_Y_STYLE, NO_CONTENT_LENGTH_SENT, averageMeasuredLength, buildListPlan, buildOffsets, buildViewabilityPairs, computeEndReached, computeStartReached, computeViewableSet, computeWindow, diffViewable, highestMeasuredIndex as findHighestMeasuredIndex, maxMinimumViewTime, offsetForIndex as resolveOffsetForIndex, readLayoutLength, readScrollOffset, throttleWindow, } from '@symbiote-native/components';
|
|
22
|
+
import { ScrollView } from '../scroll-view';
|
|
23
|
+
import { RefreshControl } from '../refresh-control';
|
|
24
|
+
function resolveElement(component) {
|
|
25
|
+
if (component === undefined)
|
|
26
|
+
return undefined;
|
|
27
|
+
if (typeof component === 'function')
|
|
28
|
+
return createElement(component, {});
|
|
29
|
+
return component;
|
|
30
|
+
}
|
|
31
|
+
// Build the ItemSeparatorComponent element for the gap between leadingItem and trailingItem, with
|
|
32
|
+
// the highlight flag and any handle-pushed overrides merged on top (RN renders
|
|
33
|
+
// `<ItemSeparatorComponent {...separatorProps} />`). A bare element is returned as-is.
|
|
34
|
+
function renderSeparatorElement(component, leadingItem, trailingItem, overrides) {
|
|
35
|
+
if (component === undefined)
|
|
36
|
+
return undefined;
|
|
37
|
+
const props = {
|
|
38
|
+
highlighted: false,
|
|
39
|
+
leadingItem,
|
|
40
|
+
trailingItem,
|
|
41
|
+
...overrides,
|
|
42
|
+
};
|
|
43
|
+
return createElement(component, props);
|
|
44
|
+
}
|
|
45
|
+
// React 19 passes `ref` as a regular prop, so a generic function component can
|
|
46
|
+
// expose an imperative handle without forwardRef (which erases the ItemT
|
|
47
|
+
// generic). The ref is destructured here and wired through useImperativeHandle.
|
|
48
|
+
export function VirtualizedList(props) {
|
|
49
|
+
const forwardedRef = props.ref;
|
|
50
|
+
const { data, getItem, getItemCount, renderItem, keyExtractor, getItemLayout, ItemSeparatorComponent, ListHeaderComponent, ListFooterComponent, ListEmptyComponent, horizontal = false, inverted = false, extraData, onEndReached, onEndReachedThreshold = DEFAULT_END_REACHED_THRESHOLD, onStartReached, onStartReachedThreshold = DEFAULT_START_REACHED_THRESHOLD, onRefresh, refreshing, progressViewOffset, onViewableItemsChanged, viewabilityConfig, viewabilityConfigCallbackPairs, onScrollToIndexFailed, initialNumToRender = DEFAULT_INITIAL_NUM_TO_RENDER, initialScrollIndex, maxToRenderPerBatch = DEFAULT_MAX_TO_RENDER_PER_BATCH, updateCellsBatchingPeriod = DEFAULT_UPDATE_CELLS_BATCHING_PERIOD, windowSize = DEFAULT_WINDOW_SIZE, stickyHeaderIndices, maintainVisibleContentPosition, style, contentContainerStyle,
|
|
51
|
+
// Pulled out of the rest so the user's onScroll does NOT arrive raw via
|
|
52
|
+
// ...accessibilityRest and overwrite the internal windowing handler. Composed below.
|
|
53
|
+
onScroll: userOnScroll, onScrollBeginDrag, onScrollEndDrag, onMomentumScrollBegin, onMomentumScrollEnd, scrollEventThrottle, keyboardShouldPersistTaps, keyboardDismissMode,
|
|
54
|
+
// The accessibility surface rides down to the underlying ScrollView, which runs
|
|
55
|
+
// resolveAccessibilityProps itself; ref is pulled separately above, re-listed here only
|
|
56
|
+
// to keep it out of the forwarded rest.
|
|
57
|
+
ref: _ref, ...accessibilityRest } = props;
|
|
58
|
+
const count = getItemCount(data);
|
|
59
|
+
const [scrollOffset, setScrollOffset] = useState(EMPTY_OFFSET);
|
|
60
|
+
const [viewportLength, setViewportLength] = useState(EMPTY_OFFSET);
|
|
61
|
+
// The offset we are imperatively driving native to (scrollTo*). Pushed down as the
|
|
62
|
+
// ScrollView's contentOffset prop; fresh object identity each time. undefined = none pending.
|
|
63
|
+
const [commandedOffset, setCommandedOffset] = useState(undefined);
|
|
64
|
+
const scrollViewRef = useRef(null);
|
|
65
|
+
// Measured cell lengths by index. A ref-backed Map mutated in place plus a version counter to
|
|
66
|
+
// request a re-render only when a NEW measurement lands.
|
|
67
|
+
const measuredRef = useRef(new Map());
|
|
68
|
+
const [, setMeasureVersion] = useState(EMPTY_OFFSET);
|
|
69
|
+
// The content length we last fired onEndReached / onStartReached for (RN's
|
|
70
|
+
// _sentEndForContentLength / _sentStartForContentLength): dedup by content length, re-armed
|
|
71
|
+
// on scroll away from the edge.
|
|
72
|
+
const sentEndForContentLengthRef = useRef(NO_CONTENT_LENGTH_SENT);
|
|
73
|
+
const sentStartForContentLengthRef = useRef(NO_CONTENT_LENGTH_SENT);
|
|
74
|
+
// The previously committed window, so throttleWindow can grow it by at most
|
|
75
|
+
// maxToRenderPerBatch per batch tick instead of snapping.
|
|
76
|
+
const committedWindowRef = useRef({
|
|
77
|
+
first: FIRST_INDEX,
|
|
78
|
+
last: -1,
|
|
79
|
+
});
|
|
80
|
+
// The tokens reported viewable on the last onViewableItemsChanged, keyed by cell key.
|
|
81
|
+
const lastViewableRef = useRef(new Map());
|
|
82
|
+
// Pending minimumViewTime debounce timer (RN ViewabilityHelper._timers). null = no timer.
|
|
83
|
+
const viewableTimerRef = useRef(null);
|
|
84
|
+
// Flips true on the first scroll (RN's ViewabilityHelper._hasInteracted).
|
|
85
|
+
const hasInteractedRef = useRef(false);
|
|
86
|
+
// Per-gap separator overrides, keyed by the LEADING cell index of the gap.
|
|
87
|
+
const separatorOverridesRef = useRef(new Map());
|
|
88
|
+
const [, setSeparatorVersion] = useState(EMPTY_OFFSET);
|
|
89
|
+
// initialScrollIndex is applied once, after the first layout gives a viewport.
|
|
90
|
+
const appliedInitialScrollRef = useRef(false);
|
|
91
|
+
// maintainVisibleContentPosition anchor tracking (RN's State.firstVisibleItemKey).
|
|
92
|
+
const firstVisibleKeyRef = useRef(null);
|
|
93
|
+
const fixedLayout = useMemo(() => {
|
|
94
|
+
if (getItemLayout === undefined)
|
|
95
|
+
return undefined;
|
|
96
|
+
return (index) => {
|
|
97
|
+
const layout = getItemLayout(data, index);
|
|
98
|
+
return { length: layout.length, offset: layout.offset };
|
|
99
|
+
};
|
|
100
|
+
}, [getItemLayout, data]);
|
|
101
|
+
// Running average of known cell lengths, used to size not-yet-measured cells and the trailing
|
|
102
|
+
// spacer so the total is plausible before full measurement.
|
|
103
|
+
const averageLength = useMemo(() => {
|
|
104
|
+
if (fixedLayout)
|
|
105
|
+
return fixedLayout(FIRST_INDEX).length;
|
|
106
|
+
return averageMeasuredLength(measuredRef.current);
|
|
107
|
+
}, [fixedLayout, scrollOffset, viewportLength]);
|
|
108
|
+
const { offsets, lengths, total } = buildOffsets(count, measuredRef.current, fixedLayout, averageLength);
|
|
109
|
+
const targetWindow = computeWindow(count, offsets, lengths, scrollOffset, viewportLength, windowSize, initialNumToRender);
|
|
110
|
+
const { first, last } = throttleWindow(targetWindow, committedWindowRef.current, maxToRenderPerBatch);
|
|
111
|
+
committedWindowRef.current = { first, last };
|
|
112
|
+
dlog(`VirtualizedList window [${first}, ${last}] of ${count} ` +
|
|
113
|
+
`(offset=${scrollOffset}, viewport=${viewportLength}, rendered=${Math.max(0, last - first + 1)})`);
|
|
114
|
+
// When the throttled window has not yet reached the target, schedule another render after the
|
|
115
|
+
// batching period so the window keeps filling toward target.
|
|
116
|
+
useEffect(() => {
|
|
117
|
+
if (first <= targetWindow.first && last >= targetWindow.last)
|
|
118
|
+
return;
|
|
119
|
+
const timer = setTimeout(() => {
|
|
120
|
+
setMeasureVersion(version => version + 1);
|
|
121
|
+
}, updateCellsBatchingPeriod);
|
|
122
|
+
return () => clearTimeout(timer);
|
|
123
|
+
}, [first, last, targetWindow.first, targetWindow.last, updateCellsBatchingPeriod]);
|
|
124
|
+
const onScroll = useCallback((event) => {
|
|
125
|
+
const offset = readScrollOffset(event, horizontal);
|
|
126
|
+
if (offset === undefined)
|
|
127
|
+
return;
|
|
128
|
+
dlog(`VirtualizedList onScroll offset=${offset}`);
|
|
129
|
+
// First scroll is the interaction that ungates waitForInteraction configs.
|
|
130
|
+
hasInteractedRef.current = true;
|
|
131
|
+
setScrollOffset(offset);
|
|
132
|
+
// A real user/native scroll supersedes any pending commanded offset.
|
|
133
|
+
setCommandedOffset(undefined);
|
|
134
|
+
// Compose, don't clobber: internal windowing ran first, now the user's onScroll.
|
|
135
|
+
if (userOnScroll !== undefined)
|
|
136
|
+
userOnScroll(event);
|
|
137
|
+
}, [horizontal, userOnScroll]);
|
|
138
|
+
// onEndReached gating (RN _maybeCallOnEdgeReached). Run against the COMMITTED window. Fire only
|
|
139
|
+
// when the last cell is rendered AND within threshold; dedup by content length; re-arm on
|
|
140
|
+
// scroll away from the end.
|
|
141
|
+
useEffect(() => {
|
|
142
|
+
if (onEndReached === undefined || viewportLength <= EMPTY_OFFSET)
|
|
143
|
+
return;
|
|
144
|
+
const { distanceFromEnd, withinThreshold } = computeEndReached(total, scrollOffset, viewportLength, onEndReachedThreshold);
|
|
145
|
+
const lastCellRendered = last === count - 1;
|
|
146
|
+
if (withinThreshold && lastCellRendered && sentEndForContentLengthRef.current !== total) {
|
|
147
|
+
sentEndForContentLengthRef.current = total;
|
|
148
|
+
dlog(`VirtualizedList onEndReached distanceFromEnd=${distanceFromEnd} ` +
|
|
149
|
+
`(last=${last} of ${count}, contentLength=${total})`);
|
|
150
|
+
onEndReached({ distanceFromEnd });
|
|
151
|
+
}
|
|
152
|
+
if (!withinThreshold) {
|
|
153
|
+
sentEndForContentLengthRef.current = NO_CONTENT_LENGTH_SENT;
|
|
154
|
+
}
|
|
155
|
+
}, [onEndReached, onEndReachedThreshold, viewportLength, scrollOffset, total, last, count]);
|
|
156
|
+
// onStartReached gating: the top-edge twin of the onEndReached effect.
|
|
157
|
+
useEffect(() => {
|
|
158
|
+
if (onStartReached === undefined || viewportLength <= EMPTY_OFFSET)
|
|
159
|
+
return;
|
|
160
|
+
const { distanceFromStart, withinThreshold } = computeStartReached(scrollOffset, viewportLength, onStartReachedThreshold);
|
|
161
|
+
const firstCellRendered = first === FIRST_INDEX;
|
|
162
|
+
if (withinThreshold && firstCellRendered && sentStartForContentLengthRef.current !== total) {
|
|
163
|
+
sentStartForContentLengthRef.current = total;
|
|
164
|
+
dlog(`VirtualizedList onStartReached distanceFromStart=${distanceFromStart} ` +
|
|
165
|
+
`(first=${first}, contentLength=${total})`);
|
|
166
|
+
onStartReached({ distanceFromStart });
|
|
167
|
+
}
|
|
168
|
+
if (!withinThreshold) {
|
|
169
|
+
sentStartForContentLengthRef.current = NO_CONTENT_LENGTH_SENT;
|
|
170
|
+
}
|
|
171
|
+
}, [onStartReached, onStartReachedThreshold, viewportLength, scrollOffset, total, first]);
|
|
172
|
+
// The single-config and pairs forms both feed one viewability pass (RN supports either).
|
|
173
|
+
const viewabilityPairs = useMemo(() => buildViewabilityPairs(onViewableItemsChanged, viewabilityConfig, viewabilityConfigCallbackPairs), [onViewableItemsChanged, viewabilityConfig, viewabilityConfigCallbackPairs]);
|
|
174
|
+
// Viewability detection: after each scroll/window change, recompute which rendered cells clear
|
|
175
|
+
// the threshold and, if the viewable set changed, fire onViewableItemsChanged + every pair.
|
|
176
|
+
useEffect(() => {
|
|
177
|
+
if (viewabilityPairs.length === EMPTY_OFFSET || viewportLength <= EMPTY_OFFSET)
|
|
178
|
+
return;
|
|
179
|
+
if (count === FIRST_INDEX)
|
|
180
|
+
return;
|
|
181
|
+
const { tokens, map } = computeViewableSet({
|
|
182
|
+
first,
|
|
183
|
+
last,
|
|
184
|
+
count,
|
|
185
|
+
offsets,
|
|
186
|
+
lengths,
|
|
187
|
+
scrollOffset,
|
|
188
|
+
viewportLength,
|
|
189
|
+
data,
|
|
190
|
+
getItem,
|
|
191
|
+
keyExtractor,
|
|
192
|
+
pairs: viewabilityPairs,
|
|
193
|
+
hasInteracted: hasInteractedRef.current,
|
|
194
|
+
});
|
|
195
|
+
const diff = diffViewable(lastViewableRef.current, map, tokens);
|
|
196
|
+
if (!diff.hasChanged)
|
|
197
|
+
return;
|
|
198
|
+
const commitAndFire = () => {
|
|
199
|
+
lastViewableRef.current = map;
|
|
200
|
+
dlog(`VirtualizedList viewable=${tokens.length} changed=${diff.changed.length} ` +
|
|
201
|
+
`(window [${first}, ${last}])`);
|
|
202
|
+
const info = {
|
|
203
|
+
viewableItems: tokens,
|
|
204
|
+
changed: diff.changed,
|
|
205
|
+
};
|
|
206
|
+
for (const pair of viewabilityPairs)
|
|
207
|
+
pair.onViewableItemsChanged(info);
|
|
208
|
+
};
|
|
209
|
+
const minimumViewTime = maxMinimumViewTime(viewabilityPairs);
|
|
210
|
+
if (viewableTimerRef.current !== null) {
|
|
211
|
+
clearTimeout(viewableTimerRef.current);
|
|
212
|
+
viewableTimerRef.current = null;
|
|
213
|
+
}
|
|
214
|
+
if (minimumViewTime > EMPTY_OFFSET) {
|
|
215
|
+
dlog(`VirtualizedList viewability debounce ${minimumViewTime}ms (window [${first}, ${last}])`);
|
|
216
|
+
viewableTimerRef.current = setTimeout(() => {
|
|
217
|
+
viewableTimerRef.current = null;
|
|
218
|
+
commitAndFire();
|
|
219
|
+
}, minimumViewTime);
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
commitAndFire();
|
|
223
|
+
}, [
|
|
224
|
+
viewabilityPairs,
|
|
225
|
+
viewportLength,
|
|
226
|
+
scrollOffset,
|
|
227
|
+
first,
|
|
228
|
+
last,
|
|
229
|
+
count,
|
|
230
|
+
data,
|
|
231
|
+
getItem,
|
|
232
|
+
keyExtractor,
|
|
233
|
+
offsets,
|
|
234
|
+
lengths,
|
|
235
|
+
]);
|
|
236
|
+
// Clear any pending minimumViewTime debounce on unmount (RN ViewabilityHelper.dispose).
|
|
237
|
+
useEffect(() => {
|
|
238
|
+
return () => {
|
|
239
|
+
if (viewableTimerRef.current !== null)
|
|
240
|
+
clearTimeout(viewableTimerRef.current);
|
|
241
|
+
};
|
|
242
|
+
}, []);
|
|
243
|
+
const onViewportLayout = useCallback((event) => {
|
|
244
|
+
const length = readLayoutLength(event, horizontal);
|
|
245
|
+
if (length === undefined)
|
|
246
|
+
return;
|
|
247
|
+
dlog(`VirtualizedList onLayout viewport=${length}`);
|
|
248
|
+
setViewportLength(length);
|
|
249
|
+
}, [horizontal]);
|
|
250
|
+
const makeCellMeasure = useCallback((index) => (event) => {
|
|
251
|
+
if (fixedLayout)
|
|
252
|
+
return;
|
|
253
|
+
const length = readLayoutLength(event, horizontal);
|
|
254
|
+
if (length === undefined)
|
|
255
|
+
return;
|
|
256
|
+
const measured = measuredRef.current;
|
|
257
|
+
if (measured.get(index) === length)
|
|
258
|
+
return;
|
|
259
|
+
measured.set(index, length);
|
|
260
|
+
dlog(`VirtualizedList cell ${index} measured length=${length}`);
|
|
261
|
+
setMeasureVersion(version => version + 1);
|
|
262
|
+
}, [fixedLayout, horizontal]);
|
|
263
|
+
// Merge an override onto the separator at a given gap and request a re-render. A gap index
|
|
264
|
+
// outside [0, count-2] has no separator, so the write is a no-op (RN bails the same way).
|
|
265
|
+
const mergeSeparator = useCallback((gapIndex, patch) => {
|
|
266
|
+
if (gapIndex < FIRST_INDEX || gapIndex > count - 2)
|
|
267
|
+
return;
|
|
268
|
+
const overrides = separatorOverridesRef.current;
|
|
269
|
+
overrides.set(gapIndex, { ...overrides.get(gapIndex), ...patch });
|
|
270
|
+
setSeparatorVersion(version => version + 1);
|
|
271
|
+
}, [count]);
|
|
272
|
+
// The ISeparators handle for the cell at `index` (RN CellRenderer._separators).
|
|
273
|
+
const makeSeparators = useCallback((index) => ({
|
|
274
|
+
highlight: () => {
|
|
275
|
+
dlog(`VirtualizedList separator highlight cell=${index}`);
|
|
276
|
+
mergeSeparator(index - 1, { highlighted: true });
|
|
277
|
+
mergeSeparator(index, { highlighted: true });
|
|
278
|
+
},
|
|
279
|
+
unhighlight: () => {
|
|
280
|
+
dlog(`VirtualizedList separator unhighlight cell=${index}`);
|
|
281
|
+
mergeSeparator(index - 1, { highlighted: false });
|
|
282
|
+
mergeSeparator(index, { highlighted: false });
|
|
283
|
+
},
|
|
284
|
+
updateProps: (select, newProps) => {
|
|
285
|
+
mergeSeparator(select === 'leading' ? index - 1 : index, newProps);
|
|
286
|
+
},
|
|
287
|
+
}), [mergeSeparator]);
|
|
288
|
+
// Resolve an index to a pixel offset (RN's scrollToIndex options), via the shared math.
|
|
289
|
+
const offsetForIndex = useCallback((index, viewPosition, viewOffset) => resolveOffsetForIndex(index, viewPosition, viewOffset, count, offsets, lengths, viewportLength), [count, offsets, lengths, viewportLength]);
|
|
290
|
+
const scrollToPixel = useCallback((offset, animated) => {
|
|
291
|
+
const clamped = Math.max(EMPTY_OFFSET, offset);
|
|
292
|
+
const target = horizontal ? { x: clamped, y: EMPTY_OFFSET } : { x: EMPTY_OFFSET, y: clamped };
|
|
293
|
+
// Both animated and instant scrolls ride the ScrollView's native scrollTo command, exactly
|
|
294
|
+
// like RN. The contentOffset prop path stays as a fallback for the pre-mount window.
|
|
295
|
+
if (scrollViewRef.current !== null) {
|
|
296
|
+
dlog(`VirtualizedList scrollTo offset=${clamped} animated=${animated} (horizontal=${horizontal})`);
|
|
297
|
+
scrollViewRef.current.scrollTo({ x: target.x, y: target.y, animated });
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
dlog(`VirtualizedList scrollTo offset=${clamped} pending-ref (horizontal=${horizontal})`);
|
|
301
|
+
setCommandedOffset(target);
|
|
302
|
+
}, [horizontal]);
|
|
303
|
+
// The largest index whose length we have actually measured (RN
|
|
304
|
+
// ListMetricsAggregator.getHighestMeasuredCellIndex). With getItemLayout this is irrelevant.
|
|
305
|
+
const highestMeasuredIndex = useCallback(() => findHighestMeasuredIndex(measuredRef.current), []);
|
|
306
|
+
useImperativeHandle(forwardedRef ?? null, () => ({
|
|
307
|
+
// RN animates every imperative scroll unless the caller passes animated: false.
|
|
308
|
+
scrollToOffset: (params) => {
|
|
309
|
+
scrollToPixel(params.offset, params.animated ?? true);
|
|
310
|
+
},
|
|
311
|
+
scrollToIndex: (params) => {
|
|
312
|
+
// No getItemLayout AND the target is past the last measured cell: report the failure
|
|
313
|
+
// instead of scrolling to a fabricated estimate (RN VirtualizedList.js:179-195).
|
|
314
|
+
if (fixedLayout === undefined && params.index > highestMeasuredIndex()) {
|
|
315
|
+
dlog(`VirtualizedList onScrollToIndexFailed index=${params.index} ` +
|
|
316
|
+
`highestMeasured=${highestMeasuredIndex()} (no getItemLayout)`);
|
|
317
|
+
onScrollToIndexFailed?.({
|
|
318
|
+
index: params.index,
|
|
319
|
+
highestMeasuredFrameIndex: highestMeasuredIndex(),
|
|
320
|
+
averageItemLength: averageLength,
|
|
321
|
+
});
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
scrollToPixel(offsetForIndex(params.index, params.viewPosition ?? FIRST_INDEX, params.viewOffset ?? EMPTY_OFFSET), params.animated ?? true);
|
|
325
|
+
},
|
|
326
|
+
scrollToItem: (params) => {
|
|
327
|
+
for (let index = FIRST_INDEX; index < count; index += 1) {
|
|
328
|
+
if (getItem(data, index) === params.item) {
|
|
329
|
+
scrollToPixel(offsetForIndex(index, params.viewPosition ?? FIRST_INDEX, EMPTY_OFFSET), params.animated ?? true);
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
dlog('VirtualizedList scrollToItem: item not found');
|
|
334
|
+
},
|
|
335
|
+
scrollToEnd: (params) => {
|
|
336
|
+
scrollToPixel(Math.max(EMPTY_OFFSET, total - viewportLength), params?.animated ?? true);
|
|
337
|
+
},
|
|
338
|
+
flashScrollIndicators: () => {
|
|
339
|
+
scrollViewRef.current?.flashScrollIndicators?.();
|
|
340
|
+
},
|
|
341
|
+
getNativeScrollRef: () => scrollViewRef.current,
|
|
342
|
+
getScrollableNode: () => scrollViewRef.current,
|
|
343
|
+
getScrollResponder: () => scrollViewRef.current,
|
|
344
|
+
getScrollNode: () => scrollViewRef.current?.getScrollNode() ?? null,
|
|
345
|
+
// Manual trigger for RN's recordInteraction: flip the interaction flag so
|
|
346
|
+
// waitForInteraction viewability configs start reporting.
|
|
347
|
+
recordInteraction: () => {
|
|
348
|
+
hasInteractedRef.current = true;
|
|
349
|
+
},
|
|
350
|
+
}), [
|
|
351
|
+
scrollToPixel,
|
|
352
|
+
offsetForIndex,
|
|
353
|
+
count,
|
|
354
|
+
data,
|
|
355
|
+
getItem,
|
|
356
|
+
total,
|
|
357
|
+
viewportLength,
|
|
358
|
+
fixedLayout,
|
|
359
|
+
highestMeasuredIndex,
|
|
360
|
+
onScrollToIndexFailed,
|
|
361
|
+
averageLength,
|
|
362
|
+
]);
|
|
363
|
+
// initialScrollIndex: once the first viewport is known, jump to that index a single time.
|
|
364
|
+
useEffect(() => {
|
|
365
|
+
if (initialScrollIndex === undefined || appliedInitialScrollRef.current)
|
|
366
|
+
return;
|
|
367
|
+
if (viewportLength <= EMPTY_OFFSET || count === FIRST_INDEX)
|
|
368
|
+
return;
|
|
369
|
+
appliedInitialScrollRef.current = true;
|
|
370
|
+
// The initial jump is instant (RN doesn't animate initialScrollIndex).
|
|
371
|
+
scrollToPixel(offsetForIndex(initialScrollIndex, FIRST_INDEX, EMPTY_OFFSET), false);
|
|
372
|
+
}, [initialScrollIndex, viewportLength, count, scrollToPixel, offsetForIndex]);
|
|
373
|
+
const keyForIndex = useCallback((index) => {
|
|
374
|
+
const item = getItem(data, index);
|
|
375
|
+
return keyExtractor ? keyExtractor(item, index) : String(index);
|
|
376
|
+
}, [getItem, data, keyExtractor]);
|
|
377
|
+
// maintainVisibleContentPosition JS anchor adjustment (RN getDerivedStateFromProps:715-768): the
|
|
378
|
+
// native MVCP cannot see prepended items above the window (they are collapsed into the leading
|
|
379
|
+
// SPACER), so we replicate the JS shift for exactly those. Runs in a layout effect so the
|
|
380
|
+
// correction lands before paint.
|
|
381
|
+
useLayoutEffect(() => {
|
|
382
|
+
if (maintainVisibleContentPosition === undefined || count === FIRST_INDEX) {
|
|
383
|
+
firstVisibleKeyRef.current = null;
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
const minIndexForVisible = maintainVisibleContentPosition.minIndexForVisible;
|
|
387
|
+
const newFirstVisibleKey = count > minIndexForVisible ? keyForIndex(minIndexForVisible) : null;
|
|
388
|
+
const prevKey = firstVisibleKeyRef.current;
|
|
389
|
+
if (prevKey !== null && newFirstVisibleKey !== null && prevKey !== newFirstVisibleKey) {
|
|
390
|
+
let anchorIndex = -1;
|
|
391
|
+
for (let index = minIndexForVisible; index < count; index += 1) {
|
|
392
|
+
if (keyForIndex(index) === prevKey) {
|
|
393
|
+
anchorIndex = index;
|
|
394
|
+
break;
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
if (anchorIndex > minIndexForVisible) {
|
|
398
|
+
// Native MVCP shifts in-window cells itself; the JS shift covers ONLY the inserted items
|
|
399
|
+
// in the leading SPACER (above the first rendered index). Counting the full inserted extent
|
|
400
|
+
// here would double-correct.
|
|
401
|
+
const spacerEnd = Math.min(anchorIndex, committedWindowRef.current.first);
|
|
402
|
+
const insertedExtent = spacerEnd > minIndexForVisible
|
|
403
|
+
? offsets[spacerEnd] - offsets[minIndexForVisible]
|
|
404
|
+
: EMPTY_OFFSET;
|
|
405
|
+
if (insertedExtent > EMPTY_OFFSET) {
|
|
406
|
+
const autoThreshold = maintainVisibleContentPosition.autoscrollToTopThreshold;
|
|
407
|
+
const anchoredNearTop = autoThreshold !== undefined && scrollOffset <= autoThreshold;
|
|
408
|
+
if (anchoredNearTop) {
|
|
409
|
+
dlog(`VirtualizedList MVCP autoscroll-to-top (offset=${scrollOffset} <= ${autoThreshold})`);
|
|
410
|
+
scrollToPixel(EMPTY_OFFSET, true);
|
|
411
|
+
}
|
|
412
|
+
else {
|
|
413
|
+
dlog(`VirtualizedList MVCP adjust +${insertedExtent}px ` +
|
|
414
|
+
`(anchor "${prevKey}" moved ${minIndexForVisible}->${anchorIndex})`);
|
|
415
|
+
scrollToPixel(scrollOffset + insertedExtent, false);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
firstVisibleKeyRef.current = newFirstVisibleKey;
|
|
421
|
+
}, [maintainVisibleContentPosition, count, keyForIndex, offsets, scrollOffset, scrollToPixel]);
|
|
422
|
+
// ---- assemble the windowed child list ----------------------------------
|
|
423
|
+
// extraData needs no wiring: this component is not memoized, so any prop change (including
|
|
424
|
+
// extraData) already re-renders and re-runs renderItem. Voided to mark the deliberate no-op.
|
|
425
|
+
void extraData;
|
|
426
|
+
const children = [];
|
|
427
|
+
const stickySet = stickyHeaderIndices !== undefined ? new Set(stickyHeaderIndices) : undefined;
|
|
428
|
+
let renderedStickyIndices = [];
|
|
429
|
+
const header = resolveElement(ListHeaderComponent);
|
|
430
|
+
if (header !== undefined) {
|
|
431
|
+
children.push(createElement('symbiote-view', { key: 'list-header' }, header));
|
|
432
|
+
}
|
|
433
|
+
if (count === FIRST_INDEX) {
|
|
434
|
+
const empty = resolveElement(ListEmptyComponent);
|
|
435
|
+
if (empty !== undefined) {
|
|
436
|
+
children.push(createElement('symbiote-view', { key: 'list-empty' }, empty));
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
else {
|
|
440
|
+
// The shared plan: spacer extents, in-window cell keys, and the sticky child positions.
|
|
441
|
+
const plan = buildListPlan({
|
|
442
|
+
count,
|
|
443
|
+
first,
|
|
444
|
+
last,
|
|
445
|
+
offsets,
|
|
446
|
+
lengths,
|
|
447
|
+
total,
|
|
448
|
+
keyFor: keyForIndex,
|
|
449
|
+
stickyIndices: stickySet,
|
|
450
|
+
hasHeader: header !== undefined,
|
|
451
|
+
hasSeparators: ItemSeparatorComponent !== undefined,
|
|
452
|
+
});
|
|
453
|
+
renderedStickyIndices = plan.stickyChildPositions;
|
|
454
|
+
if (plan.leadingExtent > EMPTY_OFFSET) {
|
|
455
|
+
children.push(createElement('symbiote-view', {
|
|
456
|
+
key: 'spacer-leading',
|
|
457
|
+
style: horizontal ? { width: plan.leadingExtent } : { height: plan.leadingExtent },
|
|
458
|
+
}));
|
|
459
|
+
}
|
|
460
|
+
for (const planCell of plan.cells) {
|
|
461
|
+
const item = getItem(data, planCell.index);
|
|
462
|
+
// renderItem gets a separators handle so a row can highlight/update its own dividers.
|
|
463
|
+
const cell = renderItem({
|
|
464
|
+
item,
|
|
465
|
+
index: planCell.index,
|
|
466
|
+
separators: makeSeparators(planCell.index),
|
|
467
|
+
});
|
|
468
|
+
// Wrap each cell in a measuring View. When inverted, each cell carries the counter-flip so
|
|
469
|
+
// its content reads upright inside the flipped content container.
|
|
470
|
+
children.push(createElement('symbiote-view', {
|
|
471
|
+
key: `cell-${planCell.key}`,
|
|
472
|
+
onLayout: makeCellMeasure(planCell.index),
|
|
473
|
+
style: inverted ? (horizontal ? INVERTED_X_STYLE : INVERTED_Y_STYLE) : undefined,
|
|
474
|
+
}, cell));
|
|
475
|
+
const separator = planCell.index < last
|
|
476
|
+
? renderSeparatorElement(ItemSeparatorComponent, item, getItem(data, planCell.index + 1), separatorOverridesRef.current.get(planCell.index))
|
|
477
|
+
: undefined;
|
|
478
|
+
if (separator !== undefined) {
|
|
479
|
+
children.push(createElement('symbiote-view', { key: `sep-${planCell.key}` }, separator));
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
if (plan.trailingExtent > EMPTY_OFFSET) {
|
|
483
|
+
children.push(createElement('symbiote-view', {
|
|
484
|
+
key: 'spacer-trailing',
|
|
485
|
+
style: horizontal ? { width: plan.trailingExtent } : { height: plan.trailingExtent },
|
|
486
|
+
}));
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
const footer = resolveElement(ListFooterComponent);
|
|
490
|
+
if (footer !== undefined) {
|
|
491
|
+
children.push(createElement('symbiote-view', { key: 'list-footer' }, footer));
|
|
492
|
+
}
|
|
493
|
+
// A horizontal list pins the content container to the full row width so the row overflows for
|
|
494
|
+
// iOS to scroll. The inversion flip rides ONLY the outer ScrollView style and each cell, never
|
|
495
|
+
// the content container (flipping it too would cancel the ScrollView flip).
|
|
496
|
+
const resolvedContentContainerStyle = horizontal
|
|
497
|
+
? [contentContainerStyle, { width: total }]
|
|
498
|
+
: contentContainerStyle;
|
|
499
|
+
const resolvedStyle = inverted
|
|
500
|
+
? [style, horizontal ? INVERTED_X_STYLE : INVERTED_Y_STYLE]
|
|
501
|
+
: style;
|
|
502
|
+
const scrollProps = {
|
|
503
|
+
// The list's accessibility surface rides down onto the ScrollView. Spread first so the
|
|
504
|
+
// explicit windowing props below always win.
|
|
505
|
+
...accessibilityRest,
|
|
506
|
+
style: resolvedStyle,
|
|
507
|
+
contentContainerStyle: resolvedContentContainerStyle,
|
|
508
|
+
horizontal,
|
|
509
|
+
onScroll,
|
|
510
|
+
onLayout: onViewportLayout,
|
|
511
|
+
};
|
|
512
|
+
if (onScrollBeginDrag !== undefined)
|
|
513
|
+
scrollProps.onScrollBeginDrag = onScrollBeginDrag;
|
|
514
|
+
if (onScrollEndDrag !== undefined)
|
|
515
|
+
scrollProps.onScrollEndDrag = onScrollEndDrag;
|
|
516
|
+
if (onMomentumScrollBegin !== undefined)
|
|
517
|
+
scrollProps.onMomentumScrollBegin = onMomentumScrollBegin;
|
|
518
|
+
if (onMomentumScrollEnd !== undefined)
|
|
519
|
+
scrollProps.onMomentumScrollEnd = onMomentumScrollEnd;
|
|
520
|
+
if (scrollEventThrottle !== undefined)
|
|
521
|
+
scrollProps.scrollEventThrottle = scrollEventThrottle;
|
|
522
|
+
if (keyboardShouldPersistTaps !== undefined)
|
|
523
|
+
scrollProps.keyboardShouldPersistTaps = keyboardShouldPersistTaps;
|
|
524
|
+
if (keyboardDismissMode !== undefined)
|
|
525
|
+
scrollProps.keyboardDismissMode = keyboardDismissMode;
|
|
526
|
+
// A pending imperative/initial scroll rides down as contentOffset (fresh identity each push).
|
|
527
|
+
if (commandedOffset !== undefined)
|
|
528
|
+
scrollProps.contentOffset = commandedOffset;
|
|
529
|
+
// Headers in the window stick; an empty list leaves the prop off entirely.
|
|
530
|
+
if (renderedStickyIndices.length > 0)
|
|
531
|
+
scrollProps.stickyHeaderIndices = renderedStickyIndices;
|
|
532
|
+
// Forward maintainVisibleContentPosition to the native ScrollView so it anchors in-window cells.
|
|
533
|
+
// minIndexForVisible is bumped by 1 when a ListHeaderComponent occupies child 0.
|
|
534
|
+
if (maintainVisibleContentPosition !== undefined) {
|
|
535
|
+
scrollProps.maintainVisibleContentPosition = {
|
|
536
|
+
...maintainVisibleContentPosition,
|
|
537
|
+
minIndexForVisible: maintainVisibleContentPosition.minIndexForVisible + (header !== undefined ? 1 : 0),
|
|
538
|
+
};
|
|
539
|
+
}
|
|
540
|
+
// Pull-to-refresh: when onRefresh is set, build a RefreshControl for the ScrollView's
|
|
541
|
+
// refreshControl prop. refreshing is RN-required alongside onRefresh, default false when nullish.
|
|
542
|
+
if (onRefresh !== undefined) {
|
|
543
|
+
dlog('VirtualizedList wiring RefreshControl (onRefresh provided)');
|
|
544
|
+
scrollProps.refreshControl = createElement(RefreshControl, {
|
|
545
|
+
refreshing: refreshing ?? false,
|
|
546
|
+
onRefresh,
|
|
547
|
+
progressViewOffset,
|
|
548
|
+
});
|
|
549
|
+
}
|
|
550
|
+
// The ScrollView handle (ref) backs animated imperative scrolls via its native command.
|
|
551
|
+
return createElement(ScrollView, { ...scrollProps, ref: scrollViewRef }, ...children);
|
|
552
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { type ComponentType, type ReactElement, type ReactNode, type Ref } from 'react';
|
|
2
|
+
import { type ISymbioteEvent } from '@symbiote-native/engine';
|
|
3
|
+
import { type ISection, type IVirtualizedSectionListHandle } from '@symbiote-native/components';
|
|
4
|
+
import { type ISeparators, type ISeparatorProps } from '../virtualized-list';
|
|
5
|
+
import type { IAccessibilityProps, IAriaProps } from '@symbiote-native/components';
|
|
6
|
+
import type { IStyleProp, IViewStyle } from '../../utils/styles';
|
|
7
|
+
export type { ISection } from '@symbiote-native/components';
|
|
8
|
+
export type { IVirtualizedSectionListHandle };
|
|
9
|
+
export interface IVirtualizedSectionListProps<ItemT> extends IAccessibilityProps, IAriaProps {
|
|
10
|
+
sections: ReadonlyArray<ISection<ItemT>>;
|
|
11
|
+
renderItem: (info: {
|
|
12
|
+
item: ItemT;
|
|
13
|
+
index: number;
|
|
14
|
+
section: ISection<ItemT>;
|
|
15
|
+
separators: ISeparators;
|
|
16
|
+
}) => ReactNode;
|
|
17
|
+
renderSectionHeader?: (info: {
|
|
18
|
+
section: ISection<ItemT>;
|
|
19
|
+
}) => ReactNode;
|
|
20
|
+
renderSectionFooter?: (info: {
|
|
21
|
+
section: ISection<ItemT>;
|
|
22
|
+
}) => ReactNode;
|
|
23
|
+
SectionSeparatorComponent?: ComponentType<Record<string, never>> | ReactElement;
|
|
24
|
+
keyExtractor?: (item: ItemT, index: number) => string;
|
|
25
|
+
stickySectionHeadersEnabled?: boolean;
|
|
26
|
+
extraData?: unknown;
|
|
27
|
+
ItemSeparatorComponent?: ComponentType<ISeparatorProps<ItemT>>;
|
|
28
|
+
ListHeaderComponent?: ComponentType<Record<string, never>> | ReactElement;
|
|
29
|
+
ListFooterComponent?: ComponentType<Record<string, never>> | ReactElement;
|
|
30
|
+
ListEmptyComponent?: ComponentType<Record<string, never>> | ReactElement;
|
|
31
|
+
onEndReached?: (info: {
|
|
32
|
+
distanceFromEnd: number;
|
|
33
|
+
}) => void;
|
|
34
|
+
onEndReachedThreshold?: number;
|
|
35
|
+
onStartReached?: (info: {
|
|
36
|
+
distanceFromStart: number;
|
|
37
|
+
}) => void;
|
|
38
|
+
onStartReachedThreshold?: number;
|
|
39
|
+
onRefresh?: () => void;
|
|
40
|
+
refreshing?: boolean | null;
|
|
41
|
+
progressViewOffset?: number;
|
|
42
|
+
initialNumToRender?: number;
|
|
43
|
+
initialScrollIndex?: number;
|
|
44
|
+
maxToRenderPerBatch?: number;
|
|
45
|
+
updateCellsBatchingPeriod?: number;
|
|
46
|
+
windowSize?: number;
|
|
47
|
+
inverted?: boolean;
|
|
48
|
+
maintainVisibleContentPosition?: {
|
|
49
|
+
minIndexForVisible: number;
|
|
50
|
+
autoscrollToTopThreshold?: number;
|
|
51
|
+
};
|
|
52
|
+
onScroll?: (event: ISymbioteEvent) => void;
|
|
53
|
+
onScrollBeginDrag?: (event: ISymbioteEvent) => void;
|
|
54
|
+
onScrollEndDrag?: (event: ISymbioteEvent) => void;
|
|
55
|
+
onMomentumScrollBegin?: (event: ISymbioteEvent) => void;
|
|
56
|
+
onMomentumScrollEnd?: (event: ISymbioteEvent) => void;
|
|
57
|
+
scrollEventThrottle?: number;
|
|
58
|
+
keyboardShouldPersistTaps?: boolean | 'always' | 'never' | 'handled';
|
|
59
|
+
keyboardDismissMode?: 'none' | 'on-drag' | 'interactive';
|
|
60
|
+
style?: IStyleProp<IViewStyle>;
|
|
61
|
+
contentContainerStyle?: IStyleProp<IViewStyle>;
|
|
62
|
+
className?: string;
|
|
63
|
+
}
|
|
64
|
+
export declare function VirtualizedSectionList<ItemT>(props: IVirtualizedSectionListProps<ItemT> & {
|
|
65
|
+
ref?: Ref<IVirtualizedSectionListHandle>;
|
|
66
|
+
}): ReactElement;
|