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, +};