UNPKG

react-native-dropdown-autocomplete-revised

Version:

[npm-badge]: https://img.shields.io/npm/v/react-native-dropdown-autocomplete.svg?colorA=6b7c93&colorB=5ab1b8&style=flat-square [npm-url]: https://www.npmjs.com/package/react-native-dropdown-autocomplete [npm-downloads]: https://img.shields.io/npm/dt/react

609 lines (510 loc) 15.1 kB
import React, { PureComponent } from 'react' import { View, Animated, Dimensions, Platform, FlatList, Text, ViewPropTypes } from 'react-native' import PropTypes from 'prop-types' import Ripple from 'react-native-material-ripple' import DropdownItem from '../DropdownItem' import styles from './Dropdown.styles' import { capitalizeFirstLetter, highlightString } from '../../utils/string' import { NO_DATA } from '../../constants/Autocomplete' import { theme } from '../../constants/Theme' import locales from '../../constants/Locales' export default class Dropdown extends PureComponent { constructor(props) { super(props) this.onPress = this.onPress.bind(this) this.onClose = this.onClose.bind(this) this.onSelect = this.onSelect.bind(this) this.onLayout = this.onLayout.bind(this) this.renderSeparator = this.renderSeparator.bind(this) this.renderFooter = this.renderFooter.bind(this) this.updateRippleRef = this.updateRef.bind(this, 'ripple') this.updateScrollRef = this.updateRef.bind(this, 'scroll') this.renderItem = this.renderItem.bind(this) this.keyExtractor = this.keyExtractor.bind(this) this.blur = () => this.onClose() this.focus = this.onPress const { value } = this.props this.mounted = false this.focused = false this.state = { opacity: new Animated.Value(0), selected: -1, modal: false, value, } this.renderHeader = this.renderHeader.bind(this) } componentWillReceiveProps(nextProps) { const { value, data } = this.props if (nextProps.value !== value) { this.setState({ value: nextProps.value }) } if (nextProps.data !== data) { this.setState({ data: nextProps.data, modal: true }) } } componentDidMount() { this.mounted = true } componentWillUnmount() { this.mounted = false } onPress(container) { const { data, disabled, onFocus, itemPadding, rippleDuration, dropdownOffset, dropdownMargins: { min: minMargin, max: maxMargin }, animationDuration, onDropdownShow, useNativeDriver, } = this.props if (disabled) { return } const itemCount = data.length const timestamp = Date.now() if (!itemCount) { return } this.focused = true if (typeof onFocus === 'function') { onFocus() } const dimensions = Dimensions.get('window') container.measureInWindow((x, y, containerWidth) => { const { opacity } = this.state const delay = Math.max(0, rippleDuration - animationDuration - (Date.now() - timestamp)) const selected = this.selectedIndex() let leftInset let left = x + dropdownOffset.left - maxMargin if (left > minMargin) { leftInset = maxMargin } else { left = minMargin leftInset = minMargin } let right = x + containerWidth + maxMargin let rightInset if (dimensions.width - right > minMargin) { rightInset = maxMargin } else { right = dimensions.width - minMargin rightInset = minMargin } const top = y + dropdownOffset.top - itemPadding this.setState({ modal: true, width: right - left, top, left, leftInset, rightInset, selected, }) setTimeout(() => { if (this.mounted) { this.resetScrollOffset() Animated.timing(opacity, { duration: animationDuration, toValue: 1, useNativeDriver: true, }).start(() => { if (this.mounted && Platform.OS === 'ios') { const { flashScrollIndicators } = this.scroll || {} if (typeof flashScrollIndicators === 'function') { flashScrollIndicators.call(this.scroll) } if (typeof onDropdownShow === 'function') { onDropdownShow() } } }) } }, delay) }) } close() { const { onBlur, onDropdownClose } = this.props if (typeof onBlur === 'function') { onBlur() } if (typeof onDropdownClose === 'function') { onDropdownClose() } if (this.mounted) { this.setState({ modal: false }) } } onClose(val) { const { onBlur, onDropdownClose } = this.props const { value } = this.state const finalValue = val || value if (typeof onBlur === 'function') { onBlur() } if (typeof onDropdownClose === 'function') { onDropdownClose() } if (this.mounted) { this.setState({ value: finalValue, modal: false }) } } onSelect(index) { const { data, onChangeValue, animationDuration, rippleDuration } = this.props const value = data[index] const delay = Math.max(0, rippleDuration - animationDuration) if (typeof onChangeValue === 'function') { onChangeValue(value) } // setTimeout(() => this.onClose(value), delay); this.onClose(value) } onLayout(event) { const { onLayout } = this.props if (typeof onLayout === 'function') { onLayout(event) } } value() { const { value } = this.state return value } selectedIndex() { const { data } = this.props return data.findIndex((item) => item != null) } itemSize() { const { fontSize, itemPadding } = this.props return Math.ceil(fontSize * 1.5 + itemPadding * 2) } visibleItemCount() { const { data, itemCount, listHeader } = this.props const properLength = listHeader ? data.length + 1 : data.length return Math.min(properLength, itemCount) } tailItemCount() { return Math.max(this.visibleItemCount() - 2, 0) } rippleInsets() { const { rippleInsets } = this.props const { top = 16, right = 0, bottom = -8, left = 0 } = rippleInsets || {} return { top, right, bottom, left } } resetScrollOffset() { const { selected } = this.state const { data, dropdownPosition } = this.props let offset = 0 const itemCount = data.length const itemSize = this.itemSize() const tailItemCount = this.tailItemCount() const visibleItemCount = this.visibleItemCount() if (itemCount > visibleItemCount) { if (dropdownPosition == null) { switch (selected) { case -1: break case 0: case 1: break default: if (selected >= itemCount - tailItemCount) { offset = itemSize * (itemCount - visibleItemCount) } else { offset = itemSize * (selected - 1) } } } else { let index = selected - dropdownPosition if (dropdownPosition < 0) { index -= visibleItemCount } index = Math.max(0, index) index = Math.min(index, itemCount - visibleItemCount) if (selected >= 0) { offset = itemSize * index } } } if (this.scroll) { this.scroll.scrollToOffset({ offset, animated: false }) } } updateRef(name, ref) { this[name] = ref } keyExtractor(item, index) { const { valueExtractor } = this.props return `${index}-${valueExtractor(item, index)}` } renderRipple() { const { baseColor, rippleColor = baseColor, rippleOpacity, rippleDuration, rippleCentered, rippleSequential, } = this.props const { bottom, ...insets } = this.rippleInsets() const style = { ...insets, height: this.itemSize() - bottom, position: 'absolute', } return ( <Ripple style={style} rippleColor={rippleColor} rippleDuration={rippleDuration} rippleOpacity={rippleOpacity} rippleCentered={rippleCentered} rippleSequential={rippleSequential} ref={this.updateRippleRef} /> ) } renderEmptyItem() { const { noDataText, noDataTextStyle } = this.props return ( <DropdownItem index={0} style={{ paddingLeft: 15 }}> <Text style={[styles.listItemText, styles.noData, noDataTextStyle]}>{noDataText}</Text> </DropdownItem> ) } renderItem({ item, index }) { const { highlightText, inputValue, rightContent, valueExtractor, highLightColor } = this.props if (item === NO_DATA) { return this.renderEmptyItem() } let text if (highlightText) { text = highlightString( String(valueExtractor(item)), inputValue, highLightColor || theme.primary, ) } else { text = capitalizeFirstLetter(String(valueExtractor(item))) } if (item == null) { return null } const { propsExtractor, baseColor, rippleOpacity, rippleDuration, shadeOpacity, rightTextExtractor, listItemTextStyle, rightContentStyle, rightContentItemStyle, } = this.props const props = !propsExtractor(item, index) && { rippleDuration, rippleOpacity, rippleColor: baseColor, shadeColor: baseColor, shadeOpacity, ...props, onPress: this.onSelect, } props.style = [ props.style, { height: this.itemSize(), paddingLeft: 15, }, rightContent ? { display: 'flex', flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', } : {}, ] return ( <DropdownItem index={index} {...props}> <Text style={[styles.listItemText, rightContent ? { maxWidth: 200 } : {}, listItemTextStyle]} numberOfLines={1} ellipsizeMode='tail'> {text} </Text> {rightContent && ( <View style={[styles.rightContent, rightContentStyle]}> <Text key={item.id} style={[styles.rightContentItem, rightContentItemStyle]}> {rightTextExtractor(item)} </Text> </View> )} </DropdownItem> ) } renderSeparator() { const { separatorStyle } = this.props return <View style={[styles.separator, separatorStyle]} /> } renderFooter() { const { listFooterStyle } = this.props return <View style={[styles.listItem, styles.listFooter, listFooterStyle]} /> } renderHeader() { const { listHeader, listHeaderStyle, listHeaderTextStyle } = this.props return listHeader ? ( <View style={[styles.listItem, styles.listHeader, listHeaderStyle]}> <Text style={[styles.listHeaderText, listHeaderTextStyle]} key={listHeader}> {listHeader.toUpperCase()} </Text> </View> ) : null } render() { const { containerStyle, scrollStyle, overlayStyle: overlayStyleOverrides, pickerStyle: pickerStyleOverrides, supportedOrientations, ...props } = this.props const { data, itemPadding } = props const { left, top, width, modal } = this.state const itemCount = data.length const visibleItemCount = this.visibleItemCount() const itemSize = this.itemSize() const height = 2 * itemPadding + itemSize * visibleItemCount const translateY = -itemPadding const pickerStyle = { width, height, left, transform: [{ translateY }], } const itemData = itemCount ? data : [NO_DATA] return ( <View onLayout={this.onLayout} style={containerStyle}> {modal && ( <View style={[styles.picker, pickerStyleOverrides]}> <FlatList keyboardShouldPersistTaps='always' ref={this.updateScrollRef} data={data || [NO_DATA]} style={[styles.scroll, scrollStyle]} renderItem={this.renderItem} keyExtractor={this.keyExtractor} scrollEnabled={visibleItemCount <= itemCount} ItemSeparatorComponent={this.renderSeparator} /> </View> )} </View> ) } } Dropdown.propTypes = { noDataText: PropTypes.string, hitSlop: PropTypes.object, onChangeValue: PropTypes.func, inputValue: PropTypes.string, listHeader: PropTypes.string, disabled: PropTypes.bool, value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), data: PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.object, PropTypes.string])), valueExtractor: PropTypes.func, labelExtractor: PropTypes.func, propsExtractor: PropTypes.func, absoluteRTLLayout: PropTypes.bool, dropdownOffset: PropTypes.shape({ top: PropTypes.number.isRequired, left: PropTypes.number.isRequired, }), dropdownMargins: PropTypes.shape({ min: PropTypes.number.isRequired, max: PropTypes.number.isRequired, }), dropdownPosition: PropTypes.number, rippleColor: PropTypes.string, rippleCentered: PropTypes.bool, rippleSequential: PropTypes.bool, rippleInsets: PropTypes.shape({ top: PropTypes.number, right: PropTypes.number, bottom: PropTypes.number, left: PropTypes.number, }), rippleOpacity: PropTypes.number, shadeOpacity: PropTypes.number, rippleDuration: PropTypes.number, animationDuration: PropTypes.number, fontSize: PropTypes.number, textColor: PropTypes.string, itemColor: PropTypes.string, selectedItemColor: PropTypes.string, disabledItemColor: PropTypes.string, baseColor: PropTypes.string, itemTextStyle: Text.propTypes.style, separatorStyle: (ViewPropTypes || View.propTypes).style, listFooterStyle: (ViewPropTypes || View.propTypes).style, listHeaderStyle: (ViewPropTypes || View.propTypes).style, itemCount: PropTypes.number, itemPadding: PropTypes.number, onLayout: PropTypes.func, onFocus: PropTypes.func, onBlur: PropTypes.func, onChangeText: PropTypes.func, renderBase: PropTypes.func, renderAccessory: PropTypes.func, containerStyle: (ViewPropTypes || View.propTypes).style, overlayStyle: (ViewPropTypes || View.propTypes).style, pickerStyle: (ViewPropTypes || View.propTypes).style, supportedOrientations: PropTypes.arrayOf(PropTypes.string), useNativeDriver: PropTypes.bool, } Dropdown.defaultProps = { noDataText: locales.components.Autocomplete.noData, hitSlop: { top: 6, right: 4, bottom: 6, left: 4 }, disabled: false, data: [], valueExtractor: ({ value } = {}) => value, propsExtractor: () => null, dropdownOffset: { top: 50, left: 20, }, dropdownMargins: { min: 8, max: 16, }, rippleCentered: false, rippleSequential: true, rippleInsets: { top: 16, right: 0, bottom: -8, left: 0, }, rippleOpacity: 0.54, shadeOpacity: 0.12, rippleDuration: 400, animationDuration: 225, fontSize: theme.sizes.size16, textColor: 'rgba(0, 0, 0, .87)', itemColor: 'rgba(0, 0, 0, .54)', baseColor: 'rgba(0, 0, 0, .38)', itemCount: 4, itemPadding: 8, supportedOrientations: [ 'portrait', 'portrait-upside-down', 'landscape', 'landscape-left', 'landscape-right', ], useNativeDriver: false, }