@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,27 @@
|
|
|
1
|
+
// What a list is built from: driver, nesting among lists, sticky state, window and handlers
|
|
2
|
+
import { onCleanup, untrack } from 'solid-js';
|
|
3
|
+
import { createListNesting, listHasMore, resolveItemKey, } from '@symbiote-native/components';
|
|
4
|
+
import { useVirtualizedListScope } from './nested-scope.js';
|
|
5
|
+
import { createListWindow } from './list-window.js';
|
|
6
|
+
import { createScrollHandlers } from './scroll-handlers.js';
|
|
7
|
+
import { createStickyState } from './sticky-state.js';
|
|
8
|
+
import { createListDriver } from './use-list-driver.js';
|
|
9
|
+
export function createListParts(props) {
|
|
10
|
+
// The driver is built first, so it reaches the nesting through `created`, read on first use
|
|
11
|
+
const driver = createListDriver(props, (first, last) => created.findFirstChildWithMore(first, last));
|
|
12
|
+
const created = createListNesting({
|
|
13
|
+
parent: useVirtualizedListScope(),
|
|
14
|
+
horizontal: untrack(() => props.horizontal === true),
|
|
15
|
+
getState: driver.getState,
|
|
16
|
+
dispatch: driver.dispatch,
|
|
17
|
+
getContainerNode: driver.getHostNode,
|
|
18
|
+
getHasMore: () => listHasMore(driver.getState()),
|
|
19
|
+
keyFor: index => resolveItemKey(props.getItem(props.data, index), index, props.keyExtractor),
|
|
20
|
+
handlers: () => handlers,
|
|
21
|
+
});
|
|
22
|
+
onCleanup(created.detach);
|
|
23
|
+
const sticky = createStickyState(props);
|
|
24
|
+
const listWindow = createListWindow(props, driver.metrics);
|
|
25
|
+
const handlers = createScrollHandlers(props, driver, sticky, () => created);
|
|
26
|
+
return { driver, nesting: created, sticky, listWindow, handlers };
|
|
27
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { type Accessor } from 'solid-js';
|
|
2
|
+
import type { JSX } from '../../jsx-runtime';
|
|
3
|
+
import { type IListMetrics } from '@symbiote-native/components';
|
|
4
|
+
import type { ISymbioteEvent } from '@symbiote-native/engine';
|
|
5
|
+
import type { IListWindow } from './list-window';
|
|
6
|
+
import type { IStickyState } from './sticky-state';
|
|
7
|
+
import type { IListSeparators } from './use-separators';
|
|
8
|
+
import type { IVirtualizedListProps } from './virtualized-list-props';
|
|
9
|
+
export type IRowDeps<ItemT> = {
|
|
10
|
+
props: IVirtualizedListProps<ItemT>;
|
|
11
|
+
listWindow: IListWindow;
|
|
12
|
+
metrics: Accessor<IListMetrics>;
|
|
13
|
+
separators: IListSeparators<ItemT>;
|
|
14
|
+
sticky: IStickyState;
|
|
15
|
+
measureCell: (index: Accessor<number>) => (event: ISymbioteEvent) => void;
|
|
16
|
+
focusCell: (index: Accessor<number>) => () => void;
|
|
17
|
+
};
|
|
18
|
+
export declare function buildRow<ItemT>(deps: IRowDeps<ItemT>, rowKey: string): JSX.Element;
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
// The rows of the windowed list: a cell with its separator, or a spacer standing in for a gap
|
|
2
|
+
import { Show, createMemo, on, untrack } from 'solid-js';
|
|
3
|
+
import { EMPTY_OFFSET, LIST_SEGMENT_KIND, NO_INDEX, cellStyleOf, spacerStyleOf, } from '@symbiote-native/components';
|
|
4
|
+
import { renderItemContent } from './item-content.jsx';
|
|
5
|
+
import { ScrollViewStickyHeader } from './sticky-header.jsx';
|
|
6
|
+
function separatorElement(deps, index) {
|
|
7
|
+
const Separator = deps.props.ItemSeparatorComponent;
|
|
8
|
+
if (Separator === undefined)
|
|
9
|
+
return undefined;
|
|
10
|
+
// A spread of a CALL compiles to `mergeProps(() => ...)`, so the separator's props stay live
|
|
11
|
+
return <Separator {...deps.separators.separatorPropsFor(index)}/>;
|
|
12
|
+
}
|
|
13
|
+
// RN gates the separator on the last index of the DATA, not of the WINDOW: it lives INSIDE the
|
|
14
|
+
// measuring wrapper, so gating on the window would change a cell's height as the window slides
|
|
15
|
+
function hasSeparatorAfter(deps, index) {
|
|
16
|
+
return (deps.props.ItemSeparatorComponent !== undefined &&
|
|
17
|
+
index < deps.metrics().count - 1);
|
|
18
|
+
}
|
|
19
|
+
function cellPartsOf(deps, index) {
|
|
20
|
+
const { props } = deps;
|
|
21
|
+
// Called once and untracked, a tracked call would rebuild the subtree on every window step
|
|
22
|
+
const content = untrack(() => renderItemContent(props, () => ({
|
|
23
|
+
item: props.getItem(props.data, index()),
|
|
24
|
+
index: index(),
|
|
25
|
+
separators: deps.separators.makeSeparators(index()),
|
|
26
|
+
})));
|
|
27
|
+
// The separator rides INSIDE the measuring wrapper, as a sibling it would be an extra flex child
|
|
28
|
+
const separator = (<Show when={hasSeparatorAfter(deps, index())}>
|
|
29
|
+
<view>{separatorElement(deps, index())}</view>
|
|
30
|
+
</Show>);
|
|
31
|
+
return { content, separator };
|
|
32
|
+
}
|
|
33
|
+
// The view around a cell, or the app's `CellRendererComponent` standing in for it
|
|
34
|
+
function cellWrapper(deps, index, cellKey, parts) {
|
|
35
|
+
const { props } = deps;
|
|
36
|
+
const style = cellStyleOf({
|
|
37
|
+
inverted: props.inverted === true,
|
|
38
|
+
horizontal: props.horizontal === true,
|
|
39
|
+
});
|
|
40
|
+
const Custom = props.CellRendererComponent;
|
|
41
|
+
if (Custom === undefined) {
|
|
42
|
+
return (<view onLayout={deps.measureCell(index)} onFocus={deps.focusCell(index)} style={style}>
|
|
43
|
+
{parts.content}
|
|
44
|
+
{parts.separator}
|
|
45
|
+
</view>);
|
|
46
|
+
}
|
|
47
|
+
return (<Custom cellKey={cellKey} index={index()} item={props.getItem(props.data, index())} style={style} onLayout={deps.measureCell(index)} onFocus={deps.focusCell(index)}>
|
|
48
|
+
{parts.content}
|
|
49
|
+
{parts.separator}
|
|
50
|
+
</Custom>);
|
|
51
|
+
}
|
|
52
|
+
function buildCell(deps, index, sticky, cellKey) {
|
|
53
|
+
const { props } = deps;
|
|
54
|
+
const parts = cellPartsOf(deps, index);
|
|
55
|
+
if (!sticky)
|
|
56
|
+
return cellWrapper(deps, index, cellKey, parts);
|
|
57
|
+
// A plain sticky cell is measured by the header itself, which REPLACES the view rather than
|
|
58
|
+
// nesting, the app's own cell component stays inside it
|
|
59
|
+
// Every reactive input is a CALL so the header reads it in its own effect
|
|
60
|
+
return (<ScrollViewStickyHeader onLayout={deps.sticky.makeStickyCellLayout(index, deps.measureCell(index))} onFocus={deps.focusCell(index)} nextHeaderLayoutY={deps.sticky.nextStickyHeaderYFor(index(), deps.listWindow.mountedIndices())} scrollAnimatedValue={deps.sticky.scrollAnimatedValue} inverted={undefined} hiddenOnScroll={props.stickyHeaderHiddenOnScroll} scrollViewHeight={undefined}>
|
|
61
|
+
{props.CellRendererComponent === undefined
|
|
62
|
+
? [parts.content, parts.separator]
|
|
63
|
+
: cellWrapper(deps, index, cellKey, parts)}
|
|
64
|
+
</ScrollViewStickyHeader>);
|
|
65
|
+
}
|
|
66
|
+
// A row is keyed, so its segment changes under it as the window slides, the previous value covers
|
|
67
|
+
// the tick between a key leaving the plan and `<For>` disposing the row
|
|
68
|
+
function latestSegmentValue(deps, rowKey, read, initial) {
|
|
69
|
+
return createMemo((previous) => {
|
|
70
|
+
const segment = deps.listWindow.segmentOf(rowKey);
|
|
71
|
+
return (segment === undefined ? undefined : read(segment)) ?? previous;
|
|
72
|
+
}, initial);
|
|
73
|
+
}
|
|
74
|
+
function buildCellRow(deps, rowKey) {
|
|
75
|
+
const index = latestSegmentValue(deps, rowKey, segment => segment.kind === LIST_SEGMENT_KIND.cell ? segment.index : undefined, NO_INDEX);
|
|
76
|
+
const isSticky = createMemo(() => deps.listWindow.stickySet()?.has(index()) === true);
|
|
77
|
+
// A sticky cell is a different host subtree, not a different prop, and Solid has no reconciler to
|
|
78
|
+
// swap one for the other, so the flip is an explicit rebuild boundary keyed on the discriminator
|
|
79
|
+
// The row is keyed by its cell's key, which never changes over the row's life
|
|
80
|
+
const segment = deps.listWindow.segmentOf(rowKey);
|
|
81
|
+
const cellKey = segment?.kind === LIST_SEGMENT_KIND.cell ? segment.key : rowKey;
|
|
82
|
+
return createMemo(on(isSticky, sticky => buildCell(deps, index, sticky, cellKey)));
|
|
83
|
+
}
|
|
84
|
+
function buildSpacerRow(deps, rowKey) {
|
|
85
|
+
const extent = latestSegmentValue(deps, rowKey, segment => segment.kind === LIST_SEGMENT_KIND.spacer ? segment.extent : undefined, EMPTY_OFFSET);
|
|
86
|
+
return (<view style={spacerStyleOf(extent(), deps.props.horizontal === true)}/>);
|
|
87
|
+
}
|
|
88
|
+
export function buildRow(deps, rowKey) {
|
|
89
|
+
const isSpacer = deps.listWindow.segmentOf(rowKey)?.kind === LIST_SEGMENT_KIND.spacer;
|
|
90
|
+
return isSpacer ? buildSpacerRow(deps, rowKey) : buildCellRow(deps, rowKey);
|
|
91
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { type Accessor } from 'solid-js';
|
|
2
|
+
import { type IListMetrics, type IListSegment } from '@symbiote-native/components';
|
|
3
|
+
import type { IVirtualizedListProps } from './virtualized-list-props';
|
|
4
|
+
export type IListWindow = {
|
|
5
|
+
stickySet: Accessor<ReadonlySet<number> | undefined>;
|
|
6
|
+
rowKeys: Accessor<string[]>;
|
|
7
|
+
segmentOf: (rowKey: string) => IListSegment | undefined;
|
|
8
|
+
mountedIndices: Accessor<ReadonlySet<number>>;
|
|
9
|
+
};
|
|
10
|
+
export declare function rowKeyOf(segment: IListSegment): string;
|
|
11
|
+
export declare function createListWindow<ItemT>(props: IVirtualizedListProps<ItemT>, metrics: Accessor<IListMetrics>): IListWindow;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// The plan for the window the driver derived, keyed so a row survives a window slide
|
|
2
|
+
import { createMemo } from 'solid-js';
|
|
3
|
+
import { FIRST_INDEX, LIST_SEGMENT_KIND, planFromMetrics, resolveItemKey, } from '@symbiote-native/components';
|
|
4
|
+
// A cell and a spacer never share a row key, an item key cannot start with `spacer-`
|
|
5
|
+
export function rowKeyOf(segment) {
|
|
6
|
+
return segment.kind === LIST_SEGMENT_KIND.spacer
|
|
7
|
+
? segment.key
|
|
8
|
+
: `cell-${segment.key}`;
|
|
9
|
+
}
|
|
10
|
+
export function createListWindow(props, metrics) {
|
|
11
|
+
const keyFor = (index) => resolveItemKey(props.getItem(props.data, index), index, props.keyExtractor);
|
|
12
|
+
const windowPlan = createMemo(() => {
|
|
13
|
+
const current = metrics();
|
|
14
|
+
if (current.count === FIRST_INDEX)
|
|
15
|
+
return null;
|
|
16
|
+
return planFromMetrics(current, keyFor, props.stickyHeaderIndices);
|
|
17
|
+
});
|
|
18
|
+
const segments = createMemo(() => windowPlan()?.plan.segments ?? []);
|
|
19
|
+
const segmentsByKey = createMemo(() => new Map(segments().map((segment) => [
|
|
20
|
+
rowKeyOf(segment),
|
|
21
|
+
segment,
|
|
22
|
+
])));
|
|
23
|
+
return {
|
|
24
|
+
stickySet: () => windowPlan()?.stickySet,
|
|
25
|
+
rowKeys: createMemo(() => segments().map(rowKeyOf)),
|
|
26
|
+
segmentOf: rowKey => segmentsByKey().get(rowKey),
|
|
27
|
+
mountedIndices: createMemo(() => new Set(segments().flatMap(segment => segment.kind === LIST_SEGMENT_KIND.cell ? [segment.index] : []))),
|
|
28
|
+
};
|
|
29
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { IListScope } from '@symbiote-native/components';
|
|
2
|
+
import type { JSX } from '../../jsx-runtime';
|
|
3
|
+
export declare function useVirtualizedListScope(): IListScope | null;
|
|
4
|
+
export declare function provideVirtualizedListScope(scope: IListScope | null, build: () => JSX.Element): JSX.Element;
|
|
5
|
+
export declare function VirtualizedListScopeResetter(props: {
|
|
6
|
+
children?: JSX.Element;
|
|
7
|
+
}): JSX.Element;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// The scope a list hands down to the lists nested in its cells, Solid's way to carry it
|
|
2
|
+
// What it holds and how it behaves is `createListNesting` in the shared components package
|
|
3
|
+
import { createContext, useContext } from 'solid-js';
|
|
4
|
+
const ListScope = createContext(null);
|
|
5
|
+
// Null outside every list, the scope of the nearest list above otherwise
|
|
6
|
+
export function useVirtualizedListScope() {
|
|
7
|
+
return useContext(ListScope);
|
|
8
|
+
}
|
|
9
|
+
// Called as a function, the Provider's children are built with the scope in their owner chain
|
|
10
|
+
export function provideVirtualizedListScope(scope, build) {
|
|
11
|
+
return ListScope.Provider({
|
|
12
|
+
value: scope,
|
|
13
|
+
get children() {
|
|
14
|
+
return build();
|
|
15
|
+
},
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
// A portal-like component (Modal) passes null, its content sits outside the list's scroll
|
|
19
|
+
export function VirtualizedListScopeResetter(props) {
|
|
20
|
+
return provideVirtualizedListScope(null, () => props.children);
|
|
21
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { type Accessor } from 'solid-js';
|
|
2
|
+
import { type IListHandlers, type IListNesting } from '@symbiote-native/components';
|
|
3
|
+
import { type ISymbioteEvent } from '@symbiote-native/engine';
|
|
4
|
+
import type { IListDriver } from './use-list-driver';
|
|
5
|
+
import type { IStickyState } from './sticky-state';
|
|
6
|
+
import type { IVirtualizedListProps } from './virtualized-list-props';
|
|
7
|
+
export type IScrollHandlers = Omit<IListHandlers, 'makeCellMeasure'> & {
|
|
8
|
+
measureCell: (index: Accessor<number>) => (event: ISymbioteEvent) => void;
|
|
9
|
+
focusCell: (index: Accessor<number>) => () => void;
|
|
10
|
+
scrollHandler: Accessor<unknown>;
|
|
11
|
+
};
|
|
12
|
+
export declare function createScrollHandlers<ItemT>(props: IVirtualizedListProps<ItemT>, driver: Pick<IListDriver<ItemT>, 'dispatch' | 'clearCommandedOffset' | 'getHostNode'>, sticky: Pick<IStickyState, 'forwarding' | 'scrollAnimatedValue'>, nesting: () => IListNesting<ItemT>): IScrollHandlers;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// Native scroll, layout and cell-measure events turned into reducer actions
|
|
2
|
+
import { createMemo } from 'solid-js';
|
|
3
|
+
import { LIST_ACTION_KIND, createListHandlers, forwardScrollEvent, resolveItemKey, } from '@symbiote-native/components';
|
|
4
|
+
import { event as animatedEvent, } from '@symbiote-native/engine';
|
|
5
|
+
export function createScrollHandlers(props, driver, sticky, nesting) {
|
|
6
|
+
const { dispatch } = driver;
|
|
7
|
+
const core = createListHandlers({
|
|
8
|
+
isHorizontal: () => props.horizontal === true,
|
|
9
|
+
user: () => props,
|
|
10
|
+
nesting,
|
|
11
|
+
dispatch,
|
|
12
|
+
clearCommandedOffset: driver.clearCommandedOffset,
|
|
13
|
+
getNode: driver.getHostNode,
|
|
14
|
+
keyFor: index => resolveItemKey(props.getItem(props.data, index), index, props.keyExtractor),
|
|
15
|
+
});
|
|
16
|
+
// Without the native animated module the sticky value is driven off the JS thread, so the
|
|
17
|
+
// handler is wrapped in `Animated.event`, the native and plain paths forward it untouched
|
|
18
|
+
const scrollHandler = createMemo(() => {
|
|
19
|
+
if (sticky.forwarding().mode !== 'sticky-js')
|
|
20
|
+
return core.onScroll;
|
|
21
|
+
return animatedEvent([{ nativeEvent: { contentOffset: { y: sticky.scrollAnimatedValue } } }], {
|
|
22
|
+
listener: (...args) => forwardScrollEvent(core.onScroll, args),
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
return {
|
|
26
|
+
...core,
|
|
27
|
+
scrollHandler,
|
|
28
|
+
measureCell: index => event => core.makeCellMeasure(index())(event),
|
|
29
|
+
focusCell: index => () => dispatch({ kind: LIST_ACTION_KIND.cellFocused, index: index() }),
|
|
30
|
+
};
|
|
31
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { JSX } from '../../jsx-runtime';
|
|
2
|
+
import { resolveAccessibilityProps, type IListNesting } from '@symbiote-native/components';
|
|
3
|
+
import { type ISymbioteNode } from '@symbiote-native/engine';
|
|
4
|
+
import type { IScrollHandlers } from './scroll-handlers';
|
|
5
|
+
import type { IStickyState } from './sticky-state';
|
|
6
|
+
import type { IListDriver } from './use-list-driver';
|
|
7
|
+
import type { IVirtualizedListProps } from './virtualized-list-props';
|
|
8
|
+
export type IScrollTreeDeps<ItemT> = {
|
|
9
|
+
props: IVirtualizedListProps<ItemT>;
|
|
10
|
+
accessibility: () => ReturnType<typeof resolveAccessibilityProps>;
|
|
11
|
+
driver: Pick<IListDriver<ItemT>, 'metrics' | 'setHostNode' | 'commandedOffset'>;
|
|
12
|
+
sticky: IStickyState;
|
|
13
|
+
handlers: IScrollHandlers;
|
|
14
|
+
nesting: IListNesting<ItemT>;
|
|
15
|
+
hasHeader: boolean;
|
|
16
|
+
body: () => JSX.Element;
|
|
17
|
+
};
|
|
18
|
+
export declare function createScrollTree<ItemT>(deps: IScrollTreeDeps<ItemT>): () => ISymbioteNode;
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
// The scroll host: the tag, its props, and the engine-owned content node the list body goes into
|
|
2
|
+
import { createEffect, createMemo, on, onCleanup } from 'solid-js';
|
|
3
|
+
import { attachStickyScroll, buildListScrollProps, resolveAccessibilityProps, selectScrollIntrinsics, } from '@symbiote-native/components';
|
|
4
|
+
import { dlog, isSymbioteNode, resolveClassName, whenCommitted, } from '@symbiote-native/engine';
|
|
5
|
+
import { createElement, insert, insertNode, spread } from '../../renderer.js';
|
|
6
|
+
import { withStableKeys } from '../../utils/stable-keys.js';
|
|
7
|
+
// A class-name string resolves through the shared registry before it reaches the intrinsic
|
|
8
|
+
// selector, which only understands style objects and arrays
|
|
9
|
+
function contentContainerStyleInput(props) {
|
|
10
|
+
const style = props.contentContainerStyle;
|
|
11
|
+
return typeof style === 'string' ? resolveClassName(style) : style;
|
|
12
|
+
}
|
|
13
|
+
// `withStableKeys` because several keys are conditional and Solid's `spread` has no removal pass, a
|
|
14
|
+
// key that vanished would keep its last value on the native view forever
|
|
15
|
+
function createOuterBag(deps) {
|
|
16
|
+
const { props, driver, sticky, handlers } = deps;
|
|
17
|
+
return withStableKeys(() => ({
|
|
18
|
+
...buildListScrollProps({
|
|
19
|
+
horizontal: props.horizontal === true,
|
|
20
|
+
inverted: props.inverted === true,
|
|
21
|
+
style: props.style,
|
|
22
|
+
contentContainerStyle: contentContainerStyleInput(props),
|
|
23
|
+
maintainVisibleContentPosition: props.maintainVisibleContentPosition,
|
|
24
|
+
onScrollBeginDrag: handlers.onScrollBeginDrag,
|
|
25
|
+
onScrollEndDrag: handlers.onScrollEndDrag,
|
|
26
|
+
onMomentumScrollBegin: handlers.onMomentumScrollBegin,
|
|
27
|
+
onMomentumScrollEnd: handlers.onMomentumScrollEnd,
|
|
28
|
+
onContentSizeChange: handlers.onContentSizeChange,
|
|
29
|
+
// The folded throttle, not the raw prop: it carries the sticky-mode default
|
|
30
|
+
scrollEventThrottle: sticky.forwarding().scrollEventThrottle,
|
|
31
|
+
keyboardShouldPersistTaps: props.keyboardShouldPersistTaps,
|
|
32
|
+
keyboardDismissMode: props.keyboardDismissMode,
|
|
33
|
+
removeClippedSubviews: props.removeClippedSubviews,
|
|
34
|
+
nestedScrollEnabled: props.nestedScrollEnabled,
|
|
35
|
+
stickyHeaderHiddenOnScroll: props.stickyHeaderHiddenOnScroll,
|
|
36
|
+
total: driver.metrics().total,
|
|
37
|
+
hasHeader: deps.hasHeader,
|
|
38
|
+
commandedOffset: driver.commandedOffset(),
|
|
39
|
+
onScroll: handlers.onScroll,
|
|
40
|
+
onLayout: handlers.onViewportLayout,
|
|
41
|
+
}, { ...deps.accessibility(), class: props.class }),
|
|
42
|
+
onScroll: handlers.scrollHandler(),
|
|
43
|
+
}));
|
|
44
|
+
}
|
|
45
|
+
// The list above scrolls, so RN renders a `View` with no content container
|
|
46
|
+
function createNestedBag(deps) {
|
|
47
|
+
const { props, handlers } = deps;
|
|
48
|
+
return withStableKeys(() => ({
|
|
49
|
+
...deps.accessibility(),
|
|
50
|
+
class: props.class,
|
|
51
|
+
style: props.style,
|
|
52
|
+
onLayout: handlers.onViewportLayout,
|
|
53
|
+
}));
|
|
54
|
+
}
|
|
55
|
+
function buildNestedTree(deps, onHost) {
|
|
56
|
+
dlog('VirtualizedList nested in a list of the same orientation -> view');
|
|
57
|
+
const view = hostElement('view');
|
|
58
|
+
onHost(view);
|
|
59
|
+
spread(view, createNestedBag(deps), true);
|
|
60
|
+
insert(view, deps.body);
|
|
61
|
+
return view;
|
|
62
|
+
}
|
|
63
|
+
function hostElement(tag) {
|
|
64
|
+
const node = createElement(tag);
|
|
65
|
+
// Narrowing, the renderer types `createElement` over a union that includes the surface
|
|
66
|
+
if (!isSymbioteNode(node)) {
|
|
67
|
+
throw new Error(`VirtualizedList: ${tag} did not create a host node`);
|
|
68
|
+
}
|
|
69
|
+
return node;
|
|
70
|
+
}
|
|
71
|
+
// Built once: `refreshing` is written as a CALL, so the compiler emits a getter and the controlled
|
|
72
|
+
// state keeps reaching this same node
|
|
73
|
+
function buildRefreshControl(props) {
|
|
74
|
+
const onRefresh = props.onRefresh;
|
|
75
|
+
if (onRefresh === undefined)
|
|
76
|
+
return undefined;
|
|
77
|
+
dlog('VirtualizedList wiring RefreshControl (onRefresh provided)');
|
|
78
|
+
const element = (<refresh-control refreshing={props.refreshing ?? false} onRefresh={onRefresh} progressViewOffset={props.progressViewOffset}/>);
|
|
79
|
+
return isSymbioteNode(element) ? element : undefined;
|
|
80
|
+
}
|
|
81
|
+
// The scroll host is wired to the ENGINE-OWNED content node, building a second one here would nest
|
|
82
|
+
// `RCTScrollContentView` inside the behavior's own
|
|
83
|
+
function buildTree(deps, outerBag, scrollViewIntrinsic, onHost) {
|
|
84
|
+
dlog(`VirtualizedList -> ${scrollViewIntrinsic}`);
|
|
85
|
+
const scroll = hostElement(scrollViewIntrinsic);
|
|
86
|
+
onHost(scroll);
|
|
87
|
+
spread(scroll, outerBag, true);
|
|
88
|
+
const content = scroll.childHost;
|
|
89
|
+
if (content === undefined) {
|
|
90
|
+
throw new Error(`VirtualizedList: ${scrollViewIntrinsic} built no content node, ` +
|
|
91
|
+
'is registerScrollViewBehavior() wired?');
|
|
92
|
+
}
|
|
93
|
+
insert(content, deps.body);
|
|
94
|
+
// An ordinary child on both platforms, the behavior's `claimedChildren` places it
|
|
95
|
+
const refresh = buildRefreshControl(deps.props);
|
|
96
|
+
if (refresh !== undefined)
|
|
97
|
+
insertNode(scroll, refresh);
|
|
98
|
+
// A wrap claim re-parents `scroll` under the RefreshControl, the wrapper is what occupies this
|
|
99
|
+
// subtree's place once claimed
|
|
100
|
+
return scroll.wrapper ?? scroll;
|
|
101
|
+
}
|
|
102
|
+
// Drives the sticky scroll value on the native UI thread, no JS per frame
|
|
103
|
+
// The engine commits on a microtask, so the node has no Fabric tag on the first run and
|
|
104
|
+
// `whenCommitted` is the retry
|
|
105
|
+
function attachSticky(deps, tree, hostNode) {
|
|
106
|
+
createEffect(() => {
|
|
107
|
+
// Read for the dependency: a rebuild means a NEW scroll node to attach to
|
|
108
|
+
tree();
|
|
109
|
+
if (deps.nesting.isNested || !deps.sticky.nativeStickyAvailable())
|
|
110
|
+
return;
|
|
111
|
+
const node = hostNode();
|
|
112
|
+
if (node === null)
|
|
113
|
+
return;
|
|
114
|
+
let detach;
|
|
115
|
+
const cancel = whenCommitted(node, () => {
|
|
116
|
+
detach = attachStickyScroll(node, deps.sticky.scrollAnimatedValue);
|
|
117
|
+
});
|
|
118
|
+
onCleanup(() => {
|
|
119
|
+
cancel();
|
|
120
|
+
detach?.();
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
export function createScrollTree(deps) {
|
|
125
|
+
const { props } = deps;
|
|
126
|
+
let scrollNode = null;
|
|
127
|
+
const outerBag = createOuterBag(deps);
|
|
128
|
+
const horizontalAxis = createMemo(() => String(props.horizontal === true));
|
|
129
|
+
// The axis picks a different host TAG and Solid cannot swap a tag under a live node, so the flip
|
|
130
|
+
// rebuilds, `on()` runs the build untracked so every other read re-props the same nodes
|
|
131
|
+
const tree = createMemo(on(horizontalAxis, () => {
|
|
132
|
+
const onHost = (node) => {
|
|
133
|
+
scrollNode = node;
|
|
134
|
+
deps.driver.setHostNode(node);
|
|
135
|
+
};
|
|
136
|
+
if (deps.nesting.isNested)
|
|
137
|
+
return buildNestedTree(deps, onHost);
|
|
138
|
+
const { scrollViewIntrinsic } = selectScrollIntrinsics(props.horizontal === true, contentContainerStyleInput(props));
|
|
139
|
+
return buildTree(deps, outerBag, scrollViewIntrinsic, onHost);
|
|
140
|
+
}));
|
|
141
|
+
attachSticky(deps, tree, () => scrollNode);
|
|
142
|
+
return tree;
|
|
143
|
+
}
|
|
@@ -1,75 +1,4 @@
|
|
|
1
|
-
import { type
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
import { type IClassNameValue, type IStyleProp, type ISymbioteEvent, type IViewStyle } from '@symbiote-native/engine';
|
|
5
|
-
export type { ICellLayout, ISeparators, ISeparatorProps, IViewToken, IViewableItemsChangedInfo, IViewabilityConfig, IViewabilityConfigCallbackPair, IVirtualizedListHandle, };
|
|
6
|
-
export interface IVirtualizedListCellInfo<ItemT> {
|
|
7
|
-
item: ItemT;
|
|
8
|
-
index: number;
|
|
9
|
-
separators: ISeparators;
|
|
10
|
-
}
|
|
11
|
-
export type IVirtualizedListRenderItem<ItemT> = (info: Accessor<IVirtualizedListCellInfo<ItemT>>) => JSX.Element;
|
|
12
|
-
export interface IVirtualizedListProps<ItemT> extends IAccessibilityProps, IAriaProps {
|
|
13
|
-
data: unknown;
|
|
14
|
-
getItem: (data: unknown, index: number) => ItemT;
|
|
15
|
-
getItemCount: (data: unknown) => number;
|
|
16
|
-
renderItem: IVirtualizedListRenderItem<ItemT>;
|
|
17
|
-
getItemLayout?: (data: unknown, index: number) => {
|
|
18
|
-
length: number;
|
|
19
|
-
offset: number;
|
|
20
|
-
index: number;
|
|
21
|
-
};
|
|
22
|
-
initialNumToRender?: number;
|
|
23
|
-
windowSize?: number;
|
|
24
|
-
ListHeaderComponent?: JSX.Element;
|
|
25
|
-
ListFooterComponent?: JSX.Element;
|
|
26
|
-
ListEmptyComponent?: JSX.Element;
|
|
27
|
-
ItemSeparatorComponent?: (props: ISeparatorProps<ItemT>) => JSX.Element;
|
|
28
|
-
ref?: Ref<IVirtualizedListHandle>;
|
|
29
|
-
onEndReached?: (info: {
|
|
30
|
-
distanceFromEnd: number;
|
|
31
|
-
}) => void;
|
|
32
|
-
onEndReachedThreshold?: number;
|
|
33
|
-
onStartReached?: (info: {
|
|
34
|
-
distanceFromStart: number;
|
|
35
|
-
}) => void;
|
|
36
|
-
onStartReachedThreshold?: number;
|
|
37
|
-
keyExtractor?: (item: ItemT, index: number) => string;
|
|
38
|
-
onViewableItemsChanged?: (info: IViewableItemsChangedInfo<ItemT>) => void;
|
|
39
|
-
viewabilityConfig?: IViewabilityConfig;
|
|
40
|
-
viewabilityConfigCallbackPairs?: IViewabilityConfigCallbackPair<ItemT>[];
|
|
41
|
-
onScroll?: (event: ISymbioteEvent) => void;
|
|
42
|
-
onScrollBeginDrag?: (event: ISymbioteEvent) => void;
|
|
43
|
-
onScrollEndDrag?: (event: ISymbioteEvent) => void;
|
|
44
|
-
onMomentumScrollBegin?: (event: ISymbioteEvent) => void;
|
|
45
|
-
onMomentumScrollEnd?: (event: ISymbioteEvent) => void;
|
|
46
|
-
scrollEventThrottle?: number;
|
|
47
|
-
keyboardShouldPersistTaps?: boolean | 'always' | 'never' | 'handled';
|
|
48
|
-
keyboardDismissMode?: 'none' | 'on-drag' | 'interactive';
|
|
49
|
-
removeClippedSubviews?: boolean;
|
|
50
|
-
nestedScrollEnabled?: boolean;
|
|
51
|
-
horizontal?: boolean;
|
|
52
|
-
inverted?: boolean;
|
|
53
|
-
style?: IStyleProp<IViewStyle>;
|
|
54
|
-
contentContainerStyle?: IStyleProp<IViewStyle> | string;
|
|
55
|
-
class?: IClassNameValue;
|
|
56
|
-
onRefresh?: () => void;
|
|
57
|
-
refreshing?: boolean | null;
|
|
58
|
-
progressViewOffset?: number;
|
|
59
|
-
stickyHeaderIndices?: number[];
|
|
60
|
-
maintainVisibleContentPosition?: {
|
|
61
|
-
minIndexForVisible: number;
|
|
62
|
-
autoscrollToTopThreshold?: number;
|
|
63
|
-
};
|
|
64
|
-
initialScrollIndex?: number;
|
|
65
|
-
maxToRenderPerBatch?: number;
|
|
66
|
-
updateCellsBatchingPeriod?: number;
|
|
67
|
-
extraData?: unknown;
|
|
68
|
-
onScrollToIndexFailed?: (info: {
|
|
69
|
-
index: number;
|
|
70
|
-
highestMeasuredFrameIndex: number;
|
|
71
|
-
averageItemLength: number;
|
|
72
|
-
}) => void;
|
|
73
|
-
}
|
|
74
|
-
export type IVirtualizedListComponent = <ItemT>(props: IVirtualizedListProps<ItemT>) => JSX.Element;
|
|
1
|
+
import { type IVirtualizedListComponent } from './virtualized-list-props';
|
|
2
|
+
export type { ICellLayout, ISeparators, ISeparatorProps, IViewToken, IViewableItemsChangedInfo, IViewabilityConfig, IViewabilityConfigCallbackPair, IVirtualizedListHandle, } from '@symbiote-native/components';
|
|
3
|
+
export type { ICellRendererComponent, ICellRendererProps, IVirtualizedListCellInfo, IVirtualizedListComponent, IVirtualizedListProps, IVirtualizedListRenderItem, } from './virtualized-list-props';
|
|
75
4
|
export declare function createVirtualizedList(): IVirtualizedListComponent;
|