diff --git a/src/components/Composer/index.tsx b/src/components/Composer/index.tsx index 7a4512ad5aea..72116a346c00 100755 --- a/src/components/Composer/index.tsx +++ b/src/components/Composer/index.tsx @@ -19,9 +19,7 @@ import * as Browser from '@libs/Browser'; import updateIsFullComposerAvailable from '@libs/ComposerUtils/updateIsFullComposerAvailable'; import * as EmojiUtils from '@libs/EmojiUtils'; import * as FileUtils from '@libs/fileDownload/FileUtils'; -import focusComposerWithDelay from '@libs/focusComposerWithDelay'; import isEnterWhileComposition from '@libs/KeyboardShortcut/isEnterWhileComposition'; -import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager'; import CONST from '@src/CONST'; import type {ComposerProps} from './types'; @@ -72,7 +70,6 @@ function Composer( start: 0, end: 0, }, - isReportActionCompose = false, isComposerFullSize = false, shouldContainScroll = true, isGroupPolicyReport = false, @@ -277,14 +274,6 @@ function Composer( useEffect(() => { setIsRendered(true); - - return () => { - if (isReportActionCompose) { - return; - } - ReportActionComposeFocusManager.clear(); - }; - // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps }, []); const clear = useCallback(() => { @@ -408,17 +397,6 @@ function Composer( }} disabled={isDisabled} onKeyPress={handleKeyPress} - onFocus={(e) => { - ReportActionComposeFocusManager.onComposerFocus(() => { - if (!textInput.current) { - return; - } - - focusComposerWithDelay(textInput.current)(true); - }); - - props.onFocus?.(e); - }} /> {shouldCalculateCaretPosition && renderElementForCaretPosition} diff --git a/src/components/Composer/types.ts b/src/components/Composer/types.ts index 8287e2de3f2d..41138970c547 100644 --- a/src/components/Composer/types.ts +++ b/src/components/Composer/types.ts @@ -69,9 +69,6 @@ type ComposerProps = Omit & { /** Function to check whether composer is covered up or not */ checkComposerVisibility?: () => boolean; - /** Whether this is the report action compose */ - isReportActionCompose?: boolean; - /** Whether the sull composer is open */ isComposerFullSize?: boolean; diff --git a/src/components/EmojiPicker/EmojiPickerButton.tsx b/src/components/EmojiPicker/EmojiPickerButton.tsx index 6e0944e5a913..412c6655bdb0 100644 --- a/src/components/EmojiPicker/EmojiPickerButton.tsx +++ b/src/components/EmojiPicker/EmojiPickerButton.tsx @@ -1,5 +1,6 @@ import {useIsFocused} from '@react-navigation/native'; import React, {memo, useEffect, useRef} from 'react'; +import type {GestureResponderEvent} from 'react-native'; import Icon from '@components/Icon'; import * as Expensicons from '@components/Icon/Expensicons'; import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeedback'; @@ -21,6 +22,9 @@ type EmojiPickerButtonProps = { /** Unique id for emoji picker */ emojiPickerID?: string; + /** A callback function when the button is pressed */ + onPress?: (event?: GestureResponderEvent | KeyboardEvent) => void; + /** Emoji popup anchor offset shift vertical */ shiftVertical?: number; @@ -29,7 +33,7 @@ type EmojiPickerButtonProps = { onEmojiSelected: EmojiPickerAction.OnEmojiSelected; }; -function EmojiPickerButton({isDisabled = false, id = '', emojiPickerID = '', shiftVertical = 0, onModalHide, onEmojiSelected}: EmojiPickerButtonProps) { +function EmojiPickerButton({isDisabled = false, id = '', emojiPickerID = '', shiftVertical = 0, onPress, onModalHide, onEmojiSelected}: EmojiPickerButtonProps) { const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); const emojiPopoverAnchor = useRef(null); @@ -44,7 +48,7 @@ function EmojiPickerButton({isDisabled = false, id = '', emojiPickerID = '', shi ref={emojiPopoverAnchor} style={({hovered, pressed}) => [styles.chatItemEmojiButton, StyleUtils.getButtonBackgroundColorStyle(getButtonState(hovered, pressed))]} disabled={isDisabled} - onPress={() => { + onPress={(e) => { if (!isFocused) { return; } @@ -64,6 +68,7 @@ function EmojiPickerButton({isDisabled = false, id = '', emojiPickerID = '', shi } else { EmojiPickerAction.emojiPickerRef.current.hideEmojiPicker(); } + onPress?.(e); }} id={id} accessibilityLabel={translate('reportActionCompose.emoji')} diff --git a/src/libs/ReportActionComposeFocusManager.ts b/src/libs/ReportActionComposeFocusManager.ts index 2dfa81d89a20..cf97524fb3a3 100644 --- a/src/libs/ReportActionComposeFocusManager.ts +++ b/src/libs/ReportActionComposeFocusManager.ts @@ -10,10 +10,10 @@ type FocusCallback = (shouldFocusForNonBlurInputOnTapOutside?: boolean) => void; const composerRef: MutableRefObject = React.createRef(); const editComposerRef = React.createRef(); -// There are two types of composer: general composer (edit composer) and main composer. -// The general composer callback will take priority if it exists. +// There are two types of focus callbacks: priority and general +// Priority callback would take priority if it existed +let priorityFocusCallback: FocusCallback | null = null; let focusCallback: FocusCallback | null = null; -let mainComposerFocusCallback: FocusCallback | null = null; /** * Register a callback to be called when focus is requested. @@ -21,9 +21,9 @@ let mainComposerFocusCallback: FocusCallback | null = null; * * @param callback callback to register */ -function onComposerFocus(callback: FocusCallback | null, isMainComposer = false) { - if (isMainComposer) { - mainComposerFocusCallback = callback; +function onComposerFocus(callback: FocusCallback | null, isPriorityCallback = false) { + if (isPriorityCallback) { + priorityFocusCallback = callback; } else { focusCallback = callback; } @@ -39,24 +39,26 @@ function focus(shouldFocusForNonBlurInputOnTapOutside?: boolean) { return; } - if (typeof focusCallback !== 'function') { - if (typeof mainComposerFocusCallback !== 'function') { - return; - } + if (typeof priorityFocusCallback !== 'function' && typeof focusCallback !== 'function') { + return; + } - mainComposerFocusCallback(shouldFocusForNonBlurInputOnTapOutside); + if (typeof priorityFocusCallback === 'function') { + priorityFocusCallback(shouldFocusForNonBlurInputOnTapOutside); return; } - focusCallback(); + if (typeof focusCallback === 'function') { + focusCallback(); + } } /** * Clear the registered focus callback */ -function clear(isMainComposer = false) { - if (isMainComposer) { - mainComposerFocusCallback = null; +function clear(isPriorityCallback = false) { + if (isPriorityCallback) { + priorityFocusCallback = null; } else { focusCallback = null; } diff --git a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx index 21521396f347..3be7b0acd48c 100644 --- a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx +++ b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx @@ -551,16 +551,22 @@ function ComposerWithSuggestions( focusComposerWithDelay(textInputRef.current)(shouldDelay); }, []); - const setUpComposeFocusManager = useCallback(() => { - // This callback is used in the contextMenuActions to manage giving focus back to the compose input. - ReportActionComposeFocusManager.onComposerFocus((shouldFocusForNonBlurInputOnTapOutside = false) => { - if ((!willBlurTextInputOnTapOutside && !shouldFocusForNonBlurInputOnTapOutside) || !isFocused) { - return; - } + /** + * Set focus callback + * @param shouldTakeOverFocus - Whether this composer should gain focus priority + */ + const setUpComposeFocusManager = useCallback( + (shouldTakeOverFocus = false) => { + ReportActionComposeFocusManager.onComposerFocus((shouldFocusForNonBlurInputOnTapOutside = false) => { + if ((!willBlurTextInputOnTapOutside && !shouldFocusForNonBlurInputOnTapOutside) || !isFocused) { + return; + } - focus(true); - }, true); - }, [focus, isFocused]); + focus(true); + }, shouldTakeOverFocus); + }, + [focus, isFocused], + ); /** * Check if the composer is visible. Returns true if the composer is not covered up by emoji picker or menu. False otherwise. @@ -623,7 +629,7 @@ function ComposerWithSuggestions( setUpComposeFocusManager(); return () => { - ReportActionComposeFocusManager.clear(true); + ReportActionComposeFocusManager.clear(); KeyDownListener.removeKeyDownPressListener(focusComposerOnKeyPress); unsubscribeNavigationBlur(); @@ -756,7 +762,11 @@ function ComposerWithSuggestions( textAlignVertical="top" style={[styles.textInputCompose, isComposerFullSize ? styles.textInputFullCompose : styles.textInputCollapseCompose]} maxLines={maxComposerLines} - onFocus={onFocus} + onFocus={() => { + // The last composer that had focus should re-gain focus + setUpComposeFocusManager(true); + onFocus(); + }} onBlur={onBlur} onClick={setShouldBlockSuggestionCalcToFalse} onPasteFile={(file) => { @@ -765,7 +775,6 @@ function ComposerWithSuggestions( }} onClear={onClear} isDisabled={isBlockedFromConcierge || disabled} - isReportActionCompose selection={selection} onSelectionChange={onSelectionChange} isFullComposerAvailable={isFullComposerAvailable} diff --git a/src/pages/home/report/ReportActionItemMessageEdit.tsx b/src/pages/home/report/ReportActionItemMessageEdit.tsx index 73bea7060b36..d1eb78bcc00e 100644 --- a/src/pages/home/report/ReportActionItemMessageEdit.tsx +++ b/src/pages/home/report/ReportActionItemMessageEdit.tsx @@ -173,6 +173,29 @@ function ReportActionItemMessageEdit( [action.reportActionID], ); + /** + * Focus the composer text input + * @param shouldDelay - Impose delay before focusing the composer + */ + const focus = useCallback((shouldDelay = false, forcedSelectionRange?: Selection) => { + focusComposerWithDelay(textInputRef.current)(shouldDelay, forcedSelectionRange); + }, []); + + // Take over focus priority + const setUpComposeFocusManager = useCallback(() => { + ReportActionComposeFocusManager.onComposerFocus(() => { + focus(true, emojiPickerSelectionRef.current ? {...emojiPickerSelectionRef.current} : undefined); + }, true); + }, [focus]); + + useEffect( + // Remove focus callback on unmount to avoid stale callbacks + () => () => { + ReportActionComposeFocusManager.clear(true); + }, + [], + ); + useEffect( () => { if (isInitialMount.current) { @@ -274,8 +297,9 @@ function ReportActionItemMessageEdit( Report.deleteReportActionDraft(reportID, action); if (isActive()) { - ReportActionComposeFocusManager.clear(); - ReportActionComposeFocusManager.focus(); + ReportActionComposeFocusManager.clear(true); + // Wait for report action compose re-mounting on mWeb + InteractionManager.runAfterInteractions(() => ReportActionComposeFocusManager.focus()); } // Scroll to the last comment after editing to make sure the whole comment is clearly visible in the report. @@ -424,11 +448,6 @@ function ReportActionItemMessageEdit( [], ); - /** - * Focus the composer text input - */ - const focus = focusComposerWithDelay(textInputRef.current); - useEffect(() => { validateCommentMaxLength(draft, {reportID}); }, [draft, reportID, validateCommentMaxLength]); @@ -503,6 +522,8 @@ function ReportActionItemMessageEdit( }); }); setShouldShowComposeInputKeyboardAware(false); + // The last composer that had focus should re-gain focus + setUpComposeFocusManager(); // Clear active report action when another action gets focused if (!EmojiPickerAction.isActive(action.reportActionID)) { @@ -546,11 +567,12 @@ function ReportActionItemMessageEdit( { - focus(true, emojiPickerSelectionRef.current ? {...emojiPickerSelectionRef.current} : undefined); + ReportActionComposeFocusManager.focus(); }} onEmojiSelected={addEmojiToTextBox} id={emojiButtonID} emojiPickerID={action.reportActionID} + onPress={setUpComposeFocusManager} />