From 5f1dba701e3d8f790b63005391afc722048c13ed Mon Sep 17 00:00:00 2001 From: "Linh Vo (via MelvinBot)" Date: Mon, 16 Mar 2026 17:15:22 +0000 Subject: [PATCH 1/5] Remove returnKeyType for numeric/phone pad keyboards On iOS, phone/numeric pad keyboards don't have a built-in return key. When FormProvider set returnKeyType: 'go' on these inputs, React Native's native iOS code created a UIToolbar with a visible "Go" button above the keyboard, duplicating the form's existing submit button. This change detects numeric keyboard types (tel, numeric, decimal, phone-pad, number-pad, decimal-pad) and skips setting returnKeyType for them, removing the extra toolbar button while keeping onSubmitEditing for hardware keyboard submission. Co-authored-by: Linh Vo --- src/components/Form/FormProvider.tsx | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/components/Form/FormProvider.tsx b/src/components/Form/FormProvider.tsx index 36817b2c4659c..84e9309088c98 100644 --- a/src/components/Form/FormProvider.tsx +++ b/src/components/Form/FormProvider.tsx @@ -351,6 +351,17 @@ function FormProvider({ const inputRef = inputProps.ref; + // Numeric/phone pad keyboards on iOS don't have a built-in return key. Setting returnKeyType + // on these creates a visible toolbar button (e.g. "Go") that duplicates the form's submit button. + const inputPropsRecord = inputProps as Record; + const isNumericKeyboard = + inputPropsRecord.inputMode === CONST.INPUT_MODE.TEL || + inputPropsRecord.inputMode === CONST.INPUT_MODE.NUMERIC || + inputPropsRecord.inputMode === CONST.INPUT_MODE.DECIMAL || + inputPropsRecord.keyboardType === 'phone-pad' || + inputPropsRecord.keyboardType === 'number-pad' || + inputPropsRecord.keyboardType === 'decimal-pad'; + return { ...inputProps, ...(shouldSubmitForm && { @@ -359,7 +370,7 @@ function FormProvider({ inputProps.onSubmitEditing?.(event); }, - returnKeyType: 'go', + ...(!isNumericKeyboard && {returnKeyType: 'go' as const}), }), ref: typeof inputRef === 'function' From 592094be7a4cc1dfc5bdfeb5e2c18d3e50d8d319 Mon Sep 17 00:00:00 2001 From: "Linh Vo (via MelvinBot)" Date: Tue, 17 Mar 2026 02:33:52 +0000 Subject: [PATCH 2/5] Extract isNumericKeyboard into helper function with unit tests Moved the numeric keyboard detection logic from FormProvider into a dedicated isNumericKeyboard helper and added comprehensive unit tests covering all inputMode and keyboardType variants. Co-authored-by: Linh Vo --- src/components/Form/FormProvider.tsx | 14 ++---- src/components/Form/isNumericKeyboard.ts | 19 ++++++++ tests/unit/isNumericKeyboardTest.ts | 56 ++++++++++++++++++++++++ 3 files changed, 78 insertions(+), 11 deletions(-) create mode 100644 src/components/Form/isNumericKeyboard.ts create mode 100644 tests/unit/isNumericKeyboardTest.ts diff --git a/src/components/Form/FormProvider.tsx b/src/components/Form/FormProvider.tsx index 84e9309088c98..2d49ae047b866 100644 --- a/src/components/Form/FormProvider.tsx +++ b/src/components/Form/FormProvider.tsx @@ -23,6 +23,7 @@ import isLoadingOnyxValue from '@src/types/utils/isLoadingOnyxValue'; import KeyboardUtils from '@src/utils/keyboard'; import type {RegisterInput} from './FormContext'; import FormContext from './FormContext'; +import isNumericKeyboard from './isNumericKeyboard'; import FormWrapper from './FormWrapper'; import type {FormInputErrors, FormOnyxValues, FormProps, FormRef, FormWrapperRef, InputComponentBaseProps, InputRefs, ValueTypeKey} from './types'; @@ -351,16 +352,7 @@ function FormProvider({ const inputRef = inputProps.ref; - // Numeric/phone pad keyboards on iOS don't have a built-in return key. Setting returnKeyType - // on these creates a visible toolbar button (e.g. "Go") that duplicates the form's submit button. - const inputPropsRecord = inputProps as Record; - const isNumericKeyboard = - inputPropsRecord.inputMode === CONST.INPUT_MODE.TEL || - inputPropsRecord.inputMode === CONST.INPUT_MODE.NUMERIC || - inputPropsRecord.inputMode === CONST.INPUT_MODE.DECIMAL || - inputPropsRecord.keyboardType === 'phone-pad' || - inputPropsRecord.keyboardType === 'number-pad' || - inputPropsRecord.keyboardType === 'decimal-pad'; + const hasNumericKeyboard = isNumericKeyboard(inputProps as Record); return { ...inputProps, @@ -370,7 +362,7 @@ function FormProvider({ inputProps.onSubmitEditing?.(event); }, - ...(!isNumericKeyboard && {returnKeyType: 'go' as const}), + ...(!hasNumericKeyboard && {returnKeyType: 'go' as const}), }), ref: typeof inputRef === 'function' diff --git a/src/components/Form/isNumericKeyboard.ts b/src/components/Form/isNumericKeyboard.ts new file mode 100644 index 0000000000000..a820af0fd9417 --- /dev/null +++ b/src/components/Form/isNumericKeyboard.ts @@ -0,0 +1,19 @@ +import CONST from '@src/CONST'; + +/** + * Determines whether the given input props indicate a numeric or phone pad keyboard type. + * Numeric/phone pad keyboards on iOS don't have a built-in return key. Setting returnKeyType + * on these creates a visible toolbar button (e.g. "Go") that duplicates the form's submit button. + */ +function isNumericKeyboard(inputProps: Record): boolean { + return ( + inputProps.inputMode === CONST.INPUT_MODE.TEL || + inputProps.inputMode === CONST.INPUT_MODE.NUMERIC || + inputProps.inputMode === CONST.INPUT_MODE.DECIMAL || + inputProps.keyboardType === 'phone-pad' || + inputProps.keyboardType === 'number-pad' || + inputProps.keyboardType === 'decimal-pad' + ); +} + +export default isNumericKeyboard; diff --git a/tests/unit/isNumericKeyboardTest.ts b/tests/unit/isNumericKeyboardTest.ts new file mode 100644 index 0000000000000..4491a8f935dc1 --- /dev/null +++ b/tests/unit/isNumericKeyboardTest.ts @@ -0,0 +1,56 @@ +import isNumericKeyboard from '@components/Form/isNumericKeyboard'; +import CONST from '@src/CONST'; + +describe('isNumericKeyboard', () => { + it('returns true for inputMode tel', () => { + expect(isNumericKeyboard({inputMode: CONST.INPUT_MODE.TEL})).toBe(true); + }); + + it('returns true for inputMode numeric', () => { + expect(isNumericKeyboard({inputMode: CONST.INPUT_MODE.NUMERIC})).toBe(true); + }); + + it('returns true for inputMode decimal', () => { + expect(isNumericKeyboard({inputMode: CONST.INPUT_MODE.DECIMAL})).toBe(true); + }); + + it('returns true for keyboardType phone-pad', () => { + expect(isNumericKeyboard({keyboardType: 'phone-pad'})).toBe(true); + }); + + it('returns true for keyboardType number-pad', () => { + expect(isNumericKeyboard({keyboardType: 'number-pad'})).toBe(true); + }); + + it('returns true for keyboardType decimal-pad', () => { + expect(isNumericKeyboard({keyboardType: 'decimal-pad'})).toBe(true); + }); + + it('returns false for inputMode text', () => { + expect(isNumericKeyboard({inputMode: CONST.INPUT_MODE.TEXT})).toBe(false); + }); + + it('returns false for inputMode email', () => { + expect(isNumericKeyboard({inputMode: CONST.INPUT_MODE.EMAIL})).toBe(false); + }); + + it('returns false for keyboardType ascii-capable', () => { + expect(isNumericKeyboard({keyboardType: 'ascii-capable'})).toBe(false); + }); + + it('returns false for empty props', () => { + expect(isNumericKeyboard({})).toBe(false); + }); + + it('returns false when neither inputMode nor keyboardType is set', () => { + expect(isNumericKeyboard({placeholder: 'Enter value'})).toBe(false); + }); + + it('returns true when inputMode is numeric even if keyboardType is non-numeric', () => { + expect(isNumericKeyboard({inputMode: CONST.INPUT_MODE.NUMERIC, keyboardType: 'default'})).toBe(true); + }); + + it('returns true when keyboardType is phone-pad even if inputMode is text', () => { + expect(isNumericKeyboard({inputMode: CONST.INPUT_MODE.TEXT, keyboardType: 'phone-pad'})).toBe(true); + }); +}); From 5d77b3134c0aa970620944c3f86079902d7842f9 Mon Sep 17 00:00:00 2001 From: "Linh Vo (via MelvinBot)" Date: Tue, 17 Mar 2026 02:36:11 +0000 Subject: [PATCH 3/5] Fix: sort imports to satisfy Prettier Co-authored-by: Linh Vo --- src/components/Form/FormProvider.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Form/FormProvider.tsx b/src/components/Form/FormProvider.tsx index 2d49ae047b866..50dad4177deca 100644 --- a/src/components/Form/FormProvider.tsx +++ b/src/components/Form/FormProvider.tsx @@ -23,8 +23,8 @@ import isLoadingOnyxValue from '@src/types/utils/isLoadingOnyxValue'; import KeyboardUtils from '@src/utils/keyboard'; import type {RegisterInput} from './FormContext'; import FormContext from './FormContext'; -import isNumericKeyboard from './isNumericKeyboard'; import FormWrapper from './FormWrapper'; +import isNumericKeyboard from './isNumericKeyboard'; 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. From 406bebe5edf2f96448fd0d355ac24a68d3a8a614 Mon Sep 17 00:00:00 2001 From: "Linh Vo (via MelvinBot)" Date: Tue, 17 Mar 2026 03:06:37 +0000 Subject: [PATCH 4/5] Use InputComponentBaseProps type for isNumericKeyboard Added inputMode and keyboardType to InputComponentBaseProps and created a NumericKeyboardProps type using Pick, replacing Record. Co-authored-by: Linh Vo --- src/components/Form/FormProvider.tsx | 2 +- src/components/Form/isNumericKeyboard.ts | 6 +++++- src/components/Form/types.ts | 4 +++- tests/unit/isNumericKeyboardTest.ts | 4 +++- 4 files changed, 12 insertions(+), 4 deletions(-) diff --git a/src/components/Form/FormProvider.tsx b/src/components/Form/FormProvider.tsx index 50dad4177deca..ac87061e7cdf3 100644 --- a/src/components/Form/FormProvider.tsx +++ b/src/components/Form/FormProvider.tsx @@ -352,7 +352,7 @@ function FormProvider({ const inputRef = inputProps.ref; - const hasNumericKeyboard = isNumericKeyboard(inputProps as Record); + const hasNumericKeyboard = isNumericKeyboard(inputProps); return { ...inputProps, diff --git a/src/components/Form/isNumericKeyboard.ts b/src/components/Form/isNumericKeyboard.ts index a820af0fd9417..4bf32df17cdcc 100644 --- a/src/components/Form/isNumericKeyboard.ts +++ b/src/components/Form/isNumericKeyboard.ts @@ -1,11 +1,14 @@ import CONST from '@src/CONST'; +import type {InputComponentBaseProps} from './types'; + +type NumericKeyboardProps = Pick; /** * Determines whether the given input props indicate a numeric or phone pad keyboard type. * Numeric/phone pad keyboards on iOS don't have a built-in return key. Setting returnKeyType * on these creates a visible toolbar button (e.g. "Go") that duplicates the form's submit button. */ -function isNumericKeyboard(inputProps: Record): boolean { +function isNumericKeyboard(inputProps: NumericKeyboardProps): boolean { return ( inputProps.inputMode === CONST.INPUT_MODE.TEL || inputProps.inputMode === CONST.INPUT_MODE.NUMERIC || @@ -17,3 +20,4 @@ function isNumericKeyboard(inputProps: Record): boolean { } export default isNumericKeyboard; +export type {NumericKeyboardProps}; diff --git a/src/components/Form/types.ts b/src/components/Form/types.ts index 70e5acbdc2a28..1b97835a9e6cd 100644 --- a/src/components/Form/types.ts +++ b/src/components/Form/types.ts @@ -1,5 +1,5 @@ import type {ComponentRef, ComponentType, FocusEvent, Key, ReactNode, Ref, RefObject} from 'react'; -import type {GestureResponderEvent, HostComponent, StyleProp, SubmitBehavior, TextInputSubmitEditingEvent, ViewStyle} from 'react-native'; +import type {GestureResponderEvent, HostComponent, InputModeOptions, KeyboardTypeOptions, StyleProp, SubmitBehavior, TextInputSubmitEditingEvent, ViewStyle} from 'react-native'; import type {ValueOf} from 'type-fest'; import type AddPlaidBankAccount from '@components/AddPlaidBankAccount'; import type AddressSearch from '@components/AddressSearch'; @@ -125,6 +125,8 @@ type InputComponentBaseProps = Input submitBehavior?: SubmitBehavior; shouldSubmitForm?: boolean; uncontrolled?: boolean; + inputMode?: InputModeOptions; + keyboardType?: KeyboardTypeOptions; getNativeRef?: () => ComponentRef> & RefObject; }; diff --git a/tests/unit/isNumericKeyboardTest.ts b/tests/unit/isNumericKeyboardTest.ts index 4491a8f935dc1..06d84b2988bbb 100644 --- a/tests/unit/isNumericKeyboardTest.ts +++ b/tests/unit/isNumericKeyboardTest.ts @@ -1,4 +1,5 @@ import isNumericKeyboard from '@components/Form/isNumericKeyboard'; +import type {NumericKeyboardProps} from '@components/Form/isNumericKeyboard'; import CONST from '@src/CONST'; describe('isNumericKeyboard', () => { @@ -43,7 +44,8 @@ describe('isNumericKeyboard', () => { }); it('returns false when neither inputMode nor keyboardType is set', () => { - expect(isNumericKeyboard({placeholder: 'Enter value'})).toBe(false); + const props: NumericKeyboardProps = {}; + expect(isNumericKeyboard(props)).toBe(false); }); it('returns true when inputMode is numeric even if keyboardType is non-numeric', () => { From 0f539d15a0d48e209972abacb8fdcae8f1ed88d9 Mon Sep 17 00:00:00 2001 From: "Linh Vo (via MelvinBot)" Date: Tue, 17 Mar 2026 03:09:06 +0000 Subject: [PATCH 5/5] Use CONST variables for keyboard type string literals Added CONST.KEYBOARD_TYPE.PHONE_PAD and replaced all string literals ('phone-pad', 'number-pad', 'decimal-pad') with their CONST equivalents in isNumericKeyboard and unit tests. Co-authored-by: Linh Vo --- src/CONST/index.ts | 1 + src/components/Form/isNumericKeyboard.ts | 6 +++--- tests/unit/isNumericKeyboardTest.ts | 10 +++++----- 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/src/CONST/index.ts b/src/CONST/index.ts index 2a170150d23da..5d35cf7263a8f 100644 --- a/src/CONST/index.ts +++ b/src/CONST/index.ts @@ -2176,6 +2176,7 @@ const CONST = { KEYBOARD_TYPE: { VISIBLE_PASSWORD: 'visible-password', ASCII_CAPABLE: 'ascii-capable', + PHONE_PAD: 'phone-pad', NUMBER_PAD: 'number-pad', DECIMAL_PAD: 'decimal-pad', NUMBERS_AND_PUNCTUATION: 'numbers-and-punctuation', diff --git a/src/components/Form/isNumericKeyboard.ts b/src/components/Form/isNumericKeyboard.ts index 4bf32df17cdcc..eaa60a7207358 100644 --- a/src/components/Form/isNumericKeyboard.ts +++ b/src/components/Form/isNumericKeyboard.ts @@ -13,9 +13,9 @@ function isNumericKeyboard(inputProps: NumericKeyboardProps): boolean { inputProps.inputMode === CONST.INPUT_MODE.TEL || inputProps.inputMode === CONST.INPUT_MODE.NUMERIC || inputProps.inputMode === CONST.INPUT_MODE.DECIMAL || - inputProps.keyboardType === 'phone-pad' || - inputProps.keyboardType === 'number-pad' || - inputProps.keyboardType === 'decimal-pad' + inputProps.keyboardType === CONST.KEYBOARD_TYPE.PHONE_PAD || + inputProps.keyboardType === CONST.KEYBOARD_TYPE.NUMBER_PAD || + inputProps.keyboardType === CONST.KEYBOARD_TYPE.DECIMAL_PAD ); } diff --git a/tests/unit/isNumericKeyboardTest.ts b/tests/unit/isNumericKeyboardTest.ts index 06d84b2988bbb..8a95fccdad048 100644 --- a/tests/unit/isNumericKeyboardTest.ts +++ b/tests/unit/isNumericKeyboardTest.ts @@ -16,15 +16,15 @@ describe('isNumericKeyboard', () => { }); it('returns true for keyboardType phone-pad', () => { - expect(isNumericKeyboard({keyboardType: 'phone-pad'})).toBe(true); + expect(isNumericKeyboard({keyboardType: CONST.KEYBOARD_TYPE.PHONE_PAD})).toBe(true); }); it('returns true for keyboardType number-pad', () => { - expect(isNumericKeyboard({keyboardType: 'number-pad'})).toBe(true); + expect(isNumericKeyboard({keyboardType: CONST.KEYBOARD_TYPE.NUMBER_PAD})).toBe(true); }); it('returns true for keyboardType decimal-pad', () => { - expect(isNumericKeyboard({keyboardType: 'decimal-pad'})).toBe(true); + expect(isNumericKeyboard({keyboardType: CONST.KEYBOARD_TYPE.DECIMAL_PAD})).toBe(true); }); it('returns false for inputMode text', () => { @@ -36,7 +36,7 @@ describe('isNumericKeyboard', () => { }); it('returns false for keyboardType ascii-capable', () => { - expect(isNumericKeyboard({keyboardType: 'ascii-capable'})).toBe(false); + expect(isNumericKeyboard({keyboardType: CONST.KEYBOARD_TYPE.ASCII_CAPABLE})).toBe(false); }); it('returns false for empty props', () => { @@ -53,6 +53,6 @@ describe('isNumericKeyboard', () => { }); it('returns true when keyboardType is phone-pad even if inputMode is text', () => { - expect(isNumericKeyboard({inputMode: CONST.INPUT_MODE.TEXT, keyboardType: 'phone-pad'})).toBe(true); + expect(isNumericKeyboard({inputMode: CONST.INPUT_MODE.TEXT, keyboardType: CONST.KEYBOARD_TYPE.PHONE_PAD})).toBe(true); }); });