mirror of
https://github.com/facebook/react-native.git
synced 2025-11-01 09:14:26 +00:00
e980d83389
Summary: It seems (I used git history to confirm) that FlatList/VirtualizedList have ([since the begining](https://github.com/facebook/react-native/blame/c13f5d48cfe3e7c0f6c6d0b745b50a089d6993ef/Libraries/Lists/VirtualizedList.js#L79)) a `disableVirtualization` prop. SectionList ([since it's begining](https://github.com/facebook/react-native/blame/abe737fe746406533798f9670e8e243cb18d5634/Libraries/Lists/VirtualizedSectionList.js#L98)) have a `enableVirtualization` prop, but since SectionList is VirtualizedSectionList which use VirtualizedList, this prop probably never did something. This fix just rename the prop properly so it can have an effect on the underlying VirtualizedList when you use a SectionList. Since props are spread it's kind of working already, but the flow annotation are wrong (so it tells you it won't work/ you can't use it) which sucks. (NB: I am doing this since I was trying to use a SectionList with react-native-web & server side rendering to get the all list, you can laugh). [General] [Fixed] - VirtualizedSectionList/SectionList: replace enableVirtualization prop annotation by correct underlying disableVirtualisation of VirtualizedList Pull Request resolved: https://github.com/facebook/react-native/pull/24312 Differential Revision: D14779449 Pulled By: cpojer fbshipit-source-id: e51e1d639d2bb265b5b286786010d01ffd9d90e0
548 lines
16 KiB
JavaScript
548 lines
16 KiB
JavaScript
/**
|
|
* Copyright (c) Facebook, Inc. and its 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';
|
|
|
|
const Platform = require('Platform');
|
|
const React = require('React');
|
|
const View = require('View');
|
|
const VirtualizedList = require('VirtualizedList');
|
|
|
|
const invariant = require('invariant');
|
|
|
|
import type {ViewToken} from 'ViewabilityHelper';
|
|
import type {Props as VirtualizedListProps} from 'VirtualizedList';
|
|
|
|
type Item = any;
|
|
type SectionItem = any;
|
|
|
|
type SectionBase = {
|
|
// Must be provided directly on each section.
|
|
data: $ReadOnlyArray<SectionItem>,
|
|
key?: string,
|
|
|
|
// Optional props will override list-wide props just for this section.
|
|
renderItem?: ?({
|
|
item: SectionItem,
|
|
index: number,
|
|
section: SectionBase,
|
|
separators: {
|
|
highlight: () => void,
|
|
unhighlight: () => void,
|
|
updateProps: (select: 'leading' | 'trailing', newProps: Object) => void,
|
|
},
|
|
}) => ?React.Element<any>,
|
|
ItemSeparatorComponent?: ?React.ComponentType<any>,
|
|
keyExtractor?: (item: SectionItem, index: ?number) => string,
|
|
|
|
// TODO: support more optional/override props
|
|
// FooterComponent?: ?ReactClass<any>,
|
|
// HeaderComponent?: ?ReactClass<any>,
|
|
// onViewableItemsChanged?: ({viewableItems: Array<ViewToken>, changed: Array<ViewToken>}) => void,
|
|
};
|
|
|
|
type RequiredProps<SectionT: SectionBase> = {
|
|
sections: $ReadOnlyArray<SectionT>,
|
|
};
|
|
|
|
type OptionalProps<SectionT: SectionBase> = {
|
|
/**
|
|
* Rendered after the last item in the last section.
|
|
*/
|
|
ListFooterComponent?: ?(React.ComponentType<any> | React.Element<any>),
|
|
/**
|
|
* Rendered at the very beginning of the list.
|
|
*/
|
|
ListHeaderComponent?: ?(React.ComponentType<any> | React.Element<any>),
|
|
/**
|
|
* Default renderer for every item in every section.
|
|
*/
|
|
renderItem?: (info: {
|
|
item: Item,
|
|
index: number,
|
|
section: SectionT,
|
|
separators: {
|
|
highlight: () => void,
|
|
unhighlight: () => void,
|
|
updateProps: (select: 'leading' | 'trailing', newProps: Object) => void,
|
|
},
|
|
}) => ?React.Element<any>,
|
|
/**
|
|
* Rendered at the top of each section.
|
|
*/
|
|
renderSectionHeader?: ?({section: SectionT}) => ?React.Element<any>,
|
|
/**
|
|
* Rendered at the bottom of each section.
|
|
*/
|
|
renderSectionFooter?: ?({section: SectionT}) => ?React.Element<any>,
|
|
/**
|
|
* Rendered at the bottom of every Section, except the very last one, in place of the normal
|
|
* ItemSeparatorComponent.
|
|
*/
|
|
SectionSeparatorComponent?: ?React.ComponentType<any>,
|
|
/**
|
|
* Rendered at the bottom of every Item except the very last one in the last section.
|
|
*/
|
|
ItemSeparatorComponent?: ?React.ComponentType<any>,
|
|
/**
|
|
* DEPRECATED: Virtualization provides significant performance and memory optimizations, but fully
|
|
* unmounts react instances that are outside of the render window. You should only need to disable
|
|
* this for debugging purposes.
|
|
*/
|
|
disableVirtualization?: ?boolean,
|
|
keyExtractor: (item: Item, index: number) => string,
|
|
onEndReached?: ?({distanceFromEnd: number}) => void,
|
|
/**
|
|
* If provided, a standard RefreshControl will be added for "Pull to Refresh" functionality. Make
|
|
* sure to also set the `refreshing` prop correctly.
|
|
*/
|
|
onRefresh?: ?() => void,
|
|
/**
|
|
* Called when the viewability of rows changes, as defined by the
|
|
* `viewabilityConfig` prop.
|
|
*/
|
|
onViewableItemsChanged?: ?({
|
|
viewableItems: Array<ViewToken>,
|
|
changed: Array<ViewToken>,
|
|
}) => void,
|
|
/**
|
|
* Set this true while waiting for new data from a refresh.
|
|
*/
|
|
refreshing?: ?boolean,
|
|
};
|
|
|
|
export type Props<SectionT> = RequiredProps<SectionT> &
|
|
OptionalProps<SectionT> &
|
|
VirtualizedListProps;
|
|
|
|
type DefaultProps = typeof VirtualizedList.defaultProps & {
|
|
data: $ReadOnlyArray<Item>,
|
|
};
|
|
type State = {childProps: VirtualizedListProps};
|
|
|
|
/**
|
|
* Right now this just flattens everything into one list and uses VirtualizedList under the
|
|
* hood. The only operation that might not scale well is concatting the data arrays of all the
|
|
* sections when new props are received, which should be plenty fast for up to ~10,000 items.
|
|
*/
|
|
class VirtualizedSectionList<SectionT: SectionBase> extends React.PureComponent<
|
|
Props<SectionT>,
|
|
State,
|
|
> {
|
|
static defaultProps: DefaultProps = {
|
|
...VirtualizedList.defaultProps,
|
|
data: [],
|
|
};
|
|
|
|
scrollToLocation(params: {
|
|
animated?: ?boolean,
|
|
itemIndex: number,
|
|
sectionIndex: number,
|
|
viewPosition?: number,
|
|
}) {
|
|
let index = Platform.OS === 'ios' ? params.itemIndex : params.itemIndex + 1;
|
|
for (let ii = 0; ii < params.sectionIndex; ii++) {
|
|
index += this.props.sections[ii].data.length + 2;
|
|
}
|
|
const toIndexParams = {
|
|
...params,
|
|
index,
|
|
};
|
|
this._listRef.scrollToIndex(toIndexParams);
|
|
}
|
|
|
|
getListRef(): VirtualizedList {
|
|
return this._listRef;
|
|
}
|
|
|
|
constructor(props: Props<SectionT>, context: Object) {
|
|
super(props, context);
|
|
this.state = this._computeState(props);
|
|
}
|
|
|
|
UNSAFE_componentWillReceiveProps(nextProps: Props<SectionT>) {
|
|
this.setState(this._computeState(nextProps));
|
|
}
|
|
|
|
_computeState(props: Props<SectionT>): State {
|
|
const offset = props.ListHeaderComponent ? 1 : 0;
|
|
const stickyHeaderIndices = [];
|
|
const itemCount = props.sections.reduce((v, section) => {
|
|
stickyHeaderIndices.push(v + offset);
|
|
return v + section.data.length + 2; // Add two for the section header and footer.
|
|
}, 0);
|
|
|
|
return {
|
|
childProps: {
|
|
...props,
|
|
renderItem: this._renderItem,
|
|
ItemSeparatorComponent: undefined, // Rendered with renderItem
|
|
data: props.sections,
|
|
getItemCount: () => itemCount,
|
|
getItem,
|
|
keyExtractor: this._keyExtractor,
|
|
onViewableItemsChanged: props.onViewableItemsChanged
|
|
? this._onViewableItemsChanged
|
|
: undefined,
|
|
stickyHeaderIndices: props.stickySectionHeadersEnabled
|
|
? stickyHeaderIndices
|
|
: undefined,
|
|
},
|
|
};
|
|
}
|
|
|
|
render() {
|
|
return (
|
|
<VirtualizedList {...this.state.childProps} ref={this._captureRef} />
|
|
);
|
|
}
|
|
|
|
_keyExtractor = (item: Item, index: number) => {
|
|
const info = this._subExtractor(index);
|
|
return (info && info.key) || String(index);
|
|
};
|
|
|
|
_subExtractor(
|
|
index: number,
|
|
): ?{
|
|
section: SectionT,
|
|
key: string, // Key of the section or combined key for section + item
|
|
index: ?number, // Relative index within the section
|
|
header?: ?boolean, // True if this is the section header
|
|
leadingItem?: ?Item,
|
|
leadingSection?: ?SectionT,
|
|
trailingItem?: ?Item,
|
|
trailingSection?: ?SectionT,
|
|
} {
|
|
let itemIndex = index;
|
|
const {sections} = this.props;
|
|
for (let ii = 0; ii < sections.length; ii++) {
|
|
const section = sections[ii];
|
|
const key = section.key || String(ii);
|
|
itemIndex -= 1; // The section adds an item for the header
|
|
if (itemIndex >= section.data.length + 1) {
|
|
itemIndex -= section.data.length + 1; // The section adds an item for the footer.
|
|
} else if (itemIndex === -1) {
|
|
return {
|
|
section,
|
|
key: key + ':header',
|
|
index: null,
|
|
header: true,
|
|
trailingSection: sections[ii + 1],
|
|
};
|
|
} else if (itemIndex === section.data.length) {
|
|
return {
|
|
section,
|
|
key: key + ':footer',
|
|
index: null,
|
|
header: false,
|
|
trailingSection: sections[ii + 1],
|
|
};
|
|
} else {
|
|
const keyExtractor = section.keyExtractor || this.props.keyExtractor;
|
|
return {
|
|
section,
|
|
key: key + ':' + keyExtractor(section.data[itemIndex], itemIndex),
|
|
index: itemIndex,
|
|
leadingItem: section.data[itemIndex - 1],
|
|
leadingSection: sections[ii - 1],
|
|
trailingItem:
|
|
section.data.length > itemIndex + 1
|
|
? section.data[itemIndex + 1]
|
|
: undefined,
|
|
trailingSection: sections[ii + 1],
|
|
};
|
|
}
|
|
}
|
|
}
|
|
|
|
_convertViewable = (viewable: ViewToken): ?ViewToken => {
|
|
invariant(viewable.index != null, 'Received a broken ViewToken');
|
|
const info = this._subExtractor(viewable.index);
|
|
if (!info) {
|
|
return null;
|
|
}
|
|
const keyExtractor = info.section.keyExtractor || this.props.keyExtractor;
|
|
return {
|
|
...viewable,
|
|
index: info.index,
|
|
/* $FlowFixMe(>=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. */
|
|
key: keyExtractor(viewable.item, info.index),
|
|
section: info.section,
|
|
};
|
|
};
|
|
|
|
_onViewableItemsChanged = ({
|
|
viewableItems,
|
|
changed,
|
|
}: {
|
|
viewableItems: Array<ViewToken>,
|
|
changed: Array<ViewToken>,
|
|
}) => {
|
|
if (this.props.onViewableItemsChanged) {
|
|
this.props.onViewableItemsChanged({
|
|
viewableItems: viewableItems
|
|
.map(this._convertViewable, this)
|
|
.filter(Boolean),
|
|
changed: changed.map(this._convertViewable, this).filter(Boolean),
|
|
});
|
|
}
|
|
};
|
|
|
|
_renderItem = ({item, index}: {item: Item, index: number}) => {
|
|
const info = this._subExtractor(index);
|
|
if (!info) {
|
|
return null;
|
|
}
|
|
const infoIndex = info.index;
|
|
if (infoIndex == null) {
|
|
const {section} = info;
|
|
if (info.header === true) {
|
|
const {renderSectionHeader} = this.props;
|
|
return renderSectionHeader ? renderSectionHeader({section}) : null;
|
|
} else {
|
|
const {renderSectionFooter} = this.props;
|
|
return renderSectionFooter ? renderSectionFooter({section}) : null;
|
|
}
|
|
} else {
|
|
const renderItem = info.section.renderItem || this.props.renderItem;
|
|
const SeparatorComponent = this._getSeparatorComponent(index, info);
|
|
invariant(renderItem, 'no renderItem!');
|
|
return (
|
|
<ItemWithSeparator
|
|
SeparatorComponent={SeparatorComponent}
|
|
LeadingSeparatorComponent={
|
|
infoIndex === 0 ? this.props.SectionSeparatorComponent : undefined
|
|
}
|
|
cellKey={info.key}
|
|
index={infoIndex}
|
|
item={item}
|
|
leadingItem={info.leadingItem}
|
|
leadingSection={info.leadingSection}
|
|
onUpdateSeparator={this._onUpdateSeparator}
|
|
prevCellKey={(this._subExtractor(index - 1) || {}).key}
|
|
ref={ref => {
|
|
this._cellRefs[info.key] = ref;
|
|
}}
|
|
renderItem={renderItem}
|
|
section={info.section}
|
|
trailingItem={info.trailingItem}
|
|
trailingSection={info.trailingSection}
|
|
/>
|
|
);
|
|
}
|
|
};
|
|
|
|
_onUpdateSeparator = (key: string, newProps: Object) => {
|
|
const ref = this._cellRefs[key];
|
|
ref && ref.updateSeparatorProps(newProps);
|
|
};
|
|
|
|
_getSeparatorComponent(
|
|
index: number,
|
|
info?: ?Object,
|
|
): ?React.ComponentType<any> {
|
|
info = info || this._subExtractor(index);
|
|
if (!info) {
|
|
return null;
|
|
}
|
|
const ItemSeparatorComponent =
|
|
info.section.ItemSeparatorComponent || this.props.ItemSeparatorComponent;
|
|
const {SectionSeparatorComponent} = this.props;
|
|
const isLastItemInList = index === this.state.childProps.getItemCount() - 1;
|
|
const isLastItemInSection = info.index === info.section.data.length - 1;
|
|
if (SectionSeparatorComponent && isLastItemInSection) {
|
|
return SectionSeparatorComponent;
|
|
}
|
|
if (ItemSeparatorComponent && !isLastItemInSection && !isLastItemInList) {
|
|
return ItemSeparatorComponent;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
_cellRefs = {};
|
|
_listRef: VirtualizedList;
|
|
_captureRef = ref => {
|
|
/* $FlowFixMe(>=0.53.0 site=react_native_fb,react_native_oss) This comment
|
|
* suppresses an error when upgrading Flow's support for React. To see the
|
|
* error delete this comment and run Flow. */
|
|
this._listRef = ref;
|
|
};
|
|
}
|
|
|
|
type ItemWithSeparatorCommonProps = $ReadOnly<{|
|
|
leadingItem: ?Item,
|
|
leadingSection: ?Object,
|
|
section: Object,
|
|
trailingItem: ?Item,
|
|
trailingSection: ?Object,
|
|
|}>;
|
|
|
|
type ItemWithSeparatorProps = $ReadOnly<{|
|
|
...ItemWithSeparatorCommonProps,
|
|
LeadingSeparatorComponent: ?React.ComponentType<any>,
|
|
SeparatorComponent: ?React.ComponentType<any>,
|
|
cellKey: string,
|
|
index: number,
|
|
item: Item,
|
|
onUpdateSeparator: (cellKey: string, newProps: Object) => void,
|
|
prevCellKey?: ?string,
|
|
renderItem: Function,
|
|
|}>;
|
|
|
|
type ItemWithSeparatorState = {
|
|
separatorProps: $ReadOnly<{|
|
|
highlighted: false,
|
|
...ItemWithSeparatorCommonProps,
|
|
|}>,
|
|
leadingSeparatorProps: $ReadOnly<{|
|
|
highlighted: false,
|
|
...ItemWithSeparatorCommonProps,
|
|
|}>,
|
|
};
|
|
|
|
class ItemWithSeparator extends React.Component<
|
|
ItemWithSeparatorProps,
|
|
ItemWithSeparatorState,
|
|
> {
|
|
state = {
|
|
separatorProps: {
|
|
highlighted: false,
|
|
leadingItem: this.props.item,
|
|
leadingSection: this.props.leadingSection,
|
|
section: this.props.section,
|
|
trailingItem: this.props.trailingItem,
|
|
trailingSection: this.props.trailingSection,
|
|
},
|
|
leadingSeparatorProps: {
|
|
highlighted: false,
|
|
leadingItem: this.props.leadingItem,
|
|
leadingSection: this.props.leadingSection,
|
|
section: this.props.section,
|
|
trailingItem: this.props.item,
|
|
trailingSection: this.props.trailingSection,
|
|
},
|
|
};
|
|
|
|
_separators = {
|
|
highlight: () => {
|
|
['leading', 'trailing'].forEach(s =>
|
|
this._separators.updateProps(s, {highlighted: true}),
|
|
);
|
|
},
|
|
unhighlight: () => {
|
|
['leading', 'trailing'].forEach(s =>
|
|
this._separators.updateProps(s, {highlighted: false}),
|
|
);
|
|
},
|
|
updateProps: (select: 'leading' | 'trailing', newProps: Object) => {
|
|
const {LeadingSeparatorComponent, cellKey, prevCellKey} = this.props;
|
|
if (select === 'leading' && LeadingSeparatorComponent != null) {
|
|
this.setState(state => ({
|
|
leadingSeparatorProps: {...state.leadingSeparatorProps, ...newProps},
|
|
}));
|
|
} else {
|
|
this.props.onUpdateSeparator(
|
|
(select === 'leading' && prevCellKey) || cellKey,
|
|
newProps,
|
|
);
|
|
}
|
|
},
|
|
};
|
|
|
|
static getDerivedStateFromProps(
|
|
props: ItemWithSeparatorProps,
|
|
prevState: ItemWithSeparatorState,
|
|
): ?ItemWithSeparatorState {
|
|
return {
|
|
separatorProps: {
|
|
...prevState.separatorProps,
|
|
leadingItem: props.item,
|
|
leadingSection: props.leadingSection,
|
|
section: props.section,
|
|
trailingItem: props.trailingItem,
|
|
trailingSection: props.trailingSection,
|
|
},
|
|
leadingSeparatorProps: {
|
|
...prevState.leadingSeparatorProps,
|
|
leadingItem: props.leadingItem,
|
|
leadingSection: props.leadingSection,
|
|
section: props.section,
|
|
trailingItem: props.item,
|
|
trailingSection: props.trailingSection,
|
|
},
|
|
};
|
|
}
|
|
|
|
updateSeparatorProps(newProps: Object) {
|
|
this.setState(state => ({
|
|
separatorProps: {...state.separatorProps, ...newProps},
|
|
}));
|
|
}
|
|
|
|
render() {
|
|
const {
|
|
LeadingSeparatorComponent,
|
|
SeparatorComponent,
|
|
item,
|
|
index,
|
|
section,
|
|
} = this.props;
|
|
const element = this.props.renderItem({
|
|
item,
|
|
index,
|
|
section,
|
|
separators: this._separators,
|
|
});
|
|
const leadingSeparator = LeadingSeparatorComponent && (
|
|
<LeadingSeparatorComponent {...this.state.leadingSeparatorProps} />
|
|
);
|
|
const separator = SeparatorComponent && (
|
|
<SeparatorComponent {...this.state.separatorProps} />
|
|
);
|
|
return leadingSeparator || separator ? (
|
|
/* $FlowFixMe(>=0.89.0 site=react_native_fb) This comment suppresses an
|
|
* error found when Flow v0.89 was deployed. To see the error, delete
|
|
* this comment and run Flow. */
|
|
<View>
|
|
{leadingSeparator}
|
|
{element}
|
|
{separator}
|
|
</View>
|
|
) : (
|
|
element
|
|
);
|
|
}
|
|
}
|
|
|
|
function getItem(sections: ?$ReadOnlyArray<Item>, index: number): ?Item {
|
|
if (!sections) {
|
|
return null;
|
|
}
|
|
let itemIdx = index - 1;
|
|
for (let ii = 0; ii < sections.length; ii++) {
|
|
if (itemIdx === -1 || itemIdx === sections[ii].data.length) {
|
|
// We intend for there to be overflow by one on both ends of the list.
|
|
// This will be for headers and footers. When returning a header or footer
|
|
// item the section itself is the item.
|
|
return sections[ii];
|
|
} else if (itemIdx < sections[ii].data.length) {
|
|
// If we are in the bounds of the list's data then return the item.
|
|
return sections[ii].data[itemIdx];
|
|
} else {
|
|
itemIdx -= sections[ii].data.length + 2; // Add two for the header and footer
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
module.exports = VirtualizedSectionList;
|