@shopify/flash-list 1.2.2 → 1.3.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.
- package/CHANGELOG.md +12 -0
- package/dist/FlashList.d.ts +1 -1
- package/dist/FlashList.d.ts.map +1 -1
- package/dist/FlashList.js +8 -4
- package/dist/FlashList.js.map +1 -1
- package/dist/MasonryFlashList.d.ts +51 -0
- package/dist/MasonryFlashList.d.ts.map +1 -0
- package/dist/MasonryFlashList.js +241 -0
- package/dist/MasonryFlashList.js.map +1 -0
- package/dist/__tests__/MasonryFlashList.test.d.ts +2 -0
- package/dist/__tests__/MasonryFlashList.test.d.ts.map +1 -0
- package/dist/__tests__/MasonryFlashList.test.js +226 -0
- package/dist/__tests__/MasonryFlashList.test.js.map +1 -0
- package/dist/__tests__/helpers/mountMasonryFlashList.d.ts +18 -0
- package/dist/__tests__/helpers/mountMasonryFlashList.d.ts.map +1 -0
- package/dist/__tests__/helpers/mountMasonryFlashList.js +44 -0
- package/dist/__tests__/helpers/mountMasonryFlashList.js.map +1 -0
- package/dist/errors/ExceptionList.d.ts +4 -0
- package/dist/errors/ExceptionList.d.ts.map +1 -1
- package/dist/errors/ExceptionList.js +4 -0
- package/dist/errors/ExceptionList.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/viewability/ViewabilityManager.d.ts.map +1 -1
- package/dist/viewability/ViewabilityManager.js +3 -3
- package/dist/viewability/ViewabilityManager.js.map +1 -1
- package/package.json +1 -1
- package/src/FlashList.tsx +10 -5
- package/src/MasonryFlashList.tsx +459 -0
- package/src/__tests__/MasonryFlashList.test.ts +264 -0
- package/src/__tests__/helpers/mountMasonryFlashList.tsx +65 -0
- package/src/errors/ExceptionList.ts +5 -0
- package/src/index.ts +9 -0
- package/src/viewability/ViewabilityManager.ts +2 -1
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
import { ScrollView, Text, View } from "react-native";
|
|
2
|
+
import "@quilted/react-testing/matchers";
|
|
3
|
+
import { ProgressiveListView } from "recyclerlistview";
|
|
4
|
+
import React from "react";
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
MasonryFlashListProps,
|
|
8
|
+
MasonryFlashListRef,
|
|
9
|
+
} from "../MasonryFlashList";
|
|
10
|
+
import FlashList from "../FlashList";
|
|
11
|
+
|
|
12
|
+
import { mountMasonryFlashList } from "./helpers/mountMasonryFlashList";
|
|
13
|
+
|
|
14
|
+
describe("MasonryFlashList", () => {
|
|
15
|
+
beforeEach(() => {
|
|
16
|
+
jest.clearAllMocks();
|
|
17
|
+
jest.useFakeTimers();
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it("renders items and has 3 internal lists", () => {
|
|
21
|
+
const masonryFlashList = mountMasonryFlashList();
|
|
22
|
+
expect(masonryFlashList.findAll(ProgressiveListView).length).toBe(3);
|
|
23
|
+
expect(masonryFlashList).toContainReactComponent(Text, { children: "One" });
|
|
24
|
+
expect(masonryFlashList).toContainReactComponent(ProgressiveListView, {
|
|
25
|
+
isHorizontal: false,
|
|
26
|
+
});
|
|
27
|
+
masonryFlashList.unmount();
|
|
28
|
+
});
|
|
29
|
+
it("invokes renderItem with columnIndex and columnSpan", () => {
|
|
30
|
+
const mockRenderItem = jest.fn(() => null);
|
|
31
|
+
const masonryFlashList = mountMasonryFlashList({
|
|
32
|
+
renderItem: mockRenderItem,
|
|
33
|
+
data: ["One", "Two", "Three"],
|
|
34
|
+
numColumns: 3,
|
|
35
|
+
});
|
|
36
|
+
expect(mockRenderItem).toHaveBeenCalledWith(
|
|
37
|
+
expect.objectContaining({
|
|
38
|
+
columnIndex: 0,
|
|
39
|
+
columnSpan: 1,
|
|
40
|
+
})
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
expect(mockRenderItem).toHaveBeenCalledWith(
|
|
44
|
+
expect.objectContaining({
|
|
45
|
+
columnIndex: 1,
|
|
46
|
+
columnSpan: 1,
|
|
47
|
+
})
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
expect(mockRenderItem).toHaveBeenCalledWith(
|
|
51
|
+
expect.objectContaining({
|
|
52
|
+
columnSpan: 1,
|
|
53
|
+
columnIndex: 2,
|
|
54
|
+
})
|
|
55
|
+
);
|
|
56
|
+
masonryFlashList.unmount();
|
|
57
|
+
});
|
|
58
|
+
it("raised onLoad event only when first internal child mounts", () => {
|
|
59
|
+
const onLoadMock = jest.fn();
|
|
60
|
+
const ref = React.createRef<MasonryFlashListRef<string>>();
|
|
61
|
+
const masonryFlashList = mountMasonryFlashList(
|
|
62
|
+
{
|
|
63
|
+
onLoad: onLoadMock,
|
|
64
|
+
},
|
|
65
|
+
ref
|
|
66
|
+
);
|
|
67
|
+
expect(onLoadMock).not.toHaveBeenCalled();
|
|
68
|
+
masonryFlashList.findAll(ProgressiveListView)[1]?.instance.onItemLayout(0);
|
|
69
|
+
expect(onLoadMock).toHaveBeenCalledTimes(1);
|
|
70
|
+
|
|
71
|
+
// on load shouldn't be passed to wrapper list
|
|
72
|
+
expect((ref.current as FlashList<string>).props.onLoad).toBeUndefined();
|
|
73
|
+
masonryFlashList.unmount();
|
|
74
|
+
});
|
|
75
|
+
it("can resize columns using getColumnFlex", () => {
|
|
76
|
+
const masonryFlashList = mountMasonryFlashList({
|
|
77
|
+
getColumnFlex: (_, column) => (column === 0 ? 1 : 3),
|
|
78
|
+
});
|
|
79
|
+
const progressiveListView =
|
|
80
|
+
masonryFlashList.find(ProgressiveListView)!.instance;
|
|
81
|
+
expect(progressiveListView.getLayout(0).width).toBe(100);
|
|
82
|
+
expect(progressiveListView.getLayout(1).width).toBe(300);
|
|
83
|
+
|
|
84
|
+
expect(masonryFlashList.findAll(ProgressiveListView).length).toBe(3);
|
|
85
|
+
masonryFlashList.findAll(ProgressiveListView).forEach((plv, index) => {
|
|
86
|
+
if (index === 1) {
|
|
87
|
+
expect(plv.instance.props.layoutSize.width).toBe(100);
|
|
88
|
+
}
|
|
89
|
+
if (index === 2) {
|
|
90
|
+
expect(plv.instance.props.layoutSize.width).toBe(300);
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
masonryFlashList.unmount();
|
|
94
|
+
});
|
|
95
|
+
it("mounts a single ScrollView", () => {
|
|
96
|
+
const masonryFlashList = mountMasonryFlashList();
|
|
97
|
+
expect(masonryFlashList.findAll(ScrollView)).toHaveLength(1);
|
|
98
|
+
masonryFlashList.unmount();
|
|
99
|
+
});
|
|
100
|
+
it("forwards single onScroll event to external listener", () => {
|
|
101
|
+
const onScrollMock = jest.fn();
|
|
102
|
+
const masonryFlashList = mountMasonryFlashList({
|
|
103
|
+
onScroll: onScrollMock,
|
|
104
|
+
});
|
|
105
|
+
masonryFlashList.find(ScrollView)?.instance.props.onScroll({
|
|
106
|
+
nativeEvent: { contentOffset: { x: 0, y: 0 } },
|
|
107
|
+
});
|
|
108
|
+
expect(onScrollMock).toHaveBeenCalledTimes(1);
|
|
109
|
+
masonryFlashList.unmount();
|
|
110
|
+
});
|
|
111
|
+
it("updates scroll offset of all internal lists", () => {
|
|
112
|
+
const onScrollMock = jest.fn();
|
|
113
|
+
const masonryFlashList = mountMasonryFlashList({
|
|
114
|
+
onScroll: onScrollMock,
|
|
115
|
+
});
|
|
116
|
+
masonryFlashList.find(ScrollView)?.instance.props.onScroll({
|
|
117
|
+
nativeEvent: { contentOffset: { x: 0, y: 100 } },
|
|
118
|
+
});
|
|
119
|
+
masonryFlashList.findAll(ProgressiveListView).forEach((list) => {
|
|
120
|
+
expect(list.instance.getCurrentScrollOffset()).toBe(100);
|
|
121
|
+
});
|
|
122
|
+
masonryFlashList.unmount();
|
|
123
|
+
});
|
|
124
|
+
it("has a valid ref object", () => {
|
|
125
|
+
const ref = React.createRef<MasonryFlashListRef<string>>();
|
|
126
|
+
const masonryFlashList = mountMasonryFlashList({}, ref);
|
|
127
|
+
expect(ref.current).toBeDefined();
|
|
128
|
+
masonryFlashList.unmount();
|
|
129
|
+
});
|
|
130
|
+
it("forwards overrideItemLayout to internal lists", () => {
|
|
131
|
+
const overrideItemLayout = jest.fn((layout) => {
|
|
132
|
+
layout.size = 300;
|
|
133
|
+
});
|
|
134
|
+
const masonryFlashList = mountMasonryFlashList({
|
|
135
|
+
overrideItemLayout,
|
|
136
|
+
});
|
|
137
|
+
expect(masonryFlashList.findAll(ProgressiveListView).length).toBe(3);
|
|
138
|
+
masonryFlashList.findAll(ProgressiveListView).forEach((list, index) => {
|
|
139
|
+
if (index !== 0) {
|
|
140
|
+
expect(list.instance.getLayout(0).height).toBe(300);
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
masonryFlashList.unmount();
|
|
144
|
+
});
|
|
145
|
+
it("forwards keyExtractor to internal list", () => {
|
|
146
|
+
const keyExtractor = (_: string, index: number) => (index + 1).toString();
|
|
147
|
+
const masonryFlashList = mountMasonryFlashList({
|
|
148
|
+
keyExtractor,
|
|
149
|
+
});
|
|
150
|
+
expect(masonryFlashList.findAll(ProgressiveListView).length).toBe(3);
|
|
151
|
+
expect(
|
|
152
|
+
masonryFlashList
|
|
153
|
+
.findAll(ProgressiveListView)[0]
|
|
154
|
+
.instance.props.dataProvider.getStableId(0)
|
|
155
|
+
).toBe("0");
|
|
156
|
+
expect(
|
|
157
|
+
masonryFlashList
|
|
158
|
+
.findAll(ProgressiveListView)[1]
|
|
159
|
+
.instance.props.dataProvider.getStableId(0)
|
|
160
|
+
).toBe("1");
|
|
161
|
+
expect(
|
|
162
|
+
masonryFlashList
|
|
163
|
+
.findAll(ProgressiveListView)[2]
|
|
164
|
+
.instance.props.dataProvider.getStableId(0)
|
|
165
|
+
).toBe("2");
|
|
166
|
+
masonryFlashList.unmount();
|
|
167
|
+
});
|
|
168
|
+
it("correctly maps list indices to actual indices", () => {
|
|
169
|
+
const data = new Array(20).fill(0).map((_, index) => index.toString());
|
|
170
|
+
const getItemType = (item: string, index: number) => {
|
|
171
|
+
expect(index.toString()).toBe(item);
|
|
172
|
+
return 0;
|
|
173
|
+
};
|
|
174
|
+
const renderItem: MasonryFlashListProps<string>["renderItem"] = ({
|
|
175
|
+
item,
|
|
176
|
+
index,
|
|
177
|
+
}) => {
|
|
178
|
+
expect(index.toString()).toBe(item);
|
|
179
|
+
return null;
|
|
180
|
+
};
|
|
181
|
+
const overrideItemLayout: MasonryFlashListProps<string>["overrideItemLayout"] =
|
|
182
|
+
(layout, item: string, index: number) => {
|
|
183
|
+
expect(index.toString()).toBe(item);
|
|
184
|
+
};
|
|
185
|
+
const keyExtractor = (item: string, index: number) => {
|
|
186
|
+
expect(index.toString()).toBe(item);
|
|
187
|
+
return index.toString();
|
|
188
|
+
};
|
|
189
|
+
const onViewableItemsChanged: MasonryFlashListProps<string>["onViewableItemsChanged"] =
|
|
190
|
+
(info) => {
|
|
191
|
+
info.viewableItems.forEach((viewToken) => {
|
|
192
|
+
expect(viewToken.index?.toString()).toBe(viewToken.item);
|
|
193
|
+
});
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
const masonryFlashList = mountMasonryFlashList({
|
|
197
|
+
data,
|
|
198
|
+
renderItem,
|
|
199
|
+
getItemType,
|
|
200
|
+
overrideItemLayout,
|
|
201
|
+
keyExtractor,
|
|
202
|
+
onViewableItemsChanged,
|
|
203
|
+
});
|
|
204
|
+
jest.advanceTimersByTime(1000);
|
|
205
|
+
masonryFlashList.unmount();
|
|
206
|
+
});
|
|
207
|
+
it("internal list height should be derived from the parent and width from itself", () => {
|
|
208
|
+
const masonryFlashList = mountMasonryFlashList({
|
|
209
|
+
testID: "MasonryProxyScrollView",
|
|
210
|
+
});
|
|
211
|
+
expect(masonryFlashList.findAll(ProgressiveListView).length).toBe(3);
|
|
212
|
+
masonryFlashList.findAll(View).forEach((view: any) => {
|
|
213
|
+
view.props?.onLayout?.({
|
|
214
|
+
nativeEvent: { layout: { width: 500, height: 500 } },
|
|
215
|
+
});
|
|
216
|
+
});
|
|
217
|
+
masonryFlashList.findAll(ProgressiveListView).forEach((list, index) => {
|
|
218
|
+
if (index !== 0) {
|
|
219
|
+
expect(list.instance.getRenderedSize().width).toBe(500);
|
|
220
|
+
expect(list.instance.getRenderedSize().height).toBe(900);
|
|
221
|
+
}
|
|
222
|
+
});
|
|
223
|
+
masonryFlashList.unmount();
|
|
224
|
+
});
|
|
225
|
+
it("can optimize item arrangement", () => {
|
|
226
|
+
const columnCount = 3;
|
|
227
|
+
const data = new Array(999).fill(null).map((_, index) => {
|
|
228
|
+
return "1";
|
|
229
|
+
});
|
|
230
|
+
const masonryFlashList = mountMasonryFlashList({
|
|
231
|
+
data,
|
|
232
|
+
optimizeItemArrangement: true,
|
|
233
|
+
numColumns: columnCount,
|
|
234
|
+
overrideItemLayout(layout, _, index, __, ___?) {
|
|
235
|
+
layout.size = ((index * 10) % 100) + 100 / ((index % columnCount) + 1);
|
|
236
|
+
},
|
|
237
|
+
});
|
|
238
|
+
expect(masonryFlashList.findAll(ProgressiveListView).length).toBe(4);
|
|
239
|
+
|
|
240
|
+
// I've verified that the following values are correct by observing the algorithm in action
|
|
241
|
+
// Captured values will help prevent regression in the future
|
|
242
|
+
expect(
|
|
243
|
+
Math.floor(
|
|
244
|
+
masonryFlashList
|
|
245
|
+
.findAll(ProgressiveListView)[1]
|
|
246
|
+
.instance.getContentDimension().height
|
|
247
|
+
)
|
|
248
|
+
).toBe(35306);
|
|
249
|
+
expect(
|
|
250
|
+
Math.floor(
|
|
251
|
+
masonryFlashList
|
|
252
|
+
.findAll(ProgressiveListView)[2]
|
|
253
|
+
.instance.getContentDimension().height
|
|
254
|
+
)
|
|
255
|
+
).toBe(35313);
|
|
256
|
+
expect(
|
|
257
|
+
Math.floor(
|
|
258
|
+
masonryFlashList
|
|
259
|
+
.findAll(ProgressiveListView)[3]
|
|
260
|
+
.instance.getContentDimension().height
|
|
261
|
+
)
|
|
262
|
+
).toBe(35339);
|
|
263
|
+
});
|
|
264
|
+
});
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { Text } from "react-native";
|
|
3
|
+
import "@quilted/react-testing/matchers";
|
|
4
|
+
import { mount, Root } from "@quilted/react-testing";
|
|
5
|
+
|
|
6
|
+
import { ListRenderItem } from "../../FlashListProps";
|
|
7
|
+
import {
|
|
8
|
+
MasonryFlashList,
|
|
9
|
+
MasonryFlashListProps,
|
|
10
|
+
MasonryFlashListRef,
|
|
11
|
+
} from "../../MasonryFlashList";
|
|
12
|
+
|
|
13
|
+
jest.mock("../../FlashList", () => {
|
|
14
|
+
const ActualFlashList = jest.requireActual("../../FlashList").default;
|
|
15
|
+
class MockFlashList extends ActualFlashList {
|
|
16
|
+
componentDidMount() {
|
|
17
|
+
super.componentDidMount();
|
|
18
|
+
this.rlvRef?._scrollComponent?._scrollViewRef?.props.onLayout({
|
|
19
|
+
nativeEvent: { layout: { height: 900, width: 400 } },
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return MockFlashList;
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
export type MockMasonryFlashListProps = Omit<
|
|
27
|
+
MasonryFlashListProps<string>,
|
|
28
|
+
"estimatedItemSize" | "data" | "renderItem"
|
|
29
|
+
> & {
|
|
30
|
+
estimatedItemSize?: number;
|
|
31
|
+
data?: string[];
|
|
32
|
+
renderItem?: ListRenderItem<string>;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Helper to mount MasonryFlashList for testing.
|
|
37
|
+
*/
|
|
38
|
+
export const mountMasonryFlashList = (
|
|
39
|
+
props?: MockMasonryFlashListProps,
|
|
40
|
+
ref?: React.RefObject<MasonryFlashListRef<string>>
|
|
41
|
+
) => {
|
|
42
|
+
const flashList = mount(renderMasonryFlashList(props, ref)) as Omit<
|
|
43
|
+
Root<MasonryFlashListProps<string>>,
|
|
44
|
+
"instance"
|
|
45
|
+
> & {
|
|
46
|
+
instance: MasonryFlashListRef<string>;
|
|
47
|
+
};
|
|
48
|
+
return flashList;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export function renderMasonryFlashList(
|
|
52
|
+
props?: MockMasonryFlashListProps,
|
|
53
|
+
ref?: React.RefObject<MasonryFlashListRef<string>>
|
|
54
|
+
) {
|
|
55
|
+
return (
|
|
56
|
+
<MasonryFlashList
|
|
57
|
+
{...props}
|
|
58
|
+
ref={ref}
|
|
59
|
+
numColumns={props?.numColumns ?? 2}
|
|
60
|
+
renderItem={props?.renderItem || (({ item }) => <Text>{item}</Text>)}
|
|
61
|
+
estimatedItemSize={props?.estimatedItemSize ?? 200}
|
|
62
|
+
data={props?.data || ["One", "Two", "Three", "Four"]}
|
|
63
|
+
/>
|
|
64
|
+
);
|
|
65
|
+
}
|
|
@@ -19,5 +19,10 @@ const ExceptionList = {
|
|
|
19
19
|
"You can set exactly one of itemVisiblePercentThreshold or viewAreaCoveragePercentThreshold. Specifying both is not supported.",
|
|
20
20
|
type: "MultipleViewabilityThresholdTypesException",
|
|
21
21
|
},
|
|
22
|
+
overrideItemLayoutRequiredForMasonryOptimization: {
|
|
23
|
+
message:
|
|
24
|
+
"optimizeItemArrangement has been enabled on `MasonryFlashList` but overrideItemLayout is not set.",
|
|
25
|
+
type: "InvariantViolation",
|
|
26
|
+
},
|
|
22
27
|
};
|
|
23
28
|
export default ExceptionList;
|
package/src/index.ts
CHANGED
|
@@ -28,6 +28,15 @@ export {
|
|
|
28
28
|
BlankAreaTrackerResult,
|
|
29
29
|
BlankAreaTrackerConfig,
|
|
30
30
|
} from "./benchmark/useBlankAreaTracker";
|
|
31
|
+
export {
|
|
32
|
+
MasonryFlashList,
|
|
33
|
+
MasonryFlashListProps,
|
|
34
|
+
MasonryFlashListScrollEvent,
|
|
35
|
+
MasonryFlashListRef,
|
|
36
|
+
MasonryListItem,
|
|
37
|
+
MasonryListRenderItem,
|
|
38
|
+
MasonryListRenderItemInfo,
|
|
39
|
+
} from "./MasonryFlashList";
|
|
31
40
|
export { JSFPSMonitor, JSFPSResult } from "./benchmark/JSFPSMonitor";
|
|
32
41
|
export { autoScroll, Cancellable } from "./benchmark/AutoScrollHelper";
|
|
33
42
|
export { default as ViewToken } from "./viewability/ViewToken";
|
|
@@ -68,7 +68,8 @@ export default class ViewabilityManager<T> {
|
|
|
68
68
|
|
|
69
69
|
public updateViewableItems = (newViewableIndices?: number[]) => {
|
|
70
70
|
const listSize =
|
|
71
|
-
this.flashListRef.recyclerlistview_unsafe?.getRenderedSize()
|
|
71
|
+
this.flashListRef.recyclerlistview_unsafe?.getRenderedSize() ??
|
|
72
|
+
this.flashListRef.props.estimatedListSize;
|
|
72
73
|
if (listSize === undefined || !this.shouldListenToVisibleIndices) {
|
|
73
74
|
return;
|
|
74
75
|
}
|