diff --git a/src/components/CountryPicker.js b/src/components/CountryPicker.js deleted file mode 100644 index 61bfd26a0e67..000000000000 --- a/src/components/CountryPicker.js +++ /dev/null @@ -1,67 +0,0 @@ -import _ from 'underscore'; -import React, {forwardRef} from 'react'; -import PropTypes from 'prop-types'; -import Picker from './Picker'; -import withLocalize, {withLocalizePropTypes} from './withLocalize'; - -const propTypes = { - /** The label for the field */ - label: PropTypes.string, - - /** A callback method that is called when the value changes and it receives the selected value as an argument. */ - onInputChange: PropTypes.func.isRequired, - - /** The value that needs to be selected */ - value: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), - - /** The ID used to uniquely identify the input in a form */ - inputID: PropTypes.string, - - /** Saves a draft of the input value when used in a form */ - shouldSaveDraft: PropTypes.bool, - - /** Callback that is called when the text input is blurred */ - onBlur: PropTypes.func, - - /** Error text to display */ - errorText: PropTypes.string, - - ...withLocalizePropTypes, -}; - -const defaultProps = { - label: '', - value: undefined, - errorText: '', - shouldSaveDraft: false, - inputID: undefined, - onBlur: () => {}, -}; - -const CountryPicker = forwardRef((props, ref) => { - const COUNTRIES = _.map(props.translate('allCountries'), (countryName, countryISO) => ({ - value: countryISO, - label: countryName, - })); - - return ( - - ); -}); - -CountryPicker.propTypes = propTypes; -CountryPicker.defaultProps = defaultProps; -CountryPicker.displayName = 'CountryPicker'; - -export default withLocalize(CountryPicker); diff --git a/src/components/CountryPicker/CountrySelectorModal.js b/src/components/CountryPicker/CountrySelectorModal.js new file mode 100644 index 000000000000..d16d97741d7c --- /dev/null +++ b/src/components/CountryPicker/CountrySelectorModal.js @@ -0,0 +1,96 @@ +import _ from 'underscore'; +import React, {useMemo} from 'react'; +import PropTypes from 'prop-types'; +import CONST from '../../CONST'; +import useLocalize from '../../hooks/useLocalize'; +import HeaderWithBackButton from '../HeaderWithBackButton'; +import SelectionListRadio from '../SelectionListRadio'; +import Modal from '../Modal'; + +const propTypes = { + /** Whether the modal is visible */ + isVisible: PropTypes.bool.isRequired, + + /** Country value selected */ + currentCountry: PropTypes.string, + + /** Function to call when the user selects a Country */ + onCountrySelected: PropTypes.func, + + /** Function to call when the user closes the Country modal */ + onClose: PropTypes.func, + + /** The search value from the selection list */ + searchValue: PropTypes.string.isRequired, + + /** Function to call when the user types in the search input */ + setSearchValue: PropTypes.func.isRequired, +}; + +const defaultProps = { + currentCountry: '', + onClose: () => {}, + onCountrySelected: () => {}, +}; + +function filterOptions(searchValue, data) { + const trimmedSearchValue = searchValue.trim(); + if (trimmedSearchValue.length === 0) { + return []; + } + + return _.filter(data, (country) => country.text.toLowerCase().includes(searchValue.toLowerCase())); +} + +function CountrySelectorModal({currentCountry, isVisible, onClose, onCountrySelected, setSearchValue, searchValue}) { + const {translate} = useLocalize(); + + const countries = useMemo( + () => + _.map(translate('allCountries'), (countryName, countryISO) => ({ + value: countryISO, + keyForList: countryISO, + text: countryName, + isSelected: currentCountry === countryISO, + })), + [translate, currentCountry], + ); + + const filteredData = filterOptions(searchValue, countries); + const headerMessage = searchValue.trim() && !filteredData.length ? translate('common.noResultsFound') : ''; + + return ( + + + + + ); +} + +CountrySelectorModal.propTypes = propTypes; +CountrySelectorModal.defaultProps = defaultProps; +CountrySelectorModal.displayName = 'CountrySelectorModal'; + +export default CountrySelectorModal; diff --git a/src/components/CountryPicker/index.js b/src/components/CountryPicker/index.js new file mode 100644 index 000000000000..20f9a981accb --- /dev/null +++ b/src/components/CountryPicker/index.js @@ -0,0 +1,93 @@ +import React, {useEffect, useState} from 'react'; +import {View} from 'react-native'; +import PropTypes from 'prop-types'; +import lodashGet from 'lodash/get'; +import styles from '../../styles/styles'; +import MenuItemWithTopDescription from '../MenuItemWithTopDescription'; +import useLocalize from '../../hooks/useLocalize'; +import CountrySelectorModal from './CountrySelectorModal'; +import FormHelpMessage from '../FormHelpMessage'; + +const propTypes = { + /** Form Error description */ + errorText: PropTypes.string, + + /** Country to display */ + value: PropTypes.string, + + /** Callback to call when the input changes */ + onInputChange: PropTypes.func, + + /** A ref to forward to MenuItemWithTopDescription */ + forwardedRef: PropTypes.oneOfType([PropTypes.func, PropTypes.shape({current: PropTypes.instanceOf(React.Component)})]), +}; + +const defaultProps = { + value: undefined, + forwardedRef: undefined, + errorText: '', + onInputChange: () => {}, +}; + +function CountryPicker({value, errorText, onInputChange, forwardedRef}) { + const {translate} = useLocalize(); + const allCountries = translate('allCountries'); + const [isPickerVisible, setIsPickerVisible] = useState(false); + const [searchValue, setSearchValue] = useState(lodashGet(allCountries, value, '')); + + useEffect(() => { + setSearchValue(lodashGet(allCountries, value, '')); + }, [value, allCountries]); + + const showPickerModal = () => { + setIsPickerVisible(true); + }; + + const hidePickerModal = () => { + setIsPickerVisible(false); + }; + + const updateCountryInput = (country) => { + onInputChange(country.value); + hidePickerModal(); + }; + + const title = allCountries[value] || ''; + const descStyle = title.length === 0 ? styles.textNormal : null; + + return ( + + + + + + + + ); +} + +CountryPicker.propTypes = propTypes; +CountryPicker.defaultProps = defaultProps; +CountryPicker.displayName = 'CountryPicker'; + +export default React.forwardRef((props, ref) => ( + +)); diff --git a/src/components/Form.js b/src/components/Form.js index c9c1b79d7679..ffcd3cbe8cd5 100644 --- a/src/components/Form.js +++ b/src/components/Form.js @@ -334,7 +334,7 @@ function Form(props) { } if (child.props.onValueChange) { - child.props.onValueChange(value); + child.props.onValueChange(value, inputKey); } }, }); diff --git a/src/components/MenuItemWithTopDescription.js b/src/components/MenuItemWithTopDescription.js index ce6fd452341a..ee51d2f41ccd 100644 --- a/src/components/MenuItemWithTopDescription.js +++ b/src/components/MenuItemWithTopDescription.js @@ -9,6 +9,7 @@ function MenuItemWithTopDescription(props) { @@ -18,4 +19,10 @@ function MenuItemWithTopDescription(props) { MenuItemWithTopDescription.propTypes = propTypes; MenuItemWithTopDescription.displayName = 'MenuItemWithTopDescription'; -export default MenuItemWithTopDescription; +export default React.forwardRef((props, ref) => ( + +)); diff --git a/src/components/StatePicker.js b/src/components/StatePicker.js deleted file mode 100644 index 1d18652a625f..000000000000 --- a/src/components/StatePicker.js +++ /dev/null @@ -1,67 +0,0 @@ -import _ from 'underscore'; -import React, {forwardRef} from 'react'; -import PropTypes from 'prop-types'; -import Picker from './Picker'; -import withLocalize, {withLocalizePropTypes} from './withLocalize'; - -const propTypes = { - /** The label for the field */ - label: PropTypes.string, - - /** A callback method that is called when the value changes and it receives the selected value as an argument. */ - onInputChange: PropTypes.func.isRequired, - - /** The value that needs to be selected */ - value: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), - - /** The ID used to uniquely identify the input in a Form */ - inputID: PropTypes.string, - - /** Saves a draft of the input value when used in a form */ - shouldSaveDraft: PropTypes.bool, - - /** Callback that is called when the text input is blurred */ - onBlur: PropTypes.func, - - /** Error text to display */ - errorText: PropTypes.string, - - ...withLocalizePropTypes, -}; - -const defaultProps = { - label: '', - value: undefined, - errorText: '', - shouldSaveDraft: false, - inputID: undefined, - onBlur: () => {}, -}; - -const StatePicker = forwardRef((props, ref) => { - const STATES = _.chain(props.translate('allStates')) - .sortBy((state) => state.stateName.toLowerCase()) - .map((state) => ({value: state.stateISO, label: state.stateName})) - .value(); - - return ( - - ); -}); - -StatePicker.propTypes = propTypes; -StatePicker.defaultProps = defaultProps; -StatePicker.displayName = 'StatePicker'; - -export default withLocalize(StatePicker); diff --git a/src/components/StatePicker/StateSelectorModal.js b/src/components/StatePicker/StateSelectorModal.js new file mode 100644 index 000000000000..4497eab72de8 --- /dev/null +++ b/src/components/StatePicker/StateSelectorModal.js @@ -0,0 +1,97 @@ +import _ from 'underscore'; +import React, {useMemo} from 'react'; +import PropTypes from 'prop-types'; +import CONST from '../../CONST'; +import Modal from '../Modal'; +import HeaderWithBackButton from '../HeaderWithBackButton'; +import SelectionListRadio from '../SelectionListRadio'; +import useLocalize from '../../hooks/useLocalize'; + +const propTypes = { + /** Whether the modal is visible */ + isVisible: PropTypes.bool.isRequired, + + /** State value selected */ + currentState: PropTypes.string, + + /** Function to call when the user selects a State */ + onStateSelected: PropTypes.func, + + /** Function to call when the user closes the State modal */ + onClose: PropTypes.func, + + /** The search value from the selection list */ + searchValue: PropTypes.string.isRequired, + + /** Function to call when the user types in the search input */ + setSearchValue: PropTypes.func.isRequired, +}; + +const defaultProps = { + currentState: '', + onClose: () => {}, + onStateSelected: () => {}, +}; + +function filterOptions(searchValue, data) { + const trimmedSearchValue = searchValue.trim(); + if (trimmedSearchValue.length === 0) { + return []; + } + + return _.filter(data, (country) => country.text.toLowerCase().includes(searchValue.toLowerCase())); +} + +function StateSelectorModal({currentState, isVisible, onClose, onStateSelected, searchValue, setSearchValue}) { + const {translate} = useLocalize(); + + const countryStates = useMemo( + () => + _.map(translate('allStates'), (state) => ({ + value: state.stateISO, + keyForList: state.stateISO, + text: state.stateName, + isSelected: currentState === state.stateISO, + })), + [translate, currentState], + ); + + const filteredData = filterOptions(searchValue, countryStates); + const headerMessage = searchValue.trim() && !filteredData.length ? translate('common.noResultsFound') : ''; + + return ( + + + + + ); +} + +StateSelectorModal.propTypes = propTypes; +StateSelectorModal.defaultProps = defaultProps; +StateSelectorModal.displayName = 'StateSelectorModal'; + +export default StateSelectorModal; diff --git a/src/components/StatePicker/index.js b/src/components/StatePicker/index.js new file mode 100644 index 000000000000..7c8fbdae36bb --- /dev/null +++ b/src/components/StatePicker/index.js @@ -0,0 +1,93 @@ +import React, {useEffect, useState} from 'react'; +import {View} from 'react-native'; +import lodashGet from 'lodash/get'; +import PropTypes from 'prop-types'; +import styles from '../../styles/styles'; +import MenuItemWithTopDescription from '../MenuItemWithTopDescription'; +import useLocalize from '../../hooks/useLocalize'; +import FormHelpMessage from '../FormHelpMessage'; +import StateSelectorModal from './StateSelectorModal'; + +const propTypes = { + /** Error text to display */ + errorText: PropTypes.string, + + /** State to display */ + value: PropTypes.string, + + /** Callback to call when the input changes */ + onInputChange: PropTypes.func, + + /** A ref to forward to MenuItemWithTopDescription */ + forwardedRef: PropTypes.oneOfType([PropTypes.func, PropTypes.shape({current: PropTypes.instanceOf(React.Component)})]), +}; + +const defaultProps = { + value: undefined, + forwardedRef: undefined, + errorText: '', + onInputChange: () => {}, +}; + +function StatePicker({value, errorText, onInputChange, forwardedRef}) { + const {translate} = useLocalize(); + const allStates = translate('allStates'); + const [isPickerVisible, setIsPickerVisible] = useState(false); + const [searchValue, setSearchValue] = useState(lodashGet(allStates, `${value}.stateName`, '')); + + useEffect(() => { + setSearchValue(lodashGet(allStates, `${value}.stateName`, '')); + }, [value, allStates]); + + const showPickerModal = () => { + setIsPickerVisible(true); + }; + + const hidePickerModal = () => { + setIsPickerVisible(false); + }; + + const updateStateInput = (state) => { + onInputChange(state.value); + hidePickerModal(); + }; + + const title = allStates[value] ? allStates[value].stateName : ''; + const descStyle = title.length === 0 ? styles.textNormal : null; + + return ( + + + + + + + + ); +} + +StatePicker.propTypes = propTypes; +StatePicker.defaultProps = defaultProps; +StatePicker.displayName = 'StatePicker'; + +export default React.forwardRef((props, ref) => ( + +)); diff --git a/src/languages/en.js b/src/languages/en.js index d7d34fa94580..91996e9e7d29 100755 --- a/src/languages/en.js +++ b/src/languages/en.js @@ -1535,4 +1535,10 @@ export default { levelTwoResult: 'Message hidden from channel, plus anonymous warning and message is reported for review.', levelThreeResult: 'Message removed from channel plus anonymous warning and message is reported for review.', }, + countrySelectorModal: { + placeholderText: 'Search to see options', + }, + stateSelectorModal: { + placeholderText: 'Search to see options', + }, }; diff --git a/src/languages/es.js b/src/languages/es.js index c6d6f2f2b67a..712709f68d08 100644 --- a/src/languages/es.js +++ b/src/languages/es.js @@ -2006,4 +2006,10 @@ export default { levelTwoResult: 'Mensaje ocultado del canal, más advertencia anónima y mensaje reportado para revisión.', levelThreeResult: 'Mensaje eliminado del canal, más advertencia anónima y mensaje reportado para revisión.', }, + countrySelectorModal: { + placeholderText: 'Buscar para ver opciones', + }, + stateSelectorModal: { + placeholderText: 'Buscar para ver opciones', + }, }; diff --git a/src/pages/ReimbursementAccount/AddressForm.js b/src/pages/ReimbursementAccount/AddressForm.js index 03b31cc92196..d8fbc0290136 100644 --- a/src/pages/ReimbursementAccount/AddressForm.js +++ b/src/pages/ReimbursementAccount/AddressForm.js @@ -121,12 +121,13 @@ function AddressForm(props) { errorText={props.errors.city ? props.translate('bankAccount.error.addressCity') : ''} containerStyles={[styles.mt4]} /> - + + props.onFieldChange({state: value})} errorText={props.errors.state ? props.translate('bankAccount.error.addressState') : ''} /> @@ -137,7 +138,6 @@ function AddressForm(props) { label={props.translate('common.zip')} accessibilityLabel={props.translate('common.zip')} accessibilityRole={CONST.ACCESSIBILITY_ROLE.TEXT} - containerStyles={[styles.mt4]} keyboardType={CONST.KEYBOARD_TYPE.NUMBER_PAD} value={props.values.zipCode} defaultValue={props.defaultValues.zipCode} diff --git a/src/pages/ReimbursementAccount/CompanyStep.js b/src/pages/ReimbursementAccount/CompanyStep.js index 24eda3cc9b66..a57c80b0a7e2 100644 --- a/src/pages/ReimbursementAccount/CompanyStep.js +++ b/src/pages/ReimbursementAccount/CompanyStep.js @@ -7,6 +7,7 @@ import {withOnyx} from 'react-native-onyx'; import PropTypes from 'prop-types'; import {parsePhoneNumber} from 'awesome-phonenumber'; import HeaderWithBackButton from '../../components/HeaderWithBackButton'; +import StatePicker from '../../components/StatePicker'; import CONST from '../../CONST'; import * as BankAccounts from '../../libs/actions/BankAccounts'; import Text from '../../components/Text'; @@ -15,7 +16,6 @@ import TextInput from '../../components/TextInput'; import styles from '../../styles/styles'; import CheckboxWithLabel from '../../components/CheckboxWithLabel'; import TextLink from '../../components/TextLink'; -import StatePicker from '../../components/StatePicker'; import withLocalize from '../../components/withLocalize'; import * as ValidationUtils from '../../libs/ValidationUtils'; import compose from '../../libs/compose'; @@ -171,7 +171,7 @@ class CompanyStep extends React.Component { onSubmit={this.submit} scrollContextEnabled submitButtonText={this.props.translate('common.saveAndContinue')} - style={[styles.ph5, styles.flexGrow1]} + style={[styles.mh5, styles.flexGrow1]} > {this.props.translate('companyStep.subtitle')} - + { const errors = {}; - const requiredFields = ['addressLine1', 'city', 'country', 'state']; // Check "State" dropdown is a valid state if selected Country is USA @@ -120,31 +115,39 @@ function AddressPage(props) { return errors; }, []); - if (lodashGet(props.privatePersonalDetails, 'isLoading', true)) { + const handleAddressChange = (value, key) => { + if (key !== 'country') { + return; + } + setCurrentCountry(value); + }; + + if (lodashGet(privatePersonalDetails, 'isLoading', true)) { return ; } return ( Navigation.goBack(ROUTES.SETTINGS_PERSONAL_DETAILS)} />
- + - - - - - + + + + - - {isUSAForm ? ( + + {isUSAForm ? ( + - ) : ( - - )} - - + + ) : ( - - - - + )} + + + +
); @@ -220,11 +222,8 @@ function AddressPage(props) { AddressPage.propTypes = propTypes; AddressPage.defaultProps = defaultProps; -export default compose( - withLocalize, - withOnyx({ - privatePersonalDetails: { - key: ONYXKEYS.PRIVATE_PERSONAL_DETAILS, - }, - }), -)(AddressPage); +export default withOnyx({ + privatePersonalDetails: { + key: ONYXKEYS.PRIVATE_PERSONAL_DETAILS, + }, +})(AddressPage); diff --git a/src/pages/workspace/WorkspaceInvitePage.js b/src/pages/workspace/WorkspaceInvitePage.js index 9bfcf1871e0b..0810682d34e2 100644 --- a/src/pages/workspace/WorkspaceInvitePage.js +++ b/src/pages/workspace/WorkspaceInvitePage.js @@ -21,7 +21,7 @@ import FullPageNotFoundView from '../../components/BlockingViews/FullPageNotFoun import ROUTES from '../../ROUTES'; import * as Browser from '../../libs/Browser'; import * as PolicyUtils from '../../libs/PolicyUtils'; -import useOnNetworkReconnect from '../../hooks/useOnNetworkReconnect'; +import useNetwork from '../../hooks/useNetwork'; import useLocalize from '../../hooks/useLocalize'; const personalDetailsPropTypes = PropTypes.shape({ @@ -78,7 +78,7 @@ function WorkspaceInvitePage(props) { // eslint-disable-next-line react-hooks/exhaustive-deps -- policyID changes remount the component }, []); - useOnNetworkReconnect(openWorkspaceInvitePage); + useNetwork({onReconnect: openWorkspaceInvitePage}); useEffect(() => { const inviteOptions = OptionsListUtils.getMemberInviteOptions( diff --git a/src/styles/styles.js b/src/styles/styles.js index 19a6da129792..2618074a8763 100644 --- a/src/styles/styles.js +++ b/src/styles/styles.js @@ -3426,6 +3426,11 @@ const styles = { maxWidth: 375, }, + formSpaceVertical: { + height: 20, + width: 1, + }, + taskCheckbox: { height: 16, width: 16,