From 51392fad732a46a65f70360b5cad5d534b9933bf Mon Sep 17 00:00:00 2001 From: Lorenzo Bloedow Date: Sat, 2 Aug 2025 11:59:06 -0300 Subject: [PATCH 1/6] Fix suggestion list selection not moving cursor to the end --- .../Composer/implementation/index.native.tsx | 1 + .../ComposerWithSuggestions.tsx | 28 +------------------ 2 files changed, 2 insertions(+), 27 deletions(-) diff --git a/src/components/Composer/implementation/index.native.tsx b/src/components/Composer/implementation/index.native.tsx index d7b6747d8921c..4d35268170315 100644 --- a/src/components/Composer/implementation/index.native.tsx +++ b/src/components/Composer/implementation/index.native.tsx @@ -127,6 +127,7 @@ function Composer( textAlignVertical="center" style={[composerStyle, maxHeightStyle]} markdownStyle={markdownStyle} + selection={selection} /* eslint-disable-next-line react/jsx-props-no-spreading */ {...props} readOnly={isDisabled} diff --git a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx index ea17a85766d99..bb926a59973ad 100644 --- a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx +++ b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx @@ -12,7 +12,7 @@ import type { TextInputKeyPressEventData, TextInputScrollEventData, } from 'react-native'; -import {DeviceEventEmitter, findNodeHandle, InteractionManager, NativeModules, StyleSheet, View} from 'react-native'; +import {DeviceEventEmitter, findNodeHandle, 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'; @@ -37,7 +37,6 @@ import convertToLTRForComposer from '@libs/convertToLTRForComposer'; import {getDraftComment} from '@libs/DraftCommentUtils'; 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 Parser from '@libs/Parser'; import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager'; @@ -60,11 +59,6 @@ import ONYXKEYS from '@src/ONYXKEYS'; import type * as OnyxTypes from '@src/types/onyx'; import type ChildrenProps from '@src/types/utils/ChildrenProps'; -type SyncSelection = { - position: number; - value: string; -}; - type NewlyAddedChars = {startIndex: number; endIndex: number; diff: string}; type ComposerWithSuggestionsProps = Partial & { @@ -167,8 +161,6 @@ 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. */ @@ -281,8 +273,6 @@ function ComposerWithSuggestions( const textInputRef = useRef(null); - const syncSelectionWithOnChangeTextRef = useRef(null); - // The ref to check whether the comment saving is in progress const isCommentPendingSaved = useRef(false); @@ -404,10 +394,6 @@ 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, @@ -505,18 +491,6 @@ function ComposerWithSuggestions( const onChangeText = useCallback( (commentValue: string) => { updateComment(commentValue, true); - - if (isIOSNative && syncSelectionWithOnChangeTextRef.current) { - const positionSnapshot = syncSelectionWithOnChangeTextRef.current.position; - syncSelectionWithOnChangeTextRef.current = null; - - // ensure that selection is set imperatively after all state changes are effective - 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], ); From 993ee291020a273c913d8df2d19f74df8ac26b00 Mon Sep 17 00:00:00 2001 From: Lorenzo Bloedow Date: Sat, 2 Aug 2025 12:22:49 -0300 Subject: [PATCH 2/6] Remove outdated comment --- src/components/Composer/implementation/index.native.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/Composer/implementation/index.native.tsx b/src/components/Composer/implementation/index.native.tsx index 4d35268170315..afdec2869ea2d 100644 --- a/src/components/Composer/implementation/index.native.tsx +++ b/src/components/Composer/implementation/index.native.tsx @@ -31,7 +31,6 @@ function Composer( style, // 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 selection, value, isGroupPolicyReport = false, From 0b90afbb416ae3ddffcc2d622e652a9ccc654cc9 Mon Sep 17 00:00:00 2001 From: Lorenzo Bloedow Date: Tue, 7 Oct 2025 10:01:01 -0300 Subject: [PATCH 3/6] Revert "Remove outdated comment" This reverts commit 993ee291020a273c913d8df2d19f74df8ac26b00. --- src/components/Composer/implementation/index.native.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/Composer/implementation/index.native.tsx b/src/components/Composer/implementation/index.native.tsx index b8e5c01c9bce8..0cd6b084b1be7 100644 --- a/src/components/Composer/implementation/index.native.tsx +++ b/src/components/Composer/implementation/index.native.tsx @@ -31,6 +31,7 @@ function Composer( style, // 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 selection, value, isGroupPolicyReport = false, From b94138570aa6be36716ea9cbd202872bf46c444a Mon Sep 17 00:00:00 2001 From: Lorenzo Bloedow Date: Tue, 7 Oct 2025 10:01:50 -0300 Subject: [PATCH 4/6] Revert "Fix suggestion list selection not moving cursor to the end" This reverts commit 51392fad732a46a65f70360b5cad5d534b9933bf. --- .../Composer/implementation/index.native.tsx | 1 - .../ComposerWithSuggestions.tsx | 29 ++++++++++++++++++- 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/src/components/Composer/implementation/index.native.tsx b/src/components/Composer/implementation/index.native.tsx index 0cd6b084b1be7..a660a228e413a 100644 --- a/src/components/Composer/implementation/index.native.tsx +++ b/src/components/Composer/implementation/index.native.tsx @@ -127,7 +127,6 @@ function Composer( textAlignVertical="center" style={[composerStyle, maxHeightStyle]} markdownStyle={markdownStyle} - selection={selection} /* eslint-disable-next-line react/jsx-props-no-spreading */ {...props} readOnly={isDisabled} diff --git a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx index ba0ceefc1c4b0..43571aa64809e 100644 --- a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx +++ b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx @@ -12,7 +12,7 @@ import type { TextInputKeyPressEventData, TextInputScrollEventData, } 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'; @@ -37,6 +37,7 @@ import convertToLTRForComposer from '@libs/convertToLTRForComposer'; import {getDraftComment} from '@libs/DraftCommentUtils'; 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 Parser from '@libs/Parser'; import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager'; @@ -61,6 +62,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 & { @@ -163,6 +169,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. */ @@ -272,6 +280,8 @@ function ComposerWithSuggestions({ const textInputRef = useRef(null); + const syncSelectionWithOnChangeTextRef = useRef(null); + // The ref to check whether the comment saving is in progress const isCommentPendingSaved = useRef(false); @@ -393,6 +403,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, @@ -490,6 +504,19 @@ function ComposerWithSuggestions({ const onChangeText = useCallback( (commentValue: string) => { updateComment(commentValue, true); + + if (isIOSNative && syncSelectionWithOnChangeTextRef.current) { + const positionSnapshot = syncSelectionWithOnChangeTextRef.current.position; + syncSelectionWithOnChangeTextRef.current = null; + + // ensure that selection is set imperatively after all state changes are effective + // eslint-disable-next-line deprecation/deprecation + 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], ); From bddb1770fc6d1c4ff00476fc302e4f870f586f07 Mon Sep 17 00:00:00 2001 From: Lorenzo Bloedow Date: Tue, 7 Oct 2025 15:28:06 -0300 Subject: [PATCH 5/6] Override current cursor position on suggestion selection --- .../ComposerWithSuggestions.tsx | 49 ++++++------------- 1 file changed, 15 insertions(+), 34 deletions(-) diff --git a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx index 43571aa64809e..f4874d96e2ff6 100644 --- a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx +++ b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx @@ -12,7 +12,7 @@ import type { TextInputKeyPressEventData, TextInputScrollEventData, } from 'react-native'; -import {DeviceEventEmitter, InteractionManager, NativeModules, StyleSheet, View} from 'react-native'; +import {DeviceEventEmitter, 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'; @@ -37,7 +37,6 @@ import convertToLTRForComposer from '@libs/convertToLTRForComposer'; import {getDraftComment} from '@libs/DraftCommentUtils'; 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 Parser from '@libs/Parser'; import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager'; @@ -62,11 +61,6 @@ 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 & { @@ -169,8 +163,6 @@ 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. */ @@ -278,9 +270,9 @@ function ComposerWithSuggestions({ const [composerHeight, setComposerHeight] = useState(0); - const textInputRef = useRef(null); + const [suggestionPosition, setSuggestionPosition] = useState(null); - const syncSelectionWithOnChangeTextRef = useRef(null); + const textInputRef = useRef(null); // The ref to check whether the comment saving is in progress const isCommentPendingSaved = useRef(false); @@ -403,10 +395,6 @@ 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, @@ -501,25 +489,18 @@ function ComposerWithSuggestions({ [shouldUseNarrowLayout, isKeyboardShown, suggestionsRef, selection.start, includeChronos, handleSendMessage, lastReportAction, reportID, updateComment, selection.end], ); - const onChangeText = useCallback( - (commentValue: string) => { - updateComment(commentValue, true); + useEffect(() => { + if (suggestionPosition === null) { + return; + } - if (isIOSNative && syncSelectionWithOnChangeTextRef.current) { - const positionSnapshot = syncSelectionWithOnChangeTextRef.current.position; - syncSelectionWithOnChangeTextRef.current = null; + textInputRef.current?.setSelection(suggestionPosition, suggestionPosition); + }, [suggestionPosition]); - // ensure that selection is set imperatively after all state changes are effective - // eslint-disable-next-line deprecation/deprecation - 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 onSuggestionSelected = useCallback((suggestion: TextSelection) => { + setSelection(suggestion); + setSuggestionPosition(suggestion.end ?? 0); + }, []); const onSelectionChange = useCallback( (e: CustomSelectionChangeEvent) => { @@ -798,7 +779,7 @@ function ComposerWithSuggestions({ ref={setTextInputRef} placeholder={inputPlaceholder} placeholderTextColor={theme.placeholderText} - onChangeText={onChangeText} + onChangeText={updateComment} onKeyPress={handleKeyPress} textAlignVertical="top" style={[styles.textInputCompose, isComposerFullSize ? styles.textInputFullCompose : styles.textInputCollapseCompose]} @@ -840,7 +821,7 @@ function ComposerWithSuggestions({ // Input value={value} selection={selection} - setSelection={setSelection} + setSelection={onSuggestionSelected} resetKeyboardInput={resetKeyboardInput} /> From 5f633416b1476c7a4b368d62fb73b8150be2d231 Mon Sep 17 00:00:00 2001 From: Lorenzo Bloedow Date: Sun, 9 Nov 2025 18:28:02 -0300 Subject: [PATCH 6/6] Fix crash due to null textInputRef being called when selecting from suggestions box on web --- .../ComposerWithSuggestions/ComposerWithSuggestions.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx index 2a0f83f370cbc..f2b0794b3b1a8 100644 --- a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx +++ b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx @@ -517,7 +517,7 @@ function ComposerWithSuggestions({ return; } - textInputRef.current?.setSelection(suggestionPosition, suggestionPosition); + textInputRef.current?.setSelection?.(suggestionPosition, suggestionPosition); }, [suggestionPosition]); const onSuggestionSelected = useCallback((suggestion: TextSelection) => {