src/index.js
import { Picker } from '@react-native-picker/picker';
import isEqual from 'lodash.isequal';
import isObject from 'lodash.isobject';
import PropTypes from 'prop-types';
import React, { PureComponent } from 'react';
import { Keyboard, Modal, Platform, Text, TextInput, TouchableOpacity, View } from 'react-native';
import { defaultStyles } from './styles';
export default class RNPickerSelect extends PureComponent {
static propTypes = {
onValueChange: PropTypes.func.isRequired,
items: PropTypes.arrayOf(
PropTypes.shape({
label: PropTypes.string.isRequired,
value: PropTypes.any.isRequired,
testID: PropTypes.string,
inputLabel: PropTypes.string,
key: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
color: PropTypes.string,
})
).isRequired,
value: PropTypes.any,
placeholder: PropTypes.shape({
label: PropTypes.string,
value: PropTypes.any,
key: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
color: PropTypes.string,
}),
disabled: PropTypes.bool,
itemKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
style: PropTypes.shape({}),
children: PropTypes.any,
onOpen: PropTypes.func,
useNativeAndroidPickerStyle: PropTypes.bool,
fixAndroidTouchableBug: PropTypes.bool,
darkTheme: PropTypes.bool,
// Custom Modal props (iOS only)
doneText: PropTypes.string,
onDonePress: PropTypes.func,
onUpArrow: PropTypes.func,
onDownArrow: PropTypes.func,
onClose: PropTypes.func,
// Modal props (iOS only)
modalProps: PropTypes.shape({}),
// TextInput props
textInputProps: PropTypes.shape({}),
// Picker props
pickerProps: PropTypes.shape({}),
// Touchable Done props (iOS only)
touchableDoneProps: PropTypes.shape({}),
// Touchable wrapper props
touchableWrapperProps: PropTypes.shape({}),
// Custom Icon
Icon: PropTypes.func,
InputAccessoryView: PropTypes.func,
dropdownItemStyle: PropTypes.shape({}),
activeItemStyle: PropTypes.shape({}),
};
static defaultProps = {
value: undefined,
placeholder: {
label: 'Select an item...',
value: null,
color: '#9EA0A4',
},
disabled: false,
itemKey: null,
style: {},
children: null,
useNativeAndroidPickerStyle: true,
fixAndroidTouchableBug: false,
doneText: 'Done',
onDonePress: null,
onUpArrow: null,
onDownArrow: null,
onOpen: null,
onClose: null,
modalProps: {},
textInputProps: {},
pickerProps: {},
touchableDoneProps: {},
touchableWrapperProps: {},
Icon: null,
InputAccessoryView: null,
darkTheme: false,
dropdownItemStyle: {},
activeItemStyle: {},
};
static handlePlaceholder({ placeholder }) {
if (isEqual(placeholder, {})) {
return [];
}
return [placeholder];
}
static getSelectedItem({ items, key, value }) {
let idx = items.findIndex((item) => {
if (item.key && key) {
return isEqual(item.key, key);
}
if (isObject(item.value) || isObject(value)) {
return isEqual(item.value, value);
}
// convert to string to make sure types match
return isEqual(String(item.value), String(value));
});
if (idx === -1) {
idx = 0;
}
return {
selectedItem: items[idx] || {},
idx,
};
}
constructor(props) {
super(props);
const items = RNPickerSelect.handlePlaceholder({
placeholder: props.placeholder,
}).concat(props.items);
const { selectedItem } = RNPickerSelect.getSelectedItem({
items,
key: props.itemKey,
value: props.value,
});
this.state = {
items,
selectedItem,
showPicker: false,
animationType: undefined,
orientation: 'portrait',
doneDepressed: false,
};
this.onUpArrow = this.onUpArrow.bind(this);
this.onDownArrow = this.onDownArrow.bind(this);
this.onValueChange = this.onValueChange.bind(this);
this.onOrientationChange = this.onOrientationChange.bind(this);
this.setInputRef = this.setInputRef.bind(this);
this.togglePicker = this.togglePicker.bind(this);
this.renderInputAccessoryView = this.renderInputAccessoryView.bind(this);
}
componentDidUpdate = (prevProps, prevState) => {
// update items if items or placeholder prop changes
const items = RNPickerSelect.handlePlaceholder({
placeholder: this.props.placeholder,
}).concat(this.props.items);
const itemsChanged = !isEqual(prevState.items, items);
// update selectedItem if value prop is defined and differs from currently selected item
const { selectedItem, idx } = RNPickerSelect.getSelectedItem({
items,
key: this.props.itemKey,
value: this.props.value,
});
const selectedItemChanged =
!isEqual(this.props.value, undefined) && !isEqual(prevState.selectedItem, selectedItem);
if (itemsChanged || selectedItemChanged) {
this.props.onValueChange(selectedItem.value, idx);
this.setState({
...(itemsChanged ? { items } : {}),
...(selectedItemChanged ? { selectedItem } : {}),
});
}
};
onUpArrow() {
const { onUpArrow } = this.props;
this.togglePicker(false, onUpArrow);
}
onDownArrow() {
const { onDownArrow } = this.props;
this.togglePicker(false, onDownArrow);
}
onValueChange(value, index) {
const { onValueChange } = this.props;
onValueChange(value, index);
this.setState((prevState) => {
return {
selectedItem: prevState.items[index],
};
});
}
onOrientationChange({ nativeEvent }) {
this.setState({
orientation: nativeEvent.orientation,
});
}
setInputRef(ref) {
this.inputRef = ref;
}
getPlaceholderStyle() {
const { placeholder, style } = this.props;
const { selectedItem } = this.state;
if (!isEqual(placeholder, {}) && selectedItem.label === placeholder.label) {
return {
...defaultStyles.placeholder,
...style.placeholder,
};
}
return {};
}
isDarkTheme() {
const { darkTheme } = this.props;
return Platform.OS === 'ios' && darkTheme;
}
triggerOpenCloseCallbacks(donePressed) {
const { onOpen, onClose } = this.props;
const { showPicker } = this.state;
if (!showPicker && onOpen) {
onOpen();
}
if (showPicker && onClose) {
onClose(donePressed);
}
}
togglePicker(animate = false, postToggleCallback, donePressed = false) {
const { modalProps, disabled } = this.props;
const { showPicker } = this.state;
if (disabled) {
return;
}
if (!showPicker) {
Keyboard.dismiss();
}
const animationType =
modalProps && modalProps.animationType ? modalProps.animationType : 'slide';
this.triggerOpenCloseCallbacks(donePressed);
this.setState(
(prevState) => {
return {
animationType: animate ? animationType : undefined,
showPicker: !prevState.showPicker,
};
},
() => {
if (postToggleCallback) {
postToggleCallback();
}
}
);
}
renderPickerItems() {
const { items, selectedItem } = this.state;
const defaultItemColor = this.isDarkTheme() ? '#fff' : undefined;
const { dropdownItemStyle, activeItemStyle } = this.props;
return items.map((item) => {
return (
<Picker.Item
style={selectedItem.value === item.value ? activeItemStyle : dropdownItemStyle}
label={item.label}
value={item.value}
key={item.key || item.label}
color={item.color || defaultItemColor}
testID={item.testID}
/>
);
});
}
renderInputAccessoryView() {
const {
InputAccessoryView,
doneText,
onUpArrow,
onDownArrow,
onDonePress,
style,
touchableDoneProps,
} = this.props;
const { doneDepressed } = this.state;
if (InputAccessoryView) {
return <InputAccessoryView testID="custom_input_accessory_view" />;
}
return (
<View
style={[
defaultStyles.modalViewMiddle,
this.isDarkTheme() ? defaultStyles.modalViewMiddleDark : {},
this.isDarkTheme() ? style.modalViewMiddleDark : style.modalViewMiddle,
]}
testID="input_accessory_view"
>
<View style={[defaultStyles.chevronContainer, style.chevronContainer]}>
<TouchableOpacity
activeOpacity={onUpArrow ? 0.5 : 1}
onPress={onUpArrow ? this.onUpArrow : null}
>
<View
style={[
defaultStyles.chevron,
this.isDarkTheme() ? defaultStyles.chevronDark : {},
this.isDarkTheme() ? style.chevronDark : style.chevron,
defaultStyles.chevronUp,
style.chevronUp,
onUpArrow ? [defaultStyles.chevronActive, style.chevronActive] : {},
]}
/>
</TouchableOpacity>
<TouchableOpacity
activeOpacity={onDownArrow ? 0.5 : 1}
onPress={onDownArrow ? this.onDownArrow : null}
>
<View
style={[
defaultStyles.chevron,
this.isDarkTheme() ? defaultStyles.chevronDark : {},
this.isDarkTheme() ? style.chevronDark : style.chevron,
defaultStyles.chevronDown,
style.chevronDown,
onDownArrow ? [defaultStyles.chevronActive, style.chevronActive] : {},
]}
/>
</TouchableOpacity>
</View>
<TouchableOpacity
testID="done_button"
onPress={() => {
this.togglePicker(true, onDonePress, true);
}}
onPressIn={() => {
this.setState({ doneDepressed: true });
}}
onPressOut={() => {
this.setState({ doneDepressed: false });
}}
hitSlop={{
top: 4,
right: 4,
bottom: 4,
left: 4,
}}
{...touchableDoneProps}
>
<View testID="needed_for_touchable">
<Text
testID="done_text"
allowFontScaling={false}
style={[
defaultStyles.done,
this.isDarkTheme() ? defaultStyles.doneDark : {},
this.isDarkTheme() ? style.doneDark : style.done,
doneDepressed ? [defaultStyles.doneDepressed, style.doneDepressed] : {},
]}
>
{doneText}
</Text>
</View>
</TouchableOpacity>
</View>
);
}
renderIcon() {
const { style, Icon } = this.props;
if (!Icon) {
return null;
}
return (
<View testID="icon_container" style={[defaultStyles.iconContainer, style.iconContainer]}>
<Icon testID="icon" />
</View>
);
}
renderTextInputOrChildren() {
const { children, style, textInputProps } = this.props;
const { selectedItem } = this.state;
const containerStyle =
Platform.OS === 'ios' ? style.inputIOSContainer : style.inputAndroidContainer;
if (children) {
return (
<View pointerEvents="box-only" style={containerStyle}>
{children}
</View>
);
}
return (
<View pointerEvents="box-only" style={containerStyle}>
<TextInput
testID="text_input"
style={[
Platform.OS === 'ios' ? style.inputIOS : style.inputAndroid,
this.getPlaceholderStyle(),
]}
value={selectedItem.inputLabel ? selectedItem.inputLabel : selectedItem.label}
ref={this.setInputRef}
editable={false}
{...textInputProps}
/>
{this.renderIcon()}
</View>
);
}
renderIOS() {
const { style, modalProps, pickerProps, touchableWrapperProps } = this.props;
const { animationType, orientation, selectedItem, showPicker } = this.state;
return (
<View style={[defaultStyles.viewContainer, style.viewContainer]}>
<TouchableOpacity
testID="ios_touchable_wrapper"
onPress={() => {
this.togglePicker(true);
}}
activeOpacity={1}
{...touchableWrapperProps}
>
{this.renderTextInputOrChildren()}
</TouchableOpacity>
<Modal
testID="ios_modal"
visible={showPicker}
transparent
animationType={animationType}
supportedOrientations={['portrait', 'landscape']}
onOrientationChange={this.onOrientationChange}
{...modalProps}
>
<TouchableOpacity
style={[defaultStyles.modalViewTop, style.modalViewTop]}
testID="ios_modal_top"
onPress={() => {
this.togglePicker(true);
}}
/>
{this.renderInputAccessoryView()}
<View
style={[
defaultStyles.modalViewBottom,
this.isDarkTheme() ? defaultStyles.modalViewBottomDark : {},
{ height: orientation === 'portrait' ? 215 : 162 },
this.isDarkTheme() ? style.modalViewBottomDark : style.modalViewBottom,
]}
>
<Picker
testID="ios_picker"
onValueChange={this.onValueChange}
selectedValue={selectedItem.value}
{...pickerProps}
>
{this.renderPickerItems()}
</Picker>
</View>
</Modal>
</View>
);
}
renderAndroidHeadless() {
const {
disabled,
Icon,
style,
pickerProps,
onOpen,
touchableWrapperProps,
fixAndroidTouchableBug,
} = this.props;
const { selectedItem } = this.state;
const Component = fixAndroidTouchableBug ? View : TouchableOpacity;
return (
<Component
testID="android_touchable_wrapper"
onPress={onOpen}
activeOpacity={1}
{...touchableWrapperProps}
>
<View style={style.headlessAndroidContainer}>
{this.renderTextInputOrChildren()}
<Picker
style={[
Icon ? { backgroundColor: 'transparent' } : {}, // to hide native icon
defaultStyles.headlessAndroidPicker,
style.headlessAndroidPicker,
]}
testID="android_picker_headless"
enabled={!disabled}
onValueChange={this.onValueChange}
selectedValue={selectedItem.value}
{...pickerProps}
>
{this.renderPickerItems()}
</Picker>
</View>
</Component>
);
}
renderAndroidNativePickerStyle() {
const { disabled, Icon, style, pickerProps } = this.props;
const { selectedItem } = this.state;
return (
<View style={[defaultStyles.viewContainer, style.viewContainer]}>
<Picker
style={[
Icon ? { backgroundColor: 'transparent' } : {}, // to hide native icon
style.inputAndroid,
this.getPlaceholderStyle(),
]}
testID="android_picker"
enabled={!disabled}
onValueChange={this.onValueChange}
selectedValue={selectedItem.value}
{...pickerProps}
>
{this.renderPickerItems()}
</Picker>
{this.renderIcon()}
</View>
);
}
renderWeb() {
const { disabled, style, pickerProps } = this.props;
const { selectedItem } = this.state;
return (
<View style={[defaultStyles.viewContainer, style.viewContainer]}>
<Picker
style={[style.inputWeb]}
testID="web_picker"
enabled={!disabled}
onValueChange={this.onValueChange}
selectedValue={selectedItem.value}
{...pickerProps}
>
{this.renderPickerItems()}
</Picker>
{this.renderIcon()}
</View>
);
}
render() {
const { children, useNativeAndroidPickerStyle } = this.props;
if (Platform.OS === 'ios') {
return this.renderIOS();
}
if (Platform.OS === 'web') {
return this.renderWeb();
}
if (children || !useNativeAndroidPickerStyle) {
return this.renderAndroidHeadless();
}
return this.renderAndroidNativePickerStyle();
}
}
export { defaultStyles };