react-native-list 2.0.0-alpha.1 → 2.0.0-alpha.2
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/README.md +26 -28
- package/lib/ReactFabricMirror.d.ts +4 -0
- package/lib/ReactFabricMirror.js +515 -0
- package/lib/UiListModule.d.ts +2 -0
- package/lib/UiListModule.js +2 -0
- package/lib/index.d.ts +8 -0
- package/lib/index.js +21 -0
- package/lib/privateGlobals.d.ts +14 -0
- package/lib/privateGlobals.js +2 -0
- package/lib/renderer/RenderHelper.d.ts +2 -0
- package/lib/renderer/RenderHelper.js +11 -0
- package/lib/renderer/UiManagerHelper.d.ts +2 -0
- package/lib/renderer/UiManagerHelper.js +2 -0
- package/lib/renderer/fabric/RenderHelper.d.ts +2 -0
- package/lib/renderer/fabric/RenderHelper.js +11 -0
- package/lib/renderer/fabric/UiManagerHelper.d.ts +2 -0
- package/lib/renderer/fabric/UiManagerHelper.js +2 -0
- package/lib/renderer/react/ReactFabricMirror.d.ts +4 -0
- package/lib/renderer/react/ReactFabricMirror.js +515 -0
- package/lib/renderer/react/ReactFabricRenderer.d.ts +3 -0
- package/lib/renderer/react/ReactFabricRenderer.js +9 -0
- package/lib/specs/IOSWorkletsModuleProxyHolder.nitro.d.ts +6 -0
- package/lib/specs/IOSWorkletsModuleProxyHolder.nitro.js +1 -0
- package/lib/specs/UIListModule.nitro.d.ts +9 -0
- package/lib/specs/UIListModule.nitro.js +1 -0
- package/lib/specs/UIManagerHelper.nitro.d.ts +13 -0
- package/lib/specs/UIManagerHelper.nitro.js +1 -0
- package/lib/specs/UiListView.nitro.d.ts +20 -0
- package/lib/specs/UiListView.nitro.js +1 -0
- package/lib/specs/ViewHolder.nitro.d.ts +6 -0
- package/lib/specs/ViewHolder.nitro.js +1 -0
- package/lib/views/List.d.ts +35 -0
- package/lib/views/List.js +225 -0
- package/lib/views/UiListHostComponent.d.ts +2 -0
- package/lib/views/UiListHostComponent.js +3 -0
- package/metro-config.js +77 -5
- package/package.json +2 -2
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { HybridView, HybridViewMethods, HybridViewProps } from 'react-native-nitro-modules';
|
|
2
|
+
export interface ViewHolderProps extends HybridViewProps {
|
|
3
|
+
}
|
|
4
|
+
export interface ViewHolderMethods extends HybridViewMethods {
|
|
5
|
+
}
|
|
6
|
+
export type ViewHolder = HybridView<ViewHolderProps, ViewHolderMethods>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import type { AnyMap } from 'react-native-nitro-modules';
|
|
3
|
+
import { ViewStyle } from 'react-native';
|
|
4
|
+
export type ListKey = string | number;
|
|
5
|
+
export type ListItemSize = {
|
|
6
|
+
width?: number;
|
|
7
|
+
height?: number;
|
|
8
|
+
};
|
|
9
|
+
export type ListRenderer<T extends AnyMap, TType extends string> = {
|
|
10
|
+
renderItemWorklet: (info: {
|
|
11
|
+
item?: T;
|
|
12
|
+
index?: number;
|
|
13
|
+
key?: string;
|
|
14
|
+
type: TType;
|
|
15
|
+
}) => React.ReactElement<any>;
|
|
16
|
+
isContentEqualWorklet: (oldItem: T, newItem: T) => boolean;
|
|
17
|
+
};
|
|
18
|
+
export type ListProps<T extends AnyMap, TType extends string> = {
|
|
19
|
+
data: readonly T[];
|
|
20
|
+
keyExtractor: (item: T, index: number) => ListKey;
|
|
21
|
+
getItemType: (item: T, index: number) => TType;
|
|
22
|
+
getItemSize?: (item: T, index: number) => ListItemSize;
|
|
23
|
+
renderers: Record<TType, ListRenderer<T, TType>>;
|
|
24
|
+
style?: ViewStyle;
|
|
25
|
+
};
|
|
26
|
+
export type ListRef<T extends AnyMap> = {
|
|
27
|
+
replaceData(data: readonly T[], animated?: boolean): void;
|
|
28
|
+
insertItem(index: number, item: T): void;
|
|
29
|
+
updateItem(index: number, item: T): void;
|
|
30
|
+
removeItem(index: number): void;
|
|
31
|
+
moveItem(fromIndex: number, toIndex: number): void;
|
|
32
|
+
};
|
|
33
|
+
export declare const List: <T extends AnyMap, TType extends string>(props: ListProps<T, TType> & {
|
|
34
|
+
ref?: React.ForwardedRef<ListRef<T>>;
|
|
35
|
+
}) => React.ReactElement | null;
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import React, { forwardRef, useEffect, useImperativeHandle, useMemo, useRef, useState, } from 'react';
|
|
2
|
+
import { UiListHostComponent } from './UiListHostComponent';
|
|
3
|
+
import { callback } from 'react-native-nitro-modules';
|
|
4
|
+
import { scheduleOnUI } from 'react-native-worklets';
|
|
5
|
+
import { renderSyncWorklet, uiListModuleBoxed, } from '../renderer/fabric/RenderHelper';
|
|
6
|
+
import { View } from 'react-native';
|
|
7
|
+
import { getReactFabricRenderer } from '../renderer/react/ReactFabricRenderer';
|
|
8
|
+
function assertValidSize(size, index) {
|
|
9
|
+
const width = size.width;
|
|
10
|
+
const height = size.height;
|
|
11
|
+
if (width != null && (!Number.isFinite(width) || width <= 0)) {
|
|
12
|
+
throw new Error(`List item at index ${index} has an invalid width.`);
|
|
13
|
+
}
|
|
14
|
+
if (height != null && (!Number.isFinite(height) || height <= 0)) {
|
|
15
|
+
throw new Error(`List item at index ${index} has an invalid height.`);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
function stringifyKey(key, index) {
|
|
19
|
+
if (typeof key === 'string') {
|
|
20
|
+
return key;
|
|
21
|
+
}
|
|
22
|
+
if (typeof key === 'number') {
|
|
23
|
+
return String(key);
|
|
24
|
+
}
|
|
25
|
+
throw new Error(`List item at index ${index} returned an invalid key.`);
|
|
26
|
+
}
|
|
27
|
+
function createNativeItem(item, index, props) {
|
|
28
|
+
const rawKey = props.keyExtractor(item, index);
|
|
29
|
+
const key = stringifyKey(rawKey, index);
|
|
30
|
+
const type = props.getItemType(item, index);
|
|
31
|
+
const renderer = props.renderers[type];
|
|
32
|
+
if (renderer == null) {
|
|
33
|
+
throw new Error(`List item at index ${index} uses unknown type "${type}".`);
|
|
34
|
+
}
|
|
35
|
+
const size = props.getItemSize?.(item, index) ?? {};
|
|
36
|
+
assertValidSize(size, index);
|
|
37
|
+
return {
|
|
38
|
+
key: key,
|
|
39
|
+
type: type,
|
|
40
|
+
width: size.width,
|
|
41
|
+
height: size.height,
|
|
42
|
+
data: item,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
function createNativeItems(data, props) {
|
|
46
|
+
const seenIdentities = new Set();
|
|
47
|
+
const items = [];
|
|
48
|
+
for (let index = 0; index < data.length; index++) {
|
|
49
|
+
const item = data[index];
|
|
50
|
+
if (item == null) {
|
|
51
|
+
throw new Error(`List item at index ${index} is undefined.`);
|
|
52
|
+
}
|
|
53
|
+
const nativeItem = createNativeItem(item, index, props);
|
|
54
|
+
const identity = nativeItem.type + ':' + nativeItem.key;
|
|
55
|
+
if (seenIdentities.has(identity)) {
|
|
56
|
+
throw new Error(`List contains duplicate item identity "${identity}".`);
|
|
57
|
+
}
|
|
58
|
+
seenIdentities.add(identity);
|
|
59
|
+
items.push(nativeItem);
|
|
60
|
+
}
|
|
61
|
+
return items;
|
|
62
|
+
}
|
|
63
|
+
function ListInner(props, forwardedRef) {
|
|
64
|
+
const { data, getItemSize, getItemType, keyExtractor, renderers, style } = props;
|
|
65
|
+
const isSetup = useRef(false);
|
|
66
|
+
const nativeListRef = useRef(null);
|
|
67
|
+
const [isNativeReady, setIsNativeReady] = useState(false);
|
|
68
|
+
const nativeItems = useMemo(() => {
|
|
69
|
+
const normalizationProps = {
|
|
70
|
+
data,
|
|
71
|
+
keyExtractor,
|
|
72
|
+
getItemType,
|
|
73
|
+
getItemSize,
|
|
74
|
+
renderers,
|
|
75
|
+
};
|
|
76
|
+
return createNativeItems(data, normalizationProps);
|
|
77
|
+
}, [data, getItemSize, getItemType, keyExtractor, renderers]);
|
|
78
|
+
useImperativeHandle(forwardedRef, () => {
|
|
79
|
+
return {
|
|
80
|
+
replaceData(nextData, animated = true) {
|
|
81
|
+
const ref = nativeListRef.current;
|
|
82
|
+
if (ref == null)
|
|
83
|
+
return;
|
|
84
|
+
const nextItems = createNativeItems(nextData, props);
|
|
85
|
+
ref.setData(nextItems, animated);
|
|
86
|
+
},
|
|
87
|
+
insertItem(index, item) {
|
|
88
|
+
const ref = nativeListRef.current;
|
|
89
|
+
if (ref == null)
|
|
90
|
+
return;
|
|
91
|
+
const nativeItem = createNativeItem(item, index, props);
|
|
92
|
+
ref.insertItem(index, nativeItem);
|
|
93
|
+
},
|
|
94
|
+
updateItem(index, item) {
|
|
95
|
+
const ref = nativeListRef.current;
|
|
96
|
+
if (ref == null)
|
|
97
|
+
return;
|
|
98
|
+
const nativeItem = createNativeItem(item, index, props);
|
|
99
|
+
ref.updateItem(index, nativeItem);
|
|
100
|
+
},
|
|
101
|
+
removeItem(index) {
|
|
102
|
+
const ref = nativeListRef.current;
|
|
103
|
+
if (ref == null)
|
|
104
|
+
return;
|
|
105
|
+
ref.removeItem(index);
|
|
106
|
+
},
|
|
107
|
+
moveItem(fromIndex, toIndex) {
|
|
108
|
+
const ref = nativeListRef.current;
|
|
109
|
+
if (ref == null)
|
|
110
|
+
return;
|
|
111
|
+
ref.moveItem(fromIndex, toIndex);
|
|
112
|
+
},
|
|
113
|
+
};
|
|
114
|
+
}, [props]);
|
|
115
|
+
useEffect(() => {
|
|
116
|
+
const ref = nativeListRef.current;
|
|
117
|
+
if (ref == null || !isNativeReady)
|
|
118
|
+
return;
|
|
119
|
+
ref.setData(nativeItems, true);
|
|
120
|
+
}, [isNativeReady, nativeItems]);
|
|
121
|
+
return (React.createElement(UiListHostComponent, { style: style, hybridRef: callback((ref) => {
|
|
122
|
+
nativeListRef.current = ref;
|
|
123
|
+
if (isSetup.current)
|
|
124
|
+
return;
|
|
125
|
+
isSetup.current = true;
|
|
126
|
+
scheduleOnUI(() => {
|
|
127
|
+
'worklet';
|
|
128
|
+
const { nativeLog, reactRender } = getReactFabricRenderer();
|
|
129
|
+
const tagToArrayPosition = {};
|
|
130
|
+
const tagToItemId = {};
|
|
131
|
+
let nextItemId = 0;
|
|
132
|
+
const elementsRendered = [];
|
|
133
|
+
const uiListModuleUnboxed = uiListModuleBoxed.unbox();
|
|
134
|
+
ref.setListCallbacks(uiListModuleUnboxed, (type) => {
|
|
135
|
+
const nativeRef = globalThis.React.createRef();
|
|
136
|
+
const itemId = nextItemId++;
|
|
137
|
+
const typedType = type;
|
|
138
|
+
const renderer = renderers[typedType];
|
|
139
|
+
if (renderer == null) {
|
|
140
|
+
throw new Error('No renderer for list item type ' + type);
|
|
141
|
+
}
|
|
142
|
+
const newElement = renderer.renderItemWorklet({
|
|
143
|
+
type: typedType,
|
|
144
|
+
});
|
|
145
|
+
const newProps = {
|
|
146
|
+
key: 'itemid-' + itemId,
|
|
147
|
+
ref: nativeRef,
|
|
148
|
+
collapsable: false,
|
|
149
|
+
};
|
|
150
|
+
const newElementWithKey = globalThis.React.cloneElement(newElement, newProps);
|
|
151
|
+
const newLength = elementsRendered.push(newElementWithKey);
|
|
152
|
+
const currentIndex = newLength - 1;
|
|
153
|
+
const parentContainer = React.createElement(View, null, elementsRendered);
|
|
154
|
+
reactRender(parentContainer, () => {
|
|
155
|
+
nativeLog('Render complete');
|
|
156
|
+
});
|
|
157
|
+
if (nativeRef.current == null) {
|
|
158
|
+
throw new Error('Ref is null after render');
|
|
159
|
+
}
|
|
160
|
+
const tag = nativeRef.current.__nativeTag;
|
|
161
|
+
tagToArrayPosition[tag] = currentIndex;
|
|
162
|
+
tagToItemId[tag] = itemId;
|
|
163
|
+
const start = globalThis.performance.now();
|
|
164
|
+
renderSyncWorklet();
|
|
165
|
+
const end = globalThis.performance.now();
|
|
166
|
+
nativeLog('renderSync took ', end - start, 'ms');
|
|
167
|
+
return tag;
|
|
168
|
+
}, (reactTag, item, index) => {
|
|
169
|
+
const typedType = item.type;
|
|
170
|
+
const renderer = renderers[typedType];
|
|
171
|
+
if (renderer == null) {
|
|
172
|
+
throw new Error('No renderer for list item type ' + item.type);
|
|
173
|
+
}
|
|
174
|
+
const itemId = tagToItemId[reactTag];
|
|
175
|
+
if (itemId == null) {
|
|
176
|
+
throw new Error('No itemId for tag ' + reactTag);
|
|
177
|
+
}
|
|
178
|
+
const newElement = renderer.renderItemWorklet({
|
|
179
|
+
item: item.data,
|
|
180
|
+
index,
|
|
181
|
+
key: item.key,
|
|
182
|
+
type: typedType,
|
|
183
|
+
});
|
|
184
|
+
const newProps = {
|
|
185
|
+
key: 'itemid-' + itemId,
|
|
186
|
+
collapsable: false,
|
|
187
|
+
};
|
|
188
|
+
const newElementWithKey = globalThis.React.cloneElement(newElement, newProps);
|
|
189
|
+
const position = tagToArrayPosition[reactTag];
|
|
190
|
+
if (position == null) {
|
|
191
|
+
throw new Error('No position for tag ' + reactTag);
|
|
192
|
+
}
|
|
193
|
+
elementsRendered[position] = newElementWithKey;
|
|
194
|
+
const parentContainer = React.createElement(View, null, elementsRendered);
|
|
195
|
+
reactRender(parentContainer, () => {
|
|
196
|
+
nativeLog('Update Render complete');
|
|
197
|
+
});
|
|
198
|
+
const start = globalThis.performance.now();
|
|
199
|
+
renderSyncWorklet();
|
|
200
|
+
const end = globalThis.performance.now();
|
|
201
|
+
nativeLog('Update renderSync took ', end - start, 'ms');
|
|
202
|
+
return true;
|
|
203
|
+
}, (oldItem, newItem) => {
|
|
204
|
+
if (oldItem.type !== newItem.type) {
|
|
205
|
+
return false;
|
|
206
|
+
}
|
|
207
|
+
if (oldItem.width !== newItem.width) {
|
|
208
|
+
return false;
|
|
209
|
+
}
|
|
210
|
+
if (oldItem.height !== newItem.height) {
|
|
211
|
+
return false;
|
|
212
|
+
}
|
|
213
|
+
const typedType = newItem.type;
|
|
214
|
+
const renderer = renderers[typedType];
|
|
215
|
+
if (renderer == null) {
|
|
216
|
+
throw new Error('No renderer for list item type ' + newItem.type);
|
|
217
|
+
}
|
|
218
|
+
return renderer.isContentEqualWorklet(oldItem.data, newItem.data);
|
|
219
|
+
});
|
|
220
|
+
ref.setData(nativeItems, false);
|
|
221
|
+
});
|
|
222
|
+
setIsNativeReady(true);
|
|
223
|
+
}) }));
|
|
224
|
+
}
|
|
225
|
+
export const List = forwardRef(ListInner);
|
package/metro-config.js
CHANGED
|
@@ -11,6 +11,67 @@ function isRendererProxyImport(moduleName) {
|
|
|
11
11
|
return /(?:^|\/)RendererProxy(?:\.js)?$/.test(moduleName)
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
+
//#region Turbo module handling
|
|
15
|
+
// react-native-worklets installs a shim preventing the usage of TurboModules on other runtimes:
|
|
16
|
+
// - https://github.com/software-mansion/react-native-reanimated/blob/9c2e3b05216b22d3c1ed4be67719876d1fa9115d/packages/react-native-worklets/bundleMode/shims/turboModuleRegistryShim.js#L36
|
|
17
|
+
// As we properly handle turbo modules we want to allow for it.
|
|
18
|
+
// So we basically redirect all imports of the shim back to the original TurboModuleRegistry.js file:
|
|
19
|
+
const turboModuleRegistryModuleName =
|
|
20
|
+
'react-native/Libraries/TurboModule/TurboModuleRegistry'
|
|
21
|
+
const turboModuleRegistryPath = require.resolve(turboModuleRegistryModuleName)
|
|
22
|
+
const turboModuleRegistryFileSuffix = path.join(
|
|
23
|
+
'react-native',
|
|
24
|
+
'Libraries',
|
|
25
|
+
'TurboModule',
|
|
26
|
+
'TurboModuleRegistry.js'
|
|
27
|
+
)
|
|
28
|
+
const workletsTurboModuleRegistryShimSuffix = path.join(
|
|
29
|
+
'react-native-worklets',
|
|
30
|
+
'bundleMode',
|
|
31
|
+
'shims',
|
|
32
|
+
'turboModuleRegistryShim.js'
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
function isTurboModuleRegistryImport(moduleName) {
|
|
36
|
+
return moduleName === turboModuleRegistryModuleName
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function isSourceFileResolution(result) {
|
|
40
|
+
if (result == null) {
|
|
41
|
+
return false
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (result.type !== 'sourceFile') {
|
|
45
|
+
return false
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return typeof result.filePath === 'string'
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function isTurboModuleRegistryResolution(result) {
|
|
52
|
+
if (!isSourceFileResolution(result)) {
|
|
53
|
+
return false
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return result.filePath.endsWith(turboModuleRegistryFileSuffix)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function isWorkletsTurboModuleRegistryShimResolution(result) {
|
|
60
|
+
if (!isSourceFileResolution(result)) {
|
|
61
|
+
return false
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return result.filePath.endsWith(workletsTurboModuleRegistryShimSuffix)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function createTurboModuleRegistryResolution() {
|
|
68
|
+
return {
|
|
69
|
+
type: 'sourceFile',
|
|
70
|
+
filePath: turboModuleRegistryPath,
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
//#endregion
|
|
74
|
+
|
|
14
75
|
function isReactNativeDomInternalsImporter(originModulePath) {
|
|
15
76
|
if (typeof originModulePath !== 'string') {
|
|
16
77
|
return false
|
|
@@ -26,6 +87,10 @@ function getReactNativeListMetroConfig(config) {
|
|
|
26
87
|
const currentResolveRequest = config.resolver.resolveRequest
|
|
27
88
|
|
|
28
89
|
config.resolver.resolveRequest = (context, moduleName, platform) => {
|
|
90
|
+
if (isTurboModuleRegistryImport(moduleName)) {
|
|
91
|
+
return createTurboModuleRegistryResolution()
|
|
92
|
+
}
|
|
93
|
+
|
|
29
94
|
// Redirect only React Native DOM internals to a runtime-switching proxy.
|
|
30
95
|
if (
|
|
31
96
|
isRendererProxyImport(moduleName) &&
|
|
@@ -37,11 +102,18 @@ function getReactNativeListMetroConfig(config) {
|
|
|
37
102
|
}
|
|
38
103
|
}
|
|
39
104
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
105
|
+
const resolveRequest = currentResolveRequest || context.resolveRequest
|
|
106
|
+
const resolved = resolveRequest(context, moduleName, platform)
|
|
107
|
+
|
|
108
|
+
if (isTurboModuleRegistryResolution(resolved)) {
|
|
109
|
+
return createTurboModuleRegistryResolution()
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (isWorkletsTurboModuleRegistryShimResolution(resolved)) {
|
|
113
|
+
return createTurboModuleRegistryResolution()
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return resolved
|
|
45
117
|
}
|
|
46
118
|
|
|
47
119
|
return config
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-native-list",
|
|
3
|
-
"version": "2.0.0-alpha.
|
|
3
|
+
"version": "2.0.0-alpha.2",
|
|
4
4
|
"description": "React Native List",
|
|
5
5
|
"main": "lib/index",
|
|
6
6
|
"module": "lib/index",
|
|
@@ -81,7 +81,7 @@
|
|
|
81
81
|
"peerDependencies": {
|
|
82
82
|
"react": "*",
|
|
83
83
|
"react-native": "*",
|
|
84
|
-
"react-native-worklets": "
|
|
84
|
+
"react-native-worklets": ">=0.10.0",
|
|
85
85
|
"react-native-nitro-modules": "*"
|
|
86
86
|
},
|
|
87
87
|
"eslintConfig": {
|