@shopify/flash-list 2.2.2 → 2.3.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.
- package/dist/FlashListProps.d.ts +44 -8
- package/dist/FlashListProps.d.ts.map +1 -1
- package/dist/native/config/PlatformHelper.android.d.ts +10 -0
- package/dist/native/config/PlatformHelper.android.d.ts.map +1 -1
- package/dist/native/config/PlatformHelper.android.js +7 -0
- package/dist/native/config/PlatformHelper.android.js.map +1 -1
- package/dist/native/config/PlatformHelper.d.ts +10 -0
- package/dist/native/config/PlatformHelper.d.ts.map +1 -1
- package/dist/native/config/PlatformHelper.ios.d.ts +10 -0
- package/dist/native/config/PlatformHelper.ios.d.ts.map +1 -1
- package/dist/native/config/PlatformHelper.ios.js +2 -0
- package/dist/native/config/PlatformHelper.ios.js.map +1 -1
- package/dist/native/config/PlatformHelper.js +2 -0
- package/dist/native/config/PlatformHelper.js.map +1 -1
- package/dist/native/config/PlatformHelper.web.d.ts +10 -0
- package/dist/native/config/PlatformHelper.web.d.ts.map +1 -1
- package/dist/native/config/PlatformHelper.web.js +2 -0
- package/dist/native/config/PlatformHelper.web.js.map +1 -1
- package/dist/recyclerview/RecyclerView.d.ts.map +1 -1
- package/dist/recyclerview/RecyclerView.js +25 -15
- 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 +20 -15
- package/dist/recyclerview/RecyclerViewManager.js.map +1 -1
- package/dist/recyclerview/ViewHolder.d.ts +2 -0
- package/dist/recyclerview/ViewHolder.d.ts.map +1 -1
- package/dist/recyclerview/ViewHolder.js +8 -2
- package/dist/recyclerview/ViewHolder.js.map +1 -1
- package/dist/recyclerview/ViewHolderCollection.d.ts +4 -0
- package/dist/recyclerview/ViewHolderCollection.d.ts.map +1 -1
- package/dist/recyclerview/ViewHolderCollection.js +6 -3
- package/dist/recyclerview/ViewHolderCollection.js.map +1 -1
- package/dist/recyclerview/components/StickyHeaders.d.ts +3 -1
- package/dist/recyclerview/components/StickyHeaders.d.ts.map +1 -1
- package/dist/recyclerview/components/StickyHeaders.js +3 -2
- package/dist/recyclerview/components/StickyHeaders.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 +2 -2
- package/dist/recyclerview/hooks/useSecondaryProps.d.ts.map +1 -1
- package/dist/recyclerview/hooks/useSecondaryProps.js +36 -16
- 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 +13 -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/getInvertedTransformStyle.d.ts +10 -0
- package/dist/recyclerview/utils/getInvertedTransformStyle.d.ts.map +1 -0
- package/dist/recyclerview/utils/getInvertedTransformStyle.js +7 -0
- package/dist/recyclerview/utils/getInvertedTransformStyle.js.map +1 -0
- 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/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 +53 -3
- package/src/native/config/PlatformHelper.android.ts +7 -0
- package/src/native/config/PlatformHelper.ios.ts +2 -0
- package/src/native/config/PlatformHelper.ts +2 -0
- package/src/native/config/PlatformHelper.web.ts +2 -0
- package/src/recyclerview/RecyclerView.tsx +26 -11
- package/src/recyclerview/RecyclerViewManager.ts +21 -16
- package/src/recyclerview/ViewHolder.tsx +11 -1
- package/src/recyclerview/ViewHolderCollection.tsx +14 -3
- package/src/recyclerview/components/StickyHeaders.tsx +5 -0
- package/src/recyclerview/hooks/useBoundDetection.ts +1 -1
- package/src/recyclerview/hooks/useRecyclerViewController.tsx +2 -3
- package/src/recyclerview/hooks/useSecondaryProps.tsx +48 -18
- package/src/recyclerview/layout-managers/GridLayoutManager.ts +13 -0
- package/src/recyclerview/layout-managers/LayoutManager.ts +14 -0
- package/src/recyclerview/layout-managers/MasonryLayoutManager.ts +4 -0
- package/src/recyclerview/utils/componentUtils.ts +27 -5
- package/src/recyclerview/utils/getInvertedTransformStyle.ts +7 -0
- package/src/recyclerview/utils/measureLayout.ts +12 -6
- package/src/recyclerview/utils/measureLayout.web.ts +7 -4
- package/src/utils/AverageWindow.ts +4 -2
|
@@ -48,6 +48,7 @@ import { CompatScroller } from "./components/CompatScroller";
|
|
|
48
48
|
import { useBoundDetection } from "./hooks/useBoundDetection";
|
|
49
49
|
import { adjustOffsetForRTL } from "./utils/adjustOffsetForRTL";
|
|
50
50
|
import { useSecondaryProps } from "./hooks/useSecondaryProps";
|
|
51
|
+
import { getInvertedTransformStyle } from "./utils/getInvertedTransformStyle";
|
|
51
52
|
import { StickyHeaders, StickyHeaderRef } from "./components/StickyHeaders";
|
|
52
53
|
import { ScrollAnchor, ScrollAnchorRef } from "./components/ScrollAnchor";
|
|
53
54
|
import { useRecyclerViewController } from "./hooks/useRecyclerViewController";
|
|
@@ -86,6 +87,7 @@ const RecyclerViewComponent = <T,>(
|
|
|
86
87
|
onCommitLayoutEffect,
|
|
87
88
|
onChangeStickyIndex,
|
|
88
89
|
stickyHeaderConfig,
|
|
90
|
+
inverted,
|
|
89
91
|
...rest
|
|
90
92
|
} = props;
|
|
91
93
|
|
|
@@ -100,6 +102,11 @@ const RecyclerViewComponent = <T,>(
|
|
|
100
102
|
const stickyHeaderHideRelatedCell =
|
|
101
103
|
stickyHeaderConfig?.hideRelatedCell ?? false;
|
|
102
104
|
|
|
105
|
+
// Compute the inverted transform style based on platform and orientation
|
|
106
|
+
const invertedTransformStyle = inverted
|
|
107
|
+
? getInvertedTransformStyle(horizontal)
|
|
108
|
+
: undefined;
|
|
109
|
+
|
|
103
110
|
// Core refs for managing scroll view, internal view, and child container
|
|
104
111
|
const scrollViewRef = useRef<CompatScroller>(null);
|
|
105
112
|
const internalViewRef = useRef<CompatView>(null);
|
|
@@ -157,29 +164,28 @@ const RecyclerViewComponent = <T,>(
|
|
|
157
164
|
*/
|
|
158
165
|
useLayoutEffect(() => {
|
|
159
166
|
if (internalViewRef.current && firstChildViewRef.current) {
|
|
160
|
-
// Measure the outer and inner container
|
|
161
|
-
const
|
|
167
|
+
// Measure the outer container size and inner container layout
|
|
168
|
+
const outerViewSize = measureParentSize(internalViewRef.current);
|
|
162
169
|
const firstChildViewLayout = measureFirstChildLayout(
|
|
163
170
|
firstChildViewRef.current,
|
|
164
171
|
internalViewRef.current
|
|
165
172
|
);
|
|
166
173
|
|
|
167
|
-
containerViewSizeRef.current =
|
|
174
|
+
containerViewSizeRef.current = outerViewSize;
|
|
168
175
|
|
|
169
|
-
//
|
|
176
|
+
// firstChildViewLayout is already relative to the outer container,
|
|
177
|
+
// so its x/y directly gives the first item offset.
|
|
170
178
|
const firstItemOffset = horizontal
|
|
171
|
-
? firstChildViewLayout.x
|
|
172
|
-
: firstChildViewLayout.y
|
|
179
|
+
? firstChildViewLayout.x
|
|
180
|
+
: firstChildViewLayout.y;
|
|
173
181
|
|
|
174
182
|
// Update the RecyclerView manager with window dimensions
|
|
175
183
|
recyclerViewManager.updateLayoutParams(
|
|
176
184
|
{
|
|
177
|
-
width: horizontal
|
|
178
|
-
? outerViewLayout.width
|
|
179
|
-
: firstChildViewLayout.width,
|
|
185
|
+
width: horizontal ? outerViewSize.width : firstChildViewLayout.width,
|
|
180
186
|
height: horizontal
|
|
181
187
|
? firstChildViewLayout.height
|
|
182
|
-
:
|
|
188
|
+
: outerViewSize.height,
|
|
183
189
|
},
|
|
184
190
|
isHorizontalRTL && recyclerViewManager.hasLayout()
|
|
185
191
|
? firstItemOffset -
|
|
@@ -300,7 +306,11 @@ const RecyclerViewComponent = <T,>(
|
|
|
300
306
|
checkBounds();
|
|
301
307
|
|
|
302
308
|
// Record interaction and compute item visibility
|
|
303
|
-
|
|
309
|
+
// Skip recording interaction during programmatic initial scroll
|
|
310
|
+
// to respect waitForInteraction in viewability config
|
|
311
|
+
if (recyclerViewManager.isInitialScrollComplete) {
|
|
312
|
+
recyclerViewManager.recordInteraction();
|
|
313
|
+
}
|
|
304
314
|
recyclerViewManager.computeItemViewability();
|
|
305
315
|
|
|
306
316
|
// Call user-provided onScroll handler
|
|
@@ -430,6 +440,7 @@ const RecyclerViewComponent = <T,>(
|
|
|
430
440
|
stickyHeaderRef={stickyHeaderRef}
|
|
431
441
|
recyclerViewManager={recyclerViewManager}
|
|
432
442
|
extraData={extraData}
|
|
443
|
+
inverted={inverted}
|
|
433
444
|
onChangeStickyIndex={(newStickyHeaderIndex) => {
|
|
434
445
|
if (stickyHeaderHideRelatedCell) {
|
|
435
446
|
setCurrentStickyIndex(newStickyHeaderIndex);
|
|
@@ -452,6 +463,7 @@ const RecyclerViewComponent = <T,>(
|
|
|
452
463
|
currentStickyIndex,
|
|
453
464
|
onChangeStickyIndex,
|
|
454
465
|
stickyHeaderHideRelatedCell,
|
|
466
|
+
inverted,
|
|
455
467
|
]);
|
|
456
468
|
|
|
457
469
|
// Set up scroll event handling with animation support for sticky headers
|
|
@@ -523,6 +535,7 @@ const RecyclerViewComponent = <T,>(
|
|
|
523
535
|
overflow: "hidden",
|
|
524
536
|
},
|
|
525
537
|
style,
|
|
538
|
+
invertedTransformStyle,
|
|
526
539
|
]}
|
|
527
540
|
ref={internalViewRef}
|
|
528
541
|
collapsable={false}
|
|
@@ -612,6 +625,7 @@ const RecyclerViewComponent = <T,>(
|
|
|
612
625
|
}}
|
|
613
626
|
CellRendererComponent={CellRendererComponent}
|
|
614
627
|
ItemSeparatorComponent={ItemSeparatorComponent}
|
|
628
|
+
isInLastRow={(index) => recyclerViewManager.isInLastRow(index)}
|
|
615
629
|
getChildContainerLayout={() =>
|
|
616
630
|
recyclerViewManager.hasLayout()
|
|
617
631
|
? recyclerViewManager.getChildContainerDimensions()
|
|
@@ -619,6 +633,7 @@ const RecyclerViewComponent = <T,>(
|
|
|
619
633
|
}
|
|
620
634
|
currentStickyIndex={currentStickyIndex}
|
|
621
635
|
hideStickyHeaderRelatedCell={stickyHeaderHideRelatedCell}
|
|
636
|
+
inverted={inverted}
|
|
622
637
|
/>
|
|
623
638
|
{renderEmpty}
|
|
624
639
|
{renderFooter}
|
|
@@ -39,6 +39,7 @@ export class RecyclerViewManager<T> {
|
|
|
39
39
|
public firstItemOffset = 0;
|
|
40
40
|
public ignoreScrollEvents = false;
|
|
41
41
|
public isFirstPaintOnUiComplete = false;
|
|
42
|
+
public isInitialScrollComplete = false;
|
|
42
43
|
|
|
43
44
|
public get animationOptimizationsEnabled() {
|
|
44
45
|
return this._animationOptimizationsEnabled;
|
|
@@ -71,6 +72,7 @@ export class RecyclerViewManager<T> {
|
|
|
71
72
|
props.maxItemsInRecyclePool
|
|
72
73
|
);
|
|
73
74
|
this.itemViewabilityManager = new ViewabilityManager<T>(this as any);
|
|
75
|
+
this.isInitialScrollComplete = this.getInitialScrollIndex() === undefined;
|
|
74
76
|
this.checkPropsAndWarn();
|
|
75
77
|
}
|
|
76
78
|
|
|
@@ -151,6 +153,10 @@ export class RecyclerViewManager<T> {
|
|
|
151
153
|
return undefined;
|
|
152
154
|
}
|
|
153
155
|
|
|
156
|
+
isInLastRow(index: number): boolean {
|
|
157
|
+
return this.layoutManager?.isInLastRow(index) ?? false;
|
|
158
|
+
}
|
|
159
|
+
|
|
154
160
|
// Doesn't include header / foot etc
|
|
155
161
|
getChildContainerDimensions() {
|
|
156
162
|
if (!this.layoutManager) {
|
|
@@ -376,27 +382,26 @@ export class RecyclerViewManager<T> {
|
|
|
376
382
|
}
|
|
377
383
|
|
|
378
384
|
const initialScrollIndex = this.getInitialScrollIndex();
|
|
379
|
-
const initialItemLayout = this.layoutManager?.getLayout(
|
|
380
|
-
initialScrollIndex ?? 0
|
|
381
|
-
);
|
|
382
|
-
const initialItemOffset = this.propsRef.horizontal
|
|
383
|
-
? initialItemLayout?.x
|
|
384
|
-
: initialItemLayout?.y;
|
|
385
385
|
|
|
386
386
|
if (initialScrollIndex !== undefined) {
|
|
387
|
-
//
|
|
388
|
-
//
|
|
389
|
-
//
|
|
390
|
-
//
|
|
391
|
-
// this.firstItemOffset
|
|
392
|
-
// );
|
|
387
|
+
// Recompute layouts first, then read the offset. The recompute may
|
|
388
|
+
// re-estimate unmeasured items with an updated average height, changing
|
|
389
|
+
// the target item's position. Reading before recompute would capture a
|
|
390
|
+
// stale offset, causing the wrong items to be rendered.
|
|
393
391
|
this.layoutManager.recomputeLayouts(0, initialScrollIndex);
|
|
394
|
-
|
|
395
|
-
|
|
392
|
+
const initialItemLayout =
|
|
393
|
+
this.layoutManager.getLayout(initialScrollIndex);
|
|
394
|
+
const initialItemOffset = this.propsRef.horizontal
|
|
395
|
+
? initialItemLayout.x
|
|
396
|
+
: initialItemLayout.y;
|
|
397
|
+
this.engagedIndicesTracker.scrollOffset = initialItemOffset;
|
|
396
398
|
} else {
|
|
397
|
-
|
|
399
|
+
const initialItemLayout = this.layoutManager.getLayout(0);
|
|
400
|
+
const initialItemOffset = this.propsRef.horizontal
|
|
401
|
+
? initialItemLayout.x
|
|
402
|
+
: initialItemLayout.y;
|
|
398
403
|
this.engagedIndicesTracker.scrollOffset =
|
|
399
|
-
|
|
404
|
+
initialItemOffset - this.firstItemOffset;
|
|
400
405
|
}
|
|
401
406
|
}
|
|
402
407
|
|
|
@@ -17,6 +17,7 @@ import { FlashListProps, RenderTarget } from "../FlashListProps";
|
|
|
17
17
|
|
|
18
18
|
import { RVDimension, RVLayout } from "./layout-managers/LayoutManager";
|
|
19
19
|
import { CompatView } from "./components/CompatView";
|
|
20
|
+
import { getInvertedTransformStyle } from "./utils/getInvertedTransformStyle";
|
|
20
21
|
|
|
21
22
|
/**
|
|
22
23
|
* Props interface for the ViewHolder component
|
|
@@ -49,6 +50,8 @@ export interface ViewHolderProps<TItem> {
|
|
|
49
50
|
onSizeChanged?: (index: number, size: RVDimension) => void;
|
|
50
51
|
/** Whether this item should be hidden (likely because it is associated with the active sticky header) */
|
|
51
52
|
hidden: boolean;
|
|
53
|
+
/** Whether the list is inverted */
|
|
54
|
+
inverted?: FlashListProps<TItem>["inverted"];
|
|
52
55
|
}
|
|
53
56
|
|
|
54
57
|
/**
|
|
@@ -72,6 +75,7 @@ const ViewHolderInternal = <TItem,>(props: ViewHolderProps<TItem>) => {
|
|
|
72
75
|
trailingItem,
|
|
73
76
|
horizontal,
|
|
74
77
|
hidden,
|
|
78
|
+
inverted,
|
|
75
79
|
} = props;
|
|
76
80
|
|
|
77
81
|
useLayoutEffect(() => {
|
|
@@ -105,6 +109,10 @@ const ViewHolderInternal = <TItem,>(props: ViewHolderProps<TItem>) => {
|
|
|
105
109
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
106
110
|
}, [item, extraData, target, renderItem]);
|
|
107
111
|
|
|
112
|
+
const invertedTransformStyle = inverted
|
|
113
|
+
? getInvertedTransformStyle(horizontal)
|
|
114
|
+
: undefined;
|
|
115
|
+
|
|
108
116
|
const style = {
|
|
109
117
|
flexDirection: horizontal ? "row" : "column",
|
|
110
118
|
position: target === "StickyHeader" ? "relative" : "absolute",
|
|
@@ -117,6 +125,7 @@ const ViewHolderInternal = <TItem,>(props: ViewHolderProps<TItem>) => {
|
|
|
117
125
|
left: layout.x,
|
|
118
126
|
top: layout.y,
|
|
119
127
|
opacity: hidden ? 0 : 1,
|
|
128
|
+
...invertedTransformStyle,
|
|
120
129
|
} as const;
|
|
121
130
|
|
|
122
131
|
// TODO: Fix this type issue
|
|
@@ -157,7 +166,8 @@ export const ViewHolder = React.memo(
|
|
|
157
166
|
prevProps.ItemSeparatorComponent === nextProps.ItemSeparatorComponent &&
|
|
158
167
|
prevProps.trailingItem === nextProps.trailingItem &&
|
|
159
168
|
prevProps.horizontal === nextProps.horizontal &&
|
|
160
|
-
prevProps.hidden === nextProps.hidden
|
|
169
|
+
prevProps.hidden === nextProps.hidden &&
|
|
170
|
+
prevProps.inverted === nextProps.inverted
|
|
161
171
|
);
|
|
162
172
|
}
|
|
163
173
|
);
|
|
@@ -54,6 +54,10 @@ 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;
|
|
59
|
+
/** Whether the list is inverted */
|
|
60
|
+
inverted: FlashListProps<TItem>["inverted"];
|
|
57
61
|
}
|
|
58
62
|
|
|
59
63
|
/**
|
|
@@ -90,6 +94,8 @@ export const ViewHolderCollection = <TItem,>(
|
|
|
90
94
|
getAdjustmentMargin,
|
|
91
95
|
currentStickyIndex,
|
|
92
96
|
hideStickyHeaderRelatedCell,
|
|
97
|
+
isInLastRow,
|
|
98
|
+
inverted,
|
|
93
99
|
} = props;
|
|
94
100
|
|
|
95
101
|
const [renderId, setRenderId] = React.useState(0);
|
|
@@ -171,9 +177,13 @@ export const ViewHolderCollection = <TItem,>(
|
|
|
171
177
|
hasData &&
|
|
172
178
|
Array.from(renderStack.entries(), ([reactKey, { index }]) => {
|
|
173
179
|
const item = data[index];
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
180
|
+
// Suppress separators for items in the last row to prevent
|
|
181
|
+
// height mismatch. The last data item has no separator (no
|
|
182
|
+
// trailingItem), so all items sharing its row must match.
|
|
183
|
+
const trailingItem =
|
|
184
|
+
ItemSeparatorComponent && !isInLastRow(index)
|
|
185
|
+
? data[index + 1]
|
|
186
|
+
: undefined;
|
|
177
187
|
|
|
178
188
|
return (
|
|
179
189
|
<ViewHolder
|
|
@@ -195,6 +205,7 @@ export const ViewHolderCollection = <TItem,>(
|
|
|
195
205
|
hidden={
|
|
196
206
|
hideStickyHeaderRelatedCell && currentStickyIndex === index
|
|
197
207
|
}
|
|
208
|
+
inverted={inverted}
|
|
198
209
|
/>
|
|
199
210
|
);
|
|
200
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) {
|
|
@@ -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,9 +2,10 @@ 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
|
+
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
|
-
|
|
97
|
-
|
|
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,32 +126,41 @@ export function useSecondaryProps<T>(props: RecyclerViewProps<T>) {
|
|
|
105
126
|
}
|
|
106
127
|
return (
|
|
107
128
|
<CompatView
|
|
108
|
-
style={
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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.
|
|
121
145
|
* If no custom component is provided, uses the default CompatAnimatedScroller.
|
|
122
146
|
*/
|
|
123
147
|
const CompatScrollView = useMemo(() => {
|
|
124
|
-
let scrollComponent = CompatAnimatedScroller;
|
|
125
|
-
if (
|
|
148
|
+
let scrollComponent: React.ComponentType<any> = CompatAnimatedScroller;
|
|
149
|
+
if (
|
|
150
|
+
typeof renderScrollComponent === "function" &&
|
|
151
|
+
!isComponentClass(renderScrollComponent)
|
|
152
|
+
) {
|
|
126
153
|
// Create a forwarded ref wrapper for the custom scroll component
|
|
127
154
|
const ForwardedScrollComponent = React.forwardRef((_props, ref) =>
|
|
128
|
-
(renderScrollComponent as
|
|
155
|
+
(renderScrollComponent as (...args: unknown[]) => React.ReactNode)({
|
|
156
|
+
..._props,
|
|
157
|
+
ref,
|
|
158
|
+
})
|
|
129
159
|
);
|
|
130
160
|
ForwardedScrollComponent.displayName = "CustomScrollView";
|
|
131
|
-
scrollComponent = ForwardedScrollComponent as any
|
|
161
|
+
scrollComponent = ForwardedScrollComponent as React.ComponentType<any>;
|
|
132
162
|
} else if (renderScrollComponent) {
|
|
133
|
-
scrollComponent = renderScrollComponent
|
|
163
|
+
scrollComponent = renderScrollComponent as React.ComponentType<any>;
|
|
134
164
|
}
|
|
135
165
|
// Wrap the scroll component with Animated.createAnimatedComponent
|
|
136
166
|
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
|
}
|
|
@@ -264,6 +264,20 @@ export abstract class RVLayoutManager {
|
|
|
264
264
|
return this.layouts.length;
|
|
265
265
|
}
|
|
266
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
|
+
|
|
267
281
|
/**
|
|
268
282
|
* Abstract method to recompute layouts for items in the given range.
|
|
269
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
|
};
|