diff --git a/src/components/AddressSearch/index.js b/src/components/AddressSearch/index.js
index 3e122e029969..f8219c028853 100644
--- a/src/components/AddressSearch/index.js
+++ b/src/components/AddressSearch/index.js
@@ -1,6 +1,6 @@
import lodashGet from 'lodash/get';
import PropTypes from 'prop-types';
-import React, {useEffect, useMemo, useRef, useState} from 'react';
+import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react';
import {ActivityIndicator, Keyboard, LogBox, ScrollView, Text, View} from 'react-native';
import {GooglePlacesAutocomplete} from 'react-native-google-places-autocomplete';
import _ from 'underscore';
@@ -140,27 +140,46 @@ const defaultProps = {
resultTypes: 'address',
};
-// Do not convert to class component! It's been tried before and presents more challenges than it's worth.
-// Relevant thread: https://expensify.slack.com/archives/C03TQ48KC/p1634088400387400
-// Reference: https://github.com/FaridSafi/react-native-google-places-autocomplete/issues/609#issuecomment-886133839
-function AddressSearch(props) {
+function AddressSearch({
+ canUseCurrentLocation,
+ containerStyles,
+ defaultValue,
+ errorText,
+ hint,
+ innerRef,
+ inputID,
+ isLimitedToUSA,
+ label,
+ maxInputLength,
+ network,
+ onBlur,
+ onInputChange,
+ onPress,
+ predefinedPlaces,
+ preferredLocale,
+ renamedInputKeys,
+ resultTypes,
+ shouldSaveDraft,
+ translate,
+ value,
+}) {
const [displayListViewBorder, setDisplayListViewBorder] = useState(false);
const [isTyping, setIsTyping] = useState(false);
const [isFocused, setIsFocused] = useState(false);
- const [searchValue, setSearchValue] = useState(props.value || props.defaultValue || '');
+ const [searchValue, setSearchValue] = useState(value || defaultValue || '');
const [locationErrorCode, setLocationErrorCode] = useState(null);
const [isFetchingCurrentLocation, setIsFetchingCurrentLocation] = useState(false);
const shouldTriggerGeolocationCallbacks = useRef(true);
const containerRef = useRef();
const query = useMemo(
() => ({
- language: props.preferredLocale,
- types: props.resultTypes,
- components: props.isLimitedToUSA ? 'country:us' : undefined,
+ language: preferredLocale,
+ types: resultTypes,
+ components: isLimitedToUSA ? 'country:us' : undefined,
}),
- [props.preferredLocale, props.resultTypes, props.isLimitedToUSA],
+ [preferredLocale, resultTypes, isLimitedToUSA],
);
- const shouldShowCurrentLocationButton = props.canUseCurrentLocation && searchValue.trim().length === 0 && isFocused;
+ const shouldShowCurrentLocationButton = canUseCurrentLocation && searchValue.trim().length === 0 && isFocused;
const saveLocationDetails = (autocompleteData, details) => {
const addressComponents = details.address_components;
@@ -169,7 +188,7 @@ function AddressSearch(props) {
// to this component which don't match the usual properties coming from auto-complete. In that case, only a limited
// amount of data massaging needs to happen for what the parent expects to get from this function.
if (_.size(details)) {
- props.onPress({
+ onPress({
address: lodashGet(details, 'description'),
lat: lodashGet(details, 'geometry.location.lat', 0),
lng: lodashGet(details, 'geometry.location.lng', 0),
@@ -256,7 +275,7 @@ function AddressSearch(props) {
// Not all pages define the Address Line 2 field, so in that case we append any additional address details
// (e.g. Apt #) to Address Line 1
- if (subpremise && typeof props.renamedInputKeys.street2 === 'undefined') {
+ if (subpremise && typeof renamedInputKeys.street2 === 'undefined') {
values.street += `, ${subpremise}`;
}
@@ -265,19 +284,19 @@ function AddressSearch(props) {
values.country = country;
}
- if (props.inputID) {
- _.each(values, (value, key) => {
- const inputKey = lodashGet(props.renamedInputKeys, key, key);
+ if (inputID) {
+ _.each(values, (inputValue, key) => {
+ const inputKey = lodashGet(renamedInputKeys, key, key);
if (!inputKey) {
return;
}
- props.onInputChange(value, inputKey);
+ onInputChange(inputValue, inputKey);
});
} else {
- props.onInputChange(values);
+ onInputChange(values);
}
- props.onPress(values);
+ onPress(values);
};
/** Gets the user's current location and registers success/error callbacks */
@@ -307,7 +326,7 @@ function AddressSearch(props) {
lng: successData.coords.longitude,
address: CONST.YOUR_LOCATION_TEXT,
};
- props.onPress(location);
+ onPress(location);
},
(errorData) => {
if (!shouldTriggerGeolocationCallbacks.current) {
@@ -325,16 +344,16 @@ function AddressSearch(props) {
};
const renderHeaderComponent = () =>
- props.predefinedPlaces.length > 0 && (
+ predefinedPlaces.length > 0 && (
<>
{/* This will show current location button in list if there are some recent destinations */}
{shouldShowCurrentLocationButton && (
)}
- {!props.value && {props.translate('common.recentDestinations')}}
+ {!value && {translate('common.recentDestinations')}}
>
);
@@ -346,6 +365,26 @@ function AddressSearch(props) {
};
}, []);
+ const listEmptyComponent = useCallback(
+ () =>
+ network.isOffline || !isTyping ? null : (
+ {translate('common.noResultsFound')}
+ ),
+ [isTyping, translate, network.isOffline],
+ );
+
+ const listLoader = useCallback(
+ () => (
+
+
+
+ ),
+ [],
+ );
+
return (
/*
* The GooglePlacesAutocomplete component uses a VirtualizedList internally,
@@ -372,20 +411,10 @@ function AddressSearch(props) {
fetchDetails
suppressDefaultStyles
enablePoweredByContainer={false}
- predefinedPlaces={props.predefinedPlaces}
- listEmptyComponent={
- props.network.isOffline || !isTyping ? null : (
- {props.translate('common.noResultsFound')}
- )
- }
- listLoaderComponent={
-
-
-
- }
+ predefinedPlaces={predefinedPlaces}
+ listEmptyComponent={listEmptyComponent}
+ listLoaderComponent={listLoader}
+ renderHeaderComponent={renderHeaderComponent}
renderRow={(data) => {
const title = data.isPredefinedPlace ? data.name : data.structured_formatting.main_text;
const subtitle = data.isPredefinedPlace ? data.description : data.structured_formatting.secondary_text;
@@ -396,7 +425,6 @@ function AddressSearch(props) {
);
}}
- renderHeaderComponent={renderHeaderComponent}
onPress={(data, details) => {
saveLocationDetails(data, details);
setIsTyping(false);
@@ -411,34 +439,31 @@ function AddressSearch(props) {
query={query}
requestUrl={{
useOnPlatform: 'all',
- url: props.network.isOffline ? null : ApiUtils.getCommandURL({command: 'Proxy_GooglePlaces&proxyUrl='}),
+ url: network.isOffline ? null : ApiUtils.getCommandURL({command: 'Proxy_GooglePlaces&proxyUrl='}),
}}
textInputProps={{
InputComp: TextInput,
ref: (node) => {
- if (!props.innerRef) {
+ if (!innerRef) {
return;
}
- if (_.isFunction(props.innerRef)) {
- props.innerRef(node);
+ if (_.isFunction(innerRef)) {
+ innerRef(node);
return;
}
// eslint-disable-next-line no-param-reassign
- props.innerRef.current = node;
+ innerRef.current = node;
},
- label: props.label,
- containerStyles: props.containerStyles,
- errorText: props.errorText,
- hint:
- displayListViewBorder || (props.predefinedPlaces.length === 0 && shouldShowCurrentLocationButton) || (props.canUseCurrentLocation && isTyping)
- ? undefined
- : props.hint,
- value: props.value,
- defaultValue: props.defaultValue,
- inputID: props.inputID,
- shouldSaveDraft: props.shouldSaveDraft,
+ label,
+ containerStyles,
+ errorText,
+ hint: displayListViewBorder || (predefinedPlaces.length === 0 && shouldShowCurrentLocationButton) || (canUseCurrentLocation && isTyping) ? undefined : hint,
+ value,
+ defaultValue,
+ inputID,
+ shouldSaveDraft,
onFocus: () => {
setIsFocused(true);
},
@@ -448,24 +473,24 @@ function AddressSearch(props) {
setIsFocused(false);
setIsTyping(false);
}
- props.onBlur();
+ onBlur();
},
autoComplete: 'off',
onInputChange: (text) => {
setSearchValue(text);
setIsTyping(true);
- if (props.inputID) {
- props.onInputChange(text);
+ if (inputID) {
+ onInputChange(text);
} else {
- props.onInputChange({street: text});
+ onInputChange({street: text});
}
// If the text is empty and we have no predefined places, we set displayListViewBorder to false to prevent UI flickering
- if (_.isEmpty(text) && _.isEmpty(props.predefinedPlaces)) {
+ if (_.isEmpty(text) && _.isEmpty(predefinedPlaces)) {
setDisplayListViewBorder(false);
}
},
- maxLength: props.maxInputLength,
+ maxLength: maxInputLength,
spellCheck: false,
}}
styles={{
@@ -486,17 +511,18 @@ function AddressSearch(props) {
}}
inbetweenCompo={
// We want to show the current location button even if there are no recent destinations
- props.predefinedPlaces.length === 0 && shouldShowCurrentLocationButton ? (
+ predefinedPlaces.length === 0 && shouldShowCurrentLocationButton ? (
) : (
<>>
)
}
+ placeholder=""
/>
setLocationErrorCode(null)}
diff --git a/src/components/CheckboxWithLabel.js b/src/components/CheckboxWithLabel.js
index 4bffadecb733..f18ec346dfa2 100644
--- a/src/components/CheckboxWithLabel.js
+++ b/src/components/CheckboxWithLabel.js
@@ -7,6 +7,7 @@ import variables from '@styles/variables';
import Checkbox from './Checkbox';
import FormHelpMessage from './FormHelpMessage';
import PressableWithFeedback from './Pressable/PressableWithFeedback';
+import refPropTypes from './refPropTypes';
import Text from './Text';
/**
@@ -54,7 +55,7 @@ const propTypes = {
defaultValue: PropTypes.bool,
/** React ref being forwarded to the Checkbox input */
- forwardedRef: PropTypes.func,
+ forwardedRef: refPropTypes,
/** The ID used to uniquely identify the input in a Form */
/* eslint-disable-next-line react/no-unused-prop-types */
diff --git a/src/components/Form/FormProvider.js b/src/components/Form/FormProvider.js
index 92baa9727832..85408323c9f2 100644
--- a/src/components/Form/FormProvider.js
+++ b/src/components/Form/FormProvider.js
@@ -71,6 +71,8 @@ const propTypes = {
shouldValidateOnChange: PropTypes.bool,
};
+const VALIDATE_DELAY = 200;
+
const defaultProps = {
isSubmitButtonVisible: true,
formState: {
@@ -246,19 +248,28 @@ function FormProvider({validate, formID, shouldValidateOnBlur, shouldValidateOnC
// as this is already happening by the value prop.
defaultValue: undefined,
onTouched: (event) => {
- setTouchedInput(inputID);
+ setTimeout(() => {
+ setTouchedInput(inputID);
+ }, VALIDATE_DELAY);
if (_.isFunction(propsToParse.onTouched)) {
propsToParse.onTouched(event);
}
},
onPress: (event) => {
- setTouchedInput(inputID);
+ setTimeout(() => {
+ setTouchedInput(inputID);
+ }, VALIDATE_DELAY);
if (_.isFunction(propsToParse.onPress)) {
propsToParse.onPress(event);
}
},
- onPressIn: (event) => {
- setTouchedInput(inputID);
+ onPressOut: (event) => {
+ // To prevent validating just pressed inputs, we need to set the touched input right after
+ // onValidate and to do so, we need to delays setTouchedInput of the same amount of time
+ // as the onValidate is delayed
+ setTimeout(() => {
+ setTouchedInput(inputID);
+ }, VALIDATE_DELAY);
if (_.isFunction(propsToParse.onPressIn)) {
propsToParse.onPressIn(event);
}
@@ -274,7 +285,7 @@ function FormProvider({validate, formID, shouldValidateOnBlur, shouldValidateOnC
if (shouldValidateOnBlur) {
onValidate(inputValues, !hasServerError);
}
- }, 200);
+ }, VALIDATE_DELAY);
}
if (_.isFunction(propsToParse.onBlur)) {
diff --git a/src/pages/settings/Wallet/AddDebitCardPage.js b/src/pages/settings/Wallet/AddDebitCardPage.js
index ff20d518ff5d..8b8938a9ea42 100644
--- a/src/pages/settings/Wallet/AddDebitCardPage.js
+++ b/src/pages/settings/Wallet/AddDebitCardPage.js
@@ -4,7 +4,8 @@ import {View} from 'react-native';
import {withOnyx} from 'react-native-onyx';
import AddressSearch from '@components/AddressSearch';
import CheckboxWithLabel from '@components/CheckboxWithLabel';
-import Form from '@components/Form';
+import FormProvider from '@components/Form/FormProvider';
+import InputWrapper from '@components/Form/InputWrapper';
import HeaderWithBackButton from '@components/HeaderWithBackButton';
import ScreenWrapper from '@components/ScreenWrapper';
import StatePicker from '@components/StatePicker';
@@ -114,7 +115,7 @@ function DebitCardPage(props) {
title={translate('addDebitCardPage.addADebitCard')}
onBackButtonPress={() => Navigation.goBack(ROUTES.SETTINGS_WALLET)}
/>
-
+
);
}