From c221cdfd9316d7d14f5953a7e6d078591ac3be84 Mon Sep 17 00:00:00 2001 From: Abdelhafidh Belalia <16493223+s77rt@users.noreply.github.com> Date: Wed, 19 Apr 2023 20:58:14 +0100 Subject: [PATCH 1/2] Make Composer no longer selection controlled --- src/components/Composer/index.android.js | 10 ---- src/components/Composer/index.ios.js | 17 +------ src/components/Composer/index.js | 16 ------ src/pages/home/report/ReportActionCompose.js | 51 +++++++++---------- .../report/ReportActionItemMessageEdit.js | 34 ++++++------- 5 files changed, 40 insertions(+), 88 deletions(-) diff --git a/src/components/Composer/index.android.js b/src/components/Composer/index.android.js index 603bdf829edd..83dc6be587a1 100644 --- a/src/components/Composer/index.android.js +++ b/src/components/Composer/index.android.js @@ -24,12 +24,6 @@ const propTypes = { /** Prevent edits and interactions like focus for this input. */ isDisabled: PropTypes.bool, - /** Selection Object */ - selection: PropTypes.shape({ - start: PropTypes.number, - end: PropTypes.number, - }), - /** Whether the full composer can be opened */ isFullComposerAvailable: PropTypes.bool, @@ -51,10 +45,6 @@ const defaultProps = { autoFocus: false, isDisabled: false, forwardedRef: null, - selection: { - start: 0, - end: 0, - }, isFullComposerAvailable: false, setIsFullComposerAvailable: () => {}, isComposerFullSize: false, diff --git a/src/components/Composer/index.ios.js b/src/components/Composer/index.ios.js index b31a5b462f50..83dc6be587a1 100644 --- a/src/components/Composer/index.ios.js +++ b/src/components/Composer/index.ios.js @@ -24,12 +24,6 @@ const propTypes = { /** Prevent edits and interactions like focus for this input. */ isDisabled: PropTypes.bool, - /** Selection Object */ - selection: PropTypes.shape({ - start: PropTypes.number, - end: PropTypes.number, - }), - /** Whether the full composer can be opened */ isFullComposerAvailable: PropTypes.bool, @@ -51,10 +45,6 @@ const defaultProps = { autoFocus: false, isDisabled: false, forwardedRef: null, - selection: { - start: 0, - end: 0, - }, isFullComposerAvailable: false, setIsFullComposerAvailable: () => {}, isComposerFullSize: false, @@ -92,11 +82,6 @@ class Composer extends React.Component { } render() { - // On native layers we like to have the Text Input not focused so the - // user can read new chats without the keyboard in the way of the view. - // On Android, the selection prop is required on the TextInput but this prop has issues on IOS - // https://github.com/facebook/react-native/issues/29063 - const propsToPass = _.omit(this.props, 'selection'); return ( ); diff --git a/src/components/Composer/index.js b/src/components/Composer/index.js index 9b2d028a7350..5994e3f84502 100755 --- a/src/components/Composer/index.js +++ b/src/components/Composer/index.js @@ -58,12 +58,6 @@ const propTypes = { /** Update selection position on change */ onSelectionChange: PropTypes.func, - /** Selection Object */ - selection: PropTypes.shape({ - start: PropTypes.number, - end: PropTypes.number, - }), - /** Whether the full composer can be opened */ isFullComposerAvailable: PropTypes.bool, @@ -92,10 +86,6 @@ const defaultProps = { autoFocus: false, forwardedRef: null, onSelectionChange: () => {}, - selection: { - start: 0, - end: 0, - }, isFullComposerAvailable: false, setIsFullComposerAvailable: () => {}, isComposerFullSize: false, @@ -174,11 +164,6 @@ class Composer extends React.Component { || prevProps.numberOfLines !== this.props.numberOfLines) { this.updateNumberOfLines(); } - - if (prevProps.selection !== this.props.selection) { - // eslint-disable-next-line react/no-did-update-set-state - this.setState({selection: this.props.selection}); - } } componentWillUnmount() { @@ -365,7 +350,6 @@ class Composer extends React.Component { autoCorrect={!Browser.isMobileSafari()} placeholderTextColor={themeColors.placeholderText} ref={el => this.textInput = el} - selection={this.state.selection} onChange={this.shouldCallUpdateNumberOfLines} onSelectionChange={this.onSelectionChange} style={[ diff --git a/src/pages/home/report/ReportActionCompose.js b/src/pages/home/report/ReportActionCompose.js index 1fa0874824d8..ee7bbd429a33 100644 --- a/src/pages/home/report/ReportActionCompose.js +++ b/src/pages/home/report/ReportActionCompose.js @@ -53,6 +53,7 @@ import ArrowKeyFocusManager from '../../../components/ArrowKeyFocusManager'; import OfflineWithFeedback from '../../../components/OfflineWithFeedback'; import KeyboardShortcut from '../../../libs/KeyboardShortcut'; import Permissions from '../../../libs/Permissions'; +import setSelection from '../../../libs/setSelection'; const propTypes = { /** Beta features list */ @@ -479,15 +480,12 @@ class ReportActionCompose extends React.Component { const emojiObject = this.state.suggestedEmojis[highlightedEmojiIndex]; const emojiCode = emojiObject.types && emojiObject.types[this.props.preferredSkinTone] ? emojiObject.types[this.props.preferredSkinTone] : emojiObject.code; const commentAfterColonWithEmojiNameRemoved = this.state.value.slice(this.state.selection.end).replace(CONST.REGEX.EMOJI_REPLACER, CONST.SPACE); + const oldColonIndex = this.state.colonIndex; this.updateComment(`${commentBeforeColon}${emojiCode} ${commentAfterColonWithEmojiNameRemoved}`, true); - this.setState(prevState => ({ - selection: { - start: prevState.colonIndex + emojiCode.length + CONST.SPACE_LENGTH, - end: prevState.colonIndex + emojiCode.length + CONST.SPACE_LENGTH, - }, - suggestedEmojis: [], - })); + this.setState({suggestedEmojis: []}, () => { + setSelection(this.textInput, oldColonIndex + emojiCode.length + CONST.SPACE_LENGTH, oldColonIndex + emojiCode.length + CONST.SPACE_LENGTH); + }); EmojiUtils.addToFrequentlyUsedEmojis(this.props.frequentlyUsedEmojis, emojiObject); } @@ -505,13 +503,11 @@ class ReportActionCompose extends React.Component { const newComment = this.comment.slice(0, this.state.selection.start) + emojiWithSpace + this.comment.slice(this.state.selection.end, this.comment.length); - this.setState(prevState => ({ - selection: { - start: prevState.selection.start + emojiWithSpace.length, - end: prevState.selection.start + emojiWithSpace.length, - }, - })); - this.updateComment(newComment); + const newSelection = { + start: this.state.selection.start + emojiWithSpace.length, + end: this.state.selection.start + emojiWithSpace.length, + }; + this.updateComment(newComment, false, newSelection); } /** @@ -562,22 +558,22 @@ class ReportActionCompose extends React.Component { * * @param {String} comment * @param {Boolean} shouldDebounceSaveComment + * @param {Object} selection */ - updateComment(comment, shouldDebounceSaveComment) { + updateComment(comment, shouldDebounceSaveComment, selection) { + const oldComment = this.state.value; + const oldSelection = this.state.selection; const newComment = EmojiUtils.replaceEmojis(comment, this.props.isSmallScreenWidth, this.props.preferredSkinTone); - this.setState((prevState) => { - const newState = { - isCommentEmpty: !!newComment.match(/^(\s)*$/), - value: newComment, - }; - if (comment !== newComment) { - const remainder = prevState.value.slice(prevState.selection.end).length; - newState.selection = { - start: newComment.length - remainder, - end: newComment.length - remainder, - }; + this.setState({ + isCommentEmpty: !!newComment.match(/^(\s)*$/), + value: newComment, + }, () => { + if (selection) { + setSelection(this.textInput, selection.start, selection.end); + } else if (comment !== newComment) { + const remainder = oldComment.slice(oldSelection.end).length; + setSelection(this.textInput, newComment.length - remainder, newComment.length - remainder); } - return newState; }); // Indicate that draft has been created. @@ -879,7 +875,6 @@ class ReportActionCompose extends React.Component { shouldClear={this.state.textInputShouldClear} onClear={() => this.setTextInputShouldClear(false)} isDisabled={isComposeDisabled || isBlockedFromConcierge || this.props.disabled} - selection={this.state.selection} onSelectionChange={this.onSelectionChange} isFullComposerAvailable={this.state.isFullComposerAvailable} setIsFullComposerAvailable={this.setIsFullComposerAvailable} diff --git a/src/pages/home/report/ReportActionItemMessageEdit.js b/src/pages/home/report/ReportActionItemMessageEdit.js index db037c4122b0..fb723d52acb5 100644 --- a/src/pages/home/report/ReportActionItemMessageEdit.js +++ b/src/pages/home/report/ReportActionItemMessageEdit.js @@ -27,6 +27,7 @@ import withWindowDimensions, {windowDimensionsPropTypes} from '../../../componen import withLocalize, {withLocalizePropTypes} from '../../../components/withLocalize'; import withKeyboardState, {keyboardStatePropTypes} from '../../../components/withKeyboardState'; import ONYXKEYS from '../../../ONYXKEYS'; +import setSelection from '../../../libs/setSelection'; const propTypes = { /** All the data of the action */ @@ -135,19 +136,19 @@ class ReportActionItemMessageEdit extends React.Component { * Update the value of the draft in Onyx * * @param {String} draft + * @param {Object} selection */ - updateDraft(draft) { + updateDraft(draft, selection) { + const oldDraft = this.state.draft; + const oldSelection = this.state.selection; const newDraft = EmojiUtils.replaceEmojis(draft, this.props.isSmallScreenWidth, this.props.preferredSkinTone); - this.setState((prevState) => { - const newState = {draft: newDraft}; - if (draft !== newDraft) { - const remainder = prevState.draft.slice(prevState.selection.end).length; - newState.selection = { - start: newDraft.length - remainder, - end: newDraft.length - remainder, - }; + this.setState({draft: newDraft}, () => { + if (selection) { + setSelection(this.textInput, selection.start, selection.end); + } else if (draft !== newDraft) { + const remainder = oldDraft.slice(oldSelection.end).length; + setSelection(this.textInput, newDraft.length - remainder, newDraft.length - remainder); } - return newState; }); // This component is rendered only when draft is set to a non-empty string. In order to prevent component @@ -231,13 +232,11 @@ class ReportActionItemMessageEdit extends React.Component { addEmojiToTextBox(emoji) { const newComment = this.state.draft.slice(0, this.state.selection.start) + emoji + this.state.draft.slice(this.state.selection.end, this.state.draft.length); - this.setState(prevState => ({ - selection: { - start: prevState.selection.start + emoji.length, - end: prevState.selection.start + emoji.length, - }, - })); - this.updateDraft(newComment); + const newSelection = { + start: this.state.selection.start + emoji.length, + end: this.state.selection.start + emoji.length, + }; + this.updateDraft(newComment, newSelection); } /** @@ -302,7 +301,6 @@ class ReportActionItemMessageEdit extends React.Component { } openReportActionComposeViewWhenClosingMessageEdit(this.props.isSmallScreenWidth); }} - selection={this.state.selection} onSelectionChange={this.onSelectionChange} numberOfLines={this.props.numberOfLines} onNumberOfLinesChange={this.updateNumberOfLines} From 137e8fc93d8303e8c3329906b7add872f43953c0 Mon Sep 17 00:00:00 2001 From: Abdelhafidh Belalia <16493223+s77rt@users.noreply.github.com> Date: Fri, 21 Apr 2023 10:19:02 +0100 Subject: [PATCH 2/2] Composer: merged index.ios.js and index.android.js into index.native.js --- src/components/Composer/index.ios.js | 109 ------------------ .../{index.android.js => index.native.js} | 0 2 files changed, 109 deletions(-) delete mode 100644 src/components/Composer/index.ios.js rename src/components/Composer/{index.android.js => index.native.js} (100%) diff --git a/src/components/Composer/index.ios.js b/src/components/Composer/index.ios.js deleted file mode 100644 index 83dc6be587a1..000000000000 --- a/src/components/Composer/index.ios.js +++ /dev/null @@ -1,109 +0,0 @@ -import React from 'react'; -import {StyleSheet} from 'react-native'; -import PropTypes from 'prop-types'; -import _ from 'underscore'; -import RNTextInput from '../RNTextInput'; -import themeColors from '../../styles/themes/default'; -import CONST from '../../CONST'; -import * as ComposerUtils from '../../libs/ComposerUtils'; - -const propTypes = { - /** If the input should clear, it actually gets intercepted instead of .clear() */ - shouldClear: PropTypes.bool, - - /** A ref to forward to the text input */ - forwardedRef: PropTypes.func, - - /** When the input has cleared whoever owns this input should know about it */ - onClear: PropTypes.func, - - /** Set focus to this component the first time it renders. - * Override this in case you need to set focus on one field out of many, or when you want to disable autoFocus */ - autoFocus: PropTypes.bool, - - /** Prevent edits and interactions like focus for this input. */ - isDisabled: PropTypes.bool, - - /** Whether the full composer can be opened */ - isFullComposerAvailable: PropTypes.bool, - - /** Allow the full composer to be opened */ - setIsFullComposerAvailable: PropTypes.func, - - /** Whether the composer is full size */ - isComposerFullSize: PropTypes.bool, - - /** General styles to apply to the text input */ - // eslint-disable-next-line react/forbid-prop-types - style: PropTypes.any, - -}; - -const defaultProps = { - shouldClear: false, - onClear: () => {}, - autoFocus: false, - isDisabled: false, - forwardedRef: null, - isFullComposerAvailable: false, - setIsFullComposerAvailable: () => {}, - isComposerFullSize: false, - style: null, -}; - -class Composer extends React.Component { - constructor(props) { - super(props); - - this.state = { - propStyles: StyleSheet.flatten(this.props.style), - }; - } - - componentDidMount() { - // This callback prop is used by the parent component using the constructor to - // get a ref to the inner textInput element e.g. if we do - // this.textInput = el} /> this will not - // return a ref to the component, but rather the HTML element by default - if (!this.props.forwardedRef || !_.isFunction(this.props.forwardedRef)) { - return; - } - - this.props.forwardedRef(this.textInput); - } - - componentDidUpdate(prevProps) { - if (prevProps.shouldClear || !this.props.shouldClear) { - return; - } - - this.textInput.clear(); - this.props.onClear(); - } - - render() { - return ( - this.textInput = el} - maxHeight={this.props.isComposerFullSize ? '100%' : CONST.COMPOSER_MAX_HEIGHT} - onContentSizeChange={e => ComposerUtils.updateNumberOfLines(this.props, e)} - rejectResponderTermination={false} - textAlignVertical="center" - style={this.state.propStyles} - /* eslint-disable-next-line react/jsx-props-no-spreading */ - {...this.props} - editable={!this.props.isDisabled} - /> - ); - } -} - -Composer.propTypes = propTypes; -Composer.defaultProps = defaultProps; - -export default React.forwardRef((props, ref) => ( - /* eslint-disable-next-line react/jsx-props-no-spreading */ - -)); diff --git a/src/components/Composer/index.android.js b/src/components/Composer/index.native.js similarity index 100% rename from src/components/Composer/index.android.js rename to src/components/Composer/index.native.js