From 71847805a79431e14c3ef4ea77f4c82e1d1702f0 Mon Sep 17 00:00:00 2001 From: Pranav Yadav Date: Mon, 21 Nov 2022 15:30:05 +0000 Subject: [PATCH] Feat: `FlatList`s as `@react-native/flat-lists` package Summary: This diff moves `FlatList` and `SectionList` to a new package `@react-native/flat-lists`. This is a first step towards moving all the `FlatList`-related code to a separate package. This will allow us to make changes to the `FlatList` implementation without affecting the rest of the React Native codebase. Changelog: [General] [Changed] - Move `FlatList`s to `@react-native/flat-lists` package. `FlatList` and `SectionList` are now available in `@react-native/flat-lists` package. **Warning**: This though this is NOT a breaking change, and you will NOT need to update your imports to use the new package. But, moving forward it will be better to install `@react-native/flat-list` if you want to import explicitly. --- packages/flat-lists/.npmignore | 1 + packages/flat-lists/Lists/FlatList.d.ts | 246 ++++++ packages/flat-lists/Lists/FlatList.js | 700 ++++++++++++++++++ .../Lists/FlatList.js.flow | 8 +- packages/flat-lists/Lists/SectionList.d.ts | 257 +++++++ packages/flat-lists/Lists/SectionList.js | 266 +++++++ .../flat-lists/Lists/SectionListModern.js | 247 ++++++ .../Lists/__flowtests__/FlatList-flowtest.js | 0 .../__flowtests__/SectionList-flowtest.js | 2 +- .../Lists/__tests__/FlatList-test.js | 4 +- .../Lists/__tests__/SectionList-test.js | 2 +- .../__snapshots__/FlatList-test.js.snap | 0 .../__snapshots__/SectionList-test.js.snap | 0 packages/flat-lists/README.md | 32 + packages/flat-lists/index.d.ts | 11 + packages/flat-lists/index.js | 29 + packages/flat-lists/package.json | 27 + .../Libraries/Lists/FlatList.d.ts | 240 +----- .../react-native/Libraries/Lists/FlatList.js | 692 +---------------- .../Libraries/Lists/SectionList.d.ts | 265 +------ .../Libraries/Lists/SectionList.js | 255 +------ .../Libraries/Lists/SectionListModern.js | 239 +----- packages/react-native/index.js | 2 +- packages/react-native/metro.config.js | 1 + packages/react-native/package.json | 1 + packages/react-native/types/index.d.ts | 3 +- packages/rn-tester/metro.config.js | 1 + 27 files changed, 1860 insertions(+), 1671 deletions(-) create mode 100644 packages/flat-lists/.npmignore create mode 100644 packages/flat-lists/Lists/FlatList.d.ts create mode 100644 packages/flat-lists/Lists/FlatList.js rename packages/{react-native/Libraries => flat-lists}/Lists/FlatList.js.flow (88%) create mode 100644 packages/flat-lists/Lists/SectionList.d.ts create mode 100644 packages/flat-lists/Lists/SectionList.js create mode 100644 packages/flat-lists/Lists/SectionListModern.js rename packages/{react-native/Libraries => flat-lists}/Lists/__flowtests__/FlatList-flowtest.js (100%) rename packages/{react-native/Libraries => flat-lists}/Lists/__flowtests__/SectionList-flowtest.js (100%) rename packages/{react-native/Libraries => flat-lists}/Lists/__tests__/FlatList-test.js (97%) rename packages/{react-native/Libraries => flat-lists}/Lists/__tests__/SectionList-test.js (100%) rename packages/{react-native/Libraries => flat-lists}/Lists/__tests__/__snapshots__/FlatList-test.js.snap (100%) rename packages/{react-native/Libraries => flat-lists}/Lists/__tests__/__snapshots__/SectionList-test.js.snap (100%) create mode 100644 packages/flat-lists/README.md create mode 100644 packages/flat-lists/index.d.ts create mode 100644 packages/flat-lists/index.js create mode 100644 packages/flat-lists/package.json diff --git a/packages/flat-lists/.npmignore b/packages/flat-lists/.npmignore new file mode 100644 index 00000000000000..60e6e53b998ca2 --- /dev/null +++ b/packages/flat-lists/.npmignore @@ -0,0 +1 @@ +__tests__ diff --git a/packages/flat-lists/Lists/FlatList.d.ts b/packages/flat-lists/Lists/FlatList.d.ts new file mode 100644 index 00000000000000..771c4d1530e6b8 --- /dev/null +++ b/packages/flat-lists/Lists/FlatList.d.ts @@ -0,0 +1,246 @@ +/** + * 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. + * + * @format + */ + +import type * as React from 'react'; +import type { + ListRenderItem, + ViewToken, + VirtualizedListProps, +} from '@react-native/virtualized-lists'; +import type { + ScrollViewComponent, + StyleProp, + ViewStyle, + View, +} from 'react-native'; + +export interface FlatListProps extends VirtualizedListProps { + /** + * Optional custom style for multi-item rows generated when numColumns > 1 + */ + columnWrapperStyle?: StyleProp | undefined; + + /** + * Determines when the keyboard should stay visible after a tap. + * - 'never' (the default), tapping outside of the focused text input when the keyboard is up dismisses the keyboard. When this happens, children won't receive the tap. + * - 'always', the keyboard will not dismiss automatically, and the scroll view will not catch taps, but children of the scroll view can catch taps. + * - 'handled', the keyboard will not dismiss automatically when the tap was handled by a children, (or captured by an ancestor). + * - false, deprecated, use 'never' instead + * - true, deprecated, use 'always' instead + */ + keyboardShouldPersistTaps?: + | boolean + | 'always' + | 'never' + | 'handled' + | undefined; + + /** + * An array (or array-like list) of items to render. Other data types can be + * used by targeting VirtualizedList directly. + */ + data: ArrayLike | null | undefined; + + /** + * A marker property for telling the list to re-render (since it implements PureComponent). + * If any of your `renderItem`, Header, Footer, etc. functions depend on anything outside of the `data` prop, + * stick it here and treat it immutably. + */ + extraData?: any | undefined; + + /** + * `getItemLayout` is an optional optimization that lets us skip measurement of dynamic + * content if you know the height of items a priori. getItemLayout is the most efficient, + * and is easy to use if you have fixed height items, for example: + * ``` + * getItemLayout={(data, index) => ( + * {length: ITEM_HEIGHT, offset: ITEM_HEIGHT * index, index} + * )} + * ``` + * Remember to include separator length (height or width) in your offset calculation if you specify + * `ItemSeparatorComponent`. + */ + getItemLayout?: + | (( + data: ArrayLike | null | undefined, + index: number, + ) => {length: number; offset: number; index: number}) + | undefined; + + /** + * If true, renders items next to each other horizontally instead of stacked vertically. + */ + horizontal?: boolean | null | undefined; + + /** + * How many items to render in the initial batch + */ + initialNumToRender?: number | undefined; + + /** + * Instead of starting at the top with the first item, start at initialScrollIndex + */ + initialScrollIndex?: number | null | undefined; + + /** + * Used to extract a unique key for a given item at the specified index. Key is used for caching + * and as the react key to track item re-ordering. The default extractor checks `item.key`, then + * falls back to using the index, like React does. + */ + keyExtractor?: ((item: ItemT, index: number) => string) | undefined; + + /** + * Uses legacy MetroListView instead of default VirtualizedSectionList + */ + legacyImplementation?: boolean | undefined; + + /** + * Multiple columns can only be rendered with `horizontal={false}` and will zig-zag like a `flexWrap` layout. + * Items should all be the same height - masonry layouts are not supported. + */ + numColumns?: number | undefined; + + /** + * If provided, a standard RefreshControl will be added for "Pull to Refresh" functionality. + * Make sure to also set the refreshing prop correctly. + */ + onRefresh?: (() => void) | null | undefined; + + /** + * Called when the viewability of rows changes, as defined by the `viewablePercentThreshold` prop. + */ + onViewableItemsChanged?: + | ((info: { + viewableItems: Array; + changed: Array; + }) => void) + | null + | undefined; + + /** + * Set this true while waiting for new data from a refresh. + */ + refreshing?: boolean | null | undefined; + + /** + * Takes an item from data and renders it into the list. Typical usage: + * ``` + * _renderItem = ({item}) => ( + * this._onPress(item)}> + * {item.title} + * + * ); + * ... + * + * ``` + * Provides additional metadata like `index` if you need it. + */ + renderItem: ListRenderItem | null | undefined; + + /** + * See `ViewabilityHelper` for flow type and further documentation. + */ + viewabilityConfig?: any | undefined; + + /** + * Note: may have bugs (missing content) in some circumstances - use at your own risk. + * + * This may improve scroll performance for large lists. + */ + removeClippedSubviews?: boolean | undefined; + + /** + * Fades out the edges of the scroll content. + * + * If the value is greater than 0, the fading edges will be set accordingly + * to the current scroll direction and position, + * indicating if there is more content to show. + * + * The default value is 0. + * @platform android + */ + fadingEdgeLength?: number | undefined; +} + +export abstract class FlatListComponent< + ItemT, + Props, +> extends React.Component { + /** + * Scrolls to the end of the content. May be janky without `getItemLayout` prop. + */ + scrollToEnd: (params?: {animated?: boolean | null | undefined}) => void; + + /** + * Scrolls to the item at the specified index such that it is positioned in the viewable area + * such that viewPosition 0 places it at the top, 1 at the bottom, and 0.5 centered in the middle. + * Cannot scroll to locations outside the render window without specifying the getItemLayout prop. + */ + scrollToIndex: (params: { + animated?: boolean | null | undefined; + index: number; + viewOffset?: number | undefined; + viewPosition?: number | undefined; + }) => void; + + /** + * Requires linear scan through data - use `scrollToIndex` instead if possible. + * May be janky without `getItemLayout` prop. + */ + scrollToItem: (params: { + animated?: boolean | null | undefined; + item: ItemT; + viewOffset?: number | undefined; + viewPosition?: number | undefined; + }) => void; + + /** + * Scroll to a specific content pixel offset, like a normal `ScrollView`. + */ + scrollToOffset: (params: { + animated?: boolean | null | undefined; + offset: number; + }) => void; + + /** + * Tells the list an interaction has occurred, which should trigger viewability calculations, + * e.g. if waitForInteractions is true and the user has not scrolled. This is typically called + * by taps on items or by navigation actions. + */ + recordInteraction: () => void; + + /** + * Displays the scroll indicators momentarily. + */ + flashScrollIndicators: () => void; + + /** + * Provides a handle to the underlying scroll responder. + */ + getScrollResponder: () => JSX.Element | null | undefined; + + /** + * Provides a reference to the underlying host component + */ + getNativeScrollRef: () => + | React.ElementRef + | React.ElementRef + | null + | undefined; + + getScrollableNode: () => any; + + // TODO: use `unknown` instead of `any` for Typescript >= 3.0 + setNativeProps: (props: {[key: string]: any}) => void; +} + +export class FlatList extends FlatListComponent< + ItemT, + FlatListProps +> {} diff --git a/packages/flat-lists/Lists/FlatList.js b/packages/flat-lists/Lists/FlatList.js new file mode 100644 index 00000000000000..c6fd06192f7b41 --- /dev/null +++ b/packages/flat-lists/Lists/FlatList.js @@ -0,0 +1,700 @@ +/** + * 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 + * @format + */ + +import typeof ScrollViewNativeComponent from 'react-native'; +import type {ViewStyleProp} from 'react-native/Libraries/StyleSheet/StyleSheet'; +import type { + RenderItemProps, + RenderItemType, + ViewabilityConfigCallbackPair, + ViewToken, +} from '@react-native/virtualized-lists'; +import {type ScrollResponderType} from 'react-native/Libraries/Components/ScrollView/ScrollView'; + +import { + VirtualizedList, + keyExtractor as defaultKeyExtractor, +} from '@react-native/virtualized-lists'; +import {View, StyleSheet, Platform} from 'react-native'; +import deepDiffer from 'react-native/Libraries/Utilities/differ/deepDiffer'; +import memoizeOne from 'memoize-one'; +import invariant from 'invariant'; +import * as React from 'react'; + +type RequiredProps = {| + /** + * An array (or array-like list) of items to render. Other data types can be + * used by targeting VirtualizedList directly. + */ + data: ?$ArrayLike, +|}; +type OptionalProps = {| + /** + * Takes an item from `data` and renders it into the list. Example usage: + * + * ( + * + * )} + * data={[{title: 'Title Text', key: 'item1'}]} + * renderItem={({item, separators}) => ( + * this._onPress(item)} + * onShowUnderlay={separators.highlight} + * onHideUnderlay={separators.unhighlight}> + * + * {item.title} + * + * + * )} + * /> + * + * Provides additional metadata like `index` if you need it, as well as a more generic + * `separators.updateProps` function which let's you set whatever props you want to change the + * rendering of either the leading separator or trailing separator in case the more common + * `highlight` and `unhighlight` (which set the `highlighted: boolean` prop) are insufficient for + * your use-case. + */ + renderItem?: ?RenderItemType, + + /** + * Optional custom style for multi-item rows generated when numColumns > 1. + */ + columnWrapperStyle?: ViewStyleProp, + /** + * A marker property for telling the list to re-render (since it implements `PureComponent`). If + * any of your `renderItem`, Header, Footer, etc. functions depend on anything outside of the + * `data` prop, stick it here and treat it immutably. + */ + extraData?: any, + /** + * `getItemLayout` is an optional optimizations that let us skip measurement of dynamic content if + * you know the height of items a priori. `getItemLayout` is the most efficient, and is easy to + * use if you have fixed height items, for example: + * + * getItemLayout={(data, index) => ( + * {length: ITEM_HEIGHT, offset: ITEM_HEIGHT * index, index} + * )} + * + * Adding `getItemLayout` can be a great performance boost for lists of several hundred items. + * Remember to include separator length (height or width) in your offset calculation if you + * specify `ItemSeparatorComponent`. + */ + getItemLayout?: ( + data: ?$ArrayLike, + index: number, + ) => { + length: number, + offset: number, + index: number, + ... + }, + /** + * If true, renders items next to each other horizontally instead of stacked vertically. + */ + horizontal?: ?boolean, + /** + * How many items to render in the initial batch. This should be enough to fill the screen but not + * much more. Note these items will never be unmounted as part of the windowed rendering in order + * to improve perceived performance of scroll-to-top actions. + */ + initialNumToRender?: ?number, + /** + * Instead of starting at the top with the first item, start at `initialScrollIndex`. This + * disables the "scroll to top" optimization that keeps the first `initialNumToRender` items + * always rendered and immediately renders the items starting at this initial index. Requires + * `getItemLayout` to be implemented. + */ + initialScrollIndex?: ?number, + /** + * Reverses the direction of scroll. Uses scale transforms of -1. + */ + inverted?: ?boolean, + /** + * Used to extract a unique key for a given item at the specified index. Key is used for caching + * and as the react key to track item re-ordering. The default extractor checks `item.key`, then + * falls back to using the index, like React does. + */ + keyExtractor?: ?(item: ItemT, index: number) => string, + /** + * Multiple columns can only be rendered with `horizontal={false}` and will zig-zag like a + * `flexWrap` layout. Items should all be the same height - masonry layouts are not supported. + * + * The default value is 1. + */ + numColumns?: number, + /** + * Note: may have bugs (missing content) in some circumstances - use at your own risk. + * + * This may improve scroll performance for large lists. + * + * The default value is true for Android. + */ + removeClippedSubviews?: boolean, + /** + * See `ScrollView` for flow type and further documentation. + */ + fadingEdgeLength?: ?number, + /** + * Enable an optimization to memoize the item renderer to prevent unnecessary rerenders. + */ + strictMode?: boolean, +|}; + +/** + * Default Props Helper Functions + * Use the following helper functions for default values + */ + +// removeClippedSubviewsOrDefault(this.props.removeClippedSubviews) +function removeClippedSubviewsOrDefault(removeClippedSubviews: ?boolean) { + return removeClippedSubviews ?? Platform.OS === 'android'; +} + +// numColumnsOrDefault(this.props.numColumns) +function numColumnsOrDefault(numColumns: ?number) { + return numColumns ?? 1; +} + +function isArrayLike(data: mixed): boolean { + // $FlowExpectedError[incompatible-use] + return typeof Object(data).length === 'number'; +} + +type FlatListProps = {| + ...RequiredProps, + ...OptionalProps, +|}; + +type VirtualizedListProps = React.ElementConfig; + +export type Props = { + ...$Diff< + VirtualizedListProps, + { + getItem: $PropertyType, + getItemCount: $PropertyType, + getItemLayout: $PropertyType, + renderItem: $PropertyType, + keyExtractor: $PropertyType, + ... + }, + >, + ...FlatListProps, + ... +}; + +/** + * A performant interface for rendering simple, flat lists, supporting the most handy features: + * + * - Fully cross-platform. + * - Optional horizontal mode. + * - Configurable viewability callbacks. + * - Header support. + * - Footer support. + * - Separator support. + * - Pull to Refresh. + * - Scroll loading. + * - ScrollToIndex support. + * + * If you need section support, use [``](docs/sectionlist.html). + * + * Minimal Example: + * + * {item.key}} + * /> + * + * More complex, multi-select example demonstrating `PureComponent` usage for perf optimization and avoiding bugs. + * + * - By binding the `onPressItem` handler, the props will remain `===` and `PureComponent` will + * prevent wasteful re-renders unless the actual `id`, `selected`, or `title` props change, even + * if the components rendered in `MyListItem` did not have such optimizations. + * - By passing `extraData={this.state}` to `FlatList` we make sure `FlatList` itself will re-render + * when the `state.selected` changes. Without setting this prop, `FlatList` would not know it + * needs to re-render any items because it is also a `PureComponent` and the prop comparison will + * not show any changes. + * - `keyExtractor` tells the list to use the `id`s for the react keys instead of the default `key` property. + * + * + * class MyListItem extends React.PureComponent { + * _onPress = () => { + * this.props.onPressItem(this.props.id); + * }; + * + * render() { + * const textColor = this.props.selected ? "red" : "black"; + * return ( + * + * + * + * {this.props.title} + * + * + * + * ); + * } + * } + * + * class MultiSelectList extends React.PureComponent { + * state = {selected: (new Map(): Map)}; + * + * _keyExtractor = (item, index) => item.id; + * + * _onPressItem = (id: string) => { + * // updater functions are preferred for transactional updates + * this.setState((state) => { + * // copy the map rather than modifying state. + * const selected = new Map(state.selected); + * selected.set(id, !selected.get(id)); // toggle + * return {selected}; + * }); + * }; + * + * _renderItem = ({item}) => ( + * + * ); + * + * render() { + * return ( + * + * ); + * } + * } + * + * This is a convenience wrapper around [``](docs/virtualizedlist.html), + * and thus inherits its props (as well as those of `ScrollView`) that aren't explicitly listed + * here, along with the following caveats: + * + * - Internal state is not preserved when content scrolls out of the render window. Make sure all + * your data is captured in the item data or external stores like Flux, Redux, or Relay. + * - This is a `PureComponent` which means that it will not re-render if `props` remain shallow- + * equal. Make sure that everything your `renderItem` function depends on is passed as a prop + * (e.g. `extraData`) that is not `===` after updates, otherwise your UI may not update on + * changes. This includes the `data` prop and parent component state. + * - In order to constrain memory and enable smooth scrolling, content is rendered asynchronously + * offscreen. This means it's possible to scroll faster than the fill rate ands momentarily see + * blank content. This is a tradeoff that can be adjusted to suit the needs of each application, + * and we are working on improving it behind the scenes. + * - By default, the list looks for a `key` prop on each item and uses that for the React key. + * Alternatively, you can provide a custom `keyExtractor` prop. + * + * Also inherits [ScrollView Props](docs/scrollview.html#props), unless it is nested in another FlatList of same orientation. + */ +class FlatList extends React.PureComponent, void> { + props: Props; + /** + * Scrolls to the end of the content. May be janky without `getItemLayout` prop. + */ + scrollToEnd(params?: ?{animated?: ?boolean, ...}) { + if (this._listRef) { + this._listRef.scrollToEnd(params); + } + } + + /** + * Scrolls to the item at the specified index such that it is positioned in the viewable area + * such that `viewPosition` 0 places it at the top, 1 at the bottom, and 0.5 centered in the + * middle. `viewOffset` is a fixed number of pixels to offset the final target position. + * + * Note: cannot scroll to locations outside the render window without specifying the + * `getItemLayout` prop. + */ + scrollToIndex(params: { + animated?: ?boolean, + index: number, + viewOffset?: number, + viewPosition?: number, + ... + }) { + if (this._listRef) { + this._listRef.scrollToIndex(params); + } + } + + /** + * Requires linear scan through data - use `scrollToIndex` instead if possible. + * + * Note: cannot scroll to locations outside the render window without specifying the + * `getItemLayout` prop. + */ + scrollToItem(params: { + animated?: ?boolean, + item: ItemT, + viewOffset?: number, + viewPosition?: number, + ... + }) { + if (this._listRef) { + this._listRef.scrollToItem(params); + } + } + + /** + * Scroll to a specific content pixel offset in the list. + * + * Check out [scrollToOffset](docs/virtualizedlist.html#scrolltooffset) of VirtualizedList + */ + scrollToOffset(params: {animated?: ?boolean, offset: number, ...}) { + if (this._listRef) { + this._listRef.scrollToOffset(params); + } + } + + /** + * Tells the list an interaction has occurred, which should trigger viewability calculations, e.g. + * if `waitForInteractions` is true and the user has not scrolled. This is typically called by + * taps on items or by navigation actions. + */ + recordInteraction() { + if (this._listRef) { + this._listRef.recordInteraction(); + } + } + + /** + * Displays the scroll indicators momentarily. + * + * @platform ios + */ + flashScrollIndicators() { + if (this._listRef) { + this._listRef.flashScrollIndicators(); + } + } + + /** + * Provides a handle to the underlying scroll responder. + */ + getScrollResponder(): ?ScrollResponderType { + if (this._listRef) { + return this._listRef.getScrollResponder(); + } + } + + /** + * Provides a reference to the underlying host component + */ + getNativeScrollRef(): + | ?React.ElementRef + | ?React.ElementRef { + if (this._listRef) { + /* $FlowFixMe[incompatible-return] Suppresses errors found when fixing + * TextInput typing */ + return this._listRef.getScrollRef(); + } + } + + getScrollableNode(): any { + if (this._listRef) { + return this._listRef.getScrollableNode(); + } + } + + setNativeProps(props: {[string]: mixed, ...}) { + if (this._listRef) { + this._listRef.setNativeProps(props); + } + } + + constructor(props: Props) { + super(props); + this._checkProps(this.props); + if (this.props.viewabilityConfigCallbackPairs) { + this._virtualizedListPairs = + this.props.viewabilityConfigCallbackPairs.map(pair => ({ + viewabilityConfig: pair.viewabilityConfig, + onViewableItemsChanged: this._createOnViewableItemsChanged( + pair.onViewableItemsChanged, + ), + })); + } else if (this.props.onViewableItemsChanged) { + this._virtualizedListPairs.push({ + /* $FlowFixMe[incompatible-call] (>=0.63.0 site=react_native_fb) This + * comment suppresses an error found when Flow v0.63 was deployed. To + * see the error delete this comment and run Flow. */ + viewabilityConfig: this.props.viewabilityConfig, + onViewableItemsChanged: this._createOnViewableItemsChanged( + this.props.onViewableItemsChanged, + ), + }); + } + } + + // $FlowFixMe[missing-local-annot] + componentDidUpdate(prevProps: Props) { + invariant( + prevProps.numColumns === this.props.numColumns, + 'Changing numColumns on the fly is not supported. Change the key prop on FlatList when ' + + 'changing the number of columns to force a fresh render of the component.', + ); + invariant( + prevProps.onViewableItemsChanged === this.props.onViewableItemsChanged, + 'Changing onViewableItemsChanged on the fly is not supported', + ); + invariant( + !deepDiffer(prevProps.viewabilityConfig, this.props.viewabilityConfig), + 'Changing viewabilityConfig on the fly is not supported', + ); + invariant( + prevProps.viewabilityConfigCallbackPairs === + this.props.viewabilityConfigCallbackPairs, + 'Changing viewabilityConfigCallbackPairs on the fly is not supported', + ); + + this._checkProps(this.props); + } + + _listRef: ?React.ElementRef; + _virtualizedListPairs: Array = []; + + _captureRef = (ref: ?React.ElementRef) => { + this._listRef = ref; + }; + + // $FlowFixMe[missing-local-annot] + _checkProps(props: Props) { + const { + // $FlowFixMe[prop-missing] this prop doesn't exist, is only used for an invariant + getItem, + // $FlowFixMe[prop-missing] this prop doesn't exist, is only used for an invariant + getItemCount, + horizontal, + columnWrapperStyle, + onViewableItemsChanged, + viewabilityConfigCallbackPairs, + } = props; + const numColumns = numColumnsOrDefault(this.props.numColumns); + invariant( + !getItem && !getItemCount, + 'FlatList does not support custom data formats.', + ); + if (numColumns > 1) { + invariant(!horizontal, 'numColumns does not support horizontal.'); + } else { + invariant( + !columnWrapperStyle, + 'columnWrapperStyle not supported for single column lists', + ); + } + invariant( + !(onViewableItemsChanged && viewabilityConfigCallbackPairs), + 'FlatList does not support setting both onViewableItemsChanged and ' + + 'viewabilityConfigCallbackPairs.', + ); + } + + _getItem = ( + data: $ArrayLike, + index: number, + ): ?(ItemT | $ReadOnlyArray) => { + const numColumns = numColumnsOrDefault(this.props.numColumns); + if (numColumns > 1) { + const ret = []; + for (let kk = 0; kk < numColumns; kk++) { + const itemIndex = index * numColumns + kk; + if (itemIndex < data.length) { + const item = data[itemIndex]; + ret.push(item); + } + } + return ret; + } else { + return data[index]; + } + }; + + _getItemCount = (data: ?$ArrayLike): number => { + // Legacy behavior of FlatList was to forward "undefined" length if invalid + // data like a non-arraylike object is passed. VirtualizedList would then + // coerce this, and the math would work out to no-op. For compatibility, if + // invalid data is passed, we tell VirtualizedList there are zero items + // available to prevent it from trying to read from the invalid data + // (without propagating invalidly typed data). + if (data != null && isArrayLike(data)) { + const numColumns = numColumnsOrDefault(this.props.numColumns); + return numColumns > 1 ? Math.ceil(data.length / numColumns) : data.length; + } else { + return 0; + } + }; + + _keyExtractor = (items: ItemT | Array, index: number): string => { + const numColumns = numColumnsOrDefault(this.props.numColumns); + const keyExtractor = this.props.keyExtractor ?? defaultKeyExtractor; + + if (numColumns > 1) { + invariant( + Array.isArray(items), + 'FlatList: Encountered internal consistency error, expected each item to consist of an ' + + 'array with 1-%s columns; instead, received a single item.', + numColumns, + ); + return items + .map((item, kk) => + keyExtractor(((item: $FlowFixMe): ItemT), index * numColumns + kk), + ) + .join(':'); + } + + // $FlowFixMe[incompatible-call] Can't call keyExtractor with an array + return keyExtractor(items, index); + }; + + _pushMultiColumnViewable(arr: Array, v: ViewToken): void { + const numColumns = numColumnsOrDefault(this.props.numColumns); + const keyExtractor = this.props.keyExtractor ?? defaultKeyExtractor; + v.item.forEach((item, ii) => { + invariant(v.index != null, 'Missing index!'); + const index = v.index * numColumns + ii; + arr.push({...v, item, key: keyExtractor(item, index), index}); + }); + } + + _createOnViewableItemsChanged( + onViewableItemsChanged: ?(info: { + viewableItems: Array, + changed: Array, + ... + }) => void, + // $FlowFixMe[missing-local-annot] + ) { + return (info: { + viewableItems: Array, + changed: Array, + ... + }) => { + const numColumns = numColumnsOrDefault(this.props.numColumns); + if (onViewableItemsChanged) { + if (numColumns > 1) { + const changed: Array = []; + const viewableItems: Array = []; + info.viewableItems.forEach(v => + this._pushMultiColumnViewable(viewableItems, v), + ); + info.changed.forEach(v => this._pushMultiColumnViewable(changed, v)); + onViewableItemsChanged({viewableItems, changed}); + } else { + onViewableItemsChanged(info); + } + } + }; + } + + _renderer = ( + ListItemComponent: ?(React.ComponentType | React.Element), + renderItem: ?RenderItemType, + columnWrapperStyle: ?ViewStyleProp, + numColumns: ?number, + extraData: ?any, + // $FlowFixMe[missing-local-annot] + ) => { + const cols = numColumnsOrDefault(numColumns); + + const render = (props: RenderItemProps): React.Node => { + if (ListItemComponent) { + // $FlowFixMe[not-a-component] Component isn't valid + // $FlowFixMe[incompatible-type-arg] Component isn't valid + // $FlowFixMe[incompatible-return] Component isn't valid + return ; + } else if (renderItem) { + // $FlowFixMe[incompatible-call] + return renderItem(props); + } else { + return null; + } + }; + + const renderProp = (info: RenderItemProps) => { + if (cols > 1) { + const {item, index} = info; + invariant( + Array.isArray(item), + 'Expected array of items with numColumns > 1', + ); + return ( + + {item.map((it, kk) => { + const element = render({ + // $FlowFixMe[incompatible-call] + item: it, + index: index * cols + kk, + separators: info.separators, + }); + return element != null ? ( + {element} + ) : null; + })} + + ); + } else { + return render(info); + } + }; + + return ListItemComponent + ? {ListItemComponent: renderProp} + : {renderItem: renderProp}; + }; + + // $FlowFixMe[missing-local-annot] + _memoizedRenderer = memoizeOne(this._renderer); + + render(): React.Node { + const { + numColumns, + columnWrapperStyle, + removeClippedSubviews: _removeClippedSubviews, + strictMode = false, + ...restProps + } = this.props; + + const renderer = strictMode ? this._memoizedRenderer : this._renderer; + + return ( + // $FlowFixMe[incompatible-exact] - `restProps` (`Props`) is inexact. + + ); + } +} + +const styles = StyleSheet.create({ + row: {flexDirection: 'row'}, +}); + +module.exports = FlatList; diff --git a/packages/react-native/Libraries/Lists/FlatList.js.flow b/packages/flat-lists/Lists/FlatList.js.flow similarity index 88% rename from packages/react-native/Libraries/Lists/FlatList.js.flow rename to packages/flat-lists/Lists/FlatList.js.flow index 304a1006182186..fc0fe56249277c 100644 --- a/packages/react-native/Libraries/Lists/FlatList.js.flow +++ b/packages/flat-lists/Lists/FlatList.js.flow @@ -9,11 +9,11 @@ */ const React = require('react'); -const View = require('../Components/View/View'); +const View = require('react-native/Libraries/Components/View/View'); -import typeof ScrollViewNativeComponent from '../Components/ScrollView/ScrollViewNativeComponent'; -import {type ScrollResponderType} from '../Components/ScrollView/ScrollView'; -import type {ViewStyleProp} from '../StyleSheet/StyleSheet'; +import typeof ScrollViewNativeComponent from 'react-native/Libraries/Components/ScrollView/ScrollViewNativeComponent'; +import {type ScrollResponderType} from 'react-native/Libraries/Components/ScrollView/ScrollView'; +import type {ViewStyleProp} from 'react-native/Libraries/StyleSheet/StyleSheet'; import type { RenderItemType, RenderItemProps, diff --git a/packages/flat-lists/Lists/SectionList.d.ts b/packages/flat-lists/Lists/SectionList.d.ts new file mode 100644 index 00000000000000..139eaa7e188fa6 --- /dev/null +++ b/packages/flat-lists/Lists/SectionList.d.ts @@ -0,0 +1,257 @@ +/** + * 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. + * + * @format + */ + +import type * as React from 'react'; +import type { + ListRenderItemInfo, + VirtualizedListWithoutRenderItemProps, +} from '@react-native/virtualized-lists'; +import type {ScrollView, ScrollViewProps} from 'react-native'; +import {NodeHandle, StyleProp, ViewStyle} from 'react-native'; + +/** + * @see https://reactnative.dev/docs/sectionlist + */ + +type DefaultSectionT = { + [key: string]: any; +}; + +export interface SectionBase { + data: ReadonlyArray; + + key?: string | undefined; + + renderItem?: SectionListRenderItem | undefined; + + ItemSeparatorComponent?: React.ComponentType | null | undefined; + + keyExtractor?: ((item: ItemT, index: number) => string) | undefined; +} + +export type SectionListData = SectionBase< + ItemT, + SectionT +> & + SectionT; + +/** + * @see https://reactnative.dev/docs/sectionlist.html#props + */ + +export interface SectionListRenderItemInfo + extends ListRenderItemInfo { + section: SectionListData; +} + +export type SectionListRenderItem = ( + info: SectionListRenderItemInfo, +) => React.ReactElement | null; + +export interface SectionListProps + extends VirtualizedListWithoutRenderItemProps { + /** + * Rendered in between each section. + */ + SectionSeparatorComponent?: + | React.ComponentType + | React.ReactElement + | null + | undefined; + + /** + * A marker property for telling the list to re-render (since it implements PureComponent). + * If any of your `renderItem`, Header, Footer, etc. functions depend on anything outside of the `data` prop, + * stick it here and treat it immutably. + */ + extraData?: any | undefined; + + /** + * `getItemLayout` is an optional optimization that lets us skip measurement of dynamic + * content if you know the height of items a priori. getItemLayout is the most efficient, + * and is easy to use if you have fixed height items, for example: + * ``` + * getItemLayout={(data, index) => ( + * {length: ITEM_HEIGHT, offset: ITEM_HEIGHT * index, index} + * )} + * ``` + */ + getItemLayout?: + | (( + data: SectionListData[] | null, + index: number, + ) => {length: number; offset: number; index: number}) + | undefined; + + /** + * How many items to render in the initial batch + */ + initialNumToRender?: number | undefined; + + /** + * Reverses the direction of scroll. Uses scale transforms of -1. + */ + inverted?: boolean | null | undefined; + + /** + * Used to extract a unique key for a given item at the specified index. Key is used for caching + * and as the react key to track item re-ordering. The default extractor checks `item.key`, then + * falls back to using the index, like React does. + */ + keyExtractor?: ((item: ItemT, index: number) => string) | undefined; + + /** + * Called once when the scroll position gets within onEndReachedThreshold of the rendered content. + */ + onEndReached?: ((info: {distanceFromEnd: number}) => void) | null | undefined; + + /** + * How far from the end (in units of visible length of the list) the bottom edge of the + * list must be from the end of the content to trigger the `onEndReached` callback. + * Thus a value of 0.5 will trigger `onEndReached` when the end of the content is + * within half the visible length of the list. + */ + onEndReachedThreshold?: number | null | undefined; + + /** + * If provided, a standard RefreshControl will be added for "Pull to Refresh" functionality. + * Make sure to also set the refreshing prop correctly. + */ + onRefresh?: (() => void) | null | undefined; + + /** + * Used to handle failures when scrolling to an index that has not been measured yet. + * Recommended action is to either compute your own offset and `scrollTo` it, or scroll as far + * as possible and then try again after more items have been rendered. + */ + onScrollToIndexFailed?: + | ((info: { + index: number; + highestMeasuredFrameIndex: number; + averageItemLength: number; + }) => void) + | undefined; + + /** + * Set this true while waiting for new data from a refresh. + */ + refreshing?: boolean | null | undefined; + + /** + * Default renderer for every item in every section. Can be over-ridden on a per-section basis. + */ + renderItem?: SectionListRenderItem | undefined; + + /** + * Rendered at the top of each section. Sticky headers are not yet supported. + */ + renderSectionHeader?: + | ((info: { + section: SectionListData; + }) => React.ReactElement | null) + | undefined; + + /** + * Rendered at the bottom of each section. + */ + renderSectionFooter?: + | ((info: { + section: SectionListData; + }) => React.ReactElement | null) + | undefined; + + /** + * An array of objects with data for each section. + */ + sections: ReadonlyArray>; + + /** + * Render a custom scroll component, e.g. with a differently styled `RefreshControl`. + */ + renderScrollComponent?: + | ((props: ScrollViewProps) => React.ReactElement) + | undefined; + + /** + * Note: may have bugs (missing content) in some circumstances - use at your own risk. + * + * This may improve scroll performance for large lists. + */ + removeClippedSubviews?: boolean | undefined; + + /** + * Makes section headers stick to the top of the screen until the next one pushes it off. + * Only enabled by default on iOS because that is the platform standard there. + */ + stickySectionHeadersEnabled?: boolean | undefined; + + /** + * Uses legacy MetroListView instead of default VirtualizedSectionList + */ + legacyImplementation?: boolean | undefined; +} + +export interface SectionListScrollParams { + animated?: boolean | undefined; + itemIndex: number; + sectionIndex: number; + viewOffset?: number | undefined; + viewPosition?: number | undefined; +} + +export abstract class SectionListComponent< + Props, +> extends React.Component { + /** + * Scrolls to the item at the specified sectionIndex and itemIndex (within the section) + * positioned in the viewable area such that viewPosition 0 places it at the top + * (and may be covered by a sticky header), 1 at the bottom, and 0.5 centered in the middle. + */ + scrollToLocation(params: SectionListScrollParams): void; + + /** + * Tells the list an interaction has occurred, which should trigger viewability calculations, e.g. + * if `waitForInteractions` is true and the user has not scrolled. This is typically called by + * taps on items or by navigation actions. + */ + recordInteraction(): void; + + /** + * Displays the scroll indicators momentarily. + * + * @platform ios + */ + flashScrollIndicators(): void; + + /** + * Provides a handle to the underlying scroll responder. + */ + getScrollResponder(): ScrollView | undefined; + + /** + * Provides a handle to the underlying scroll node. + */ + getScrollableNode(): NodeHandle | undefined; +} + +export class SectionList< + ItemT = any, + SectionT = DefaultSectionT, +> extends SectionListComponent> {} + +/* This definition is deprecated because it extends the wrong base type */ +export interface SectionListStatic + extends React.ComponentClass> { + /** + * Scrolls to the item at the specified sectionIndex and itemIndex (within the section) + * positioned in the viewable area such that viewPosition 0 places it at the top + * (and may be covered by a sticky header), 1 at the bottom, and 0.5 centered in the middle. + */ + scrollToLocation?(params: SectionListScrollParams): void; +} diff --git a/packages/flat-lists/Lists/SectionList.js b/packages/flat-lists/Lists/SectionList.js new file mode 100644 index 00000000000000..052b6399ba4487 --- /dev/null +++ b/packages/flat-lists/Lists/SectionList.js @@ -0,0 +1,266 @@ +/** + * 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 + * @format + */ + +import type {ScrollResponderType} from 'react-native/Libraries/Components/ScrollView/ScrollView'; +import type { + ScrollToLocationParamsType, + SectionBase as _SectionBase, + VirtualizedSectionListProps, +} from '@react-native/virtualized-lists'; + +import Platform from 'react-native/Libraries/Utilities/Platform'; +import {VirtualizedSectionList} from '@react-native/virtualized-lists'; +import * as React from 'react'; + +type Item = any; + +export type SectionBase = _SectionBase; + +type RequiredProps> = {| + /** + * The actual data to render, akin to the `data` prop in [``](https://reactnative.dev/docs/flatlist). + * + * General shape: + * + * sections: $ReadOnlyArray<{ + * data: $ReadOnlyArray, + * renderItem?: ({item: SectionItem, ...}) => ?React.Element<*>, + * ItemSeparatorComponent?: ?ReactClass<{highlighted: boolean, ...}>, + * }> + */ + sections: $ReadOnlyArray, +|}; + +type OptionalProps> = {| + /** + * Default renderer for every item in every section. Can be over-ridden on a per-section basis. + */ + renderItem?: (info: { + item: Item, + index: number, + section: SectionT, + separators: { + highlight: () => void, + unhighlight: () => void, + updateProps: (select: 'leading' | 'trailing', newProps: Object) => void, + ... + }, + ... + }) => null | React.Element, + /** + * A marker property for telling the list to re-render (since it implements `PureComponent`). If + * any of your `renderItem`, Header, Footer, etc. functions depend on anything outside of the + * `data` prop, stick it here and treat it immutably. + */ + extraData?: any, + /** + * How many items to render in the initial batch. This should be enough to fill the screen but not + * much more. Note these items will never be unmounted as part of the windowed rendering in order + * to improve perceived performance of scroll-to-top actions. + */ + initialNumToRender?: ?number, + /** + * Reverses the direction of scroll. Uses scale transforms of -1. + */ + inverted?: ?boolean, + /** + * Used to extract a unique key for a given item at the specified index. Key is used for caching + * and as the react key to track item re-ordering. The default extractor checks item.key, then + * falls back to using the index, like react does. Note that this sets keys for each item, but + * each overall section still needs its own key. + */ + keyExtractor?: ?(item: Item, index: number) => string, + /** + * Called once when the scroll position gets within `onEndReachedThreshold` of the rendered + * content. + */ + onEndReached?: ?(info: {distanceFromEnd: number, ...}) => void, + /** + * Note: may have bugs (missing content) in some circumstances - use at your own risk. + * + * This may improve scroll performance for large lists. + */ + removeClippedSubviews?: boolean, +|}; + +export type Props = {| + ...$Diff< + VirtualizedSectionListProps, + { + getItem: $PropertyType, 'getItem'>, + getItemCount: $PropertyType< + VirtualizedSectionListProps, + 'getItemCount', + >, + renderItem: $PropertyType< + VirtualizedSectionListProps, + 'renderItem', + >, + keyExtractor: $PropertyType< + VirtualizedSectionListProps, + 'keyExtractor', + >, + ... + }, + >, + ...RequiredProps, + ...OptionalProps, +|}; + +/** + * A performant interface for rendering sectioned lists, supporting the most handy features: + * + * - Fully cross-platform. + * - Configurable viewability callbacks. + * - List header support. + * - List footer support. + * - Item separator support. + * - Section header support. + * - Section separator support. + * - Heterogeneous data and item rendering support. + * - Pull to Refresh. + * - Scroll loading. + * + * If you don't need section support and want a simpler interface, use + * [``](https://reactnative.dev/docs/flatlist). + * + * Simple Examples: + * + * } + * renderSectionHeader={({section}) =>
} + * sections={[ // homogeneous rendering between sections + * {data: [...], title: ...}, + * {data: [...], title: ...}, + * {data: [...], title: ...}, + * ]} + * /> + * + * + * + * This is a convenience wrapper around [``](docs/virtualizedlist), + * and thus inherits its props (as well as those of `ScrollView`) that aren't explicitly listed + * here, along with the following caveats: + * + * - Internal state is not preserved when content scrolls out of the render window. Make sure all + * your data is captured in the item data or external stores like Flux, Redux, or Relay. + * - This is a `PureComponent` which means that it will not re-render if `props` remain shallow- + * equal. Make sure that everything your `renderItem` function depends on is passed as a prop + * (e.g. `extraData`) that is not `===` after updates, otherwise your UI may not update on + * changes. This includes the `data` prop and parent component state. + * - In order to constrain memory and enable smooth scrolling, content is rendered asynchronously + * offscreen. This means it's possible to scroll faster than the fill rate and momentarily see + * blank content. This is a tradeoff that can be adjusted to suit the needs of each application, + * and we are working on improving it behind the scenes. + * - By default, the list looks for a `key` prop on each item and uses that for the React key. + * Alternatively, you can provide a custom `keyExtractor` prop. + * + */ +class SectionList> extends React.PureComponent< + Props, + void, +> { + props: Props; + + /** + * Scrolls to the item at the specified `sectionIndex` and `itemIndex` (within the section) + * positioned in the viewable area such that `viewPosition` 0 places it at the top (and may be + * covered by a sticky header), 1 at the bottom, and 0.5 centered in the middle. `viewOffset` is a + * fixed number of pixels to offset the final target position, e.g. to compensate for sticky + * headers. + * + * Note: cannot scroll to locations outside the render window without specifying the + * `getItemLayout` prop. + */ + scrollToLocation(params: ScrollToLocationParamsType) { + if (this._wrapperListRef != null) { + this._wrapperListRef.scrollToLocation(params); + } + } + + /** + * Tells the list an interaction has occurred, which should trigger viewability calculations, e.g. + * if `waitForInteractions` is true and the user has not scrolled. This is typically called by + * taps on items or by navigation actions. + */ + recordInteraction() { + const listRef = this._wrapperListRef && this._wrapperListRef.getListRef(); + listRef && listRef.recordInteraction(); + } + + /** + * Displays the scroll indicators momentarily. + * + * @platform ios + */ + flashScrollIndicators() { + const listRef = this._wrapperListRef && this._wrapperListRef.getListRef(); + listRef && listRef.flashScrollIndicators(); + } + + /** + * Provides a handle to the underlying scroll responder. + */ + getScrollResponder(): ?ScrollResponderType { + const listRef = this._wrapperListRef && this._wrapperListRef.getListRef(); + if (listRef) { + return listRef.getScrollResponder(); + } + } + + getScrollableNode(): any { + const listRef = this._wrapperListRef && this._wrapperListRef.getListRef(); + if (listRef) { + return listRef.getScrollableNode(); + } + } + + setNativeProps(props: Object) { + const listRef = this._wrapperListRef && this._wrapperListRef.getListRef(); + if (listRef) { + listRef.setNativeProps(props); + } + } + + render(): React.Node { + const { + stickySectionHeadersEnabled: _stickySectionHeadersEnabled, + ...restProps + } = this.props; + const stickySectionHeadersEnabled = + _stickySectionHeadersEnabled ?? Platform.OS === 'ios'; + return ( + items.length} + // $FlowFixMe[missing-local-annot] + getItem={(items, index) => items[index]} + /> + ); + } + + _wrapperListRef: ?React.ElementRef; + /* $FlowFixMe[missing-local-annot] The type annotation(s) required by Flow's + * LTI update could not be added via codemod */ + _captureRef = ref => { + this._wrapperListRef = ref; + }; +} + +module.exports = SectionList; diff --git a/packages/flat-lists/Lists/SectionListModern.js b/packages/flat-lists/Lists/SectionListModern.js new file mode 100644 index 00000000000000..b71f54c3955b37 --- /dev/null +++ b/packages/flat-lists/Lists/SectionListModern.js @@ -0,0 +1,247 @@ +/** + * 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 + * @format + */ + +import type {ScrollResponderType} from 'react-native/Libraries/Components/ScrollView/ScrollView'; +import type { + ScrollToLocationParamsType, + SectionBase as _SectionBase, + VirtualizedSectionListProps, +} from '@react-native/virtualized-lists'; +import type {AbstractComponent, Element, ElementRef} from 'react'; + +import Platform from 'react-native/Libraries/Utilities/Platform'; +import {VirtualizedSectionList} from '@react-native/virtualized-lists'; +import React, {forwardRef, useImperativeHandle, useRef} from 'react'; + +type Item = any; + +export type SectionBase = _SectionBase; + +type RequiredProps> = {| + /** + * The actual data to render, akin to the `data` prop in [``](https://reactnative.dev/docs/flatlist). + * + * General shape: + * + * sections: $ReadOnlyArray<{ + * data: $ReadOnlyArray, + * renderItem?: ({item: SectionItem, ...}) => ?React.Element<*>, + * ItemSeparatorComponent?: ?ReactClass<{highlighted: boolean, ...}>, + * }> + */ + sections: $ReadOnlyArray, +|}; + +type OptionalProps> = {| + /** + * Default renderer for every item in every section. Can be over-ridden on a per-section basis. + */ + renderItem?: (info: { + item: Item, + index: number, + section: SectionT, + separators: { + highlight: () => void, + unhighlight: () => void, + updateProps: (select: 'leading' | 'trailing', newProps: Object) => void, + ... + }, + ... + }) => null | Element, + /** + * A marker property for telling the list to re-render (since it implements `PureComponent`). If + * any of your `renderItem`, Header, Footer, etc. functions depend on anything outside of the + * `data` prop, stick it here and treat it immutably. + */ + extraData?: any, + /** + * How many items to render in the initial batch. This should be enough to fill the screen but not + * much more. Note these items will never be unmounted as part of the windowed rendering in order + * to improve perceived performance of scroll-to-top actions. + */ + initialNumToRender?: ?number, + /** + * Reverses the direction of scroll. Uses scale transforms of -1. + */ + inverted?: ?boolean, + /** + * Used to extract a unique key for a given item at the specified index. Key is used for caching + * and as the react key to track item re-ordering. The default extractor checks item.key, then + * falls back to using the index, like react does. Note that this sets keys for each item, but + * each overall section still needs its own key. + */ + keyExtractor?: ?(item: Item, index: number) => string, + /** + * Called once when the scroll position gets within `onEndReachedThreshold` of the rendered + * content. + */ + onEndReached?: ?(info: {distanceFromEnd: number, ...}) => void, + /** + * Note: may have bugs (missing content) in some circumstances - use at your own risk. + * + * This may improve scroll performance for large lists. + */ + removeClippedSubviews?: boolean, +|}; + +export type Props = {| + ...$Diff< + VirtualizedSectionListProps, + { + getItem: $PropertyType, 'getItem'>, + getItemCount: $PropertyType< + VirtualizedSectionListProps, + 'getItemCount', + >, + renderItem: $PropertyType< + VirtualizedSectionListProps, + 'renderItem', + >, + keyExtractor: $PropertyType< + VirtualizedSectionListProps, + 'keyExtractor', + >, + ... + }, + >, + ...RequiredProps, + ...OptionalProps, +|}; + +/** + * A performant interface for rendering sectioned lists, supporting the most handy features: + * + * - Fully cross-platform. + * - Configurable viewability callbacks. + * - List header support. + * - List footer support. + * - Item separator support. + * - Section header support. + * - Section separator support. + * - Heterogeneous data and item rendering support. + * - Pull to Refresh. + * - Scroll loading. + * + * If you don't need section support and want a simpler interface, use + * [``](https://reactnative.dev/docs/flatlist). + * + * Simple Examples: + * + * } + * renderSectionHeader={({section}) =>
} + * sections={[ // homogeneous rendering between sections + * {data: [...], title: ...}, + * {data: [...], title: ...}, + * {data: [...], title: ...}, + * ]} + * /> + * + * + * + * This is a convenience wrapper around [``](docs/virtualizedlist), + * and thus inherits its props (as well as those of `ScrollView`) that aren't explicitly listed + * here, along with the following caveats: + * + * - Internal state is not preserved when content scrolls out of the render window. Make sure all + * your data is captured in the item data or external stores like Flux, Redux, or Relay. + * - This is a `PureComponent` which means that it will not re-render if `props` remain shallow- + * equal. Make sure that everything your `renderItem` function depends on is passed as a prop + * (e.g. `extraData`) that is not `===` after updates, otherwise your UI may not update on + * changes. This includes the `data` prop and parent component state. + * - In order to constrain memory and enable smooth scrolling, content is rendered asynchronously + * offscreen. This means it's possible to scroll faster than the fill rate and momentarily see + * blank content. This is a tradeoff that can be adjusted to suit the needs of each application, + * and we are working on improving it behind the scenes. + * - By default, the list looks for a `key` prop on each item and uses that for the React key. + * Alternatively, you can provide a custom `keyExtractor` prop. + * + */ +const SectionList: AbstractComponent>, any> = forwardRef< + Props>, + any, +>((props, ref) => { + const propsWithDefaults = { + stickySectionHeadersEnabled: Platform.OS === 'ios', + ...props, + }; + + const wrapperRef = useRef>(); + + useImperativeHandle( + ref, + () => ({ + /** + * Scrolls to the item at the specified `sectionIndex` and `itemIndex` (within the section) + * positioned in the viewable area such that `viewPosition` 0 places it at the top (and may be + * covered by a sticky header), 1 at the bottom, and 0.5 centered in the middle. `viewOffset` is a + * fixed number of pixels to offset the final target position, e.g. to compensate for sticky + * headers. + * + * Note: cannot scroll to locations outside the render window without specifying the + * `getItemLayout` prop. + */ + scrollToLocation(params: ScrollToLocationParamsType) { + wrapperRef.current?.scrollToLocation(params); + }, + + /** + * Tells the list an interaction has occurred, which should trigger viewability calculations, e.g. + * if `waitForInteractions` is true and the user has not scrolled. This is typically called by + * taps on items or by navigation actions. + */ + recordInteraction() { + wrapperRef.current?.getListRef()?.recordInteraction(); + }, + + /** + * Displays the scroll indicators momentarily. + * + * @platform ios + */ + flashScrollIndicators() { + wrapperRef.current?.getListRef()?.flashScrollIndicators(); + }, + + /** + * Provides a handle to the underlying scroll responder. + */ + getScrollResponder(): ?ScrollResponderType { + wrapperRef.current?.getListRef()?.getScrollResponder(); + }, + + getScrollableNode(): any { + wrapperRef.current?.getListRef()?.getScrollableNode(); + }, + + setNativeProps(nativeProps: Object) { + wrapperRef.current?.getListRef()?.setNativeProps(nativeProps); + }, + }), + [wrapperRef], + ); + + return ( + items.length} + getItem={(items, index) => items[index]} + /> + ); +}); + +module.exports = SectionList; diff --git a/packages/react-native/Libraries/Lists/__flowtests__/FlatList-flowtest.js b/packages/flat-lists/Lists/__flowtests__/FlatList-flowtest.js similarity index 100% rename from packages/react-native/Libraries/Lists/__flowtests__/FlatList-flowtest.js rename to packages/flat-lists/Lists/__flowtests__/FlatList-flowtest.js diff --git a/packages/react-native/Libraries/Lists/__flowtests__/SectionList-flowtest.js b/packages/flat-lists/Lists/__flowtests__/SectionList-flowtest.js similarity index 100% rename from packages/react-native/Libraries/Lists/__flowtests__/SectionList-flowtest.js rename to packages/flat-lists/Lists/__flowtests__/SectionList-flowtest.js index 9f878d89b0607d..0860556f489b29 100644 --- a/packages/react-native/Libraries/Lists/__flowtests__/SectionList-flowtest.js +++ b/packages/flat-lists/Lists/__flowtests__/SectionList-flowtest.js @@ -10,8 +10,8 @@ 'use strict'; -import SectionList from '../SectionList'; import * as React from 'react'; +import SectionList from '../SectionList'; function renderMyListItem(info: { item: {title: string, ...}, diff --git a/packages/react-native/Libraries/Lists/__tests__/FlatList-test.js b/packages/flat-lists/Lists/__tests__/FlatList-test.js similarity index 97% rename from packages/react-native/Libraries/Lists/__tests__/FlatList-test.js rename to packages/flat-lists/Lists/__tests__/FlatList-test.js index e9f9197b2c5038..45f2f228e303d4 100644 --- a/packages/react-native/Libraries/Lists/__tests__/FlatList-test.js +++ b/packages/flat-lists/Lists/__tests__/FlatList-test.js @@ -94,7 +94,7 @@ describe('FlatList', () => { }); it('getNativeScrollRef for case where it returns a native view', () => { jest.resetModules(); - jest.unmock('../../Components/ScrollView/ScrollView'); + jest.unmock('react-native/Libraries/Components/ScrollView/ScrollView'); const listRef = React.createRef(null); @@ -127,7 +127,7 @@ describe('FlatList', () => { it('getNativeScrollRef for case where it returns a native scroll view', () => { jest.resetModules(); - jest.unmock('../../Components/ScrollView/ScrollView'); + jest.unmock('react-native/Libraries/Components/ScrollView/ScrollView'); function ListItemComponent({item}) { return ; diff --git a/packages/react-native/Libraries/Lists/__tests__/SectionList-test.js b/packages/flat-lists/Lists/__tests__/SectionList-test.js similarity index 100% rename from packages/react-native/Libraries/Lists/__tests__/SectionList-test.js rename to packages/flat-lists/Lists/__tests__/SectionList-test.js index 9eb6e340e30d16..2064f2e50f7e43 100644 --- a/packages/react-native/Libraries/Lists/__tests__/SectionList-test.js +++ b/packages/flat-lists/Lists/__tests__/SectionList-test.js @@ -10,8 +10,8 @@ 'use strict'; -import SectionList from '../SectionList'; import * as React from 'react'; +import SectionList from '../SectionList'; import ReactTestRenderer from 'react-test-renderer'; describe('SectionList', () => { diff --git a/packages/react-native/Libraries/Lists/__tests__/__snapshots__/FlatList-test.js.snap b/packages/flat-lists/Lists/__tests__/__snapshots__/FlatList-test.js.snap similarity index 100% rename from packages/react-native/Libraries/Lists/__tests__/__snapshots__/FlatList-test.js.snap rename to packages/flat-lists/Lists/__tests__/__snapshots__/FlatList-test.js.snap diff --git a/packages/react-native/Libraries/Lists/__tests__/__snapshots__/SectionList-test.js.snap b/packages/flat-lists/Lists/__tests__/__snapshots__/SectionList-test.js.snap similarity index 100% rename from packages/react-native/Libraries/Lists/__tests__/__snapshots__/SectionList-test.js.snap rename to packages/flat-lists/Lists/__tests__/__snapshots__/SectionList-test.js.snap diff --git a/packages/flat-lists/README.md b/packages/flat-lists/README.md new file mode 100644 index 00000000000000..69c86d78a77457 --- /dev/null +++ b/packages/flat-lists/README.md @@ -0,0 +1,32 @@ +# [@react-native/flat-lists](https://github.com/facebook/react-native/tree/HEAD/packages/flat-lists) + +[![Version][version-badge]][package] + +⚛️ Flat lists for React Native + +## Installation + +```bash +yarn add @react-native/flat-lists +``` + +*Note: We're using `yarn` to install deps. Feel free to change commands to use `npm` 3+ and `npx` if you like* + +[version-badge]: https://img.shields.io/npm/v/@react-native/flat-lists?style=flat-square +[package]: https://www.npmjs.com/package/@react-native/flat-lists + +## Testing + +To run the tests in this package, run the following commands from the react Native root folder: + +1. `yarn` to install the dependencies. You just need to run this once. +2. `yarn jest flat-lists` to run the tests. + +## 📄 License + +Flat lists for React Native is MIT licensed, as found in the [LICENSE][l] file. + +React Native documentation is Creative Commons licensed, as found in the [LICENSE-docs][ld] file. + +[l]: https://github.com/facebook/react-native/blob/HEAD/LICENSE +[ld]: https://github.com/facebook/react-native/blob/HEAD/LICENSE-docs diff --git a/packages/flat-lists/index.d.ts b/packages/flat-lists/index.d.ts new file mode 100644 index 00000000000000..a166843bd859eb --- /dev/null +++ b/packages/flat-lists/index.d.ts @@ -0,0 +1,11 @@ +/** + * 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. + * + * @format + */ + +export * from './Lists/FlatList'; +export * from './Lists/SectionList'; diff --git a/packages/flat-lists/index.js b/packages/flat-lists/index.js new file mode 100644 index 00000000000000..d896b7c63c3714 --- /dev/null +++ b/packages/flat-lists/index.js @@ -0,0 +1,29 @@ +/** + * 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 + * @format + */ + +'use strict'; + +import typeof FlatList from './Lists/FlatList'; +import typeof SectionList from './Lists/SectionList'; + +export type {Props as FlatListProps} from './Lists/FlatList'; +export type {Props as SectionListProps, SectionBase} from './Lists/SectionList'; + +module.exports = { + get FlatList(): FlatList { + return require('./Lists/FlatList'); + }, + get SectionList(): SectionList { + return require('./Lists/SectionList'); + }, + get SectionListModern(): $FlowFixMe { + return require('./Lists/SectionListModern'); + }, +}; diff --git a/packages/flat-lists/package.json b/packages/flat-lists/package.json new file mode 100644 index 00000000000000..63b5ddd632b279 --- /dev/null +++ b/packages/flat-lists/package.json @@ -0,0 +1,27 @@ +{ + "name": "@react-native/flat-lists", + "version": "0.72.2", + "description": "Flat lists for React Native.", + "homepage": "https://github.com/facebook/react-native/tree/HEAD/packages/flat-lists", + "repository": { + "type": "git", + "url": "git@github.com:facebook/react-native.git", + "directory": "packages/flat-lists" + }, + "scripts": { + "test": "jest" + }, + "license": "MIT", + "dependencies": { + "@react-native/virtualized-lists": "^0.72.2", + "invariant": "^2.2.4", + "memoize-one": "^5.0.0" + }, + "devDependencies": { + "react-test-renderer": "18.2.0" + }, + "peerDependencies": { + "react-native": "*", + "react-test-renderer": "18.2.0" + } + } diff --git a/packages/react-native/Libraries/Lists/FlatList.d.ts b/packages/react-native/Libraries/Lists/FlatList.d.ts index b6b28675a7c6e1..4de5e6a658547f 100644 --- a/packages/react-native/Libraries/Lists/FlatList.d.ts +++ b/packages/react-native/Libraries/Lists/FlatList.d.ts @@ -7,238 +7,8 @@ * @format */ -import type * as React from 'react'; -import type { - ListRenderItem, - ViewToken, - VirtualizedListProps, -} from '@react-native/virtualized-lists'; -import type {ScrollViewComponent} from '../Components/ScrollView/ScrollView'; -import type {StyleProp} from '../StyleSheet/StyleSheet'; -import type {ViewStyle} from '../StyleSheet/StyleSheetTypes'; -import type {View} from '../Components/View/View'; - -export interface FlatListProps extends VirtualizedListProps { - /** - * Optional custom style for multi-item rows generated when numColumns > 1 - */ - columnWrapperStyle?: StyleProp | undefined; - - /** - * Determines when the keyboard should stay visible after a tap. - * - 'never' (the default), tapping outside of the focused text input when the keyboard is up dismisses the keyboard. When this happens, children won't receive the tap. - * - 'always', the keyboard will not dismiss automatically, and the scroll view will not catch taps, but children of the scroll view can catch taps. - * - 'handled', the keyboard will not dismiss automatically when the tap was handled by a children, (or captured by an ancestor). - * - false, deprecated, use 'never' instead - * - true, deprecated, use 'always' instead - */ - keyboardShouldPersistTaps?: - | boolean - | 'always' - | 'never' - | 'handled' - | undefined; - - /** - * An array (or array-like list) of items to render. Other data types can be - * used by targeting VirtualizedList directly. - */ - data: ArrayLike | null | undefined; - - /** - * A marker property for telling the list to re-render (since it implements PureComponent). - * If any of your `renderItem`, Header, Footer, etc. functions depend on anything outside of the `data` prop, - * stick it here and treat it immutably. - */ - extraData?: any | undefined; - - /** - * `getItemLayout` is an optional optimization that lets us skip measurement of dynamic - * content if you know the height of items a priori. getItemLayout is the most efficient, - * and is easy to use if you have fixed height items, for example: - * ``` - * getItemLayout={(data, index) => ( - * {length: ITEM_HEIGHT, offset: ITEM_HEIGHT * index, index} - * )} - * ``` - * Remember to include separator length (height or width) in your offset calculation if you specify - * `ItemSeparatorComponent`. - */ - getItemLayout?: - | (( - data: ArrayLike | null | undefined, - index: number, - ) => {length: number; offset: number; index: number}) - | undefined; - - /** - * If true, renders items next to each other horizontally instead of stacked vertically. - */ - horizontal?: boolean | null | undefined; - - /** - * How many items to render in the initial batch - */ - initialNumToRender?: number | undefined; - - /** - * Instead of starting at the top with the first item, start at initialScrollIndex - */ - initialScrollIndex?: number | null | undefined; - - /** - * Used to extract a unique key for a given item at the specified index. Key is used for caching - * and as the react key to track item re-ordering. The default extractor checks `item.key`, then - * falls back to using the index, like React does. - */ - keyExtractor?: ((item: ItemT, index: number) => string) | undefined; - - /** - * Uses legacy MetroListView instead of default VirtualizedSectionList - */ - legacyImplementation?: boolean | undefined; - - /** - * Multiple columns can only be rendered with `horizontal={false}` and will zig-zag like a `flexWrap` layout. - * Items should all be the same height - masonry layouts are not supported. - */ - numColumns?: number | undefined; - - /** - * If provided, a standard RefreshControl will be added for "Pull to Refresh" functionality. - * Make sure to also set the refreshing prop correctly. - */ - onRefresh?: (() => void) | null | undefined; - - /** - * Called when the viewability of rows changes, as defined by the `viewablePercentThreshold` prop. - */ - onViewableItemsChanged?: - | ((info: { - viewableItems: Array; - changed: Array; - }) => void) - | null - | undefined; - - /** - * Set this true while waiting for new data from a refresh. - */ - refreshing?: boolean | null | undefined; - - /** - * Takes an item from data and renders it into the list. Typical usage: - * ``` - * _renderItem = ({item}) => ( - * this._onPress(item)}> - * {item.title} - * - * ); - * ... - * - * ``` - * Provides additional metadata like `index` if you need it. - */ - renderItem: ListRenderItem | null | undefined; - - /** - * See `ViewabilityHelper` for flow type and further documentation. - */ - viewabilityConfig?: any | undefined; - - /** - * Note: may have bugs (missing content) in some circumstances - use at your own risk. - * - * This may improve scroll performance for large lists. - */ - removeClippedSubviews?: boolean | undefined; - - /** - * Fades out the edges of the scroll content. - * - * If the value is greater than 0, the fading edges will be set accordingly - * to the current scroll direction and position, - * indicating if there is more content to show. - * - * The default value is 0. - * @platform android - */ - fadingEdgeLength?: number | undefined; -} - -export abstract class FlatListComponent< - ItemT, - Props, -> extends React.Component { - /** - * Scrolls to the end of the content. May be janky without `getItemLayout` prop. - */ - scrollToEnd: (params?: {animated?: boolean | null | undefined}) => void; - - /** - * Scrolls to the item at the specified index such that it is positioned in the viewable area - * such that viewPosition 0 places it at the top, 1 at the bottom, and 0.5 centered in the middle. - * Cannot scroll to locations outside the render window without specifying the getItemLayout prop. - */ - scrollToIndex: (params: { - animated?: boolean | null | undefined; - index: number; - viewOffset?: number | undefined; - viewPosition?: number | undefined; - }) => void; - - /** - * Requires linear scan through data - use `scrollToIndex` instead if possible. - * May be janky without `getItemLayout` prop. - */ - scrollToItem: (params: { - animated?: boolean | null | undefined; - item: ItemT; - viewOffset?: number | undefined; - viewPosition?: number | undefined; - }) => void; - - /** - * Scroll to a specific content pixel offset, like a normal `ScrollView`. - */ - scrollToOffset: (params: { - animated?: boolean | null | undefined; - offset: number; - }) => void; - - /** - * Tells the list an interaction has occurred, which should trigger viewability calculations, - * e.g. if waitForInteractions is true and the user has not scrolled. This is typically called - * by taps on items or by navigation actions. - */ - recordInteraction: () => void; - - /** - * Displays the scroll indicators momentarily. - */ - flashScrollIndicators: () => void; - - /** - * Provides a handle to the underlying scroll responder. - */ - getScrollResponder: () => JSX.Element | null | undefined; - - /** - * Provides a reference to the underlying host component - */ - getNativeScrollRef: () => - | React.ElementRef - | React.ElementRef - | null - | undefined; - - getScrollableNode: () => any; - - // TODO: use `unknown` instead of `any` for Typescript >= 3.0 - setNativeProps: (props: {[key: string]: any}) => void; -} - -export class FlatList extends FlatListComponent< - ItemT, - FlatListProps -> {} +export { + FlatList, + FlatListProps, + FlatListComponent, +} from '@react-native/flat-lists'; diff --git a/packages/react-native/Libraries/Lists/FlatList.js b/packages/react-native/Libraries/Lists/FlatList.js index ddb929e6a80f4a..131e34957735fd 100644 --- a/packages/react-native/Libraries/Lists/FlatList.js +++ b/packages/react-native/Libraries/Lists/FlatList.js @@ -8,696 +8,12 @@ * @format */ -import typeof ScrollViewNativeComponent from '../Components/ScrollView/ScrollViewNativeComponent'; -import type {ViewStyleProp} from '../StyleSheet/StyleSheet'; -import type { - RenderItemProps, - RenderItemType, - ViewabilityConfigCallbackPair, - ViewToken, -} from '@react-native/virtualized-lists'; +'use strict'; -import {type ScrollResponderType} from '../Components/ScrollView/ScrollView'; -import { - VirtualizedList, - keyExtractor as defaultKeyExtractor, -} from '@react-native/virtualized-lists'; -import memoizeOne from 'memoize-one'; +import {typeof FlatList as FlatListType} from '@react-native/flat-lists'; -const View = require('../Components/View/View'); -const StyleSheet = require('../StyleSheet/StyleSheet'); -const deepDiffer = require('../Utilities/differ/deepDiffer'); -const Platform = require('../Utilities/Platform'); -const invariant = require('invariant'); -const React = require('react'); +const FlatList: FlatListType = require('@react-native/flat-lists').FlatList; -type RequiredProps = {| - /** - * An array (or array-like list) of items to render. Other data types can be - * used by targeting VirtualizedList directly. - */ - data: ?$ArrayLike, -|}; -type OptionalProps = {| - /** - * Takes an item from `data` and renders it into the list. Example usage: - * - * ( - * - * )} - * data={[{title: 'Title Text', key: 'item1'}]} - * renderItem={({item, separators}) => ( - * this._onPress(item)} - * onShowUnderlay={separators.highlight} - * onHideUnderlay={separators.unhighlight}> - * - * {item.title} - * - * - * )} - * /> - * - * Provides additional metadata like `index` if you need it, as well as a more generic - * `separators.updateProps` function which let's you set whatever props you want to change the - * rendering of either the leading separator or trailing separator in case the more common - * `highlight` and `unhighlight` (which set the `highlighted: boolean` prop) are insufficient for - * your use-case. - */ - renderItem?: ?RenderItemType, - - /** - * Optional custom style for multi-item rows generated when numColumns > 1. - */ - columnWrapperStyle?: ViewStyleProp, - /** - * A marker property for telling the list to re-render (since it implements `PureComponent`). If - * any of your `renderItem`, Header, Footer, etc. functions depend on anything outside of the - * `data` prop, stick it here and treat it immutably. - */ - extraData?: any, - /** - * `getItemLayout` is an optional optimizations that let us skip measurement of dynamic content if - * you know the height of items a priori. `getItemLayout` is the most efficient, and is easy to - * use if you have fixed height items, for example: - * - * getItemLayout={(data, index) => ( - * {length: ITEM_HEIGHT, offset: ITEM_HEIGHT * index, index} - * )} - * - * Adding `getItemLayout` can be a great performance boost for lists of several hundred items. - * Remember to include separator length (height or width) in your offset calculation if you - * specify `ItemSeparatorComponent`. - */ - getItemLayout?: ( - data: ?$ArrayLike, - index: number, - ) => { - length: number, - offset: number, - index: number, - ... - }, - /** - * If true, renders items next to each other horizontally instead of stacked vertically. - */ - horizontal?: ?boolean, - /** - * How many items to render in the initial batch. This should be enough to fill the screen but not - * much more. Note these items will never be unmounted as part of the windowed rendering in order - * to improve perceived performance of scroll-to-top actions. - */ - initialNumToRender?: ?number, - /** - * Instead of starting at the top with the first item, start at `initialScrollIndex`. This - * disables the "scroll to top" optimization that keeps the first `initialNumToRender` items - * always rendered and immediately renders the items starting at this initial index. Requires - * `getItemLayout` to be implemented. - */ - initialScrollIndex?: ?number, - /** - * Reverses the direction of scroll. Uses scale transforms of -1. - */ - inverted?: ?boolean, - /** - * Used to extract a unique key for a given item at the specified index. Key is used for caching - * and as the react key to track item re-ordering. The default extractor checks `item.key`, then - * falls back to using the index, like React does. - */ - keyExtractor?: ?(item: ItemT, index: number) => string, - /** - * Multiple columns can only be rendered with `horizontal={false}` and will zig-zag like a - * `flexWrap` layout. Items should all be the same height - masonry layouts are not supported. - * - * The default value is 1. - */ - numColumns?: number, - /** - * Note: may have bugs (missing content) in some circumstances - use at your own risk. - * - * This may improve scroll performance for large lists. - * - * The default value is true for Android. - */ - removeClippedSubviews?: boolean, - /** - * See `ScrollView` for flow type and further documentation. - */ - fadingEdgeLength?: ?number, - /** - * Enable an optimization to memoize the item renderer to prevent unnecessary rerenders. - */ - strictMode?: boolean, -|}; - -/** - * Default Props Helper Functions - * Use the following helper functions for default values - */ - -// removeClippedSubviewsOrDefault(this.props.removeClippedSubviews) -function removeClippedSubviewsOrDefault(removeClippedSubviews: ?boolean) { - return removeClippedSubviews ?? Platform.OS === 'android'; -} - -// numColumnsOrDefault(this.props.numColumns) -function numColumnsOrDefault(numColumns: ?number) { - return numColumns ?? 1; -} - -function isArrayLike(data: mixed): boolean { - // $FlowExpectedError[incompatible-use] - return typeof Object(data).length === 'number'; -} - -type FlatListProps = {| - ...RequiredProps, - ...OptionalProps, -|}; - -type VirtualizedListProps = React.ElementConfig; - -export type Props = { - ...$Diff< - VirtualizedListProps, - { - getItem: $PropertyType, - getItemCount: $PropertyType, - getItemLayout: $PropertyType, - renderItem: $PropertyType, - keyExtractor: $PropertyType, - ... - }, - >, - ...FlatListProps, - ... -}; - -/** - * A performant interface for rendering simple, flat lists, supporting the most handy features: - * - * - Fully cross-platform. - * - Optional horizontal mode. - * - Configurable viewability callbacks. - * - Header support. - * - Footer support. - * - Separator support. - * - Pull to Refresh. - * - Scroll loading. - * - ScrollToIndex support. - * - * If you need section support, use [``](docs/sectionlist.html). - * - * Minimal Example: - * - * {item.key}} - * /> - * - * More complex, multi-select example demonstrating `PureComponent` usage for perf optimization and avoiding bugs. - * - * - By binding the `onPressItem` handler, the props will remain `===` and `PureComponent` will - * prevent wasteful re-renders unless the actual `id`, `selected`, or `title` props change, even - * if the components rendered in `MyListItem` did not have such optimizations. - * - By passing `extraData={this.state}` to `FlatList` we make sure `FlatList` itself will re-render - * when the `state.selected` changes. Without setting this prop, `FlatList` would not know it - * needs to re-render any items because it is also a `PureComponent` and the prop comparison will - * not show any changes. - * - `keyExtractor` tells the list to use the `id`s for the react keys instead of the default `key` property. - * - * - * class MyListItem extends React.PureComponent { - * _onPress = () => { - * this.props.onPressItem(this.props.id); - * }; - * - * render() { - * const textColor = this.props.selected ? "red" : "black"; - * return ( - * - * - * - * {this.props.title} - * - * - * - * ); - * } - * } - * - * class MultiSelectList extends React.PureComponent { - * state = {selected: (new Map(): Map)}; - * - * _keyExtractor = (item, index) => item.id; - * - * _onPressItem = (id: string) => { - * // updater functions are preferred for transactional updates - * this.setState((state) => { - * // copy the map rather than modifying state. - * const selected = new Map(state.selected); - * selected.set(id, !selected.get(id)); // toggle - * return {selected}; - * }); - * }; - * - * _renderItem = ({item}) => ( - * - * ); - * - * render() { - * return ( - * - * ); - * } - * } - * - * This is a convenience wrapper around [``](docs/virtualizedlist.html), - * and thus inherits its props (as well as those of `ScrollView`) that aren't explicitly listed - * here, along with the following caveats: - * - * - Internal state is not preserved when content scrolls out of the render window. Make sure all - * your data is captured in the item data or external stores like Flux, Redux, or Relay. - * - This is a `PureComponent` which means that it will not re-render if `props` remain shallow- - * equal. Make sure that everything your `renderItem` function depends on is passed as a prop - * (e.g. `extraData`) that is not `===` after updates, otherwise your UI may not update on - * changes. This includes the `data` prop and parent component state. - * - In order to constrain memory and enable smooth scrolling, content is rendered asynchronously - * offscreen. This means it's possible to scroll faster than the fill rate ands momentarily see - * blank content. This is a tradeoff that can be adjusted to suit the needs of each application, - * and we are working on improving it behind the scenes. - * - By default, the list looks for a `key` prop on each item and uses that for the React key. - * Alternatively, you can provide a custom `keyExtractor` prop. - * - * Also inherits [ScrollView Props](docs/scrollview.html#props), unless it is nested in another FlatList of same orientation. - */ -class FlatList extends React.PureComponent, void> { - props: Props; - /** - * Scrolls to the end of the content. May be janky without `getItemLayout` prop. - */ - scrollToEnd(params?: ?{animated?: ?boolean, ...}) { - if (this._listRef) { - this._listRef.scrollToEnd(params); - } - } - - /** - * Scrolls to the item at the specified index such that it is positioned in the viewable area - * such that `viewPosition` 0 places it at the top, 1 at the bottom, and 0.5 centered in the - * middle. `viewOffset` is a fixed number of pixels to offset the final target position. - * - * Note: cannot scroll to locations outside the render window without specifying the - * `getItemLayout` prop. - */ - scrollToIndex(params: { - animated?: ?boolean, - index: number, - viewOffset?: number, - viewPosition?: number, - ... - }) { - if (this._listRef) { - this._listRef.scrollToIndex(params); - } - } - - /** - * Requires linear scan through data - use `scrollToIndex` instead if possible. - * - * Note: cannot scroll to locations outside the render window without specifying the - * `getItemLayout` prop. - */ - scrollToItem(params: { - animated?: ?boolean, - item: ItemT, - viewOffset?: number, - viewPosition?: number, - ... - }) { - if (this._listRef) { - this._listRef.scrollToItem(params); - } - } - - /** - * Scroll to a specific content pixel offset in the list. - * - * Check out [scrollToOffset](docs/virtualizedlist.html#scrolltooffset) of VirtualizedList - */ - scrollToOffset(params: {animated?: ?boolean, offset: number, ...}) { - if (this._listRef) { - this._listRef.scrollToOffset(params); - } - } - - /** - * Tells the list an interaction has occurred, which should trigger viewability calculations, e.g. - * if `waitForInteractions` is true and the user has not scrolled. This is typically called by - * taps on items or by navigation actions. - */ - recordInteraction() { - if (this._listRef) { - this._listRef.recordInteraction(); - } - } - - /** - * Displays the scroll indicators momentarily. - * - * @platform ios - */ - flashScrollIndicators() { - if (this._listRef) { - this._listRef.flashScrollIndicators(); - } - } - - /** - * Provides a handle to the underlying scroll responder. - */ - getScrollResponder(): ?ScrollResponderType { - if (this._listRef) { - return this._listRef.getScrollResponder(); - } - } - - /** - * Provides a reference to the underlying host component - */ - getNativeScrollRef(): - | ?React.ElementRef - | ?React.ElementRef { - if (this._listRef) { - /* $FlowFixMe[incompatible-return] Suppresses errors found when fixing - * TextInput typing */ - return this._listRef.getScrollRef(); - } - } - - getScrollableNode(): any { - if (this._listRef) { - return this._listRef.getScrollableNode(); - } - } - - setNativeProps(props: {[string]: mixed, ...}) { - if (this._listRef) { - this._listRef.setNativeProps(props); - } - } - - constructor(props: Props) { - super(props); - this._checkProps(this.props); - if (this.props.viewabilityConfigCallbackPairs) { - this._virtualizedListPairs = - this.props.viewabilityConfigCallbackPairs.map(pair => ({ - viewabilityConfig: pair.viewabilityConfig, - onViewableItemsChanged: this._createOnViewableItemsChanged( - pair.onViewableItemsChanged, - ), - })); - } else if (this.props.onViewableItemsChanged) { - this._virtualizedListPairs.push({ - /* $FlowFixMe[incompatible-call] (>=0.63.0 site=react_native_fb) This - * comment suppresses an error found when Flow v0.63 was deployed. To - * see the error delete this comment and run Flow. */ - viewabilityConfig: this.props.viewabilityConfig, - onViewableItemsChanged: this._createOnViewableItemsChanged( - this.props.onViewableItemsChanged, - ), - }); - } - } - - // $FlowFixMe[missing-local-annot] - componentDidUpdate(prevProps: Props) { - invariant( - prevProps.numColumns === this.props.numColumns, - 'Changing numColumns on the fly is not supported. Change the key prop on FlatList when ' + - 'changing the number of columns to force a fresh render of the component.', - ); - invariant( - prevProps.onViewableItemsChanged === this.props.onViewableItemsChanged, - 'Changing onViewableItemsChanged on the fly is not supported', - ); - invariant( - !deepDiffer(prevProps.viewabilityConfig, this.props.viewabilityConfig), - 'Changing viewabilityConfig on the fly is not supported', - ); - invariant( - prevProps.viewabilityConfigCallbackPairs === - this.props.viewabilityConfigCallbackPairs, - 'Changing viewabilityConfigCallbackPairs on the fly is not supported', - ); - - this._checkProps(this.props); - } - - _listRef: ?React.ElementRef; - _virtualizedListPairs: Array = []; - - _captureRef = (ref: ?React.ElementRef) => { - this._listRef = ref; - }; - - // $FlowFixMe[missing-local-annot] - _checkProps(props: Props) { - const { - // $FlowFixMe[prop-missing] this prop doesn't exist, is only used for an invariant - getItem, - // $FlowFixMe[prop-missing] this prop doesn't exist, is only used for an invariant - getItemCount, - horizontal, - columnWrapperStyle, - onViewableItemsChanged, - viewabilityConfigCallbackPairs, - } = props; - const numColumns = numColumnsOrDefault(this.props.numColumns); - invariant( - !getItem && !getItemCount, - 'FlatList does not support custom data formats.', - ); - if (numColumns > 1) { - invariant(!horizontal, 'numColumns does not support horizontal.'); - } else { - invariant( - !columnWrapperStyle, - 'columnWrapperStyle not supported for single column lists', - ); - } - invariant( - !(onViewableItemsChanged && viewabilityConfigCallbackPairs), - 'FlatList does not support setting both onViewableItemsChanged and ' + - 'viewabilityConfigCallbackPairs.', - ); - } - - _getItem = ( - data: $ArrayLike, - index: number, - ): ?(ItemT | $ReadOnlyArray) => { - const numColumns = numColumnsOrDefault(this.props.numColumns); - if (numColumns > 1) { - const ret = []; - for (let kk = 0; kk < numColumns; kk++) { - const itemIndex = index * numColumns + kk; - if (itemIndex < data.length) { - const item = data[itemIndex]; - ret.push(item); - } - } - return ret; - } else { - return data[index]; - } - }; - - _getItemCount = (data: ?$ArrayLike): number => { - // Legacy behavior of FlatList was to forward "undefined" length if invalid - // data like a non-arraylike object is passed. VirtualizedList would then - // coerce this, and the math would work out to no-op. For compatibility, if - // invalid data is passed, we tell VirtualizedList there are zero items - // available to prevent it from trying to read from the invalid data - // (without propagating invalidly typed data). - if (data != null && isArrayLike(data)) { - const numColumns = numColumnsOrDefault(this.props.numColumns); - return numColumns > 1 ? Math.ceil(data.length / numColumns) : data.length; - } else { - return 0; - } - }; - - _keyExtractor = (items: ItemT | Array, index: number): string => { - const numColumns = numColumnsOrDefault(this.props.numColumns); - const keyExtractor = this.props.keyExtractor ?? defaultKeyExtractor; - - if (numColumns > 1) { - invariant( - Array.isArray(items), - 'FlatList: Encountered internal consistency error, expected each item to consist of an ' + - 'array with 1-%s columns; instead, received a single item.', - numColumns, - ); - return items - .map((item, kk) => - keyExtractor(((item: $FlowFixMe): ItemT), index * numColumns + kk), - ) - .join(':'); - } - - // $FlowFixMe[incompatible-call] Can't call keyExtractor with an array - return keyExtractor(items, index); - }; - - _pushMultiColumnViewable(arr: Array, v: ViewToken): void { - const numColumns = numColumnsOrDefault(this.props.numColumns); - const keyExtractor = this.props.keyExtractor ?? defaultKeyExtractor; - v.item.forEach((item, ii) => { - invariant(v.index != null, 'Missing index!'); - const index = v.index * numColumns + ii; - arr.push({...v, item, key: keyExtractor(item, index), index}); - }); - } - - _createOnViewableItemsChanged( - onViewableItemsChanged: ?(info: { - viewableItems: Array, - changed: Array, - ... - }) => void, - // $FlowFixMe[missing-local-annot] - ) { - return (info: { - viewableItems: Array, - changed: Array, - ... - }) => { - const numColumns = numColumnsOrDefault(this.props.numColumns); - if (onViewableItemsChanged) { - if (numColumns > 1) { - const changed: Array = []; - const viewableItems: Array = []; - info.viewableItems.forEach(v => - this._pushMultiColumnViewable(viewableItems, v), - ); - info.changed.forEach(v => this._pushMultiColumnViewable(changed, v)); - onViewableItemsChanged({viewableItems, changed}); - } else { - onViewableItemsChanged(info); - } - } - }; - } - - _renderer = ( - ListItemComponent: ?(React.ComponentType | React.Element), - renderItem: ?RenderItemType, - columnWrapperStyle: ?ViewStyleProp, - numColumns: ?number, - extraData: ?any, - // $FlowFixMe[missing-local-annot] - ) => { - const cols = numColumnsOrDefault(numColumns); - - const render = (props: RenderItemProps): React.Node => { - if (ListItemComponent) { - // $FlowFixMe[not-a-component] Component isn't valid - // $FlowFixMe[incompatible-type-arg] Component isn't valid - // $FlowFixMe[incompatible-return] Component isn't valid - return ; - } else if (renderItem) { - // $FlowFixMe[incompatible-call] - return renderItem(props); - } else { - return null; - } - }; - - const renderProp = (info: RenderItemProps) => { - if (cols > 1) { - const {item, index} = info; - invariant( - Array.isArray(item), - 'Expected array of items with numColumns > 1', - ); - return ( - - {item.map((it, kk) => { - const element = render({ - // $FlowFixMe[incompatible-call] - item: it, - index: index * cols + kk, - separators: info.separators, - }); - return element != null ? ( - {element} - ) : null; - })} - - ); - } else { - return render(info); - } - }; - - return ListItemComponent - ? {ListItemComponent: renderProp} - : {renderItem: renderProp}; - }; - - // $FlowFixMe[missing-local-annot] - _memoizedRenderer = memoizeOne(this._renderer); - - render(): React.Node { - const { - numColumns, - columnWrapperStyle, - removeClippedSubviews: _removeClippedSubviews, - strictMode = false, - ...restProps - } = this.props; - - const renderer = strictMode ? this._memoizedRenderer : this._renderer; - - return ( - // $FlowFixMe[incompatible-exact] - `restProps` (`Props`) is inexact. - - ); - } -} - -const styles = StyleSheet.create({ - row: {flexDirection: 'row'}, -}); +export type {FlatListProps} from '@react-native/flat-lists'; module.exports = FlatList; diff --git a/packages/react-native/Libraries/Lists/SectionList.d.ts b/packages/react-native/Libraries/Lists/SectionList.d.ts index 396092286b05d1..f6d990408ae0ac 100644 --- a/packages/react-native/Libraries/Lists/SectionList.d.ts +++ b/packages/react-native/Libraries/Lists/SectionList.d.ts @@ -7,256 +7,15 @@ * @format */ -import type * as React from 'react'; -import type { - ListRenderItemInfo, - VirtualizedListWithoutRenderItemProps, -} from '@react-native/virtualized-lists'; -import type { - ScrollView, - ScrollViewProps, -} from '../Components/ScrollView/ScrollView'; -import {NodeHandle} from '../ReactNative/RendererProxy'; -import {StyleProp} from '../StyleSheet/StyleSheet'; -import {ViewStyle} from '../StyleSheet/StyleSheetTypes'; - -/** - * @see https://reactnative.dev/docs/sectionlist - */ - -type DefaultSectionT = { - [key: string]: any; -}; - -export interface SectionBase { - data: ReadonlyArray; - - key?: string | undefined; - - renderItem?: SectionListRenderItem | undefined; - - ItemSeparatorComponent?: React.ComponentType | null | undefined; - - keyExtractor?: ((item: ItemT, index: number) => string) | undefined; -} - -export type SectionListData = SectionBase< - ItemT, - SectionT -> & - SectionT; - -/** - * @see https://reactnative.dev/docs/sectionlist.html#props - */ - -export interface SectionListRenderItemInfo - extends ListRenderItemInfo { - section: SectionListData; -} - -export type SectionListRenderItem = ( - info: SectionListRenderItemInfo, -) => React.ReactElement | null; - -export interface SectionListProps - extends VirtualizedListWithoutRenderItemProps { - /** - * Rendered in between each section. - */ - SectionSeparatorComponent?: - | React.ComponentType - | React.ReactElement - | null - | undefined; - - /** - * A marker property for telling the list to re-render (since it implements PureComponent). - * If any of your `renderItem`, Header, Footer, etc. functions depend on anything outside of the `data` prop, - * stick it here and treat it immutably. - */ - extraData?: any | undefined; - - /** - * `getItemLayout` is an optional optimization that lets us skip measurement of dynamic - * content if you know the height of items a priori. getItemLayout is the most efficient, - * and is easy to use if you have fixed height items, for example: - * ``` - * getItemLayout={(data, index) => ( - * {length: ITEM_HEIGHT, offset: ITEM_HEIGHT * index, index} - * )} - * ``` - */ - getItemLayout?: - | (( - data: SectionListData[] | null, - index: number, - ) => {length: number; offset: number; index: number}) - | undefined; - - /** - * How many items to render in the initial batch - */ - initialNumToRender?: number | undefined; - - /** - * Reverses the direction of scroll. Uses scale transforms of -1. - */ - inverted?: boolean | null | undefined; - - /** - * Used to extract a unique key for a given item at the specified index. Key is used for caching - * and as the react key to track item re-ordering. The default extractor checks `item.key`, then - * falls back to using the index, like React does. - */ - keyExtractor?: ((item: ItemT, index: number) => string) | undefined; - - /** - * Called once when the scroll position gets within onEndReachedThreshold of the rendered content. - */ - onEndReached?: ((info: {distanceFromEnd: number}) => void) | null | undefined; - - /** - * How far from the end (in units of visible length of the list) the bottom edge of the - * list must be from the end of the content to trigger the `onEndReached` callback. - * Thus a value of 0.5 will trigger `onEndReached` when the end of the content is - * within half the visible length of the list. - */ - onEndReachedThreshold?: number | null | undefined; - - /** - * If provided, a standard RefreshControl will be added for "Pull to Refresh" functionality. - * Make sure to also set the refreshing prop correctly. - */ - onRefresh?: (() => void) | null | undefined; - - /** - * Used to handle failures when scrolling to an index that has not been measured yet. - * Recommended action is to either compute your own offset and `scrollTo` it, or scroll as far - * as possible and then try again after more items have been rendered. - */ - onScrollToIndexFailed?: - | ((info: { - index: number; - highestMeasuredFrameIndex: number; - averageItemLength: number; - }) => void) - | undefined; - - /** - * Set this true while waiting for new data from a refresh. - */ - refreshing?: boolean | null | undefined; - - /** - * Default renderer for every item in every section. Can be over-ridden on a per-section basis. - */ - renderItem?: SectionListRenderItem | undefined; - - /** - * Rendered at the top of each section. Sticky headers are not yet supported. - */ - renderSectionHeader?: - | ((info: { - section: SectionListData; - }) => React.ReactElement | null) - | undefined; - - /** - * Rendered at the bottom of each section. - */ - renderSectionFooter?: - | ((info: { - section: SectionListData; - }) => React.ReactElement | null) - | undefined; - - /** - * An array of objects with data for each section. - */ - sections: ReadonlyArray>; - - /** - * Render a custom scroll component, e.g. with a differently styled `RefreshControl`. - */ - renderScrollComponent?: - | ((props: ScrollViewProps) => React.ReactElement) - | undefined; - - /** - * Note: may have bugs (missing content) in some circumstances - use at your own risk. - * - * This may improve scroll performance for large lists. - */ - removeClippedSubviews?: boolean | undefined; - - /** - * Makes section headers stick to the top of the screen until the next one pushes it off. - * Only enabled by default on iOS because that is the platform standard there. - */ - stickySectionHeadersEnabled?: boolean | undefined; - - /** - * Uses legacy MetroListView instead of default VirtualizedSectionList - */ - legacyImplementation?: boolean | undefined; -} - -export interface SectionListScrollParams { - animated?: boolean | undefined; - itemIndex: number; - sectionIndex: number; - viewOffset?: number | undefined; - viewPosition?: number | undefined; -} - -export abstract class SectionListComponent< - Props, -> extends React.Component { - /** - * Scrolls to the item at the specified sectionIndex and itemIndex (within the section) - * positioned in the viewable area such that viewPosition 0 places it at the top - * (and may be covered by a sticky header), 1 at the bottom, and 0.5 centered in the middle. - */ - scrollToLocation(params: SectionListScrollParams): void; - - /** - * Tells the list an interaction has occurred, which should trigger viewability calculations, e.g. - * if `waitForInteractions` is true and the user has not scrolled. This is typically called by - * taps on items or by navigation actions. - */ - recordInteraction(): void; - - /** - * Displays the scroll indicators momentarily. - * - * @platform ios - */ - flashScrollIndicators(): void; - - /** - * Provides a handle to the underlying scroll responder. - */ - getScrollResponder(): ScrollView | undefined; - - /** - * Provides a handle to the underlying scroll node. - */ - getScrollableNode(): NodeHandle | undefined; -} - -export class SectionList< - ItemT = any, - SectionT = DefaultSectionT, -> extends SectionListComponent> {} - -/* This definition is deprecated because it extends the wrong base type */ -export interface SectionListStatic - extends React.ComponentClass> { - /** - * Scrolls to the item at the specified sectionIndex and itemIndex (within the section) - * positioned in the viewable area such that viewPosition 0 places it at the top - * (and may be covered by a sticky header), 1 at the bottom, and 0.5 centered in the middle. - */ - scrollToLocation?(params: SectionListScrollParams): void; -} +export { + DefaultSectionT, + SectionBase, + SectionList, + SectionListComponent, + SectionListData, + SectionListProps, + SectionListRenderItem, + SectionListRenderItemInfo, + SectionListScrollParams, + SectionListStatic, +} from '@react-native/flat-lists'; diff --git a/packages/react-native/Libraries/Lists/SectionList.js b/packages/react-native/Libraries/Lists/SectionList.js index 0f199487b92a23..809dfc92ff71a8 100644 --- a/packages/react-native/Libraries/Lists/SectionList.js +++ b/packages/react-native/Libraries/Lists/SectionList.js @@ -10,256 +10,11 @@ 'use strict'; -import type {ScrollResponderType} from '../Components/ScrollView/ScrollView'; -import type { - ScrollToLocationParamsType, - SectionBase as _SectionBase, - VirtualizedSectionListProps, -} from '@react-native/virtualized-lists'; +import {typeof SectionList as SectionListType} from '@react-native/flat-lists'; -import Platform from '../Utilities/Platform'; -import {VirtualizedSectionList} from '@react-native/virtualized-lists'; -import * as React from 'react'; +const SectionList: SectionListType = + require('@react-native/flat-lists').SectionList; -type Item = any; +export type {SectionListProps, SectionBase} from '@react-native/flat-lists'; -export type SectionBase = _SectionBase; - -type RequiredProps> = {| - /** - * The actual data to render, akin to the `data` prop in [``](https://reactnative.dev/docs/flatlist). - * - * General shape: - * - * sections: $ReadOnlyArray<{ - * data: $ReadOnlyArray, - * renderItem?: ({item: SectionItem, ...}) => ?React.Element<*>, - * ItemSeparatorComponent?: ?ReactClass<{highlighted: boolean, ...}>, - * }> - */ - sections: $ReadOnlyArray, -|}; - -type OptionalProps> = {| - /** - * Default renderer for every item in every section. Can be over-ridden on a per-section basis. - */ - renderItem?: (info: { - item: Item, - index: number, - section: SectionT, - separators: { - highlight: () => void, - unhighlight: () => void, - updateProps: (select: 'leading' | 'trailing', newProps: Object) => void, - ... - }, - ... - }) => null | React.Element, - /** - * A marker property for telling the list to re-render (since it implements `PureComponent`). If - * any of your `renderItem`, Header, Footer, etc. functions depend on anything outside of the - * `data` prop, stick it here and treat it immutably. - */ - extraData?: any, - /** - * How many items to render in the initial batch. This should be enough to fill the screen but not - * much more. Note these items will never be unmounted as part of the windowed rendering in order - * to improve perceived performance of scroll-to-top actions. - */ - initialNumToRender?: ?number, - /** - * Reverses the direction of scroll. Uses scale transforms of -1. - */ - inverted?: ?boolean, - /** - * Used to extract a unique key for a given item at the specified index. Key is used for caching - * and as the react key to track item re-ordering. The default extractor checks item.key, then - * falls back to using the index, like react does. Note that this sets keys for each item, but - * each overall section still needs its own key. - */ - keyExtractor?: ?(item: Item, index: number) => string, - /** - * Called once when the scroll position gets within `onEndReachedThreshold` of the rendered - * content. - */ - onEndReached?: ?(info: {distanceFromEnd: number, ...}) => void, - /** - * Note: may have bugs (missing content) in some circumstances - use at your own risk. - * - * This may improve scroll performance for large lists. - */ - removeClippedSubviews?: boolean, -|}; - -export type Props = {| - ...$Diff< - VirtualizedSectionListProps, - { - getItem: $PropertyType, 'getItem'>, - getItemCount: $PropertyType< - VirtualizedSectionListProps, - 'getItemCount', - >, - renderItem: $PropertyType< - VirtualizedSectionListProps, - 'renderItem', - >, - keyExtractor: $PropertyType< - VirtualizedSectionListProps, - 'keyExtractor', - >, - ... - }, - >, - ...RequiredProps, - ...OptionalProps, -|}; - -/** - * A performant interface for rendering sectioned lists, supporting the most handy features: - * - * - Fully cross-platform. - * - Configurable viewability callbacks. - * - List header support. - * - List footer support. - * - Item separator support. - * - Section header support. - * - Section separator support. - * - Heterogeneous data and item rendering support. - * - Pull to Refresh. - * - Scroll loading. - * - * If you don't need section support and want a simpler interface, use - * [``](https://reactnative.dev/docs/flatlist). - * - * Simple Examples: - * - * } - * renderSectionHeader={({section}) =>
} - * sections={[ // homogeneous rendering between sections - * {data: [...], title: ...}, - * {data: [...], title: ...}, - * {data: [...], title: ...}, - * ]} - * /> - * - * - * - * This is a convenience wrapper around [``](docs/virtualizedlist), - * and thus inherits its props (as well as those of `ScrollView`) that aren't explicitly listed - * here, along with the following caveats: - * - * - Internal state is not preserved when content scrolls out of the render window. Make sure all - * your data is captured in the item data or external stores like Flux, Redux, or Relay. - * - This is a `PureComponent` which means that it will not re-render if `props` remain shallow- - * equal. Make sure that everything your `renderItem` function depends on is passed as a prop - * (e.g. `extraData`) that is not `===` after updates, otherwise your UI may not update on - * changes. This includes the `data` prop and parent component state. - * - In order to constrain memory and enable smooth scrolling, content is rendered asynchronously - * offscreen. This means it's possible to scroll faster than the fill rate and momentarily see - * blank content. This is a tradeoff that can be adjusted to suit the needs of each application, - * and we are working on improving it behind the scenes. - * - By default, the list looks for a `key` prop on each item and uses that for the React key. - * Alternatively, you can provide a custom `keyExtractor` prop. - * - */ -export default class SectionList< - SectionT: SectionBase, -> extends React.PureComponent, void> { - props: Props; - - /** - * Scrolls to the item at the specified `sectionIndex` and `itemIndex` (within the section) - * positioned in the viewable area such that `viewPosition` 0 places it at the top (and may be - * covered by a sticky header), 1 at the bottom, and 0.5 centered in the middle. `viewOffset` is a - * fixed number of pixels to offset the final target position, e.g. to compensate for sticky - * headers. - * - * Note: cannot scroll to locations outside the render window without specifying the - * `getItemLayout` prop. - */ - scrollToLocation(params: ScrollToLocationParamsType) { - if (this._wrapperListRef != null) { - this._wrapperListRef.scrollToLocation(params); - } - } - - /** - * Tells the list an interaction has occurred, which should trigger viewability calculations, e.g. - * if `waitForInteractions` is true and the user has not scrolled. This is typically called by - * taps on items or by navigation actions. - */ - recordInteraction() { - const listRef = this._wrapperListRef && this._wrapperListRef.getListRef(); - listRef && listRef.recordInteraction(); - } - - /** - * Displays the scroll indicators momentarily. - * - * @platform ios - */ - flashScrollIndicators() { - const listRef = this._wrapperListRef && this._wrapperListRef.getListRef(); - listRef && listRef.flashScrollIndicators(); - } - - /** - * Provides a handle to the underlying scroll responder. - */ - getScrollResponder(): ?ScrollResponderType { - const listRef = this._wrapperListRef && this._wrapperListRef.getListRef(); - if (listRef) { - return listRef.getScrollResponder(); - } - } - - getScrollableNode(): any { - const listRef = this._wrapperListRef && this._wrapperListRef.getListRef(); - if (listRef) { - return listRef.getScrollableNode(); - } - } - - setNativeProps(props: Object) { - const listRef = this._wrapperListRef && this._wrapperListRef.getListRef(); - if (listRef) { - listRef.setNativeProps(props); - } - } - - render(): React.Node { - const { - stickySectionHeadersEnabled: _stickySectionHeadersEnabled, - ...restProps - } = this.props; - const stickySectionHeadersEnabled = - _stickySectionHeadersEnabled ?? Platform.OS === 'ios'; - return ( - items.length} - // $FlowFixMe[missing-local-annot] - getItem={(items, index) => items[index]} - /> - ); - } - - _wrapperListRef: ?React.ElementRef; - /* $FlowFixMe[missing-local-annot] The type annotation(s) required by Flow's - * LTI update could not be added via codemod */ - _captureRef = ref => { - this._wrapperListRef = ref; - }; -} +module.exports = SectionList; diff --git a/packages/react-native/Libraries/Lists/SectionListModern.js b/packages/react-native/Libraries/Lists/SectionListModern.js index d9676f106f8cfc..ca33750f0a50b4 100644 --- a/packages/react-native/Libraries/Lists/SectionListModern.js +++ b/packages/react-native/Libraries/Lists/SectionListModern.js @@ -10,240 +10,11 @@ 'use strict'; -import type {ScrollResponderType} from '../Components/ScrollView/ScrollView'; -import type { - ScrollToLocationParamsType, - SectionBase as _SectionBase, - VirtualizedSectionListProps, -} from '@react-native/virtualized-lists'; -import type {AbstractComponent, Element, ElementRef} from 'react'; +import {typeof SectionListModern as SectionListModernType} from '@react-native/flat-lists'; -import Platform from '../Utilities/Platform'; -import {VirtualizedSectionList} from '@react-native/virtualized-lists'; -import React, {forwardRef, useImperativeHandle, useRef} from 'react'; +const SectionList: SectionListModernType = + require('@react-native/flat-lists').SectionListModern; -type Item = any; +export type {SectionListProps, SectionBase} from '@react-native/flat-lists'; -export type SectionBase = _SectionBase; - -type RequiredProps> = {| - /** - * The actual data to render, akin to the `data` prop in [``](https://reactnative.dev/docs/flatlist). - * - * General shape: - * - * sections: $ReadOnlyArray<{ - * data: $ReadOnlyArray, - * renderItem?: ({item: SectionItem, ...}) => ?React.Element<*>, - * ItemSeparatorComponent?: ?ReactClass<{highlighted: boolean, ...}>, - * }> - */ - sections: $ReadOnlyArray, -|}; - -type OptionalProps> = {| - /** - * Default renderer for every item in every section. Can be over-ridden on a per-section basis. - */ - renderItem?: (info: { - item: Item, - index: number, - section: SectionT, - separators: { - highlight: () => void, - unhighlight: () => void, - updateProps: (select: 'leading' | 'trailing', newProps: Object) => void, - ... - }, - ... - }) => null | Element, - /** - * A marker property for telling the list to re-render (since it implements `PureComponent`). If - * any of your `renderItem`, Header, Footer, etc. functions depend on anything outside of the - * `data` prop, stick it here and treat it immutably. - */ - extraData?: any, - /** - * How many items to render in the initial batch. This should be enough to fill the screen but not - * much more. Note these items will never be unmounted as part of the windowed rendering in order - * to improve perceived performance of scroll-to-top actions. - */ - initialNumToRender?: ?number, - /** - * Reverses the direction of scroll. Uses scale transforms of -1. - */ - inverted?: ?boolean, - /** - * Used to extract a unique key for a given item at the specified index. Key is used for caching - * and as the react key to track item re-ordering. The default extractor checks item.key, then - * falls back to using the index, like react does. Note that this sets keys for each item, but - * each overall section still needs its own key. - */ - keyExtractor?: ?(item: Item, index: number) => string, - /** - * Called once when the scroll position gets within `onEndReachedThreshold` of the rendered - * content. - */ - onEndReached?: ?(info: {distanceFromEnd: number, ...}) => void, - /** - * Note: may have bugs (missing content) in some circumstances - use at your own risk. - * - * This may improve scroll performance for large lists. - */ - removeClippedSubviews?: boolean, -|}; - -export type Props = {| - ...$Diff< - VirtualizedSectionListProps, - { - getItem: $PropertyType, 'getItem'>, - getItemCount: $PropertyType< - VirtualizedSectionListProps, - 'getItemCount', - >, - renderItem: $PropertyType< - VirtualizedSectionListProps, - 'renderItem', - >, - keyExtractor: $PropertyType< - VirtualizedSectionListProps, - 'keyExtractor', - >, - ... - }, - >, - ...RequiredProps, - ...OptionalProps, -|}; - -/** - * A performant interface for rendering sectioned lists, supporting the most handy features: - * - * - Fully cross-platform. - * - Configurable viewability callbacks. - * - List header support. - * - List footer support. - * - Item separator support. - * - Section header support. - * - Section separator support. - * - Heterogeneous data and item rendering support. - * - Pull to Refresh. - * - Scroll loading. - * - * If you don't need section support and want a simpler interface, use - * [``](https://reactnative.dev/docs/flatlist). - * - * Simple Examples: - * - * } - * renderSectionHeader={({section}) =>
} - * sections={[ // homogeneous rendering between sections - * {data: [...], title: ...}, - * {data: [...], title: ...}, - * {data: [...], title: ...}, - * ]} - * /> - * - * - * - * This is a convenience wrapper around [``](docs/virtualizedlist), - * and thus inherits its props (as well as those of `ScrollView`) that aren't explicitly listed - * here, along with the following caveats: - * - * - Internal state is not preserved when content scrolls out of the render window. Make sure all - * your data is captured in the item data or external stores like Flux, Redux, or Relay. - * - This is a `PureComponent` which means that it will not re-render if `props` remain shallow- - * equal. Make sure that everything your `renderItem` function depends on is passed as a prop - * (e.g. `extraData`) that is not `===` after updates, otherwise your UI may not update on - * changes. This includes the `data` prop and parent component state. - * - In order to constrain memory and enable smooth scrolling, content is rendered asynchronously - * offscreen. This means it's possible to scroll faster than the fill rate and momentarily see - * blank content. This is a tradeoff that can be adjusted to suit the needs of each application, - * and we are working on improving it behind the scenes. - * - By default, the list looks for a `key` prop on each item and uses that for the React key. - * Alternatively, you can provide a custom `keyExtractor` prop. - * - */ -const SectionList: AbstractComponent>, any> = forwardRef< - Props>, - any, ->((props, ref) => { - const propsWithDefaults = { - stickySectionHeadersEnabled: Platform.OS === 'ios', - ...props, - }; - - const wrapperRef = useRef>(); - - useImperativeHandle( - ref, - () => ({ - /** - * Scrolls to the item at the specified `sectionIndex` and `itemIndex` (within the section) - * positioned in the viewable area such that `viewPosition` 0 places it at the top (and may be - * covered by a sticky header), 1 at the bottom, and 0.5 centered in the middle. `viewOffset` is a - * fixed number of pixels to offset the final target position, e.g. to compensate for sticky - * headers. - * - * Note: cannot scroll to locations outside the render window without specifying the - * `getItemLayout` prop. - */ - scrollToLocation(params: ScrollToLocationParamsType) { - wrapperRef.current?.scrollToLocation(params); - }, - - /** - * Tells the list an interaction has occurred, which should trigger viewability calculations, e.g. - * if `waitForInteractions` is true and the user has not scrolled. This is typically called by - * taps on items or by navigation actions. - */ - recordInteraction() { - wrapperRef.current?.getListRef()?.recordInteraction(); - }, - - /** - * Displays the scroll indicators momentarily. - * - * @platform ios - */ - flashScrollIndicators() { - wrapperRef.current?.getListRef()?.flashScrollIndicators(); - }, - - /** - * Provides a handle to the underlying scroll responder. - */ - getScrollResponder(): ?ScrollResponderType { - wrapperRef.current?.getListRef()?.getScrollResponder(); - }, - - getScrollableNode(): any { - wrapperRef.current?.getListRef()?.getScrollableNode(); - }, - - setNativeProps(nativeProps: Object) { - wrapperRef.current?.getListRef()?.setNativeProps(nativeProps); - }, - }), - [wrapperRef], - ); - - return ( - items.length} - getItem={(items, index) => items[index]} - /> - ); -}); - -export default SectionList; +module.exports = SectionList; diff --git a/packages/react-native/index.js b/packages/react-native/index.js index 7149c6463b52fa..7ee9df82fc1c92 100644 --- a/packages/react-native/index.js +++ b/packages/react-native/index.js @@ -158,7 +158,7 @@ module.exports = { return require('./Libraries/Components/ScrollView/ScrollView'); }, get SectionList(): SectionList { - return require('./Libraries/Lists/SectionList').default; + return require('./Libraries/Lists/SectionList'); }, get StatusBar(): StatusBar { return require('./Libraries/Components/StatusBar/StatusBar'); diff --git a/packages/react-native/metro.config.js b/packages/react-native/metro.config.js index 0805c2d035ef1b..27baf8e56c43ce 100644 --- a/packages/react-native/metro.config.js +++ b/packages/react-native/metro.config.js @@ -24,6 +24,7 @@ module.exports = { path.resolve(__dirname, '../normalize-color'), path.resolve(__dirname, '../polyfills'), path.resolve(__dirname, '../virtualized-lists'), + path.resolve(__dirname, '../flat-lists'), ], resolver: { blockList: [/buck-out/, /sdks\/hermes/], diff --git a/packages/react-native/package.json b/packages/react-native/package.json index 2a319632997fa1..f45c796cf32b3b 100644 --- a/packages/react-native/package.json +++ b/packages/react-native/package.json @@ -88,6 +88,7 @@ "@react-native/js-polyfills": "^0.72.1", "@react-native/normalize-colors": "^0.72.0", "@react-native/virtualized-lists": "^0.72.2", + "@react-native/flat-lists": "^0.72.2", "abort-controller": "^3.0.0", "anser": "^1.4.9", "base64-js": "^1.1.2", diff --git a/packages/react-native/types/index.d.ts b/packages/react-native/types/index.d.ts index d50dd04b12b602..22bb6f801eb9f3 100644 --- a/packages/react-native/types/index.d.ts +++ b/packages/react-native/types/index.d.ts @@ -113,8 +113,7 @@ export * from '../Libraries/Interaction/InteractionManager'; export * from '../Libraries/Interaction/PanResponder'; export * from '../Libraries/LayoutAnimation/LayoutAnimation'; export * from '../Libraries/Linking/Linking'; -export * from '../Libraries/Lists/FlatList'; -export * from '../Libraries/Lists/SectionList'; +export * from '@react-native/flat-lists'; export * from '@react-native/virtualized-lists'; export * from '../Libraries/LogBox/LogBox'; export * from '../Libraries/Modal/Modal'; diff --git a/packages/rn-tester/metro.config.js b/packages/rn-tester/metro.config.js index a49778e984d0d8..b7eebdcd5a8fea 100644 --- a/packages/rn-tester/metro.config.js +++ b/packages/rn-tester/metro.config.js @@ -25,6 +25,7 @@ module.exports = { path.resolve(__dirname, '../polyfills'), path.resolve(__dirname, '../react-native'), path.resolve(__dirname, '../virtualized-lists'), + path.resolve(__dirname, '../flat-lists'), ], resolver: { blockList: [/..\/react-native\/sdks\/hermes/],