add separator highlighting/updating support to `SectionList`

Reviewed By: thechefchen

Differential Revision: D4833604

fbshipit-source-id: cc1f85d8048221d9d26d728994b61237be625e4f
This commit is contained in:
Spencer Ahrens 2017-04-12 16:57:04 -07:00 committed by Facebook Github Bot
parent f25df504ed
commit 76307f47b9
7 changed files with 189 additions and 48 deletions

View File

@ -205,7 +205,7 @@ class FlatListExample extends React.PureComponent {
); );
} }
}; };
_pressItem = (key: number) => { _pressItem = (key: string) => {
this._listRef.getNode().recordInteraction(); this._listRef.getNode().recordInteraction();
pressItem(this, key); pressItem(this, key);
}; };

View File

@ -37,7 +37,7 @@ const {
View, View,
} = ReactNative; } = 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<Item> { function genItemData(count: number, start: number = 0): Array<Item> {
const dataBlob = []; const dataBlob = [];
@ -46,7 +46,7 @@ function genItemData(count: number, start: number = 0): Array<Item> {
dataBlob.push({ dataBlob.push({
title: 'Item ' + ii, title: 'Item ' + ii,
text: LOREM_IPSUM.substr(0, itemHash % 301 + 20), text: LOREM_IPSUM.substr(0, itemHash % 301 + 20),
key: ii, key: String(ii),
pressed: false, pressed: false,
}); });
} }
@ -61,7 +61,7 @@ class ItemComponent extends React.PureComponent {
fixedHeight?: ?boolean, fixedHeight?: ?boolean,
horizontal?: ?boolean, horizontal?: ?boolean,
item: Item, item: Item,
onPress: (key: number) => void, onPress: (key: string) => void,
onShowUnderlay?: () => void, onShowUnderlay?: () => void,
onHideUnderlay?: () => void, onHideUnderlay?: () => void,
}; };
@ -199,12 +199,13 @@ function getItemLayout(data: any, index: number, horizontal?: boolean) {
return {length, offset: (length + separator) * index + header, index}; return {length, offset: (length + separator) * index + header, index};
} }
function pressItem(context: Object, key: number) { function pressItem(context: Object, key: string) {
const pressed = !context.state.data[key].pressed; const index = Number(key);
const pressed = !context.state.data[index].pressed;
context.setState((state) => { context.setState((state) => {
const newData = [...state.data]; const newData = [...state.data];
newData[key] = { newData[index] = {
...state.data[key], ...state.data[index],
pressed, pressed,
title: 'Item ' + key + (pressed ? ' (pressed)' : ''), title: 'Item ' + key + (pressed ? ' (pressed)' : ''),
}; };

View File

@ -139,7 +139,7 @@ class MultiColumnExample extends React.PureComponent {
infoLog('onViewableItemsChanged: ', info.changed.map((v) => ({...v, item: '...'}))); infoLog('onViewableItemsChanged: ', info.changed.map((v) => ({...v, item: '...'})));
} }
}; };
_pressItem = (key: number) => { _pressItem = (key: string) => {
pressItem(this, key); pressItem(this, key);
}; };
} }

View File

@ -65,11 +65,9 @@ const renderSectionHeader = ({section}) => (
</View> </View>
); );
const CustomSeparatorComponent = ({text}) => ( const CustomSeparatorComponent = ({text, highlighted}) => (
<View> <View style={[styles.customSeparator, highlighted && {backgroundColor: 'rgb(217, 217, 217)'}]}>
<SeparatorComponent />
<Text style={styles.separatorText}>{text}</Text> <Text style={styles.separatorText}>{text}</Text>
<SeparatorComponent />
</View> </View>
); );
@ -130,11 +128,11 @@ class SectionListExample extends React.PureComponent {
<AnimatedSectionList <AnimatedSectionList
ListHeaderComponent={HeaderComponent} ListHeaderComponent={HeaderComponent}
ListFooterComponent={FooterComponent} ListFooterComponent={FooterComponent}
SectionSeparatorComponent={() => SectionSeparatorComponent={({highlighted}) =>
<CustomSeparatorComponent text="SECTION SEPARATOR" /> <CustomSeparatorComponent highlighted={highlighted} text="SECTION SEPARATOR" />
} }
ItemSeparatorComponent={() => ItemSeparatorComponent={({highlighted}) =>
<CustomSeparatorComponent text="ITEM SEPARATOR" /> <CustomSeparatorComponent highlighted={highlighted} text="ITEM SEPARATOR" />
} }
debug={this.state.debug} debug={this.state.debug}
enableVirtualization={this.state.virtualized} enableVirtualization={this.state.virtualized}
@ -147,22 +145,30 @@ class SectionListExample extends React.PureComponent {
stickySectionHeadersEnabled stickySectionHeadersEnabled
sections={[ sections={[
{renderItem: renderStackedItem, key: 's1', data: [ {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: [ {key: 's2', data: [
{noImage: true, title: '1st item', text: 'Section s2', key: '0'}, {noImage: true, title: '1st item', text: 'Section s2', key: 'noimage0'},
{noImage: true, title: '2nd item', text: 'Section s2', key: '1'}, {noImage: true, title: '2nd item', text: 'Section s2', key: 'noimage1'},
]}, ]},
...filteredSectionData, ...filteredSectionData,
]} ]}
style={styles.list}
viewabilityConfig={VIEWABILITY_CONFIG} viewabilityConfig={VIEWABILITY_CONFIG}
/> />
</UIExplorerPage> </UIExplorerPage>
); );
} }
_renderItemComponent = ({item}) => (
<ItemComponent item={item} onPress={this._pressItem} /> _renderItemComponent = ({item, separators}) => (
<ItemComponent
item={item}
onPress={this._pressItem}
onHideUnderlay={separators.unhighlight}
onShowUnderlay={separators.highlight}
/>
); );
// This is called when items change viewability by scrolling into our out of // This is called when items change viewability by scrolling into our out of
// the viewable area. // the viewable area.
_onViewableItemsChanged = (info: { _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({ const styles = StyleSheet.create({
customSeparator: {
backgroundColor: 'rgb(200, 199, 204)',
},
header: { header: {
backgroundColor: '#e9eaed', backgroundColor: '#e9eaed',
}, },
headerText: { headerText: {
padding: 4, padding: 4,
fontWeight: '600',
},
list: {
backgroundColor: 'white',
}, },
optionSection: { optionSection: {
flexDirection: 'row', flexDirection: 'row',
@ -202,8 +216,7 @@ const styles = StyleSheet.create({
separatorText: { separatorText: {
color: 'gray', color: 'gray',
alignSelf: 'center', alignSelf: 'center',
padding: 4, fontSize: 7,
fontSize: 9,
}, },
}); });

View File

@ -27,7 +27,15 @@ type SectionBase<SectionItemT> = {
key: string, key: string,
// Optional props will override list-wide props just for this section. // Optional props will override list-wide props just for this section.
renderItem?: ?(info: {item: SectionItemT, index: number}) => ?React.Element<any>, renderItem?: ?(info: {
item: SectionItemT,
index: number,
separators: {
highlight: () => void,
unhighlight: () => void,
updateProps: (select: 'leading' | 'trailing', newProps: Object) => void,
},
}) => ?React.Element<any>,
ItemSeparatorComponent?: ?ReactClass<any>, ItemSeparatorComponent?: ?ReactClass<any>,
keyExtractor?: (item: SectionItemT) => string, keyExtractor?: (item: SectionItemT) => string,
@ -36,6 +44,18 @@ type SectionBase<SectionItemT> = {
}; };
type RequiredProps<SectionT: SectionBase<any>> = { type RequiredProps<SectionT: SectionBase<any>> = {
/**
* The actual data to render, akin to the `data` prop in [`<FlatList>`](/react-native/docs/flatlist.html).
*
* General shape:
*
* sections: Array<{
* data: Array<SectionItem>,
* key: string,
* renderItem?: ({item: SectionItem, ...}) => ?React.Element<*>,
* ItemSeparatorComponent?: ?ReactClass<{highlighted: boolean, ...}>,
* }>
*/
sections: Array<SectionT>, sections: Array<SectionT>,
}; };
@ -43,9 +63,20 @@ type OptionalProps<SectionT: SectionBase<any>> = {
/** /**
* Default renderer for every item in every section. Can be over-ridden on a per-section basis. * 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<any>, renderItem: (info: {
item: Item,
index: number,
separators: {
highlight: () => void,
unhighlight: () => void,
updateProps: (select: 'leading' | 'trailing', newProps: Object) => void,
},
}) => ?React.Element<any>,
/** /**
* 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<any>, ItemSeparatorComponent?: ?ReactClass<any>,
/** /**
@ -57,7 +88,8 @@ type OptionalProps<SectionT: SectionBase<any>> = {
*/ */
ListFooterComponent?: ?(ReactClass<any> | React.Element<any>), ListFooterComponent?: ?(ReactClass<any> | React.Element<any>),
/** /**
* Rendered in between each section. * Rendered in between each section. Also receives `highlighted`, `leadingItem`, and any custom
* props from `separators.updateProps`.
*/ */
SectionSeparatorComponent?: ?ReactClass<any>, SectionSeparatorComponent?: ?ReactClass<any>,
/** /**

View File

@ -30,7 +30,15 @@ type SectionBase = {
key: string, key: string,
// Optional props will override list-wide props just for this section. // 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<*>, ItemSeparatorComponent?: ?ReactClass<*>,
keyExtractor?: (item: SectionItem) => string, keyExtractor?: (item: SectionItem) => string,
@ -56,7 +64,15 @@ type OptionalProps<SectionT: SectionBase> = {
/** /**
* Default renderer for every item in every section. * 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<any>,
/** /**
* Rendered at the top of each section. * Rendered at the top of each section.
*/ */
@ -204,7 +220,9 @@ class VirtualizedSectionList<SectionT: SectionBase>
const info = this._subExtractor(index); const info = this._subExtractor(index);
if (!info) { if (!info) {
return null; return null;
} else if (info.index == null) { }
const infoIndex = info.index;
if (infoIndex == null) {
const {renderSectionHeader} = this.props; const {renderSectionHeader} = this.props;
return renderSectionHeader ? renderSectionHeader({section: info.section}) : null; return renderSectionHeader ? renderSectionHeader({section: info.section}) : null;
} else { } else {
@ -212,14 +230,30 @@ class VirtualizedSectionList<SectionT: SectionBase>
const SeparatorComponent = this._getSeparatorComponent(index, info); const SeparatorComponent = this._getSeparatorComponent(index, info);
invariant(renderItem, 'no renderItem!'); invariant(renderItem, 'no renderItem!');
return ( return (
<View> <ItemWithSeparator
{renderItem({item, index: info.index || 0})} SeparatorComponent={SeparatorComponent}
{SeparatorComponent && <SeparatorComponent />} LeadingSeparatorComponent={infoIndex === 0
</View> ? this.props.SectionSeparatorComponent
: undefined
}
cellKey={info.key}
index={infoIndex}
item={item}
onUpdateSeparator={this._onUpdateSeparator}
prevCellKey={(this._subExtractor(index - 1) || {}).key}
ref={(ref) => {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<*> { _getSeparatorComponent(index: number, info?: ?Object): ?ReactClass<*> {
info = info || this._subExtractor(index); info = info || this._subExtractor(index);
if (!info) { if (!info) {
@ -229,7 +263,7 @@ class VirtualizedSectionList<SectionT: SectionBase>
const {SectionSeparatorComponent} = this.props; const {SectionSeparatorComponent} = this.props;
const isLastItemInList = index === this.state.childProps.getItemCount() - 1; const isLastItemInList = index === this.state.childProps.getItemCount() - 1;
const isLastItemInSection = info.index === info.section.data.length - 1; const isLastItemInSection = info.index === info.section.data.length - 1;
if (SectionSeparatorComponent && isLastItemInSection && !isLastItemInList) { if (SectionSeparatorComponent && isLastItemInSection) {
return SectionSeparatorComponent; return SectionSeparatorComponent;
} }
if (ItemSeparatorComponent && !isLastItemInSection && !isLastItemInList) { if (ItemSeparatorComponent && !isLastItemInSection && !isLastItemInList) {
@ -277,10 +311,72 @@ class VirtualizedSectionList<SectionT: SectionBase>
return <VirtualizedList {...this.state.childProps} ref={this._captureRef} />; return <VirtualizedList {...this.state.childProps} ref={this._captureRef} />;
} }
_cellRefs = {};
_listRef: VirtualizedList; _listRef: VirtualizedList;
_captureRef = (ref) => { this._listRef = ref; }; _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 &&
<LeadingSeparatorComponent {...this.state.leadingSeparatorProps} />;
const separator = SeparatorComponent && <SeparatorComponent {...this.state.separatorProps} />;
return separator ? <View>{leadingSeparator}{element}{separator}</View> : element;
}
}
function getItem(sections: ?Array<Item>, index: number): ?Item { function getItem(sections: ?Array<Item>, index: number): ?Item {
if (!sections) { if (!sections) {
return null; return null;

View File

@ -67,22 +67,18 @@ exports[`SectionList rendering empty section headers is fine 1`] = `
<View <View
onLayout={[Function]} onLayout={[Function]}
> >
<View>
<item <item
value="i1" value="i1"
/> />
</View> </View>
</View>
<View <View
onLayout={[Function]} onLayout={[Function]}
> >
<View>
<item <item
value="i2" value="i2"
/> />
</View> </View>
</View> </View>
</View>
</RCTScrollView> </RCTScrollView>
`; `;
@ -204,6 +200,7 @@ exports[`SectionList renders all the bells and whistles 1`] = `
onLayout={[Function]} onLayout={[Function]}
> >
<View> <View>
<sectionSeparator />
<itemForSection1 <itemForSection1
value="i1s1" value="i1s1"
/> />
@ -231,6 +228,7 @@ exports[`SectionList renders all the bells and whistles 1`] = `
onLayout={[Function]} onLayout={[Function]}
> >
<View> <View>
<sectionSeparator />
<defaultItem <defaultItem
value="i1s2" value="i1s2"
/> />
@ -244,6 +242,7 @@ exports[`SectionList renders all the bells and whistles 1`] = `
<defaultItem <defaultItem
value="i2s2" value="i2s2"
/> />
<sectionSeparator />
</View> </View>
</View> </View>
<View <View