@symbiote-native/solid 3.2.0 → 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/build/components/flat-list/index.d.ts +3 -2
- package/build/components/flat-list/index.jsx +8 -10
- package/build/components/input-accessory-view-props.d.ts +4 -4
- package/build/components/keyboard-avoiding-view.d.ts +2 -2
- package/build/components/keyboard-avoiding-view.jsx +34 -114
- package/build/components/layout-conformance-props.d.ts +6 -0
- package/build/components/layout-conformance-props.js +1 -0
- package/build/components/modal.d.ts +4 -4
- package/build/components/modal.jsx +11 -11
- package/build/components/refresh-control-props.d.ts +7 -7
- package/build/components/scroll-view-props.d.ts +8 -5
- package/build/components/text-input-props.d.ts +2 -1
- package/build/components/text-props.d.ts +3 -3
- package/build/components/touchable-props.d.ts +6 -6
- package/build/components/virtualized-list/index.android.d.ts +2 -2
- package/build/components/virtualized-list/index.ios.d.ts +2 -2
- package/build/components/virtualized-list/item-content.d.ts +6 -0
- package/build/components/virtualized-list/item-content.jsx +9 -0
- package/build/components/virtualized-list/list-body.d.ts +15 -0
- package/build/components/virtualized-list/list-body.jsx +20 -0
- package/build/components/virtualized-list/list-parts.d.ts +14 -0
- package/build/components/virtualized-list/list-parts.js +27 -0
- package/build/components/virtualized-list/list-rows.d.ts +18 -0
- package/build/components/virtualized-list/list-rows.jsx +91 -0
- package/build/components/virtualized-list/list-window.d.ts +11 -0
- package/build/components/virtualized-list/list-window.js +29 -0
- package/build/components/virtualized-list/nested-scope.d.ts +7 -0
- package/build/components/virtualized-list/nested-scope.js +21 -0
- package/build/components/virtualized-list/scroll-handlers.d.ts +12 -0
- package/build/components/virtualized-list/scroll-handlers.js +31 -0
- package/build/components/virtualized-list/scroll-tree.d.ts +18 -0
- package/build/components/virtualized-list/scroll-tree.jsx +143 -0
- package/build/components/virtualized-list/shared.d.ts +3 -74
- package/build/components/virtualized-list/shared.jsx +53 -752
- package/build/components/virtualized-list/sticky-binding.d.ts +13 -0
- package/build/components/virtualized-list/sticky-binding.js +111 -0
- package/build/components/virtualized-list/sticky-header.d.ts +1 -0
- package/build/components/virtualized-list/sticky-header.jsx +11 -159
- package/build/components/virtualized-list/sticky-state.d.ts +14 -0
- package/build/components/virtualized-list/sticky-state.js +54 -0
- package/build/components/virtualized-list/use-list-driver.d.ts +22 -0
- package/build/components/virtualized-list/use-list-driver.js +128 -0
- package/build/components/virtualized-list/use-separators.d.ts +7 -0
- package/build/components/virtualized-list/use-separators.js +22 -0
- package/build/components/virtualized-list/virtualized-list-props.d.ts +86 -0
- package/build/components/virtualized-list/virtualized-list-props.js +53 -0
- package/build/components/virtualized-section-list/index.d.ts +22 -9
- package/build/components/virtualized-section-list/index.jsx +87 -115
- package/build/components/virtualized-section-list/section-item-cell.d.ts +18 -0
- package/build/components/virtualized-section-list/section-item-cell.jsx +37 -0
- package/build/components.d.ts +5 -1
- package/build/components.js +1 -0
- package/build/index.d.ts +9 -4
- package/build/index.js +20 -37
- package/build/modules/animated/index.d.ts +54 -41
- package/build/modules/animated/index.js +2 -26
- package/build/modules/app-registry/index.d.ts +3 -3
- package/build/modules/app-registry/index.js +2 -2
- package/build/modules/status-bar.js +14 -27
- package/build/primitives/create-color-scheme.d.ts +1 -1
- package/package.json +6 -6
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { type Accessor } from 'solid-js';
|
|
2
|
+
import { type IStickyHeaderProps } from '@symbiote-native/components';
|
|
3
|
+
import { type AnimatedNode, type ISymbioteEvent } from '@symbiote-native/engine';
|
|
4
|
+
export type IStickyBinding = {
|
|
5
|
+
animatedTranslateY: Accessor<AnimatedNode>;
|
|
6
|
+
committedStyle: Accessor<{
|
|
7
|
+
transform: {
|
|
8
|
+
translateY: number;
|
|
9
|
+
}[];
|
|
10
|
+
} | undefined>;
|
|
11
|
+
onLayout: (event: ISymbioteEvent) => void;
|
|
12
|
+
};
|
|
13
|
+
export declare function createStickyBinding(props: IStickyHeaderProps): IStickyBinding;
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
// Solid's reactivity around the shared sticky reducer: one state cell, a signal for the
|
|
2
|
+
// interpolation node, a version counter for the committed transform, and the listener wiring
|
|
3
|
+
import { createEffect, createMemo, createSignal, onCleanup, } from 'solid-js';
|
|
4
|
+
import { buildStickyPin, createInitialStickyState, readLayoutNumber, reduceSticky, } from '@symbiote-native/components';
|
|
5
|
+
import { Platform, dlog, } from '@symbiote-native/engine';
|
|
6
|
+
function detachListener(runtime) {
|
|
7
|
+
const { interpolation, listenerId } = runtime.held;
|
|
8
|
+
if (interpolation !== undefined && listenerId !== undefined) {
|
|
9
|
+
interpolation.removeListener(listenerId);
|
|
10
|
+
}
|
|
11
|
+
runtime.held.listenerId = undefined;
|
|
12
|
+
}
|
|
13
|
+
function dispatchSticky(runtime, action) {
|
|
14
|
+
runEffects(runtime, reduceSticky(runtime.state, action, runtime.inputs()).effects);
|
|
15
|
+
}
|
|
16
|
+
function rebuild(runtime, effect) {
|
|
17
|
+
dlog(`Solid ScrollViewStickyHeader[y=${runtime.state.layoutY}] rebuild ` +
|
|
18
|
+
`inputRange=${JSON.stringify(effect.inputRange)} outputRange=${JSON.stringify(effect.outputRange)}`);
|
|
19
|
+
detachListener(runtime);
|
|
20
|
+
const next = buildStickyPin(runtime.scrollAnimatedValue, effect, effect.hideOffset);
|
|
21
|
+
// Feeds only the debounced committed transform, the visible pin is the native connection
|
|
22
|
+
runtime.held.listenerId = next.addListener(({ value }) => {
|
|
23
|
+
if (typeof value === 'number') {
|
|
24
|
+
dispatchSticky(runtime, { kind: 'animated-tick', value });
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
runtime.held.interpolation = next;
|
|
28
|
+
runtime.setAnimatedTranslateY(next);
|
|
29
|
+
}
|
|
30
|
+
// The animated value updates several times per frame, hit detection needs the settled one
|
|
31
|
+
function debounce(runtime, value, delay) {
|
|
32
|
+
const { held } = runtime;
|
|
33
|
+
if (held.debounceTimer !== undefined)
|
|
34
|
+
clearTimeout(held.debounceTimer);
|
|
35
|
+
held.debounceTimer = setTimeout(() => {
|
|
36
|
+
held.debounceTimer = undefined;
|
|
37
|
+
dispatchSticky(runtime, { kind: 'debounce-fired', value });
|
|
38
|
+
}, delay);
|
|
39
|
+
}
|
|
40
|
+
function runEffects(runtime, effects) {
|
|
41
|
+
for (const effect of effects) {
|
|
42
|
+
switch (effect.kind) {
|
|
43
|
+
case 'rebuild-interpolation':
|
|
44
|
+
rebuild(runtime, effect);
|
|
45
|
+
break;
|
|
46
|
+
case 'schedule-debounce':
|
|
47
|
+
debounce(runtime, effect.value, effect.delay);
|
|
48
|
+
break;
|
|
49
|
+
case 'apply-passthrough':
|
|
50
|
+
dlog(`Solid ScrollViewStickyHeader[y=${runtime.state.layoutY}] passthrough translateY=${effect.translateY}`);
|
|
51
|
+
runtime.setVersion(tick => tick + 1);
|
|
52
|
+
break;
|
|
53
|
+
case 'record-header-y':
|
|
54
|
+
// `onLayout` of the wrapper records it, the reducer never needs an index here
|
|
55
|
+
break;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
// Every prop that changes over time is read here INSIDE an effect, as a call, so the header
|
|
60
|
+
// tracks it itself and the list's `insert` effect never sees those signals
|
|
61
|
+
export function createStickyBinding(props) {
|
|
62
|
+
const [version, setVersion] = createSignal(0);
|
|
63
|
+
// The unmeasured identity stub until the first rebuild replaces it
|
|
64
|
+
const [animatedTranslateY, setAnimatedTranslateY] = createSignal(props.scrollAnimatedValue.interpolate({
|
|
65
|
+
inputRange: [-1, 0],
|
|
66
|
+
outputRange: [0, 0],
|
|
67
|
+
}));
|
|
68
|
+
const runtime = {
|
|
69
|
+
state: createInitialStickyState(),
|
|
70
|
+
scrollAnimatedValue: props.scrollAnimatedValue,
|
|
71
|
+
setAnimatedTranslateY,
|
|
72
|
+
setVersion,
|
|
73
|
+
inputs: () => ({
|
|
74
|
+
os: Platform.OS,
|
|
75
|
+
inverted: props.inverted,
|
|
76
|
+
hiddenOnScroll: props.hiddenOnScroll,
|
|
77
|
+
scrollViewHeight: props.scrollViewHeight,
|
|
78
|
+
nextHeaderLayoutY: props.nextHeaderLayoutY,
|
|
79
|
+
}),
|
|
80
|
+
held: {
|
|
81
|
+
interpolation: undefined,
|
|
82
|
+
listenerId: undefined,
|
|
83
|
+
debounceTimer: undefined,
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
// Rebuilds on a collision or viewport input change and once on mount
|
|
87
|
+
createEffect(() => dispatchSticky(runtime, { kind: 'inputs-changed' }));
|
|
88
|
+
onCleanup(() => {
|
|
89
|
+
detachListener(runtime);
|
|
90
|
+
if (runtime.held.debounceTimer !== undefined) {
|
|
91
|
+
clearTimeout(runtime.held.debounceTimer);
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
// RN also re-invokes the `onLayout` of the wrapped child, here that child is committed in its
|
|
95
|
+
// own right and Fabric fires its listener directly
|
|
96
|
+
const onLayout = (event) => {
|
|
97
|
+
const { state } = runtime;
|
|
98
|
+
dispatchSticky(runtime, {
|
|
99
|
+
kind: 'layout',
|
|
100
|
+
y: readLayoutNumber(event, 'y') ?? state.layoutY,
|
|
101
|
+
height: readLayoutNumber(event, 'height') ?? state.layoutHeight,
|
|
102
|
+
});
|
|
103
|
+
props.onLayout(event);
|
|
104
|
+
};
|
|
105
|
+
const committedStyle = createMemo(() => {
|
|
106
|
+
version();
|
|
107
|
+
const { translateY } = runtime.state;
|
|
108
|
+
return translateY === null ? undefined : { transform: [{ translateY }] };
|
|
109
|
+
});
|
|
110
|
+
return { animatedTranslateY, committedStyle, onLayout };
|
|
111
|
+
}
|
|
@@ -2,5 +2,6 @@ import type { JSX } from '../../jsx-runtime';
|
|
|
2
2
|
import { type IStickyHeaderProps } from '@symbiote-native/components';
|
|
3
3
|
export type IStickyHeaderComponentProps = IStickyHeaderProps & {
|
|
4
4
|
children?: JSX.Element;
|
|
5
|
+
onFocus?: () => void;
|
|
5
6
|
};
|
|
6
7
|
export declare function ScrollViewStickyHeader(props: IStickyHeaderComponentProps): JSX.Element;
|
|
@@ -1,168 +1,20 @@
|
|
|
1
|
-
// Sticky headers
|
|
2
|
-
// ScrollViewStickyHeader.js.
|
|
1
|
+
// Sticky headers, the Solid lifecycle half of the JS layer RN keeps in `ScrollViewStickyHeader`
|
|
3
2
|
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
// entirely — forwarding that array to Fabric is a silent no-op, so we replicate the JS layer
|
|
11
|
-
// instead. The math (top and inverted input/output ranges) is `computeStickyInterpolation` and the
|
|
12
|
-
// DECISIONS (the zero-swallow gate, the debounce window, when to rebuild) are `reduceSticky`, both
|
|
13
|
-
// in @symbiote-native/components and both shared verbatim with React, Vue and Angular. This file
|
|
14
|
-
// contributes only Solid's reactivity: one signal for the interpolation node, one version counter,
|
|
15
|
-
// the listener/debounce wiring, and the child-wrapping.
|
|
16
|
-
//
|
|
17
|
-
// WHY THE WRAPPER TAKES ITS REACTIVE INPUTS AS PROPS AND NOT AS RESOLVED VALUES. Every prop below
|
|
18
|
-
// that changes over time — `nextHeaderLayoutY` (cross-talk as a later header measures),
|
|
19
|
-
// `scrollViewHeight` (the inverted viewport capture), `inverted` — is written into the JSX as a
|
|
20
|
-
// CALL, so the Solid compiler emits it as a getter and the header reads it inside its OWN effect.
|
|
21
|
-
// That is not a style choice: the wrap runs inside the content view's `insert` render effect, and
|
|
22
|
-
// `insert` REPLACES a subtree rather than diffing one, so a value read there would put those
|
|
23
|
-
// signals in the insert effect's dependency set and every header layout would tear down and rebuild
|
|
24
|
-
// every header. React/Vue/Angular can pass plain values because each has a node-reusing layer
|
|
25
|
-
// underneath; Solid does not (.claude/rules/solid-descriptor-bridge.md §4).
|
|
26
|
-
import { createEffect, createMemo, createSignal, onCleanup } from 'solid-js';
|
|
27
|
-
import { createInitialStickyState, readLayoutNumber, reduceSticky, STICKY_HEADER_Z_INDEX, } from '@symbiote-native/components';
|
|
28
|
-
import { AnimatedInterpolation, Platform, dlog, } from '@symbiote-native/engine';
|
|
29
|
-
// One sticky header. Measures its own y/height through onLayout, interpolates the shared scroll
|
|
30
|
-
// offset into a translateY that pins it to the top (or the bottom, inverted) until the next header
|
|
31
|
-
// collides with it, and drives that translate through the native driver when available so the pin
|
|
32
|
-
// tracks scroll on the UI thread with no JS per frame.
|
|
3
|
+
// Private to VirtualizedList: it hand-builds its scroll host, so `shared.tsx` wraps the cell here
|
|
4
|
+
// The math and the decisions live in `@symbiote-native/components`, the binding in `sticky-binding`
|
|
5
|
+
import { STICKY_HEADER_Z_INDEX, } from '@symbiote-native/components';
|
|
6
|
+
import { createStickyBinding } from './sticky-binding.js';
|
|
7
|
+
// Pins a cell to the top (or the bottom, inverted) until the next header collides with it
|
|
8
|
+
// `collapsable: false` keeps it a real Yoga node, the zIndex paints it over the rows under it
|
|
33
9
|
export function ScrollViewStickyHeader(props) {
|
|
34
|
-
|
|
35
|
-
// mutated in place by reduceSticky. A plain object, never a signal: nothing reads it reactively —
|
|
36
|
-
// the two signals below are what render depends on.
|
|
37
|
-
const state = createInitialStickyState();
|
|
38
|
-
// Bumped on every committed translateY so the passthrough memo re-reads state.translateY (RN's
|
|
39
|
-
// forceRender()).
|
|
40
|
-
const [version, setVersion] = createSignal(0);
|
|
41
|
-
// Read ONCE and held by identity: the value is stable for the header's lifetime by contract (the
|
|
42
|
-
// parent allocates exactly one per ScrollView).
|
|
43
|
-
const scrollAnimatedValue = props.scrollAnimatedValue;
|
|
44
|
-
// The interpolation node currently bound into style.transform — the un-measured identity stub
|
|
45
|
-
// until the first rebuild-interpolation effect replaces it. A signal, not a plain variable:
|
|
46
|
-
// render has to repaint when it is swapped.
|
|
47
|
-
const [animatedTranslateY, setAnimatedTranslateY] = createSignal(scrollAnimatedValue.interpolate({
|
|
48
|
-
inputRange: [-1, 0],
|
|
49
|
-
outputRange: [0, 0],
|
|
50
|
-
}));
|
|
51
|
-
// Held so the next rebuild can detach the previous settled-value listener (an engine call the
|
|
52
|
-
// reducer does not own) and cleanup can drop both.
|
|
53
|
-
let interpolation;
|
|
54
|
-
let listenerId;
|
|
55
|
-
let debounceTimer;
|
|
56
|
-
const inputs = () => ({
|
|
57
|
-
os: Platform.OS,
|
|
58
|
-
inverted: props.inverted,
|
|
59
|
-
scrollViewHeight: props.scrollViewHeight,
|
|
60
|
-
nextHeaderLayoutY: props.nextHeaderLayoutY,
|
|
61
|
-
});
|
|
62
|
-
function runEffects(effects) {
|
|
63
|
-
for (const effect of effects) {
|
|
64
|
-
switch (effect.kind) {
|
|
65
|
-
case 'rebuild-interpolation': {
|
|
66
|
-
dlog(`Solid ScrollViewStickyHeader[y=${state.layoutY}] rebuild-interpolation ` +
|
|
67
|
-
`inputRange=${JSON.stringify(effect.inputRange)} outputRange=${JSON.stringify(effect.outputRange)}`);
|
|
68
|
-
if (interpolation !== undefined && listenerId !== undefined) {
|
|
69
|
-
interpolation.removeListener(listenerId);
|
|
70
|
-
listenerId = undefined;
|
|
71
|
-
}
|
|
72
|
-
const next = scrollAnimatedValue.interpolate({
|
|
73
|
-
inputRange: effect.inputRange,
|
|
74
|
-
outputRange: effect.outputRange,
|
|
75
|
-
});
|
|
76
|
-
// Feeds ONLY the debounced committed transform below (RN adds this listener solely on
|
|
77
|
-
// Fabric, for hit-testing); the visible pin is the native AnimatedProps connection.
|
|
78
|
-
listenerId = next.addListener(({ value }) => {
|
|
79
|
-
if (typeof value === 'number')
|
|
80
|
-
dispatch({ kind: 'animated-tick', value });
|
|
81
|
-
});
|
|
82
|
-
interpolation = next;
|
|
83
|
-
setAnimatedTranslateY(next);
|
|
84
|
-
break;
|
|
85
|
-
}
|
|
86
|
-
case 'schedule-debounce':
|
|
87
|
-
// The animated value updates several times per frame; debounce the settled value into the
|
|
88
|
-
// committed transform so hit detection stays current (a Fabric issue, worse on Android).
|
|
89
|
-
if (debounceTimer !== undefined)
|
|
90
|
-
clearTimeout(debounceTimer);
|
|
91
|
-
debounceTimer = setTimeout(() => {
|
|
92
|
-
debounceTimer = undefined;
|
|
93
|
-
dispatch({ kind: 'debounce-fired', value: effect.value });
|
|
94
|
-
}, effect.delay);
|
|
95
|
-
break;
|
|
96
|
-
case 'apply-passthrough':
|
|
97
|
-
dlog(`Solid ScrollViewStickyHeader[y=${state.layoutY}] apply-passthrough translateY=${effect.translateY}`);
|
|
98
|
-
setVersion(tick => tick + 1);
|
|
99
|
-
break;
|
|
100
|
-
case 'record-header-y':
|
|
101
|
-
// Solid records through the wrapper's own onLayout closure (the public
|
|
102
|
-
// IStickyHeaderProps.onLayout contract, same as React/Vue), so the reducer never emits an
|
|
103
|
-
// index for it.
|
|
104
|
-
break;
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
function dispatch(action) {
|
|
109
|
-
runEffects(reduceSticky(state, action, inputs()).effects);
|
|
110
|
-
}
|
|
111
|
-
// Rebuild whenever a collision/viewport input changes (RN's [inverted, scrollViewHeight,
|
|
112
|
-
// nextHeaderLayoutY] dep array); also does the initial identity build on mount. All three are read
|
|
113
|
-
// through inputs() INSIDE the effect, which is what makes them dependencies — a guarded or
|
|
114
|
-
// deferred read would silently drop one.
|
|
115
|
-
createEffect(() => {
|
|
116
|
-
runEffects(reduceSticky(state, { kind: 'inputs-changed' }, inputs()).effects);
|
|
117
|
-
});
|
|
118
|
-
onCleanup(() => {
|
|
119
|
-
if (interpolation !== undefined && listenerId !== undefined) {
|
|
120
|
-
interpolation.removeListener(listenerId);
|
|
121
|
-
}
|
|
122
|
-
if (debounceTimer !== undefined)
|
|
123
|
-
clearTimeout(debounceTimer);
|
|
124
|
-
});
|
|
125
|
-
// RN's ScrollViewStickyHeader._onLayout additionally re-invokes the wrapped child's OWN onLayout.
|
|
126
|
-
// Deliberately not replicated: by the time a Solid component sees `children` they are already
|
|
127
|
-
// real engine nodes, and a node's listeners live in the engine's event map, not as a readable JS
|
|
128
|
-
// prop — there is nothing to re-invoke. Nothing is lost, because the child node is committed in
|
|
129
|
-
// its own right and Fabric fires its onLayout at it directly; React/Vue's forwarding actually
|
|
130
|
-
// makes a child handler run twice.
|
|
131
|
-
const onLayout = (event) => {
|
|
132
|
-
const y = readLayoutNumber(event, 'y');
|
|
133
|
-
const height = readLayoutNumber(event, 'height');
|
|
134
|
-
// Keep the previous value when a field is absent (RN sets state only on a defined read).
|
|
135
|
-
dispatch({
|
|
136
|
-
kind: 'layout',
|
|
137
|
-
y: y ?? state.layoutY,
|
|
138
|
-
height: height ?? state.layoutHeight,
|
|
139
|
-
});
|
|
140
|
-
props.onLayout(event);
|
|
141
|
-
};
|
|
142
|
-
// The EXPLICIT debounced translateY overrides the committed transform for hit-testing, while
|
|
143
|
-
// animatedTranslateY does the smooth (native-driven) pin. It rides the style ARRAY now rather
|
|
144
|
-
// than `passthroughAnimatedPropExplicitValues`: that prop was read by `createAnimatedComponent`,
|
|
145
|
-
// and the host is a bare tag, where the engine ignores it
|
|
146
|
-
// (core/engine/src/animated/host-binding.ts calls it a wrapper-ism by name). The wrapper did
|
|
147
|
-
// exactly this merge — `[reduced.style, passthroughStyle]` — so a later array entry overriding
|
|
148
|
-
// the transform is the same semantics, one layer down, and the engine still binds the
|
|
149
|
-
// interpolation it finds in entry 0.
|
|
150
|
-
const passthrough = createMemo(() => {
|
|
151
|
-
// Read the bump so a committed translateY repaints.
|
|
152
|
-
version();
|
|
153
|
-
if (state.translateY === null)
|
|
154
|
-
return undefined;
|
|
155
|
-
return { transform: [{ translateY: state.translateY }] };
|
|
156
|
-
});
|
|
157
|
-
// collapsable:false keeps the wrapper a real Yoga node; zIndex makes the pinned header paint OVER
|
|
158
|
-
// the rows scrolling up under it.
|
|
10
|
+
const binding = createStickyBinding(props);
|
|
159
11
|
return (<view style={[
|
|
160
12
|
{
|
|
161
|
-
transform: [{ translateY: animatedTranslateY() }],
|
|
13
|
+
transform: [{ translateY: binding.animatedTranslateY() }],
|
|
162
14
|
zIndex: STICKY_HEADER_Z_INDEX,
|
|
163
15
|
},
|
|
164
|
-
|
|
165
|
-
]} onLayout={onLayout} collapsable={false}>
|
|
16
|
+
binding.committedStyle(),
|
|
17
|
+
]} onLayout={binding.onLayout} onFocus={props.onFocus} collapsable={false}>
|
|
166
18
|
{props.children}
|
|
167
19
|
</view>);
|
|
168
20
|
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { type Accessor } from 'solid-js';
|
|
2
|
+
import { resolveScrollForwarding } from '@symbiote-native/components';
|
|
3
|
+
import { AnimatedValue, type ISymbioteEvent } from '@symbiote-native/engine';
|
|
4
|
+
import type { IVirtualizedListProps } from './virtualized-list-props';
|
|
5
|
+
type ICellMeasure = (event: ISymbioteEvent) => void;
|
|
6
|
+
export type IStickyState = {
|
|
7
|
+
scrollAnimatedValue: AnimatedValue;
|
|
8
|
+
forwarding: Accessor<ReturnType<typeof resolveScrollForwarding>>;
|
|
9
|
+
nativeStickyAvailable: () => boolean;
|
|
10
|
+
makeStickyCellLayout: (index: Accessor<number>, measure: ICellMeasure) => ICellMeasure;
|
|
11
|
+
nextStickyHeaderYFor: (index: number, mounted: ReadonlySet<number>) => number | undefined;
|
|
12
|
+
};
|
|
13
|
+
export declare function createStickyState<ItemT>(props: IVirtualizedListProps<ItemT>): IStickyState;
|
|
14
|
+
export {};
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// Sticky header state: the scroll value that drives each header and the cross-talk between them
|
|
2
|
+
import { createMemo, createSignal } from 'solid-js';
|
|
3
|
+
import { FIRST_INDEX, nextStickyHeaderY, readLayoutNumber, resolveScrollForwarding, } from '@symbiote-native/components';
|
|
4
|
+
import { AnimatedValue, dlog, isNativeAnimatedAvailable, } from '@symbiote-native/engine';
|
|
5
|
+
export function createStickyState(props) {
|
|
6
|
+
// Data index to measured y, so each header learns where the NEXT one starts (its push-off point)
|
|
7
|
+
// `stickyVersion` lets the previous header re-read the map
|
|
8
|
+
const headerLayoutYs = new Map();
|
|
9
|
+
const [stickyVersion, setStickyVersion] = createSignal(0);
|
|
10
|
+
const hasStickyHeaders = () => {
|
|
11
|
+
const indices = props.stickyHeaderIndices;
|
|
12
|
+
return indices !== undefined && indices.length > FIRST_INDEX;
|
|
13
|
+
};
|
|
14
|
+
const nativeStickyAvailable = () => hasStickyHeaders() && isNativeAnimatedAvailable();
|
|
15
|
+
// RN's list has no `invertStickyHeaders`, so headers always pin to the top
|
|
16
|
+
const forwarding = createMemo(() => resolveScrollForwarding({
|
|
17
|
+
hasStickyHeaders: hasStickyHeaders(),
|
|
18
|
+
nativeStickyAvailable: nativeStickyAvailable(),
|
|
19
|
+
invertStickyHeaders: undefined,
|
|
20
|
+
scrollEventThrottle: props.scrollEventThrottle,
|
|
21
|
+
maintainVisibleContentPosition: props.maintainVisibleContentPosition,
|
|
22
|
+
snapToAlignment: undefined,
|
|
23
|
+
}));
|
|
24
|
+
// Without the measured y the header AHEAD of this one has no collision point and sticks forever
|
|
25
|
+
const makeStickyCellLayout = (index, measure) => {
|
|
26
|
+
return (event) => {
|
|
27
|
+
measure(event);
|
|
28
|
+
const y = readLayoutNumber(event, 'y');
|
|
29
|
+
if (y === undefined || headerLayoutYs.get(index()) === y)
|
|
30
|
+
return;
|
|
31
|
+
dlog(`VirtualizedList sticky header index=${index()} y=${y}`);
|
|
32
|
+
headerLayoutYs.set(index(), y);
|
|
33
|
+
setStickyVersion(tick => tick + 1);
|
|
34
|
+
};
|
|
35
|
+
};
|
|
36
|
+
// Only a header mounted right now can collide, a stale y from one that scrolled out freezes it
|
|
37
|
+
// The map is deliberately not pruned, a measured y stays valid for when it scrolls back in
|
|
38
|
+
const nextStickyHeaderYFor = (index, mounted) => {
|
|
39
|
+
// Read the bump first so this stays a dependency of whichever header calls it
|
|
40
|
+
stickyVersion();
|
|
41
|
+
const indices = props.stickyHeaderIndices;
|
|
42
|
+
if (indices === undefined)
|
|
43
|
+
return undefined;
|
|
44
|
+
const rendered = indices.filter(sticky => mounted.has(sticky));
|
|
45
|
+
return nextStickyHeaderY(rendered, rendered.indexOf(index), headerLayoutYs);
|
|
46
|
+
};
|
|
47
|
+
return {
|
|
48
|
+
scrollAnimatedValue: new AnimatedValue(0),
|
|
49
|
+
forwarding,
|
|
50
|
+
nativeStickyAvailable,
|
|
51
|
+
makeStickyCellLayout,
|
|
52
|
+
nextStickyHeaderYFor,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { type Accessor } from 'solid-js';
|
|
2
|
+
import { type IListAction, type IListMetrics, type IListReducerInputs, type IListState, type IVirtualizedListHandle } from '@symbiote-native/components';
|
|
3
|
+
import { type ISymbioteNode } from '@symbiote-native/engine';
|
|
4
|
+
import type { IVirtualizedListProps } from './virtualized-list-props';
|
|
5
|
+
type ICommandedOffset = {
|
|
6
|
+
x: number;
|
|
7
|
+
y: number;
|
|
8
|
+
};
|
|
9
|
+
export type IListDriver<ItemT> = {
|
|
10
|
+
metrics: Accessor<IListMetrics>;
|
|
11
|
+
dispatch: (action: IListAction<ItemT>) => void;
|
|
12
|
+
handle: IVirtualizedListHandle;
|
|
13
|
+
currentCount: () => number;
|
|
14
|
+
getState: () => IListState<ItemT>;
|
|
15
|
+
getHostNode: () => ISymbioteNode | null;
|
|
16
|
+
setHostNode: (node: ISymbioteNode | null) => void;
|
|
17
|
+
commandedOffset: Accessor<ICommandedOffset | undefined>;
|
|
18
|
+
clearCommandedOffset: () => void;
|
|
19
|
+
};
|
|
20
|
+
type IFindFirstChildWithMore = IListReducerInputs<unknown>['findFirstChildWithMore'];
|
|
21
|
+
export declare function createListDriver<ItemT>(props: IVirtualizedListProps<ItemT>, findFirstChildWithMore: IFindFirstChildWithMore): IListDriver<ItemT>;
|
|
22
|
+
export {};
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
// The list's lifecycle: one folded state cell, native events turned into reducer actions, and the
|
|
2
|
+
// returned effects run with Solid primitives
|
|
3
|
+
import { createEffect, createMemo, createSignal, on, onCleanup, untrack, } from 'solid-js';
|
|
4
|
+
import { DEFAULT_INITIAL_NUM_TO_RENDER, DEFAULT_MAX_TO_RENDER_PER_BATCH, DEFAULT_UPDATE_CELLS_BATCHING_PERIOD, DEFAULT_WINDOW_SIZE, LIST_ACTION_KIND, buildListHandle, buildListReducerInputs, buildScrollViewHandle, buildViewabilityPairs, clearTimers, createInitialListState, listEffectSignature, reduceList, runListEffects, scrollTargetOf, } from '@symbiote-native/components';
|
|
5
|
+
import { dlog, getNativeTag, } from '@symbiote-native/engine';
|
|
6
|
+
function viewabilityPairsOf(props) {
|
|
7
|
+
return buildViewabilityPairs(props.onViewableItemsChanged, props.viewabilityConfig, props.viewabilityConfigCallbackPairs);
|
|
8
|
+
}
|
|
9
|
+
function reducerInputsOf(props, findFirstChildWithMore) {
|
|
10
|
+
return buildListReducerInputs({
|
|
11
|
+
findFirstChildWithMore,
|
|
12
|
+
data: props.data,
|
|
13
|
+
getItem: props.getItem,
|
|
14
|
+
getItemCount: props.getItemCount,
|
|
15
|
+
keyExtractor: props.keyExtractor,
|
|
16
|
+
getItemLayout: props.getItemLayout,
|
|
17
|
+
horizontal: props.horizontal === true,
|
|
18
|
+
windowSize: props.windowSize ?? DEFAULT_WINDOW_SIZE,
|
|
19
|
+
disableVirtualization: props.disableVirtualization,
|
|
20
|
+
initialNumToRender: props.initialNumToRender ?? DEFAULT_INITIAL_NUM_TO_RENDER,
|
|
21
|
+
maxToRenderPerBatch: props.maxToRenderPerBatch ?? DEFAULT_MAX_TO_RENDER_PER_BATCH,
|
|
22
|
+
updateCellsBatchingPeriod: props.updateCellsBatchingPeriod ?? DEFAULT_UPDATE_CELLS_BATCHING_PERIOD,
|
|
23
|
+
onEndReachedThreshold: props.onEndReachedThreshold,
|
|
24
|
+
onStartReachedThreshold: props.onStartReachedThreshold,
|
|
25
|
+
maintainVisibleContentPosition: props.maintainVisibleContentPosition,
|
|
26
|
+
initialScrollIndex: props.initialScrollIndex,
|
|
27
|
+
onEndReached: props.onEndReached,
|
|
28
|
+
onStartReached: props.onStartReached,
|
|
29
|
+
}, viewabilityPairsOf(props));
|
|
30
|
+
}
|
|
31
|
+
// The host node is held by identity in a plain variable, a store or proxy would be a different key
|
|
32
|
+
// than the engine's commit mirror holds and every imperative command would silently no-op
|
|
33
|
+
function createScrollTarget(props) {
|
|
34
|
+
let hostNode = null;
|
|
35
|
+
const scrollHandle = buildScrollViewHandle(() => hostNode);
|
|
36
|
+
const [commandedOffset, setCommandedOffset] = createSignal(undefined);
|
|
37
|
+
// A native `scrollTo` needs the committed Fabric tag and this adapter commits on a microtask, so
|
|
38
|
+
// a scroll requested in the tick of mount rides `contentOffset` instead
|
|
39
|
+
const scrollToPixel = (offset, animated) => {
|
|
40
|
+
const target = scrollTargetOf(offset, props.horizontal === true);
|
|
41
|
+
if (hostNode !== null && getNativeTag(hostNode) !== undefined) {
|
|
42
|
+
dlog(`VirtualizedList scrollTo offset=${offset} animated=${animated}`);
|
|
43
|
+
setCommandedOffset(undefined);
|
|
44
|
+
scrollHandle.scrollTo({ ...target, animated });
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
dlog(`VirtualizedList scrollTo offset=${offset} pending-commit`);
|
|
48
|
+
setCommandedOffset(target);
|
|
49
|
+
};
|
|
50
|
+
return {
|
|
51
|
+
scrollHandle,
|
|
52
|
+
scrollToPixel,
|
|
53
|
+
commandedOffset,
|
|
54
|
+
clearCommandedOffset: () => setCommandedOffset(undefined),
|
|
55
|
+
setHostNode: (node) => {
|
|
56
|
+
hostNode = node;
|
|
57
|
+
},
|
|
58
|
+
getHostNode: () => hostNode,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
// Kept apart so the driver body reads as state, memos and effects, with the host wiring named
|
|
62
|
+
function effectHostOf(props, scrollToPixel, getDispatch, [viewableTimer, batchTimer]) {
|
|
63
|
+
return {
|
|
64
|
+
callbacks: () => ({
|
|
65
|
+
onEndReached: props.onEndReached,
|
|
66
|
+
onStartReached: props.onStartReached,
|
|
67
|
+
onScrollToIndexFailed: props.onScrollToIndexFailed,
|
|
68
|
+
viewabilityPairs: viewabilityPairsOf(props),
|
|
69
|
+
}),
|
|
70
|
+
scrollToPixel,
|
|
71
|
+
dispatch: getDispatch,
|
|
72
|
+
viewableTimer,
|
|
73
|
+
batchTimer,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
export function createListDriver(props, findFirstChildWithMore) {
|
|
77
|
+
const listState = createInitialListState();
|
|
78
|
+
// Bumped on every render-relevant transition, `listState` is plain and triggers nothing itself
|
|
79
|
+
const [version, setVersion] = createSignal(0);
|
|
80
|
+
const target = createScrollTarget(props);
|
|
81
|
+
const viewableTimer = { current: null };
|
|
82
|
+
const batchTimer = { current: null };
|
|
83
|
+
const host = effectHostOf(props, target.scrollToPixel, () => dispatch, [
|
|
84
|
+
viewableTimer,
|
|
85
|
+
batchTimer,
|
|
86
|
+
]);
|
|
87
|
+
// Untracked by construction: it is reached from native events, timers and the imperative handle,
|
|
88
|
+
// and reading every prop through the inputs in a tracked scope would subscribe it to all of them
|
|
89
|
+
function dispatch(action) {
|
|
90
|
+
untrack(() => {
|
|
91
|
+
const result = reduceList(listState, action, reducerInputsOf(props, findFirstChildWithMore));
|
|
92
|
+
runListEffects(result.effects, host);
|
|
93
|
+
if (result.changed)
|
|
94
|
+
setVersion(tick => tick + 1);
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
// The window is recomputed once per pass, deriving twice would advance the throttle twice
|
|
98
|
+
const metrics = createMemo(() => {
|
|
99
|
+
version();
|
|
100
|
+
reduceList(listState, { kind: LIST_ACTION_KIND.refreshMetrics }, reducerInputsOf(props, findFirstChildWithMore));
|
|
101
|
+
return listState.metrics;
|
|
102
|
+
});
|
|
103
|
+
const commitSignature = createMemo(() => {
|
|
104
|
+
metrics();
|
|
105
|
+
return listEffectSignature(listState);
|
|
106
|
+
});
|
|
107
|
+
// `on()` runs the body untracked, so the commit pass depends on the windowing signature alone
|
|
108
|
+
createEffect(on(commitSignature, () => {
|
|
109
|
+
const result = reduceList(listState, { kind: LIST_ACTION_KIND.commit }, reducerInputsOf(props, findFirstChildWithMore));
|
|
110
|
+
runListEffects(result.effects, host);
|
|
111
|
+
}));
|
|
112
|
+
onCleanup(() => clearTimers([viewableTimer, batchTimer]));
|
|
113
|
+
return {
|
|
114
|
+
metrics,
|
|
115
|
+
dispatch,
|
|
116
|
+
handle: buildListHandle({
|
|
117
|
+
dispatch,
|
|
118
|
+
scrollHandle: target.scrollHandle,
|
|
119
|
+
getNode: target.getHostNode,
|
|
120
|
+
}),
|
|
121
|
+
currentCount: () => listState.metrics.count,
|
|
122
|
+
getState: () => listState,
|
|
123
|
+
getHostNode: target.getHostNode,
|
|
124
|
+
setHostNode: target.setHostNode,
|
|
125
|
+
commandedOffset: target.commandedOffset,
|
|
126
|
+
clearCommandedOffset: target.clearCommandedOffset,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { type ISeparatorProps, type ISeparators } from '@symbiote-native/components';
|
|
2
|
+
import type { IVirtualizedListProps } from './virtualized-list-props';
|
|
3
|
+
export type IListSeparators<ItemT> = {
|
|
4
|
+
makeSeparators: (index: number) => ISeparators;
|
|
5
|
+
separatorPropsFor: (index: number) => ISeparatorProps<ItemT>;
|
|
6
|
+
};
|
|
7
|
+
export declare function createSeparators<ItemT>(props: IVirtualizedListProps<ItemT>, currentCount: () => number): IListSeparators<ItemT>;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// Per-gap separator overrides, read straight by the cell walk
|
|
2
|
+
import { createSignal } from 'solid-js';
|
|
3
|
+
import { buildSeparatorHandles, buildSeparatorProps, isSeparatorGapInRange, } from '@symbiote-native/components';
|
|
4
|
+
// Keyed by the leading cell index of the gap, a gap outside [0, count - 2] addresses no separator
|
|
5
|
+
export function createSeparators(props, currentCount) {
|
|
6
|
+
const overrides = new Map();
|
|
7
|
+
const [version, setVersion] = createSignal(0);
|
|
8
|
+
const mergeSeparator = (gapIndex, patch) => {
|
|
9
|
+
if (!isSeparatorGapInRange(gapIndex, currentCount()))
|
|
10
|
+
return;
|
|
11
|
+
overrides.set(gapIndex, { ...overrides.get(gapIndex), ...patch });
|
|
12
|
+
setVersion(tick => tick + 1);
|
|
13
|
+
};
|
|
14
|
+
return {
|
|
15
|
+
makeSeparators: index => buildSeparatorHandles(index, mergeSeparator),
|
|
16
|
+
// Reads `version` so a highlight call reactively refreshes this gap, the Map tracks nothing
|
|
17
|
+
separatorPropsFor: index => {
|
|
18
|
+
version();
|
|
19
|
+
return buildSeparatorProps(props.getItem(props.data, index), props.getItem(props.data, index + 1), overrides.get(index));
|
|
20
|
+
},
|
|
21
|
+
};
|
|
22
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import type { Accessor, Ref } from 'solid-js';
|
|
2
|
+
import type { JSX } from '../../jsx-runtime';
|
|
3
|
+
import type { IAccessibilityProps, IAriaProps, ICellRendererBaseProps, IInnerViewRef, ISeparatorProps, ISeparators, IViewabilityConfig, IViewabilityConfigCallbackPair, IViewableItemsChangedInfo, IVirtualizedListHandle } from '@symbiote-native/components';
|
|
4
|
+
import type { IClassNameValue, IStyleProp, ISymbioteEvent, IViewStyle } from '@symbiote-native/engine';
|
|
5
|
+
export type ICellRendererProps<ItemT> = ICellRendererBaseProps<ItemT> & {
|
|
6
|
+
children?: JSX.Element;
|
|
7
|
+
};
|
|
8
|
+
export type ICellRendererComponent<ItemT> = (props: ICellRendererProps<ItemT>) => JSX.Element;
|
|
9
|
+
export type IVirtualizedListCellInfo<ItemT> = {
|
|
10
|
+
item: ItemT;
|
|
11
|
+
index: number;
|
|
12
|
+
separators: ISeparators;
|
|
13
|
+
};
|
|
14
|
+
export type IVirtualizedListRenderItem<ItemT> = (info: Accessor<IVirtualizedListCellInfo<ItemT>>) => JSX.Element;
|
|
15
|
+
export type IVirtualizedListProps<ItemT> = IAccessibilityProps & IAriaProps & {
|
|
16
|
+
data: unknown;
|
|
17
|
+
getItem: (data: unknown, index: number) => ItemT;
|
|
18
|
+
getItemCount: (data: unknown) => number;
|
|
19
|
+
renderItem?: IVirtualizedListRenderItem<ItemT>;
|
|
20
|
+
ListItemComponent?: (props: IVirtualizedListCellInfo<ItemT>) => JSX.Element;
|
|
21
|
+
getItemLayout?: (data: unknown, index: number) => {
|
|
22
|
+
length: number;
|
|
23
|
+
offset: number;
|
|
24
|
+
index: number;
|
|
25
|
+
};
|
|
26
|
+
initialNumToRender?: number;
|
|
27
|
+
windowSize?: number;
|
|
28
|
+
disableVirtualization?: boolean;
|
|
29
|
+
ListHeaderComponent?: JSX.Element;
|
|
30
|
+
ListFooterComponent?: JSX.Element;
|
|
31
|
+
ListEmptyComponent?: JSX.Element;
|
|
32
|
+
ItemSeparatorComponent?: (props: ISeparatorProps<ItemT>) => JSX.Element;
|
|
33
|
+
CellRendererComponent?: ICellRendererComponent<ItemT>;
|
|
34
|
+
ref?: Ref<IVirtualizedListHandle>;
|
|
35
|
+
onEndReached?: (info: {
|
|
36
|
+
distanceFromEnd: number;
|
|
37
|
+
}) => void;
|
|
38
|
+
onEndReachedThreshold?: number;
|
|
39
|
+
onStartReached?: (info: {
|
|
40
|
+
distanceFromStart: number;
|
|
41
|
+
}) => void;
|
|
42
|
+
onStartReachedThreshold?: number;
|
|
43
|
+
keyExtractor?: (item: ItemT, index: number) => string;
|
|
44
|
+
onViewableItemsChanged?: (info: IViewableItemsChangedInfo<ItemT>) => void;
|
|
45
|
+
viewabilityConfig?: IViewabilityConfig;
|
|
46
|
+
viewabilityConfigCallbackPairs?: IViewabilityConfigCallbackPair<ItemT>[];
|
|
47
|
+
onScroll?: (event: ISymbioteEvent) => void;
|
|
48
|
+
onScrollBeginDrag?: (event: ISymbioteEvent) => void;
|
|
49
|
+
onScrollEndDrag?: (event: ISymbioteEvent) => void;
|
|
50
|
+
onMomentumScrollBegin?: (event: ISymbioteEvent) => void;
|
|
51
|
+
onMomentumScrollEnd?: (event: ISymbioteEvent) => void;
|
|
52
|
+
onContentSizeChange?: (width: number, height: number) => void;
|
|
53
|
+
scrollEventThrottle?: number;
|
|
54
|
+
keyboardShouldPersistTaps?: boolean | 'always' | 'never' | 'handled';
|
|
55
|
+
keyboardDismissMode?: 'none' | 'on-drag' | 'interactive';
|
|
56
|
+
removeClippedSubviews?: boolean;
|
|
57
|
+
nestedScrollEnabled?: boolean;
|
|
58
|
+
stickyHeaderHiddenOnScroll?: boolean;
|
|
59
|
+
innerViewRef?: IInnerViewRef;
|
|
60
|
+
horizontal?: boolean;
|
|
61
|
+
inverted?: boolean;
|
|
62
|
+
style?: IStyleProp<IViewStyle>;
|
|
63
|
+
contentContainerStyle?: IStyleProp<IViewStyle> | string;
|
|
64
|
+
ListHeaderComponentStyle?: IStyleProp<IViewStyle>;
|
|
65
|
+
ListFooterComponentStyle?: IStyleProp<IViewStyle>;
|
|
66
|
+
class?: IClassNameValue;
|
|
67
|
+
onRefresh?: () => void;
|
|
68
|
+
refreshing?: boolean | null;
|
|
69
|
+
progressViewOffset?: number;
|
|
70
|
+
stickyHeaderIndices?: number[];
|
|
71
|
+
maintainVisibleContentPosition?: {
|
|
72
|
+
minIndexForVisible: number;
|
|
73
|
+
autoscrollToTopThreshold?: number;
|
|
74
|
+
};
|
|
75
|
+
initialScrollIndex?: number;
|
|
76
|
+
maxToRenderPerBatch?: number;
|
|
77
|
+
updateCellsBatchingPeriod?: number;
|
|
78
|
+
extraData?: unknown;
|
|
79
|
+
onScrollToIndexFailed?: (info: {
|
|
80
|
+
index: number;
|
|
81
|
+
highestMeasuredFrameIndex: number;
|
|
82
|
+
averageItemLength: number;
|
|
83
|
+
}) => void;
|
|
84
|
+
};
|
|
85
|
+
export type IVirtualizedListComponent = <ItemT>(props: IVirtualizedListProps<ItemT>) => JSX.Element;
|
|
86
|
+
export declare const HANDLED_PROPS: readonly ["data", "getItem", "getItemCount", "renderItem", "ListItemComponent", "getItemLayout", "initialNumToRender", "windowSize", "disableVirtualization", "ListHeaderComponent", "ListFooterComponent", "ListEmptyComponent", "ItemSeparatorComponent", "CellRendererComponent", "ref", "onEndReached", "onEndReachedThreshold", "onStartReached", "onStartReachedThreshold", "keyExtractor", "onViewableItemsChanged", "viewabilityConfig", "viewabilityConfigCallbackPairs", "onScroll", "onScrollBeginDrag", "onScrollEndDrag", "onMomentumScrollBegin", "onMomentumScrollEnd", "scrollEventThrottle", "keyboardShouldPersistTaps", "keyboardDismissMode", "horizontal", "inverted", "style", "contentContainerStyle", "ListHeaderComponentStyle", "ListFooterComponentStyle", "class", "onScrollToIndexFailed", "onRefresh", "refreshing", "progressViewOffset", "stickyHeaderIndices", "maintainVisibleContentPosition", "initialScrollIndex", "maxToRenderPerBatch", "updateCellsBatchingPeriod", "extraData"];
|