diff --git a/Libraries/Components/ScrollView/ScrollViewStickyHeader.js b/Libraries/Components/ScrollView/ScrollViewStickyHeader.js index a6826f3ea7e..a7b904b3664 100644 --- a/Libraries/Components/ScrollView/ScrollViewStickyHeader.js +++ b/Libraries/Components/ScrollView/ScrollViewStickyHeader.js @@ -170,7 +170,9 @@ class ScrollViewStickyHeader extends React.Component { this.props.onLayout(event); const child = React.Children.only(this.props.children); - if (child.props.onLayout) { + if (child.props.onCellLayout) { + child.props.onCellLayout(event, child.props.cellKey, child.props.index); + } else if (child.props.onLayout) { child.props.onLayout(event); } }; diff --git a/Libraries/Lists/VirtualizedList.js b/Libraries/Lists/VirtualizedList.js index 2d491d4a6c7..728a4b06582 100644 --- a/Libraries/Lists/VirtualizedList.js +++ b/Libraries/Lists/VirtualizedList.js @@ -34,6 +34,7 @@ import type { ViewToken, ViewabilityConfigCallbackPair, } from './ViewabilityHelper'; +import type {LayoutEvent} from '../Types/CoreEventTypes'; import { VirtualizedListCellContextProvider, VirtualizedListContext, @@ -824,8 +825,8 @@ class VirtualizedList extends React.PureComponent { item={item} key={key} prevCellKey={prevCellKey} + onCellLayout={this._onCellLayout} onUpdateSeparators={this._onUpdateSeparators} - onLayout={e => this._onCellLayout(e, key, ii)} onUnmount={this._onCellUnmount} parentProps={this.props} ref={ref => { @@ -1268,7 +1269,7 @@ class VirtualizedList extends React.PureComponent { } }; - _onCellLayout(e, cellKey, index) { + _onCellLayout = (e: LayoutEvent, cellKey: string, index: number): void => { const layout = e.nativeEvent.layout; const next = { offset: this._selectOffset(layout), @@ -1301,7 +1302,7 @@ class VirtualizedList extends React.PureComponent { this._computeBlankness(); this._updateViewableItems(this.props.data); - } + }; _onCellUnmount = (cellKey: string) => { const curr = this._frames[cellKey]; @@ -1380,7 +1381,7 @@ class VirtualizedList extends React.PureComponent { } } - _onLayout = (e: Object) => { + _onLayout = (e: LayoutEvent) => { if (this._isNestedWithSameOrientation()) { // Need to adjust our scroll metrics to be relative to our containing // VirtualizedList before we can make claims about list item viewability @@ -1395,7 +1396,7 @@ class VirtualizedList extends React.PureComponent { this._maybeCallOnEndReached(); }; - _onLayoutEmpty = e => { + _onLayoutEmpty = (e: LayoutEvent) => { this.props.onLayout && this.props.onLayout(e); }; @@ -1403,12 +1404,12 @@ class VirtualizedList extends React.PureComponent { return this._getCellKey() + '-footer'; } - _onLayoutFooter = e => { + _onLayoutFooter = (e: LayoutEvent) => { this._triggerRemeasureForChildListsInCell(this._getFooterCellKey()); this._footerLength = this._selectLength(e.nativeEvent.layout); }; - _onLayoutHeader = e => { + _onLayoutHeader = (e: LayoutEvent) => { this._headerLength = this._selectLength(e.nativeEvent.layout); }; @@ -1893,7 +1894,7 @@ type CellRendererProps = { inversionStyle: ViewStyleProp, item: Item, // This is extracted by ScrollViewStickyHeader - onLayout: (event: Object) => void, + onCellLayout: (event: Object, cellKey: string, index: number) => void, onUnmount: (cellKey: string) => void, onUpdateSeparators: (cellKeys: Array, props: Object) => void, parentProps: { @@ -1980,6 +1981,15 @@ class CellRenderer extends React.Component< this.props.onUnmount(this.props.cellKey); } + _onLayout = (nativeEvent: LayoutEvent): void => { + this.props.onCellLayout && + this.props.onCellLayout( + nativeEvent, + this.props.cellKey, + this.props.index, + ); + }; + _renderElement(renderItem, ListItemComponent, item, index) { if (renderItem && ListItemComponent) { console.warn( @@ -2039,9 +2049,10 @@ class CellRenderer extends React.Component< /* $FlowFixMe[prop-missing] (>=0.68.0 site=react_native_fb) This comment * suppresses an error found when Flow v0.68 was deployed. To see the * error delete this comment and run Flow. */ - getItemLayout && !parentProps.debug && !fillRateHelper.enabled() + (getItemLayout && !parentProps.debug && !fillRateHelper.enabled()) || + !this.props.onCellLayout ? undefined - : this.props.onLayout; + : this._onLayout; // NOTE: that when this is a sticky header, `onLayout` will get automatically extracted and // called explicitly by `ScrollViewStickyHeader`. const itemSeparator = ItemSeparatorComponent && ( diff --git a/Libraries/Lists/__tests__/VirtualizedList-test.js b/Libraries/Lists/__tests__/VirtualizedList-test.js index 1efb5e53fb3..a2da1b2abc7 100644 --- a/Libraries/Lists/__tests__/VirtualizedList-test.js +++ b/Libraries/Lists/__tests__/VirtualizedList-test.js @@ -1448,6 +1448,36 @@ it('renders windowSize derived region at bottom', () => { expect(component).toMatchSnapshot(); }); +it('calls _onCellLayout properly', () => { + const items = [{key: 'i1'}, {key: 'i2'}, {key: 'i3'}]; + const mock = jest.fn(); + const component = ReactTestRenderer.create( + } + getItem={(data, index) => data[index]} + getItemCount={data => data.length} + />, + ); + const virtualList: VirtualizedList = component.getInstance(); + virtualList._onCellLayout = mock; + component.update( + } + getItem={(data, index) => data[index]} + getItemCount={data => data.length} + />, + ); + const cell = virtualList._cellRefs.i4; + const event = { + nativeEvent: {layout: {x: 0, y: 0, width: 50, height: 50}}, + }; + cell._onLayout(event); + expect(mock).toHaveBeenCalledWith(event, 'i4', 3); + expect(mock).not.toHaveBeenCalledWith(event, 'i3', 2); +}); + function generateItems(count) { return Array(count) .fill() diff --git a/packages/rn-tester/js/examples/FlatList/FlatList-stickyHeaders.js b/packages/rn-tester/js/examples/FlatList/FlatList-stickyHeaders.js new file mode 100644 index 00000000000..8660e5c48cb --- /dev/null +++ b/packages/rn-tester/js/examples/FlatList/FlatList-stickyHeaders.js @@ -0,0 +1,72 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow strict-local + * @format + */ + +import type {RenderItemProps} from 'react-native/Libraries/Lists/VirtualizedList'; +import type {RNTesterModuleExample} from '../../types/RNTesterTypes'; + +import * as React from 'react'; +import {FlatList, StyleSheet, Text, View} from 'react-native'; + +const DATA = [ + 'Sticky Pizza', + 'Burger', + 'Sticky Risotto', + 'French Fries', + 'Sticky Onion Rings', + 'Fried Shrimps', + 'Water', + 'Coke', + 'Beer', + 'Cheesecake', + 'Ice Cream', +]; + +const STICKY_HEADER_INDICES = [0, 2, 4]; + +const Item = ({item, separators}: RenderItemProps) => { + return ( + + {item} + + ); +}; + +export function FlatList_stickyHeaders(): React.Node { + return ( + item + index} + style={styles.list} + stickyHeaderIndices={STICKY_HEADER_INDICES} + renderItem={Item} + /> + ); +} + +const styles = StyleSheet.create({ + item: { + backgroundColor: 'pink', + padding: 20, + marginVertical: 8, + }, + list: { + flex: 1, + }, + title: { + fontSize: 24, + }, +}); + +export default ({ + title: 'Sticky Headers', + name: 'stickyHeaders', + description: 'Test sticky headers on FlatList', + render: () => , +}: RNTesterModuleExample); diff --git a/packages/rn-tester/js/examples/FlatList/FlatListExampleIndex.js b/packages/rn-tester/js/examples/FlatList/FlatListExampleIndex.js index 6b5e0b5b603..7bcfa14d7ac 100644 --- a/packages/rn-tester/js/examples/FlatList/FlatListExampleIndex.js +++ b/packages/rn-tester/js/examples/FlatList/FlatListExampleIndex.js @@ -16,6 +16,7 @@ import InvertedExample from './FlatList-inverted'; import onViewableItemsChangedExample from './FlatList-onViewableItemsChanged'; import WithSeparatorsExample from './FlatList-withSeparators'; import MultiColumnExample from './FlatList-multiColumn'; +import StickyHeadersExample from './FlatList-stickyHeaders'; export default ({ framework: 'React', @@ -32,5 +33,6 @@ export default ({ onViewableItemsChangedExample, WithSeparatorsExample, MultiColumnExample, + StickyHeadersExample, ], }: RNTesterModule);