Skip to content

Commit 6e68ac6

Browse files
committed
fix: sticky header calculation
1 parent b565489 commit 6e68ac6

File tree

8 files changed

+155
-14
lines changed

8 files changed

+155
-14
lines changed

packages/react-native/Libraries/Components/ScrollView/ScrollView.d.ts

+16
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {Constructor} from '../../../types/private/Utilities';
1212
import {Insets} from '../../../types/public/Insets';
1313
import {ColorValue, StyleProp} from '../../StyleSheet/StyleSheet';
1414
import {ViewStyle} from '../../StyleSheet/StyleSheetTypes';
15+
import {LayoutChangeEvent} from '../../Types/CoreEventTypes';
1516
import {
1617
NativeSyntheticEvent,
1718
NativeTouchEvent,
@@ -29,6 +30,14 @@ export interface PointProp {
2930
export interface ScrollResponderEvent
3031
extends NativeSyntheticEvent<NativeTouchEvent> {}
3132

33+
export type StickyHeaderOnLayoutEventContext<
34+
T extends Record<string, unknown>,
35+
> = {
36+
index: number;
37+
key: string;
38+
itemProps: T;
39+
};
40+
3241
interface SubscribableMixin {
3342
/**
3443
* Special form of calling `addListener` that *guarantees* that a
@@ -661,6 +670,13 @@ export interface ScrollViewProps
661670
*/
662671
onContentSizeChange?: ((w: number, h: number) => void) | undefined;
663672

673+
onStickyHeaderLayout?:
674+
| ((
675+
event: LayoutChangeEvent,
676+
context: StickyHeaderOnLayoutEventContext<Record<string, unknown>>,
677+
) => void)
678+
| undefined;
679+
664680
/**
665681
* Fires at most once per frame during scrolling.
666682
*/

packages/react-native/Libraries/Components/ScrollView/ScrollView.js

+18
Original file line numberDiff line numberDiff line change
@@ -371,6 +371,12 @@ type StickyHeaderComponentType = component(
371371
...ScrollViewStickyHeaderProps
372372
);
373373

374+
export type StickyHeaderOnLayoutEventContext<T: {...} = {...}> = $ReadOnly<{
375+
index: number,
376+
key: React.Key,
377+
itemProps: T,
378+
}>;
379+
374380
export type ScrollViewProps = $ReadOnly<{
375381
...ViewProps,
376382
...ScrollViewPropsIOS,
@@ -530,6 +536,10 @@ export type ScrollViewProps = $ReadOnly<{
530536
* which this ScrollView renders.
531537
*/
532538
onContentSizeChange?: (contentWidth: number, contentHeight: number) => void,
539+
onStickyHeaderLayout?: (
540+
event: LayoutChangeEvent,
541+
context: StickyHeaderOnLayoutEventContext<{...}>,
542+
) => void,
533543
onKeyboardDidShow?: (event: KeyboardEvent) => void,
534544
onKeyboardDidHide?: (event: KeyboardEvent) => void,
535545
onKeyboardWillShow?: (event: KeyboardEvent) => void,
@@ -1120,11 +1130,19 @@ class ScrollView extends React.Component<ScrollViewProps, State> {
11201130
return;
11211131
}
11221132

1133+
this.props.onStickyHeaderLayout &&
1134+
this.props.onStickyHeaderLayout(event, {
1135+
key,
1136+
index,
1137+
itemProps: childArray[index].props,
1138+
});
1139+
11231140
const layoutY = event.nativeEvent.layout.y;
11241141
this._headerLayoutYs.set(key, layoutY);
11251142

11261143
const indexOfIndex = stickyHeaderIndices.indexOf(index);
11271144
const previousHeaderIndex = stickyHeaderIndices[indexOfIndex - 1];
1145+
11281146
if (previousHeaderIndex != null) {
11291147
const previousHeader = this._stickyHeaderRefs.get(
11301148
this._getKeyForIndex(previousHeaderIndex, childArray),

packages/react-native/Libraries/Lists/__tests__/__snapshots__/FlatList-test.js.snap

+9
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ exports[`FlatList ignores invalid data 1`] = `
2121
onScrollShouldSetResponder={[Function]}
2222
onStartShouldSetResponder={[Function]}
2323
onStartShouldSetResponderCapture={[Function]}
24+
onStickyHeaderLayout={[Function]}
2425
onTouchCancel={[Function]}
2526
onTouchEnd={[Function]}
2627
onTouchMove={[Function]}
@@ -96,6 +97,7 @@ exports[`FlatList renders all the bells and whistles 1`] = `
9697
onScroll={[Function]}
9798
onScrollBeginDrag={[Function]}
9899
onScrollEndDrag={[Function]}
100+
onStickyHeaderLayout={[Function]}
99101
refreshControl={
100102
<RefreshControlMock
101103
onRefresh={[MockFunction]}
@@ -216,6 +218,7 @@ exports[`FlatList renders array-like data 1`] = `
216218
onScrollShouldSetResponder={[Function]}
217219
onStartShouldSetResponder={[Function]}
218220
onStartShouldSetResponderCapture={[Function]}
221+
onStickyHeaderLayout={[Function]}
219222
onTouchCancel={[Function]}
220223
onTouchEnd={[Function]}
221224
onTouchMove={[Function]}
@@ -295,6 +298,7 @@ exports[`FlatList renders empty list 1`] = `
295298
onScroll={[Function]}
296299
onScrollBeginDrag={[Function]}
297300
onScrollEndDrag={[Function]}
301+
onStickyHeaderLayout={[Function]}
298302
removeClippedSubviews={false}
299303
renderItem={[Function]}
300304
scrollEventThrottle={0.0001}
@@ -317,6 +321,7 @@ exports[`FlatList renders null list 1`] = `
317321
onScroll={[Function]}
318322
onScrollBeginDrag={[Function]}
319323
onScrollEndDrag={[Function]}
324+
onStickyHeaderLayout={[Function]}
320325
removeClippedSubviews={false}
321326
renderItem={[Function]}
322327
scrollEventThrottle={0.0001}
@@ -352,6 +357,7 @@ exports[`FlatList renders simple list (multiple columns) 1`] = `
352357
onScroll={[Function]}
353358
onScrollBeginDrag={[Function]}
354359
onScrollEndDrag={[Function]}
360+
onStickyHeaderLayout={[Function]}
355361
removeClippedSubviews={false}
356362
renderItem={[Function]}
357363
scrollEventThrottle={0.0001}
@@ -425,6 +431,7 @@ exports[`FlatList renders simple list 1`] = `
425431
onScroll={[Function]}
426432
onScrollBeginDrag={[Function]}
427433
onScrollEndDrag={[Function]}
434+
onStickyHeaderLayout={[Function]}
428435
removeClippedSubviews={false}
429436
renderItem={[Function]}
430437
scrollEventThrottle={0.0001}
@@ -489,6 +496,7 @@ exports[`FlatList renders simple list using ListItemComponent (multiple columns)
489496
onScroll={[Function]}
490497
onScrollBeginDrag={[Function]}
491498
onScrollEndDrag={[Function]}
499+
onStickyHeaderLayout={[Function]}
492500
removeClippedSubviews={false}
493501
scrollEventThrottle={0.0001}
494502
stickyHeaderIndices={Array []}
@@ -562,6 +570,7 @@ exports[`FlatList renders simple list using ListItemComponent 1`] = `
562570
onScroll={[Function]}
563571
onScrollBeginDrag={[Function]}
564572
onScrollEndDrag={[Function]}
573+
onStickyHeaderLayout={[Function]}
565574
removeClippedSubviews={false}
566575
scrollEventThrottle={0.0001}
567576
stickyHeaderIndices={Array []}

packages/react-native/Libraries/Lists/__tests__/__snapshots__/SectionList-test.js.snap

+5-6
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ exports[`SectionList rendering empty section headers is fine 1`] = `
2727
onScroll={[Function]}
2828
onScrollBeginDrag={[Function]}
2929
onScrollEndDrag={[Function]}
30+
onStickyHeaderLayout={[Function]}
3031
renderItem={[Function]}
3132
scrollEventThrottle={0.0001}
3233
stickyHeaderIndices={
@@ -38,7 +39,6 @@ exports[`SectionList rendering empty section headers is fine 1`] = `
3839
<View>
3940
<View
4041
onFocusCapture={[Function]}
41-
onLayout={[Function]}
4242
style={null}
4343
/>
4444
<View
@@ -88,6 +88,7 @@ exports[`SectionList renders a footer when there is no data 1`] = `
8888
onScroll={[Function]}
8989
onScrollBeginDrag={[Function]}
9090
onScrollEndDrag={[Function]}
91+
onStickyHeaderLayout={[Function]}
9192
renderItem={[Function]}
9293
scrollEventThrottle={0.0001}
9394
stickyHeaderIndices={
@@ -99,7 +100,6 @@ exports[`SectionList renders a footer when there is no data 1`] = `
99100
<View>
100101
<View
101102
onFocusCapture={[Function]}
102-
onLayout={[Function]}
103103
style={null}
104104
>
105105
<sectionHeader
@@ -139,6 +139,7 @@ exports[`SectionList renders a footer when there is no data and no header 1`] =
139139
onScroll={[Function]}
140140
onScrollBeginDrag={[Function]}
141141
onScrollEndDrag={[Function]}
142+
onStickyHeaderLayout={[Function]}
142143
renderItem={[Function]}
143144
scrollEventThrottle={0.0001}
144145
stickyHeaderIndices={
@@ -150,7 +151,6 @@ exports[`SectionList renders a footer when there is no data and no header 1`] =
150151
<View>
151152
<View
152153
onFocusCapture={[Function]}
153-
onLayout={[Function]}
154154
style={null}
155155
/>
156156
<View
@@ -223,6 +223,7 @@ exports[`SectionList renders all the bells and whistles 1`] = `
223223
onScroll={[Function]}
224224
onScrollBeginDrag={[Function]}
225225
onScrollEndDrag={[Function]}
226+
onStickyHeaderLayout={[Function]}
226227
refreshControl={
227228
<RefreshControlMock
228229
onRefresh={[MockFunction]}
@@ -252,7 +253,6 @@ exports[`SectionList renders all the bells and whistles 1`] = `
252253
</View>
253254
<View
254255
onFocusCapture={[Function]}
255-
onLayout={[Function]}
256256
style={null}
257257
>
258258
<sectionHeader
@@ -297,7 +297,6 @@ exports[`SectionList renders all the bells and whistles 1`] = `
297297
</View>
298298
<View
299299
onFocusCapture={[Function]}
300-
onLayout={[Function]}
301300
style={null}
302301
>
303302
<sectionHeader
@@ -342,7 +341,6 @@ exports[`SectionList renders all the bells and whistles 1`] = `
342341
</View>
343342
<View
344343
onFocusCapture={[Function]}
345-
onLayout={[Function]}
346344
style={null}
347345
>
348346
<sectionHeader
@@ -409,6 +407,7 @@ exports[`SectionList renders empty list 1`] = `
409407
onScroll={[Function]}
410408
onScrollBeginDrag={[Function]}
411409
onScrollEndDrag={[Function]}
410+
onStickyHeaderLayout={[Function]}
412411
renderItem={[Function]}
413412
scrollEventThrottle={0.0001}
414413
stickyHeaderIndices={Array []}

packages/react-native/Libraries/__tests__/__snapshots__/public-api-test.js.snap

+9
Original file line numberDiff line numberDiff line change
@@ -1869,6 +1869,11 @@ type StickyHeaderComponentType = component(
18691869
>,
18701870
...ScrollViewStickyHeaderProps
18711871
);
1872+
export type StickyHeaderOnLayoutEventContext<T: { ... } = { ... }> = $ReadOnly<{
1873+
index: number,
1874+
key: React.Key,
1875+
itemProps: T,
1876+
}>;
18721877
export type ScrollViewProps = $ReadOnly<{
18731878
...ViewProps,
18741879
...ScrollViewPropsIOS,
@@ -1892,6 +1897,10 @@ export type ScrollViewProps = $ReadOnly<{
18921897
onScrollBeginDrag?: ?(event: ScrollEvent) => void,
18931898
onScrollEndDrag?: ?(event: ScrollEvent) => void,
18941899
onContentSizeChange?: (contentWidth: number, contentHeight: number) => void,
1900+
onStickyHeaderLayout?: (
1901+
event: LayoutChangeEvent,
1902+
context: StickyHeaderOnLayoutEventContext<{ ... }>
1903+
) => void,
18951904
onKeyboardDidShow?: (event: KeyboardEvent) => void,
18961905
onKeyboardDidHide?: (event: KeyboardEvent) => void,
18971906
onKeyboardWillShow?: (event: KeyboardEvent) => void,

packages/virtualized-lists/Lists/VirtualizedList.js

+27-8
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,18 @@
1010

1111
import type {CellMetricProps, ListOrientation} from './ListMetricsAggregator';
1212
import type {ViewToken} from './ViewabilityHelper';
13+
import type {Props as CellRendererProps} from './VirtualizedListCellRenderer';
1314
import type {
1415
Item,
15-
VirtualizedListProps,
16-
ListRenderItemInfo,
1716
ListRenderItem,
17+
ListRenderItemInfo,
1818
Separators,
19+
VirtualizedListProps,
1920
} from './VirtualizedListProps';
20-
import type {ScrollResponderType} from 'react-native/Libraries/Components/ScrollView/ScrollView';
21+
import type {
22+
ScrollResponderType,
23+
StickyHeaderOnLayoutEventContext,
24+
} from 'react-native/Libraries/Components/ScrollView/ScrollView';
2125
import type {ViewStyleProp} from 'react-native/Libraries/StyleSheet/StyleSheet';
2226
import type {
2327
LayoutChangeEvent,
@@ -791,9 +795,9 @@ class VirtualizedList extends StateSafePureComponent<
791795
for (let ii = first; ii <= last; ii++) {
792796
const item = getItem(data, ii);
793797
const key = VirtualizedList._keyExtractor(item, ii, this.props);
794-
798+
const isSticky = stickyIndicesFromProps.has(ii + stickyOffset);
795799
this._indicesToKeys.set(ii, key);
796-
if (stickyIndicesFromProps.has(ii + stickyOffset)) {
800+
if (isSticky) {
797801
stickyHeaderIndices.push(cells.length);
798802
}
799803

@@ -819,9 +823,10 @@ class VirtualizedList extends StateSafePureComponent<
819823
this._cellRefs[key] = ref;
820824
}}
821825
renderItem={renderItem}
822-
{...(shouldListenForLayout && {
823-
onCellLayout: this._onCellLayout,
824-
})}
826+
{...(shouldListenForLayout &&
827+
!isSticky && {
828+
onCellLayout: this._onCellLayout,
829+
})}
825830
/>,
826831
);
827832
prevCellKey = key;
@@ -1072,6 +1077,7 @@ class VirtualizedList extends StateSafePureComponent<
10721077
...this.props,
10731078
onContentSizeChange: this._onContentSizeChange,
10741079
onLayout: this._onLayout,
1080+
onStickyHeaderLayout: this._onStickyHeaderLayout,
10751081
onScroll: this._onScroll,
10761082
onScrollBeginDrag: this._onScrollBeginDrag,
10771083
onScrollEndDrag: this._onScrollEndDrag,
@@ -1394,6 +1400,19 @@ class VirtualizedList extends StateSafePureComponent<
13941400
this._maybeCallOnEdgeReached();
13951401
};
13961402

1403+
_onStickyHeaderLayout = (
1404+
e: LayoutChangeEvent,
1405+
{
1406+
key: _key,
1407+
index: _index,
1408+
itemProps,
1409+
}: StickyHeaderOnLayoutEventContext<CellRendererProps<{...}>>,
1410+
) => {
1411+
// Key and index provided in event are relative to current window.
1412+
const {index: cellIndex, cellKey} = itemProps;
1413+
this._onCellLayout(e, cellKey, cellIndex);
1414+
};
1415+
13971416
_onLayoutEmpty = (e: LayoutChangeEvent) => {
13981417
this.props.onLayout && this.props.onLayout(e);
13991418
};

0 commit comments

Comments
 (0)