UNPKG

@react-native-mac/virtualized-lists

Version:
257 lines (235 loc) 6.71 kB
/** * 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 {CellRendererProps, RenderItemType} from './VirtualizedListProps'; import type {ViewStyleProp} from 'react-native/Libraries/StyleSheet/StyleSheet'; import type { FocusEvent, LayoutEvent, } from 'react-native/Libraries/Types/CoreEventTypes'; import {VirtualizedListCellContextProvider} from './VirtualizedListContext.js'; import invariant from 'invariant'; import * as React from 'react'; import {Platform, StyleSheet, View} from 'react-native'; // [macOS] export type Props<ItemT> = { CellRendererComponent?: ?React.ComponentType<CellRendererProps<ItemT>>, ItemSeparatorComponent: ?React.ComponentType< any | {highlighted: boolean, leadingItem: ?ItemT}, >, ListItemComponent?: ?(React.ComponentType<any> | React.MixedElement), cellKey: string, horizontal: ?boolean, index: number, inversionStyle: ViewStyleProp, isSelected: ?boolean, // [macOS] item: ItemT, onCellLayout?: (event: LayoutEvent, cellKey: string, index: number) => void, onCellFocusCapture?: (cellKey: string) => void, onUnmount: (cellKey: string) => void, onUpdateSeparators: ( cellKeys: Array<?string>, props: Partial<SeparatorProps<ItemT>>, ) => void, prevCellKey: ?string, renderItem?: ?RenderItemType<ItemT>, ... }; type SeparatorProps<ItemT> = $ReadOnly<{| highlighted: boolean, leadingItem: ?ItemT, |}>; type State<ItemT> = { separatorProps: SeparatorProps<ItemT>, ... }; export default class CellRenderer<ItemT> extends React.PureComponent< Props<ItemT>, State<ItemT>, > { state: State<ItemT> = { separatorProps: { highlighted: false, leadingItem: this.props.item, }, }; static getDerivedStateFromProps( props: Props<ItemT>, prevState: State<ItemT>, ): ?State<ItemT> { if (props.item !== prevState.separatorProps.leadingItem) { return { separatorProps: { ...prevState.separatorProps, leadingItem: props.item, }, }; } return null; } // TODO: consider factoring separator stuff out of VirtualizedList into FlatList since it's not // reused by SectionList and we can keep VirtualizedList simpler. // $FlowFixMe[missing-local-annot] _separators = { highlight: () => { const {cellKey, prevCellKey} = this.props; this.props.onUpdateSeparators([cellKey, prevCellKey], { highlighted: true, }); }, unhighlight: () => { const {cellKey, prevCellKey} = this.props; this.props.onUpdateSeparators([cellKey, prevCellKey], { highlighted: false, }); }, updateProps: ( select: 'leading' | 'trailing', newProps: SeparatorProps<ItemT>, ) => { const {cellKey, prevCellKey} = this.props; this.props.onUpdateSeparators( [select === 'leading' ? prevCellKey : cellKey], newProps, ); }, }; updateSeparatorProps(newProps: SeparatorProps<ItemT>) { this.setState(state => ({ separatorProps: {...state.separatorProps, ...newProps}, })); } componentWillUnmount() { this.props.onUnmount(this.props.cellKey); } _onLayout = (nativeEvent: LayoutEvent): void => { this.props.onCellLayout?.( nativeEvent, this.props.cellKey, this.props.index, ); }; _onCellFocusCapture = (e: FocusEvent): void => { this.props.onCellFocusCapture?.(this.props.cellKey); }; _renderElement( renderItem: ?RenderItemType<ItemT>, ListItemComponent: any, item: ItemT, index: number, isSelected: ?boolean, // [macOS] ): React.Node { if (renderItem && ListItemComponent) { console.warn( 'VirtualizedList: Both ListItemComponent and renderItem props are present. ListItemComponent will take' + ' precedence over renderItem.', ); } if (ListItemComponent) { return ( <ListItemComponent item={item} index={index} separators={this._separators} /> ); } if (renderItem) { return renderItem({ item, index, isSelected, // [macOS] separators: this._separators, }); } invariant( false, 'VirtualizedList: Either ListItemComponent or renderItem props are required but none were found.', ); } render(): React.Node { const { CellRendererComponent, ItemSeparatorComponent, ListItemComponent, cellKey, horizontal, item, index, inversionStyle, isSelected, // [macOS] onCellLayout, renderItem, } = this.props; const element = this._renderElement( renderItem, ListItemComponent, item, index, isSelected, // [macOS] ); // NOTE: that when this is a sticky header, `onLayout` will get automatically extracted and // called explicitly by `ScrollViewStickyHeader`. const itemSeparator: React.Node = React.isValidElement( ItemSeparatorComponent, ) ? // $FlowFixMe[incompatible-type] ItemSeparatorComponent : // $FlowFixMe[incompatible-type] ItemSeparatorComponent && ( <ItemSeparatorComponent {...this.state.separatorProps} /> ); const cellStyle = inversionStyle ? horizontal ? [styles.rowReverse, inversionStyle] : [styles.columnReverse, inversionStyle] : horizontal ? [styles.row, inversionStyle] : inversionStyle; let result = !CellRendererComponent ? ( // [macOS] <View style={cellStyle} onFocusCapture={this._onCellFocusCapture} {...(onCellLayout && {onLayout: this._onLayout})}> {element} {itemSeparator} </View> ) : ( <CellRendererComponent cellKey={cellKey} index={index} item={item} style={cellStyle} onFocusCapture={this._onCellFocusCapture} {...(onCellLayout && {onLayout: this._onLayout})}> {element} {itemSeparator} </CellRendererComponent> ); if (Platform.OS === 'macos') { // [macOS result = React.cloneElement(result, {collapsable: false}); } // macOS] return ( <VirtualizedListCellContextProvider cellKey={this.props.cellKey}> {result} </VirtualizedListCellContextProvider> ); } } const styles = StyleSheet.create({ row: { flexDirection: 'row', }, rowReverse: { flexDirection: 'row-reverse', }, columnReverse: { flexDirection: 'column-reverse', }, });