react-native-material-dropdown
Version:
432 lines (349 loc) • 9.95 kB
JavaScript
import PropTypes from 'prop-types';
import React, { PureComponent } from 'react';
import {
Text,
View,
ScrollView,
Animated,
Modal,
TouchableWithoutFeedback,
Dimensions,
Platform,
ViewPropTypes,
} from 'react-native';
import Ripple from 'react-native-material-ripple';
import { TextField } from 'react-native-material-textfield';
import DropdownItem from '../item';
import styles from './styles';
const minMargin = 8;
const maxMargin = 16;
export default class Dropdown extends PureComponent {
static defaultProps = {
disabled: false,
rippleOpacity: 0.54,
shadeOpacity: 0.12,
animationDuration: 225,
fontSize: 16,
textColor: 'rgba(0, 0, 0, .87)',
itemColor: 'rgba(0, 0, 0, .54)',
baseColor: 'rgba(0, 0, 0, .38)',
itemCount: 4,
};
static propTypes = {
disabled: PropTypes.bool,
rippleOpacity: PropTypes.number,
shadeOpacity: PropTypes.number,
animationDuration: PropTypes.number,
fontSize: PropTypes.number,
value: PropTypes.string,
data: PropTypes.arrayOf(PropTypes.shape({
value: PropTypes.string,
})),
textColor: PropTypes.string,
itemColor: PropTypes.string,
baseColor: PropTypes.string,
itemCount: PropTypes.number,
onFocus: PropTypes.func,
onBlur: PropTypes.func,
onChangeText: PropTypes.func,
containerStyle: (ViewPropTypes || View.propTypes).style,
};
constructor(props) {
super(props);
this.onPress = this.onPress.bind(this);
this.onClose = this.onClose.bind(this);
this.onSelect = this.onSelect.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.blur = this.onClose;
this.focus = this.onPress;
let { value } = this.props;
this.mounted = false;
this.state = {
opacity: new Animated.Value(0),
selected: -1,
modal: false,
value,
};
}
componentWillReceiveProps({ value }) {
if (value !== this.props.value) {
this.setState({ value });
}
}
componentDidMount() {
this.mounted = true;
}
componentWillUnmount() {
this.mounted = false;
}
onPress(event) {
let { data = [], disabled, onFocus, animationDuration } = this.props;
if (disabled) {
return;
}
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 -= 16;
/* Start ripple directly from event */
this.ripple.startRipple(event);
}
if (!itemCount) {
return;
}
if ('function' === typeof onFocus) {
onFocus();
}
let dimensions = Dimensions.get('window');
this.container.measureInWindow((x, y, containerWidth, containerHeight) => {
let { opacity } = this.state;
let delay = Math.max(0, animationDuration - (Date.now() - timestamp));
let selected = this.selectedIndex();
let offset = 0;
if (itemCount > visibleItemCount) {
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);
}
}
}
let left = x - maxMargin;
let leftInset;
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;
}
this.setState({
modal: true,
width: right - left,
top: y + Platform.select({ ios: 1, android: 2 }) + 24,
left,
leftInset,
rightInset,
selected,
});
setTimeout((() => {
if (this.mounted) {
this.scroll
.scrollTo({ x: 0, y: offset, animated: false });
Animated
.timing(opacity, {
duration: animationDuration,
toValue: 1,
})
.start();
}
}), delay);
});
}
onClose() {
let { onBlur, animationDuration } = this.props;
let { opacity } = this.state;
Animated
.timing(opacity, {
duration: animationDuration,
toValue: 0,
})
.start(() => {
if ('function' === typeof onBlur) {
onBlur();
}
if (this.mounted) {
this.setState({ modal: false });
}
});
}
onSelect(index) {
let { data, onChangeText, animationDuration } = this.props;
let { value } = data[index];
this.setState({ value });
if ('function' === typeof onChangeText) {
onChangeText(value, index, data);
}
setTimeout(this.onClose, animationDuration);
}
isFocused() {
return this.state.modal;
}
selectedIndex() {
let { data = [] } = this.props;
return data
.findIndex(({ value }) => value === this.state.value);
}
selectedItem() {
let { data = [] } = this.props;
return data
.find(({ value }) => value === this.state.value);
}
itemSize() {
let { fontSize } = this.props;
return fontSize * 1.5 + 16;
}
visibleItemCount() {
let { data = [], itemCount } = this.props;
return Math.min(data.length, itemCount);
}
tailItemCount() {
return Math.max(this.visibleItemCount() - 2, 0);
}
updateRef(name, ref) {
this[name] = ref;
}
renderAccessory() {
let { baseColor: backgroundColor } = this.props;
let triangleStyle = { backgroundColor };
return (
<View style={styles.accessory}>
<View style={styles.triangleContainer}>
<View style={[styles.triangle, triangleStyle]} />
</View>
</View>
);
}
renderItems() {
let { selected, leftInset, rightInset } = this.state;
let {
data = [],
textColor,
itemColor,
baseColor,
fontSize,
animationDuration,
rippleOpacity,
shadeOpacity,
} = this.props;
let props = {
baseColor,
fontSize,
animationDuration,
rippleOpacity,
shadeOpacity,
onPress: this.onSelect,
style: {
height: this.itemSize(),
paddingLeft: leftInset,
paddingRight: rightInset,
},
};
return data
.map(({ value }, index) => {
let color = ~selected?
index === selected?
textColor:
itemColor:
textColor;
return (
<DropdownItem index={index} key={index} {...props}>
<Text style={{ color, fontSize }} numberOfLines={1}>{value}</Text>
</DropdownItem>
);
});
}
render() {
let { value, left, top, width, opacity, selected, modal } = this.state;
let { data = [], rippleOpacity, containerStyle, ...props } = this.props;
let { baseColor, animationDuration } = props;
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 = 16 + itemSize * visibleItemCount;
let translateY = -8;
switch (selected) {
case -1:
translateY -= 1 === itemCount? 0 : itemSize;
break;
case 0:
break;
default:
if (selected >= itemCount - tailItemCount) {
translateY -= (visibleItemCount - (itemCount - selected)) * itemSize;
} else {
translateY -= itemSize;
}
}
let pickerStyle = {
width,
height,
top,
left,
opacity,
transform: [{ translateY }],
};
let rippleStyle = {
position: 'absolute',
top: 16,
left: 0,
right: 0,
height: itemSize + 8,
};
return (
<View onLayout={() => undefined} ref={this.updateContainerRef} style={containerStyle}>
<TouchableWithoutFeedback onPress={this.onPress}>
<View pointerEvents='box-only'>
<TextField
{...props}
value={value}
editable={false}
onChangeText={undefined}
renderAccessory={this.renderAccessory}
/>
<Ripple
style={rippleStyle}
rippleColor={baseColor}
rippleDuration={animationDuration * 2}
rippleOpacity={rippleOpacity}
rippleSequential={true}
ref={this.updateRippleRef}
/>
</View>
</TouchableWithoutFeedback>
<Modal visible={modal} transparent={true} onRequestClose={this.onClose}>
<TouchableWithoutFeedback onPress={this.onClose}>
<View style={overlayStyle}>
<Animated.View style={[styles.picker, pickerStyle]}>
<ScrollView
ref={this.updateScrollRef}
style={styles.scroll}
scrollEnabled={visibleItemCount < itemCount}
contentContainerStyle={styles.scrollContainer}
>
{this.renderItems()}
</ScrollView>
</Animated.View>
</View>
</TouchableWithoutFeedback>
</Modal>
</View>
);
}
}