diff --git a/src/components/dropdown/index.js b/src/components/dropdown/index.js index b1e65a1f..16ae5930 100644 --- a/src/components/dropdown/index.js +++ b/src/components/dropdown/index.js @@ -1,17 +1,6 @@ import PropTypes from 'prop-types'; import React, { PureComponent } from 'react'; -import { - Text, - View, - FlatList, - Animated, - Modal, - TouchableWithoutFeedback, - Dimensions, - Platform, - ViewPropTypes, - I18nManager, -} from 'react-native'; +import { Text, View, ScrollView, Animated, Modal, TouchableWithoutFeedback, Dimensions, Platform, ViewPropTypes, I18nManager, Keyboard } from 'react-native'; import Ripple from 'react-native-material-ripple'; import { TextField } from 'react-native-material-textfield'; @@ -28,15 +17,9 @@ export default class Dropdown extends PureComponent { valueExtractor: ({ value } = {}, index) => value, labelExtractor: ({ label } = {}, index) => label, - propsExtractor: () => null, absoluteRTLLayout: false, - dropdownOffset: { - top: 32, - left: 0, - }, - dropdownMargins: { min: 8, max: 16, @@ -55,9 +38,7 @@ export default class Dropdown extends PureComponent { rippleOpacity: 0.54, shadeOpacity: 0.12, - rippleDuration: 400, animationDuration: 225, - fontSize: 16, textColor: 'rgba(0, 0, 0, .87)', @@ -67,15 +48,9 @@ export default class Dropdown extends PureComponent { itemCount: 4, itemPadding: 8, - supportedOrientations: [ - 'portrait', - 'portrait-upside-down', - 'landscape', - 'landscape-left', - 'landscape-right', - ], + labelHeight: 32, - useNativeDriver: false, + supportedOrientations: ['portrait', 'portrait-upside-down', 'landscape', 'landscape-left', 'landscape-right'], }; static propTypes = { @@ -83,24 +58,15 @@ export default class Dropdown extends PureComponent { disabled: PropTypes.bool, - value: PropTypes.oneOfType([ - PropTypes.string, - PropTypes.number, - ]), + value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), data: PropTypes.arrayOf(PropTypes.object), 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, @@ -108,7 +74,6 @@ export default class Dropdown extends PureComponent { dropdownPosition: PropTypes.number, - rippleColor: PropTypes.string, rippleCentered: PropTypes.bool, rippleSequential: PropTypes.bool, @@ -122,15 +87,12 @@ export default class Dropdown extends PureComponent { 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, @@ -138,6 +100,8 @@ export default class Dropdown extends PureComponent { itemCount: PropTypes.number, itemPadding: PropTypes.number, + labelHeight: PropTypes.number, + onLayout: PropTypes.func, onFocus: PropTypes.func, onBlur: PropTypes.func, @@ -147,12 +111,9 @@ export default class Dropdown extends PureComponent { 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, }; constructor(props) { @@ -162,17 +123,12 @@ export default class Dropdown extends PureComponent { this.onClose = this.onClose.bind(this); this.onSelect = this.onSelect.bind(this); this.onLayout = this.onLayout.bind(this); - this.updateRippleRef = this.updateRef.bind(this, 'ripple'); this.updateContainerRef = this.updateRef.bind(this, 'container'); this.updateScrollRef = this.updateRef.bind(this, 'scroll'); - this.renderAccessory = this.renderAccessory.bind(this); - this.renderItem = this.renderItem.bind(this); - - this.keyExtractor = this.keyExtractor.bind(this); - this.blur = () => this.onClose(); + this.blur = this.onClose; this.focus = this.onPress; let { value } = this.props; @@ -194,12 +150,24 @@ export default class Dropdown extends PureComponent { } } + componentWillMount() { + const handler = (e) => { + if (this.state.modal) { + this.onPress(null); + } + }; + this.keyboardDidShowListener = Keyboard.addListener('keyboardDidShow', handler); + this.keyboardDidHideListener = Keyboard.addListener('keyboardDidHide', handler); + } + componentDidMount() { this.mounted = true; } componentWillUnmount() { this.mounted = false; + this.keyboardDidShowListener.remove(); + this.keyboardDidHideListener.remove(); } onPress(event) { @@ -207,13 +175,12 @@ export default class Dropdown extends PureComponent { data, disabled, onFocus, + labelHeight, itemPadding, - rippleDuration, - dropdownOffset, dropdownMargins: { min: minMargin, max: maxMargin }, + dropdownPosition, animationDuration, absoluteRTLLayout, - useNativeDriver, } = this.props; if (disabled) { @@ -221,12 +188,13 @@ export default class Dropdown extends PureComponent { } let itemCount = data.length; + let visibleItemCount = this.visibleItemCount(); + let tailItemCount = this.tailItemCount(); let timestamp = Date.now(); if (null != event) { /* Adjust event location */ event.nativeEvent.locationY -= this.rippleInsets().top; - event.nativeEvent.locationX -= this.rippleInsets().left; /* Start ripple directly from event */ this.ripple.startRipple(event); @@ -252,13 +220,40 @@ export default class Dropdown extends PureComponent { x = dimensions.width - (x + containerWidth); } - let delay = Math.max(0, rippleDuration - animationDuration - (Date.now() - timestamp)); + let delay = Math.max(0, animationDuration - (Date.now() - timestamp)); let selected = this.selectedIndex(); + let offset = 0; + + if (itemCount > visibleItemCount) { + if (null == dropdownPosition) { + switch (selected) { + case -1: + break; + + case 0: + case 1: + break; + + default: + if (selected >= itemCount - tailItemCount) { + offset = this.itemSize() * (itemCount - visibleItemCount); + } else { + offset = this.itemSize() * (selected - 1); + } + } + } else { + if (~selected) { + if (dropdownPosition < 0) { + offset = this.itemSize() * (selected - visibleItemCount - dropdownPosition); + } else { + offset = this.itemSize() * (selected - dropdownPosition); + } + } + } + } + let left = x - maxMargin; let leftInset; - let left = x - + dropdownOffset.left - - maxMargin; if (left > minMargin) { leftInset = maxMargin; @@ -277,9 +272,7 @@ export default class Dropdown extends PureComponent { rightInset = minMargin; } - let top = y - + dropdownOffset.top - - itemPadding; + let top = y + Platform.select({ ios: 1, android: 2 }) + labelHeight - itemPadding; this.setState({ modal: true, @@ -291,70 +284,60 @@ export default class Dropdown extends PureComponent { selected, }); - setTimeout((() => { + setTimeout(() => { if (this.mounted) { - this.resetScrollOffset(); - - Animated - .timing(opacity, { - duration: animationDuration, - toValue: 1, - useNativeDriver, - }) - .start(() => { - if (this.mounted && 'ios' === Platform.OS) { - let { flashScrollIndicators } = this.scroll || {}; - - if ('function' === typeof flashScrollIndicators) { - flashScrollIndicators.call(this.scroll); - } + if (this.scroll) { + this.scroll.scrollTo({ x: 0, y: offset, animated: false }); + } + + Animated.timing(opacity, { + duration: animationDuration, + toValue: 1, + }).start(() => { + if (this.mounted && 'ios' === Platform.OS) { + let { flashScrollIndicators } = this.scroll || {}; + + if ('function' === typeof flashScrollIndicators) { + flashScrollIndicators.call(this.scroll); } - }); + } + }); } - }), delay); + }, delay); }); } - onClose(value = this.state.value) { - let { onBlur, animationDuration, useNativeDriver } = this.props; + onClose() { + let { onBlur, animationDuration } = this.props; let { opacity } = this.state; - Animated - .timing(opacity, { - duration: animationDuration, - toValue: 0, - useNativeDriver, - }) - .start(() => { - this.focused = false; - - if ('function' === typeof onBlur) { - onBlur(); - } + Animated.timing(opacity, { + duration: animationDuration, + toValue: 0, + }).start(() => { + this.focused = false; - if (this.mounted) { - this.setState({ value, modal: false }); - } - }); + if ('function' === typeof onBlur) { + onBlur(); + } + + if (this.mounted) { + this.setState({ modal: false }); + } + }); } onSelect(index) { - let { - data, - valueExtractor, - onChangeText, - animationDuration, - rippleDuration, - } = this.props; - + let { data, valueExtractor, onChangeText, animationDuration } = this.props; let value = valueExtractor(data[index], index); - let delay = Math.max(0, rippleDuration - animationDuration); + + this.setState({ value }); if ('function' === typeof onChangeText) { onChangeText(value, index, data); } - setTimeout(() => this.onClose(value), delay); + setTimeout(this.onClose, animationDuration); } onLayout(event) { @@ -375,8 +358,7 @@ export default class Dropdown extends PureComponent { let { value } = this.state; let { data, valueExtractor } = this.props; - return data - .findIndex((item, index) => null != item && value === valueExtractor(item, index)); + return data.findIndex((item, index) => null != item && value === valueExtractor(item, index)); } selectedItem() { @@ -392,7 +374,7 @@ export default class Dropdown extends PureComponent { itemSize() { let { fontSize, itemPadding } = this.props; - return Math.ceil(fontSize * 1.5 + itemPadding * 2); + return fontSize * 1.5 + itemPadding * 2; } visibleItemCount() { @@ -406,127 +388,41 @@ export default class Dropdown extends PureComponent { } rippleInsets() { - let { - top = 16, - right = 0, - bottom = -8, - left = 0, - } = this.props.rippleInsets || {}; + let { top = 16, right = 0, bottom = -8, left = 0 } = this.props.rippleInsets || {}; return { top, right, bottom, left }; } - resetScrollOffset() { - let { selected } = this.state; - let { data, dropdownPosition } = this.props; - - let offset = 0; - let itemCount = data.length; - let itemSize = this.itemSize(); - let tailItemCount = this.tailItemCount(); - let visibleItemCount = this.visibleItemCount(); - - if (itemCount > visibleItemCount) { - if (null == dropdownPosition) { - 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) { - offset = itemSize * index; - } - } - } - - if (this.scroll) { - this.scroll.scrollToOffset({ offset, animated: false }); - } - } - updateRef(name, ref) { this[name] = ref; } - keyExtractor(item, index) { - let { valueExtractor } = this.props; - - return `${index}-${valueExtractor(item, index)}`; - } - renderBase(props) { let { value } = this.state; - let { - data, - renderBase, - labelExtractor, - dropdownOffset, - renderAccessory = this.renderAccessory, - } = this.props; + let { data, renderBase, labelExtractor, renderAccessory = this.renderAccessory } = this.props; let index = this.selectedIndex(); - let title; + let label; if (~index) { - title = labelExtractor(data[index], index); + label = labelExtractor(data[index], index); } - if (null == title) { - title = value; + if (null == label) { + label = value; } if ('function' === typeof renderBase) { - return renderBase({ ...props, title, value, renderAccessory }); + return renderBase({ ...props, label, value, renderAccessory }); } - title = null == title || 'string' === typeof title? - title: - String(title); - - return ( - - ); + return ; } renderRipple() { - let { - baseColor, - rippleColor = baseColor, - rippleOpacity, - rippleDuration, - rippleCentered, - rippleSequential, - } = this.props; + let { baseColor, animationDuration, rippleOpacity, rippleCentered, rippleSequential } = this.props; let { bottom, ...insets } = this.rippleInsets(); let style = { @@ -536,17 +432,7 @@ export default class Dropdown extends PureComponent { position: 'absolute', }; - return ( - - ); + return ; } renderAccessory() { @@ -562,79 +448,47 @@ export default class Dropdown extends PureComponent { ); } - renderItem({ item, index }) { - if (null == item) { - return null; - } - + renderItems() { let { selected, leftInset, rightInset } = this.state; - let { - valueExtractor, - labelExtractor, - propsExtractor, - textColor, - itemColor, + let { data, valueExtractor, labelExtractor, textColor, itemColor, selectedItemColor = textColor, baseColor, fontSize, itemTextStyle, animationDuration, rippleOpacity, shadeOpacity } = this.props; + + let props = { baseColor, - selectedItemColor = textColor, - disabledItemColor = baseColor, fontSize, - itemTextStyle, + animationDuration, rippleOpacity, - rippleDuration, shadeOpacity, - } = this.props; - - let props = propsExtractor(item, index); - - let { style, disabled } - = props - = { - rippleDuration, - rippleOpacity, - rippleColor: baseColor, - - shadeColor: baseColor, - shadeOpacity, - - ...props, - - onPress: this.onSelect, - }; + onPress: this.onSelect, + style: { + height: this.itemSize(), + paddingLeft: leftInset, + paddingRight: rightInset, + }, + }; - let value = valueExtractor(item, index); - let label = labelExtractor(item, index); + return data.map((item, index) => { + if (null == item) { + return null; + } - let title = null == label? - value: - label; + let value = valueExtractor(item, index); + let label = labelExtractor(item, index); - let color = disabled? - disabledItemColor: - ~selected? - index === selected? - selectedItemColor: - itemColor: - selectedItemColor; + let title = null == label ? value : label; - let textStyle = { color, fontSize }; + let color = ~selected ? (index === selected ? selectedItemColor : itemColor) : selectedItemColor; - props.style = [ - style, - { - height: this.itemSize(), - paddingLeft: leftInset, - paddingRight: rightInset, - }, - ]; + let style = { color, fontSize }; - return ( - - - {title} - - - ); + return ( + + + {title} + + + ); + }); } render() { @@ -642,7 +496,6 @@ export default class Dropdown extends PureComponent { renderBase, renderAccessory, containerStyle, - overlayStyle: overlayStyleOverrides, pickerStyle: pickerStyleOverrides, rippleInsets, @@ -662,27 +515,29 @@ export default class Dropdown extends PureComponent { ...props } = this.props; - let { - data, - disabled, - itemPadding, - dropdownPosition, - } = props; + let { data, disabled, itemPadding, dropdownPosition } = props; let { left, top, width, opacity, selected, modal } = this.state; + let dimensions = Dimensions.get('window'); + let itemCount = data.length; let visibleItemCount = this.visibleItemCount(); let tailItemCount = this.tailItemCount(); let itemSize = this.itemSize(); + let overlayStyle = { + width: dimensions.width, + height: dimensions.height, + }; + let height = 2 * itemPadding + itemSize * visibleItemCount; let translateY = -itemPadding; if (null == dropdownPosition) { switch (selected) { case -1: - translateY -= 1 === itemCount? 0 : itemSize; + translateY -= 1 === itemCount ? 0 : itemSize; break; case 0: @@ -703,13 +558,12 @@ export default class Dropdown extends PureComponent { } } - let overlayStyle = { opacity }; - let pickerStyle = { width, height, top, left, + opacity, transform: [{ translateY }], }; @@ -727,38 +581,22 @@ export default class Dropdown extends PureComponent { return ( - + {this.renderBase(props)} {this.renderRipple()} - - true} - onResponderRelease={this.blur} - > - true} - > - + + + + + + {this.renderItems()} + + - + );