@shopify/flash-list 2.1.0 → 2.2.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 (29) hide show
  1. package/dist/FlashListProps.d.ts +32 -0
  2. package/dist/FlashListProps.d.ts.map +1 -1
  3. package/dist/recyclerview/RecyclerView.d.ts.map +1 -1
  4. package/dist/recyclerview/RecyclerView.js +30 -9
  5. package/dist/recyclerview/RecyclerView.js.map +1 -1
  6. package/dist/recyclerview/ViewHolder.d.ts +2 -0
  7. package/dist/recyclerview/ViewHolder.d.ts.map +1 -1
  8. package/dist/recyclerview/ViewHolder.js +4 -2
  9. package/dist/recyclerview/ViewHolder.js.map +1 -1
  10. package/dist/recyclerview/ViewHolderCollection.d.ts +4 -0
  11. package/dist/recyclerview/ViewHolderCollection.d.ts.map +1 -1
  12. package/dist/recyclerview/ViewHolderCollection.js +2 -2
  13. package/dist/recyclerview/ViewHolderCollection.js.map +1 -1
  14. package/dist/recyclerview/components/StickyHeaders.d.ts +5 -1
  15. package/dist/recyclerview/components/StickyHeaders.d.ts.map +1 -1
  16. package/dist/recyclerview/components/StickyHeaders.js +47 -15
  17. package/dist/recyclerview/components/StickyHeaders.js.map +1 -1
  18. package/dist/recyclerview/hooks/useSecondaryProps.d.ts +2 -0
  19. package/dist/recyclerview/hooks/useSecondaryProps.d.ts.map +1 -1
  20. package/dist/recyclerview/hooks/useSecondaryProps.js +16 -1
  21. package/dist/recyclerview/hooks/useSecondaryProps.js.map +1 -1
  22. package/dist/tsconfig.tsbuildinfo +1 -1
  23. package/package.json +1 -1
  24. package/src/FlashListProps.ts +43 -0
  25. package/src/recyclerview/RecyclerView.tsx +35 -3
  26. package/src/recyclerview/ViewHolder.tsx +6 -1
  27. package/src/recyclerview/ViewHolderCollection.tsx +10 -0
  28. package/src/recyclerview/components/StickyHeaders.tsx +54 -13
  29. package/src/recyclerview/hooks/useSecondaryProps.tsx +23 -0
@@ -50,6 +50,10 @@ export interface ViewHolderCollectionProps<TItem> {
50
50
  * For startRenderingFromBottom, we need to adjust the height of the container
51
51
  */
52
52
  getAdjustmentMargin: () => number;
53
+ /** Current sticky index */
54
+ currentStickyIndex: number;
55
+ /** Whether the cell associated with an active sticky header is hidden */
56
+ hideStickyHeaderRelatedCell: boolean;
53
57
  }
54
58
 
55
59
  /**
@@ -84,6 +88,8 @@ export const ViewHolderCollection = <TItem,>(
84
88
  onCommitEffect,
85
89
  horizontal,
86
90
  getAdjustmentMargin,
91
+ currentStickyIndex,
92
+ hideStickyHeaderRelatedCell,
87
93
  } = props;
88
94
 
89
95
  const [renderId, setRenderId] = React.useState(0);
@@ -168,6 +174,7 @@ export const ViewHolderCollection = <TItem,>(
168
174
  const trailingItem = ItemSeparatorComponent
169
175
  ? data[index + 1]
170
176
  : undefined;
177
+
171
178
  return (
172
179
  <ViewHolder
173
180
  key={reactKey}
@@ -185,6 +192,9 @@ export const ViewHolderCollection = <TItem,>(
185
192
  CellRendererComponent={CellRendererComponent}
186
193
  ItemSeparatorComponent={ItemSeparatorComponent}
187
194
  horizontal={horizontal}
195
+ hidden={
196
+ hideStickyHeaderRelatedCell && currentStickyIndex === index
197
+ }
188
198
  />
189
199
  );
190
200
  })}
@@ -27,6 +27,10 @@ import { CompatAnimatedView } from "./CompatView";
27
27
  export interface StickyHeaderProps<TItem> {
28
28
  /** Array of indices that should have sticky headers */
29
29
  stickyHeaderIndices: number[];
30
+ /** Offset from the top where sticky headers should stick (in pixels) */
31
+ stickyHeaderOffset: number;
32
+ /** Sticky header change handler */
33
+ onChangeStickyIndex: (index: number) => void;
30
34
  /** The data array being rendered */
31
35
  data: ReadonlyArray<TItem>;
32
36
  /** Animated value tracking scroll position */
@@ -56,12 +60,14 @@ interface StickyHeaderState {
56
60
 
57
61
  export const StickyHeaders = <TItem,>({
58
62
  stickyHeaderIndices,
63
+ stickyHeaderOffset,
59
64
  renderItem,
60
65
  stickyHeaderRef,
61
66
  recyclerViewManager,
62
67
  scrollY,
63
68
  data,
64
69
  extraData,
70
+ onChangeStickyIndex,
65
71
  }: StickyHeaderProps<TItem>) => {
66
72
  const [stickyHeaderState, setStickyHeaderState] = useState<StickyHeaderState>(
67
73
  {
@@ -91,7 +97,7 @@ export const StickyHeaders = <TItem,>({
91
97
  // Binary search for current sticky index
92
98
  const currentIndexInArray = findCurrentStickyIndex(
93
99
  sortedIndices,
94
- adjustedScrollOffset,
100
+ adjustedScrollOffset + stickyHeaderOffset,
95
101
  (index) => recyclerViewManager.getLayout(index).y
96
102
  );
97
103
 
@@ -102,7 +108,8 @@ export const StickyHeaders = <TItem,>({
102
108
  newNextStickyIndex = -1;
103
109
  }
104
110
 
105
- // To make sure header offset is 0 in the interpolate compute
111
+ // Calculate when the next sticky header should start pushing the current one
112
+ // The next header starts pushing when it reaches the bottom of the current sticky header
106
113
  const newNextStickyY =
107
114
  newNextStickyIndex === -1
108
115
  ? Number.MAX_SAFE_INTEGER
@@ -111,6 +118,7 @@ export const StickyHeaders = <TItem,>({
111
118
  const newCurrentStickyHeight =
112
119
  recyclerViewManager.tryGetLayout(newStickyIndex)?.height ?? 0;
113
120
 
121
+ // Push should start when the next header reaches the bottom of the current sticky header
114
122
  const newPushStartsAt = newNextStickyY - newCurrentStickyHeight;
115
123
 
116
124
  if (
@@ -119,15 +127,21 @@ export const StickyHeaders = <TItem,>({
119
127
  ) {
120
128
  setStickyHeaderState({
121
129
  currentStickyIndex: newStickyIndex,
122
- pushStartsAt: newPushStartsAt,
130
+ pushStartsAt: newPushStartsAt - stickyHeaderOffset,
123
131
  });
124
132
  }
133
+
134
+ if (newStickyIndex !== currentStickyIndex) {
135
+ onChangeStickyIndex?.(newStickyIndex);
136
+ }
125
137
  }, [
126
138
  legthInvalid,
127
139
  recyclerViewManager,
128
140
  sortedIndices,
129
141
  currentStickyIndex,
130
142
  pushStartsAt,
143
+ onChangeStickyIndex,
144
+ stickyHeaderOffset,
131
145
  ]);
132
146
 
133
147
  useEffect(() => {
@@ -147,16 +161,32 @@ export const StickyHeaders = <TItem,>({
147
161
 
148
162
  const refHolder = useRef(new Map()).current;
149
163
 
150
- const translateY = useMemo(() => {
164
+ const { translateY, opacity } = useMemo(() => {
151
165
  const currentStickyHeight =
152
166
  recyclerViewManager.tryGetLayout(currentStickyIndex)?.height ?? 0;
153
167
 
154
- return scrollY.interpolate({
155
- inputRange: [pushStartsAt, pushStartsAt + currentStickyHeight],
156
- outputRange: [0, -currentStickyHeight],
157
- extrapolate: "clamp",
158
- });
159
- }, [recyclerViewManager, currentStickyIndex, scrollY, pushStartsAt]);
168
+ return {
169
+ translateY: scrollY.interpolate({
170
+ inputRange: [pushStartsAt, pushStartsAt + currentStickyHeight],
171
+ outputRange: [0, -currentStickyHeight],
172
+ extrapolate: "clamp",
173
+ }),
174
+ opacity:
175
+ stickyHeaderOffset > 0
176
+ ? scrollY.interpolate({
177
+ inputRange: [pushStartsAt, pushStartsAt + currentStickyHeight],
178
+ outputRange: [1, 0],
179
+ extrapolate: "clamp",
180
+ })
181
+ : undefined,
182
+ };
183
+ }, [
184
+ recyclerViewManager,
185
+ currentStickyIndex,
186
+ scrollY,
187
+ pushStartsAt,
188
+ stickyHeaderOffset,
189
+ ]);
160
190
 
161
191
  // Memoize header content
162
192
  const headerContent = useMemo(() => {
@@ -164,11 +194,12 @@ export const StickyHeaders = <TItem,>({
164
194
  <CompatAnimatedView
165
195
  style={{
166
196
  position: "absolute",
167
- top: 0,
197
+ top: stickyHeaderOffset,
168
198
  left: 0,
169
199
  right: 0,
170
- zIndex: 1,
200
+ zIndex: 2,
171
201
  transform: [{ translateY }],
202
+ opacity,
172
203
  }}
173
204
  >
174
205
  {currentStickyIndex !== -1 && currentStickyIndex < data.length ? (
@@ -181,11 +212,21 @@ export const StickyHeaders = <TItem,>({
181
212
  extraData={extraData}
182
213
  trailingItem={null}
183
214
  target="StickyHeader"
215
+ hidden={false}
184
216
  />
185
217
  ) : null}
186
218
  </CompatAnimatedView>
187
219
  );
188
- }, [translateY, currentStickyIndex, data, renderItem, refHolder, extraData]);
220
+ }, [
221
+ translateY,
222
+ opacity,
223
+ currentStickyIndex,
224
+ data,
225
+ renderItem,
226
+ refHolder,
227
+ extraData,
228
+ stickyHeaderOffset,
229
+ ]);
189
230
 
190
231
  return headerContent;
191
232
  };
@@ -20,6 +20,7 @@ import { CompatAnimatedScroller } from "../components/CompatScroller";
20
20
  * - renderHeader: The header component renderer
21
21
  * - renderFooter: The footer component renderer
22
22
  * - renderEmpty: The empty state component renderer
23
+ * - renderStickyHeaderBackdrop: The sticky header backdrop component renderer
23
24
  * - CompatScrollView: The animated scroll component
24
25
  */
25
26
  export function useSecondaryProps<T>(props: RecyclerViewProps<T>) {
@@ -35,6 +36,7 @@ export function useSecondaryProps<T>(props: RecyclerViewProps<T>) {
35
36
  onRefresh,
36
37
  data,
37
38
  refreshControl: customRefreshControl,
39
+ stickyHeaderConfig,
38
40
  } = props;
39
41
 
40
42
  /**
@@ -94,6 +96,26 @@ export function useSecondaryProps<T>(props: RecyclerViewProps<T>) {
94
96
  return getValidComponent(ListEmptyComponent);
95
97
  }, [ListEmptyComponent, data]);
96
98
 
99
+ /**
100
+ * Creates the sticky header backdrop component.
101
+ */
102
+ const renderStickyHeaderBackdrop = useMemo(() => {
103
+ if (!stickyHeaderConfig?.backdropComponent) {
104
+ return null;
105
+ }
106
+ return (
107
+ <CompatView
108
+ style={{
109
+ position: "absolute",
110
+ inset: 0,
111
+ pointerEvents: "none",
112
+ }}
113
+ >
114
+ {getValidComponent(stickyHeaderConfig?.backdropComponent)}
115
+ </CompatView>
116
+ );
117
+ }, [stickyHeaderConfig?.backdropComponent]);
118
+
97
119
  /**
98
120
  * Creates an animated scroll component based on the provided renderScrollComponent.
99
121
  * If no custom component is provided, uses the default CompatAnimatedScroller.
@@ -120,5 +142,6 @@ export function useSecondaryProps<T>(props: RecyclerViewProps<T>) {
120
142
  renderFooter,
121
143
  renderEmpty,
122
144
  CompatScrollView,
145
+ renderStickyHeaderBackdrop,
123
146
  };
124
147
  }