diff --git a/src/components/RoomNameInput.js b/src/components/RoomNameInput.js deleted file mode 100644 index 95e016cd6a1..00000000000 --- a/src/components/RoomNameInput.js +++ /dev/null @@ -1,94 +0,0 @@ -import React, {Component} from 'react'; -import PropTypes from 'prop-types'; -import CONST from '../CONST'; -import withLocalize, {withLocalizePropTypes} from './withLocalize'; -import TextInput from './TextInput'; - -const propTypes = { - /** Callback to execute when the text input is modified correctly */ - onChangeText: PropTypes.func, - - /** Room name to show in input field. This should include the '#' already prefixed to the name */ - value: PropTypes.string, - - /** Whether we should show the input as disabled */ - disabled: PropTypes.bool, - - /** Error text to show */ - errorText: PropTypes.string, - - ...withLocalizePropTypes, - - /** A ref forwarded to the TextInput */ - forwardedRef: PropTypes.func, -}; - -const defaultProps = { - onChangeText: () => {}, - value: '', - disabled: false, - errorText: '', - forwardedRef: () => {}, -}; - -class RoomNameInput extends Component { - constructor(props) { - super(props); - - this.setModifiedRoomName = this.setModifiedRoomName.bind(this); - } - - /** - * Calls the onChangeText callback with a modified room name - * @param {Event} event - */ - setModifiedRoomName(event) { - const roomName = event.nativeEvent.text; - const modifiedRoomName = this.modifyRoomName(roomName); - this.props.onChangeText(modifiedRoomName); - } - - /** - * Modifies the room name to follow our conventions: - * - Max length 80 characters - * - Cannot not include space or special characters, and we automatically apply an underscore for spaces - * - Must be lowercase - * @param {String} roomName - * @returns {String} - */ - modifyRoomName(roomName) { - const modifiedRoomNameWithoutHash = roomName - .replace(/ /g, '_') - .replace(/[^a-zA-Z\d_]/g, '') - .substr(0, CONST.REPORT.MAX_ROOM_NAME_LENGTH) - .toLowerCase(); - - return `${CONST.POLICY.ROOM_PREFIX}${modifiedRoomNameWithoutHash}`; - } - - render() { - return ( - - ); - } -} - -RoomNameInput.propTypes = propTypes; -RoomNameInput.defaultProps = defaultProps; - -export default withLocalize( - React.forwardRef((props, ref) => ( - // eslint-disable-next-line react/jsx-props-no-spreading - - )), -); diff --git a/src/components/RoomNameInput/index.js b/src/components/RoomNameInput/index.js new file mode 100644 index 00000000000..6c015ec361d --- /dev/null +++ b/src/components/RoomNameInput/index.js @@ -0,0 +1,81 @@ +import React, {Component} from 'react'; +import CONST from '../../CONST'; +import withLocalize from '../withLocalize'; +import TextInput from '../TextInput'; +import * as roomNameInputPropTypes from './roomNameInputPropTypes'; +import * as RoomNameInputUtils from '../../libs/RoomNameInputUtils'; + +class RoomNameInput extends Component { + constructor(props) { + super(props); + + this.setModifiedRoomName = this.setModifiedRoomName.bind(this); + this.setSelection = this.setSelection.bind(this); + + this.state = { + selection: undefined, + }; + } + + /** + * Calls the onChangeText callback with a modified room name + * @param {Event} event + */ + setModifiedRoomName(event) { + const roomName = event.nativeEvent.text; + const modifiedRoomName = RoomNameInputUtils.modifyRoomName(roomName); + this.props.onChangeText(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 = this.props.value || ''; + const newRoomNameWithHash = `${CONST.POLICY.ROOM_PREFIX}${roomName}`; + if (modifiedRoomName !== newRoomNameWithHash) { + const offset = modifiedRoomName.length - oldRoomNameWithHash.length; + const selection = { + start: this.state.selection.start + offset, + end: this.state.selection.end + offset, + }; + this.setSelection(selection); + } + } + + /** + * Set the selection + * @param {Object} selection + */ + setSelection(selection) { + this.setState({selection}); + } + + render() { + return ( + this.setSelection(event.nativeEvent.selection)} + errorText={this.props.errorText} + autoCapitalize="none" + /> + ); + } +} + +RoomNameInput.propTypes = roomNameInputPropTypes.propTypes; +RoomNameInput.defaultProps = roomNameInputPropTypes.defaultProps; + +export default withLocalize( + React.forwardRef((props, ref) => ( + // eslint-disable-next-line react/jsx-props-no-spreading + + )), +); diff --git a/src/components/RoomNameInput/index.native.js b/src/components/RoomNameInput/index.native.js new file mode 100644 index 00000000000..2eb22be46e1 --- /dev/null +++ b/src/components/RoomNameInput/index.native.js @@ -0,0 +1,50 @@ +import React, {Component} from 'react'; +import CONST from '../../CONST'; +import withLocalize from '../withLocalize'; +import TextInput from '../TextInput'; +import * as roomNameInputPropTypes from './roomNameInputPropTypes'; +import * as RoomNameInputUtils from '../../libs/RoomNameInputUtils'; + +class RoomNameInput extends Component { + constructor(props) { + super(props); + + this.setModifiedRoomName = this.setModifiedRoomName.bind(this); + } + + /** + * Calls the onChangeText callback with a modified room name + * @param {Event} event + */ + setModifiedRoomName(event) { + const roomName = event.nativeEvent.text; + const modifiedRoomName = RoomNameInputUtils.modifyRoomName(roomName); + this.props.onChangeText(modifiedRoomName); + } + + render() { + return ( + + ); + } +} + +RoomNameInput.propTypes = roomNameInputPropTypes.propTypes; +RoomNameInput.defaultProps = roomNameInputPropTypes.defaultProps; + +export default withLocalize( + React.forwardRef((props, ref) => ( + // eslint-disable-next-line react/jsx-props-no-spreading + + )), +); diff --git a/src/components/RoomNameInput/roomNameInputPropTypes.js b/src/components/RoomNameInput/roomNameInputPropTypes.js new file mode 100644 index 00000000000..db5ce4b3c0c --- /dev/null +++ b/src/components/RoomNameInput/roomNameInputPropTypes.js @@ -0,0 +1,31 @@ +import PropTypes from 'prop-types'; +import {withLocalizePropTypes} from '../withLocalize'; + +const propTypes = { + /** Callback to execute when the text input is modified correctly */ + onChangeText: PropTypes.func, + + /** Room name to show in input field. This should include the '#' already prefixed to the name */ + value: PropTypes.string, + + /** Whether we should show the input as disabled */ + disabled: PropTypes.bool, + + /** Error text to show */ + errorText: PropTypes.string, + + ...withLocalizePropTypes, + + /** A ref forwarded to the TextInput */ + forwardedRef: PropTypes.func, +}; + +const defaultProps = { + onChangeText: () => {}, + value: '', + disabled: false, + errorText: '', + forwardedRef: () => {}, +}; + +export {propTypes, defaultProps}; diff --git a/src/libs/RoomNameInputUtils.js b/src/libs/RoomNameInputUtils.js new file mode 100644 index 00000000000..5522096973d --- /dev/null +++ b/src/libs/RoomNameInputUtils.js @@ -0,0 +1,24 @@ +import CONST from '../CONST'; + +/** + * Modifies the room name to follow our conventions: + * - Max length 80 characters + * - Cannot not include space or special characters, and we automatically apply an underscore for spaces + * - Must be lowercase + * @param {String} roomName + * @returns {String} + */ +function modifyRoomName(roomName) { + const modifiedRoomNameWithoutHash = roomName + .replace(/ /g, '_') + .replace(/[^a-zA-Z\d_]/g, '') + .substr(0, CONST.REPORT.MAX_ROOM_NAME_LENGTH) + .toLowerCase(); + + return `${CONST.POLICY.ROOM_PREFIX}${modifiedRoomNameWithoutHash}`; +} + +export { + // eslint-disable-next-line import/prefer-default-export + modifyRoomName, +};