diff --git a/contributingGuides/FORMS.md b/contributingGuides/FORMS.md
index e3c017dcd2159..cfcb76743312e 100644
--- a/contributingGuides/FORMS.md
+++ b/contributingGuides/FORMS.md
@@ -261,3 +261,15 @@ Any `Form.js` that has a button will also add safe area padding by default. If t
```
+
+### Handling nested Pickers in Form
+
+In case there's a nested Picker in Form, we should pass the props below to Form, as needed:
+
+#### Enable ScrollContext
+
+Pass the `scrollContextEnabled` prop to enable scrolling up when Picker is pressed, making sure the Picker is always in view and doesn't get covered by virtual keyboards for example.
+
+#### Enable scrolling to overflow
+
+In addition to the `scrollContextEnabled` prop, we can also pass `scrollToOverflowEnabled` when the nested Picker is at the bottom of the Form to prevent the popup selector from covering Picker.
diff --git a/src/components/Form.js b/src/components/Form.js
index 6898947dca241..b5bd554cca674 100644
--- a/src/components/Form.js
+++ b/src/components/Form.js
@@ -1,6 +1,6 @@
import lodashGet from 'lodash/get';
import React from 'react';
-import {View} from 'react-native';
+import {View, ScrollView} from 'react-native';
import PropTypes from 'prop-types';
import _ from 'underscore';
import {withOnyx} from 'react-native-onyx';
@@ -56,6 +56,17 @@ const propTypes = {
/** Whether the form submit action is dangerous */
isSubmitActionDangerous: PropTypes.bool,
+ /** Whether the ScrollView overflow content is scrollable.
+ * Set to true to avoid nested Picker components at the bottom of the Form from rendering the popup selector over Picker
+ * e.g. https://github.com/Expensify/App/issues/13909#issuecomment-1396859008
+ */
+ scrollToOverflowEnabled: PropTypes.bool,
+
+ /** Whether ScrollWithContext should be used instead of regular ScrollView.
+ * Set to true when there's a nested Picker component in Form.
+ */
+ scrollContextEnabled: PropTypes.bool,
+
...withLocalizePropTypes,
};
@@ -68,6 +79,8 @@ const defaultProps = {
draftValues: {},
enabledWhenOffline: false,
isSubmitActionDangerous: false,
+ scrollToOverflowEnabled: false,
+ scrollContextEnabled: false,
};
class Form extends React.Component {
@@ -79,6 +92,7 @@ class Form extends React.Component {
inputValues: {},
};
+ this.formRef = React.createRef(null);
this.inputRefs = {};
this.touchedInputs = {};
@@ -258,45 +272,60 @@ class Form extends React.Component {
}
render() {
+ const scrollViewContent = safeAreaPaddingBottomStyle => (
+
+ {this.childrenWrapperWithProps(this.props.children)}
+ {this.props.isSubmitButtonVisible && (
+ 0 || Boolean(this.getErrorMessage()) || !_.isEmpty(this.props.formState.errorFields)}
+ isLoading={this.props.formState.isLoading}
+ message={_.isEmpty(this.props.formState.errorFields) ? this.getErrorMessage() : null}
+ onSubmit={this.submit}
+ onFixTheErrorsLinkPressed={() => {
+ const errors = !_.isEmpty(this.state.errors) ? this.state.errors : this.props.formState.errorFields;
+ const focusKey = _.find(_.keys(this.inputRefs), key => _.keys(errors).includes(key));
+ const focusInput = this.inputRefs[focusKey];
+ if (focusInput.focus && typeof focusInput.focus === 'function') {
+ focusInput.focus();
+ }
+
+ // We subtract 10 to scroll slightly above the input
+ if (focusInput.measureLayout && typeof focusInput.measureLayout === 'function') {
+ focusInput.measureLayout(this.formRef.current, (x, y) => this.formRef.current.scrollTo({y: y - 10, animated: false}));
+ }
+ }}
+ containerStyles={[styles.mh0, styles.mt5, styles.flex1]}
+ enabledWhenOffline={this.props.enabledWhenOffline}
+ isSubmitActionDangerous={this.props.isSubmitActionDangerous}
+ />
+ )}
+
+ );
+
return (
- {({safeAreaPaddingBottomStyle}) => (
+ {({safeAreaPaddingBottomStyle}) => (this.props.scrollContextEnabled ? (
this.form = el}
+ scrollToOverflowEnabled={this.props.scrollToOverflowEnabled}
+ ref={this.formRef}
>
-
- {this.childrenWrapperWithProps(this.props.children)}
- {this.props.isSubmitButtonVisible && (
- 0 || Boolean(this.getErrorMessage()) || !_.isEmpty(this.props.formState.errorFields)}
- isLoading={this.props.formState.isLoading}
- message={_.isEmpty(this.props.formState.errorFields) ? this.getErrorMessage() : null}
- onSubmit={this.submit}
- onFixTheErrorsLinkPressed={() => {
- const errors = !_.isEmpty(this.state.errors) ? this.state.errors : this.props.formState.errorFields;
- const focusKey = _.find(_.keys(this.inputRefs), key => _.keys(errors).includes(key));
- const focusInput = this.inputRefs[focusKey];
- if (focusInput.focus && typeof focusInput.focus === 'function') {
- focusInput.focus();
- }
-
- // We subtract 10 to scroll slightly above the input
- if (focusInput.measureLayout && typeof focusInput.measureLayout === 'function') {
- focusInput.measureLayout(this.form, (x, y) => this.form.scrollTo({y: y - 10, animated: false}));
- }
- }}
- containerStyles={[styles.mh0, styles.mt5, styles.flex1]}
- enabledWhenOffline={this.props.enabledWhenOffline}
- isSubmitActionDangerous={this.props.isSubmitActionDangerous}
- />
- )}
-
+ {scrollViewContent(safeAreaPaddingBottomStyle)}
- )}
+ ) : (
+
+ {scrollViewContent(safeAreaPaddingBottomStyle)}
+
+ ))}
);
}
diff --git a/src/components/ScrollViewWithContext.js b/src/components/ScrollViewWithContext.js
index 6d6c74c33245c..63c6e6f89db73 100644
--- a/src/components/ScrollViewWithContext.js
+++ b/src/components/ScrollViewWithContext.js
@@ -22,7 +22,7 @@ class ScrollViewWithContext extends React.Component {
this.state = {
contentOffsetY: 0,
};
- this.scrollViewRef = React.createRef(null);
+ this.scrollViewRef = this.props.innerRef || React.createRef(null);
this.setContextScrollPosition = this.setContextScrollPosition.bind(this);
}
@@ -42,7 +42,6 @@ class ScrollViewWithContext extends React.Component {
ref={this.scrollViewRef}
onScroll={this.setContextScrollPosition}
scrollEventThrottle={this.props.scrollEventThrottle || MIN_SMOOTH_SCROLL_EVENT_THROTTLE}
- scrollToOverflowEnabled
>
(
+ // eslint-disable-next-line react/jsx-props-no-spreading
+
+));
+
export {
ScrollContext,
};
diff --git a/src/pages/AddPersonalBankAccountPage.js b/src/pages/AddPersonalBankAccountPage.js
index d755d06e345f9..b140d46578016 100644
--- a/src/pages/AddPersonalBankAccountPage.js
+++ b/src/pages/AddPersonalBankAccountPage.js
@@ -108,6 +108,7 @@ class AddPersonalBankAccountPage extends React.Component {
formID={ONYXKEYS.PERSONAL_BANK_ACCOUNT}
isSubmitButtonVisible={Boolean(this.state.selectedPlaidAccountID)}
submitButtonText={this.props.translate('common.saveAndContinue')}
+ scrollContextEnabled
onSubmit={this.submit}
validate={this.validate}
style={[styles.mh5, styles.flex1]}
diff --git a/src/pages/EnablePayments/AdditionalDetailsStep.js b/src/pages/EnablePayments/AdditionalDetailsStep.js
index c303c6fac53ca..57d966be05aa8 100644
--- a/src/pages/EnablePayments/AdditionalDetailsStep.js
+++ b/src/pages/EnablePayments/AdditionalDetailsStep.js
@@ -225,6 +225,8 @@ class AdditionalDetailsStep extends React.Component {
formID={ONYXKEYS.WALLET_ADDITIONAL_DETAILS}
validate={this.validate}
onSubmit={this.activateWallet}
+ scrollContextEnabled
+ scrollToOverflowEnabled
submitButtonText={this.props.translate('common.saveAndContinue')}
style={[styles.mh5, styles.flexGrow1]}
>
diff --git a/src/pages/ReimbursementAccount/BankAccountPlaidStep.js b/src/pages/ReimbursementAccount/BankAccountPlaidStep.js
index b23e593e96c48..45ef47dd4686c 100644
--- a/src/pages/ReimbursementAccount/BankAccountPlaidStep.js
+++ b/src/pages/ReimbursementAccount/BankAccountPlaidStep.js
@@ -82,6 +82,7 @@ class BankAccountPlaidStep extends React.Component {
formID={ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM}
validate={() => ({})}
onSubmit={this.submit}
+ scrollContextEnabled
submitButtonText={this.props.translate('common.saveAndContinue')}
style={[styles.mh5, styles.flexGrow1]}
isSubmitButtonVisible={Boolean(selectedPlaidAccountID) && !_.isEmpty(lodashGet(this.props.plaidData, 'bankAccounts'))}
diff --git a/src/pages/ReimbursementAccount/CompanyStep.js b/src/pages/ReimbursementAccount/CompanyStep.js
index fefa825e04872..9309a31701e1d 100644
--- a/src/pages/ReimbursementAccount/CompanyStep.js
+++ b/src/pages/ReimbursementAccount/CompanyStep.js
@@ -156,6 +156,8 @@ class CompanyStep extends React.Component {
formID={ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM}
validate={this.validate}
onSubmit={this.submit}
+ scrollContextEnabled
+ scrollToOverflowEnabled
submitButtonText={this.props.translate('common.saveAndContinue')}
style={[styles.ph5, styles.flexGrow1]}
>
diff --git a/src/pages/ReimbursementAccount/RequestorStep.js b/src/pages/ReimbursementAccount/RequestorStep.js
index 6823eedddfe36..9a9139277f8d5 100644
--- a/src/pages/ReimbursementAccount/RequestorStep.js
+++ b/src/pages/ReimbursementAccount/RequestorStep.js
@@ -126,6 +126,7 @@ class RequestorStep extends React.Component {
formID={ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM}
submitButtonText={this.props.translate('common.saveAndContinue')}
validate={this.validate}
+ scrollContextEnabled
onSubmit={this.submit}
style={[styles.mh5, styles.flexGrow1]}
>
diff --git a/src/pages/ReportSettingsPage.js b/src/pages/ReportSettingsPage.js
index f9cc86372fe91..1ff79c3b98a2d 100644
--- a/src/pages/ReportSettingsPage.js
+++ b/src/pages/ReportSettingsPage.js
@@ -128,6 +128,7 @@ class ReportSettingsPage extends Component {
style={[styles.mh5, styles.mt5, styles.flexGrow1]}
validate={this.validate}
onSubmit={this.updatePolicyRoomName}
+ scrollContextEnabled
isSubmitButtonVisible={shouldShowRoomName && !shouldDisableRename}
enabledWhenOffline
>
diff --git a/src/pages/settings/Payments/AddDebitCardPage.js b/src/pages/settings/Payments/AddDebitCardPage.js
index 5e5ff76fe51fc..7a7111b902551 100644
--- a/src/pages/settings/Payments/AddDebitCardPage.js
+++ b/src/pages/settings/Payments/AddDebitCardPage.js
@@ -120,6 +120,8 @@ class DebitCardPage extends Component {
validate={this.validate}
onSubmit={PaymentMethods.addPaymentCard}
submitButtonText={this.props.translate('common.save')}
+ scrollContextEnabled
+ scrollToOverflowEnabled
style={[styles.mh5, styles.flexGrow1]}
>