@symbiote-native/solid 3.2.0 → 3.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/build/components/flat-list/index.d.ts +3 -2
  2. package/build/components/flat-list/index.jsx +8 -10
  3. package/build/components/input-accessory-view-props.d.ts +4 -4
  4. package/build/components/keyboard-avoiding-view.d.ts +2 -2
  5. package/build/components/keyboard-avoiding-view.jsx +34 -114
  6. package/build/components/layout-conformance-props.d.ts +6 -0
  7. package/build/components/layout-conformance-props.js +1 -0
  8. package/build/components/modal.d.ts +4 -4
  9. package/build/components/modal.jsx +11 -11
  10. package/build/components/refresh-control-props.d.ts +7 -7
  11. package/build/components/scroll-view-props.d.ts +8 -5
  12. package/build/components/text-input-props.d.ts +2 -1
  13. package/build/components/text-props.d.ts +3 -3
  14. package/build/components/touchable-props.d.ts +6 -6
  15. package/build/components/virtualized-list/index.android.d.ts +2 -2
  16. package/build/components/virtualized-list/index.ios.d.ts +2 -2
  17. package/build/components/virtualized-list/item-content.d.ts +6 -0
  18. package/build/components/virtualized-list/item-content.jsx +9 -0
  19. package/build/components/virtualized-list/list-body.d.ts +15 -0
  20. package/build/components/virtualized-list/list-body.jsx +20 -0
  21. package/build/components/virtualized-list/list-parts.d.ts +14 -0
  22. package/build/components/virtualized-list/list-parts.js +27 -0
  23. package/build/components/virtualized-list/list-rows.d.ts +18 -0
  24. package/build/components/virtualized-list/list-rows.jsx +91 -0
  25. package/build/components/virtualized-list/list-window.d.ts +11 -0
  26. package/build/components/virtualized-list/list-window.js +29 -0
  27. package/build/components/virtualized-list/nested-scope.d.ts +7 -0
  28. package/build/components/virtualized-list/nested-scope.js +21 -0
  29. package/build/components/virtualized-list/scroll-handlers.d.ts +12 -0
  30. package/build/components/virtualized-list/scroll-handlers.js +31 -0
  31. package/build/components/virtualized-list/scroll-tree.d.ts +18 -0
  32. package/build/components/virtualized-list/scroll-tree.jsx +143 -0
  33. package/build/components/virtualized-list/shared.d.ts +3 -74
  34. package/build/components/virtualized-list/shared.jsx +53 -752
  35. package/build/components/virtualized-list/sticky-binding.d.ts +13 -0
  36. package/build/components/virtualized-list/sticky-binding.js +111 -0
  37. package/build/components/virtualized-list/sticky-header.d.ts +1 -0
  38. package/build/components/virtualized-list/sticky-header.jsx +11 -159
  39. package/build/components/virtualized-list/sticky-state.d.ts +14 -0
  40. package/build/components/virtualized-list/sticky-state.js +54 -0
  41. package/build/components/virtualized-list/use-list-driver.d.ts +22 -0
  42. package/build/components/virtualized-list/use-list-driver.js +128 -0
  43. package/build/components/virtualized-list/use-separators.d.ts +7 -0
  44. package/build/components/virtualized-list/use-separators.js +22 -0
  45. package/build/components/virtualized-list/virtualized-list-props.d.ts +86 -0
  46. package/build/components/virtualized-list/virtualized-list-props.js +53 -0
  47. package/build/components/virtualized-section-list/index.d.ts +22 -9
  48. package/build/components/virtualized-section-list/index.jsx +87 -115
  49. package/build/components/virtualized-section-list/section-item-cell.d.ts +18 -0
  50. package/build/components/virtualized-section-list/section-item-cell.jsx +37 -0
  51. package/build/components.d.ts +5 -1
  52. package/build/components.js +1 -0
  53. package/build/index.d.ts +10 -4
  54. package/build/index.js +21 -37
  55. package/build/modules/animated/index.d.ts +54 -41
  56. package/build/modules/animated/index.js +2 -26
  57. package/build/modules/app-registry/index.d.ts +3 -3
  58. package/build/modules/app-registry/index.js +2 -2
  59. package/build/modules/status-bar.js +14 -27
  60. package/build/primitives/create-color-scheme.d.ts +1 -1
  61. package/build/render.js +16 -28
  62. package/build/root-tag-context.d.ts +1 -0
  63. package/build/root-tag-context.js +4 -0
  64. 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 Accessor, type Ref } from 'solid-js';
2
- import type { JSX } from '../../jsx-runtime';
3
- import { type IAccessibilityProps, type IAriaProps, type ICellLayout, type IViewabilityConfig, type IViewabilityConfigCallbackPair, type IViewableItemsChangedInfo, type ISeparatorProps, type ISeparators, type IViewToken, type IVirtualizedListHandle } from '@symbiote-native/components';
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;