diff --git a/src/libs/focusComposerWithDelay.ts b/src/libs/focusComposerWithDelay/index.ts similarity index 51% rename from src/libs/focusComposerWithDelay.ts rename to src/libs/focusComposerWithDelay/index.ts index 6a2f85f7d311..75e8f6ca8a67 100644 --- a/src/libs/focusComposerWithDelay.ts +++ b/src/libs/focusComposerWithDelay/index.ts @@ -1,17 +1,18 @@ -import type {TextInput} from 'react-native'; -import * as EmojiPickerAction from './actions/EmojiPickerAction'; -import ComposerFocusManager from './ComposerFocusManager'; +import ComposerFocusManager from '@libs/ComposerFocusManager'; +import * as EmojiPickerAction from '@userActions/EmojiPickerAction'; +import setTextInputSelection from './setTextInputSelection'; +import type {FocusComposerWithDelay, InputType} from './types'; -type FocusComposerWithDelay = (shouldDelay?: boolean) => void; /** * Create a function that focuses the composer. */ -function focusComposerWithDelay(textInput: TextInput | null): FocusComposerWithDelay { +function focusComposerWithDelay(textInput: InputType | null): FocusComposerWithDelay { /** * Focus the text input * @param [shouldDelay] Impose delay before focusing the text input + * @param [forcedSelectionRange] Force selection range of text input */ - return (shouldDelay = false) => { + return (shouldDelay = false, forcedSelectionRange = undefined) => { // There could be other animations running while we trigger manual focus. // This prevents focus from making those animations janky. if (!textInput || EmojiPickerAction.isEmojiPickerVisible()) { @@ -20,6 +21,9 @@ function focusComposerWithDelay(textInput: TextInput | null): FocusComposerWithD if (!shouldDelay) { textInput.focus(); + if (forcedSelectionRange) { + setTextInputSelection(textInput, forcedSelectionRange); + } return; } ComposerFocusManager.isReadyToFocus().then(() => { @@ -27,6 +31,9 @@ function focusComposerWithDelay(textInput: TextInput | null): FocusComposerWithD return; } textInput.focus(); + if (forcedSelectionRange) { + setTextInputSelection(textInput, forcedSelectionRange); + } }); }; } diff --git a/src/libs/focusComposerWithDelay/setTextInputSelection.ts b/src/libs/focusComposerWithDelay/setTextInputSelection.ts new file mode 100644 index 000000000000..fbbc4738702f --- /dev/null +++ b/src/libs/focusComposerWithDelay/setTextInputSelection.ts @@ -0,0 +1,15 @@ +import type {TextInput} from 'react-native'; +import shouldSetSelectionRange from '@libs/shouldSetSelectionRange'; +import type {InputType, Selection} from './types'; + +const setSelectionRange = shouldSetSelectionRange(); + +const setTextInputSelection = (textInput: InputType, forcedSelectionRange: Selection) => { + if (setSelectionRange) { + (textInput as HTMLTextAreaElement).setSelectionRange(forcedSelectionRange.start, forcedSelectionRange.end); + } else { + (textInput as TextInput).setSelection(forcedSelectionRange.start, forcedSelectionRange.end); + } +}; + +export default setTextInputSelection; diff --git a/src/libs/focusComposerWithDelay/types.ts b/src/libs/focusComposerWithDelay/types.ts new file mode 100644 index 000000000000..4cd2f785f2bc --- /dev/null +++ b/src/libs/focusComposerWithDelay/types.ts @@ -0,0 +1,12 @@ +import type {TextInput} from 'react-native'; + +type Selection = { + start: number; + end: number; +}; + +type FocusComposerWithDelay = (shouldDelay?: boolean, forcedSelectionRange?: Selection) => void; + +type InputType = TextInput | HTMLTextAreaElement; + +export type {Selection, FocusComposerWithDelay, InputType}; diff --git a/src/libs/shouldSetSelectionRange/index.ts b/src/libs/shouldSetSelectionRange/index.ts new file mode 100644 index 000000000000..1b28bdef07cb --- /dev/null +++ b/src/libs/shouldSetSelectionRange/index.ts @@ -0,0 +1,5 @@ +import type ShouldSetSelectionRange from './types'; + +const shouldSetSelectionRange: ShouldSetSelectionRange = () => false; + +export default shouldSetSelectionRange; diff --git a/src/libs/shouldSetSelectionRange/index.website.ts b/src/libs/shouldSetSelectionRange/index.website.ts new file mode 100644 index 000000000000..655193ebfb67 --- /dev/null +++ b/src/libs/shouldSetSelectionRange/index.website.ts @@ -0,0 +1,5 @@ +import type ShouldSetSelectionRange from './types'; + +const shouldSetSelectionRange: ShouldSetSelectionRange = () => true; + +export default shouldSetSelectionRange; diff --git a/src/libs/shouldSetSelectionRange/types.ts b/src/libs/shouldSetSelectionRange/types.ts new file mode 100644 index 000000000000..e0a80614a32e --- /dev/null +++ b/src/libs/shouldSetSelectionRange/types.ts @@ -0,0 +1,3 @@ +type ShouldSetSelectionRange = () => boolean; + +export default ShouldSetSelectionRange; diff --git a/src/pages/home/report/ReportActionItemMessageEdit.tsx b/src/pages/home/report/ReportActionItemMessageEdit.tsx index fbf2da69aa31..79db22d83ee1 100644 --- a/src/pages/home/report/ReportActionItemMessageEdit.tsx +++ b/src/pages/home/report/ReportActionItemMessageEdit.tsx @@ -26,6 +26,7 @@ import * as Browser from '@libs/Browser'; import * as ComposerUtils from '@libs/ComposerUtils'; import * as EmojiUtils from '@libs/EmojiUtils'; import focusComposerWithDelay from '@libs/focusComposerWithDelay'; +import type {Selection} from '@libs/focusComposerWithDelay/types'; import focusEditAfterCancelDelete from '@libs/focusEditAfterCancelDelete'; import onyxSubscribe from '@libs/onyxSubscribe'; import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager'; @@ -41,6 +42,7 @@ import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type * as OnyxTypes from '@src/types/onyx'; import * as ReportActionContextMenu from './ContextMenu/ReportActionContextMenu'; +import shouldUseEmojiPickerSelection from './shouldUseEmojiPickerSelection'; type ReportActionItemMessageEditProps = { /** All the data of the action */ @@ -67,6 +69,7 @@ const emojiButtonID = 'emojiButton'; const messageEditInput = 'messageEditInput'; const isMobileSafari = Browser.isMobileSafari(); +const shouldUseForcedSelectionRange = shouldUseEmojiPickerSelection(); function ReportActionItemMessageEdit( {action, draftMessage, reportID, index, shouldDisableEmojiPicker = false, preferredSkinTone = CONST.EMOJI_DEFAULT_SKIN_TONE}: ReportActionItemMessageEditProps, @@ -106,10 +109,7 @@ function ReportActionItemMessageEdit( } return initialDraft; }); - const [selection, setSelection] = useState<{ - start: number; - end: number; - }>(getInitialSelection); + const [selection, setSelection] = useState(getInitialSelection); const [isFocused, setIsFocused] = useState(false); const {hasExceededMaxCommentLength, validateCommentMaxLength} = useHandleExceedMaxCommentLength(); const [modal, setModal] = useState({ @@ -122,6 +122,7 @@ function ReportActionItemMessageEdit( const isFocusedRef = useRef(false); const insertedEmojis = useRef([]); const draftRef = useRef(draft); + const emojiPickerSelectionRef = useRef(undefined); useEffect(() => { if (ReportActionsUtils.isDeletedAction(action) || (action.message && draftMessage === action.message[0].html)) { @@ -334,10 +335,17 @@ function ReportActionItemMessageEdit( * @param emoji */ const addEmojiToTextBox = (emoji: string) => { - setSelection((prevSelection) => ({ - start: prevSelection.start + emoji.length + CONST.SPACE_LENGTH, - end: prevSelection.start + emoji.length + CONST.SPACE_LENGTH, - })); + const newSelection = { + start: selection.start + emoji.length + CONST.SPACE_LENGTH, + end: selection.start + emoji.length + CONST.SPACE_LENGTH, + }; + setSelection(newSelection); + + if (shouldUseForcedSelectionRange) { + // On Android and Chrome mobile, focusing the input sets the cursor position back to the start. + // To fix this, immediately set the selection again after focusing the input. + emojiPickerSelectionRef.current = newSelection; + } updateDraft(ComposerUtils.insertText(draft, selection, `${emoji} `)); }; @@ -451,7 +459,9 @@ function ReportActionItemMessageEdit( focus(true)} + onModalHide={() => { + focus(true, emojiPickerSelectionRef.current ? {...emojiPickerSelectionRef.current} : undefined); + }} onEmojiSelected={addEmojiToTextBox} id={emojiButtonID} emojiPickerID={action.reportActionID} diff --git a/src/pages/home/report/shouldUseEmojiPickerSelection/index.android.ts b/src/pages/home/report/shouldUseEmojiPickerSelection/index.android.ts new file mode 100644 index 000000000000..b8ffbc0518b0 --- /dev/null +++ b/src/pages/home/report/shouldUseEmojiPickerSelection/index.android.ts @@ -0,0 +1,5 @@ +import type ShouldUseEmojiPickerSelection from './types'; + +const shouldUseEmojiPickerSelection: ShouldUseEmojiPickerSelection = () => true; + +export default shouldUseEmojiPickerSelection; diff --git a/src/pages/home/report/shouldUseEmojiPickerSelection/index.ts b/src/pages/home/report/shouldUseEmojiPickerSelection/index.ts new file mode 100644 index 000000000000..2f94b3656e12 --- /dev/null +++ b/src/pages/home/report/shouldUseEmojiPickerSelection/index.ts @@ -0,0 +1,5 @@ +import type ShouldUseEmojiPickerSelection from './types'; + +const shouldUseEmojiPickerSelection: ShouldUseEmojiPickerSelection = () => false; + +export default shouldUseEmojiPickerSelection; diff --git a/src/pages/home/report/shouldUseEmojiPickerSelection/index.website.ts b/src/pages/home/report/shouldUseEmojiPickerSelection/index.website.ts new file mode 100644 index 000000000000..ff1982325d1d --- /dev/null +++ b/src/pages/home/report/shouldUseEmojiPickerSelection/index.website.ts @@ -0,0 +1,8 @@ +import * as Browser from '@libs/Browser'; +import type ShouldUseEmojiPickerSelection from './types'; + +const isMobileChrome = Browser.isMobileChrome(); + +const shouldUseEmojiPickerSelection: ShouldUseEmojiPickerSelection = () => isMobileChrome; + +export default shouldUseEmojiPickerSelection; diff --git a/src/pages/home/report/shouldUseEmojiPickerSelection/types.ts b/src/pages/home/report/shouldUseEmojiPickerSelection/types.ts new file mode 100644 index 000000000000..bbf96f867e7a --- /dev/null +++ b/src/pages/home/report/shouldUseEmojiPickerSelection/types.ts @@ -0,0 +1,3 @@ +type ShouldUseEmojiPickerSelection = () => boolean; + +export default ShouldUseEmojiPickerSelection;