@shopify/flash-list 1.3.0 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/FlashList.d.ts +0 -1
  3. package/dist/FlashList.d.ts.map +1 -1
  4. package/dist/FlashList.js +17 -41
  5. package/dist/FlashList.js.map +1 -1
  6. package/dist/FlashListProps.d.ts +0 -1
  7. package/dist/FlashListProps.d.ts.map +1 -1
  8. package/dist/GridLayoutProviderWithProps.d.ts +9 -1
  9. package/dist/GridLayoutProviderWithProps.d.ts.map +1 -1
  10. package/dist/GridLayoutProviderWithProps.js +30 -1
  11. package/dist/GridLayoutProviderWithProps.js.map +1 -1
  12. package/dist/MasonryFlashList.d.ts +14 -2
  13. package/dist/MasonryFlashList.d.ts.map +1 -1
  14. package/dist/MasonryFlashList.js +14 -5
  15. package/dist/MasonryFlashList.js.map +1 -1
  16. package/dist/__tests__/ContentContainerUtils.test.d.ts +2 -0
  17. package/dist/__tests__/ContentContainerUtils.test.d.ts.map +1 -0
  18. package/dist/__tests__/ContentContainerUtils.test.js +85 -0
  19. package/dist/__tests__/ContentContainerUtils.test.js.map +1 -0
  20. package/dist/__tests__/FlashList.test.js +32 -0
  21. package/dist/__tests__/FlashList.test.js.map +1 -1
  22. package/dist/__tests__/GridLayoutProviderWithProps.test.js +10 -0
  23. package/dist/__tests__/GridLayoutProviderWithProps.test.js.map +1 -1
  24. package/dist/__tests__/MasonryFlashList.test.js +49 -0
  25. package/dist/__tests__/MasonryFlashList.test.js.map +1 -1
  26. package/dist/__tests__/helpers/mountMasonryFlashList.d.ts.map +1 -1
  27. package/dist/__tests__/helpers/mountMasonryFlashList.js +7 -2
  28. package/dist/__tests__/helpers/mountMasonryFlashList.js.map +1 -1
  29. package/dist/errors/Warnings.d.ts +0 -1
  30. package/dist/errors/Warnings.d.ts.map +1 -1
  31. package/dist/errors/Warnings.js +1 -3
  32. package/dist/errors/Warnings.js.map +1 -1
  33. package/dist/index.d.ts +1 -1
  34. package/dist/index.d.ts.map +1 -1
  35. package/dist/index.js.map +1 -1
  36. package/dist/tsconfig.tsbuildinfo +1 -1
  37. package/dist/utils/ContentContainerUtils.d.ts +27 -0
  38. package/dist/utils/ContentContainerUtils.d.ts.map +1 -0
  39. package/dist/utils/ContentContainerUtils.js +48 -0
  40. package/dist/utils/ContentContainerUtils.js.map +1 -0
  41. package/package.json +3 -3
  42. package/src/FlashList.tsx +29 -50
  43. package/src/FlashListProps.ts +0 -1
  44. package/src/GridLayoutProviderWithProps.ts +39 -3
  45. package/src/MasonryFlashList.tsx +39 -5
  46. package/src/__tests__/ContentContainerUtils.test.ts +130 -0
  47. package/src/__tests__/FlashList.test.tsx +32 -0
  48. package/src/__tests__/GridLayoutProviderWithProps.test.ts +29 -0
  49. package/src/__tests__/MasonryFlashList.test.ts +57 -0
  50. package/src/__tests__/helpers/mountMasonryFlashList.tsx +6 -1
  51. package/src/errors/Warnings.ts +1 -4
  52. package/src/index.ts +2 -0
  53. package/src/utils/ContentContainerUtils.ts +92 -0
@@ -11,9 +11,20 @@ import {
11
11
  import CustomError from "./errors/CustomError";
12
12
  import ExceptionList from "./errors/ExceptionList";
13
13
  import FlashList from "./FlashList";
14
- import { FlashListProps } from "./FlashListProps";
14
+ import { FlashListProps, ListRenderItemInfo } from "./FlashListProps";
15
+ import { applyContentContainerInsetForLayoutManager } from "./utils/ContentContainerUtils";
15
16
  import ViewToken from "./viewability/ViewToken";
16
17
 
18
+ export interface MasonryListRenderItemInfo<TItem>
19
+ extends ListRenderItemInfo<TItem> {
20
+ columnSpan: number;
21
+ columnIndex: number;
22
+ }
23
+
24
+ export type MasonryListRenderItem<TItem> = (
25
+ info: MasonryListRenderItemInfo<TItem>
26
+ ) => React.ReactElement | null;
27
+
17
28
  export interface MasonryFlashListProps<T>
18
29
  extends Omit<
19
30
  FlashListProps<T>,
@@ -21,6 +32,7 @@ export interface MasonryFlashListProps<T>
21
32
  | "initialScrollIndex"
22
33
  | "inverted"
23
34
  | "onBlankArea"
35
+ | "renderItem"
24
36
  | "viewabilityConfigCallbackPairs"
25
37
  > {
26
38
  /**
@@ -39,6 +51,14 @@ export interface MasonryFlashListProps<T>
39
51
  * `overrideItemLayout` is required to make this work.
40
52
  */
41
53
  optimizeItemArrangement?: boolean;
54
+
55
+ /**
56
+ * Extends typical `renderItem` to include `columnIndex` and `columnSpan` (number of columns the item spans).
57
+ * `columnIndex` gives the consumer column information in case they might need to treat items differently based on column.
58
+ * This information may not otherwise be derived if using the `optimizeItemArrangement` feature, as the items will no
59
+ * longer be linearly distributed across the columns; instead they are allocated to the column with the least estimated height.
60
+ */
61
+ renderItem: MasonryListRenderItem<T> | null | undefined;
42
62
  }
43
63
 
44
64
  type OnScrollCallback = ScrollViewProps["onScroll"];
@@ -157,6 +177,12 @@ const MasonryFlashListComponent = React.forwardRef(
157
177
  (dataSet[0]?.length ?? 0) *
158
178
  (props.estimatedItemSize ?? defaultEstimatedItemSize);
159
179
 
180
+ const insetForLayoutManager = applyContentContainerInsetForLayoutManager(
181
+ { height: 0, width: 0 },
182
+ props.contentContainerStyle,
183
+ false
184
+ );
185
+
160
186
  return (
161
187
  <FlashList
162
188
  ref={getFlashList}
@@ -179,6 +205,8 @@ const MasonryFlashListComponent = React.forwardRef(
179
205
  ...innerArgs,
180
206
  item: innerArgs.item.originalItem,
181
207
  index: innerArgs.item.originalIndex,
208
+ columnSpan: 1,
209
+ columnIndex: args.index,
182
210
  }) ?? null
183
211
  );
184
212
  }}
@@ -207,8 +235,9 @@ const MasonryFlashListComponent = React.forwardRef(
207
235
  estimatedListSize={{
208
236
  height: estimatedListSize.height,
209
237
  width:
210
- ((getListRenderedSize(parentFlashList)?.width ||
211
- estimatedListSize.width) /
238
+ (((getListRenderedSize(parentFlashList)?.width ||
239
+ estimatedListSize.width) +
240
+ insetForLayoutManager.width) /
212
241
  totalColumnFlex) *
213
242
  (getColumnFlex?.(
214
243
  args.item,
@@ -410,8 +439,13 @@ const updateViewTokens = (tokens: ViewToken[]) => {
410
439
  for (let i = 0; i < length; i++) {
411
440
  const token = tokens[i];
412
441
  if (token.index !== null && token.index !== undefined) {
413
- token.index = token.item.originalIndex;
414
- token.item = token.item.originalItem;
442
+ if (token.item) {
443
+ token.index = token.item.originalIndex;
444
+ token.item = token.item.originalItem;
445
+ } else {
446
+ token.index = null;
447
+ token.item = undefined;
448
+ }
415
449
  }
416
450
  }
417
451
  };
@@ -0,0 +1,130 @@
1
+ import {
2
+ applyContentContainerInsetForLayoutManager,
3
+ getContentContainerPadding,
4
+ hasUnsupportedKeysInContentContainerStyle,
5
+ updateContentStyle,
6
+ } from "../utils/ContentContainerUtils";
7
+
8
+ describe("ContentContainerUtils", () => {
9
+ it("detects unsupported keys in style", () => {
10
+ expect(hasUnsupportedKeysInContentContainerStyle({ flex: 1 })).toBe(true);
11
+ expect(hasUnsupportedKeysInContentContainerStyle({ paddingTop: 0 })).toBe(
12
+ false
13
+ );
14
+ expect(
15
+ hasUnsupportedKeysInContentContainerStyle({
16
+ paddingTop: 1,
17
+ paddingVertical: 1,
18
+ })
19
+ ).toBe(false);
20
+ expect(
21
+ hasUnsupportedKeysInContentContainerStyle({
22
+ paddingTop: 1,
23
+ paddingVertical: 1,
24
+ padding: 1,
25
+ paddingLeft: 1,
26
+ paddingRight: 1,
27
+ paddingBottom: 1,
28
+ backgroundColor: "red",
29
+ paddingHorizontal: 1,
30
+ })
31
+ ).toBe(false);
32
+ expect(hasUnsupportedKeysInContentContainerStyle({ margin: 1 })).toBe(true);
33
+ expect(hasUnsupportedKeysInContentContainerStyle({ padding: 1 })).toBe(
34
+ false
35
+ );
36
+ expect(
37
+ hasUnsupportedKeysInContentContainerStyle({ backgroundColor: "red" })
38
+ ).toBe(false);
39
+ });
40
+ it("updated content style to have all supported styles defined", () => {
41
+ expect(
42
+ updateContentStyle({}, { padding: 1, backgroundColor: "red" })
43
+ ).toEqual({
44
+ paddingTop: 1,
45
+ paddingBottom: 1,
46
+ paddingLeft: 1,
47
+ paddingRight: 1,
48
+ backgroundColor: "red",
49
+ });
50
+ expect(updateContentStyle({}, { paddingHorizontal: 1 })).toEqual({
51
+ paddingTop: 0,
52
+ paddingBottom: 0,
53
+ paddingLeft: 1,
54
+ paddingRight: 1,
55
+ });
56
+ expect(updateContentStyle({}, { paddingVertical: 1 })).toEqual({
57
+ paddingTop: 1,
58
+ paddingBottom: 1,
59
+ paddingLeft: 0,
60
+ paddingRight: 0,
61
+ });
62
+ expect(
63
+ updateContentStyle({}, { paddingLeft: "1", paddingVertical: "1" })
64
+ ).toEqual({
65
+ paddingTop: 1,
66
+ paddingBottom: 1,
67
+ paddingLeft: 1,
68
+ paddingRight: 0,
69
+ });
70
+ });
71
+ it("computes correct layout manager insets", () => {
72
+ expect(
73
+ applyContentContainerInsetForLayoutManager(
74
+ { height: 0, width: 0 },
75
+ { padding: 1 },
76
+ false
77
+ )
78
+ ).toEqual({ height: 0, width: -2 });
79
+ expect(
80
+ applyContentContainerInsetForLayoutManager(
81
+ { height: 0, width: 0 },
82
+ { padding: 1 },
83
+ true
84
+ )
85
+ ).toEqual({ height: -2, width: 0 });
86
+ expect(
87
+ applyContentContainerInsetForLayoutManager(
88
+ { height: 0, width: 0 },
89
+ { paddingVertical: 1 },
90
+ true
91
+ )
92
+ ).toEqual({ height: -2, width: 0 });
93
+ });
94
+ it("calculated correct padding for scrollview content", () => {
95
+ expect(
96
+ getContentContainerPadding(
97
+ {
98
+ paddingLeft: 1,
99
+ paddingTop: 1,
100
+ paddingBottom: 1,
101
+ paddingRight: 1,
102
+ backgroundColor: "red",
103
+ },
104
+ true
105
+ )
106
+ ).toEqual({
107
+ paddingTop: 1,
108
+ paddingBottom: 1,
109
+ paddingLeft: undefined,
110
+ paddingRight: undefined,
111
+ });
112
+ expect(
113
+ getContentContainerPadding(
114
+ {
115
+ paddingLeft: 1,
116
+ paddingTop: 1,
117
+ paddingBottom: 1,
118
+ paddingRight: 1,
119
+ backgroundColor: "red",
120
+ },
121
+ false
122
+ )
123
+ ).toEqual({
124
+ paddingTop: undefined,
125
+ paddingBottom: undefined,
126
+ paddingLeft: 1,
127
+ paddingRight: 1,
128
+ });
129
+ });
130
+ });
@@ -824,4 +824,36 @@ describe("FlashList", () => {
824
824
  expect(scrollToOffset).toBeCalledWith(1800, 1800, false, true);
825
825
  flashList.unmount();
826
826
  });
827
+ it("applies horizontal content container padding for vertical list", () => {
828
+ const flashList = mountFlashList({
829
+ numColumns: 4,
830
+ contentContainerStyle: { paddingHorizontal: 10 },
831
+ });
832
+ let hasLayoutItems = false;
833
+ flashList.instance.state.layoutProvider
834
+ .getLayoutManager()!
835
+ .getLayouts()
836
+ .forEach((layout) => {
837
+ hasLayoutItems = true;
838
+ expect(layout.width).toBe(95);
839
+ });
840
+ expect(hasLayoutItems).toBe(true);
841
+ flashList.unmount();
842
+ });
843
+ it("applies vertical content container padding for horizontal list", () => {
844
+ const flashList = mountFlashList({
845
+ horizontal: true,
846
+ contentContainerStyle: { paddingVertical: 10 },
847
+ });
848
+ let hasLayoutItems = false;
849
+ flashList.instance.state.layoutProvider
850
+ .getLayoutManager()!
851
+ .getLayouts()
852
+ .forEach((layout) => {
853
+ hasLayoutItems = true;
854
+ expect(layout.height).toBe(880);
855
+ });
856
+ expect(hasLayoutItems).toBe(true);
857
+ flashList.unmount();
858
+ });
827
859
  });
@@ -147,4 +147,33 @@ describe("GridLayoutProviderWithProps", () => {
147
147
  // horizontal list
148
148
  runCacheUpdateTest(true);
149
149
  });
150
+ it("expires if column count or padding changes", () => {
151
+ const flashList = mountFlashList();
152
+ const baseProps = flashList.instance.props;
153
+ expect(
154
+ flashList.instance.state.layoutProvider.updateProps({
155
+ ...baseProps,
156
+ contentContainerStyle: { paddingTop: 10 },
157
+ }).hasExpired
158
+ ).toBe(false);
159
+ expect(
160
+ flashList.instance.state.layoutProvider.updateProps({
161
+ ...baseProps,
162
+ contentContainerStyle: { paddingBottom: 10 },
163
+ }).hasExpired
164
+ ).toBe(false);
165
+ expect(
166
+ flashList.instance.state.layoutProvider.updateProps({
167
+ ...baseProps,
168
+ contentContainerStyle: { paddingLeft: 10 },
169
+ }).hasExpired
170
+ ).toBe(true);
171
+ flashList.instance.state.layoutProvider["_hasExpired"] = false;
172
+ expect(
173
+ flashList.instance.state.layoutProvider.updateProps({
174
+ ...baseProps,
175
+ numColumns: 2,
176
+ }).hasExpired
177
+ ).toBe(true);
178
+ });
150
179
  });
@@ -26,6 +26,35 @@ describe("MasonryFlashList", () => {
26
26
  });
27
27
  masonryFlashList.unmount();
28
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
+ });
29
58
  it("raised onLoad event only when first internal child mounts", () => {
30
59
  const onLoadMock = jest.fn();
31
60
  const ref = React.createRef<MasonryFlashListRef<string>>();
@@ -232,4 +261,32 @@ describe("MasonryFlashList", () => {
232
261
  )
233
262
  ).toBe(35339);
234
263
  });
264
+ it("applies horizontal content container padding to the list", () => {
265
+ const masonryFlashList = mountMasonryFlashList({
266
+ numColumns: 4,
267
+ contentContainerStyle: { paddingHorizontal: 10 },
268
+ });
269
+ expect(masonryFlashList.findAll(ProgressiveListView).length).toBe(5);
270
+ masonryFlashList.findAll(ProgressiveListView).forEach((list, index) => {
271
+ if (index === 0) {
272
+ expect(list.instance.getRenderedSize().width).toBe(400);
273
+ expect(list.instance.getRenderedSize().height).toBe(900);
274
+ } else {
275
+ expect(list.instance.getRenderedSize().width).toBe(95);
276
+ expect(list.instance.getRenderedSize().height).toBe(900);
277
+ }
278
+ });
279
+ masonryFlashList.unmount();
280
+ });
281
+ it("divides columns equally if no getColumnFlex is passed", () => {
282
+ const masonryFlashList = mountMasonryFlashList({
283
+ numColumns: 4,
284
+ });
285
+ const progressiveListView =
286
+ masonryFlashList.find(ProgressiveListView)!.instance;
287
+ expect(progressiveListView.getLayout(0).width).toBe(100);
288
+ expect(progressiveListView.getLayout(1).width).toBe(100);
289
+ expect(progressiveListView.getLayout(2).width).toBe(100);
290
+ expect(progressiveListView.getLayout(3).width).toBe(100);
291
+ });
235
292
  });
@@ -16,7 +16,12 @@ jest.mock("../../FlashList", () => {
16
16
  componentDidMount() {
17
17
  super.componentDidMount();
18
18
  this.rlvRef?._scrollComponent?._scrollViewRef?.props.onLayout({
19
- nativeEvent: { layout: { height: 900, width: 400 } },
19
+ nativeEvent: {
20
+ layout: {
21
+ height: this.props.estimatedListSize?.height ?? 900,
22
+ width: this.props.estimatedListSize?.width ?? 400,
23
+ },
24
+ },
20
25
  });
21
26
  }
22
27
  }
@@ -2,10 +2,7 @@ const WarningList = {
2
2
  styleUnsupported:
3
3
  "You have passed a style to FlashList. This list doesn't support styling, use contentContainerStyle or wrap the list in a parent and apply style to it instead.",
4
4
  styleContentContainerUnsupported:
5
- "FlashList only supports padding related props and backgroundColor in contentContainerStyle." +
6
- " Please remove other values as they're not used. In case of vertical lists horizontal padding is ignored and vice versa, if you need it apply padding to your items instead.",
7
- styleUnsupportedPaddingType:
8
- "FlashList will ignore horizontal padding in case of vertical lists and vertical padding if the list is horizontal. If you need to have it apply relevant padding to your items instead.",
5
+ "FlashList only supports padding related props and backgroundColor in contentContainerStyle. Please remove other values as they're not used.",
9
6
  unusableRenderedSize:
10
7
  "FlashList's rendered size is not usable. Either the height or width is too small (<2px). " +
11
8
  "Please make sure that the parent view of the list has a valid size. FlashList will match the size of the parent.",
package/src/index.ts CHANGED
@@ -34,6 +34,8 @@ export {
34
34
  MasonryFlashListScrollEvent,
35
35
  MasonryFlashListRef,
36
36
  MasonryListItem,
37
+ MasonryListRenderItem,
38
+ MasonryListRenderItemInfo,
37
39
  } from "./MasonryFlashList";
38
40
  export { JSFPSMonitor, JSFPSResult } from "./benchmark/JSFPSMonitor";
39
41
  export { autoScroll, Cancellable } from "./benchmark/AutoScrollHelper";
@@ -0,0 +1,92 @@
1
+ import { ViewStyle } from "react-native";
2
+ import { Dimension } from "recyclerlistview";
3
+
4
+ import { ContentStyle } from "../FlashListProps";
5
+
6
+ export interface ContentStyleExplicit {
7
+ paddingTop: number;
8
+ paddingBottom: number;
9
+ paddingLeft: number;
10
+ paddingRight: number;
11
+ backgroundColor?: string;
12
+ }
13
+
14
+ export const updateContentStyle = (
15
+ contentStyle: ContentStyle,
16
+ contentContainerStyleSource: ContentStyle | undefined
17
+ ): ContentStyleExplicit => {
18
+ const {
19
+ paddingTop,
20
+ paddingRight,
21
+ paddingBottom,
22
+ paddingLeft,
23
+ padding,
24
+ paddingVertical,
25
+ paddingHorizontal,
26
+ backgroundColor,
27
+ } = (contentContainerStyleSource ?? {}) as ViewStyle;
28
+ contentStyle.paddingLeft = Number(
29
+ paddingLeft || paddingHorizontal || padding || 0
30
+ );
31
+ contentStyle.paddingRight = Number(
32
+ paddingRight || paddingHorizontal || padding || 0
33
+ );
34
+ contentStyle.paddingTop = Number(
35
+ paddingTop || paddingVertical || padding || 0
36
+ );
37
+ contentStyle.paddingBottom = Number(
38
+ paddingBottom || paddingVertical || padding || 0
39
+ );
40
+ contentStyle.backgroundColor = backgroundColor;
41
+ return contentStyle as ContentStyleExplicit;
42
+ };
43
+
44
+ export const hasUnsupportedKeysInContentContainerStyle = (
45
+ contentContainerStyleSource: ViewStyle | undefined
46
+ ) => {
47
+ const {
48
+ paddingTop,
49
+ paddingRight,
50
+ paddingBottom,
51
+ paddingLeft,
52
+ padding,
53
+ paddingVertical,
54
+ paddingHorizontal,
55
+ backgroundColor,
56
+ ...rest
57
+ } = (contentContainerStyleSource ?? {}) as ViewStyle;
58
+ return Object.keys(rest).length > 0;
59
+ };
60
+
61
+ /** Applies padding corrections to given dimension. Mutates the dim object that was passed and returns it. */
62
+ export const applyContentContainerInsetForLayoutManager = (
63
+ dim: Dimension,
64
+ contentContainerStyle: ViewStyle | undefined,
65
+ horizontal: boolean | undefined | null
66
+ ) => {
67
+ const contentStyle = updateContentStyle({}, contentContainerStyle);
68
+ if (horizontal) {
69
+ dim.height -= contentStyle.paddingTop + contentStyle.paddingBottom;
70
+ } else {
71
+ dim.width -= contentStyle.paddingLeft + contentStyle.paddingRight;
72
+ }
73
+ return dim;
74
+ };
75
+
76
+ /** Returns padding to be applied on content container and will ignore paddings that have already been handled. */
77
+ export const getContentContainerPadding = (
78
+ contentStyle: ContentStyleExplicit,
79
+ horizontal: boolean | undefined | null
80
+ ) => {
81
+ if (horizontal) {
82
+ return {
83
+ paddingTop: contentStyle.paddingTop,
84
+ paddingBottom: contentStyle.paddingBottom,
85
+ };
86
+ } else {
87
+ return {
88
+ paddingLeft: contentStyle.paddingLeft,
89
+ paddingRight: contentStyle.paddingRight,
90
+ };
91
+ }
92
+ };