@shopify/flash-list 1.3.1 → 1.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/dist/FlashList.d.ts +0 -1
  3. package/dist/FlashList.d.ts.map +1 -1
  4. package/dist/FlashList.js +22 -44
  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 +1 -1
  13. package/dist/MasonryFlashList.d.ts.map +1 -1
  14. package/dist/MasonryFlashList.js +13 -4
  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 +28 -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/tsconfig.tsbuildinfo +1 -1
  34. package/dist/utils/ContentContainerUtils.d.ts +27 -0
  35. package/dist/utils/ContentContainerUtils.d.ts.map +1 -0
  36. package/dist/utils/ContentContainerUtils.js +48 -0
  37. package/dist/utils/ContentContainerUtils.js.map +1 -0
  38. package/ios/Sources/AutoLayoutView.swift +1 -1
  39. package/package.json +3 -3
  40. package/src/FlashList.tsx +36 -52
  41. package/src/FlashListProps.ts +0 -1
  42. package/src/GridLayoutProviderWithProps.ts +39 -3
  43. package/src/MasonryFlashList.tsx +18 -4
  44. package/src/__tests__/ContentContainerUtils.test.ts +130 -0
  45. package/src/__tests__/FlashList.test.tsx +32 -0
  46. package/src/__tests__/GridLayoutProviderWithProps.test.ts +29 -0
  47. package/src/__tests__/MasonryFlashList.test.ts +28 -0
  48. package/src/__tests__/helpers/mountMasonryFlashList.tsx +6 -1
  49. package/src/errors/Warnings.ts +1 -4
  50. package/src/utils/ContentContainerUtils.ts +92 -0
@@ -12,6 +12,7 @@ import CustomError from "./errors/CustomError";
12
12
  import ExceptionList from "./errors/ExceptionList";
13
13
  import FlashList from "./FlashList";
14
14
  import { FlashListProps, ListRenderItemInfo } from "./FlashListProps";
15
+ import { applyContentContainerInsetForLayoutManager } from "./utils/ContentContainerUtils";
15
16
  import ViewToken from "./viewability/ViewToken";
16
17
 
17
18
  export interface MasonryListRenderItemInfo<TItem>
@@ -31,6 +32,7 @@ export interface MasonryFlashListProps<T>
31
32
  | "initialScrollIndex"
32
33
  | "inverted"
33
34
  | "onBlankArea"
35
+ | "renderItem"
34
36
  | "viewabilityConfigCallbackPairs"
35
37
  > {
36
38
  /**
@@ -175,6 +177,12 @@ const MasonryFlashListComponent = React.forwardRef(
175
177
  (dataSet[0]?.length ?? 0) *
176
178
  (props.estimatedItemSize ?? defaultEstimatedItemSize);
177
179
 
180
+ const insetForLayoutManager = applyContentContainerInsetForLayoutManager(
181
+ { height: 0, width: 0 },
182
+ props.contentContainerStyle,
183
+ false
184
+ );
185
+
178
186
  return (
179
187
  <FlashList
180
188
  ref={getFlashList}
@@ -227,8 +235,9 @@ const MasonryFlashListComponent = React.forwardRef(
227
235
  estimatedListSize={{
228
236
  height: estimatedListSize.height,
229
237
  width:
230
- ((getListRenderedSize(parentFlashList)?.width ||
231
- estimatedListSize.width) /
238
+ (((getListRenderedSize(parentFlashList)?.width ||
239
+ estimatedListSize.width) +
240
+ insetForLayoutManager.width) /
232
241
  totalColumnFlex) *
233
242
  (getColumnFlex?.(
234
243
  args.item,
@@ -430,8 +439,13 @@ const updateViewTokens = (tokens: ViewToken[]) => {
430
439
  for (let i = 0; i < length; i++) {
431
440
  const token = tokens[i];
432
441
  if (token.index !== null && token.index !== undefined) {
433
- token.index = token.item.originalIndex;
434
- 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
+ }
435
449
  }
436
450
  }
437
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
  });
@@ -261,4 +261,32 @@ describe("MasonryFlashList", () => {
261
261
  )
262
262
  ).toBe(35339);
263
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
+ });
264
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.",
@@ -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
+ };