From d4fef2ff42250ae7cd2835cb1cfbd3527b56968b Mon Sep 17 00:00:00 2001 From: nkdengineer Date: Tue, 24 Feb 2026 23:18:16 +0700 Subject: [PATCH 1/5] fix: Expensify card - The Choose a limit type page re-appears briefly --- .../expensifyCard/issueNew/LimitTypeStep.tsx | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/pages/workspace/expensifyCard/issueNew/LimitTypeStep.tsx b/src/pages/workspace/expensifyCard/issueNew/LimitTypeStep.tsx index e8d95b57a92c..a7d59ca5bec0 100644 --- a/src/pages/workspace/expensifyCard/issueNew/LimitTypeStep.tsx +++ b/src/pages/workspace/expensifyCard/issueNew/LimitTypeStep.tsx @@ -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 @@ -59,12 +60,14 @@ function LimitTypeStep({policy, stepNames, startStepIndex}: LimitTypeStepProps) 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], From 3526d4f5d9e93ab94acdf4f032ee7fa98da7c518 Mon Sep 17 00:00:00 2001 From: nkdengineer Date: Wed, 25 Feb 2026 00:16:06 +0700 Subject: [PATCH 2/5] scroll to end when focusing on the input --- src/components/Form/FormProvider.tsx | 9 ++++++++- src/components/Form/FormWrapper.tsx | 17 +++++++++++++--- src/components/Form/types.ts | 20 ++++++++++++++++++- .../expensifyCard/issueNew/LimitTypeStep.tsx | 17 +++++++++++++--- 4 files changed, 55 insertions(+), 8 deletions(-) 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..50f277cfee50 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,10 @@ function FormWrapper({ focusInput?.focus?.(); }; + const scrollToEnd = () => { + formRef.current?.scrollToEnd({animated: true}); + }; + // 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 +168,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/pages/workspace/expensifyCard/issueNew/LimitTypeStep.tsx b/src/pages/workspace/expensifyCard/issueNew/LimitTypeStep.tsx index a7d59ca5bec0..506a4569b10e 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 {View} from 'react-native'; +import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; +import {Keyboard, 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'; @@ -41,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); @@ -58,6 +59,15 @@ function LimitTypeStep({policy, stepNames, startStepIndex}: LimitTypeStepProps) return CONST.EXPENSIFY_CARD.STEP.CARD_NAME; }, [isBetaEnabled, isEditing, issueNewCard?.data?.cardType]); + useEffect(() => { + const listener = Keyboard.addListener('keyboardDidShow', () => { + formRef.current?.scrollToEnd(); + }); + return () => { + listener.remove(); + }; + }, []); + const submit = useCallback( (values: FormOnyxValues) => { KeyboardUtils.dismiss().then(() => { @@ -163,6 +173,7 @@ function LimitTypeStep({policy, stepNames, startStepIndex}: LimitTypeStepProps) validate={validate} enabledWhenOffline addBottomSafeAreaPadding + ref={formRef} > {translate('workspace.card.issueNewCard.chooseLimitType')} Date: Wed, 25 Feb 2026 00:25:26 +0700 Subject: [PATCH 3/5] use focus event instead --- src/components/AmountForm.tsx | 5 +++++ src/components/NumberWithSymbolForm.tsx | 1 + .../expensifyCard/issueNew/LimitTypeStep.tsx | 14 +++++--------- 3 files changed, 11 insertions(+), 9 deletions(-) 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/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 506a4569b10e..e50be5f2c55f 100644 --- a/src/pages/workspace/expensifyCard/issueNew/LimitTypeStep.tsx +++ b/src/pages/workspace/expensifyCard/issueNew/LimitTypeStep.tsx @@ -1,5 +1,5 @@ -import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; -import {Keyboard, View} from 'react-native'; +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'; @@ -59,13 +59,8 @@ function LimitTypeStep({policy, stepNames, startStepIndex}: LimitTypeStepProps) return CONST.EXPENSIFY_CARD.STEP.CARD_NAME; }, [isBetaEnabled, isEditing, issueNewCard?.data?.cardType]); - useEffect(() => { - const listener = Keyboard.addListener('keyboardDidShow', () => { - formRef.current?.scrollToEnd(); - }); - return () => { - listener.remove(); - }; + const onInputFocus = useCallback(() => { + formRef.current?.scrollToEnd(); }, []); const submit = useCallback( @@ -204,6 +199,7 @@ function LimitTypeStep({policy, stepNames, startStepIndex}: LimitTypeStepProps) currency={issueNewCard?.data?.currency} inputID={INPUT_IDS.LIMIT} displayAsTextInput + onFocus={onInputFocus} /> From 1fe44570a1cbe7e8cfb147b037c3ab06d52824dd Mon Sep 17 00:00:00 2001 From: nkdengineer Date: Wed, 25 Feb 2026 12:11:38 +0700 Subject: [PATCH 4/5] fix IOS issue --- src/components/Form/FormWrapper.tsx | 5 ++++- src/pages/workspace/expensifyCard/issueNew/LimitTypeStep.tsx | 1 + 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/components/Form/FormWrapper.tsx b/src/components/Form/FormWrapper.tsx index 50f277cfee50..73aaaadf22d0 100644 --- a/src/components/Form/FormWrapper.tsx +++ b/src/components/Form/FormWrapper.tsx @@ -142,7 +142,10 @@ function FormWrapper({ }; const scrollToEnd = () => { - formRef.current?.scrollToEnd({animated: true}); + // 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, diff --git a/src/pages/workspace/expensifyCard/issueNew/LimitTypeStep.tsx b/src/pages/workspace/expensifyCard/issueNew/LimitTypeStep.tsx index e50be5f2c55f..753f4f25d408 100644 --- a/src/pages/workspace/expensifyCard/issueNew/LimitTypeStep.tsx +++ b/src/pages/workspace/expensifyCard/issueNew/LimitTypeStep.tsx @@ -60,6 +60,7 @@ function LimitTypeStep({policy, stepNames, startStepIndex}: LimitTypeStepProps) }, [isBetaEnabled, isEditing, issueNewCard?.data?.cardType]); const onInputFocus = useCallback(() => { + console.log("scroll to end"); formRef.current?.scrollToEnd(); }, []); From e57648b29634be50abb940770b7a1f902281697c Mon Sep 17 00:00:00 2001 From: nkdengineer Date: Wed, 25 Feb 2026 12:20:15 +0700 Subject: [PATCH 5/5] fix prettier --- src/pages/workspace/expensifyCard/issueNew/LimitTypeStep.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/pages/workspace/expensifyCard/issueNew/LimitTypeStep.tsx b/src/pages/workspace/expensifyCard/issueNew/LimitTypeStep.tsx index 753f4f25d408..e50be5f2c55f 100644 --- a/src/pages/workspace/expensifyCard/issueNew/LimitTypeStep.tsx +++ b/src/pages/workspace/expensifyCard/issueNew/LimitTypeStep.tsx @@ -60,7 +60,6 @@ function LimitTypeStep({policy, stepNames, startStepIndex}: LimitTypeStepProps) }, [isBetaEnabled, isEditing, issueNewCard?.data?.cardType]); const onInputFocus = useCallback(() => { - console.log("scroll to end"); formRef.current?.scrollToEnd(); }, []);