@shopify/flash-list 2.2.1 → 2.2.3
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/dist/FlashListProps.d.ts +33 -8
- package/dist/FlashListProps.d.ts.map +1 -1
- package/dist/recyclerview/RecyclerView.d.ts.map +1 -1
- package/dist/recyclerview/RecyclerView.js +15 -12
- package/dist/recyclerview/RecyclerView.js.map +1 -1
- package/dist/recyclerview/RecyclerViewManager.d.ts +2 -0
- package/dist/recyclerview/RecyclerViewManager.d.ts.map +1 -1
- package/dist/recyclerview/RecyclerViewManager.js +8 -1
- package/dist/recyclerview/RecyclerViewManager.js.map +1 -1
- package/dist/recyclerview/ViewHolderCollection.d.ts +2 -0
- package/dist/recyclerview/ViewHolderCollection.d.ts.map +1 -1
- package/dist/recyclerview/ViewHolderCollection.js +5 -2
- package/dist/recyclerview/ViewHolderCollection.js.map +1 -1
- package/dist/recyclerview/hooks/useBoundDetection.js +1 -1
- package/dist/recyclerview/hooks/useBoundDetection.js.map +1 -1
- package/dist/recyclerview/hooks/useRecyclerViewController.d.ts.map +1 -1
- package/dist/recyclerview/hooks/useRecyclerViewController.js +2 -3
- package/dist/recyclerview/hooks/useRecyclerViewController.js.map +1 -1
- package/dist/recyclerview/hooks/useSecondaryProps.d.ts +1 -1
- package/dist/recyclerview/hooks/useSecondaryProps.d.ts.map +1 -1
- package/dist/recyclerview/hooks/useSecondaryProps.js +7 -3
- package/dist/recyclerview/hooks/useSecondaryProps.js.map +1 -1
- package/dist/recyclerview/layout-managers/GridLayoutManager.d.ts +5 -0
- package/dist/recyclerview/layout-managers/GridLayoutManager.d.ts.map +1 -1
- package/dist/recyclerview/layout-managers/GridLayoutManager.js +12 -0
- package/dist/recyclerview/layout-managers/GridLayoutManager.js.map +1 -1
- package/dist/recyclerview/layout-managers/LayoutManager.d.ts +11 -0
- package/dist/recyclerview/layout-managers/LayoutManager.d.ts.map +1 -1
- package/dist/recyclerview/layout-managers/LayoutManager.js +17 -0
- package/dist/recyclerview/layout-managers/LayoutManager.js.map +1 -1
- package/dist/recyclerview/layout-managers/MasonryLayoutManager.d.ts.map +1 -1
- package/dist/recyclerview/layout-managers/MasonryLayoutManager.js.map +1 -1
- package/dist/recyclerview/utils/componentUtils.d.ts +12 -3
- package/dist/recyclerview/utils/componentUtils.d.ts.map +1 -1
- package/dist/recyclerview/utils/componentUtils.js +16 -3
- package/dist/recyclerview/utils/componentUtils.js.map +1 -1
- package/dist/recyclerview/utils/measureLayout.d.ts +9 -5
- package/dist/recyclerview/utils/measureLayout.d.ts.map +1 -1
- package/dist/recyclerview/utils/measureLayout.js +6 -5
- package/dist/recyclerview/utils/measureLayout.js.map +1 -1
- package/dist/recyclerview/utils/measureLayout.web.d.ts +6 -2
- package/dist/recyclerview/utils/measureLayout.web.d.ts.map +1 -1
- package/dist/recyclerview/utils/measureLayout.web.js +1 -3
- package/dist/recyclerview/utils/measureLayout.web.js.map +1 -1
- package/dist/recyclerview/viewability/ViewabilityManager.d.ts +1 -1
- package/dist/recyclerview/viewability/ViewabilityManager.d.ts.map +1 -1
- package/dist/recyclerview/viewability/ViewabilityManager.js +1 -2
- package/dist/recyclerview/viewability/ViewabilityManager.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/utils/AverageWindow.d.ts.map +1 -1
- package/dist/utils/AverageWindow.js +2 -3
- package/dist/utils/AverageWindow.js.map +1 -1
- package/package.json +1 -1
- package/src/FlashListProps.ts +40 -3
- package/src/recyclerview/RecyclerView.tsx +15 -11
- package/src/recyclerview/RecyclerViewManager.ts +8 -1
- package/src/recyclerview/ViewHolderCollection.tsx +10 -3
- package/src/recyclerview/hooks/useBoundDetection.ts +1 -1
- package/src/recyclerview/hooks/useRecyclerViewController.tsx +2 -3
- package/src/recyclerview/hooks/useSecondaryProps.tsx +12 -6
- package/src/recyclerview/layout-managers/GridLayoutManager.ts +13 -0
- package/src/recyclerview/layout-managers/LayoutManager.ts +18 -0
- package/src/recyclerview/layout-managers/MasonryLayoutManager.ts +4 -0
- package/src/recyclerview/utils/componentUtils.ts +27 -5
- package/src/recyclerview/utils/measureLayout.ts +12 -6
- package/src/recyclerview/utils/measureLayout.web.ts +7 -4
- package/src/recyclerview/viewability/ViewabilityManager.ts +1 -3
- package/src/utils/AverageWindow.ts +4 -2
|
@@ -54,6 +54,8 @@ export interface ViewHolderCollectionProps<TItem> {
|
|
|
54
54
|
currentStickyIndex: number;
|
|
55
55
|
/** Whether the cell associated with an active sticky header is hidden */
|
|
56
56
|
hideStickyHeaderRelatedCell: boolean;
|
|
57
|
+
/** Returns whether the item at the given index is in the last row of the layout */
|
|
58
|
+
isInLastRow: (index: number) => boolean;
|
|
57
59
|
}
|
|
58
60
|
|
|
59
61
|
/**
|
|
@@ -90,6 +92,7 @@ export const ViewHolderCollection = <TItem,>(
|
|
|
90
92
|
getAdjustmentMargin,
|
|
91
93
|
currentStickyIndex,
|
|
92
94
|
hideStickyHeaderRelatedCell,
|
|
95
|
+
isInLastRow,
|
|
93
96
|
} = props;
|
|
94
97
|
|
|
95
98
|
const [renderId, setRenderId] = React.useState(0);
|
|
@@ -171,9 +174,13 @@ export const ViewHolderCollection = <TItem,>(
|
|
|
171
174
|
hasData &&
|
|
172
175
|
Array.from(renderStack.entries(), ([reactKey, { index }]) => {
|
|
173
176
|
const item = data[index];
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
+
// Suppress separators for items in the last row to prevent
|
|
178
|
+
// height mismatch. The last data item has no separator (no
|
|
179
|
+
// trailingItem), so all items sharing its row must match.
|
|
180
|
+
const trailingItem =
|
|
181
|
+
ItemSeparatorComponent && !isInLastRow(index)
|
|
182
|
+
? data[index + 1]
|
|
183
|
+
: undefined;
|
|
177
184
|
|
|
178
185
|
return (
|
|
179
186
|
<ViewHolder
|
|
@@ -140,7 +140,7 @@ export function useBoundDetection<T>(
|
|
|
140
140
|
recyclerViewManager.props.maintainVisibleContentPosition
|
|
141
141
|
?.animateAutoScrollToBottom ?? true;
|
|
142
142
|
scrollViewRef.current?.scrollToEnd({
|
|
143
|
-
animated: shouldAnimate,
|
|
143
|
+
animated: shouldAnimate && !recyclerViewManager.ignoreScrollEvents,
|
|
144
144
|
});
|
|
145
145
|
});
|
|
146
146
|
}
|
|
@@ -51,7 +51,6 @@ export function useRecyclerViewController<T>(
|
|
|
51
51
|
const isUnmounted = useUnmountFlag();
|
|
52
52
|
const [_, setRenderId] = useState(0);
|
|
53
53
|
const pauseOffsetCorrection = useRef(false);
|
|
54
|
-
const initialScrollCompletedRef = useRef(false);
|
|
55
54
|
const lastDataLengthRef = useRef(recyclerViewManager.getDataLength());
|
|
56
55
|
const { setTimeout } = useUnmountAwareTimeout();
|
|
57
56
|
|
|
@@ -573,12 +572,12 @@ export function useRecyclerViewController<T>(
|
|
|
573
572
|
if (
|
|
574
573
|
initialScrollIndex >= 0 &&
|
|
575
574
|
initialScrollIndex < dataLength &&
|
|
576
|
-
!
|
|
575
|
+
!recyclerViewManager.isInitialScrollComplete &&
|
|
577
576
|
recyclerViewManager.getIsFirstLayoutComplete()
|
|
578
577
|
) {
|
|
579
578
|
// Use setTimeout to ensure that we keep trying to scroll on first few renders
|
|
580
579
|
setTimeout(() => {
|
|
581
|
-
|
|
580
|
+
recyclerViewManager.isInitialScrollComplete = true;
|
|
582
581
|
pauseOffsetCorrection.current = false;
|
|
583
582
|
}, 100);
|
|
584
583
|
|
|
@@ -2,7 +2,7 @@ import { Animated, RefreshControl } from "react-native";
|
|
|
2
2
|
import React, { useMemo } from "react";
|
|
3
3
|
|
|
4
4
|
import { RecyclerViewProps } from "../RecyclerViewProps";
|
|
5
|
-
import { getValidComponent } from "../utils/componentUtils";
|
|
5
|
+
import { getValidComponent, isComponentClass } from "../utils/componentUtils";
|
|
6
6
|
import { CompatView } from "../components/CompatView";
|
|
7
7
|
import { CompatAnimatedScroller } from "../components/CompatScroller";
|
|
8
8
|
|
|
@@ -121,16 +121,22 @@ export function useSecondaryProps<T>(props: RecyclerViewProps<T>) {
|
|
|
121
121
|
* If no custom component is provided, uses the default CompatAnimatedScroller.
|
|
122
122
|
*/
|
|
123
123
|
const CompatScrollView = useMemo(() => {
|
|
124
|
-
let scrollComponent = CompatAnimatedScroller;
|
|
125
|
-
if (
|
|
124
|
+
let scrollComponent: React.ComponentType<any> = CompatAnimatedScroller;
|
|
125
|
+
if (
|
|
126
|
+
typeof renderScrollComponent === "function" &&
|
|
127
|
+
!isComponentClass(renderScrollComponent)
|
|
128
|
+
) {
|
|
126
129
|
// Create a forwarded ref wrapper for the custom scroll component
|
|
127
130
|
const ForwardedScrollComponent = React.forwardRef((_props, ref) =>
|
|
128
|
-
(renderScrollComponent as
|
|
131
|
+
(renderScrollComponent as (...args: unknown[]) => React.ReactNode)({
|
|
132
|
+
..._props,
|
|
133
|
+
ref,
|
|
134
|
+
})
|
|
129
135
|
);
|
|
130
136
|
ForwardedScrollComponent.displayName = "CustomScrollView";
|
|
131
|
-
scrollComponent = ForwardedScrollComponent as any
|
|
137
|
+
scrollComponent = ForwardedScrollComponent as React.ComponentType<any>;
|
|
132
138
|
} else if (renderScrollComponent) {
|
|
133
|
-
scrollComponent = renderScrollComponent
|
|
139
|
+
scrollComponent = renderScrollComponent as React.ComponentType<any>;
|
|
134
140
|
}
|
|
135
141
|
// Wrap the scroll component with Animated.createAnimatedComponent
|
|
136
142
|
return Animated.createAnimatedComponent(scrollComponent);
|
|
@@ -253,4 +253,17 @@ export class RVGridLayoutManagerImpl extends RVLayoutManager {
|
|
|
253
253
|
}
|
|
254
254
|
return Math.max(i, 0);
|
|
255
255
|
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Returns whether the item is in the same row as the last item.
|
|
259
|
+
* Items in the same row share the same y-coordinate, regardless of column spans.
|
|
260
|
+
*/
|
|
261
|
+
isInLastRow(index: number): boolean {
|
|
262
|
+
if (this.layouts.length === 0) return false;
|
|
263
|
+
const lastIndex = this.layouts.length - 1;
|
|
264
|
+
return (
|
|
265
|
+
index === lastIndex ||
|
|
266
|
+
this.layouts[index]?.y === this.layouts[lastIndex]?.y
|
|
267
|
+
);
|
|
268
|
+
}
|
|
256
269
|
}
|
|
@@ -182,6 +182,10 @@ export abstract class RVLayoutManager {
|
|
|
182
182
|
this.layouts.length = totalItemCount;
|
|
183
183
|
this.spanTracker.length = totalItemCount;
|
|
184
184
|
minRecomputeIndex = totalItemCount - 1; // <0 gets skipped so it's safe to set to totalItemCount - 1
|
|
185
|
+
// layoutInfo may contain stale indices from ViewHolders that were rendered
|
|
186
|
+
// before the data shrunk. Filter out any indices that are now out of bounds.
|
|
187
|
+
// eslint-disable-next-line no-param-reassign
|
|
188
|
+
layoutInfo = layoutInfo.filter((info) => info.index < totalItemCount);
|
|
185
189
|
}
|
|
186
190
|
// update average windows
|
|
187
191
|
minRecomputeIndex = Math.min(
|
|
@@ -260,6 +264,20 @@ export abstract class RVLayoutManager {
|
|
|
260
264
|
return this.layouts.length;
|
|
261
265
|
}
|
|
262
266
|
|
|
267
|
+
/**
|
|
268
|
+
* Returns whether the item at the given index is in the last row of the layout.
|
|
269
|
+
* Used to suppress separators for all items in the last row, preventing
|
|
270
|
+
* height mismatch when the last data item has no separator.
|
|
271
|
+
*
|
|
272
|
+
* Base implementation returns false — only layouts that normalize heights
|
|
273
|
+
* across multiple items (e.g., grid) need to override this.
|
|
274
|
+
* @param index Index of the item
|
|
275
|
+
* @returns True if the item is in the last row
|
|
276
|
+
*/
|
|
277
|
+
isInLastRow(index: number): boolean {
|
|
278
|
+
return false;
|
|
279
|
+
}
|
|
280
|
+
|
|
263
281
|
/**
|
|
264
282
|
* Abstract method to recompute layouts for items in the given range.
|
|
265
283
|
* @param startIndex Starting index of items to recompute
|
|
@@ -320,4 +320,8 @@ export class RVMasonryLayoutManagerImpl extends RVLayoutManager {
|
|
|
320
320
|
}
|
|
321
321
|
}
|
|
322
322
|
}
|
|
323
|
+
|
|
324
|
+
// TODO: For masonry, the "last row" is the last item in each column.
|
|
325
|
+
// Override isInLastRow if ItemSeparatorComponent support is needed
|
|
326
|
+
// for masonry layouts.
|
|
323
327
|
}
|
|
@@ -1,11 +1,31 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
|
|
3
|
+
type RenderableComponent =
|
|
4
|
+
| React.ComponentType
|
|
5
|
+
| React.ExoticComponent
|
|
6
|
+
| React.ReactElement
|
|
7
|
+
| null
|
|
8
|
+
| undefined;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Returns true if the value is a React class component.
|
|
12
|
+
* Class components set `prototype.isReactComponent` per React convention,
|
|
13
|
+
* which distinguishes them from plain functions and render props.
|
|
14
|
+
*/
|
|
15
|
+
export const isComponentClass = (value: unknown): boolean =>
|
|
16
|
+
typeof value === "function" &&
|
|
17
|
+
Boolean(
|
|
18
|
+
(value as { prototype?: { isReactComponent?: unknown } }).prototype
|
|
19
|
+
?.isReactComponent
|
|
20
|
+
);
|
|
21
|
+
|
|
3
22
|
/**
|
|
4
23
|
* Helper function to handle both React components and React elements.
|
|
5
24
|
* This utility ensures proper rendering of components whether they are passed as
|
|
6
|
-
* component types or pre-rendered elements.
|
|
25
|
+
* component types or pre-rendered elements. Supports function components, class
|
|
26
|
+
* components, React.memo, React.forwardRef, and pre-rendered elements.
|
|
7
27
|
*
|
|
8
|
-
* @param component - Can be a React component type, React element, null, or undefined
|
|
28
|
+
* @param component - Can be a React component type, exotic component, React element, null, or undefined
|
|
9
29
|
* @returns A valid React element if the input is valid, null otherwise
|
|
10
30
|
*
|
|
11
31
|
* @example
|
|
@@ -17,12 +37,14 @@ import React from "react";
|
|
|
17
37
|
* getValidComponent(<MyComponent />)
|
|
18
38
|
*/
|
|
19
39
|
export const getValidComponent = (
|
|
20
|
-
component:
|
|
40
|
+
component: RenderableComponent
|
|
21
41
|
): React.ReactElement | null => {
|
|
22
42
|
if (React.isValidElement(component)) {
|
|
23
43
|
return component;
|
|
24
|
-
} else if (
|
|
25
|
-
|
|
44
|
+
} else if (component != null) {
|
|
45
|
+
// Cast needed: React.createElement's type overloads don't include ExoticComponent,
|
|
46
|
+
// but it handles memo/forwardRef/lazy correctly at runtime.
|
|
47
|
+
return React.createElement(component as React.ComponentType);
|
|
26
48
|
}
|
|
27
49
|
return null;
|
|
28
50
|
};
|
|
@@ -7,6 +7,11 @@ interface Layout {
|
|
|
7
7
|
height: number;
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
+
interface Size {
|
|
11
|
+
width: number;
|
|
12
|
+
height: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
10
15
|
/**
|
|
11
16
|
* Measures the layout of a view relative to itselft.
|
|
12
17
|
* Using measure wasn't returing accurate values but this workaround does.
|
|
@@ -89,14 +94,15 @@ export function roundOffPixel(value: number): number {
|
|
|
89
94
|
}
|
|
90
95
|
|
|
91
96
|
/**
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
*
|
|
97
|
+
* Measures the size of the RecyclerView's outer container.
|
|
98
|
+
* Uses a self-relative measureLayout call to get width/height synchronously.
|
|
99
|
+
*
|
|
95
100
|
* @param view - The React Native View component to measure
|
|
96
|
-
* @returns An object containing
|
|
101
|
+
* @returns An object containing width and height
|
|
97
102
|
*/
|
|
98
|
-
export function measureParentSize(view: View):
|
|
99
|
-
|
|
103
|
+
export function measureParentSize(view: View): Size {
|
|
104
|
+
const layout = measureLayout(view, undefined);
|
|
105
|
+
return { width: layout.width, height: layout.height };
|
|
100
106
|
}
|
|
101
107
|
|
|
102
108
|
/**
|
|
@@ -5,6 +5,11 @@ interface Layout {
|
|
|
5
5
|
height: number;
|
|
6
6
|
}
|
|
7
7
|
|
|
8
|
+
interface Size {
|
|
9
|
+
width: number;
|
|
10
|
+
height: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
8
13
|
/**
|
|
9
14
|
* Gets scroll offsets from up to 3 parent elements
|
|
10
15
|
*/
|
|
@@ -43,12 +48,10 @@ export function roundOffPixel(value: number): number {
|
|
|
43
48
|
}
|
|
44
49
|
|
|
45
50
|
/**
|
|
46
|
-
* Measures the
|
|
51
|
+
* Measures the size of the RecyclerView's outer container.
|
|
47
52
|
*/
|
|
48
|
-
export function measureParentSize(view: Element):
|
|
53
|
+
export function measureParentSize(view: Element): Size {
|
|
49
54
|
return {
|
|
50
|
-
x: 0,
|
|
51
|
-
y: 0,
|
|
52
55
|
width: view.clientWidth,
|
|
53
56
|
height: view.clientHeight,
|
|
54
57
|
};
|
|
@@ -89,12 +89,10 @@ export default class ViewabilityManager<T> {
|
|
|
89
89
|
});
|
|
90
90
|
};
|
|
91
91
|
|
|
92
|
-
public
|
|
92
|
+
public clearLastReportedViewableIndices = () => {
|
|
93
93
|
this.viewabilityHelpers.forEach((viewabilityHelper) =>
|
|
94
94
|
viewabilityHelper.clearLastReportedViewableIndices()
|
|
95
95
|
);
|
|
96
|
-
|
|
97
|
-
this.updateViewableItems();
|
|
98
96
|
};
|
|
99
97
|
|
|
100
98
|
/**
|
|
@@ -33,9 +33,11 @@ export class AverageWindow {
|
|
|
33
33
|
|
|
34
34
|
this.inputValues[target] = value;
|
|
35
35
|
|
|
36
|
-
this.currentAverage =
|
|
36
|
+
this.currentAverage = Math.max(
|
|
37
|
+
0,
|
|
37
38
|
this.currentAverage * (this.currentCount / newCount) +
|
|
38
|
-
|
|
39
|
+
(value - (oldValue ?? 0)) / newCount
|
|
40
|
+
);
|
|
39
41
|
|
|
40
42
|
this.currentCount = newCount;
|
|
41
43
|
}
|