@symbiote-native/react 3.3.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.
- package/build/batched-updates.d.ts +2 -0
- package/build/batched-updates.js +5 -0
- package/build/components/flat-list/index.d.ts +16 -13
- package/build/components/flat-list/index.js +86 -72
- package/build/components/input-accessory-view-props.d.ts +4 -3
- package/build/components/keyboard-avoiding-view/index.d.ts +2 -2
- package/build/components/keyboard-avoiding-view/index.js +45 -84
- package/build/components/layout-conformance-props.d.ts +6 -0
- package/build/components/layout-conformance-props.js +1 -0
- package/build/components/modal/index.d.ts +4 -4
- package/build/components/modal/index.js +47 -18
- package/build/components/refresh-control-props.d.ts +7 -6
- package/build/components/scroll-view/scroll-view-props.d.ts +8 -5
- package/build/components/text-input/text-input-props.d.ts +4 -0
- package/build/components/touchable/touchable-props.d.ts +6 -5
- package/build/components/virtualized-list/index.d.ts +3 -70
- package/build/components/virtualized-list/index.js +50 -548
- package/build/components/virtualized-list/list-children.d.ts +13 -0
- package/build/components/virtualized-list/list-children.js +79 -0
- package/build/components/virtualized-list/list-config.d.ts +11 -0
- package/build/components/virtualized-list/list-config.js +14 -0
- package/build/components/virtualized-list/list-elements.d.ts +11 -0
- package/build/components/virtualized-list/list-elements.js +26 -0
- package/build/components/virtualized-list/list-host.d.ts +14 -0
- package/build/components/virtualized-list/list-host.js +24 -0
- package/build/components/virtualized-list/list-scroll-props.d.ts +32 -0
- package/build/components/virtualized-list/list-scroll-props.js +30 -0
- package/build/components/virtualized-list/nested-scope.d.ts +10 -0
- package/build/components/virtualized-list/nested-scope.js +16 -0
- package/build/components/virtualized-list/use-list-driver.d.ts +22 -0
- package/build/components/virtualized-list/use-list-driver.js +104 -0
- package/build/components/virtualized-list/use-list-nesting.d.ts +12 -0
- package/build/components/virtualized-list/use-list-nesting.js +37 -0
- package/build/components/virtualized-list/use-list-parts.d.ts +12 -0
- package/build/components/virtualized-list/use-list-parts.js +20 -0
- package/build/components/virtualized-list/use-scroll-handlers.d.ts +17 -0
- package/build/components/virtualized-list/use-scroll-handlers.js +106 -0
- package/build/components/virtualized-list/use-separators.d.ts +8 -0
- package/build/components/virtualized-list/use-separators.js +17 -0
- package/build/components/virtualized-list/virtualized-list-props.d.ts +85 -0
- package/build/components/virtualized-list/virtualized-list-props.js +2 -0
- package/build/components/virtualized-section-list/index.d.ts +24 -8
- package/build/components/virtualized-section-list/index.js +89 -93
- package/build/components/virtualized-section-list/section-item-cell.d.ts +23 -0
- package/build/components/virtualized-section-list/section-item-cell.js +48 -0
- package/build/components.d.ts +5 -5
- package/build/hooks/use-animated-value.d.ts +7 -0
- package/build/hooks/use-animated-value.js +19 -0
- package/build/hooks/use-color-scheme.d.ts +2 -2
- package/build/hooks/use-color-scheme.js +4 -13
- package/build/hooks/use-pressability.d.ts +34 -0
- package/build/hooks/use-pressability.js +39 -0
- package/build/hooks/use-window-dimensions.js +6 -10
- package/build/host-config.js +6 -5
- package/build/index.d.ts +19 -5
- package/build/index.js +20 -14
- package/build/jsx-runtime.d.ts +15 -16
- package/build/modules/animated/create-animated-component.d.ts +2 -2
- package/build/modules/animated/create-animated-component.js +10 -22
- package/build/modules/animated/index.d.ts +54 -41
- package/build/modules/animated/index.js +2 -27
- package/build/modules/app-registry/index.d.ts +3 -3
- package/build/modules/app-registry/index.js +2 -2
- package/build/modules/status-bar/index.android.d.ts +1 -1
- package/build/modules/status-bar/index.android.js +7 -27
- package/build/modules/status-bar/index.ios.d.ts +1 -1
- package/build/modules/status-bar/index.ios.js +4 -28
- package/build/modules/status-bar/shared.d.ts +5 -4
- package/build/modules/status-bar/shared.js +33 -6
- package/build/native-host-tags.d.ts +2 -0
- package/build/native-host-tags.js +4 -0
- package/build/render.d.ts +1 -1
- package/build/render.js +3 -1
- package/build/root-tag-context.d.ts +1 -0
- package/build/root-tag-context.js +4 -0
- 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
|
+
}
|