From 76307f47b9b6d1eddbdab527da57b021379dc879 Mon Sep 17 00:00:00 2001 From: Spencer Ahrens Date: Wed, 12 Apr 2017 16:57:04 -0700 Subject: [PATCH] add separator highlighting/updating support to `SectionList` Reviewed By: thechefchen Differential Revision: D4833604 fbshipit-source-id: cc1f85d8048221d9d26d728994b61237be625e4f --- Examples/UIExplorer/js/FlatListExample.js | 2 +- Examples/UIExplorer/js/ListExampleShared.js | 15 +-- Examples/UIExplorer/js/MultiColumnExample.js | 2 +- Examples/UIExplorer/js/SectionListExample.js | 47 +++++--- Libraries/Lists/SectionList.js | 40 ++++++- Libraries/Lists/VirtualizedSectionList.js | 112 ++++++++++++++++-- .../__snapshots__/SectionList-test.js.snap | 19 ++- 7 files changed, 189 insertions(+), 48 deletions(-) diff --git a/Examples/UIExplorer/js/FlatListExample.js b/Examples/UIExplorer/js/FlatListExample.js index 527fb7ed9..c4d67cb1f 100644 --- a/Examples/UIExplorer/js/FlatListExample.js +++ b/Examples/UIExplorer/js/FlatListExample.js @@ -205,7 +205,7 @@ class FlatListExample extends React.PureComponent { ); } }; - _pressItem = (key: number) => { + _pressItem = (key: string) => { this._listRef.getNode().recordInteraction(); pressItem(this, key); }; diff --git a/Examples/UIExplorer/js/ListExampleShared.js b/Examples/UIExplorer/js/ListExampleShared.js index 238386e41..825b73642 100644 --- a/Examples/UIExplorer/js/ListExampleShared.js +++ b/Examples/UIExplorer/js/ListExampleShared.js @@ -37,7 +37,7 @@ const { View, } = ReactNative; -type Item = {title: string, text: string, key: number, pressed: boolean, noImage?: ?boolean}; +type Item = {title: string, text: string, key: string, pressed: boolean, noImage?: ?boolean}; function genItemData(count: number, start: number = 0): Array { const dataBlob = []; @@ -46,7 +46,7 @@ function genItemData(count: number, start: number = 0): Array { dataBlob.push({ title: 'Item ' + ii, text: LOREM_IPSUM.substr(0, itemHash % 301 + 20), - key: ii, + key: String(ii), pressed: false, }); } @@ -61,7 +61,7 @@ class ItemComponent extends React.PureComponent { fixedHeight?: ?boolean, horizontal?: ?boolean, item: Item, - onPress: (key: number) => void, + onPress: (key: string) => void, onShowUnderlay?: () => void, onHideUnderlay?: () => void, }; @@ -199,12 +199,13 @@ function getItemLayout(data: any, index: number, horizontal?: boolean) { return {length, offset: (length + separator) * index + header, index}; } -function pressItem(context: Object, key: number) { - const pressed = !context.state.data[key].pressed; +function pressItem(context: Object, key: string) { + const index = Number(key); + const pressed = !context.state.data[index].pressed; context.setState((state) => { const newData = [...state.data]; - newData[key] = { - ...state.data[key], + newData[index] = { + ...state.data[index], pressed, title: 'Item ' + key + (pressed ? ' (pressed)' : ''), }; diff --git a/Examples/UIExplorer/js/MultiColumnExample.js b/Examples/UIExplorer/js/MultiColumnExample.js index 5dfd192de..876110cbf 100644 --- a/Examples/UIExplorer/js/MultiColumnExample.js +++ b/Examples/UIExplorer/js/MultiColumnExample.js @@ -139,7 +139,7 @@ class MultiColumnExample extends React.PureComponent { infoLog('onViewableItemsChanged: ', info.changed.map((v) => ({...v, item: '...'}))); } }; - _pressItem = (key: number) => { + _pressItem = (key: string) => { pressItem(this, key); }; } diff --git a/Examples/UIExplorer/js/SectionListExample.js b/Examples/UIExplorer/js/SectionListExample.js index 8b795c5bf..be919d38b 100644 --- a/Examples/UIExplorer/js/SectionListExample.js +++ b/Examples/UIExplorer/js/SectionListExample.js @@ -65,11 +65,9 @@ const renderSectionHeader = ({section}) => ( ); -const CustomSeparatorComponent = ({text}) => ( - - +const CustomSeparatorComponent = ({text, highlighted}) => ( + {text} - ); @@ -130,11 +128,11 @@ class SectionListExample extends React.PureComponent { - + SectionSeparatorComponent={({highlighted}) => + } - ItemSeparatorComponent={() => - + ItemSeparatorComponent={({highlighted}) => + } debug={this.state.debug} enableVirtualization={this.state.virtualized} @@ -147,22 +145,30 @@ class SectionListExample extends React.PureComponent { stickySectionHeadersEnabled sections={[ {renderItem: renderStackedItem, key: 's1', data: [ - {title: 'Item In Header Section', text: 'Section s1', key: '0'}, + {title: 'Item In Header Section', text: 'Section s1', key: 'header item'}, ]}, {key: 's2', data: [ - {noImage: true, title: '1st item', text: 'Section s2', key: '0'}, - {noImage: true, title: '2nd item', text: 'Section s2', key: '1'}, + {noImage: true, title: '1st item', text: 'Section s2', key: 'noimage0'}, + {noImage: true, title: '2nd item', text: 'Section s2', key: 'noimage1'}, ]}, ...filteredSectionData, ]} + style={styles.list} viewabilityConfig={VIEWABILITY_CONFIG} /> ); } - _renderItemComponent = ({item}) => ( - + + _renderItemComponent = ({item, separators}) => ( + ); + // This is called when items change viewability by scrolling into our out of // the viewable area. _onViewableItemsChanged = (info: { @@ -181,17 +187,25 @@ class SectionListExample extends React.PureComponent { ))); } }; - _pressItem = (index: number) => { - pressItem(this, index); + + _pressItem = (key: string) => { + !isNaN(key) && pressItem(this, key); }; } const styles = StyleSheet.create({ + customSeparator: { + backgroundColor: 'rgb(200, 199, 204)', + }, header: { backgroundColor: '#e9eaed', }, headerText: { padding: 4, + fontWeight: '600', + }, + list: { + backgroundColor: 'white', }, optionSection: { flexDirection: 'row', @@ -202,8 +216,7 @@ const styles = StyleSheet.create({ separatorText: { color: 'gray', alignSelf: 'center', - padding: 4, - fontSize: 9, + fontSize: 7, }, }); diff --git a/Libraries/Lists/SectionList.js b/Libraries/Lists/SectionList.js index 8b7409d9b..c73d121b8 100644 --- a/Libraries/Lists/SectionList.js +++ b/Libraries/Lists/SectionList.js @@ -27,7 +27,15 @@ type SectionBase = { key: string, // Optional props will override list-wide props just for this section. - renderItem?: ?(info: {item: SectionItemT, index: number}) => ?React.Element, + renderItem?: ?(info: { + item: SectionItemT, + index: number, + separators: { + highlight: () => void, + unhighlight: () => void, + updateProps: (select: 'leading' | 'trailing', newProps: Object) => void, + }, + }) => ?React.Element, ItemSeparatorComponent?: ?ReactClass, keyExtractor?: (item: SectionItemT) => string, @@ -36,6 +44,18 @@ type SectionBase = { }; type RequiredProps> = { + /** + * The actual data to render, akin to the `data` prop in [``](/react-native/docs/flatlist.html). + * + * General shape: + * + * sections: Array<{ + * data: Array, + * key: string, + * renderItem?: ({item: SectionItem, ...}) => ?React.Element<*>, + * ItemSeparatorComponent?: ?ReactClass<{highlighted: boolean, ...}>, + * }> + */ sections: Array, }; @@ -43,9 +63,20 @@ 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}) => ?React.Element, + renderItem: (info: { + item: Item, + index: number, + separators: { + highlight: () => void, + unhighlight: () => void, + updateProps: (select: 'leading' | 'trailing', newProps: Object) => void, + }, + }) => ?React.Element, /** - * Rendered in between adjacent Items within each section. + * Rendered in between each item, but not at the top or bottom. By default, `highlighted` and + * `leadingItem` props are provided. `renderItem` provides `separators.highlight`/`unhighlight` + * which will update the `highlighted` prop, but you can also add custom props with + * `separators.updateProps`. */ ItemSeparatorComponent?: ?ReactClass, /** @@ -57,7 +88,8 @@ type OptionalProps> = { */ ListFooterComponent?: ?(ReactClass | React.Element), /** - * Rendered in between each section. + * Rendered in between each section. Also receives `highlighted`, `leadingItem`, and any custom + * props from `separators.updateProps`. */ SectionSeparatorComponent?: ?ReactClass, /** diff --git a/Libraries/Lists/VirtualizedSectionList.js b/Libraries/Lists/VirtualizedSectionList.js index d6de9f38d..52ef51eed 100644 --- a/Libraries/Lists/VirtualizedSectionList.js +++ b/Libraries/Lists/VirtualizedSectionList.js @@ -30,7 +30,15 @@ type SectionBase = { key: string, // Optional props will override list-wide props just for this section. - renderItem?: ?({item: SectionItem, index: number}) => ?React.Element<*>, + renderItem?: ?({ + item: SectionItem, + index: number, + separators: { + highlight: () => void, + unhighlight: () => void, + updateProps: (select: 'leading' | 'trailing', newProps: Object) => void, + }, + }) => ?React.Element<*>, ItemSeparatorComponent?: ?ReactClass<*>, keyExtractor?: (item: SectionItem) => string, @@ -56,7 +64,15 @@ type OptionalProps = { /** * Default renderer for every item in every section. */ - renderItem: ({item: Item, index: number}) => ?React.Element<*>, + renderItem: (info: { + item: Item, + index: number, + separators: { + highlight: () => void, + unhighlight: () => void, + updateProps: (select: 'leading' | 'trailing', newProps: Object) => void, + }, + }) => ?React.Element, /** * Rendered at the top of each section. */ @@ -204,7 +220,9 @@ class VirtualizedSectionList const info = this._subExtractor(index); if (!info) { return null; - } else if (info.index == null) { + } + const infoIndex = info.index; + if (infoIndex == null) { const {renderSectionHeader} = this.props; return renderSectionHeader ? renderSectionHeader({section: info.section}) : null; } else { @@ -212,14 +230,30 @@ class VirtualizedSectionList const SeparatorComponent = this._getSeparatorComponent(index, info); invariant(renderItem, 'no renderItem!'); return ( - - {renderItem({item, index: info.index || 0})} - {SeparatorComponent && } - + {this._cellRefs[info.key] = ref;}} + renderItem={renderItem} + section={info.section} + /> ); } }; + _onUpdateSeparator = (key: string, newProps: Object) => { + const ref = this._cellRefs[key]; + ref && ref.updateSeparatorProps(newProps); + }; + _getSeparatorComponent(index: number, info?: ?Object): ?ReactClass<*> { info = info || this._subExtractor(index); if (!info) { @@ -229,7 +263,7 @@ class VirtualizedSectionList const {SectionSeparatorComponent} = this.props; const isLastItemInList = index === this.state.childProps.getItemCount() - 1; const isLastItemInSection = info.index === info.section.data.length - 1; - if (SectionSeparatorComponent && isLastItemInSection && !isLastItemInList) { + if (SectionSeparatorComponent && isLastItemInSection) { return SectionSeparatorComponent; } if (ItemSeparatorComponent && !isLastItemInSection && !isLastItemInList) { @@ -277,10 +311,72 @@ class VirtualizedSectionList return ; } + _cellRefs = {}; _listRef: VirtualizedList; _captureRef = (ref) => { this._listRef = ref; }; } +class ItemWithSeparator extends React.Component { + props: { + LeadingSeparatorComponent: ?ReactClass<*>, + SeparatorComponent: ?ReactClass<*>, + cellKey: string, + index: number, + item: Item, + onUpdateSeparator: (cellKey: string, newProps: Object) => void, + prevCellKey?: ?string, + renderItem: Function, + section: Object, + }; + + state = { + separatorProps: { + highlighted: false, + leadingItem: this.props.item, + leadingSection: this.props.section, + }, + leadingSeparatorProps: { + highlighted: false, + }, + }; + + _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) { + this.setState(state => ({ + leadingSeparatorProps: {...state.leadingSeparatorProps, ...newProps} + })); + } else { + this.props.onUpdateSeparator((select === 'leading' && prevCellKey) || cellKey, newProps); + } + }, + }; + + updateSeparatorProps(newProps: Object) { + this.setState(state => ({separatorProps: {...state.separatorProps, ...newProps}})); + } + + render() { + const {LeadingSeparatorComponent, SeparatorComponent, renderItem, item, index} = this.props; + const element = renderItem({ + item, + index, + separators: this._separators, + }); + const leadingSeparator = LeadingSeparatorComponent && + ; + const separator = SeparatorComponent && ; + return separator ? {leadingSeparator}{element}{separator} : element; + } +} + function getItem(sections: ?Array, index: number): ?Item { if (!sections) { return null; diff --git a/Libraries/Lists/__tests__/__snapshots__/SectionList-test.js.snap b/Libraries/Lists/__tests__/__snapshots__/SectionList-test.js.snap index f9fb946f3..62ef2dfb9 100644 --- a/Libraries/Lists/__tests__/__snapshots__/SectionList-test.js.snap +++ b/Libraries/Lists/__tests__/__snapshots__/SectionList-test.js.snap @@ -67,20 +67,16 @@ exports[`SectionList rendering empty section headers is fine 1`] = ` - - - + - - - + @@ -204,6 +200,7 @@ exports[`SectionList renders all the bells and whistles 1`] = ` onLayout={[Function]} > + @@ -231,6 +228,7 @@ exports[`SectionList renders all the bells and whistles 1`] = ` onLayout={[Function]} > + @@ -244,6 +242,7 @@ exports[`SectionList renders all the bells and whistles 1`] = ` +