diff --git a/src/CONST.ts b/src/CONST.ts index 6d81abc37a85..7659f5a18e9e 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -1244,7 +1244,7 @@ const CONST = { MAX_AMOUNT_OF_SUGGESTIONS: 20, MAX_AMOUNT_OF_VISIBLE_SUGGESTIONS_IN_CONTAINER: 5, HERE_TEXT: '@here', - SUGGESTION_BOX_MAX_SAFE_DISTANCE: 38, + SUGGESTION_BOX_MAX_SAFE_DISTANCE: 10, BIG_SCREEN_SUGGESTION_WIDTH: 300, }, COMPOSER_MAX_HEIGHT: 125, diff --git a/src/components/AutoCompleteSuggestions/AutoCompleteSuggestionsPortal/getBottomSuggestionPadding/index.ts b/src/components/AutoCompleteSuggestions/AutoCompleteSuggestionsPortal/getBottomSuggestionPadding/index.ts index 3ad9bbe7b152..acdc643b6b70 100644 --- a/src/components/AutoCompleteSuggestions/AutoCompleteSuggestionsPortal/getBottomSuggestionPadding/index.ts +++ b/src/components/AutoCompleteSuggestions/AutoCompleteSuggestionsPortal/getBottomSuggestionPadding/index.ts @@ -1,5 +1,5 @@ function getBottomSuggestionPadding(): number { - return 0; + return 6; } export default getBottomSuggestionPadding; diff --git a/src/components/AutoCompleteSuggestions/index.tsx b/src/components/AutoCompleteSuggestions/index.tsx index 8634d6dd0ca0..1aa486eccd4d 100644 --- a/src/components/AutoCompleteSuggestions/index.tsx +++ b/src/components/AutoCompleteSuggestions/index.tsx @@ -22,8 +22,16 @@ const measureHeightOfSuggestionRows = (numRows: number, canBeBig: boolean): numb } return numRows * CONST.AUTO_COMPLETE_SUGGESTER.SUGGESTION_ROW_HEIGHT; }; -function isSuggestionRenderedAbove(isEnoughSpaceAboveForBig: boolean, isEnoughSpaceAboveForSmall: boolean): boolean { - return isEnoughSpaceAboveForBig || isEnoughSpaceAboveForSmall; +function isSuggestionMenuRenderedAbove(isEnoughSpaceAboveForBigMenu: boolean, isEnoughSpaceAboveForSmallMenu: boolean): boolean { + return isEnoughSpaceAboveForBigMenu || isEnoughSpaceAboveForSmallMenu; +} + +type IsEnoughSpaceToRenderMenuAboveCursor = Pick & { + contentHeight: number; + topInset: number; +}; +function isEnoughSpaceToRenderMenuAboveCursor({y, cursorCoordinates, scrollValue, contentHeight, topInset}: IsEnoughSpaceToRenderMenuAboveCursor): boolean { + return y + (cursorCoordinates.y - scrollValue) > contentHeight + topInset + CONST.AUTO_COMPLETE_SUGGESTER.SUGGESTION_BOX_MAX_SAFE_DISTANCE; } /** @@ -35,7 +43,7 @@ function isSuggestionRenderedAbove(isEnoughSpaceAboveForBig: boolean, isEnoughSp function AutoCompleteSuggestions({measureParentContainerAndReportCursor = () => {}, ...props}: AutoCompleteSuggestionsProps) { const containerRef = React.useRef(null); const isInitialRender = React.useRef(true); - const isSuggestionAboveRef = React.useRef(false); + const isSuggestionMenuAboveRef = React.useRef(false); const leftValue = React.useRef(0); const prevLeftValue = React.useRef(0); const {windowHeight, windowWidth, isSmallScreenWidth} = useWindowDimensions(); @@ -44,11 +52,12 @@ function AutoCompleteSuggestions({measureParentContainerAndReportCu width: 0, left: 0, bottom: 0, + cursorCoordinates: {x: 0, y: 0}, }); const StyleUtils = useStyleUtils(); const insets = useSafeAreaInsets(); const {keyboardHeight} = useKeyboardState(); - const {paddingBottom: bottomInset} = StyleUtils.getSafeAreaPadding(insets ?? undefined); + const {paddingBottom: bottomInset, paddingTop: topInset} = StyleUtils.getSafeAreaPadding(insets ?? undefined); useEffect(() => { const container = containerRef.current; @@ -73,51 +82,51 @@ function AutoCompleteSuggestions({measureParentContainerAndReportCu measureParentContainerAndReportCursor(({x, y, width, scrollValue, cursorCoordinates}: MeasureParentContainerAndCursor) => { const xCoordinatesOfCursor = x + cursorCoordinates.x; - const leftValueForBigScreen = + const bigScreenLeftOffset = xCoordinatesOfCursor + CONST.AUTO_COMPLETE_SUGGESTER.BIG_SCREEN_SUGGESTION_WIDTH > windowWidth ? windowWidth - CONST.AUTO_COMPLETE_SUGGESTER.BIG_SCREEN_SUGGESTION_WIDTH : xCoordinatesOfCursor; - - let bottomValue = windowHeight - y - cursorCoordinates.y + scrollValue - (keyboardHeight || bottomInset); - const widthValue = isSmallScreenWidth ? width : CONST.AUTO_COMPLETE_SUGGESTER.BIG_SCREEN_SUGGESTION_WIDTH; - const contentMaxHeight = measureHeightOfSuggestionRows(suggestionsLength, true); const contentMinHeight = measureHeightOfSuggestionRows(suggestionsLength, false); - const isEnoughSpaceAboveForBig = windowHeight - bottomValue - contentMaxHeight > CONST.AUTO_COMPLETE_SUGGESTER.SUGGESTION_BOX_MAX_SAFE_DISTANCE; - const isEnoughSpaceAboveForSmall = windowHeight - bottomValue - contentMinHeight > CONST.AUTO_COMPLETE_SUGGESTER.SUGGESTION_BOX_MAX_SAFE_DISTANCE; + let bottomValue = windowHeight - (cursorCoordinates.y - scrollValue + y) - keyboardHeight; + const widthValue = isSmallScreenWidth ? width : CONST.AUTO_COMPLETE_SUGGESTER.BIG_SCREEN_SUGGESTION_WIDTH; + + const isEnoughSpaceToRenderMenuAboveForBig = isEnoughSpaceToRenderMenuAboveCursor({y, cursorCoordinates, scrollValue, contentHeight: contentMaxHeight, topInset}); + const isEnoughSpaceToRenderMenuAboveForSmall = isEnoughSpaceToRenderMenuAboveCursor({y, cursorCoordinates, scrollValue, contentHeight: contentMinHeight, topInset}); - const newLeftValue = isSmallScreenWidth ? x : leftValueForBigScreen; + const newLeftOffset = isSmallScreenWidth ? x : bigScreenLeftOffset; // If the suggested word is longer than 150 (approximately half the width of the suggestion popup), then adjust a new position of popup - const isAdjustmentNeeded = Math.abs(prevLeftValue.current - leftValueForBigScreen) > 150; + const isAdjustmentNeeded = Math.abs(prevLeftValue.current - bigScreenLeftOffset) > 150; if (isInitialRender.current || isAdjustmentNeeded) { - isSuggestionAboveRef.current = isSuggestionRenderedAbove(isEnoughSpaceAboveForBig, isEnoughSpaceAboveForSmall); - leftValue.current = newLeftValue; + isSuggestionMenuAboveRef.current = isSuggestionMenuRenderedAbove(isEnoughSpaceToRenderMenuAboveForBig, isEnoughSpaceToRenderMenuAboveForSmall); + leftValue.current = newLeftOffset; isInitialRender.current = false; - prevLeftValue.current = newLeftValue; + prevLeftValue.current = newLeftOffset; } let measuredHeight = 0; - if (isSuggestionAboveRef.current && isEnoughSpaceAboveForBig) { + if (isSuggestionMenuAboveRef.current && isEnoughSpaceToRenderMenuAboveForBig) { // calculation for big suggestion box above the cursor measuredHeight = measureHeightOfSuggestionRows(suggestionsLength, true); - } else if (isSuggestionAboveRef.current && isEnoughSpaceAboveForSmall) { + } else if (isSuggestionMenuAboveRef.current && isEnoughSpaceToRenderMenuAboveForSmall) { // calculation for small suggestion box above the cursor measuredHeight = measureHeightOfSuggestionRows(suggestionsLength, false); } else { // calculation for big suggestion box below the cursor measuredHeight = measureHeightOfSuggestionRows(suggestionsLength, true); - bottomValue = windowHeight - y - cursorCoordinates.y + scrollValue - measuredHeight - CONST.AUTO_COMPLETE_SUGGESTER.SUGGESTION_ROW_HEIGHT; + bottomValue = windowHeight - y - cursorCoordinates.y + scrollValue - measuredHeight - CONST.AUTO_COMPLETE_SUGGESTER.SUGGESTION_ROW_HEIGHT - keyboardHeight; } setSuggestionHeight(measuredHeight); setContainerState({ left: leftValue.current, bottom: bottomValue, width: widthValue, + cursorCoordinates, }); }); - }, [measureParentContainerAndReportCursor, windowHeight, windowWidth, keyboardHeight, isSmallScreenWidth, suggestionsLength, bottomInset]); + }, [measureParentContainerAndReportCursor, windowHeight, windowWidth, keyboardHeight, isSmallScreenWidth, suggestionsLength, bottomInset, topInset]); - if (containerState.width === 0 && containerState.left === 0 && containerState.bottom === 0) { + if ((containerState.width === 0 && containerState.left === 0 && containerState.bottom === 0) || (containerState.cursorCoordinates.x === 0 && containerState.cursorCoordinates.y === 0)) { return null; } return ( diff --git a/src/libs/focusComposerWithDelay/types.ts b/src/libs/focusComposerWithDelay/types.ts index 4cd2f785f2bc..97a1298e8c7a 100644 --- a/src/libs/focusComposerWithDelay/types.ts +++ b/src/libs/focusComposerWithDelay/types.ts @@ -3,6 +3,8 @@ import type {TextInput} from 'react-native'; type Selection = { start: number; end: number; + positionX?: number; + positionY?: number; }; type FocusComposerWithDelay = (shouldDelay?: boolean, forcedSelectionRange?: Selection) => void; diff --git a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx index 6cc55c825983..46abfba93bf5 100644 --- a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx +++ b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx @@ -748,6 +748,8 @@ function ComposerWithSuggestions( }, []); useEffect(() => { + // We use the tag to store the native ID of the text input. Later, we use it in onSelectionChange to pick up the proper text input data. + tag.value = findNodeHandle(textInputRef.current) ?? -1; // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps }, []); diff --git a/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx b/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx index bf82a28dd48a..9fede8068e64 100644 --- a/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx +++ b/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx @@ -60,6 +60,7 @@ type SuggestionsRef = { updateShouldShowSuggestionMenuToFalse: (shouldShowSuggestionMenu?: boolean) => void; setShouldBlockSuggestionCalc: (shouldBlock: boolean) => void; getSuggestions: () => Mention[] | Emoji[]; + getIsSuggestionsMenuVisible: () => boolean; }; type ReportActionComposeOnyxProps = { diff --git a/src/pages/home/report/ReportActionCompose/SuggestionEmoji.tsx b/src/pages/home/report/ReportActionCompose/SuggestionEmoji.tsx index 9b04fd7df4dc..8d5a544afd42 100644 --- a/src/pages/home/report/ReportActionCompose/SuggestionEmoji.tsx +++ b/src/pages/home/report/ReportActionCompose/SuggestionEmoji.tsx @@ -150,7 +150,7 @@ function SuggestionEmoji( */ const calculateEmojiSuggestion = useCallback( (selectionStart?: number, selectionEnd?: number) => { - if (selectionStart !== selectionEnd || !selectionEnd || shouldBlockCalc.current || !value) { + if (selectionStart !== selectionEnd || !selectionEnd || shouldBlockCalc.current || !value || (selectionStart === 0 && selectionEnd === 0)) { shouldBlockCalc.current = false; resetSuggestions(); return; @@ -181,6 +181,7 @@ function SuggestionEmoji( if (!isComposerFocused) { return; } + calculateEmojiSuggestion(selection.start, selection.end); }, [selection, calculateEmojiSuggestion, isComposerFocused]); @@ -193,6 +194,8 @@ function SuggestionEmoji( const getSuggestions = useCallback(() => suggestionValues.suggestedEmojis, [suggestionValues]); + const getIsSuggestionsMenuVisible = useCallback(() => isEmojiSuggestionsMenuVisible, [isEmojiSuggestionsMenuVisible]); + useImperativeHandle( ref, () => ({ @@ -201,8 +204,9 @@ function SuggestionEmoji( setShouldBlockSuggestionCalc, updateShouldShowSuggestionMenuToFalse, getSuggestions, + getIsSuggestionsMenuVisible, }), - [resetSuggestions, setShouldBlockSuggestionCalc, triggerHotkeyActions, updateShouldShowSuggestionMenuToFalse, getSuggestions], + [resetSuggestions, setShouldBlockSuggestionCalc, triggerHotkeyActions, updateShouldShowSuggestionMenuToFalse, getSuggestions, getIsSuggestionsMenuVisible], ); if (!isEmojiSuggestionsMenuVisible) { diff --git a/src/pages/home/report/ReportActionCompose/SuggestionMention.tsx b/src/pages/home/report/ReportActionCompose/SuggestionMention.tsx index 129c8c822d74..86a05bad1994 100644 --- a/src/pages/home/report/ReportActionCompose/SuggestionMention.tsx +++ b/src/pages/home/report/ReportActionCompose/SuggestionMention.tsx @@ -408,6 +408,7 @@ function SuggestionMention( ); const getSuggestions = useCallback(() => suggestionValues.suggestedMentions, [suggestionValues]); + const getIsSuggestionsMenuVisible = useCallback(() => isMentionSuggestionsMenuVisible, [isMentionSuggestionsMenuVisible]); useImperativeHandle( ref, @@ -417,8 +418,9 @@ function SuggestionMention( setShouldBlockSuggestionCalc, updateShouldShowSuggestionMenuToFalse, getSuggestions, + getIsSuggestionsMenuVisible, }), - [resetSuggestions, setShouldBlockSuggestionCalc, triggerHotkeyActions, updateShouldShowSuggestionMenuToFalse, getSuggestions], + [resetSuggestions, setShouldBlockSuggestionCalc, triggerHotkeyActions, updateShouldShowSuggestionMenuToFalse, getSuggestions, getIsSuggestionsMenuVisible], ); if (!isMentionSuggestionsMenuVisible) { diff --git a/src/pages/home/report/ReportActionCompose/Suggestions.tsx b/src/pages/home/report/ReportActionCompose/Suggestions.tsx index f82b38c3e154..158c60b0e89a 100644 --- a/src/pages/home/report/ReportActionCompose/Suggestions.tsx +++ b/src/pages/home/report/ReportActionCompose/Suggestions.tsx @@ -124,6 +124,11 @@ function Suggestions( suggestionEmojiRef.current?.setShouldBlockSuggestionCalc(shouldBlock); suggestionMentionRef.current?.setShouldBlockSuggestionCalc(shouldBlock); }, []); + const getIsSuggestionsMenuVisible = useCallback((): boolean => { + const isEmojiVisible = suggestionEmojiRef.current?.getIsSuggestionsMenuVisible() ?? false; + const isSuggestionVisible = suggestionMentionRef.current?.getIsSuggestionsMenuVisible() ?? false; + return isEmojiVisible || isSuggestionVisible; + }, []); useImperativeHandle( ref, @@ -134,8 +139,9 @@ function Suggestions( updateShouldShowSuggestionMenuToFalse, setShouldBlockSuggestionCalc, getSuggestions, + getIsSuggestionsMenuVisible, }), - [onSelectionChange, resetSuggestions, setShouldBlockSuggestionCalc, triggerHotkeyActions, updateShouldShowSuggestionMenuToFalse, getSuggestions], + [onSelectionChange, resetSuggestions, setShouldBlockSuggestionCalc, triggerHotkeyActions, updateShouldShowSuggestionMenuToFalse, getSuggestions, getIsSuggestionsMenuVisible], ); useEffect(() => { diff --git a/src/pages/home/report/ReportActionItemMessageEdit.tsx b/src/pages/home/report/ReportActionItemMessageEdit.tsx index 6618b20a5a6a..40e9a4c6bf41 100644 --- a/src/pages/home/report/ReportActionItemMessageEdit.tsx +++ b/src/pages/home/report/ReportActionItemMessageEdit.tsx @@ -1,11 +1,15 @@ import lodashDebounce from 'lodash/debounce'; import type {ForwardedRef} from 'react'; import React, {forwardRef, useCallback, useEffect, useMemo, useRef, useState} from 'react'; -import type {NativeSyntheticEvent, TextInput, TextInputFocusEventData, TextInputKeyPressEventData} from 'react-native'; -import {InteractionManager, Keyboard, View} from 'react-native'; +import {findNodeHandle, InteractionManager, Keyboard, View} from 'react-native'; +import type {MeasureInWindowOnSuccessCallback, NativeSyntheticEvent, TextInput, TextInputFocusEventData, TextInputKeyPressEventData, TextInputScrollEventData} from 'react-native'; +import {useFocusedInputHandler} from 'react-native-keyboard-controller'; import {useOnyx} from 'react-native-onyx'; +import {useSharedValue} from 'react-native-reanimated'; import type {Emoji} from '@assets/emojis/types'; +import type {MeasureParentContainerAndCursorCallback} from '@components/AutoCompleteSuggestions/types'; import Composer from '@components/Composer'; +import type {TextSelection} from '@components/Composer/types'; import EmojiPickerButton from '@components/EmojiPicker/EmojiPickerButton'; import ExceededCommentLength from '@components/ExceededCommentLength'; import Icon from '@components/Icon'; @@ -42,6 +46,10 @@ 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 getCursorPosition from './ReportActionCompose/getCursorPosition'; +import getScrollPosition from './ReportActionCompose/getScrollPosition'; +import type {SuggestionsRef} from './ReportActionCompose/ReportActionCompose'; +import Suggestions from './ReportActionCompose/Suggestions'; import shouldUseEmojiPickerSelection from './shouldUseEmojiPickerSelection'; type ReportActionItemMessageEditProps = { @@ -81,12 +89,17 @@ function ReportActionItemMessageEdit( const theme = useTheme(); const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); + const containerRef = useRef(null); const reportScrollManager = useReportScrollManager(); const {translate, preferredLocale} = useLocalize(); const {isKeyboardShown} = useKeyboardState(); const {isSmallScreenWidth} = useWindowDimensions(); const prevDraftMessage = usePrevious(draftMessage); - + const suggestionsRef = useRef(null); + const mobileInputScrollPosition = useRef(0); + const cursorPositionValue = useSharedValue({x: 0, y: 0}); + const tag = useSharedValue(-1); + const isInitialMount = useRef(true); const emojisPresentBefore = useRef([]); const [draft, setDraft] = useState(() => { if (draftMessage) { @@ -94,7 +107,7 @@ function ReportActionItemMessageEdit( } return draftMessage; }); - const [selection, setSelection] = useState({start: draft.length, end: draft.length}); + const [selection, setSelection] = useState({start: draft.length, end: draft.length, positionX: 0, positionY: 0}); const [isFocused, setIsFocused] = useState(false); const {hasExceededMaxCommentLength, validateCommentMaxLength} = useHandleExceedMaxCommentLength(); const [modal, setModal] = useState({ @@ -123,11 +136,6 @@ function ReportActionItemMessageEdit( setDraft(draftMessage); }, [draftMessage, action, prevDraftMessage]); - useEffect(() => { - // required for keeping last state of isFocused variable - isFocusedRef.current = isFocused; - }, [isFocused]); - useEffect(() => { InputFocus.composerFocusKeepFocusOn(textInputRef.current as HTMLElement, isFocused, modal, onyxFocused); }, [isFocused, modal, onyxFocused]); @@ -165,25 +173,32 @@ function ReportActionItemMessageEdit( ); useEffect( - () => () => { - InputFocus.callback(() => setIsFocused(false)); - InputFocus.inputFocusChange(false); - - // Skip if the current report action is not active - if (!isActive()) { + () => { + if (isInitialMount.current) { + isInitialMount.current = false; return; } - if (EmojiPickerAction.isActive(action.reportActionID)) { - EmojiPickerAction.clearActive(); - } - if (ReportActionContextMenu.isActiveReportAction(action.reportActionID)) { - ReportActionContextMenu.clearActiveReportAction(); - } + return () => { + InputFocus.callback(() => setIsFocused(false)); + InputFocus.inputFocusChange(false); - // Show the main composer when the focused message is deleted from another client - // to prevent the main composer stays hidden until we swtich to another chat. - setShouldShowComposeInputKeyboardAware(true); + // Skip if the current report action is not active + if (!isActive()) { + return; + } + + if (EmojiPickerAction.isActive(action.reportActionID)) { + EmojiPickerAction.clearActive(); + } + if (ReportActionContextMenu.isActiveReportAction(action.reportActionID)) { + ReportActionContextMenu.clearActiveReportAction(); + } + + // Show the main composer when the focused message is deleted from another client + // to prevent the main composer stays hidden until we swtich to another chat. + setShouldShowComposeInputKeyboardAware(true); + }; }, // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps -- this cleanup needs to be called only on unmount [action.reportActionID], @@ -248,10 +263,12 @@ function ReportActionItemMessageEdit( setDraft(newDraft); if (newDraftInput !== newDraft) { - const position = Math.max(selection.end + (newDraft.length - draftRef.current.length), cursorPosition ?? 0); + const position = Math.max((selection?.end ?? 0) + (newDraft.length - draftRef.current.length), cursorPosition ?? 0); setSelection({ start: position, end: position, + positionX: 0, + positionY: 0, }); } @@ -318,6 +335,8 @@ function ReportActionItemMessageEdit( const newSelection = { start: selection.start + emoji.length + CONST.SPACE_LENGTH, end: selection.start + emoji.length + CONST.SPACE_LENGTH, + positionX: 0, + positionY: 0, }; setSelection(newSelection); @@ -329,6 +348,21 @@ function ReportActionItemMessageEdit( updateDraft(ComposerUtils.insertText(draft, selection, `${emoji} `)); }; + const hideSuggestionMenu = useCallback(() => { + if (!suggestionsRef.current) { + return; + } + suggestionsRef.current.updateShouldShowSuggestionMenuToFalse(false); + }, [suggestionsRef]); + const onSaveScrollAndHideSuggestionMenu = useCallback( + (e: NativeSyntheticEvent) => { + mobileInputScrollPosition.current = e?.nativeEvent?.contentOffset?.y ?? 0; + + hideSuggestionMenu(); + }, + [hideSuggestionMenu], + ); + /** * Key event handlers that short cut to saving/canceling. * @@ -340,6 +374,17 @@ function ReportActionItemMessageEdit( return; } const keyEvent = e as KeyboardEvent; + const isSuggestionsMenuVisible = suggestionsRef.current?.getIsSuggestionsMenuVisible(); + + if (isSuggestionsMenuVisible && keyEvent.key === CONST.KEYBOARD_SHORTCUTS.ENTER.shortcutKey) { + suggestionsRef.current?.triggerHotkeyActions(keyEvent); + return; + } + if (keyEvent.key === CONST.KEYBOARD_SHORTCUTS.ESCAPE.shortcutKey && isSuggestionsMenuVisible) { + e.preventDefault(); + hideSuggestionMenu(); + return; + } if (keyEvent.key === CONST.KEYBOARD_SHORTCUTS.ENTER.shortcutKey && !keyEvent.shiftKey) { e.preventDefault(); publishDraft(); @@ -348,7 +393,59 @@ function ReportActionItemMessageEdit( deleteDraft(); } }, - [deleteDraft, isKeyboardShown, isSmallScreenWidth, publishDraft], + [deleteDraft, hideSuggestionMenu, isKeyboardShown, isSmallScreenWidth, publishDraft], + ); + + const measureContainer = useCallback( + (callback: MeasureInWindowOnSuccessCallback) => { + if (!containerRef.current) { + return; + } + containerRef.current.measureInWindow(callback); + }, + // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps + [isFocused], + ); + + const measureParentContainerAndReportCursor = useCallback( + (callback: MeasureParentContainerAndCursorCallback) => { + const {scrollValue} = getScrollPosition({mobileInputScrollPosition, textInputRef}); + const {x: xPosition, y: yPosition} = getCursorPosition({positionOnMobile: cursorPositionValue.value, positionOnWeb: selection}); + measureContainer((x, y, width, height) => { + callback({ + x, + y, + width, + height, + scrollValue, + cursorCoordinates: {x: xPosition, y: yPosition}, + }); + }); + }, + [cursorPositionValue.value, measureContainer, selection], + ); + + useEffect(() => { + // We use the tag to store the native ID of the text input. Later, we use it in onSelectionChange to pick up the proper text input data. + + // eslint-disable-next-line react-compiler/react-compiler + tag.value = findNodeHandle(textInputRef.current) ?? -1; + // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps + }, []); + useFocusedInputHandler( + { + onSelectionChange: (event) => { + 'worklet'; + + if (event.target === tag.value) { + cursorPositionValue.value = { + x: event.selection.end.x, + y: event.selection.end.y, + }; + } + }, + }, + [], ); /** @@ -360,9 +457,21 @@ function ReportActionItemMessageEdit( validateCommentMaxLength(draft, {reportID}); }, [draft, reportID, validateCommentMaxLength]); + useEffect(() => { + // required for keeping last state of isFocused variable + isFocusedRef.current = isFocused; + + if (!isFocused) { + hideSuggestionMenu(); + } + }, [isFocused, hideSuggestionMenu]); + return ( <> - + setSelection(e.nativeEvent.selection)} isGroupPolicyReport={isGroupPolicyReport} + shouldCalculateCaretPosition + onScroll={onSaveScrollAndHideSuggestionMenu} /> + + +