Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
326 changes: 156 additions & 170 deletions src/components/Picker/BasePicker.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import _ from 'underscore';
import React, {PureComponent} from 'react';
import React, {useContext, useEffect, useImperativeHandle, useRef, useState} from 'react';
import {View} from 'react-native';
import PropTypes from 'prop-types';
import RNPickerSelect from 'react-native-picker-select';
Expand All @@ -12,6 +12,9 @@ import themeColors from '../../styles/themes/default';
import {ScrollContext} from '../ScrollViewWithContext';

const propTypes = {
/** A forwarded ref */
forwardedRef: PropTypes.oneOfType([PropTypes.func, PropTypes.shape({current: PropTypes.instanceOf(React.Component)})]),

/** BasePicker label */
label: PropTypes.string,

Expand Down Expand Up @@ -81,6 +84,7 @@ const propTypes = {
};

const defaultProps = {
forwardedRef: undefined,
label: '',
isDisabled: false,
errorText: '',
Expand All @@ -104,204 +108,186 @@ const defaultProps = {
additionalPickerEvents: () => {},
};

/**
* @property {View} root - a reference to the root View
* @property {Object} picker - a reference to @react-native-picker/picker
*/
class BasePicker extends PureComponent {
constructor(props) {
super(props);
this.state = {
isHighlighted: false,
};

this.onInputChange = this.onInputChange.bind(this);
this.enableHighlight = this.enableHighlight.bind(this);
this.disableHighlight = this.disableHighlight.bind(this);
this.focus = this.focus.bind(this);
this.measureLayout = this.measureLayout.bind(this);

// Windows will reuse the text color of the select for each one of the options
// so we might need to color accordingly so it doesn't blend with the background.
this.placeholder = _.isEmpty(this.props.placeholder)
? {}
: {
...this.props.placeholder,
color: themeColors.pickerOptionsTextColor,
};
}
function BasePicker(props) {
const [isHighlighted, setIsHighlighted] = useState(false);

componentDidMount() {
this.setDefaultValue();
}
// reference to the root View
const root = useRef(null);

componentDidUpdate(prevProps) {
if (prevProps.items === this.props.items) {
return;
}
this.setDefaultValue();
}
// reference to @react-native-picker/picker
const picker = useRef(null);

/**
* Forms use inputID to set values. But BasePicker passes an index as the second parameter to onInputChange
* We are overriding this behavior to make BasePicker work with Form
* @param {String} value
* @param {Number} index
*/
onInputChange(value, index) {
if (this.props.inputID) {
this.props.onInputChange(value);
// Windows will reuse the text color of the select for each one of the options
// so we might need to color accordingly so it doesn't blend with the background.
const placeholder = _.isEmpty(props.placeholder)
? {}
: {
...props.placeholder,
color: themeColors.pickerOptionsTextColor,
};

useEffect(() => {
if (props.value || !props.items || props.items.length !== 1 || !props.onInputChange) {
return;
}

this.props.onInputChange(value, index);
}

setDefaultValue() {
// When there is only 1 element in the selector, we do the user a favor and automatically select it for them
// so they don't have to spend extra time selecting the only possible value.
if (this.props.value || !this.props.items || this.props.items.length !== 1 || !this.props.onInputChange) {
return;
}
this.props.onInputChange(this.props.items[0].value, 0);
}
props.onInputChange(props.items[0].value, 0);

enableHighlight() {
this.setState({
isHighlighted: true,
});
}

disableHighlight() {
this.setState({
isHighlighted: false,
});
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [props.items]);

/**
* Focuses the picker (if configured to do so)
*
* This method is used by Form
*/
focus() {
if (!this.props.shouldFocusPicker) {
return;
}

// Defer the focusing to work around a bug on Mobile Safari, where focusing the `select` element in the same
// task when we scrolled to it left that element in a glitched state, where the dropdown list can't be opened
// until the element gets re-focused
_.defer(() => {
this.picker.focus();
});
}
const context = useContext(ScrollContext);

/**
* Like measure(), but measures the view relative to an ancestor
*
* This method is used by Form when scrolling to the input
*
* @param {Object} relativeToNativeComponentRef - reference to an ancestor
* @param {function(x: number, y: number, width: number, height: number): void} onSuccess - callback called on success
* @param {function(): void} onFail - callback called on failure
* Forms use inputID to set values. But BasePicker passes an index as the second parameter to onInputChange
* We are overriding this behavior to make BasePicker work with Form
* @param {String} value
* @param {Number} index
*/
measureLayout(relativeToNativeComponentRef, onSuccess, onFail) {
if (!this.root) {
const onInputChange = (value, index) => {
if (props.inputID) {
props.onInputChange(value);
return;
}

this.root.measureLayout(relativeToNativeComponentRef, onSuccess, onFail);
}

render() {
const hasError = !_.isEmpty(this.props.errorText);

if (this.props.isDisabled) {
return (
<View>
{Boolean(this.props.label) && (
<Text
style={[styles.textLabelSupporting, styles.mb1]}
numberOfLines={1}
>
{this.props.label}
</Text>
)}
<Text numberOfLines={1}>{this.props.value}</Text>
{Boolean(this.props.hintText) && <Text style={[styles.textLabel, styles.colorMuted, styles.mt2]}>{this.props.hintText}</Text>}
</View>
);
}

props.onInputChange(value, index);
};

const enableHighlight = () => {
setIsHighlighted(true);
};

const disableHighlight = () => {
setIsHighlighted(false);
};

useImperativeHandle(props.forwardedRef, () => ({
/**
* Focuses the picker (if configured to do so)
*
* This method is used by Form
*/
focus() {
if (!props.shouldFocusPicker) {
return;
}

// Defer the focusing to work around a bug on Mobile Safari, where focusing the `select` element in the
// same task when we scrolled to it left that element in a glitched state, where the dropdown list can't
// be opened until the element gets re-focused
_.defer(() => {
picker.current.focus();
});
},

/**
* Like measure(), but measures the view relative to an ancestor
*
* This method is used by Form when scrolling to the input
*
* @param {Object} relativeToNativeComponentRef - reference to an ancestor
* @param {function(x: number, y: number, width: number, height: number): void} onSuccess - callback called on success
* @param {function(): void} onFail - callback called on failure
*/
measureLayout(relativeToNativeComponentRef, onSuccess, onFail) {
if (!root.current) {
return;
}

root.current.measureLayout(relativeToNativeComponentRef, onSuccess, onFail);
},
}));

const hasError = !_.isEmpty(props.errorText);

if (props.isDisabled) {
return (
<>
<View
ref={(el) => (this.root = el)}
style={[
styles.pickerContainer,
this.props.isDisabled && styles.inputDisabled,
...this.props.containerStyles,
this.state.isHighlighted && styles.borderColorFocus,
hasError && styles.borderColorDanger,
]}
>
{this.props.label && (
<Text
pointerEvents="none"
style={[styles.pickerLabel, styles.textLabelSupporting]}
>
{this.props.label}
</Text>
)}
<RNPickerSelect
onValueChange={this.onInputChange}
// We add a text color to prevent white text on white background dropdown items on Windows
items={_.map(this.props.items, (item) => ({...item, color: themeColors.pickerOptionsTextColor}))}
style={this.props.size === 'normal' ? styles.picker(this.props.isDisabled, this.props.backgroundColor) : styles.pickerSmall(this.props.backgroundColor)}
useNativeAndroidPickerStyle={false}
placeholder={this.placeholder}
value={this.props.value}
Icon={() => this.props.icon(this.props.size)}
disabled={this.props.isDisabled}
fixAndroidTouchableBug
onOpen={this.enableHighlight}
onClose={this.disableHighlight}
textInputProps={{
allowFontScaling: false,
}}
pickerProps={{
ref: (el) => (this.picker = el),
onFocus: this.enableHighlight,
onBlur: () => {
this.disableHighlight();
this.props.onBlur();
},
...this.props.additionalPickerEvents(this.enableHighlight, (value, index) => {
this.onInputChange(value, index);
this.disableHighlight();
}),
}}
scrollViewRef={this.context && this.context.scrollViewRef}
scrollViewContentOffsetY={this.context && this.context.contentOffsetY}
/>
</View>
<FormHelpMessage message={this.props.errorText} />
{Boolean(this.props.hintText) && <Text style={[styles.textLabel, styles.colorMuted, styles.mt2]}>{this.props.hintText}</Text>}
</>
<View>
{Boolean(props.label) && (
<Text
style={[styles.textLabelSupporting, styles.mb1]}
numberOfLines={1}
>
{props.label}
</Text>
)}
<Text numberOfLines={1}>{props.value}</Text>
{Boolean(props.hintText) && <Text style={[styles.textLabel, styles.colorMuted, styles.mt2]}>{props.hintText}</Text>}
</View>
);
}

return (
<>
<View
ref={root}
style={[
styles.pickerContainer,
props.isDisabled && styles.inputDisabled,
...props.containerStyles,
isHighlighted && styles.borderColorFocus,
hasError && styles.borderColorDanger,
]}
>
{props.label && (
<Text
pointerEvents="none"
style={[styles.pickerLabel, styles.textLabelSupporting]}
>
{props.label}
</Text>
)}
<RNPickerSelect
onValueChange={onInputChange}
// We add a text color to prevent white text on white background dropdown items on Windows
items={_.map(props.items, (item) => ({...item, color: themeColors.pickerOptionsTextColor}))}
style={props.size === 'normal' ? styles.picker(props.isDisabled, props.backgroundColor) : styles.pickerSmall(props.backgroundColor)}
useNativeAndroidPickerStyle={false}
placeholder={placeholder}
value={props.value}
Icon={() => props.icon(props.size)}
disabled={props.isDisabled}
fixAndroidTouchableBug
onOpen={enableHighlight}
onClose={disableHighlight}
textInputProps={{
allowFontScaling: false,
}}
pickerProps={{
ref: picker,
onFocus: enableHighlight,
onBlur: () => {
disableHighlight();
props.onBlur();
},
...props.additionalPickerEvents(enableHighlight, (value, index) => {
onInputChange(value, index);
disableHighlight();
}),
}}
scrollViewRef={context && context.scrollViewRef}
scrollViewContentOffsetY={context && context.contentOffsetY}
/>
</View>
<FormHelpMessage message={props.errorText} />
{Boolean(props.hintText) && <Text style={[styles.textLabel, styles.colorMuted, styles.mt2]}>{props.hintText}</Text>}
</>
);
}

BasePicker.propTypes = propTypes;
BasePicker.defaultProps = defaultProps;
BasePicker.contextType = ScrollContext;
BasePicker.displayName = 'BasePicker';

export default React.forwardRef((props, ref) => (
<BasePicker
// eslint-disable-next-line react/jsx-props-no-spreading
{...props}
// Forward the ref to BasePicker, as we implement imperative methods there
ref={ref}
forwardedRef={ref}
// eslint-disable-next-line react/prop-types
key={props.inputID}
/>
));