Skip to content

Commit 4d4593d

Browse files
committed
fix: sticky header calculation
1 parent d825a4d commit 4d4593d

File tree

8 files changed

+153
-12
lines changed

8 files changed

+153
-12
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
@@ -376,6 +376,12 @@ type StickyHeaderComponentType = component(
376376
...ScrollViewStickyHeaderProps
377377
);
378378

379+
export type StickyHeaderOnLayoutEventContext<T: {...} = {...}> = $ReadOnly<{
380+
index: number,
381+
key: string,
382+
itemProps: T,
383+
}>;
384+
379385
export type Props = $ReadOnly<{|
380386
...ViewProps,
381387
...IOSProps,
@@ -535,6 +541,10 @@ export type Props = $ReadOnly<{|
535541
* which this ScrollView renders.
536542
*/
537543
onContentSizeChange?: (contentWidth: number, contentHeight: number) => void,
544+
onStickyHeaderLayout?: (
545+
event: LayoutEvent,
546+
context: StickyHeaderOnLayoutEventContext<{...}>,
547+
) => void,
538548
onKeyboardDidShow?: (event: KeyboardEvent) => void,
539549
onKeyboardDidHide?: (event: KeyboardEvent) => void,
540550
onKeyboardWillShow?: (event: KeyboardEvent) => void,
@@ -1136,11 +1146,19 @@ class ScrollView extends React.Component<Props, State> {
11361146
return;
11371147
}
11381148

1149+
this.props.onStickyHeaderLayout &&
1150+
this.props.onStickyHeaderLayout(event, {
1151+
key,
1152+
index,
1153+
itemProps: childArray[index].props,
1154+
});
1155+
11391156
const layoutY = event.nativeEvent.layout.y;
11401157
this._headerLayoutYs.set(key, layoutY);
11411158

11421159
const indexOfIndex = stickyHeaderIndices.indexOf(index);
11431160
const previousHeaderIndex = stickyHeaderIndices[indexOfIndex - 1];
1161+
11441162
if (previousHeaderIndex != null) {
11451163
const previousHeader = this._stickyHeaderRefs.get(
11461164
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
@@ -2090,6 +2090,11 @@ type StickyHeaderComponentType = component(
20902090
>,
20912091
...ScrollViewStickyHeaderProps
20922092
);
2093+
export type StickyHeaderOnLayoutEventContext<T: { ... } = { ... }> = $ReadOnly<{
2094+
index: number,
2095+
key: string,
2096+
itemProps: T,
2097+
}>;
20932098
export type Props = $ReadOnly<{|
20942099
...ViewProps,
20952100
...IOSProps,
@@ -2113,6 +2118,10 @@ export type Props = $ReadOnly<{|
21132118
onScrollBeginDrag?: ?(event: ScrollEvent) => void,
21142119
onScrollEndDrag?: ?(event: ScrollEvent) => void,
21152120
onContentSizeChange?: (contentWidth: number, contentHeight: number) => void,
2121+
onStickyHeaderLayout?: (
2122+
event: LayoutEvent,
2123+
context: StickyHeaderOnLayoutEventContext<{ ... }>
2124+
) => void,
21162125
onKeyboardDidShow?: (event: KeyboardEvent) => void,
21172126
onKeyboardDidHide?: (event: KeyboardEvent) => void,
21182127
onKeyboardWillShow?: (event: KeyboardEvent) => void,

packages/virtualized-lists/Lists/VirtualizedList.js

+25-6
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,
1516
Props,
1617
RenderItemProps,
1718
RenderItemType,
1819
Separators,
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
LayoutEvent,
@@ -789,9 +793,9 @@ class VirtualizedList extends StateSafePureComponent<Props, State> {
789793
for (let ii = first; ii <= last; ii++) {
790794
const item = getItem(data, ii);
791795
const key = VirtualizedList._keyExtractor(item, ii, this.props);
792-
796+
const isSticky = stickyIndicesFromProps.has(ii + stickyOffset);
793797
this._indicesToKeys.set(ii, key);
794-
if (stickyIndicesFromProps.has(ii + stickyOffset)) {
798+
if (isSticky) {
795799
stickyHeaderIndices.push(cells.length);
796800
}
797801

@@ -817,9 +821,10 @@ class VirtualizedList extends StateSafePureComponent<Props, State> {
817821
this._cellRefs[key] = ref;
818822
}}
819823
renderItem={renderItem}
820-
{...(shouldListenForLayout && {
821-
onCellLayout: this._onCellLayout,
822-
})}
824+
{...(shouldListenForLayout &&
825+
!isSticky && {
826+
onCellLayout: this._onCellLayout,
827+
})}
823828
/>,
824829
);
825830
prevCellKey = key;
@@ -1069,6 +1074,7 @@ class VirtualizedList extends StateSafePureComponent<Props, State> {
10691074
...this.props,
10701075
onContentSizeChange: this._onContentSizeChange,
10711076
onLayout: this._onLayout,
1077+
onStickyHeaderLayout: this._onStickyHeaderLayout,
10721078
onScroll: this._onScroll,
10731079
onScrollBeginDrag: this._onScrollBeginDrag,
10741080
onScrollEndDrag: this._onScrollEndDrag,
@@ -1391,6 +1397,19 @@ class VirtualizedList extends StateSafePureComponent<Props, State> {
13911397
this._maybeCallOnEdgeReached();
13921398
};
13931399

1400+
_onStickyHeaderLayout = (
1401+
e: LayoutEvent,
1402+
{
1403+
key: _key,
1404+
index: _index,
1405+
itemProps,
1406+
}: StickyHeaderOnLayoutEventContext<CellRendererProps<{...}>>,
1407+
) => {
1408+
// Key and index provided in event are relative to current window.
1409+
const {index: cellIndex, cellKey} = itemProps;
1410+
this._onCellLayout(e, cellKey, cellIndex);
1411+
};
1412+
13941413
_onLayoutEmpty = (e: LayoutEvent) => {
13951414
this.props.onLayout && this.props.onLayout(e);
13961415
};

0 commit comments

Comments
 (0)