diff --git a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx index aa71cf00d0293..3c062deaadbd3 100644 --- a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx +++ b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx @@ -3,7 +3,7 @@ import lodashDebounce from 'lodash/debounce'; import type {ForwardedRef, RefObject} from 'react'; import React, {memo, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState} from 'react'; import type {BlurEvent, LayoutChangeEvent, MeasureInWindowOnSuccessCallback, TextInput, TextInputContentSizeChangeEvent, TextInputKeyPressEvent, TextInputScrollEvent} from 'react-native'; -import {DeviceEventEmitter, NativeModules, StyleSheet, View} from 'react-native'; +import {DeviceEventEmitter, InteractionManager, NativeModules, StyleSheet, View} from 'react-native'; import {useFocusedInputHandler} from 'react-native-keyboard-controller'; import type {OnyxEntry} from 'react-native-onyx'; import {useAnimatedRef, useSharedValue} from 'react-native-reanimated'; @@ -27,6 +27,7 @@ import {canSkipTriggerHotkeys, findCommonSuffixLength, insertText, insertWhiteSp import convertToLTRForComposer from '@libs/convertToLTRForComposer'; import {containsOnlyEmojis, extractEmojis, getAddedEmojis, getPreferredSkinToneIndex, replaceAndExtractEmojis} from '@libs/EmojiUtils'; import focusComposerWithDelay from '@libs/focusComposerWithDelay'; +import getPlatform from '@libs/getPlatform'; import {addKeyDownPressListener, removeKeyDownPressListener} from '@libs/KeyboardShortcut/KeyDownPressListener'; import {detectAndRewritePaste} from '@libs/MarkdownLinkHelpers'; import Parser from '@libs/Parser'; @@ -52,6 +53,11 @@ import type ChildrenProps from '@src/types/utils/ChildrenProps'; // eslint-disable-next-line no-restricted-imports import findNodeHandle from '@src/utils/findNodeHandle'; +type SyncSelection = { + position: number; + value: string; +}; + type NewlyAddedChars = {startIndex: number; endIndex: number; diff: string}; type ComposerWithSuggestionsProps = Partial & { @@ -154,6 +160,8 @@ type ComposerRef = { const {RNTextInputReset} = NativeModules; +const isIOSNative = getPlatform() === CONST.PLATFORM.IOS; + /** * Broadcast that the user is typing. Debounced to limit how often we publish client events. */ @@ -261,10 +269,10 @@ function ComposerWithSuggestions({ const [composerHeight, setComposerHeight] = useState(0); - const [suggestionPosition, setSuggestionPosition] = useState(null); - const textInputRef = useRef(null); + const syncSelectionWithOnChangeTextRef = useRef(null); + // The ref to check whether the comment saving is in progress const isCommentPendingSaved = useRef(false); @@ -407,6 +415,10 @@ function ComposerWithSuggestions({ if (commentValue !== newComment) { const position = Math.max((selection.end ?? 0) + (newComment.length - commentRef.current.length), cursorPosition ?? 0); + if (commentWithSpaceInserted !== newComment && isIOSNative) { + syncSelectionWithOnChangeTextRef.current = {position, value: newComment}; + } + setSelection((prevSelection) => ({ start: position, end: position, @@ -512,18 +524,25 @@ function ComposerWithSuggestions({ [shouldUseNarrowLayout, isKeyboardShown, suggestionsRef, selection.start, includeChronos, handleSendMessage, lastReportAction, reportID, updateComment, selection.end], ); - useEffect(() => { - if (suggestionPosition === null) { - return; - } + const onChangeText = useCallback( + (commentValue: string) => { + updateComment(commentValue, true); - textInputRef.current?.setSelection?.(suggestionPosition, suggestionPosition); - }, [suggestionPosition]); + if (isIOSNative && syncSelectionWithOnChangeTextRef.current) { + const positionSnapshot = syncSelectionWithOnChangeTextRef.current.position; + syncSelectionWithOnChangeTextRef.current = null; - const onSuggestionSelected = useCallback((suggestion: TextSelection) => { - setSelection(suggestion); - setSuggestionPosition(suggestion.end ?? 0); - }, []); + // ensure that selection is set imperatively after all state changes are effective + // eslint-disable-next-line @typescript-eslint/no-deprecated + InteractionManager.runAfterInteractions(() => { + // note: this implementation is only available on non-web RN, thus the wrapping + // 'if' block contains a redundant (since the ref is only used on iOS) platform check + textInputRef.current?.setSelection(positionSnapshot, positionSnapshot); + }); + } + }, + [updateComment], + ); const onSelectionChange = useCallback( (e: CustomSelectionChangeEvent) => { @@ -730,7 +749,7 @@ function ComposerWithSuggestions({ mobileInputScrollPosition.current = 0; // Note: use the value when the clear happened, not the current value which might have changed already onCleared(text); - updateComment(''); + updateComment('', true); }, [onCleared, updateComment], ); @@ -808,7 +827,7 @@ function ComposerWithSuggestions({ ref={setTextInputRef} placeholder={inputPlaceholder} placeholderTextColor={theme.placeholderText} - onChangeText={updateComment} + onChangeText={onChangeText} onKeyPress={handleKeyPress} textAlignVertical="top" style={[styles.textInputCompose, isComposerFullSize ? styles.textInputFullCompose : styles.textInputCollapseCompose]} @@ -846,7 +865,7 @@ function ComposerWithSuggestions({ // Input value={value} selection={selection} - setSelection={onSuggestionSelected} + setSelection={setSelection} resetKeyboardInput={resetKeyboardInput} />