diff --git a/src/CONST.ts b/src/CONST.ts index 072f780b54ae..d3f24b3fbacd 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -2902,6 +2902,10 @@ const CONST = { NAVIGATE: 'NAVIGATE', }, }, + TIME_PERIOD: { + AM: 'AM', + PM: 'PM', + }, INDENTS: ' ', PARENT_CHILD_SEPARATOR: ': ', CATEGORY_LIST_THRESHOLD: 8, @@ -2911,7 +2915,7 @@ const CONST = { SBE: 'SbeDemoSetup', MONEY2020: 'Money2020DemoSetup', }, - + COLON: ':', MAPBOX: { PADDING: 50, DEFAULT_ZOOM: 10, diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index 933ae678da23..0cc7934ad007 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -334,10 +334,10 @@ const ONYXKEYS = { WAYPOINT_FORM_DRAFT: 'waypointFormDraft', SETTINGS_STATUS_SET_FORM: 'settingsStatusSetForm', SETTINGS_STATUS_SET_FORM_DRAFT: 'settingsStatusSetFormDraft', - SETTINGS_STATUS_CLEAR_AFTER_FORM: 'settingsStatusClearAfterForm', - SETTINGS_STATUS_CLEAR_AFTER_FORM_DRAFT: 'settingsStatusClearAfterFormDraft', SETTINGS_STATUS_SET_CLEAR_AFTER_FORM: 'settingsStatusSetClearAfterForm', SETTINGS_STATUS_SET_CLEAR_AFTER_FORM_DRAFT: 'settingsStatusSetClearAfterFormDraft', + SETTINGS_STATUS_CLEAR_DATE_FORM: 'settingsStatusClearDateForm', + SETTINGS_STATUS_CLEAR_DATE_FORM_DRAFT: 'settingsStatusClearDateFormDraft', PRIVATE_NOTES_FORM: 'privateNotesForm', PRIVATE_NOTES_FORM_DRAFT: 'privateNotesFormDraft', I_KNOW_A_TEACHER_FORM: 'iKnowTeacherForm', @@ -508,8 +508,8 @@ type OnyxValues = { [ONYXKEYS.FORMS.WAYPOINT_FORM_DRAFT]: OnyxTypes.Form; [ONYXKEYS.FORMS.SETTINGS_STATUS_SET_FORM]: OnyxTypes.Form; [ONYXKEYS.FORMS.SETTINGS_STATUS_SET_FORM_DRAFT]: OnyxTypes.Form; - [ONYXKEYS.FORMS.SETTINGS_STATUS_CLEAR_AFTER_FORM]: OnyxTypes.Form; - [ONYXKEYS.FORMS.SETTINGS_STATUS_CLEAR_AFTER_FORM_DRAFT]: OnyxTypes.Form; + [ONYXKEYS.FORMS.SETTINGS_STATUS_CLEAR_DATE_FORM]: OnyxTypes.Form; + [ONYXKEYS.FORMS.SETTINGS_STATUS_CLEAR_DATE_FORM_DRAFT]: OnyxTypes.Form; [ONYXKEYS.FORMS.SETTINGS_STATUS_SET_CLEAR_AFTER_FORM]: OnyxTypes.Form; [ONYXKEYS.FORMS.SETTINGS_STATUS_SET_CLEAR_AFTER_FORM_DRAFT]: OnyxTypes.Form; [ONYXKEYS.FORMS.PRIVATE_NOTES_FORM]: OnyxTypes.Form; diff --git a/src/ROUTES.ts b/src/ROUTES.ts index 425ff73af56b..ca1fe9f0e81a 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -140,7 +140,9 @@ const ROUTES = { getRoute: (backTo?: string) => getUrlWithBackToParam('settings/security/two-factor-auth', backTo), }, SETTINGS_STATUS: 'settings/profile/status', - SETTINGS_STATUS_SET: 'settings/profile/status/set', + SETTINGS_STATUS_CLEAR_AFTER: 'settings/profile/status/clear-after', + SETTINGS_STATUS_CLEAR_AFTER_DATE: 'settings/profile/status/clear-after/date', + SETTINGS_STATUS_CLEAR_AFTER_TIME: 'settings/profile/status/clear-after/time', KEYBOARD_SHORTCUTS: 'keyboard-shortcuts', diff --git a/src/SCREENS.ts b/src/SCREENS.ts index 921f57953482..2cd263237866 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -37,8 +37,10 @@ const SCREENS = { CONTACT_METHODS: 'Settings_ContactMethods', CONTACT_METHOD_DETAILS: 'Settings_ContactMethodDetails', NEW_CONTACT_METHOD: 'Settings_NewContactMethod', + STATUS_CLEAR_AFTER: 'Settings_Status_Clear_After', + STATUS_CLEAR_AFTER_DATE: 'Settings_Status_Clear_After_Date', + STATUS_CLEAR_AFTER_TIME: 'Settings_Status_Clear_After_Time', STATUS: 'Settings_Status', - STATUS_SET: 'Settings_Status_Set', PRONOUNS: 'Settings_Pronouns', TIMEZONE: 'Settings_Timezone', TIMEZONE_SELECT: 'Settings_Timezone_Select', diff --git a/src/components/AmountTextInput.js b/src/components/AmountTextInput.js index 5efcc003d853..63e59d7eef4a 100644 --- a/src/components/AmountTextInput.js +++ b/src/components/AmountTextInput.js @@ -1,5 +1,6 @@ import PropTypes from 'prop-types'; import React from 'react'; +import useStyleUtils from '@styles/useStyleUtils'; import useThemeStyles from '@styles/useThemeStyles'; import CONST from '@src/CONST'; import refPropTypes from './refPropTypes'; @@ -27,6 +28,12 @@ const propTypes = { /** Function to call when selection in text input is changed */ onSelectionChange: PropTypes.func, + /** Style for the input */ + style: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.object), PropTypes.object]), + + /** Style for the container */ + containerStyles: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.object), PropTypes.object]), + /** Function to call to handle key presses in the text input */ onKeyPress: PropTypes.func, }; @@ -36,16 +43,19 @@ const defaultProps = { selection: undefined, onSelectionChange: () => {}, onKeyPress: () => {}, + style: {}, + containerStyles: {}, }; function AmountTextInput(props) { const styles = useThemeStyles(); + const StyleUtils = useStyleUtils(); return ( ); } diff --git a/src/components/BigNumberPad.tsx b/src/components/BigNumberPad.tsx index 812e9e78635b..3204b39cb19c 100644 --- a/src/components/BigNumberPad.tsx +++ b/src/components/BigNumberPad.tsx @@ -15,6 +15,9 @@ type BigNumberPadProps = { /** Used to locate this view from native classes. */ id?: string; + + /** Whether long press is disabled */ + isLongPressDisabled: boolean; }; const padNumbers = [ @@ -24,7 +27,7 @@ const padNumbers = [ ['.', '0', '<'], ] as const; -function BigNumberPad({numberPressed, longPressHandlerStateChanged = () => {}, id = 'numPadView'}: BigNumberPadProps) { +function BigNumberPad({numberPressed, longPressHandlerStateChanged = () => {}, id = 'numPadView', isLongPressDisabled = false}: BigNumberPadProps) { const {toLocaleDigit} = useLocalize(); const styles = useThemeStyles(); @@ -85,6 +88,7 @@ function BigNumberPad({numberPressed, longPressHandlerStateChanged = () => {}, i onMouseDown={(e) => { e.preventDefault(); }} + isLongPressDisabled={isLongPressDisabled} /> ); })} diff --git a/src/components/Button/index.tsx b/src/components/Button/index.tsx index eb99d4b09396..06577c3ac813 100644 --- a/src/components/Button/index.tsx +++ b/src/components/Button/index.tsx @@ -106,6 +106,9 @@ type ButtonProps = (ButtonWithText | ChildrenProps) & { /** Should enable the haptic feedback? */ shouldEnableHapticFeedback?: boolean; + /** Should disable the long press? */ + isLongPressDisabled?: boolean; + /** Id to use for this button */ id?: string; @@ -149,6 +152,7 @@ function Button( shouldRemoveRightBorderRadius = false, shouldRemoveLeftBorderRadius = false, shouldEnableHapticFeedback = false, + isLongPressDisabled = false, id = '', accessibilityLabel = '', @@ -255,6 +259,9 @@ function Button( return onPress(event); }} onLongPress={(event) => { + if (isLongPressDisabled) { + return; + } if (shouldEnableHapticFeedback) { HapticFeedback.longPress(); } diff --git a/src/components/EmojiPicker/EmojiPickerButtonDropdown.js b/src/components/EmojiPicker/EmojiPickerButtonDropdown.js index 8135fa38e992..5dc43ef47699 100644 --- a/src/components/EmojiPicker/EmojiPickerButtonDropdown.js +++ b/src/components/EmojiPicker/EmojiPickerButtonDropdown.js @@ -5,7 +5,7 @@ import Icon from '@components/Icon'; import * as Expensicons from '@components/Icon/Expensicons'; import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeedback'; import Text from '@components/Text'; -import Tooltip from '@components/Tooltip'; +import Tooltip from '@components/Tooltip/PopoverAnchorTooltip'; import withLocalize, {withLocalizePropTypes} from '@components/withLocalize'; import getButtonState from '@libs/getButtonState'; import useStyleUtils from '@styles/useStyleUtils'; @@ -47,7 +47,7 @@ function EmojiPickerButtonDropdown(props) { ({}), + submitButtonText: '', }; -function Form(props) { +const Form = forwardRef((props, forwardedRef) => { const styles = useThemeStyles(); const [errors, setErrors] = useState({}); const [inputValues, setInputValues] = useState(() => ({...props.draftValues})); @@ -245,6 +250,30 @@ function Form(props) { onSubmit(trimmedStringValues); }, [props.formState.isLoading, props.network.isOffline, props.enabledWhenOffline, inputValues, onValidate, onSubmit]); + /** + * Resets the form + */ + const resetForm = useCallback( + (optionalValue) => { + _.each(inputValues, (inputRef, inputID) => { + setInputValues((prevState) => { + const copyPrevState = _.clone(prevState); + + touchedInputs.current[inputID] = false; + copyPrevState[inputID] = optionalValue[inputID] || ''; + + return copyPrevState; + }); + }); + setErrors({}); + }, + [inputValues], + ); + + useImperativeHandle(forwardedRef, () => ({ + resetForm, + })); + /** * Loops over Form's children and automatically supplies Form props to them * @@ -464,7 +493,9 @@ function Form(props) { containerStyles={[styles.mh0, styles.mt5, styles.flex1, ...props.submitButtonStyles]} enabledWhenOffline={props.enabledWhenOffline} isSubmitActionDangerous={props.isSubmitActionDangerous} + useSmallerSubmitButtonSize={props.useSmallerSubmitButtonSize} disablePressOnEnter + errorMessageStyle={props.errorMessageStyle} /> )} @@ -474,6 +505,8 @@ function Form(props) { props.style, props.isSubmitButtonVisible, props.submitButtonText, + props.useSmallerSubmitButtonSize, + props.errorMessageStyle, props.formState.errorFields, props.formState.isLoading, props.footerContent, @@ -539,7 +572,7 @@ function Form(props) { } ); -} +}); Form.displayName = 'Form'; Form.propTypes = propTypes; diff --git a/src/components/Form/FormProvider.js b/src/components/Form/FormProvider.js index 63953d8303db..0d6dcb001091 100644 --- a/src/components/Form/FormProvider.js +++ b/src/components/Form/FormProvider.js @@ -1,6 +1,6 @@ import lodashGet from 'lodash/get'; import PropTypes from 'prop-types'; -import React, {createRef, useCallback, useMemo, useRef, useState} from 'react'; +import React, {createRef, forwardRef, useCallback, useImperativeHandle, useMemo, useRef, useState} from 'react'; import {withOnyx} from 'react-native-onyx'; import _ from 'underscore'; import networkPropTypes from '@components/networkPropTypes'; @@ -109,250 +109,278 @@ function getInitialValueByType(valueType) { } } -function FormProvider({validate, formID, shouldValidateOnBlur, shouldValidateOnChange, children, formState, network, enabledWhenOffline, draftValues, onSubmit, ...rest}) { - const inputRefs = useRef({}); - const touchedInputs = useRef({}); - const [inputValues, setInputValues] = useState(() => ({...draftValues})); - const [errors, setErrors] = useState({}); - const hasServerError = useMemo(() => Boolean(formState) && !_.isEmpty(formState.errors), [formState]); +const FormProvider = forwardRef( + ({validate, formID, shouldValidateOnBlur, shouldValidateOnChange, children, formState, network, enabledWhenOffline, draftValues, onSubmit, ...rest}, forwardedRef) => { + const inputRefs = useRef({}); + const touchedInputs = useRef({}); + const [inputValues, setInputValues] = useState(() => ({...draftValues})); + const [errors, setErrors] = useState({}); + const hasServerError = useMemo(() => Boolean(formState) && !_.isEmpty(formState.errors), [formState]); - const onValidate = useCallback( - (values, shouldClearServerError = true) => { - const trimmedStringValues = ValidationUtils.prepareValues(values); + const onValidate = useCallback( + (values, shouldClearServerError = true) => { + const trimmedStringValues = ValidationUtils.prepareValues(values); - if (shouldClearServerError) { - FormActions.setErrors(formID, null); - } - FormActions.setErrorFields(formID, null); + if (shouldClearServerError) { + FormActions.setErrors(formID, null); + } + FormActions.setErrorFields(formID, null); - const validateErrors = validate(values) || {}; + const validateErrors = validate(values) || {}; - // Validate the input for html tags. It should supercede any other error - _.each(trimmedStringValues, (inputValue, inputID) => { - // If the input value is empty OR is non-string, we don't need to validate it for HTML tags - if (!inputValue || !_.isString(inputValue)) { - return; - } - const foundHtmlTagIndex = inputValue.search(CONST.VALIDATE_FOR_HTML_TAG_REGEX); - const leadingSpaceIndex = inputValue.search(CONST.VALIDATE_FOR_LEADINGSPACES_HTML_TAG_REGEX); + // Validate the input for html tags. It should supercede any other error + _.each(trimmedStringValues, (inputValue, inputID) => { + // If the input value is empty OR is non-string, we don't need to validate it for HTML tags + if (!inputValue || !_.isString(inputValue)) { + return; + } + const foundHtmlTagIndex = inputValue.search(CONST.VALIDATE_FOR_HTML_TAG_REGEX); + const leadingSpaceIndex = inputValue.search(CONST.VALIDATE_FOR_LEADINGSPACES_HTML_TAG_REGEX); - // Return early if there are no HTML characters - if (leadingSpaceIndex === -1 && foundHtmlTagIndex === -1) { - return; - } + // Return early if there are no HTML characters + if (leadingSpaceIndex === -1 && foundHtmlTagIndex === -1) { + return; + } - const matchedHtmlTags = inputValue.match(CONST.VALIDATE_FOR_HTML_TAG_REGEX); - let isMatch = _.some(CONST.WHITELISTED_TAGS, (r) => r.test(inputValue)); - // Check for any matches that the original regex (foundHtmlTagIndex) matched - if (matchedHtmlTags) { - // Check if any matched inputs does not match in WHITELISTED_TAGS list and return early if needed. - for (let i = 0; i < matchedHtmlTags.length; i++) { - const htmlTag = matchedHtmlTags[i]; - isMatch = _.some(CONST.WHITELISTED_TAGS, (r) => r.test(htmlTag)); - if (!isMatch) { - break; + const matchedHtmlTags = inputValue.match(CONST.VALIDATE_FOR_HTML_TAG_REGEX); + let isMatch = _.some(CONST.WHITELISTED_TAGS, (r) => r.test(inputValue)); + // Check for any matches that the original regex (foundHtmlTagIndex) matched + if (matchedHtmlTags) { + // Check if any matched inputs does not match in WHITELISTED_TAGS list and return early if needed. + for (let i = 0; i < matchedHtmlTags.length; i++) { + const htmlTag = matchedHtmlTags[i]; + isMatch = _.some(CONST.WHITELISTED_TAGS, (r) => r.test(htmlTag)); + if (!isMatch) { + break; + } } } + // Add a validation error here because it is a string value that contains HTML characters + validateErrors[inputID] = 'common.error.invalidCharacter'; + }); + + if (!_.isObject(validateErrors)) { + throw new Error('Validate callback must return an empty object or an object with shape {inputID: error}'); } - // Add a validation error here because it is a string value that contains HTML characters - validateErrors[inputID] = 'common.error.invalidCharacter'; - }); - if (!_.isObject(validateErrors)) { - throw new Error('Validate callback must return an empty object or an object with shape {inputID: error}'); - } + const touchedInputErrors = _.pick(validateErrors, (inputValue, inputID) => Boolean(touchedInputs.current[inputID])); - const touchedInputErrors = _.pick(validateErrors, (inputValue, inputID) => Boolean(touchedInputs.current[inputID])); + if (!_.isEqual(errors, touchedInputErrors)) { + setErrors(touchedInputErrors); + } - if (!_.isEqual(errors, touchedInputErrors)) { - setErrors(touchedInputErrors); + return touchedInputErrors; + }, + [errors, formID, validate], + ); + + /** + * @param {String} inputID - The inputID of the input being touched + */ + const setTouchedInput = useCallback( + (inputID) => { + touchedInputs.current[inputID] = true; + }, + [touchedInputs], + ); + + const submit = useCallback(() => { + // Return early if the form is already submitting to avoid duplicate submission + if (formState.isLoading) { + return; } - return touchedInputErrors; - }, - [errors, formID, validate], - ); - - /** - * @param {String} inputID - The inputID of the input being touched - */ - const setTouchedInput = useCallback( - (inputID) => { - touchedInputs.current[inputID] = true; - }, - [touchedInputs], - ); - - const submit = useCallback(() => { - // Return early if the form is already submitting to avoid duplicate submission - if (formState.isLoading) { - return; - } - - // Prepare values before submitting - const trimmedStringValues = ValidationUtils.prepareValues(inputValues); - - // Touches all form inputs so we can validate the entire form - _.each(inputRefs.current, (inputRef, inputID) => (touchedInputs.current[inputID] = true)); - - // Validate form and return early if any errors are found - if (!_.isEmpty(onValidate(trimmedStringValues))) { - return; - } - - // Do not submit form if network is offline and the form is not enabled when offline - if (network.isOffline && !enabledWhenOffline) { - return; - } - - onSubmit(trimmedStringValues); - }, [enabledWhenOffline, formState.isLoading, inputValues, network.isOffline, onSubmit, onValidate]); - - const registerInput = useCallback( - (inputID, propsToParse = {}) => { - const newRef = inputRefs.current[inputID] || propsToParse.ref || createRef(); - if (inputRefs.current[inputID] !== newRef) { - inputRefs.current[inputID] = newRef; + // Prepare values before submitting + const trimmedStringValues = ValidationUtils.prepareValues(inputValues); + + // Touches all form inputs so we can validate the entire form + _.each(inputRefs.current, (inputRef, inputID) => (touchedInputs.current[inputID] = true)); + + // Validate form and return early if any errors are found + if (!_.isEmpty(onValidate(trimmedStringValues))) { + return; } - if (!_.isUndefined(propsToParse.value)) { - inputValues[inputID] = propsToParse.value; - } else if (propsToParse.shouldSaveDraft && !_.isUndefined(draftValues[inputID]) && _.isUndefined(inputValues[inputID])) { - inputValues[inputID] = draftValues[inputID]; - } else if (propsToParse.shouldUseDefaultValue && _.isUndefined(inputValues[inputID])) { - // We force the form to set the input value from the defaultValue props if there is a saved valid value - inputValues[inputID] = propsToParse.defaultValue; - } else if (_.isUndefined(inputValues[inputID])) { - // We want to initialize the input value if it's undefined - inputValues[inputID] = _.isUndefined(propsToParse.defaultValue) ? getInitialValueByType(propsToParse.valueType) : propsToParse.defaultValue; + // Do not submit form if network is offline and the form is not enabled when offline + if (network.isOffline && !enabledWhenOffline) { + return; } - const errorFields = lodashGet(formState, 'errorFields', {}); - const fieldErrorMessage = - _.chain(errorFields[inputID]) - .keys() - .sortBy() - .reverse() - .map((key) => errorFields[inputID][key]) - .first() - .value() || ''; - - return { - ...propsToParse, - ref: - typeof propsToParse.ref === 'function' - ? (node) => { - propsToParse.ref(node); - newRef.current = node; - } - : newRef, - inputID, - key: propsToParse.key || inputID, - errorText: errors[inputID] || fieldErrorMessage, - value: inputValues[inputID], - // As the text input is controlled, we never set the defaultValue prop - // as this is already happening by the value prop. - defaultValue: undefined, - onTouched: (event) => { - if (!propsToParse.shouldSetTouchedOnBlurOnly) { - setTimeout(() => { - setTouchedInput(inputID); - }, VALIDATE_DELAY); - } - if (_.isFunction(propsToParse.onTouched)) { - propsToParse.onTouched(event); - } - }, - onPress: (event) => { - if (!propsToParse.shouldSetTouchedOnBlurOnly) { - setTimeout(() => { - setTouchedInput(inputID); - }, VALIDATE_DELAY); - } - if (_.isFunction(propsToParse.onPress)) { - propsToParse.onPress(event); - } - }, - onPressOut: (event) => { - // To prevent validating just pressed inputs, we need to set the touched input right after - // onValidate and to do so, we need to delays setTouchedInput of the same amount of time - // as the onValidate is delayed - if (!propsToParse.shouldSetTouchedOnBlurOnly) { - setTimeout(() => { - setTouchedInput(inputID); - }, VALIDATE_DELAY); - } - if (_.isFunction(propsToParse.onPressIn)) { - propsToParse.onPressIn(event); - } - }, - onBlur: (event) => { - // Only run validation when user proactively blurs the input. - if (Visibility.isVisible() && Visibility.hasFocus()) { - const relatedTargetId = lodashGet(event, 'nativeEvent.relatedTarget.id'); - // We delay the validation in order to prevent Checkbox loss of focus when - // the user is focusing a TextInput and proceeds to toggle a CheckBox in - // web and mobile web platforms. - - setTimeout(() => { - if (relatedTargetId && _.includes([CONST.OVERLAY.BOTTOM_BUTTON_NATIVE_ID, CONST.OVERLAY.TOP_BUTTON_NATIVE_ID, CONST.BACK_BUTTON_NATIVE_ID], relatedTargetId)) { - return; - } - setTouchedInput(inputID); - if (shouldValidateOnBlur) { - onValidate(inputValues, !hasServerError); - } - }, VALIDATE_DELAY); - } + onSubmit(trimmedStringValues); + }, [enabledWhenOffline, formState.isLoading, inputValues, network.isOffline, onSubmit, onValidate]); - if (_.isFunction(propsToParse.onBlur)) { - propsToParse.onBlur(event); - } - }, - onInputChange: (value, key) => { - const inputKey = key || inputID; + /** + * Resets the form + */ + const resetForm = useCallback( + (optionalValue) => { + _.each(inputValues, (inputRef, inputID) => { setInputValues((prevState) => { - const newState = { - ...prevState, - [inputKey]: value, - }; + const copyPrevState = _.clone(prevState); - if (shouldValidateOnChange) { - onValidate(newState); - } - return newState; + touchedInputs.current[inputID] = false; + copyPrevState[inputID] = optionalValue[inputID] || ''; + + return copyPrevState; }); + }); + setErrors({}); + }, + [inputValues], + ); + useImperativeHandle(forwardedRef, () => ({ + resetForm, + })); + + const registerInput = useCallback( + (inputID, propsToParse = {}) => { + const newRef = inputRefs.current[inputID] || propsToParse.ref || createRef(); + if (inputRefs.current[inputID] !== newRef) { + inputRefs.current[inputID] = newRef; + } - if (propsToParse.shouldSaveDraft) { - FormActions.setDraftValues(formID, {[inputKey]: value}); - } + if (!_.isUndefined(propsToParse.value)) { + inputValues[inputID] = propsToParse.value; + } else if (propsToParse.shouldSaveDraft && !_.isUndefined(draftValues[inputID]) && _.isUndefined(inputValues[inputID])) { + inputValues[inputID] = draftValues[inputID]; + } else if (propsToParse.shouldUseDefaultValue && _.isUndefined(inputValues[inputID])) { + // We force the form to set the input value from the defaultValue props if there is a saved valid value + inputValues[inputID] = propsToParse.defaultValue; + } else if (_.isUndefined(inputValues[inputID])) { + // We want to initialize the input value if it's undefined + inputValues[inputID] = _.isUndefined(propsToParse.defaultValue) ? getInitialValueByType(propsToParse.valueType) : propsToParse.defaultValue; + } - if (_.isFunction(propsToParse.onValueChange)) { - propsToParse.onValueChange(value, inputKey); - } - }, - }; - }, - [draftValues, formID, errors, formState, hasServerError, inputValues, onValidate, setTouchedInput, shouldValidateOnBlur, shouldValidateOnChange], - ); - const value = useMemo(() => ({registerInput}), [registerInput]); - - return ( - - {/* eslint-disable react/jsx-props-no-spreading */} - - {_.isFunction(children) ? children({inputValues}) : children} - - - ); -} + const errorFields = lodashGet(formState, 'errorFields', {}); + const fieldErrorMessage = + _.chain(errorFields[inputID]) + .keys() + .sortBy() + .reverse() + .map((key) => errorFields[inputID][key]) + .first() + .value() || ''; + + return { + ...propsToParse, + ref: + typeof propsToParse.ref === 'function' + ? (node) => { + propsToParse.ref(node); + newRef.current = node; + } + : newRef, + inputID, + key: propsToParse.key || inputID, + errorText: errors[inputID] || fieldErrorMessage, + value: inputValues[inputID], + // As the text input is controlled, we never set the defaultValue prop + // as this is already happening by the value prop. + defaultValue: undefined, + onTouched: (event) => { + if (!propsToParse.shouldSetTouchedOnBlurOnly) { + setTimeout(() => { + setTouchedInput(inputID); + }, VALIDATE_DELAY); + } + if (_.isFunction(propsToParse.onTouched)) { + propsToParse.onTouched(event); + } + }, + onPress: (event) => { + if (!propsToParse.shouldSetTouchedOnBlurOnly) { + setTimeout(() => { + setTouchedInput(inputID); + }, VALIDATE_DELAY); + } + if (_.isFunction(propsToParse.onPress)) { + propsToParse.onPress(event); + } + }, + onPressOut: (event) => { + // To prevent validating just pressed inputs, we need to set the touched input right after + // onValidate and to do so, we need to delays setTouchedInput of the same amount of time + // as the onValidate is delayed + if (!propsToParse.shouldSetTouchedOnBlurOnly) { + setTimeout(() => { + setTouchedInput(inputID); + }, VALIDATE_DELAY); + } + if (_.isFunction(propsToParse.onPressIn)) { + propsToParse.onPressIn(event); + } + }, + onBlur: (event) => { + // Only run validation when user proactively blurs the input. + if (Visibility.isVisible() && Visibility.hasFocus()) { + const relatedTargetId = lodashGet(event, 'nativeEvent.relatedTarget.id'); + // We delay the validation in order to prevent Checkbox loss of focus when + // the user is focusing a TextInput and proceeds to toggle a CheckBox in + // web and mobile web platforms. + + setTimeout(() => { + if ( + relatedTargetId && + _.includes([CONST.OVERLAY.BOTTOM_BUTTON_NATIVE_ID, CONST.OVERLAY.TOP_BUTTON_NATIVE_ID, CONST.BACK_BUTTON_NATIVE_ID], relatedTargetId) + ) { + return; + } + setTouchedInput(inputID); + if (shouldValidateOnBlur) { + onValidate(inputValues, !hasServerError); + } + }, VALIDATE_DELAY); + } + + if (_.isFunction(propsToParse.onBlur)) { + propsToParse.onBlur(event); + } + }, + onInputChange: (value, key) => { + const inputKey = key || inputID; + setInputValues((prevState) => { + const newState = { + ...prevState, + [inputKey]: value, + }; + + if (shouldValidateOnChange) { + onValidate(newState); + } + return newState; + }); + + if (propsToParse.shouldSaveDraft) { + FormActions.setDraftValues(formID, {[inputKey]: value}); + } + + if (_.isFunction(propsToParse.onValueChange)) { + propsToParse.onValueChange(value, inputKey); + } + }, + }; + }, + [draftValues, formID, errors, formState, hasServerError, inputValues, onValidate, setTouchedInput, shouldValidateOnBlur, shouldValidateOnChange], + ); + const value = useMemo(() => ({registerInput}), [registerInput]); + + return ( + + {/* eslint-disable react/jsx-props-no-spreading */} + + {_.isFunction(children) ? children({inputValues}) : children} + + + ); + }, +); FormProvider.displayName = 'Form'; FormProvider.propTypes = propTypes; diff --git a/src/components/FormAlertWithSubmitButton.js b/src/components/FormAlertWithSubmitButton.js index b16a4d2a08ee..6886b6fdcaaf 100644 --- a/src/components/FormAlertWithSubmitButton.js +++ b/src/components/FormAlertWithSubmitButton.js @@ -50,6 +50,12 @@ const propTypes = { /** Styles for the button */ // eslint-disable-next-line react/forbid-prop-types buttonStyles: PropTypes.arrayOf(PropTypes.object), + + /** Whether to use a smaller submit button size */ + useSmallerSubmitButtonSize: PropTypes.bool, + + /** Style for the error message for submit button */ + errorMessageStyle: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.object), PropTypes.object]), }; const defaultProps = { @@ -62,8 +68,10 @@ const defaultProps = { enabledWhenOffline: false, disablePressOnEnter: false, isSubmitActionDangerous: false, + useSmallerSubmitButtonSize: false, footerContent: null, buttonStyles: [], + errorMessageStyle: [], }; function FormAlertWithSubmitButton(props) { @@ -77,6 +85,7 @@ function FormAlertWithSubmitButton(props) { isMessageHtml={props.isMessageHtml} message={props.message} onFixTheErrorsLinkPressed={props.onFixTheErrorsLinkPressed} + errorMessageStyle={props.errorMessageStyle} > {(isOffline) => ( @@ -87,6 +96,7 @@ function FormAlertWithSubmitButton(props) { text={props.buttonText} style={buttonStyles} danger={props.isSubmitActionDangerous} + medium={props.useSmallerSubmitButtonSize} /> ) : (