react-native-ui-lib
Version:
<p align="center"> <img src="https://user-images.githubusercontent.com/1780255/105469025-56759000-5ca0-11eb-993d-3568c1fd54f4.png" height="250px" style="display:block"/> </p> <p align="center">UI Toolset & Components Library for React Native</p> <p a
533 lines (481 loc) • 16.9 kB
JavaScript
// TODO: deprecate all places where we check if _.isPlainObject
// TODO: deprecate getItemValue prop
// TODO: deprecate getItemLabel prop
// TODO: Add initialValue prop
// TODO: consider deprecating renderCustomModal prop
// TODO: deprecate onShow cause it's already supported by passing it in pickerModalProps
import _ from 'lodash';
import PropTypes from 'prop-types';
import React, {Component} from 'react';
import memoize from 'memoize-one';
import {Constants, asBaseComponent, forwardRef} from '../../commons';
import {LogService} from '../../services';
// import View from '../../components/view';
import Modal from '../modal';
import ExpandableOverlay from '../../incubator/expandableOverlay';
// import Button from '../../components/button';
import {TextField} from '../inputs';
import TextFieldMigrator from '../textField/TextFieldMigrator';
import NativePicker from './NativePicker';
import PickerModal from './PickerModal';
import PickerItem from './PickerItem';
import PickerContext from './PickerContext';
import {getItemLabel as getItemLabelPresenter, shouldFilterOut} from './PickerPresenter';
const PICKER_MODES = {
SINGLE: 'SINGLE',
MULTI: 'MULTI'
};
const ItemType = PropTypes.oneOfType([
PropTypes.number,
PropTypes.string,
PropTypes.shape({
value: PropTypes.any,
label: PropTypes.string
})
]);
/**
* @description: Picker Component, support single or multiple selection, blurModel and native wheel picker
* @gif: https://github.com/wix/react-native-ui-lib/blob/master/demo/showcase/Picker/Default.gif?raw=true, https://github.com/wix/react-native-ui-lib/blob/master/demo/showcase/Picker/MultiPicker.gif?raw=true, https://github.com/wix/react-native-ui-lib/blob/master/demo/showcase/Picker/NativePicker.gif?raw=true, https://github.com/wix/react-native-ui-lib/blob/master/demo/showcase/Picker/DialogPicker.gif?raw=true, https://github.com/wix/react-native-ui-lib/blob/master/demo/showcase/Picker/CustomPicker.gif?raw=true
* @example: https://github.com/wix/react-native-ui-lib/blob/master/demo/src/screens/componentScreens/PickerScreen.js
*/
class Picker extends Component {
static displayName = 'Picker';
static propTypes = {
/**
* Temporary prop required for migration to Picker's new API
*/
migrate: PropTypes.bool,
/**
* Temporary prop required for inner text field migration
*/
migrateTextField: PropTypes.bool,
...TextField.propTypes,
/**
* Picker current value in the shape of {value: ..., label: ...}, for custom shape use 'getItemValue' prop
*/
value: PropTypes.oneOfType([
ItemType,
PropTypes.arrayOf(ItemType),
PropTypes.object,
PropTypes.string,
PropTypes.number
]),
/**
* Callback for when picker value change
*/
onChange: PropTypes.func,
/**
* SINGLE mode or MULTI mode
*/
mode: PropTypes.oneOf(Object.keys(PICKER_MODES)),
/**
* Limit the number of selected items
*/
selectionLimit: PropTypes.number,
/**
* Adds blur effect to picker modal (iOS only)
*/
enableModalBlur: PropTypes.bool,
/**
* Render custom picker - input will be value (see above)
* Example:
* renderPicker = (selectedItem) => {...}
*/
renderPicker: PropTypes.elementType,
/**
* Render custom picker item
*/
renderItem: PropTypes.elementType,
/**
* Render custom picker modal (e.g ({visible, children, toggleModal}) => {...})
*/
renderCustomModal: PropTypes.elementType,
/**
* Custom picker props (when using renderPicker, will apply on the button wrapper)
*/
customPickerProps: PropTypes.object,
/**
* Add onPress callback for when pressing the picker
*/
onPress: PropTypes.func,
/**
* @deprecated
* A function that extract the unique value out of the value prop in case value has a custom structure (e.g. {myValue, myLabel})
*/
getItemValue: PropTypes.func,
/**
* @deprecated
* A function that extract the label out of the value prop in case value has a custom structure (e.g. {myValue, myLabel})
*/
getItemLabel: PropTypes.func,
/**
* A function that returns the label to show for the selected Picker value
*/
getLabel: PropTypes.func,
/**
* The picker modal top bar props
*/
topBarProps: PropTypes.shape(Modal.TopBar.propTypes),
/**
* Show search input to filter picker items by label
*/
showSearch: PropTypes.bool,
/**
* Style object for the search input (only when passing showSearch)
*/
searchStyle: PropTypes.shape({
color: PropTypes.string,
placeholderTextColor: PropTypes.string,
selectionColor: PropTypes.string
}),
/**
* Placeholder text for the search input (only when passing showSearch)
*/
searchPlaceholder: PropTypes.string,
/**
* callback for picker modal search input text change (only when passing showSearch)
*/
onSearchChange: PropTypes.func,
/**
* Render custom search input (only when passing showSearch)
*/
renderCustomSearch: PropTypes.elementType,
/**
* Allow to use the native picker solution (different style for iOS and Android)
*/
useNativePicker: PropTypes.bool,
/**
* Callback for rendering a custom native picker inside the dialog (relevant to native picker only)
*/
renderNativePicker: PropTypes.elementType,
/**
* Pass props to the list component that wraps the picker options (allows to control FlatList behavior)
*/
listProps: PropTypes.object,
/**
* Pass props to the picker modal
*/
pickerModalProps: PropTypes.object
};
static defaultProps = {
...TextField.defaultProps,
mode: PICKER_MODES.SINGLE
};
static modes = PICKER_MODES;
pickerExpandable = React.createRef();
constructor(props) {
super(props);
this.state = {
selectedItemPosition: 0,
items: Picker.extractPickerItems(props),
multiDraftValue: props.value,
multiFinalValue: props.value
};
if (props.mode === Picker.modes.SINGLE && Array.isArray(props.value)) {
LogService.warn('Picker in SINGLE mode cannot accept an array for value');
}
if (props.mode === Picker.modes.MULTI && !Array.isArray(props.value)) {
LogService.warn('Picker in MULTI mode must accept an array for value');
}
// TODO: this warning should be replaced by the opposite
// we should warn user NOT to pass an object to the value prop
// if (props.useNativePicker && _.isPlainObject(props.value)) {
// console.warn('UILib Picker: don\'t use object as value for native picker, use either string or a number');
// }
if (_.isPlainObject(props.value)) {
LogService.warn('UILib Picker will stop supporting passing object as value in the next major version. Please use either string or a number as value');
}
}
static getDerivedStateFromProps(nextProps, prevState) {
if (nextProps.mode === Picker.modes.MULTI) {
if (prevState.multiFinalValue !== nextProps.value) {
return {multiDraftValue: nextProps.value, multiFinalValue: nextProps.value};
}
}
return null;
}
static extractPickerItems(props) {
const {children} = props;
const items = React.Children.map(children, child => ({value: child.props.value, label: child.props.label}));
return items;
}
getAccessibilityInfo() {
const {placeholder} = this.props;
return {
accessibilityLabel: this.getLabelValueText()
? `${placeholder}. selected. ${this.getLabelValueText()}`
: `Select ${placeholder}`,
accessibilityHint: this.getLabelValueText()
? 'Double tap to edit'
: `Goes to ${placeholder}. Suggestions will be provided`
};
}
getContextValue = () => {
const {multiDraftValue} = this.state;
const {migrate, mode, getItemValue, getItemLabel, renderItem, selectionLimit, value} = this.props;
const pickerValue = !migrate && _.isPlainObject(value) ? value?.value : value;
return {
migrate,
value: mode === Picker.modes.MULTI ? multiDraftValue : pickerValue,
onPress: mode === Picker.modes.MULTI ? this.toggleItemSelection : this.onDoneSelecting,
isMultiMode: mode === Picker.modes.MULTI,
getItemValue,
getItemLabel,
onSelectedLayout: this.onSelectedItemLayout,
renderItem,
selectionLimit
};
};
getLabelValueText = () => {
const {value} = this.props;
return this.getLabel(value);
};
getLabelsFromArray = value => {
const {items} = this.state;
const itemsByValue = _.keyBy(items, 'value');
const {getItemLabel = _.noop} = this.props;
return _.chain(value)
.map(item => (_.isPlainObject(item) ? getItemLabel(item) || item?.label : itemsByValue[item]?.label))
.join(', ')
.value();
};
getLabel = value => {
const {getLabel} = this.props;
if (_.isFunction(getLabel) && !_.isUndefined(getLabel(value))) {
return getLabel(value);
}
if (_.isArray(value)) {
return this.getLabelsFromArray(value);
}
if (_.isPlainObject(value)) {
return _.get(value, 'label');
}
// otherwise, extract from picker items
const {items} = this.state;
const selectedItem = _.find(items, {value});
return _.get(selectedItem, 'label');
};
getFilteredChildren = memoize((children, searchValue) => {
const {getItemLabel: getItemLabelPicker} = this.props;
return _.filter(children, child => {
const {label, value, getItemLabel} = child.props;
const itemLabel = getItemLabelPresenter(label, value, getItemLabel || getItemLabelPicker);
return !shouldFilterOut(searchValue, itemLabel);
});
});
get children() {
const {searchValue} = this.state;
const {children, showSearch} = this.props;
if (showSearch && !_.isEmpty(searchValue)) {
return this.getFilteredChildren(children, searchValue);
}
return children;
}
// handlePickerOnPress = () => {
// this.toggleExpandableModal(true);
// _.invoke(this.props, 'onPress');
// };
// toggleExpandableModal = value => {
// this.setState({showExpandableModal: value});
// this.clearSearchField();
// };
toggleItemSelection = item => {
const {getItemValue, migrate} = this.props;
const {multiDraftValue} = this.state;
let newValue;
if (!migrate) {
newValue = _.xorBy(multiDraftValue, [item], getItemValue || 'value');
} else {
newValue = _.xor(multiDraftValue, [item]);
}
this.setState({multiDraftValue: newValue});
};
cancelSelect = () => {
this.setState({multiDraftValue: this.state.multiFinalValue});
// this.toggleExpandableModal(false);
this.pickerExpandable.current?.closeExpandable?.();
this.props.topBarProps?.onCancel?.();
};
onDoneSelecting = item => {
this.clearSearchField();
this.setState({multiFinalValue: item});
// this.toggleExpandableModal(false);
this.pickerExpandable.current?.closeExpandable?.();
this.props.onChange?.(item);
};
onSearchChange = searchValue => {
this.setState({searchValue});
_.invoke(this.props, 'onSearchChange', searchValue);
};
onSelectedItemLayout = ({
nativeEvent: {
layout: {y}
}
}) => {
this.setState({selectedItemPosition: y});
};
clearSearchField = () => {
this.setState({searchValue: ''});
};
renderCustomModal = ({visible, toggleExpandable}) => {
const {renderCustomModal, children} = this.props;
const {multiDraftValue} = this.state;
if (renderCustomModal) {
const modalProps = {
visible,
toggleModal: toggleExpandable,
onSearchChange: this.onSearchChange,
children,
// onDone is relevant to multi mode only
onDone: () => this.onDoneSelecting(multiDraftValue),
onCancel: this.cancelSelect
};
return renderCustomModal(modalProps);
}
};
// TODO: Rename to renderExpandableContent
renderExpandableModal = () => {
const {
mode,
enableModalBlur,
topBarProps,
showSearch,
onShow,
searchStyle,
searchPlaceholder,
renderCustomSearch,
// renderCustomModal,
listProps,
// children,
testID,
pickerModalProps
} = this.props;
const {showExpandableModal, selectedItemPosition, multiDraftValue} = this.state;
// if (renderCustomModal) {
// const modalProps = {
// visible: showExpandableModal,
// toggleModal: this.toggleExpandableModal,
// onSearchChange: this.onSearchChange,
// children,
// onDone: () => this.onDoneSelecting(multiDraftValue),
// onCancel: this.cancelSelect
// };
// return (
// <>
// {/* <PickerContext.Provider value={this.getContextValue()}> */}
// {renderCustomModal(modalProps)}
// {/* </PickerContext.Provider> */}
// </>
// );
// }
return (
// <PickerContext.Provider value={this.getContextValue()}>
<PickerModal
testID={`${testID}.modal`}
visible={showExpandableModal}
scrollPosition={selectedItemPosition}
enableModalBlur={enableModalBlur}
topBarProps={{
...topBarProps,
onCancel: this.cancelSelect,
onDone: mode === Picker.modes.MULTI ? () => this.onDoneSelecting(multiDraftValue) : undefined
}}
showSearch={showSearch}
searchStyle={searchStyle}
searchPlaceholder={searchPlaceholder}
onSearchChange={this.onSearchChange}
renderCustomSearch={renderCustomSearch}
listProps={listProps}
onShow={onShow}
pickerModalProps={pickerModalProps}
>
{this.children}
</PickerModal>
// </PickerContext.Provider>
);
};
render() {
const {
useNativePicker,
renderPicker,
customPickerProps,
containerStyle,
testID,
onShow,
renderCustomModal,
forwardedRef,
modifiers,
enableModalBlur,
topBarProps,
pickerModalProps,
value,
editable,
migrateTextField
} = this.props;
if (useNativePicker) {
return <NativePicker {...this.props}/>;
}
// if (_.isFunction(renderPicker)) {
// const {value} = this.props;
// return (
// <PickerContext.Provider value={this.getContextValue()}>
// <View left>
// <Button {...customPickerProps} link onPress={this.handlePickerOnPress} testID={testID}>
// {renderPicker(value, this.getLabel(value))}
// </Button>
// {this.renderExpandableModal()}
// </View>
// </PickerContext.Provider>
// );
// }
const textInputProps = TextField.extractOwnProps(this.props);
const label = this.getLabelValueText();
const {paddings, margins, positionStyle} = modifiers;
const modalProps = {
animationType: 'slide',
transparent: Constants.isIOS && enableModalBlur,
enableModalBlur: Constants.isIOS && enableModalBlur,
onRequestClose: topBarProps?.onCancel,
onShow,
...pickerModalProps
};
return (
<PickerContext.Provider value={this.getContextValue()}>
<ExpandableOverlay
ref={this.pickerExpandable}
modalProps={modalProps}
expandableContent={this.renderExpandableModal()}
renderCustomOverlay={renderCustomModal ? this.renderCustomModal : undefined}
testID={testID}
{...customPickerProps}
disabled={editable === false}
>
{renderPicker ? (
renderPicker(value, this.getLabel(value))
) : (
<TextFieldMigrator
migrate={migrateTextField}
customWarning="RNUILib Picker component's internal TextField will soon be replaced with a new implementation, in order to start the migration - please pass to Picker the 'migrateTextField' prop"
ref={forwardedRef}
{...textInputProps}
testID={`${testID}.input`}
containerStyle={[paddings, margins, positionStyle, containerStyle]}
{...this.getAccessibilityInfo()}
importantForAccessibility={'no-hide-descendants'}
value={label}
selection={Constants.isAndroid ? {start: 0} : undefined}
/* Disable TextField expandable feature */
topBarProps={undefined}
// expandable={false}
// renderExpandable={_.noop}
// onToggleExpandableModal={_.noop}
/>
)}
</ExpandableOverlay>
</PickerContext.Provider>
);
}
}
Picker.Item = PickerItem;
export {Picker}; // For tests
export default asBaseComponent(forwardRef(Picker));