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]} >