Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Removed limit in number of characters that can be typed in a form #56588

Merged
merged 11 commits into from
Mar 6, 2025
17 changes: 0 additions & 17 deletions contributingGuides/FORMS.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,23 +35,6 @@ Labels and hints are enabled by passing the appropriate props to each input:
/>
```

### Character Limits

If a field has a character limit, we should give that field a max limit. This is done by passing the maxLength prop to TextInput.

```jsx
<InputWrapper
InputComponent={TextInput}
maxLength={20}
/>
```
Note: We shouldn't place a max limit on a field if the entered value can be formatted. eg: Phone number.
The phone number can be formatted in different ways.

- 2109400803
- +12109400803
- (210)-940-0803

### Native Keyboards

We should always set people up for success on native platforms by enabling the best keyboard for the type of input we’re asking them to provide. See [inputMode](https://reactnative.dev/docs/textinput#inputmode) in the React Native documentation.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import RadioListItem from '@components/SelectionList/RadioListItem';
import TextInput from '@components/TextInput';
import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
import * as ValidationUtils from '@libs/ValidationUtils';
import {getFieldRequiredErrors, isValidSecurityCode} from '@libs/ValidationUtils';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import INPUT_IDS from '@src/types/form/ChangeBillingCurrencyForm';
Expand All @@ -33,9 +33,9 @@ function PaymentCardChangeCurrencyForm({changeBillingCurrency, isSecurityCodeReq
const [currency, setCurrency] = useState<ValueOf<typeof CONST.PAYMENT_CARD_CURRENCY>>(initialCurrency ?? CONST.PAYMENT_CARD_CURRENCY.USD);

const validate = (values: FormOnyxValues<typeof ONYXKEYS.FORMS.CHANGE_BILLING_CURRENCY_FORM>): FormInputErrors<typeof ONYXKEYS.FORMS.CHANGE_BILLING_CURRENCY_FORM> => {
const errors = ValidationUtils.getFieldRequiredErrors(values, REQUIRED_FIELDS);
const errors = getFieldRequiredErrors(values, REQUIRED_FIELDS);

if (values.securityCode && !ValidationUtils.isValidSecurityCode(values.securityCode)) {
if (values.securityCode && !isValidSecurityCode(values.securityCode)) {
errors.securityCode = translate('addPaymentCardPage.error.securityCode');
}

Expand Down Expand Up @@ -102,7 +102,6 @@ function PaymentCardChangeCurrencyForm({changeBillingCurrency, isSecurityCodeReq
label={translate('addDebitCardPage.cvv')}
aria-label={translate('addDebitCardPage.cvv')}
role={CONST.ROLE.PRESENTATION}
maxLength={4}
containerStyles={[styles.mt5]}
inputMode={CONST.INPUT_MODE.NUMERIC}
/>
Expand Down
6 changes: 5 additions & 1 deletion src/components/AddPaymentCard/PaymentCardForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,11 @@ function PaymentCardForm({
// See issue: https://github.com/Expensify/App/issues/55493#issuecomment-2616349754
if (values.addressZipCode && !isValidPaymentZipCode(values.addressZipCode)) {
errors.addressZipCode = translate('addPaymentCardPage.error.addressZipCode');
} else if (values.addressZipCode.length > CONST.BANK_ACCOUNT.MAX_LENGTH.ZIP_CODE) {
errors.addressZipCode = translate('common.error.characterLimitExceedCounter', {
length: values.addressZipCode.length,
limit: CONST.BANK_ACCOUNT.MAX_LENGTH.ZIP_CODE,
});
}

if (!values.acceptTerms) {
Expand Down Expand Up @@ -290,7 +295,6 @@ function PaymentCardForm({
label={translate('common.zipPostCode')}
aria-label={translate('common.zipPostCode')}
role={CONST.ROLE.PRESENTATION}
maxLength={CONST.BANK_ACCOUNT.MAX_LENGTH.ZIP_CODE}
containerStyles={[styles.mt5]}
/>
{!!showStateSelector && (
Expand Down
28 changes: 21 additions & 7 deletions src/components/AddressForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import React, {useCallback} from 'react';
import {View} from 'react-native';
import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
import {addErrorMessage} from '@libs/ErrorUtils';
import {isRequiredFulfilled} from '@libs/ValidationUtils';
import type {Country} from '@src/CONST';
import CONST from '@src/CONST';
Expand Down Expand Up @@ -109,15 +108,34 @@ function AddressForm({
errors[fieldKey] = translate('common.error.fieldRequired');
});

if (values.addressLine2.length > CONST.FORM_CHARACTER_LIMIT) {
errors.addressLine2 = translate('common.error.characterLimitExceedCounter', {
length: values.addressLine2.length,
limit: CONST.FORM_CHARACTER_LIMIT,
});
}

if (values.city.length > CONST.FORM_CHARACTER_LIMIT) {
errors.city = translate('common.error.characterLimitExceedCounter', {
length: values.city.length,
limit: CONST.FORM_CHARACTER_LIMIT,
});
}

if (values.country !== CONST.COUNTRY.US && values.state.length > CONST.STATE_CHARACTER_LIMIT) {
errors.state = translate('common.error.characterLimitExceedCounter', {
length: values.state.length,
limit: CONST.STATE_CHARACTER_LIMIT,
});
}

// If no country is selected, default value is an empty string and there's no related regex data so we default to an empty object
const countryRegexDetails = (values.country ? CONST.COUNTRY_ZIP_REGEX_DATA?.[values.country] : {}) as CountryZipRegex;

// The postal code system might not exist for a country, so no regex either for them.
const countrySpecificZipRegex = countryRegexDetails?.regex;
const countryZipFormat = countryRegexDetails?.samples ?? '';

addErrorMessage(errors, 'firstName', translate('bankAccount.error.firstName'));

if (countrySpecificZipRegex) {
if (!countrySpecificZipRegex.test(values.zipPostCode?.trim().toUpperCase())) {
if (isRequiredFulfilled(values.zipPostCode?.trim())) {
Expand Down Expand Up @@ -173,7 +191,6 @@ function AddressForm({
aria-label={translate('common.addressLine', {lineNumber: 2})}
role={CONST.ROLE.PRESENTATION}
defaultValue={street2}
maxLength={CONST.FORM_CHARACTER_LIMIT}
spellCheck={false}
shouldSaveDraft={shouldSaveDraft}
/>
Expand Down Expand Up @@ -206,7 +223,6 @@ function AddressForm({
aria-label={translate('common.stateOrProvince')}
role={CONST.ROLE.PRESENTATION}
value={state}
maxLength={CONST.STATE_CHARACTER_LIMIT}
spellCheck={false}
onValueChange={onAddressChanged}
shouldSaveDraft={shouldSaveDraft}
Expand All @@ -220,7 +236,6 @@ function AddressForm({
aria-label={translate('common.city')}
role={CONST.ROLE.PRESENTATION}
defaultValue={city}
maxLength={CONST.FORM_CHARACTER_LIMIT}
spellCheck={false}
onValueChange={onAddressChanged}
shouldSaveDraft={shouldSaveDraft}
Expand All @@ -234,7 +249,6 @@ function AddressForm({
role={CONST.ROLE.PRESENTATION}
autoCapitalize="characters"
defaultValue={zip}
maxLength={CONST.BANK_ACCOUNT.MAX_LENGTH.ZIP_CODE}
hint={zipFormat}
onValueChange={onAddressChanged}
shouldSaveDraft={shouldSaveDraft}
Expand Down
5 changes: 2 additions & 3 deletions src/components/RoomNameInput/index.native.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import TextInput from '@components/TextInput';
import type {BaseTextInputRef} from '@components/TextInput/BaseTextInput/types';
import useLocalize from '@hooks/useLocalize';
import getOperatingSystem from '@libs/getOperatingSystem';
import * as RoomNameInputUtils from '@libs/RoomNameInputUtils';
import {modifyRoomName} from '@libs/RoomNameInputUtils';
import CONST from '@src/CONST';
import type RoomNameInputProps from './types';

Expand All @@ -20,7 +20,7 @@ function RoomNameInput(
*/
const setModifiedRoomName = (event: NativeSyntheticEvent<TextInputChangeEventData>) => {
const roomName = event.nativeEvent.text;
const modifiedRoomName = RoomNameInputUtils.modifyRoomName(roomName);
const modifiedRoomName = modifyRoomName(roomName);
onChangeText?.(modifiedRoomName);

// if custom component has onInputChange, use it to trigger changes (Form input)
Expand All @@ -43,7 +43,6 @@ function RoomNameInput(
prefixCharacter={CONST.POLICY.ROOM_PREFIX}
placeholder={translate('newRoomPage.social')}
value={value?.substring(1)} // Since the room name always starts with a prefix, we omit the first character to avoid displaying it twice.
maxLength={CONST.REPORT.MAX_ROOM_NAME_LENGTH}
onBlur={(event) => isFocused && onBlur?.(event)}
autoFocus={isFocused && autoFocus}
shouldDelayFocus={shouldDelayFocus}
Expand Down
5 changes: 2 additions & 3 deletions src/components/RoomNameInput/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import type {NativeSyntheticEvent, TextInputChangeEventData} from 'react-native'
import TextInput from '@components/TextInput';
import useLocalize from '@hooks/useLocalize';
import type {Selection} from '@libs/ComposerUtils';
import * as RoomNameInputUtils from '@libs/RoomNameInputUtils';
import {modifyRoomName} from '@libs/RoomNameInputUtils';
import type {BaseTextInputRef} from '@src/components/TextInput/BaseTextInput/types';
import CONST from '@src/CONST';
import type RoomNameInputProps from './types';
Expand All @@ -21,7 +21,7 @@ function RoomNameInput(
*/
const setModifiedRoomName = (event: NativeSyntheticEvent<TextInputChangeEventData>) => {
const roomName = event.nativeEvent.text;
const modifiedRoomName = RoomNameInputUtils.modifyRoomName(roomName);
const modifiedRoomName = modifyRoomName(roomName);
onChangeText?.(modifiedRoomName);

// if custom component has onInputChange, use it to trigger changes (Form input)
Expand Down Expand Up @@ -55,7 +55,6 @@ function RoomNameInput(
prefixCharacter={CONST.POLICY.ROOM_PREFIX}
placeholder={translate('newRoomPage.social')}
value={value?.substring(1)} // Since the room name always starts with a prefix, we omit the first character to avoid displaying it twice.
maxLength={CONST.REPORT.MAX_ROOM_NAME_LENGTH}
onBlur={(event) => isFocused && onBlur?.(event)}
autoFocus={isFocused && autoFocus}
shouldDelayFocus={shouldDelayFocus}
Expand Down
51 changes: 37 additions & 14 deletions src/components/TextPicker/TextSelectorModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import type {TextInput as TextInputType} from 'react-native';
import {Keyboard, View} from 'react-native';
import FormProvider from '@components/Form/FormProvider';
import InputWrapper from '@components/Form/InputWrapper';
import type {FormInputErrors, FormOnyxValues} from '@components/Form/types';
import HeaderWithBackButton from '@components/HeaderWithBackButton';
import Modal from '@components/Modal';
import ScreenWrapper from '@components/ScreenWrapper';
Expand All @@ -16,7 +17,17 @@ import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import type {TextSelectorModalProps} from './types';

function TextSelectorModal({value, description = '', subtitle, onValueSelected, isVisible, onClose, shouldClearOnClose, ...rest}: TextSelectorModalProps) {
function TextSelectorModal({
value,
description = '',
subtitle,
onValueSelected,
isVisible,
onClose,
shouldClearOnClose,
maxLength = CONST.CATEGORY_NAME_LIMIT,
...rest
}: TextSelectorModalProps) {
const {translate} = useLocalize();
const styles = useThemeStyles();

Expand All @@ -37,6 +48,20 @@ function TextSelectorModal({value, description = '', subtitle, onValueSelected,
}
}, [onClose, shouldClearOnClose]);

const validate = useCallback(
(values: FormOnyxValues<typeof ONYXKEYS.FORMS.TEXT_PICKER_MODAL_FORM>) => {
const errors: FormInputErrors<typeof ONYXKEYS.FORMS.TEXT_PICKER_MODAL_FORM> = {};
const formValue = values[rest.inputID];

if (formValue.length > maxLength) {
errors[rest.inputID] = translate('common.error.characterLimitExceedCounter', {length: formValue.length, limit: maxLength});
}

return errors;
},
[maxLength, rest.inputID, translate],
);

// In TextPicker, when the modal is hidden, it is not completely unmounted, so when it is shown again, the currentValue is not updated with the value prop.
// Therefore, we need to update the currentValue with the value prop when the modal is shown. This is done once when the modal is shown again.
useEffect(() => {
Expand Down Expand Up @@ -90,27 +115,25 @@ function TextSelectorModal({value, description = '', subtitle, onValueSelected,
/>
<FormProvider
formID={ONYXKEYS.FORMS.TEXT_PICKER_MODAL_FORM}
validate={validate}
onSubmit={(data) => {
Keyboard.dismiss();
onValueSelected?.(data[rest.inputID ?? ''] ?? '');
onValueSelected?.(data[rest.inputID] ?? '');
}}
submitButtonText={translate('common.save')}
style={[styles.mh5, styles.flex1]}
enabledWhenOffline
>
<View style={styles.pb4}>{!!subtitle && <Text style={[styles.sidebarLinkText, styles.optionAlternateText]}>{subtitle}</Text>}</View>
{!!rest.inputID && (
<InputWrapper
ref={inputCallbackRef}
InputComponent={TextInput}
maxLength={CONST.CATEGORY_NAME_LIMIT}
value={currentValue}
onValueChange={(changedValue) => setValue(changedValue.toString())}
// eslint-disable-next-line react/jsx-props-no-spreading
{...rest}
inputID={rest.inputID}
/>
)}
<InputWrapper
ref={inputCallbackRef}
InputComponent={TextInput}
value={currentValue}
onValueChange={(changedValue) => setValue(changedValue.toString())}
// eslint-disable-next-line react/jsx-props-no-spreading
{...rest}
inputID={rest.inputID}
/>
Comment on lines -102 to +136
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are you confident about this change?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lets keep an eye out on this one

</FormProvider>
</ScreenWrapper>
</Modal>
Expand Down
6 changes: 6 additions & 0 deletions src/components/TextPicker/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ type TextSelectorModalProps = {

/** Whether to clear the input value when the modal closes */
shouldClearOnClose?: boolean;

/** The ID used to uniquely identify the input in a Form */
inputID: string;
} & Pick<MenuItemBaseProps, 'subtitle' | 'description'> &
TextProps;

Expand All @@ -42,6 +45,9 @@ type TextPickerProps = {

/** Whether to show the tooltip text */
shouldShowTooltips?: boolean;

/** The ID used to uniquely identify the input in a Form */
inputID: string;
} & Pick<MenuItemBaseProps, 'rightLabel' | 'subtitle' | 'description' | 'interactive'> &
TextProps;

Expand Down
14 changes: 12 additions & 2 deletions src/libs/actions/TaxRate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,12 @@ const validateTaxName = (policy: Policy, values: FormOnyxValues<typeof ONYXKEYS.
const errors = ValidationUtils.getFieldRequiredErrors(values, [INPUT_IDS.NAME]);

const name = values[INPUT_IDS.NAME];
if (policy?.taxRates?.taxes && ValidationUtils.isExistingTaxName(name, policy.taxRates.taxes)) {
if (name.length > CONST.TAX_RATES.NAME_MAX_LENGTH) {
errors[INPUT_IDS.NAME] = translateLocal('common.error.characterLimitExceedCounter', {
length: name.length,
limit: CONST.TAX_RATES.NAME_MAX_LENGTH,
});
} else if (policy?.taxRates?.taxes && ValidationUtils.isExistingTaxName(name, policy.taxRates.taxes)) {
errors[INPUT_IDS.NAME] = translateLocal('workspace.taxes.error.taxRateAlreadyExists');
}

Expand All @@ -61,7 +66,12 @@ const validateTaxCode = (policy: Policy, values: FormOnyxValues<typeof ONYXKEYS.
const errors = ValidationUtils.getFieldRequiredErrors(values, [INPUT_IDS_TAX_CODE.TAX_CODE]);

const taxCode = values[INPUT_IDS_TAX_CODE.TAX_CODE];
if (policy?.taxRates?.taxes && ValidationUtils.isExistingTaxCode(taxCode, policy.taxRates.taxes)) {
if (taxCode.length > CONST.TAX_RATES.NAME_MAX_LENGTH) {
errors[INPUT_IDS_TAX_CODE.TAX_CODE] = translateLocal('common.error.characterLimitExceedCounter', {
length: taxCode.length,
limit: CONST.TAX_RATES.NAME_MAX_LENGTH,
});
} else if (policy?.taxRates?.taxes && ValidationUtils.isExistingTaxCode(taxCode, policy.taxRates.taxes)) {
errors[INPUT_IDS_TAX_CODE.TAX_CODE] = translateLocal('workspace.taxes.error.taxCodeAlreadyExists');
}

Expand Down
Loading
Loading