diff --git a/src/components/Form/FormProvider.js b/src/components/Form/FormProvider.js index 6908faaf3a01..af2511fc9f74 100644 --- a/src/components/Form/FormProvider.js +++ b/src/components/Form/FormProvider.js @@ -237,8 +237,6 @@ function FormProvider({validate, formID, shouldValidateOnBlur, shouldValidateOnC .first() .value() || ''; - const value = !_.isUndefined(inputValues[`${inputID}ToDisplay`]) ? inputValues[`${inputID}ToDisplay`] : inputValues[inputID]; - return { ...propsToParse, ref: @@ -251,7 +249,7 @@ function FormProvider({validate, formID, shouldValidateOnBlur, shouldValidateOnC inputID, key: propsToParse.key || inputID, errorText: errors[inputID] || fieldErrorMessage, - value, + 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, @@ -311,19 +309,13 @@ function FormProvider({validate, formID, shouldValidateOnBlur, shouldValidateOnC propsToParse.onBlur(event); } }, - onInputChange: (inputValue, key) => { + onInputChange: (value, key) => { const inputKey = key || inputID; setInputValues((prevState) => { - const newState = _.isFunction(propsToParse.valueParser) - ? { - ...prevState, - [inputKey]: propsToParse.valueParser(inputValue), - [`${inputKey}ToDisplay`]: _.isFunction(propsToParse.displayParser) ? propsToParse.displayParser(inputValue) : inputValue, - } - : { - ...prevState, - [inputKey]: inputValue, - }; + const newState = { + ...prevState, + [inputKey]: value, + }; if (shouldValidateOnChange) { onValidate(newState); @@ -336,7 +328,7 @@ function FormProvider({validate, formID, shouldValidateOnBlur, shouldValidateOnC } if (_.isFunction(propsToParse.onValueChange)) { - propsToParse.onValueChange(inputValue, inputKey); + propsToParse.onValueChange(value, inputKey); } }, }; diff --git a/src/components/Form/InputWrapper.js b/src/components/Form/InputWrapper.js index 11e74d2759b1..9a31210195c4 100644 --- a/src/components/Form/InputWrapper.js +++ b/src/components/Form/InputWrapper.js @@ -9,15 +9,11 @@ const propTypes = { inputID: PropTypes.string.isRequired, valueType: PropTypes.string, forwardedRef: refPropTypes, - valueParser: PropTypes.func, - displayParser: PropTypes.func, }; const defaultProps = { forwardedRef: undefined, valueType: 'string', - valueParser: undefined, - displayParser: undefined, }; function InputWrapper(props) { diff --git a/src/components/RoomNameInput/index.js b/src/components/RoomNameInput/index.js index e32c264a57cc..14529d7b594a 100644 --- a/src/components/RoomNameInput/index.js +++ b/src/components/RoomNameInput/index.js @@ -1,24 +1,49 @@ -import React from 'react'; -import InputWrapper from '@components/Form/InputWrapper'; +import React, {useState} from 'react'; +import _ from 'underscore'; import TextInput from '@components/TextInput'; import useLocalize from '@hooks/useLocalize'; -import getOperatingSystem from '@libs/getOperatingSystem'; import * as RoomNameInputUtils from '@libs/RoomNameInputUtils'; import CONST from '@src/CONST'; import * as roomNameInputPropTypes from './roomNameInputPropTypes'; -function RoomNameInput({isFocused, autoFocus, disabled, errorText, forwardedRef, onBlur, shouldDelayFocus, inputID, roomName}) { +function RoomNameInput({isFocused, autoFocus, disabled, errorText, forwardedRef, value, onBlur, onChangeText, onInputChange, shouldDelayFocus}) { const {translate} = useLocalize(); - const keyboardType = getOperatingSystem() === CONST.OS.IOS ? CONST.KEYBOARD_TYPE.ASCII_CAPABLE : CONST.KEYBOARD_TYPE.VISIBLE_PASSWORD; + const [selection, setSelection] = useState(); - const valueParser = (innerRoomName) => RoomNameInputUtils.modifyRoomName(innerRoomName); - const displayParser = (innerRoomName) => RoomNameInputUtils.modifyRoomName(innerRoomName, true); + /** + * Calls the onChangeText callback with a modified room name + * @param {Event} event + */ + const setModifiedRoomName = (event) => { + const roomName = event.nativeEvent.text; + const modifiedRoomName = RoomNameInputUtils.modifyRoomName(roomName); + onChangeText(modifiedRoomName); + + // if custom component has onInputChange, use it to trigger changes (Form input) + if (_.isFunction(onInputChange)) { + onInputChange(modifiedRoomName); + } + + // Prevent cursor jump behaviour: + // Check if newRoomNameWithHash is the same as modifiedRoomName + // If it is then the room name is valid (does not contain unallowed characters); no action required + // If not then the room name contains unvalid characters and we must adjust the cursor position manually + // Read more: https://github.com/Expensify/App/issues/12741 + const oldRoomNameWithHash = value || ''; + const newRoomNameWithHash = `${CONST.POLICY.ROOM_PREFIX}${roomName}`; + if (modifiedRoomName !== newRoomNameWithHash) { + const offset = modifiedRoomName.length - oldRoomNameWithHash.length; + const newSelection = { + start: selection.start + offset, + end: selection.end + offset, + }; + setSelection(newSelection); + } + }; return ( - setSelection(event.nativeEvent.selection)} errorText={errorText} - valueParser={valueParser} - displayParser={displayParser} autoCapitalize="none" onBlur={(event) => isFocused && onBlur(event)} shouldDelayFocus={shouldDelayFocus} autoFocus={isFocused && autoFocus} maxLength={CONST.REPORT.MAX_ROOM_NAME_LENGTH} - defaultValue={roomName} spellCheck={false} shouldInterceptSwipe - keyboardType={keyboardType} // this is a bit hacky solution to a RN issue https://github.com/facebook/react-native/issues/27449 /> ); } diff --git a/src/components/RoomNameInput/index.native.js b/src/components/RoomNameInput/index.native.js new file mode 100644 index 000000000000..a2c09996ad34 --- /dev/null +++ b/src/components/RoomNameInput/index.native.js @@ -0,0 +1,66 @@ +import React from 'react'; +import _ from 'underscore'; +import TextInput from '@components/TextInput'; +import useLocalize from '@hooks/useLocalize'; +import getOperatingSystem from '@libs/getOperatingSystem'; +import * as RoomNameInputUtils from '@libs/RoomNameInputUtils'; +import CONST from '@src/CONST'; +import * as roomNameInputPropTypes from './roomNameInputPropTypes'; + +function RoomNameInput({isFocused, autoFocus, disabled, errorText, forwardedRef, value, onBlur, onChangeText, onInputChange, shouldDelayFocus}) { + const {translate} = useLocalize(); + + /** + * Calls the onChangeText callback with a modified room name + * @param {Event} event + */ + const setModifiedRoomName = (event) => { + const roomName = event.nativeEvent.text; + const modifiedRoomName = RoomNameInputUtils.modifyRoomName(roomName); + onChangeText(modifiedRoomName); + + // if custom component has onInputChange, use it to trigger changes (Form input) + if (_.isFunction(onInputChange)) { + onInputChange(modifiedRoomName); + } + }; + + const keyboardType = getOperatingSystem() === CONST.OS.IOS ? CONST.KEYBOARD_TYPE.ASCII_CAPABLE : CONST.KEYBOARD_TYPE.VISIBLE_PASSWORD; + + return ( + isFocused && onBlur(event)} + autoFocus={isFocused && autoFocus} + autoCapitalize="none" + shouldDelayFocus={shouldDelayFocus} + /> + ); +} + +RoomNameInput.propTypes = roomNameInputPropTypes.propTypes; +RoomNameInput.defaultProps = roomNameInputPropTypes.defaultProps; +RoomNameInput.displayName = 'RoomNameInput'; + +const RoomNameInputWithRef = React.forwardRef((props, ref) => ( + +)); + +RoomNameInputWithRef.displayName = 'RoomNameInputWithRef'; + +export default RoomNameInputWithRef; diff --git a/src/components/RoomNameInput/roomNameInputPropTypes.js b/src/components/RoomNameInput/roomNameInputPropTypes.js index f457e4e2a494..7f8292f0123e 100644 --- a/src/components/RoomNameInput/roomNameInputPropTypes.js +++ b/src/components/RoomNameInput/roomNameInputPropTypes.js @@ -1,5 +1,4 @@ import PropTypes from 'prop-types'; -import refPropTypes from '@components/refPropTypes'; const propTypes = { /** Callback to execute when the text input is modified correctly */ @@ -15,10 +14,10 @@ const propTypes = { errorText: PropTypes.oneOfType([PropTypes.string, PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.string, PropTypes.object]))]), /** A ref forwarded to the TextInput */ - forwardedRef: refPropTypes, + forwardedRef: PropTypes.func, /** The ID used to uniquely identify the input in a Form */ - inputID: PropTypes.string.isRequired, + inputID: PropTypes.string, /** Callback that is called when the text input is blurred */ onBlur: PropTypes.func, @@ -31,8 +30,6 @@ const propTypes = { /** Whether navigation is focused */ isFocused: PropTypes.bool.isRequired, - - roomName: PropTypes.string, }; const defaultProps = { @@ -42,10 +39,10 @@ const defaultProps = { errorText: '', forwardedRef: () => {}, + inputID: undefined, onBlur: () => {}, autoFocus: false, shouldDelayFocus: false, - roomName: '', }; export {propTypes, defaultProps}; diff --git a/src/libs/RoomNameInputUtils.ts b/src/libs/RoomNameInputUtils.ts index e6f7d420bf59..cff0bbc30274 100644 --- a/src/libs/RoomNameInputUtils.ts +++ b/src/libs/RoomNameInputUtils.ts @@ -3,14 +3,14 @@ import CONST from '@src/CONST'; /** * Replaces spaces with dashes */ -function modifyRoomName(roomName: string, skipPolicyPrefix?: boolean): string { +function modifyRoomName(roomName: string): string { const modifiedRoomNameWithoutHash = roomName .replace(/ /g, '-') // Replaces the smart dash on iOS devices with two hyphens .replace(/—/g, '--'); - return skipPolicyPrefix ? modifiedRoomNameWithoutHash : `${CONST.POLICY.ROOM_PREFIX}${modifiedRoomNameWithoutHash}`; + return `${CONST.POLICY.ROOM_PREFIX}${modifiedRoomNameWithoutHash}`; } export { diff --git a/src/pages/settings/Report/RoomNamePage.js b/src/pages/settings/Report/RoomNamePage.js index f8b6568733c7..7916043c7e20 100644 --- a/src/pages/settings/Report/RoomNamePage.js +++ b/src/pages/settings/Report/RoomNamePage.js @@ -4,7 +4,7 @@ import React, {useCallback, useRef} from 'react'; import {View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView'; -import FormProvider from '@components/Form/FormProvider'; +import Form from '@components/Form'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import RoomNameInput from '@components/RoomNameInput'; import ScreenWrapper from '@components/ScreenWrapper'; @@ -42,8 +42,13 @@ const defaultProps = { policy: {}, }; -function RoomNamePage({policy, report, reports, translate}) { +function RoomNamePage(props) { const styles = useThemeStyles(); + const policy = props.policy; + const report = props.report; + const reports = props.reports; + const translate = props.translate; + const roomNameInputRef = useRef(null); const isFocused = useIsFocused(); @@ -86,7 +91,7 @@ function RoomNamePage({policy, report, reports, translate}) { title={translate('newRoomPage.roomName')} onBackButtonPress={() => Navigation.goBack(ROUTES.REPORT_SETTINGS.getRoute(report.reportID))} /> - Report.updatePolicyRoomNameAndNavigate(report, values.roomName)} @@ -96,14 +101,13 @@ function RoomNamePage({policy, report, reports, translate}) { > (roomNameInputRef.current = ref)} inputID="roomName" defaultValue={report.reportName} isFocused={isFocused} - roomName={report.reportName.slice(1)} /> - + ); diff --git a/src/pages/workspace/WorkspaceNewRoomPage.js b/src/pages/workspace/WorkspaceNewRoomPage.js index 2abc6c3d13fa..df78539bf665 100644 --- a/src/pages/workspace/WorkspaceNewRoomPage.js +++ b/src/pages/workspace/WorkspaceNewRoomPage.js @@ -4,8 +4,7 @@ import {View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import _ from 'underscore'; import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView'; -import FormProvider from '@components/Form/FormProvider'; -import InputWrapper from '@components/Form/InputWrapper'; +import Form from '@components/Form'; import KeyboardAvoidingView from '@components/KeyboardAvoidingView'; import OfflineIndicator from '@components/OfflineIndicator'; import RoomNameInput from '@components/RoomNameInput'; @@ -182,7 +181,7 @@ function WorkspaceNewRoomPage(props) { // This is because when wrapping whole screen the screen was freezing when changing Tabs. keyboardVerticalOffset={variables.contentHeaderHeight + variables.tabSelectorButtonHeight + variables.tabSelectorButtonPadding + insets.top} > - - - {isPolicyAdmin && ( - )} - {visibilityDescription} - + {isSmallScreenWidth && } )}