Skip to content

Commit

Permalink
Migrate BasePicker to function component
Browse files Browse the repository at this point in the history
  • Loading branch information
cubuspl42 committed Jul 24, 2023
1 parent 82669ae commit 6f216f7
Showing 1 changed file with 155 additions and 170 deletions.
325 changes: 155 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)})]).isRequired,

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

Expand Down Expand Up @@ -104,204 +107,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}
/>
));

0 comments on commit 6f216f7

Please sign in to comment.