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

Fix: Cursor moves to end after adding space in Room Name #12899

Merged
merged 8 commits into from
Nov 22, 2022
94 changes: 0 additions & 94 deletions src/components/RoomNameInput.js

This file was deleted.

81 changes: 81 additions & 0 deletions src/components/RoomNameInput/index.js
Original file line number Diff line number Diff line change
@@ -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,
Copy link
Contributor

Choose a reason for hiding this comment

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

We missed an edge case which caused #33652
When we select a text and paste longer text it preserves the selection.

};
this.setSelection(selection);
}
}

/**
* Set the selection
* @param {Object} selection
*/
setSelection(selection) {
this.setState({selection});
}

render() {
return (
<TextInput
ref={this.props.forwardedRef}
disabled={this.props.disabled}
label={this.props.translate('newRoomPage.roomName')}
prefixCharacter={CONST.POLICY.ROOM_PREFIX}
placeholder={this.props.translate('newRoomPage.social')}
onChange={this.setModifiedRoomName}
value={this.props.value.substring(1)} // Since the room name always starts with a prefix, we omit the first character to avoid displaying it twice.
selection={this.state.selection}
onSelectionChange={event => 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
<RoomNameInput {...props} forwardedRef={ref} />
)),
);
50 changes: 50 additions & 0 deletions src/components/RoomNameInput/index.native.js
Original file line number Diff line number Diff line change
@@ -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 (
<TextInput
ref={this.props.forwardedRef}
disabled={this.props.disabled}
label={this.props.translate('newRoomPage.roomName')}
prefixCharacter={CONST.POLICY.ROOM_PREFIX}
placeholder={this.props.translate('newRoomPage.social')}
onChange={this.setModifiedRoomName}
value={this.props.value.substring(1)} // Since the room name always starts with a prefix, we omit the first character to avoid displaying it twice.
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
<RoomNameInput {...props} forwardedRef={ref} />
)),
);
31 changes: 31 additions & 0 deletions src/components/RoomNameInput/roomNameInputPropTypes.js
Original file line number Diff line number Diff line change
@@ -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};
24 changes: 24 additions & 0 deletions src/libs/RoomNameInputUtils.js
Original file line number Diff line number Diff line change
@@ -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,
};
Loading