diff --git a/.storybook/main.js b/.storybook/main.js
index b229d828bfe8..ad5effb2dbf6 100644
--- a/.storybook/main.js
+++ b/.storybook/main.js
@@ -6,6 +6,7 @@ module.exports = {
addons: [
'@storybook/addon-essentials',
'@storybook/addon-a11y',
+ '@storybook/addon-react-native-web',
],
staticDirs: [
'./public',
diff --git a/package-lock.json b/package-lock.json
index 4df3ccca0171..fc64ebc9d112 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -6710,9 +6710,9 @@
"integrity": "sha512-2Y9OXWHRutYmdyW+bNMaFTW8uTBpaaK20xdIFoVtqahEOO9++B+ut3CAWKPZcdRtb9ikG0LUKChGqMeocg0PGA=="
},
"@react-native-picker/picker": {
- "version": "1.9.11",
- "resolved": "https://registry.npmjs.org/@react-native-picker/picker/-/picker-1.9.11.tgz",
- "integrity": "sha512-E7whvvMIl9Ln1sxgug7OAEVWQ69n82iV0d2OWWp5VV52+RW3azKh1IFm4rxdW5/oByMfl7FFL0eHNelGgY4BMQ=="
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/@react-native-picker/picker/-/picker-2.3.1.tgz",
+ "integrity": "sha512-DAw1o3bHNRnQPImsK53xCYkgC8bH+t9uTM+0JjNIlmMwvVLFnmDxi9v5iBTIWFMWG/pUHQaw66E29wW6oJG9Tw=="
},
"@react-native/assets": {
"version": "1.0.0",
@@ -8454,6 +8454,12 @@
}
}
},
+ "@storybook/addon-react-native-web": {
+ "version": "0.0.18",
+ "resolved": "https://registry.npmjs.org/@storybook/addon-react-native-web/-/addon-react-native-web-0.0.18.tgz",
+ "integrity": "sha512-tMZumiF+Dgk7sngMWFrbDdBN3h5C001traYM46CqNQvtJoZvwYe2MPGEeWy3wk+5D8vOv8TFw2i96f3wwfralg==",
+ "dev": true
+ },
"@storybook/addon-toolbars": {
"version": "6.4.12",
"resolved": "https://registry.npmjs.org/@storybook/addon-toolbars/-/addon-toolbars-6.4.12.tgz",
diff --git a/package.json b/package.json
index 8bf249b3ca1e..ce82dbc84f4f 100644
--- a/package.json
+++ b/package.json
@@ -50,7 +50,7 @@
"@react-native-firebase/crashlytics": "^12.3.0",
"@react-native-firebase/perf": "^12.3.0",
"@react-native-masked-view/masked-view": "^0.2.4",
- "@react-native-picker/picker": "^1.9.11",
+ "@react-native-picker/picker": "^2.3.1",
"@react-navigation/compat": "^5.3.20",
"@react-navigation/drawer": "6.1.8",
"@react-navigation/native": "6.0.8",
@@ -126,6 +126,7 @@
"@react-native-community/eslint-config": "^2.0.0",
"@storybook/addon-a11y": "^6.4.12",
"@storybook/addon-essentials": "^6.4.12",
+ "@storybook/addon-react-native-web": "0.0.18",
"@storybook/addons": "^6.4.12",
"@storybook/react": "^6.4.12",
"@storybook/theming": "^6.4.12",
diff --git a/src/components/AddPlaidBankAccount.js b/src/components/AddPlaidBankAccount.js
index f3837f8ad61d..39d1a8f9dd09 100644
--- a/src/components/AddPlaidBankAccount.js
+++ b/src/components/AddPlaidBankAccount.js
@@ -243,7 +243,7 @@ class AddPlaidBankAccount extends React.Component {
{
+ onInputChange={(index) => {
this.setState({selectedIndex: Number(index)});
this.clearError('selectedBank');
}}
diff --git a/src/components/LocalePicker.js b/src/components/LocalePicker.js
index 30eba46aba9e..aafb06ea6fd2 100644
--- a/src/components/LocalePicker.js
+++ b/src/components/LocalePicker.js
@@ -49,7 +49,7 @@ const LocalePicker = (props) => {
return (
{
+ onInputChange={(locale) => {
if (locale === props.preferredLocale) {
return;
}
diff --git a/src/components/Picker/BasePicker/basePickerPropTypes.js b/src/components/Picker/BasePicker/basePickerPropTypes.js
index 02f5ff2d2b6d..7af66b59bdf6 100644
--- a/src/components/Picker/BasePicker/basePickerPropTypes.js
+++ b/src/components/Picker/BasePicker/basePickerPropTypes.js
@@ -6,7 +6,7 @@ import * as Expensicons from '../../Icon/Expensicons';
const propTypes = {
/** A callback method that is called when the value changes and it received the selected value as an argument */
- onChange: PropTypes.func.isRequired,
+ onInputChange: PropTypes.func.isRequired,
/** Whether or not to show the disabled styles */
disabled: PropTypes.bool,
diff --git a/src/components/Picker/BasePicker/index.js b/src/components/Picker/BasePicker/index.js
index 75d06cfbc84c..a56c7d9f0d29 100644
--- a/src/components/Picker/BasePicker/index.js
+++ b/src/components/Picker/BasePicker/index.js
@@ -1,32 +1,53 @@
import React from 'react';
import RNPickerSelect from 'react-native-picker-select';
+import _ from 'underscore';
import styles from '../../../styles/styles';
import * as basePickerPropTypes from './basePickerPropTypes';
import basePickerStyles from './basePickerStyles';
-const BasePicker = props => (
- props.icon(props.size)}
- disabled={props.disabled}
- fixAndroidTouchableBug
- onOpen={props.onOpen}
- onClose={props.onClose}
- pickerProps={{
- onFocus: props.onOpen,
- onBlur: props.onClose,
- }}
- />
-);
+class BasePicker extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.state = {
+ selectedValue: this.props.value || this.props.defaultValue,
+ };
+
+ this.updateSelectedValueAndExecuteOnChange = this.updateSelectedValueAndExecuteOnChange.bind(this);
+ }
+
+ updateSelectedValueAndExecuteOnChange(value) {
+ this.props.onInputChange(value);
+ this.setState({selectedValue: value});
+ }
+
+ render() {
+ const hasError = !_.isEmpty(this.props.errorText);
+ return (
+ this.props.icon(this.props.size)}
+ disabled={this.props.disabled}
+ fixAndroidTouchableBug
+ onOpen={this.props.onOpen}
+ onClose={this.props.onClose}
+ pickerProps={{
+ onFocus: this.props.onOpen,
+ onBlur: this.props.onBlur,
+ ref: this.props.innerRef,
+ }}
+ />
+ );
+ }
+}
BasePicker.propTypes = basePickerPropTypes.propTypes;
BasePicker.defaultProps = basePickerPropTypes.defaultProps;
-BasePicker.displayName = 'BasePicker';
export default BasePicker;
diff --git a/src/components/Picker/index.js b/src/components/Picker/index.js
index b7277fcda73e..5f2f86374854 100644
--- a/src/components/Picker/index.js
+++ b/src/components/Picker/index.js
@@ -6,6 +6,7 @@ import BasePicker from './BasePicker';
import Text from '../Text';
import styles from '../../styles/styles';
import InlineErrorText from '../InlineErrorText';
+import * as FormUtils from '../../libs/FormUtils';
const propTypes = {
/** Picker label */
@@ -14,22 +15,39 @@ const propTypes = {
/** Should the picker appear disabled? */
isDisabled: PropTypes.bool,
- /** Should the input be styled for errors */
- hasError: PropTypes.bool,
+ /** Input value */
+ value: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
/** Error text to display */
errorText: PropTypes.string,
/** Customize the Picker container */
containerStyles: PropTypes.arrayOf(PropTypes.object),
+
+ /** Indicates that the input is being used with the Form component */
+ isFormInput: PropTypes.bool,
+
+ /**
+ * The ID used to uniquely identify the input
+ *
+ * @param {Object} props - props passed to the input
+ * @returns {Object} - returns an Error object if isFormInput is supplied but inputID is falsey or not a string
+ */
+ inputID: props => FormUtils.validateInputIDProps(props),
+
+ /** Saves a draft of the input value when used in a form */
+ shouldSaveDraft: PropTypes.bool,
};
const defaultProps = {
label: '',
isDisabled: false,
- hasError: false,
errorText: '',
containerStyles: [],
+ isFormInput: false,
+ inputID: undefined,
+ shouldSaveDraft: false,
+ value: undefined,
};
class Picker extends PureComponent {
@@ -48,6 +66,7 @@ class Picker extends PureComponent {
style={[
styles.pickerContainer,
this.props.isDisabled && styles.inputDisabled,
+ ...this.props.containerStyles,
]}
>
{this.props.label && (
@@ -58,12 +77,13 @@ class Picker extends PureComponent {
onClose={() => this.setState({isOpen: false})}
disabled={this.props.isDisabled}
focused={this.state.isOpen}
- hasError={this.props.hasError}
+ errorText={this.props.errorText}
+ value={this.props.value}
// eslint-disable-next-line react/jsx-props-no-spreading
{...pickerProps}
/>
-
+
{this.props.errorText}
>
@@ -74,4 +94,5 @@ class Picker extends PureComponent {
Picker.propTypes = propTypes;
Picker.defaultProps = defaultProps;
-export default Picker;
+// eslint-disable-next-line react/jsx-props-no-spreading
+export default React.forwardRef((props, ref) => );
diff --git a/src/components/StatePicker.js b/src/components/StatePicker.js
index d9fbe810eeb4..11d4cad09f55 100644
--- a/src/components/StatePicker.js
+++ b/src/components/StatePicker.js
@@ -32,7 +32,7 @@ const StatePicker = props => (
({value, label}))}
- onChange={value => this.clearErrorAndSetValue('incorporationType', value)}
+ onInputChange={value => this.clearErrorAndSetValue('incorporationType', value)}
value={this.state.incorporationType}
placeholder={{value: '', label: '-'}}
hasError={this.getErrors().incorporationType}
diff --git a/src/pages/ReportSettingsPage.js b/src/pages/ReportSettingsPage.js
index a0ddbc6c8e55..a3ec5a1b6f7e 100644
--- a/src/pages/ReportSettingsPage.js
+++ b/src/pages/ReportSettingsPage.js
@@ -168,7 +168,7 @@ class ReportSettingsPage extends Component {
{
+ onInputChange={(notificationPreference) => {
Report.updateNotificationPreference(
this.props.report.reportID,
notificationPreference,
diff --git a/src/pages/settings/PreferencesPage.js b/src/pages/settings/PreferencesPage.js
index 4752dfc291d8..ae7de7e4f51f 100755
--- a/src/pages/settings/PreferencesPage.js
+++ b/src/pages/settings/PreferencesPage.js
@@ -85,7 +85,7 @@ const PreferencesPage = (props) => {
NameValuePair.set(CONST.NVP.PRIORITY_MODE, mode, ONYXKEYS.NVP_PRIORITY_MODE)
}
items={_.values(priorityModes)}
diff --git a/src/pages/settings/Profile/ProfilePage.js b/src/pages/settings/Profile/ProfilePage.js
index cf4ecd2e5ce2..1dc7a502e788 100755
--- a/src/pages/settings/Profile/ProfilePage.js
+++ b/src/pages/settings/Profile/ProfilePage.js
@@ -250,7 +250,7 @@ class ProfilePage extends Component {
{
+ onInputChange={(pronouns) => {
const hasSelfSelectedPronouns = pronouns === CONST.PRONOUNS.SELF_SELECT;
this.setState({
pronouns: hasSelfSelectedPronouns ? '' : pronouns,
@@ -288,7 +288,7 @@ class ProfilePage extends Component {
this.setState({selectedTimezone})}
+ onInputChange={selectedTimezone => this.setState({selectedTimezone})}
items={timezones}
isDisabled={this.state.isAutomaticTimezone}
value={this.state.selectedTimezone}
diff --git a/src/pages/workspace/WorkspaceNewRoomPage.js b/src/pages/workspace/WorkspaceNewRoomPage.js
index 780bf75f487e..a31dad1708c5 100644
--- a/src/pages/workspace/WorkspaceNewRoomPage.js
+++ b/src/pages/workspace/WorkspaceNewRoomPage.js
@@ -173,7 +173,7 @@ class WorkspaceNewRoomPage extends React.Component {
items={this.state.workspaceOptions}
errorText={this.state.errors.policyID}
hasError={Boolean(this.state.errors.policyID)}
- onChange={policyID => this.clearErrorAndSetValue('policyID', policyID)}
+ onInputChange={policyID => this.clearErrorAndSetValue('policyID', policyID)}
/>
@@ -181,7 +181,7 @@ class WorkspaceNewRoomPage extends React.Component {
value={this.state.visibility}
label={this.props.translate('newRoomPage.visibility')}
items={visibilityOptions}
- onChange={visibility => this.setState({visibility})}
+ onInputChange={visibility => this.setState({visibility})}
/>
diff --git a/src/pages/workspace/WorkspaceSettingsPage.js b/src/pages/workspace/WorkspaceSettingsPage.js
index ee9989d86ff3..a0bb45fcbe6b 100644
--- a/src/pages/workspace/WorkspaceSettingsPage.js
+++ b/src/pages/workspace/WorkspaceSettingsPage.js
@@ -168,7 +168,7 @@ class WorkspaceSettingsPage extends React.Component {
this.setState({currency})}
+ onInputChange={currency => this.setState({currency})}
items={this.getCurrencyItems()}
value={this.state.currency}
isDisabled={hasVBA}
diff --git a/src/pages/workspace/reimburse/WorkspaceReimburseNoVBAView.js b/src/pages/workspace/reimburse/WorkspaceReimburseNoVBAView.js
index 6e68c98488cd..e068a8666eeb 100644
--- a/src/pages/workspace/reimburse/WorkspaceReimburseNoVBAView.js
+++ b/src/pages/workspace/reimburse/WorkspaceReimburseNoVBAView.js
@@ -181,7 +181,7 @@ class WorkspaceReimburseNoVBAView extends React.Component {
label={this.props.translate('workspace.reimburse.trackDistanceUnit')}
items={this.unitItems}
value={this.state.unitValue}
- onChange={value => this.setUnit(value)}
+ onInputChange={value => this.setUnit(value)}
/>
diff --git a/src/stories/Form.stories.js b/src/stories/Form.stories.js
index 7de80c6192af..e0e551bb17c9 100644
--- a/src/stories/Form.stories.js
+++ b/src/stories/Form.stories.js
@@ -1,6 +1,7 @@
import React, {useState} from 'react';
import {View} from 'react-native';
import TextInput from '../components/TextInput';
+import Picker from '../components/Picker';
import AddressSearch from '../components/AddressSearch';
import Form from '../components/Form';
import * as FormActions from '../libs/actions/FormActions';
@@ -16,7 +17,12 @@ import Text from '../components/Text';
const story = {
title: 'Components/Form',
component: Form,
- subcomponents: {TextInput, AddressSearch, CheckboxWithLabel},
+ subcomponents: {
+ TextInput,
+ AddressSearch,
+ CheckboxWithLabel,
+ Picker,
+ },
};
const Template = (args) => {
@@ -50,6 +56,49 @@ const Template = (args) => {
containerStyles={[styles.mt4]}
isFormInput
/>
+
+
+
+
;
+
+// Arguments can be passed to the component by binding
+// See: https://storybook.js.org/docs/react/writing-stories/introduction#using-args
+
+const Default = Template.bind({});
+Default.args = {
+ label: 'Default picker',
+ name: 'Default',
+ onInputChange: () => {},
+ items: [
+ {
+ label: 'Orange',
+ value: 'orange',
+ },
+ {
+ label: 'Apple',
+ value: 'apple',
+ },
+ ],
+};
+
+const PickerWithValue = Template.bind({});
+PickerWithValue.args = {
+ label: 'Picker with defined value',
+ name: 'Picker with defined value',
+ onInputChange: () => {},
+ value: 'apple',
+ items: [
+ {
+ label: 'Orange',
+ value: 'orange',
+ },
+ {
+ label: 'Apple',
+ value: 'apple',
+ },
+ ],
+};
+
+const ErrorStory = Template.bind({});
+ErrorStory.args = {
+ label: 'Picker with error',
+ name: 'PickerWithError',
+ errorText: 'This field has an error.',
+ onInputChange: () => {},
+ items: [
+ {
+ label: 'Orange',
+ value: 'orange',
+ },
+ {
+ label: 'Apple',
+ value: 'apple',
+ },
+ ],
+};
+
+const Disabled = Template.bind({});
+Disabled.args = {
+ label: 'Picker disabled',
+ name: 'Disabled',
+ isDisabled: true,
+ onInputChange: () => {},
+ items: [
+ {
+ label: 'Orange',
+ value: 'orange',
+ },
+ {
+ label: 'Apple',
+ value: 'apple',
+ },
+ ],
+};
+
+export default story;
+export {
+ Default,
+ PickerWithValue,
+ ErrorStory,
+ Disabled,
+};