diff --git a/Libraries/Lists/SectionList.js b/Libraries/Lists/SectionList.js index b26a13c58172..8c5ee0ea9c3c 100644 --- a/Libraries/Lists/SectionList.js +++ b/Libraries/Lists/SectionList.js @@ -188,7 +188,7 @@ type OptionalProps> = { export type Props = RequiredProps & OptionalProps & - VirtualizedSectionListProps; + VirtualizedSectionListProps; const defaultProps = { ...VirtualizedSectionList.defaultProps, @@ -325,11 +325,37 @@ class SectionList> extends React.PureComponent< } } + constructor(props: Props) { + super(props); + this._checkProps(this.props); + } + + componentDidUpdate(prevProps: Props) { + this._checkProps(this.props); + } + + _checkProps(props: Props) { + const {getItem, getItemCount} = props; + + if (__DEV__) { + if (getItem || getItemCount) { + console.warn('SectionList does not support custom data formats.'); + } + } + } + render() { /* $FlowFixMe(>=0.66.0 site=react_native_fb) This comment suppresses an * error found when Flow v0.66 was deployed. To see the error delete this * comment and run Flow. */ - return ; + return ( + items.length} + getItem={(items, index) => items[index]} + /> + ); } _wrapperListRef: ?React.ElementRef; diff --git a/Libraries/Lists/VirtualizedSectionList.js b/Libraries/Lists/VirtualizedSectionList.js index c52395954482..fe28b35a4b32 100644 --- a/Libraries/Lists/VirtualizedSectionList.js +++ b/Libraries/Lists/VirtualizedSectionList.js @@ -20,35 +20,11 @@ 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, - 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, - ItemSeparatorComponent?: ?React.ComponentType, - keyExtractor?: (item: SectionItem, index: ?number) => string, - - // TODO: support more optional/override props - // FooterComponent?: ?ReactClass, - // HeaderComponent?: ?ReactClass, - // onViewableItemsChanged?: ({viewableItems: Array, changed: Array}) => void, -}; +type SectionBase = Object; type RequiredProps = { - sections: $ReadOnlyArray, + sections: $ReadOnlyArray | $ReadOnly, }; type OptionalProps = { @@ -116,7 +92,8 @@ type OptionalProps = { */ refreshing?: ?boolean, }; - +/* $FlowFixMe - this Props seems to be missing a bunch of stuff. Remove this + * comment to see the errors */ export type Props = RequiredProps & OptionalProps & VirtualizedListProps; @@ -148,7 +125,10 @@ class VirtualizedSectionList extends React.PureComponent< }) { 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 section = this.props.getItem(this.props.sections, ii); + const sectionData = this.props.getItem(section, 'data'); + const dataLength = this.props.getItemCount(sectionData); + index += dataLength + 2; } const toIndexParams = { ...params, @@ -173,10 +153,14 @@ class VirtualizedSectionList extends React.PureComponent< _computeState(props: Props): 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); + const itemCount = props.sections + ? props.sections.reduce((v, section) => { + const sectionData = props.getItem(section, 'data'); + + stickyHeaderIndices.push(v + offset); + return v + props.getItemCount(sectionData) + 2; // Add two for the section header and footer. + }, 0) + : 0; return { childProps: { @@ -185,7 +169,7 @@ class VirtualizedSectionList extends React.PureComponent< ItemSeparatorComponent: undefined, // Rendered with renderItem data: props.sections, getItemCount: () => itemCount, - getItem, + getItem: (sections, index) => getItem(props, sections, index), keyExtractor: this._keyExtractor, onViewableItemsChanged: props.onViewableItemsChanged ? this._onViewableItemsChanged @@ -221,42 +205,44 @@ class VirtualizedSectionList extends React.PureComponent< 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); + const defaultKeyExtractor = this.props.keyExtractor; + for (let ii = 0; ii < this.props.getItemCount(this.props.sections); ii++) { + const section = this.props.getItem(this.props.sections, ii); + const sectionData = this.props.getItem(section, 'data'); + const key = this.props.getItem(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. + if (itemIndex >= this.props.getItemCount(sectionData) + 1) { + itemIndex -= this.props.getItemCount(sectionData) + 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], + trailingSection: this.props.getItem(this.props.sections, ii + 1), }; - } else if (itemIndex === section.data.length) { + } else if (itemIndex === this.props.getItemCount(sectionData)) { return { section, key: key + ':footer', index: null, header: false, - trailingSection: sections[ii + 1], + trailingSection: this.props.getItem(this.props.sections, ii + 1), }; } else { - const keyExtractor = section.keyExtractor || this.props.keyExtractor; + const keyExtractor = + this.props.getItem(section, 'keyExtractor') || defaultKeyExtractor; return { section, - key: key + ':' + keyExtractor(section.data[itemIndex], itemIndex), + key: + key + + ':' + + keyExtractor(this.props.getItem(sectionData, 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], + leadingItem: this.props.getItem(sectionData, itemIndex - 1), + leadingSection: this.props.getItem(this.props.sections, ii - 1), + trailingItem: this.props.getItem(sectionData, itemIndex + 1), + trailingSection: this.props.getItem(this.props.sections, ii + 1), }; } } @@ -268,7 +254,9 @@ class VirtualizedSectionList extends React.PureComponent< if (!info) { return null; } - const keyExtractor = info.section.keyExtractor || this.props.keyExtractor; + const keyExtractor = + this.props.getItem(info.section, 'keyExtractor') || + this.props.keyExtractor; return { ...viewable, index: info.index, @@ -313,7 +301,8 @@ class VirtualizedSectionList extends React.PureComponent< return renderSectionFooter ? renderSectionFooter({section}) : null; } } else { - const renderItem = info.section.renderItem || this.props.renderItem; + const renderItem = + this.props.getItem(info.section, 'renderItem') || this.props.renderItem; const SeparatorComponent = this._getSeparatorComponent(index, info); invariant(renderItem, 'no renderItem!'); return ( @@ -355,10 +344,13 @@ class VirtualizedSectionList extends React.PureComponent< return null; } const ItemSeparatorComponent = - info.section.ItemSeparatorComponent || this.props.ItemSeparatorComponent; + this.props.getItem(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; + const sectionData = this.props.getItem(info.section, 'data'); + const isLastItemInSection = + info.index === this.props.getItemCount(sectionData) - 1; if (SectionSeparatorComponent && isLastItemInSection) { return SectionSeparatorComponent; } @@ -445,7 +437,7 @@ class ItemWithSeparator extends React.Component< }, updateProps: (select: 'leading' | 'trailing', newProps: Object) => { const {LeadingSeparatorComponent, cellKey, prevCellKey} = this.props; - if (select === 'leading' && LeadingSeparatorComponent != null) { + if (select === 'leading' && LeadingSeparatorComponent !== null) { this.setState(state => ({ leadingSeparatorProps: {...state.leadingSeparatorProps, ...newProps}, })); @@ -523,22 +515,28 @@ class ItemWithSeparator extends React.Component< } } -function getItem(sections: ?$ReadOnlyArray, index: number): ?Item { +function getItem( + props: Props, + sections: ?$ReadOnlyArray, + 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) { + for (let ii = 0; ii < props.getItemCount(sections); ii++) { + const section = props.getItem(props.sections, ii); + const sectionData = props.getItem(section, 'data'); + if (itemIdx === -1 || itemIdx === props.getItemCount(sectionData)) { // 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) { + return section; + } else if (itemIdx < props.getItemCount(sectionData)) { // If we are in the bounds of the list's data then return the item. - return sections[ii].data[itemIdx]; + return props.getItem(sectionData, itemIdx); } else { - itemIdx -= sections[ii].data.length + 2; // Add two for the header and footer + itemIdx -= props.getItemCount(sectionData) + 2; // Add two for the header and footer } } return null; diff --git a/Libraries/Lists/__tests__/VirtualizedSectionList-test.js b/Libraries/Lists/__tests__/VirtualizedSectionList-test.js new file mode 100644 index 000000000000..4e287b037e20 --- /dev/null +++ b/Libraries/Lists/__tests__/VirtualizedSectionList-test.js @@ -0,0 +1,164 @@ +/** + * 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. + * + * + * @format + * @emails oncall+react_native + */ +'use strict'; + +const React = require('React'); +const ReactTestRenderer = require('react-test-renderer'); + +const VirtualizedSectionList = require('VirtualizedSectionList'); + +describe('VirtualizedSectionList', () => { + it('renders simple list', () => { + const component = ReactTestRenderer.create( + } + getItem={(data, key) => data[key]} + getItemCount={data => data.length} + />, + ); + expect(component).toMatchSnapshot(); + }); + + it('renders empty list', () => { + const component = ReactTestRenderer.create( + } + getItem={(data, key) => data[key]} + getItemCount={data => data.length} + />, + ); + expect(component).toMatchSnapshot(); + }); + + it('renders null list', () => { + const component = ReactTestRenderer.create( + } + getItem={(data, key) => data[key]} + getItemCount={data => 0} + />, + ); + expect(component).toMatchSnapshot(); + }); + + it('renders empty list with empty component', () => { + const component = ReactTestRenderer.create( + } + ListFooterComponent={() =>