@shopify/flash-list 2.2.3 → 2.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.
Files changed (69) hide show
  1. package/dist/FlashListProps.d.ts +11 -0
  2. package/dist/FlashListProps.d.ts.map +1 -1
  3. package/dist/native/config/PlatformHelper.android.d.ts +10 -0
  4. package/dist/native/config/PlatformHelper.android.d.ts.map +1 -1
  5. package/dist/native/config/PlatformHelper.android.js +7 -0
  6. package/dist/native/config/PlatformHelper.android.js.map +1 -1
  7. package/dist/native/config/PlatformHelper.d.ts +10 -0
  8. package/dist/native/config/PlatformHelper.d.ts.map +1 -1
  9. package/dist/native/config/PlatformHelper.ios.d.ts +10 -0
  10. package/dist/native/config/PlatformHelper.ios.d.ts.map +1 -1
  11. package/dist/native/config/PlatformHelper.ios.js +2 -0
  12. package/dist/native/config/PlatformHelper.ios.js.map +1 -1
  13. package/dist/native/config/PlatformHelper.js +2 -0
  14. package/dist/native/config/PlatformHelper.js.map +1 -1
  15. package/dist/native/config/PlatformHelper.web.d.ts +10 -0
  16. package/dist/native/config/PlatformHelper.web.d.ts.map +1 -1
  17. package/dist/native/config/PlatformHelper.web.js +2 -0
  18. package/dist/native/config/PlatformHelper.web.js.map +1 -1
  19. package/dist/recyclerview/RecyclerView.d.ts.map +1 -1
  20. package/dist/recyclerview/RecyclerView.js +10 -3
  21. package/dist/recyclerview/RecyclerView.js.map +1 -1
  22. package/dist/recyclerview/RecyclerViewManager.d.ts.map +1 -1
  23. package/dist/recyclerview/RecyclerViewManager.js +16 -18
  24. package/dist/recyclerview/RecyclerViewManager.js.map +1 -1
  25. package/dist/recyclerview/RecyclerViewProps.d.ts +1 -1
  26. package/dist/recyclerview/RecyclerViewProps.d.ts.map +1 -1
  27. package/dist/recyclerview/ViewHolder.d.ts +2 -0
  28. package/dist/recyclerview/ViewHolder.d.ts.map +1 -1
  29. package/dist/recyclerview/ViewHolder.js +8 -2
  30. package/dist/recyclerview/ViewHolder.js.map +1 -1
  31. package/dist/recyclerview/ViewHolderCollection.d.ts +2 -0
  32. package/dist/recyclerview/ViewHolderCollection.d.ts.map +1 -1
  33. package/dist/recyclerview/ViewHolderCollection.js +2 -2
  34. package/dist/recyclerview/ViewHolderCollection.js.map +1 -1
  35. package/dist/recyclerview/components/StickyHeaders.d.ts +3 -1
  36. package/dist/recyclerview/components/StickyHeaders.d.ts.map +1 -1
  37. package/dist/recyclerview/components/StickyHeaders.js +3 -2
  38. package/dist/recyclerview/components/StickyHeaders.js.map +1 -1
  39. package/dist/recyclerview/hooks/useBoundDetection.d.ts.map +1 -1
  40. package/dist/recyclerview/hooks/useBoundDetection.js +7 -0
  41. package/dist/recyclerview/hooks/useBoundDetection.js.map +1 -1
  42. package/dist/recyclerview/hooks/useRecyclerViewController.d.ts.map +1 -1
  43. package/dist/recyclerview/hooks/useRecyclerViewController.js +11 -1
  44. package/dist/recyclerview/hooks/useRecyclerViewController.js.map +1 -1
  45. package/dist/recyclerview/hooks/useSecondaryProps.d.ts +1 -1
  46. package/dist/recyclerview/hooks/useSecondaryProps.d.ts.map +1 -1
  47. package/dist/recyclerview/hooks/useSecondaryProps.js +29 -13
  48. package/dist/recyclerview/hooks/useSecondaryProps.js.map +1 -1
  49. package/dist/recyclerview/utils/getInvertedTransformStyle.d.ts +10 -0
  50. package/dist/recyclerview/utils/getInvertedTransformStyle.d.ts.map +1 -0
  51. package/dist/recyclerview/utils/getInvertedTransformStyle.js +7 -0
  52. package/dist/recyclerview/utils/getInvertedTransformStyle.js.map +1 -0
  53. package/dist/tsconfig.tsbuildinfo +1 -1
  54. package/package.json +1 -1
  55. package/src/FlashListProps.ts +13 -0
  56. package/src/native/config/PlatformHelper.android.ts +7 -0
  57. package/src/native/config/PlatformHelper.ios.ts +2 -0
  58. package/src/native/config/PlatformHelper.ts +2 -0
  59. package/src/native/config/PlatformHelper.web.ts +2 -0
  60. package/src/recyclerview/RecyclerView.tsx +11 -0
  61. package/src/recyclerview/RecyclerViewManager.ts +17 -21
  62. package/src/recyclerview/RecyclerViewProps.ts +1 -1
  63. package/src/recyclerview/ViewHolder.tsx +11 -1
  64. package/src/recyclerview/ViewHolderCollection.tsx +4 -0
  65. package/src/recyclerview/components/StickyHeaders.tsx +5 -0
  66. package/src/recyclerview/hooks/useBoundDetection.ts +8 -0
  67. package/src/recyclerview/hooks/useRecyclerViewController.tsx +11 -1
  68. package/src/recyclerview/hooks/useSecondaryProps.tsx +36 -12
  69. package/src/recyclerview/utils/getInvertedTransformStyle.ts +7 -0
@@ -56,6 +56,8 @@ export interface ViewHolderCollectionProps<TItem> {
56
56
  hideStickyHeaderRelatedCell: boolean;
57
57
  /** Returns whether the item at the given index is in the last row of the layout */
58
58
  isInLastRow: (index: number) => boolean;
59
+ /** Whether the list is inverted */
60
+ inverted: FlashListProps<TItem>["inverted"];
59
61
  }
60
62
 
61
63
  /**
@@ -93,6 +95,7 @@ export const ViewHolderCollection = <TItem,>(
93
95
  currentStickyIndex,
94
96
  hideStickyHeaderRelatedCell,
95
97
  isInLastRow,
98
+ inverted,
96
99
  } = props;
97
100
 
98
101
  const [renderId, setRenderId] = React.useState(0);
@@ -202,6 +205,7 @@ export const ViewHolderCollection = <TItem,>(
202
205
  hidden={
203
206
  hideStickyHeaderRelatedCell && currentStickyIndex === index
204
207
  }
208
+ inverted={inverted}
205
209
  />
206
210
  );
207
211
  })}
@@ -44,6 +44,8 @@ export interface StickyHeaderProps<TItem> {
44
44
  recyclerViewManager: RecyclerViewManager<TItem>;
45
45
  /** Additional data to trigger re-renders */
46
46
  extraData: FlashListProps<TItem>["extraData"];
47
+ /** Whether the list is inverted */
48
+ inverted: FlashListProps<TItem>["inverted"];
47
49
  }
48
50
 
49
51
  /**
@@ -69,6 +71,7 @@ export const StickyHeaders = <TItem,>({
69
71
  data,
70
72
  extraData,
71
73
  onChangeStickyIndex,
74
+ inverted,
72
75
  }: StickyHeaderProps<TItem>) => {
73
76
  const [stickyHeaderState, setStickyHeaderState] = useState<StickyHeaderState>(
74
77
  {
@@ -214,6 +217,7 @@ export const StickyHeaders = <TItem,>({
214
217
  trailingItem={null}
215
218
  target="StickyHeader"
216
219
  hidden={false}
220
+ inverted={inverted}
217
221
  />
218
222
  ) : null}
219
223
  </CompatAnimatedView>
@@ -227,6 +231,7 @@ export const StickyHeaders = <TItem,>({
227
231
  refHolder,
228
232
  extraData,
229
233
  stickyHeaderOffset,
234
+ inverted,
230
235
  ]);
231
236
 
232
237
  if (PlatformConfig.isRN083OrAbove && currentStickyIndex === -1) {
@@ -84,6 +84,14 @@ export function useBoundDetection<T>(
84
84
  (isHorizontal ? contentSize.width : contentSize.height) +
85
85
  recyclerViewManager.firstItemOffset;
86
86
 
87
+ // Skip bound detection if the window has no measurable size.
88
+ // This can happen when the list is mounted off-screen (e.g., in a
89
+ // background tab) and all measurements come back as 0, which would
90
+ // incorrectly trigger onEndReached/onStartReached.
91
+ if (visibleLength <= 0) {
92
+ return;
93
+ }
94
+
87
95
  // Check if we're near the end of the list
88
96
  if (onEndReached) {
89
97
  const onEndReachedThreshold = onEndReachedThresholdProp ?? 0.5;
@@ -136,6 +136,16 @@ export function useRecyclerViewController<T>(
136
136
  const hasDataChanged = currentDataLength !== lastDataLengthRef.current;
137
137
  // If we have a tracked first visible item, maintain its position
138
138
  if (firstVisibleItemKey.current) {
139
+ // Try engaged indices first (fast O(n) over rendered items), then
140
+ // fall back to a full data search. The fallback is needed when:
141
+ // 1. Data just changed (hasDataChanged) — prepended items shift
142
+ // the anchor to a new index not yet in the engaged window.
143
+ // 2. A previous correction is still settling (ignoreScrollEvents
144
+ // is true) — after the first correction pass, unmeasured item
145
+ // heights may converge and push the anchor item outside the
146
+ // engaged window. ignoreScrollEvents stays true for 100ms
147
+ // after each correction, giving subsequent layout passes a
148
+ // chance to refine the scroll position.
139
149
  const currentIndexOfFirstVisibleItem =
140
150
  recyclerViewManager
141
151
  .getEngagedIndices()
@@ -144,7 +154,7 @@ export function useRecyclerViewController<T>(
144
154
  recyclerViewManager.getDataKey(index) ===
145
155
  firstVisibleItemKey.current
146
156
  ) ??
147
- (hasDataChanged
157
+ (hasDataChanged || recyclerViewManager.ignoreScrollEvents
148
158
  ? data?.findIndex(
149
159
  (item, index) =>
150
160
  recyclerViewManager.getDataKey(index) ===
@@ -5,6 +5,7 @@ import { RecyclerViewProps } from "../RecyclerViewProps";
5
5
  import { getValidComponent, isComponentClass } from "../utils/componentUtils";
6
6
  import { CompatView } from "../components/CompatView";
7
7
  import { CompatAnimatedScroller } from "../components/CompatScroller";
8
+ import { getInvertedTransformStyle } from "../utils/getInvertedTransformStyle";
8
9
 
9
10
  /**
10
11
  * Hook that manages secondary props and components for the RecyclerView.
@@ -30,6 +31,7 @@ export function useSecondaryProps<T>(props: RecyclerViewProps<T>) {
30
31
  ListFooterComponent,
31
32
  ListFooterComponentStyle,
32
33
  ListEmptyComponent,
34
+ ListEmptyComponentStyle,
33
35
  renderScrollComponent,
34
36
  refreshing,
35
37
  progressViewOffset,
@@ -37,8 +39,14 @@ export function useSecondaryProps<T>(props: RecyclerViewProps<T>) {
37
39
  data,
38
40
  refreshControl: customRefreshControl,
39
41
  stickyHeaderConfig,
42
+ inverted,
43
+ horizontal,
40
44
  } = props;
41
45
 
46
+ const invertedTransformStyle = inverted
47
+ ? getInvertedTransformStyle(horizontal)
48
+ : undefined;
49
+
42
50
  /**
43
51
  * Creates the refresh control component if onRefresh is provided.
44
52
  */
@@ -65,11 +73,11 @@ export function useSecondaryProps<T>(props: RecyclerViewProps<T>) {
65
73
  return null;
66
74
  }
67
75
  return (
68
- <CompatView style={ListHeaderComponentStyle}>
76
+ <CompatView style={[ListHeaderComponentStyle, invertedTransformStyle]}>
69
77
  {getValidComponent(ListHeaderComponent)}
70
78
  </CompatView>
71
79
  );
72
- }, [ListHeaderComponent, ListHeaderComponentStyle]);
80
+ }, [ListHeaderComponent, ListHeaderComponentStyle, invertedTransformStyle]);
73
81
 
74
82
  /**
75
83
  * Creates the footer component with optional styling.
@@ -79,11 +87,11 @@ export function useSecondaryProps<T>(props: RecyclerViewProps<T>) {
79
87
  return null;
80
88
  }
81
89
  return (
82
- <CompatView style={ListFooterComponentStyle}>
90
+ <CompatView style={[ListFooterComponentStyle, invertedTransformStyle]}>
83
91
  {getValidComponent(ListFooterComponent)}
84
92
  </CompatView>
85
93
  );
86
- }, [ListFooterComponent, ListFooterComponentStyle]);
94
+ }, [ListFooterComponent, ListFooterComponentStyle, invertedTransformStyle]);
87
95
 
88
96
  /**
89
97
  * Creates the empty state component when there's no data.
@@ -93,8 +101,21 @@ export function useSecondaryProps<T>(props: RecyclerViewProps<T>) {
93
101
  if (!ListEmptyComponent || (data && data.length > 0)) {
94
102
  return null;
95
103
  }
96
- return getValidComponent(ListEmptyComponent);
97
- }, [ListEmptyComponent, data]);
104
+ const emptyContent = getValidComponent(ListEmptyComponent);
105
+ if (!invertedTransformStyle && !ListEmptyComponentStyle) {
106
+ return emptyContent;
107
+ }
108
+ return (
109
+ <CompatView style={[ListEmptyComponentStyle, invertedTransformStyle]}>
110
+ {emptyContent}
111
+ </CompatView>
112
+ );
113
+ }, [
114
+ ListEmptyComponent,
115
+ data,
116
+ invertedTransformStyle,
117
+ ListEmptyComponentStyle,
118
+ ]);
98
119
 
99
120
  /**
100
121
  * Creates the sticky header backdrop component.
@@ -105,16 +126,19 @@ export function useSecondaryProps<T>(props: RecyclerViewProps<T>) {
105
126
  }
106
127
  return (
107
128
  <CompatView
108
- style={{
109
- position: "absolute",
110
- inset: 0,
111
- pointerEvents: "none",
112
- }}
129
+ style={[
130
+ {
131
+ position: "absolute",
132
+ inset: 0,
133
+ pointerEvents: "none",
134
+ },
135
+ invertedTransformStyle,
136
+ ]}
113
137
  >
114
138
  {getValidComponent(stickyHeaderConfig?.backdropComponent)}
115
139
  </CompatView>
116
140
  );
117
- }, [stickyHeaderConfig?.backdropComponent]);
141
+ }, [stickyHeaderConfig?.backdropComponent, invertedTransformStyle]);
118
142
 
119
143
  /**
120
144
  * Creates an animated scroll component based on the provided renderScrollComponent.
@@ -0,0 +1,7 @@
1
+ import { PlatformConfig } from "../../native/config/PlatformHelper";
2
+
3
+ export function getInvertedTransformStyle(horizontal?: boolean | null) {
4
+ return horizontal
5
+ ? PlatformConfig.invertedTransformStyleHorizontal
6
+ : PlatformConfig.invertedTransformStyle;
7
+ }