diff --git a/src/components/AmountForm.tsx b/src/components/AmountForm.tsx index f3e9db7e802e..d8acc426172f 100644 --- a/src/components/AmountForm.tsx +++ b/src/components/AmountForm.tsx @@ -57,6 +57,9 @@ type AmountFormProps = { /** Callback when the user presses the submit key (Enter) */ onSubmitEditing?: () => void; + + /** Callback when the input is focused */ + onFocus?: () => void; } & Pick; /** @@ -79,6 +82,7 @@ function AmountForm({ autoGrowExtraSpace, autoGrowMarginSide, onSubmitEditing, + onFocus, ref, numberFormRef, }: AmountFormProps) { @@ -119,6 +123,7 @@ function AmountForm({ autoGrowMarginSide={autoGrowMarginSide} onSubmitEditing={onSubmitEditing} disabled={disabled} + onFocus={onFocus} /> ); } diff --git a/src/components/Form/FormProvider.tsx b/src/components/Form/FormProvider.tsx index a78ef32eba37..ad0f0b671049 100644 --- a/src/components/Form/FormProvider.tsx +++ b/src/components/Form/FormProvider.tsx @@ -25,7 +25,7 @@ import KeyboardUtils from '@src/utils/keyboard'; import type {RegisterInput} from './FormContext'; import FormContext from './FormContext'; import FormWrapper from './FormWrapper'; -import type {FormInputErrors, FormOnyxValues, FormProps, FormRef, InputComponentBaseProps, InputRefs, ValueTypeKey} from './types'; +import type {FormInputErrors, FormOnyxValues, FormProps, FormRef, FormWrapperRef, InputComponentBaseProps, InputRefs, ValueTypeKey} from './types'; // In order to prevent Checkbox focus loss when the user are focusing a TextInput and proceeds to toggle a CheckBox in web and mobile web. // 200ms delay was chosen as a result of empirical testing. @@ -126,6 +126,7 @@ function FormProvider({ const [draftValues, draftValuesMetadata] = useOnyx(`${formID}Draft`); const {preferredLocale, translate} = useLocalize(); const inputRefs = useRef({}); + const formWrapperRef = useRef(null); const touchedInputs = useRef>({}); const [inputValues, setInputValues] = useState
(() => ({...draftValues})); const isLoadingDraftValues = isLoadingOnyxValue(draftValuesMetadata); @@ -313,11 +314,16 @@ function FormProvider({ [errors, formID], ); + const scrollToEnd = useCallback(() => { + formWrapperRef.current?.scrollToEnd(); + }, []); + useImperativeHandle(ref, () => ({ resetForm, resetErrors, resetFormFieldError, submit, + scrollToEnd, })); const registerInput = useCallback( @@ -467,6 +473,7 @@ function FormProvider({ enabledWhenOffline={enabledWhenOffline} shouldRenderFooterAboveSubmit={shouldRenderFooterAboveSubmit} shouldPreventDefaultFocusOnPressSubmit={shouldPreventDefaultFocusOnPressSubmit} + ref={formWrapperRef} > {typeof children === 'function' ? children({inputValues}) : children} diff --git a/src/components/Form/FormWrapper.tsx b/src/components/Form/FormWrapper.tsx index 824b968b9d78..73aaaadf22d0 100644 --- a/src/components/Form/FormWrapper.tsx +++ b/src/components/Form/FormWrapper.tsx @@ -1,5 +1,5 @@ -import React, {useRef} from 'react'; -import type {RefObject} from 'react'; +import React, {useImperativeHandle, useRef} from 'react'; +import type {ForwardedRef, RefObject} from 'react'; // eslint-disable-next-line no-restricted-imports import type {ScrollView as RNScrollView, StyleProp, ViewStyle} from 'react-native'; import {InteractionManager, Keyboard, View} from 'react-native'; @@ -17,7 +17,7 @@ import type {OnyxFormKey} from '@src/ONYXKEYS'; import type {Form} from '@src/types/form'; import type ChildrenProps from '@src/types/utils/ChildrenProps'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; -import type {FormInputErrors, FormProps, InputRefs} from './types'; +import type {FormInputErrors, FormProps, FormWrapperRef, InputRefs} from './types'; type FormWrapperProps = ChildrenProps & FormProps & { @@ -65,6 +65,8 @@ type FormWrapperProps = ChildrenProps & /** Prevents the submit button from triggering blur on mouse down. */ shouldPreventDefaultFocusOnPressSubmit?: boolean; + + ref?: ForwardedRef; }; function FormWrapper({ @@ -98,6 +100,7 @@ function FormWrapper({ onScroll = () => {}, forwardedFSClass, sentryLabel = CONST.SENTRY_LABEL.FORM.SUBMIT_BUTTON, + ref, }: FormWrapperProps) { const styles = useThemeStyles(); const formRef = useRef(null); @@ -138,6 +141,13 @@ function FormWrapper({ focusInput?.focus?.(); }; + const scrollToEnd = () => { + // We need to wait for the keyboard animation to complete before scrolling to the end + setTimeout(() => { + formRef.current?.scrollToEnd({animated: true}); + }, CONST.ANIMATED_TRANSITION); + }; + // If either of `addBottomSafeAreaPadding` or `shouldSubmitButtonStickToBottom` is explicitly set, // we expect that the user wants to use the new edge-to-edge mode. // In this case, we want to get and apply the padding unconditionally. @@ -161,6 +171,10 @@ function FormWrapper({ style: submitButtonStyles, }); + useImperativeHandle(ref, () => ({ + scrollToEnd, + })); + const SubmitButton = isSubmitButtonVisible && ( = { resetErrors: () => void; resetFormFieldError: (fieldID: keyof Form) => void; submit: () => void; + scrollToEnd: () => void; +}; + +type FormWrapperRef = { + scrollToEnd: () => void; }; type InputRefs = Record>; type FormInputErrors = Partial, string | undefined>>; -export type {FormProps, ValidInputs, InputComponentValueProps, FormValue, ValueTypeKey, FormOnyxValues, FormOnyxKeys, FormInputErrors, InputRefs, InputComponentBaseProps, FormRef}; +export type { + FormProps, + ValidInputs, + InputComponentValueProps, + FormValue, + ValueTypeKey, + FormOnyxValues, + FormOnyxKeys, + FormInputErrors, + InputRefs, + InputComponentBaseProps, + FormRef, + FormWrapperRef, +}; diff --git a/src/components/NumberWithSymbolForm.tsx b/src/components/NumberWithSymbolForm.tsx index 809d8c03e4a2..30dfd3cc714a 100644 --- a/src/components/NumberWithSymbolForm.tsx +++ b/src/components/NumberWithSymbolForm.tsx @@ -401,6 +401,7 @@ function NumberWithSymbolForm({ autoGrowExtraSpace={props.autoGrowExtraSpace} autoGrowMarginSide={props.autoGrowMarginSide} onSubmitEditing={onSubmitEditing} + onFocus={props.onFocus} /> ); } diff --git a/src/pages/workspace/expensifyCard/issueNew/LimitTypeStep.tsx b/src/pages/workspace/expensifyCard/issueNew/LimitTypeStep.tsx index e8d95b57a92c..e50be5f2c55f 100644 --- a/src/pages/workspace/expensifyCard/issueNew/LimitTypeStep.tsx +++ b/src/pages/workspace/expensifyCard/issueNew/LimitTypeStep.tsx @@ -1,10 +1,10 @@ -import React, {useCallback, useMemo, useState} from 'react'; +import React, {useCallback, useMemo, useRef, useState} from 'react'; import {View} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; import AmountForm from '@components/AmountForm'; import FormProvider from '@components/Form/FormProvider'; import InputWrapperWithRef from '@components/Form/InputWrapper'; -import type {FormInputErrors, FormOnyxValues} from '@components/Form/types'; +import type {FormInputErrors, FormOnyxValues, FormRef} from '@components/Form/types'; import InteractiveStepWrapper from '@components/InteractiveStepWrapper'; import Text from '@components/Text'; import ValuePicker from '@components/ValuePicker'; @@ -22,6 +22,7 @@ import ONYXKEYS from '@src/ONYXKEYS'; import INPUT_IDS from '@src/types/form/IssueNewExpensifyCardForm'; import type * as OnyxTypes from '@src/types/onyx'; import type {CardLimitType} from '@src/types/onyx/Card'; +import KeyboardUtils from '@src/utils/keyboard'; type LimitTypeStepProps = { // The policy that the card will be issued under @@ -40,6 +41,7 @@ function LimitTypeStep({policy, stepNames, startStepIndex}: LimitTypeStepProps) const policyID = policy?.id; const [issueNewCard] = useOnyx(`${ONYXKEYS.COLLECTION.ISSUE_NEW_EXPENSIFY_CARD}${policyID}`); const {isBetaEnabled} = usePermissions(); + const formRef = useRef(null); const areApprovalsConfigured = getApprovalWorkflow(policy) !== CONST.POLICY.APPROVAL_MODE.OPTIONAL; const defaultType = getDefaultExpensifyCardLimitType(policy); @@ -57,14 +59,20 @@ function LimitTypeStep({policy, stepNames, startStepIndex}: LimitTypeStepProps) return CONST.EXPENSIFY_CARD.STEP.CARD_NAME; }, [isBetaEnabled, isEditing, issueNewCard?.data?.cardType]); + const onInputFocus = useCallback(() => { + formRef.current?.scrollToEnd(); + }, []); + const submit = useCallback( (values: FormOnyxValues) => { - const limit = convertToBackendAmount(Number(values?.limit)); - setIssueNewCardStepAndData({ - step: nextStep, - data: {limitType: typeSelected, limit}, - isEditing: false, - policyID, + KeyboardUtils.dismiss().then(() => { + const limit = convertToBackendAmount(Number(values?.limit)); + setIssueNewCardStepAndData({ + step: nextStep, + data: {limitType: typeSelected, limit}, + isEditing: false, + policyID, + }); }); }, [nextStep, typeSelected, policyID], @@ -160,6 +168,7 @@ function LimitTypeStep({policy, stepNames, startStepIndex}: LimitTypeStepProps) validate={validate} enabledWhenOffline addBottomSafeAreaPadding + ref={formRef} > {translate('workspace.card.issueNewCard.chooseLimitType')}