@symbiote-native/react 3.3.0 → 3.4.1

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 (78) hide show
  1. package/build/batched-updates.d.ts +2 -0
  2. package/build/batched-updates.js +5 -0
  3. package/build/bootstrap.d.ts +1 -0
  4. package/build/bootstrap.js +6 -5
  5. package/build/components/flat-list/index.d.ts +16 -13
  6. package/build/components/flat-list/index.js +86 -72
  7. package/build/components/input-accessory-view-props.d.ts +4 -3
  8. package/build/components/keyboard-avoiding-view/index.d.ts +2 -2
  9. package/build/components/keyboard-avoiding-view/index.js +45 -84
  10. package/build/components/layout-conformance-props.d.ts +6 -0
  11. package/build/components/layout-conformance-props.js +1 -0
  12. package/build/components/modal/index.d.ts +4 -4
  13. package/build/components/modal/index.js +47 -18
  14. package/build/components/refresh-control-props.d.ts +7 -6
  15. package/build/components/scroll-view/scroll-view-props.d.ts +8 -5
  16. package/build/components/text-input/text-input-props.d.ts +4 -0
  17. package/build/components/touchable/touchable-props.d.ts +6 -5
  18. package/build/components/virtualized-list/index.d.ts +3 -70
  19. package/build/components/virtualized-list/index.js +50 -548
  20. package/build/components/virtualized-list/list-children.d.ts +13 -0
  21. package/build/components/virtualized-list/list-children.js +79 -0
  22. package/build/components/virtualized-list/list-config.d.ts +11 -0
  23. package/build/components/virtualized-list/list-config.js +14 -0
  24. package/build/components/virtualized-list/list-elements.d.ts +11 -0
  25. package/build/components/virtualized-list/list-elements.js +26 -0
  26. package/build/components/virtualized-list/list-host.d.ts +14 -0
  27. package/build/components/virtualized-list/list-host.js +24 -0
  28. package/build/components/virtualized-list/list-scroll-props.d.ts +32 -0
  29. package/build/components/virtualized-list/list-scroll-props.js +30 -0
  30. package/build/components/virtualized-list/nested-scope.d.ts +10 -0
  31. package/build/components/virtualized-list/nested-scope.js +16 -0
  32. package/build/components/virtualized-list/use-list-driver.d.ts +22 -0
  33. package/build/components/virtualized-list/use-list-driver.js +104 -0
  34. package/build/components/virtualized-list/use-list-nesting.d.ts +12 -0
  35. package/build/components/virtualized-list/use-list-nesting.js +37 -0
  36. package/build/components/virtualized-list/use-list-parts.d.ts +12 -0
  37. package/build/components/virtualized-list/use-list-parts.js +20 -0
  38. package/build/components/virtualized-list/use-scroll-handlers.d.ts +17 -0
  39. package/build/components/virtualized-list/use-scroll-handlers.js +106 -0
  40. package/build/components/virtualized-list/use-separators.d.ts +8 -0
  41. package/build/components/virtualized-list/use-separators.js +17 -0
  42. package/build/components/virtualized-list/virtualized-list-props.d.ts +85 -0
  43. package/build/components/virtualized-list/virtualized-list-props.js +2 -0
  44. package/build/components/virtualized-section-list/index.d.ts +24 -8
  45. package/build/components/virtualized-section-list/index.js +89 -93
  46. package/build/components/virtualized-section-list/section-item-cell.d.ts +23 -0
  47. package/build/components/virtualized-section-list/section-item-cell.js +48 -0
  48. package/build/components.d.ts +5 -5
  49. package/build/hooks/use-animated-value.d.ts +7 -0
  50. package/build/hooks/use-animated-value.js +19 -0
  51. package/build/hooks/use-color-scheme.d.ts +2 -2
  52. package/build/hooks/use-color-scheme.js +4 -13
  53. package/build/hooks/use-pressability.d.ts +34 -0
  54. package/build/hooks/use-pressability.js +39 -0
  55. package/build/hooks/use-window-dimensions.js +6 -10
  56. package/build/host-config.js +6 -5
  57. package/build/index.d.ts +19 -5
  58. package/build/index.js +20 -14
  59. package/build/jsx-runtime.d.ts +15 -16
  60. package/build/modules/animated/create-animated-component.d.ts +2 -2
  61. package/build/modules/animated/create-animated-component.js +10 -22
  62. package/build/modules/animated/index.d.ts +54 -41
  63. package/build/modules/animated/index.js +2 -27
  64. package/build/modules/app-registry/index.d.ts +3 -3
  65. package/build/modules/app-registry/index.js +2 -2
  66. package/build/modules/status-bar/index.android.d.ts +1 -1
  67. package/build/modules/status-bar/index.android.js +7 -27
  68. package/build/modules/status-bar/index.ios.d.ts +1 -1
  69. package/build/modules/status-bar/index.ios.js +4 -28
  70. package/build/modules/status-bar/shared.d.ts +5 -4
  71. package/build/modules/status-bar/shared.js +33 -6
  72. package/build/native-host-tags.d.ts +2 -0
  73. package/build/native-host-tags.js +4 -0
  74. package/build/render.d.ts +1 -1
  75. package/build/render.js +3 -1
  76. package/build/root-tag-context.d.ts +1 -0
  77. package/build/root-tag-context.js +4 -0
  78. package/package.json +6 -6
@@ -0,0 +1,79 @@
1
+ // The list's child elements: header, the windowed cells with their spacers, footer
2
+ import { createElement } from 'react';
3
+ import { FIRST_INDEX, LIST_SEGMENT_KIND, cellStyleOf, listStyleOf, planFromMetrics, resolveItemKey, spacerStyleOf, } from '@symbiote-native/components';
4
+ import { renderItemElement, renderSeparatorElement, resolveElement, } from './list-elements.js';
5
+ function spacerElement(segment, horizontal) {
6
+ return createElement('view', {
7
+ key: segment.key,
8
+ style: spacerStyleOf(segment.extent, horizontal),
9
+ });
10
+ }
11
+ // The separator lives INSIDE the cell's measuring wrapper, a sibling would be an extra flex child
12
+ // that lands every cell below a collapsed spacer short by the separator plus the gap
13
+ // It gates on the last index of the DATA, or a cell's measured height would change with the window
14
+ function separatorOf(args, index) {
15
+ const { config, metrics, separators } = args;
16
+ if (index >= metrics.count - 1)
17
+ return undefined;
18
+ return renderSeparatorElement(config.ItemSeparatorComponent, config.getItem(config.data, index), config.getItem(config.data, index + 1), separators.overridesRef.current.get(index));
19
+ }
20
+ // A cell the app flagged sticky is wrapped in the `sticky-header` TAG, which pins by document
21
+ // order and so survives windowing, nothing here computes a child index
22
+ function cellElement(args, segment, stickySet) {
23
+ const { config, separators } = args;
24
+ const item = config.getItem(config.data, segment.index);
25
+ const content = renderItemElement(config, {
26
+ item,
27
+ index: segment.index,
28
+ separators: separators.makeSeparators(segment.index),
29
+ });
30
+ const isSticky = stickySet?.has(segment.index) === true;
31
+ const cellProps = {
32
+ key: `cell-${segment.key}`,
33
+ onLayout: args.measureCell(segment.index),
34
+ onFocus: args.focusCell(segment.index),
35
+ style: cellStyleOf(config),
36
+ };
37
+ const separator = separatorOf(args, segment.index);
38
+ if (config.CellRendererComponent === undefined) {
39
+ return createElement(isSticky ? 'sticky-header' : 'view', cellProps, content, separator);
40
+ }
41
+ const custom = createElement(config.CellRendererComponent, { ...cellProps, cellKey: segment.key, index: segment.index, item }, content, separator);
42
+ // RN pins the custom component itself, the sticky wrapper only holds it
43
+ return isSticky
44
+ ? createElement('sticky-header', { key: cellProps.key }, custom)
45
+ : custom;
46
+ }
47
+ function windowChildren(args) {
48
+ const { config, metrics } = args;
49
+ if (metrics.count === FIRST_INDEX) {
50
+ const empty = resolveElement(config.ListEmptyComponent);
51
+ return empty === undefined
52
+ ? []
53
+ : [
54
+ createElement('view', { key: 'list-empty', style: listStyleOf(config, undefined) }, empty),
55
+ ];
56
+ }
57
+ const { plan, stickySet } = planFromMetrics(metrics, index => resolveItemKey(config.getItem(config.data, index), index, config.keyExtractor), config.stickyHeaderIndices);
58
+ return plan.segments.map(segment => segment.kind === LIST_SEGMENT_KIND.spacer
59
+ ? spacerElement(segment, config.horizontal)
60
+ : cellElement(args, segment, stickySet));
61
+ }
62
+ // RN wires `onLayout` on the header wrapper only to assign `_headerLength` in VirtualizedList.js
63
+ // (facebook/react-native, `_onLayoutHeader`); nothing reads it, so no handler is registered here
64
+ function slotChild(config, slot, slotStyle, key) {
65
+ const element = resolveElement(slot);
66
+ return element === undefined
67
+ ? []
68
+ : [
69
+ createElement('view', { key, style: listStyleOf(config, slotStyle) }, element),
70
+ ];
71
+ }
72
+ export function buildListChildren(args) {
73
+ const { config } = args;
74
+ return [
75
+ ...slotChild(config, config.ListHeaderComponent, config.ListHeaderComponentStyle, 'list-header'),
76
+ ...windowChildren(args),
77
+ ...slotChild(config, config.ListFooterComponent, config.ListFooterComponentStyle, 'list-footer'),
78
+ ];
79
+ }
@@ -0,0 +1,11 @@
1
+ import type { IVirtualizedListProps } from './virtualized-list-props';
2
+ export type IListConfig<ItemT> = IVirtualizedListProps<ItemT> & {
3
+ horizontal: boolean;
4
+ inverted: boolean;
5
+ initialNumToRender: number;
6
+ maxToRenderPerBatch: number;
7
+ updateCellsBatchingPeriod: number;
8
+ windowSize: number;
9
+ findFirstChildWithMore?: (first: number, last: number) => number | null;
10
+ };
11
+ export declare function resolveListConfig<ItemT>(props: IVirtualizedListProps<ItemT>, findFirstChildWithMore?: IListConfig<ItemT>['findFirstChildWithMore']): IListConfig<ItemT>;
@@ -0,0 +1,14 @@
1
+ // Props with their defaults applied
2
+ import { DEFAULT_INITIAL_NUM_TO_RENDER, DEFAULT_MAX_TO_RENDER_PER_BATCH, DEFAULT_UPDATE_CELLS_BATCHING_PERIOD, DEFAULT_WINDOW_SIZE, } from '@symbiote-native/components';
3
+ export function resolveListConfig(props, findFirstChildWithMore) {
4
+ return {
5
+ ...props,
6
+ findFirstChildWithMore,
7
+ horizontal: props.horizontal ?? false,
8
+ inverted: props.inverted ?? false,
9
+ initialNumToRender: props.initialNumToRender ?? DEFAULT_INITIAL_NUM_TO_RENDER,
10
+ maxToRenderPerBatch: props.maxToRenderPerBatch ?? DEFAULT_MAX_TO_RENDER_PER_BATCH,
11
+ updateCellsBatchingPeriod: props.updateCellsBatchingPeriod ?? DEFAULT_UPDATE_CELLS_BATCHING_PERIOD,
12
+ windowSize: props.windowSize ?? DEFAULT_WINDOW_SIZE,
13
+ };
14
+ }
@@ -0,0 +1,11 @@
1
+ import { type ComponentType, type ReactNode } from 'react';
2
+ import { type ISeparatorProps } from '@symbiote-native/components';
3
+ import type { IListItemInfo, IListSlot, IRenderItem, ISeparatorComponent } from './virtualized-list-props';
4
+ type IItemRenderers<ItemT> = {
5
+ renderItem?: IRenderItem<ItemT>;
6
+ ListItemComponent?: ComponentType<IListItemInfo<ItemT>>;
7
+ };
8
+ export declare function renderItemElement<ItemT>(renderers: IItemRenderers<ItemT>, info: IListItemInfo<ItemT>): ReactNode;
9
+ export declare function resolveElement(component: IListSlot): ReactNode;
10
+ export declare function renderSeparatorElement<ItemT>(component: ISeparatorComponent<ItemT> | undefined, leadingItem: ItemT, trailingItem: ItemT, overrides: Partial<ISeparatorProps<ItemT>> | undefined): ReactNode;
11
+ export {};
@@ -0,0 +1,26 @@
1
+ // Small element builders shared by the list's child walk
2
+ import { createElement, isValidElement, } from 'react';
3
+ import { buildSeparatorProps, drawItem, } from '@symbiote-native/components';
4
+ // RN's `_renderElement`: the component wins over `renderItem`, one of the two has to be there
5
+ export function renderItemElement(renderers, info) {
6
+ return drawItem({
7
+ renderItem: renderers.renderItem,
8
+ component: renderers.ListItemComponent,
9
+ }, info, (Component, given) => createElement(Component, given));
10
+ }
11
+ export function resolveElement(component) {
12
+ if (component === undefined)
13
+ return undefined;
14
+ if (typeof component === 'function')
15
+ return createElement(component, {});
16
+ return component;
17
+ }
18
+ // The `ItemSeparatorComponent` element for the gap between two items, with the highlight flag and
19
+ // any handle-pushed overrides merged on top, RN renders `<ItemSeparatorComponent {...props} />`
20
+ export function renderSeparatorElement(component, leadingItem, trailingItem, overrides) {
21
+ if (component === undefined)
22
+ return undefined;
23
+ if (isValidElement(component))
24
+ return component;
25
+ return createElement(component, buildSeparatorProps(leadingItem, trailingItem, overrides));
26
+ }
@@ -0,0 +1,14 @@
1
+ import { type ReactElement, type ReactNode, type RefObject } from 'react';
2
+ import type { ISymbioteNode } from '@symbiote-native/engine';
3
+ import type { IAccessibilityProps, IAriaProps, IListNesting } from '@symbiote-native/components';
4
+ import type { IListConfig } from './list-config';
5
+ import { type IScrollTagProps } from './list-scroll-props';
6
+ export type IListHostArgs<ItemT> = {
7
+ config: IListConfig<ItemT>;
8
+ nesting: IListNesting<ItemT>;
9
+ scrollProps: IScrollTagProps;
10
+ accessibilityRest: IAccessibilityProps & IAriaProps;
11
+ nodeRef: RefObject<ISymbioteNode | null>;
12
+ children: ReactNode[];
13
+ };
14
+ export declare function listHostOf<ItemT>(args: IListHostArgs<ItemT>): ReactElement;
@@ -0,0 +1,24 @@
1
+ // The host element a list renders: the scroll tag, or a plain view when it is nested in a list
2
+ import { createElement, } from 'react';
3
+ import { refreshControlOf } from './list-scroll-props.js';
4
+ import { VirtualizedListScopeProvider } from './nested-scope.js';
5
+ // The scroll TAG, not a wrapper: the engine builds the content node, `ref` hands back the engine
6
+ // node that `buildScrollViewHandle` drives
7
+ function scrollTagOf(args) {
8
+ const { config, scrollProps, nodeRef, children } = args;
9
+ return createElement(config.horizontal ? 'horizontal-scroll-view' : 'scroll-view', { ...scrollProps, ref: nodeRef }, refreshControlOf(config), ...children);
10
+ }
11
+ // The list above scrolls, so RN renders a `View` with no content container
12
+ function nestedViewOf(args) {
13
+ const { scrollProps, accessibilityRest, nodeRef, children } = args;
14
+ return createElement('view', {
15
+ ...accessibilityRest,
16
+ style: scrollProps.style,
17
+ onLayout: scrollProps.onLayout,
18
+ ref: nodeRef,
19
+ }, ...children);
20
+ }
21
+ export function listHostOf(args) {
22
+ const { nesting } = args;
23
+ return createElement(VirtualizedListScopeProvider, { scope: nesting.scope }, nesting.isNested ? nestedViewOf(args) : scrollTagOf(args));
24
+ }
@@ -0,0 +1,32 @@
1
+ import { type ReactElement } from 'react';
2
+ import type { ISymbioteEvent } from '@symbiote-native/engine';
3
+ import { type IAccessibilityProps, type IAriaProps } from '@symbiote-native/components';
4
+ import type { IScrollViewProps } from '../scroll-view/scroll-view-props';
5
+ import type { IListConfig } from './list-config';
6
+ import type { IVirtualizedListProps } from './virtualized-list-props';
7
+ export type IScrollTagProps = IScrollViewProps & {
8
+ onLayout: (event: ISymbioteEvent) => void;
9
+ isInvertedVirtualizedList?: boolean;
10
+ };
11
+ export declare function accessibilityRestOf<ItemT>(props: IVirtualizedListProps<ItemT> & {
12
+ ref?: unknown;
13
+ }): IAccessibilityProps & IAriaProps;
14
+ export declare function refreshControlOf<ItemT>(config: IListConfig<ItemT>): ReactElement | undefined;
15
+ export type IScrollTagInputs<ItemT> = {
16
+ config: IListConfig<ItemT>;
17
+ accessibilityRest: IAccessibilityProps & IAriaProps;
18
+ total: number;
19
+ onScroll: (event: ISymbioteEvent) => void;
20
+ onScrollBeginDrag: (event: ISymbioteEvent) => void;
21
+ onScrollEndDrag: (event: ISymbioteEvent) => void;
22
+ onMomentumScrollBegin: (event: ISymbioteEvent) => void;
23
+ onMomentumScrollEnd: (event: ISymbioteEvent) => void;
24
+ onContentSizeChange: (width: number, height: number) => void;
25
+ onLayout: (event: ISymbioteEvent) => void;
26
+ commandedOffset: {
27
+ x: number;
28
+ y: number;
29
+ } | undefined;
30
+ hasHeader: boolean;
31
+ };
32
+ export declare function buildScrollProps<ItemT>(inputs: IScrollTagInputs<ItemT>): IScrollTagProps;
@@ -0,0 +1,30 @@
1
+ // The props handed to the scroll tag, and the accessibility rest that rides down with them
2
+ import { createElement } from 'react';
3
+ import { buildListScrollProps, } from '@symbiote-native/components';
4
+ // The data access protocol and the slots, none of which the scroll tag takes
5
+ function withoutDataAndSlots(props) {
6
+ const { data, getItem, getItemCount, renderItem, ListItemComponent, keyExtractor, getItemLayout, ItemSeparatorComponent, CellRendererComponent, ListHeaderComponent, ListFooterComponent, ListEmptyComponent, ...rest } = props;
7
+ return rest;
8
+ }
9
+ // Everything the list does not consume itself is accessibility surface for the ScrollView
10
+ // The user's `onScroll` is among the consumed keys, composed with the internal handler instead
11
+ export function accessibilityRestOf(props) {
12
+ const { horizontal, inverted, extraData, onEndReached, onEndReachedThreshold, onStartReached, onStartReachedThreshold, onRefresh, refreshing, progressViewOffset, onViewableItemsChanged, viewabilityConfig, viewabilityConfigCallbackPairs, onScrollToIndexFailed, initialNumToRender, initialScrollIndex, maxToRenderPerBatch, updateCellsBatchingPeriod, windowSize, disableVirtualization, stickyHeaderIndices, maintainVisibleContentPosition, style, contentContainerStyle, onScroll, onScrollBeginDrag, onScrollEndDrag, onMomentumScrollBegin, onMomentumScrollEnd, scrollEventThrottle, keyboardShouldPersistTaps, keyboardDismissMode, ref, ...rest } = withoutDataAndSlots(props);
13
+ return rest;
14
+ }
15
+ // With `onRefresh` the RefreshControl is an ordinary FIRST CHILD on both platforms, the scroll
16
+ // behavior claims it and places it per platform
17
+ export function refreshControlOf(config) {
18
+ if (config.onRefresh === undefined)
19
+ return undefined;
20
+ return createElement('refresh-control', {
21
+ key: 'refresh-control',
22
+ refreshing: config.refreshing ?? false,
23
+ onRefresh: config.onRefresh,
24
+ progressViewOffset: config.progressViewOffset,
25
+ });
26
+ }
27
+ export function buildScrollProps(inputs) {
28
+ const { config, accessibilityRest, ...tagInputs } = inputs;
29
+ return buildListScrollProps({ ...config, ...tagInputs }, accessibilityRest);
30
+ }
@@ -0,0 +1,10 @@
1
+ import { type ReactElement, type ReactNode } from 'react';
2
+ import type { IListScope } from '@symbiote-native/components';
3
+ export declare function useVirtualizedListScope(): IListScope | null;
4
+ export declare function VirtualizedListScopeProvider(props: {
5
+ scope: IListScope;
6
+ children?: ReactNode;
7
+ }): ReactElement;
8
+ export declare function VirtualizedListScopeResetter(props: {
9
+ children?: ReactNode;
10
+ }): ReactElement;
@@ -0,0 +1,16 @@
1
+ // The scope a list hands down to the lists nested in its cells, React's way to carry it
2
+ // What it holds and how it behaves is `createListNesting` in the shared components package
3
+ import { createContext, createElement, useContext, } from 'react';
4
+ const VirtualizedListScope = createContext(null);
5
+ // Null outside every list, the scope of the nearest list above otherwise
6
+ export function useVirtualizedListScope() {
7
+ return useContext(VirtualizedListScope);
8
+ }
9
+ export function VirtualizedListScopeProvider(props) {
10
+ return createElement(VirtualizedListScope.Provider, { value: props.scope }, props.children);
11
+ }
12
+ // A portal-like component (Modal) renders outside its parent list's scroll, so its content must
13
+ // not count itself as nested in it
14
+ export function VirtualizedListScopeResetter(props) {
15
+ return createElement(VirtualizedListScope.Provider, { value: null }, props.children);
16
+ }
@@ -0,0 +1,22 @@
1
+ import { type RefObject } from 'react';
2
+ import { type ISymbioteNode } from '@symbiote-native/engine';
3
+ import { type IListAction, type IListState } from '@symbiote-native/components';
4
+ import type { IScrollViewHandle } from '../scroll-view/scroll-view-props';
5
+ import type { IListConfig } from './list-config';
6
+ export type ICommandedOffset = {
7
+ x: number;
8
+ y: number;
9
+ };
10
+ export type IScrollTarget = {
11
+ nodeRef: RefObject<ISymbioteNode | null>;
12
+ handle: IScrollViewHandle;
13
+ commandedOffset: ICommandedOffset | undefined;
14
+ clearCommandedOffset: () => void;
15
+ scrollToPixel: (offset: number, animated: boolean) => void;
16
+ };
17
+ export type IListDriver<ItemT> = {
18
+ state: IListState<ItemT>;
19
+ dispatch: (action: IListAction<ItemT>) => void;
20
+ scroll: IScrollTarget;
21
+ };
22
+ export declare function useListDriver<ItemT>(config: IListConfig<ItemT>): IListDriver<ItemT>;
@@ -0,0 +1,104 @@
1
+ // The list's lifecycle: one folded state cell, native events turned into reducer actions, and the
2
+ // returned effects run with React primitives
3
+ import { useCallback, useEffect, useLayoutEffect, useMemo, useReducer, useRef, useState, } from 'react';
4
+ import { dlog } from '@symbiote-native/engine';
5
+ import { EMPTY_OFFSET, LIST_ACTION_KIND, buildListReducerInputs, buildScrollViewHandle, buildViewabilityPairs, clearTimers, createInitialListState, listEffectSignature, reduceList, runListEffects, } from '@symbiote-native/components';
6
+ // A `ref` on the scroll tag hands back the engine node, `buildScrollViewHandle` turns it into
7
+ // RN's scroll surface, the node is null until commit
8
+ function useScrollTarget(axisRef) {
9
+ const nodeRef = useRef(null);
10
+ const handle = useMemo(() => buildScrollViewHandle(() => nodeRef.current), []);
11
+ const [commandedOffset, setCommandedOffset] = useState(undefined);
12
+ const scrollToPixel = useCallback((offset, animated) => {
13
+ const clamped = Math.max(EMPTY_OFFSET, offset);
14
+ const horizontal = axisRef.current.horizontal;
15
+ const target = horizontal
16
+ ? { x: clamped, y: EMPTY_OFFSET }
17
+ : { x: EMPTY_OFFSET, y: clamped };
18
+ if (nodeRef.current === null) {
19
+ dlog(`VirtualizedList scrollTo offset=${clamped} pending-ref (horizontal=${horizontal})`);
20
+ setCommandedOffset(target);
21
+ return;
22
+ }
23
+ dlog(`VirtualizedList scrollTo offset=${clamped} animated=${animated} (horizontal=${horizontal})`);
24
+ handle.scrollTo({ x: target.x, y: target.y, animated });
25
+ }, [handle, axisRef]);
26
+ const clearCommandedOffset = useCallback(() => setCommandedOffset(undefined), []);
27
+ return {
28
+ nodeRef,
29
+ handle,
30
+ commandedOffset,
31
+ clearCommandedOffset,
32
+ scrollToPixel,
33
+ };
34
+ }
35
+ // The latest value of every render, read by handlers that must stay stable
36
+ function useLatest(value) {
37
+ const ref = useRef(value);
38
+ ref.current = value;
39
+ return ref;
40
+ }
41
+ function useListState() {
42
+ const stateRef = useRef(null);
43
+ return (stateRef.current ??= createInitialListState());
44
+ }
45
+ // Dispatch and the effect host recurse into each other (a refill or a fired debounce dispatches a
46
+ // follow-up action), so the host reaches `dispatch` through a ref
47
+ function useDispatch(state, parts) {
48
+ const { inputsRef, callbacksRef, scrollToPixel } = parts;
49
+ const [, forceRender] = useReducer((tick) => tick + 1, 0);
50
+ const dispatchRef = useRef(() => { });
51
+ const viewableTimer = useRef(null);
52
+ const batchTimer = useRef(null);
53
+ const host = useMemo(() => ({
54
+ callbacks: () => callbacksRef.current,
55
+ scrollToPixel,
56
+ dispatch: () => dispatchRef.current,
57
+ viewableTimer,
58
+ batchTimer,
59
+ }), [callbacksRef, scrollToPixel]);
60
+ const dispatch = useCallback((action) => {
61
+ const result = reduceList(state, action, inputsRef.current);
62
+ runListEffects(result.effects, host);
63
+ if (result.changed)
64
+ forceRender();
65
+ }, [state, inputsRef, host]);
66
+ dispatchRef.current = dispatch;
67
+ useEffect(() => () => clearTimers([viewableTimer, batchTimer]), [viewableTimer, batchTimer]);
68
+ return { dispatch, host };
69
+ }
70
+ // The after-commit pass runs in a layout effect so MVCP's shift lands before paint, and only when
71
+ // the signature changed, the same dedup key every adapter shares
72
+ function useCommitPass(state, inputsRef, host) {
73
+ const commitSignature = listEffectSignature(state);
74
+ useLayoutEffect(() => {
75
+ const result = reduceList(state, { kind: LIST_ACTION_KIND.commit }, inputsRef.current);
76
+ runListEffects(result.effects, host);
77
+ }, [commitSignature, state, inputsRef, host]);
78
+ }
79
+ export function useListDriver(config) {
80
+ const viewabilityPairs = useMemo(() => buildViewabilityPairs(config.onViewableItemsChanged, config.viewabilityConfig, config.viewabilityConfigCallbackPairs), [
81
+ config.onViewableItemsChanged,
82
+ config.viewabilityConfig,
83
+ config.viewabilityConfigCallbackPairs,
84
+ ]);
85
+ const inputs = buildListReducerInputs(config, viewabilityPairs);
86
+ const inputsRef = useLatest(inputs);
87
+ const callbacksRef = useLatest({
88
+ onEndReached: config.onEndReached,
89
+ onStartReached: config.onStartReached,
90
+ onScrollToIndexFailed: config.onScrollToIndexFailed,
91
+ viewabilityPairs,
92
+ });
93
+ const state = useListState();
94
+ const scroll = useScrollTarget(inputsRef);
95
+ const { dispatch, host } = useDispatch(state, {
96
+ inputsRef,
97
+ callbacksRef,
98
+ scrollToPixel: scroll.scrollToPixel,
99
+ });
100
+ // The single derive per render, before anything reads the metrics
101
+ reduceList(state, { kind: LIST_ACTION_KIND.refreshMetrics }, inputs);
102
+ useCommitPass(state, inputsRef, host);
103
+ return { state, dispatch, scroll };
104
+ }
@@ -0,0 +1,12 @@
1
+ import { type RefObject } from 'react';
2
+ import { type IListNesting, type IListScope, type INestedHandlers } from '@symbiote-native/components';
3
+ import type { IListConfig } from './list-config';
4
+ import type { IListDriver } from './use-list-driver';
5
+ export declare function useNestedHandlersRef(): RefObject<INestedHandlers>;
6
+ export type INestingArgs<ItemT> = {
7
+ config: IListConfig<ItemT>;
8
+ driver: IListDriver<ItemT>;
9
+ parent: IListScope | null;
10
+ handlersRef: RefObject<INestedHandlers>;
11
+ };
12
+ export declare function useListNesting<ItemT>(args: INestingArgs<ItemT>): IListNesting<ItemT>;
@@ -0,0 +1,37 @@
1
+ // The list's place among nested lists: the scope it hands down and its link to the list above
2
+ import { useEffect, useMemo, useRef } from 'react';
3
+ import { createListNesting, listHasMore, resolveItemKey, } from '@symbiote-native/components';
4
+ const noop = () => { };
5
+ const NO_HANDLERS = {
6
+ onScroll: noop,
7
+ onScrollBeginDrag: noop,
8
+ onScrollEndDrag: noop,
9
+ onMomentumScrollBegin: noop,
10
+ onMomentumScrollEnd: noop,
11
+ recordInteraction: noop,
12
+ };
13
+ // Holds the list's own handlers once they exist
14
+ export function useNestedHandlersRef() {
15
+ return useRef(NO_HANDLERS);
16
+ }
17
+ export function useListNesting(args) {
18
+ const { parent, handlersRef } = args;
19
+ const latest = useRef(args);
20
+ latest.current = args;
21
+ const horizontal = args.config.horizontal;
22
+ const nesting = useMemo(() => createListNesting({
23
+ parent,
24
+ horizontal,
25
+ getState: () => latest.current.driver.state,
26
+ dispatch: action => latest.current.driver.dispatch(action),
27
+ getContainerNode: () => latest.current.driver.scroll.nodeRef.current,
28
+ getHasMore: () => listHasMore(latest.current.driver.state),
29
+ keyFor: index => {
30
+ const { config } = latest.current;
31
+ return resolveItemKey(config.getItem(config.data, index), index, config.keyExtractor);
32
+ },
33
+ handlers: () => handlersRef.current,
34
+ }), [parent, horizontal, handlersRef]);
35
+ useEffect(() => () => nesting.detach(), [nesting]);
36
+ return nesting;
37
+ }
@@ -0,0 +1,12 @@
1
+ import type { IListNesting } from '@symbiote-native/components';
2
+ import { type IListConfig } from './list-config';
3
+ import { type IListDriver } from './use-list-driver';
4
+ import { type IScrollHandlers } from './use-scroll-handlers';
5
+ import type { IVirtualizedListProps } from './virtualized-list-props';
6
+ export type IListParts<ItemT> = {
7
+ config: IListConfig<ItemT>;
8
+ driver: IListDriver<ItemT>;
9
+ nesting: IListNesting<ItemT>;
10
+ handlers: IScrollHandlers;
11
+ };
12
+ export declare function useListParts<ItemT>(props: IVirtualizedListProps<ItemT>): IListParts<ItemT>;
@@ -0,0 +1,20 @@
1
+ // What a list renders from: config, folded state, place among nested lists, handlers
2
+ import { useRef } from 'react';
3
+ import { resolveListConfig } from './list-config.js';
4
+ import { useVirtualizedListScope } from './nested-scope.js';
5
+ import { useListDriver } from './use-list-driver.js';
6
+ import { useListNesting, useNestedHandlersRef } from './use-list-nesting.js';
7
+ import { useScrollHandlers } from './use-scroll-handlers.js';
8
+ export function useListParts(props) {
9
+ const parent = useVirtualizedListScope();
10
+ // The config exists before the nesting does, so the window reads it through this ref
11
+ const nestingRef = useRef(null);
12
+ const config = resolveListConfig(props, (first, last) => nestingRef.current?.findFirstChildWithMore(first, last) ?? null);
13
+ const driver = useListDriver(config);
14
+ const handlersRef = useNestedHandlersRef();
15
+ const nesting = useListNesting({ config, driver, parent, handlersRef });
16
+ nestingRef.current = nesting;
17
+ const handlers = useScrollHandlers({ config, driver, nesting });
18
+ handlersRef.current = handlers;
19
+ return { config, driver, nesting, handlers };
20
+ }
@@ -0,0 +1,17 @@
1
+ import type { ISymbioteEvent } from '@symbiote-native/engine';
2
+ import { type IListNesting, type INestedHandlers } from '@symbiote-native/components';
3
+ import type { IListConfig } from './list-config';
4
+ import type { IListDriver } from './use-list-driver';
5
+ export type IScrollHandlers = INestedHandlers & {
6
+ onViewportLayout: (event: ISymbioteEvent) => void;
7
+ measureCell: (index: number) => (event: ISymbioteEvent) => void;
8
+ focusCell: (index: number) => () => void;
9
+ onContentSizeChange: (width: number, height: number) => void;
10
+ };
11
+ type IHandlerDeps<ItemT> = {
12
+ config: IListConfig<ItemT>;
13
+ driver: Pick<IListDriver<ItemT>, 'dispatch' | 'scroll'>;
14
+ nesting: IListNesting<ItemT>;
15
+ };
16
+ export declare function useScrollHandlers<ItemT>(deps: IHandlerDeps<ItemT>): IScrollHandlers;
17
+ export {};
@@ -0,0 +1,106 @@
1
+ // Native scroll and layout events turned into reducer actions, and handed on to nested lists
2
+ import { useCallback } from 'react';
3
+ import { LIST_ACTION_KIND, contentSizeActionOf, layoutActionOf, measureActionOf, resolveItemKey, scrollActionOf, } from '@symbiote-native/components';
4
+ // Each one reaches the lists nested in this one first, then the app's own handler
5
+ function useDragHandlers(deps) {
6
+ const { config, driver, nesting } = deps;
7
+ const { dispatch } = driver;
8
+ const { fanOut } = nesting;
9
+ const { onScrollBeginDrag, onScrollEndDrag, onMomentumScrollBegin, onMomentumScrollEnd, } = config;
10
+ const recordInteraction = useCallback(() => {
11
+ fanOut.recordInteraction();
12
+ dispatch({ kind: LIST_ACTION_KIND.recordInteraction });
13
+ }, [fanOut, dispatch]);
14
+ const onBeginDrag = useCallback((event) => {
15
+ fanOut.onScrollBeginDrag(event);
16
+ dispatch({ kind: LIST_ACTION_KIND.recordInteraction });
17
+ onScrollBeginDrag?.(event);
18
+ }, [fanOut, dispatch, onScrollBeginDrag]);
19
+ const onEndDrag = useCallback((event) => {
20
+ fanOut.onScrollEndDrag(event);
21
+ onScrollEndDrag?.(event);
22
+ }, [fanOut, onScrollEndDrag]);
23
+ const onMomentumBegin = useCallback((event) => {
24
+ fanOut.onMomentumScrollBegin(event);
25
+ onMomentumScrollBegin?.(event);
26
+ }, [fanOut, onMomentumScrollBegin]);
27
+ const onMomentumEnd = useCallback((event) => {
28
+ fanOut.onMomentumScrollEnd(event);
29
+ onMomentumScrollEnd?.(event);
30
+ }, [fanOut, onMomentumScrollEnd]);
31
+ return {
32
+ recordInteraction,
33
+ onScrollBeginDrag: onBeginDrag,
34
+ onScrollEndDrag: onEndDrag,
35
+ onMomentumScrollBegin: onMomentumBegin,
36
+ onMomentumScrollEnd: onMomentumEnd,
37
+ };
38
+ }
39
+ // A nested list's viewport is the parent's, so its own scroll events carry nothing it can use
40
+ function useScrollHandler(deps) {
41
+ const { config, driver, nesting } = deps;
42
+ const { horizontal, onScroll: userOnScroll } = config;
43
+ const { dispatch, scroll } = driver;
44
+ const { clearCommandedOffset } = scroll;
45
+ return useCallback((event) => {
46
+ nesting.fanOut.onScroll(event);
47
+ const timestamp = performance.now();
48
+ const action = nesting.isNested
49
+ ? nesting.parentScrollAction(event, timestamp)
50
+ : scrollActionOf(event, horizontal, timestamp);
51
+ if (action === undefined)
52
+ return;
53
+ // A real user or native scroll supersedes any pending commanded offset
54
+ clearCommandedOffset();
55
+ dispatch(action);
56
+ // Compose, don't clobber: the internal windowing ran first, now the user's handler
57
+ userOnScroll?.(event);
58
+ }, [nesting, horizontal, userOnScroll, dispatch, clearCommandedOffset]);
59
+ }
60
+ // RN's `_onContentSizeChange`: the list learns its content length, then the app's own handler
61
+ function useContentSizeHandler(deps) {
62
+ const { horizontal, onContentSizeChange: userHandler } = deps.config;
63
+ const { dispatch } = deps.driver;
64
+ return useCallback((width, height) => {
65
+ dispatch(contentSizeActionOf(width, height, horizontal));
66
+ userHandler?.(width, height);
67
+ }, [horizontal, dispatch, userHandler]);
68
+ }
69
+ function useLayoutHandlers(deps) {
70
+ const { config, driver, nesting } = deps;
71
+ const { horizontal, getItem, data, keyExtractor } = config;
72
+ const { dispatch, scroll } = driver;
73
+ const onViewportLayout = useCallback((event) => {
74
+ if (nesting.isNested) {
75
+ const node = scroll.nodeRef.current;
76
+ if (node !== null)
77
+ nesting.attachAt(node);
78
+ nesting.child.measureLayoutRelativeToContainingList();
79
+ return;
80
+ }
81
+ const action = layoutActionOf(event, horizontal);
82
+ if (action !== undefined)
83
+ dispatch(action);
84
+ }, [horizontal, dispatch, nesting, scroll.nodeRef]);
85
+ const measureCell = useCallback((index) => (event) => {
86
+ const action = measureActionOf(event, index, horizontal);
87
+ if (action !== undefined)
88
+ dispatch(action);
89
+ const cellKey = resolveItemKey(getItem(data, index), index, keyExtractor);
90
+ nesting.scope.registerCellNode(event.currentTarget, cellKey);
91
+ nesting.remeasureCell(cellKey);
92
+ }, [horizontal, dispatch, nesting, getItem, data, keyExtractor]);
93
+ return { onViewportLayout, measureCell };
94
+ }
95
+ export function useScrollHandlers(deps) {
96
+ const { dispatch } = deps.driver;
97
+ const onScroll = useScrollHandler(deps);
98
+ const focusCell = useCallback((index) => () => dispatch({ kind: LIST_ACTION_KIND.cellFocused, index }), [dispatch]);
99
+ return {
100
+ onScroll,
101
+ focusCell,
102
+ onContentSizeChange: useContentSizeHandler(deps),
103
+ ...useDragHandlers(deps),
104
+ ...useLayoutHandlers(deps),
105
+ };
106
+ }
@@ -0,0 +1,8 @@
1
+ import { type RefObject } from 'react';
2
+ import { type ISeparatorProps, type ISeparators } from '@symbiote-native/components';
3
+ export type ISeparatorOverrides<ItemT> = Map<number, Partial<ISeparatorProps<ItemT>>>;
4
+ export type IListSeparators<ItemT> = {
5
+ overridesRef: RefObject<ISeparatorOverrides<ItemT>>;
6
+ makeSeparators: (index: number) => ISeparators;
7
+ };
8
+ export declare function useSeparators<ItemT>(count: number): IListSeparators<ItemT>;
@@ -0,0 +1,17 @@
1
+ // Per-gap separator overrides, render state read straight by the cell walk
2
+ import { useCallback, useRef, useState } from 'react';
3
+ import { EMPTY_OFFSET, buildSeparatorHandles, isSeparatorGapInRange, } from '@symbiote-native/components';
4
+ export function useSeparators(count) {
5
+ const overridesRef = useRef(new Map());
6
+ const [, setVersion] = useState(EMPTY_OFFSET);
7
+ // A gap outside [0, count - 2] has no separator, so the write is a no-op, RN bails the same way
8
+ const mergeSeparator = useCallback((gapIndex, patch) => {
9
+ if (!isSeparatorGapInRange(gapIndex, count))
10
+ return;
11
+ const overrides = overridesRef.current;
12
+ overrides.set(gapIndex, { ...overrides.get(gapIndex), ...patch });
13
+ setVersion(version => version + 1);
14
+ }, [count]);
15
+ const makeSeparators = useCallback((index) => buildSeparatorHandles(index, mergeSeparator), [mergeSeparator]);
16
+ return { overridesRef, makeSeparators };
17
+ }