From 4952dce763166ec622b16c0d6df69086b1cfe230 Mon Sep 17 00:00:00 2001 From: ntdiary <2471314@gmail.com> Date: Wed, 29 Nov 2023 20:58:13 +0800 Subject: [PATCH 01/28] try to refactor the modal's refocus logic --- src/CONST.ts | 5 + .../AttachmentPicker/index.native.js | 8 +- src/components/Composer/index.js | 12 +- src/components/Modal/BaseModal.tsx | 43 ++-- src/components/Modal/ModalContent.js | 18 ++ src/components/Modal/ModalContent.web.js | 21 ++ src/components/Modal/index.android.tsx | 34 ++- src/components/Modal/modalPropTypes.js | 3 + src/components/Modal/types.ts | 9 + src/components/PopoverMenu/index.js | 2 + src/components/PopoverWithoutOverlay/index.js | 50 +++- .../GenericPressable/BaseGenericPressable.tsx | 2 + .../Pressable/GenericPressable/index.tsx | 2 + src/components/RNTextInput.tsx | 10 +- src/libs/Browser/index.ts | 6 +- src/libs/Browser/index.web.ts | 7 +- src/libs/Browser/types.ts | 4 +- src/libs/ComposerFocusManager.js | 243 +++++++++++++++++- .../Navigation/AppNavigator/AuthScreens.js | 6 +- .../AppNavigator/ModalStackNavigators.js | 1 + .../AppNavigator/RHPScreenOptions.js | 1 + .../getRootNavigatorScreenOptions.js | 1 + .../BaseReportActionContextMenu.js | 5 +- .../report/ContextMenu/ContextMenuActions.js | 3 + .../PopoverReportActionContextMenu.js | 5 + ...genericReportActionContextMenuPropTypes.js | 4 + .../AttachmentPickerWithMenuItems.js | 36 +-- .../ComposerWithSuggestions.js | 55 +--- .../composerWithSuggestionsProps.js | 3 - .../ReportActionCompose.js | 47 +--- .../report/ReportActionItemMessageEdit.js | 7 - src/pages/iou/WaypointEditor.js | 8 +- 32 files changed, 476 insertions(+), 185 deletions(-) create mode 100644 src/components/Modal/ModalContent.js create mode 100644 src/components/Modal/ModalContent.web.js diff --git a/src/CONST.ts b/src/CONST.ts index dd6eafc7f0e..9aaa7a0e802 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -670,6 +670,11 @@ const CONST = { RIGHT: 'right', }, POPOVER_MENU_PADDING: 8, + RESTORE_TYPE: { + DEFAULT: 'default', + DELETE: 'delete', + PRESERVE: 'preserve', + }, }, TIMING: { CALCULATE_MOST_RECENT_LAST_MODIFIED_ACTION: 'calc_most_recent_last_modified_action', diff --git a/src/components/AttachmentPicker/index.native.js b/src/components/AttachmentPicker/index.native.js index 5b955ee69dd..b192032bed7 100644 --- a/src/components/AttachmentPicker/index.native.js +++ b/src/components/AttachmentPicker/index.native.js @@ -103,6 +103,7 @@ const getDataForUpload = (fileData) => { function AttachmentPicker({type, children, shouldHideCameraOption}) { const styles = useThemeStyles(); const [isVisible, setIsVisible] = useState(false); + const [restoreFocusType, setRestoreFocusType] = useState(); const completeAttachmentSelection = useRef(); const onModalHide = useRef(); @@ -305,12 +306,14 @@ function AttachmentPicker({type, children, shouldHideCameraOption}) { return ( <> { close(); onCanceled.current(); }} isVisible={isVisible} anchorPosition={styles.createMenuPosition} + onModalShow={() => setRestoreFocusType(CONST.MODAL.RESTORE_TYPE.DEFAULT)} onModalHide={onModalHide.current} > @@ -319,7 +322,10 @@ function AttachmentPicker({type, children, shouldHideCameraOption}) { key={item.textTranslationKey} icon={item.icon} title={translate(item.textTranslationKey)} - onPress={() => selectItem(item)} + onPress={() => { + setRestoreFocusType(CONST.MODAL.RESTORE_TYPE.PRESERVE); + selectItem(item); + }} focused={focusedIndex === menuIndex} /> ))} diff --git a/src/components/Composer/index.js b/src/components/Composer/index.js index cbf8a6e40ab..b53294f2947 100755 --- a/src/components/Composer/index.js +++ b/src/components/Composer/index.js @@ -452,16 +452,10 @@ function Composer({ disabled={isDisabled} onKeyPress={handleKeyPress} onFocus={(e) => { - ReportActionComposeFocusManager.onComposerFocus(() => { - if (!textInput.current) { - return; - } - - textInput.current.focus(); - }); - if (props.onFocus) { - props.onFocus(e); + if (!props.onFocus) { + return; } + props.onFocus(e); }} /> {shouldCalculateCaretPosition && renderElementForCaretPosition} diff --git a/src/components/Modal/BaseModal.tsx b/src/components/Modal/BaseModal.tsx index 95a7f3adc27..84971100dbf 100644 --- a/src/components/Modal/BaseModal.tsx +++ b/src/components/Modal/BaseModal.tsx @@ -1,4 +1,4 @@ -import React, {forwardRef, useCallback, useEffect, useMemo, useRef} from 'react'; +import React, {forwardRef, useCallback, useEffect, useImperativeHandle, useMemo, useRef} from 'react'; import {View} from 'react-native'; import ReactNativeModal from 'react-native-modal'; import {useSafeAreaInsets} from 'react-native-safe-area-context'; @@ -13,7 +13,8 @@ import useThemeStyles from '@styles/useThemeStyles'; import variables from '@styles/variables'; import * as Modal from '@userActions/Modal'; import CONST from '@src/CONST'; -import BaseModalProps from './types'; +import ModalContent from './ModalContent'; +import BaseModalProps, {ModalRef} from './types'; function BaseModal( { @@ -36,10 +37,11 @@ function BaseModal( animationOutTiming, statusBarTranslucent = true, onLayout, + restoreFocusType, avoidKeyboard = false, children, }: BaseModalProps, - ref: React.ForwardedRef, + ref: React.ForwardedRef, ) { const theme = useTheme(); const styles = useThemeStyles(); @@ -50,6 +52,13 @@ function BaseModal( const isVisibleRef = useRef(isVisible); const wasVisible = usePrevious(isVisible); + const modalId = useMemo(() => ComposerFocusManager.getId(), []); + + const saveFocusState = () => { + ComposerFocusManager.saveFocusState(modalId); + ComposerFocusManager.resetReadyToFocus(modalId); + }; + /** * Hides modal * @param callHideCallback - Should we call the onModalHide callback @@ -64,11 +73,9 @@ function BaseModal( onModalHide(); } Modal.onModalDidClose(); - if (!fullscreen) { - ComposerFocusManager.setReadyToFocus(); - } + ComposerFocusManager.tryRestoreFocusAfterClosedCompletely(modalId, restoreFocusType); }, - [shouldSetModalVisibility, onModalHide, fullscreen], + [shouldSetModalVisibility, onModalHide, restoreFocusType, modalId], ); useEffect(() => { @@ -116,9 +123,18 @@ function BaseModal( }; const handleDismissModal = () => { - ComposerFocusManager.setReadyToFocus(); + ComposerFocusManager.setReadyToFocus(modalId); }; + useImperativeHandle( + ref, + () => ({ + removePromise: () => ComposerFocusManager.removePromise(modalId), + setReadyToFocus: () => ComposerFocusManager.setReadyToFocus(modalId), + }), + [modalId], + ); + const { modalStyle, modalContainerStyle, @@ -178,7 +194,7 @@ function BaseModal( onModalShow={handleShowModal} propagateSwipe={propagateSwipe} onModalHide={hideModal} - onModalWillShow={() => ComposerFocusManager.resetReadyToFocus()} + onModalWillShow={saveFocusState} onDismiss={handleDismissModal} onSwipeComplete={onClose} swipeDirection={swipeDirection} @@ -201,12 +217,9 @@ function BaseModal( onLayout={onLayout} avoidKeyboard={avoidKeyboard} > - - {children} - + + {children} + ); } diff --git a/src/components/Modal/ModalContent.js b/src/components/Modal/ModalContent.js new file mode 100644 index 00000000000..a1be2765cd8 --- /dev/null +++ b/src/components/Modal/ModalContent.js @@ -0,0 +1,18 @@ +import PropTypes from 'prop-types'; + +const propTypes = { + /** Modal contents */ + children: PropTypes.node.isRequired, + + /** called after modal content is dismissed */ + onDismiss: PropTypes.func, +}; + +function ModalContent({children}) { + return children; +} + +ModalContent.propTypes = propTypes; +ModalContent.displayName = 'ModalContent'; + +export default ModalContent; diff --git a/src/components/Modal/ModalContent.web.js b/src/components/Modal/ModalContent.web.js new file mode 100644 index 00000000000..5aaba122d0d --- /dev/null +++ b/src/components/Modal/ModalContent.web.js @@ -0,0 +1,21 @@ +import PropTypes from 'prop-types'; +import React from 'react'; + +const propTypes = { + /** Modal contents */ + children: PropTypes.node.isRequired, + + /** called after modal content is dismissed */ + onDismiss: PropTypes.func, +}; + +function ModalContent({children, onDismiss = () => {}}) { + // eslint-disable-next-line react-hooks/exhaustive-deps + React.useEffect(() => () => onDismiss(), []); + return children; +} + +ModalContent.propTypes = propTypes; +ModalContent.displayName = 'ModalContent'; + +export default ModalContent; diff --git a/src/components/Modal/index.android.tsx b/src/components/Modal/index.android.tsx index 2343cb4c70a..bd725549d1f 100644 --- a/src/components/Modal/index.android.tsx +++ b/src/components/Modal/index.android.tsx @@ -1,26 +1,36 @@ -import React from 'react'; +import React, {useRef} from 'react'; import {AppState} from 'react-native'; import withWindowDimensions from '@components/withWindowDimensions'; -import ComposerFocusManager from '@libs/ComposerFocusManager'; +import CONST from '@src/CONST'; import BaseModal from './BaseModal'; -import BaseModalProps from './types'; - -AppState.addEventListener('focus', () => { - ComposerFocusManager.setReadyToFocus(); -}); - -AppState.addEventListener('blur', () => { - ComposerFocusManager.resetReadyToFocus(); -}); +import BaseModalProps, {ModalRef} from './types'; // Only want to use useNativeDriver on Android. It has strange flashes issue on IOS // https://github.com/react-native-modal/react-native-modal#the-modal-flashes-in-a-weird-way-when-animating -function Modal({useNativeDriver = true, ...rest}: BaseModalProps) { +function Modal({useNativeDriver = true, restoreFocusType, onModalHide, ...rest}: BaseModalProps) { + const modalRef = useRef(null); + const hideModal = () => { + onModalHide(); + if (restoreFocusType && restoreFocusType !== CONST.MODAL.RESTORE_TYPE.DEFAULT) { + modalRef?.current?.removePromise(); + return; + } + const listener = AppState.addEventListener('focus', () => { + // TODO:del + console.debug('android is ready to focus'); + listener.remove(); + modalRef?.current?.setReadyToFocus(); + }); + }; + return ( {rest.children} diff --git a/src/components/Modal/modalPropTypes.js b/src/components/Modal/modalPropTypes.js index 84e610b694e..a4af2ca0e4d 100644 --- a/src/components/Modal/modalPropTypes.js +++ b/src/components/Modal/modalPropTypes.js @@ -66,6 +66,9 @@ const propTypes = { * */ hideModalContentWhileAnimating: PropTypes.bool, + /** how to re-focus after the modal is dismissed */ + restoreFocusType: PropTypes.oneOf(_.values(CONST.MODAL.RESTORE_TYPE)), + ...windowDimensionsPropTypes, }; diff --git a/src/components/Modal/types.ts b/src/components/Modal/types.ts index 3fa60e6ac76..118b7cd8ed6 100644 --- a/src/components/Modal/types.ts +++ b/src/components/Modal/types.ts @@ -61,6 +61,15 @@ type BaseModalProps = WindowDimensionsProps & * See: https://github.com/react-native-modal/react-native-modal/pull/116 * */ hideModalContentWhileAnimating?: boolean; + + /** how to re-focus after the modal is dismissed */ + restoreFocusType?: ValueOf; }; +type ModalRef = { + removePromise: () => void; + setReadyToFocus: () => void; +}; + +export type {ModalRef}; export default BaseModalProps; diff --git a/src/components/PopoverMenu/index.js b/src/components/PopoverMenu/index.js index 2106efb9d31..ceb8b3ea811 100644 --- a/src/components/PopoverMenu/index.js +++ b/src/components/PopoverMenu/index.js @@ -76,11 +76,13 @@ function PopoverMenu(props) { return ( { setFocusedIndex(-1); if (selectedItemIndex.current !== null) { diff --git a/src/components/PopoverWithoutOverlay/index.js b/src/components/PopoverWithoutOverlay/index.js index 8b9dd4ac7a6..0820c030da8 100644 --- a/src/components/PopoverWithoutOverlay/index.js +++ b/src/components/PopoverWithoutOverlay/index.js @@ -1,9 +1,12 @@ -import React from 'react'; +import React, {useMemo, useRef} from 'react'; import {View} from 'react-native'; import {SafeAreaInsetsContext} from 'react-native-safe-area-context'; +import {isFunction, isObject} from 'underscore'; +import ModalContent from '@components/Modal/ModalContent'; import {defaultProps, propTypes} from '@components/Popover/popoverPropTypes'; import {PopoverContext} from '@components/PopoverProvider'; import withWindowDimensions from '@components/withWindowDimensions'; +import ComposerFocusManager from '@libs/ComposerFocusManager'; import getModalStyles from '@styles/getModalStyles'; import * as StyleUtils from '@styles/StyleUtils'; import useThemeStyles from '@styles/useThemeStyles'; @@ -11,6 +14,8 @@ import * as Modal from '@userActions/Modal'; function Popover(props) { const styles = useThemeStyles(); + const modalId = useMemo(() => ComposerFocusManager.getId(), []); + const containerRef = useRef(); const {onOpen, close} = React.useContext(PopoverContext); const {modalStyle, modalContainerStyle, shouldAddTopSafeAreaMargin, shouldAddBottomSafeAreaMargin, shouldAddTopSafeAreaPadding, shouldAddBottomSafeAreaPadding} = getModalStyles( 'popover', @@ -34,6 +39,8 @@ function Popover(props) { anchorRef: props.anchorRef, }); removeOnClose = Modal.setCloseModal(() => props.onClose(props.anchorRef)); + ComposerFocusManager.saveFocusState(modalId, containerRef.current); + ComposerFocusManager.resetReadyToFocus(modalId); } else { props.onModalHide(); close(props.anchorRef); @@ -51,6 +58,15 @@ function Popover(props) { // eslint-disable-next-line react-hooks/exhaustive-deps }, [props.isVisible]); + const restoreFocusTypeRef = useRef(); + restoreFocusTypeRef.current = props.restoreFocusType; + const handleDismissContent = () => { + ComposerFocusManager.tryRestoreFocusAfterClosedCompletely(modalId, restoreFocusTypeRef.current); + // PopoverWithMeasuredContent delays the mounting of this popover, so here we also need to defer the restoration. + // In a follow-up PR, we can also consider how to improve the former. + setImmediate(() => ComposerFocusManager.setReadyToFocus(modalId)); + }; + if (!props.isVisible) { return null; } @@ -58,7 +74,15 @@ function Popover(props) { return ( { + containerRef.current = el; + if (isFunction(props.withoutOverlayRef)) { + props.withoutOverlayRef(el); + } else if (isObject(props.withoutOverlayRef)) { + // eslint-disable-next-line no-param-reassign + props.withoutOverlayRef.current = el; + } + }} > {(insets) => { @@ -85,16 +109,18 @@ function Popover(props) { insets, }); return ( - - {props.children} - + + + {props.children} + + ); }} diff --git a/src/components/Pressable/GenericPressable/BaseGenericPressable.tsx b/src/components/Pressable/GenericPressable/BaseGenericPressable.tsx index 1576fe18da5..d99930f09f9 100644 --- a/src/components/Pressable/GenericPressable/BaseGenericPressable.tsx +++ b/src/components/Pressable/GenericPressable/BaseGenericPressable.tsx @@ -3,6 +3,7 @@ import React, {ForwardedRef, forwardRef, useCallback, useEffect, useMemo} from ' import {GestureResponderEvent, Pressable, View, ViewStyle} from 'react-native'; import useSingleExecution from '@hooks/useSingleExecution'; import Accessibility from '@libs/Accessibility'; +import ComposerFocusManager from '@libs/ComposerFocusManager'; import HapticFeedback from '@libs/HapticFeedback'; import KeyboardShortcut from '@libs/KeyboardShortcut'; import styles from '@styles/styles'; @@ -107,6 +108,7 @@ function GenericPressable( ref.current?.blur(); } onPress(event); + ComposerFocusManager.removeFocusedElement(); Accessibility.moveAccessibilityFocus(nextFocusRef); }, diff --git a/src/components/Pressable/GenericPressable/index.tsx b/src/components/Pressable/GenericPressable/index.tsx index e050e23c8e9..aef4c0ec068 100644 --- a/src/components/Pressable/GenericPressable/index.tsx +++ b/src/components/Pressable/GenericPressable/index.tsx @@ -1,5 +1,6 @@ import React, {ForwardedRef, forwardRef} from 'react'; import {Role, View} from 'react-native'; +import ComposerFocusManager from '@libs/ComposerFocusManager'; import GenericPressable from './BaseGenericPressable'; import PressableProps from './types'; @@ -10,6 +11,7 @@ function WebGenericPressable({focusable = true, ...props}: PressableProps, ref: >>) { + const inputRef = React.useRef> | null>(null); + + useEffect(() => () => ComposerFocusManager.releaseElement(inputRef.current), []); + return ( { + if (refHandle) { + inputRef.current = refHandle; + } if (typeof ref !== 'function') { return; } diff --git a/src/libs/Browser/index.ts b/src/libs/Browser/index.ts index 9569d358380..deb02bfc25c 100644 --- a/src/libs/Browser/index.ts +++ b/src/libs/Browser/index.ts @@ -1,4 +1,4 @@ -import type {GetBrowser, IsMobile, IsMobileChrome, IsMobileSafari, IsSafari, OpenRouteInDesktopApp} from './types'; +import type {GetBrowser, IsFileCancelSupported, IsMobile, IsMobileChrome, IsMobileSafari, IsSafari, OpenRouteInDesktopApp} from './types'; const getBrowser: GetBrowser = () => ''; @@ -12,4 +12,6 @@ const isSafari: IsSafari = () => false; const openRouteInDesktopApp: OpenRouteInDesktopApp = () => {}; -export {getBrowser, isMobile, isMobileSafari, isSafari, isMobileChrome, openRouteInDesktopApp}; +const isFileCancelSupported: IsFileCancelSupported = () => true; + +export {getBrowser, isMobile, isMobileSafari, isSafari, isMobileChrome, openRouteInDesktopApp, isFileCancelSupported}; diff --git a/src/libs/Browser/index.web.ts b/src/libs/Browser/index.web.ts index 7e88e2bbd5f..c09c24e69b5 100644 --- a/src/libs/Browser/index.web.ts +++ b/src/libs/Browser/index.web.ts @@ -1,7 +1,7 @@ import CONFIG from '@src/CONFIG'; import CONST from '@src/CONST'; import ROUTES from '@src/ROUTES'; -import type {GetBrowser, IsMobile, IsMobileChrome, IsMobileSafari, IsSafari, OpenRouteInDesktopApp} from './types'; +import type {GetBrowser, IsFileCancelSupported, IsMobile, IsMobileChrome, IsMobileSafari, IsSafari, OpenRouteInDesktopApp} from './types'; /** * Fetch browser name from UA string @@ -101,4 +101,7 @@ const openRouteInDesktopApp: OpenRouteInDesktopApp = (shortLivedAuthToken = '', } }; -export {getBrowser, isMobile, isMobileSafari, isSafari, isMobileChrome, openRouteInDesktopApp}; +const isFileCancelSupported: IsFileCancelSupported = () => + // Chrome: 113 / Firefox: 91 / Safari 16.4 + false; +export {getBrowser, isMobile, isMobileSafari, isSafari, isMobileChrome, openRouteInDesktopApp, isFileCancelSupported}; diff --git a/src/libs/Browser/types.ts b/src/libs/Browser/types.ts index 2e84dca3fd7..9bf81a4b87b 100644 --- a/src/libs/Browser/types.ts +++ b/src/libs/Browser/types.ts @@ -10,4 +10,6 @@ type IsSafari = () => boolean; type OpenRouteInDesktopApp = (shortLivedAuthToken?: string, email?: string) => void; -export type {GetBrowser, IsMobile, IsMobileSafari, IsMobileChrome, IsSafari, OpenRouteInDesktopApp}; +type IsFileCancelSupported = () => boolean; + +export type {GetBrowser, IsMobile, IsMobileSafari, IsMobileChrome, IsSafari, OpenRouteInDesktopApp, IsFileCancelSupported}; diff --git a/src/libs/ComposerFocusManager.js b/src/libs/ComposerFocusManager.js index 569e165da96..79221a58a06 100644 --- a/src/libs/ComposerFocusManager.js +++ b/src/libs/ComposerFocusManager.js @@ -1,23 +1,246 @@ -let isReadyToFocusPromise = Promise.resolve(); -let resolveIsReadyToFocus; +import {forEach} from 'lodash'; +import {TextInput} from 'react-native'; +import _ from 'underscore'; +import CONST from '@src/CONST'; -function resetReadyToFocus() { - isReadyToFocusPromise = new Promise((resolve) => { - resolveIsReadyToFocus = resolve; +let focusedElement = null; + +function getActiveElement() { + return TextInput.State.currentlyFocusedInput ? TextInput.State.currentlyFocusedInput() : TextInput.State.currentlyFocusedField(); +} + +function saveFocusedElement(e) { + const target = e.target; + if (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA') { + return; + } + const activeElement = getActiveElement(); + if (!activeElement || (activeElement.tagName !== 'INPUT' && activeElement.tagName !== 'TEXTAREA')) { + return; + } + focusedElement = activeElement; +} + +function removeFocusedElement() { + if (!focusedElement) { + return; + } + // we have to use timeout because of measureLayout + setTimeout(() => (focusedElement = null), CONST.ANIMATION_IN_TIMING); +} + +let uniqueModalId = 1; +const focusMap = new Map(); +const activeModals = []; +const promiseMap = new Map(); + +// TODO:debug +global.demo = new Proxy( + { + obj: () => [...focusMap.values()], + arr: () => activeModals, + promise: () => [...promiseMap], + el: () => focusedElement, + }, + { + get: (target, key) => target[key] && target[key](), + }, +); + +function releaseElement(el) { + if (!el) { + return; + } + if (el === focusedElement) { + focusedElement = null; + } + forEach(focusMap, ([key, value]) => { + if (value !== el) { + return; + } + focusMap.delete(key); }); } -function setReadyToFocus() { - if (!resolveIsReadyToFocus) { + +function getId() { + return uniqueModalId++; +} + +function saveFocusState(id, container) { + // For popoverWithoutOverlay, react calls autofocus before useEffect. + const input = focusedElement || getActiveElement(); + focusedElement = null; + if (activeModals.indexOf(id) < 0) { + activeModals.push(id); + } + if (!input) { + return; + } + // TODO: can we refine this logic? + if (container && container.contains(input)) { + return; + } + focusMap.set(id, input); + input.blur(); +} + +/** + * If we intentionally clicked on another input box, there is no need to restore focus. + * But if we are closing the RHP, we can ignore the focused input. + * + * @param {Element} element + * @param {Boolean} shouldIgnoreFocused + */ +function focusElement(element, shouldIgnoreFocused = false) { + if (shouldIgnoreFocused) { + element.focus(); return; } - resolveIsReadyToFocus(); + const focused = getActiveElement(); + if (focused) { + return; + } + element.focus(); } -function isReadyToFocus() { - return isReadyToFocusPromise; + +function restoreFocusState(id, type = CONST.MODAL.RESTORE_TYPE.DEFAULT, shouldIgnoreFocused = false) { + // TODO:del + console.debug(`restore ${id}, type is ${type}, active modals are`, activeModals.join()); + if (!id) { + // TODO:del + console.debug('todo id empty'); + return; + } + if (activeModals.length < 1) { + // TODO:del + console.debug('stack is empty'); + return; + } + const index = activeModals.indexOf(id); + if (index < 0) { + // TODO:del + console.debug('activeModals does not contain this id'); + return; + } + activeModals.splice(index, 1); + if (type === CONST.MODAL.RESTORE_TYPE.PRESERVE) { + // TODO:del + console.debug('preserve input focus'); + return; + } + if (type === CONST.MODAL.RESTORE_TYPE.DELETE) { + // TODO:del + console.debug('delete, no restore'); + focusMap.delete(id); + return; + } + if (activeModals.length > index) { + // this modal is not the topmost one, do not restore it. + // TODO:del + console.debug('modal is not the topmost one'); + return; + } + const element = focusMap.get(id); + if (element) { + focusElement(element, shouldIgnoreFocused); + focusMap.delete(id); + return; + } + + if (focusMap.size < 1) { + // TODO:del + console.debug('obj is also empty, so return'); + return; + } + + // find the topmost one + const [lastKey, lastElement] = _.last([...focusMap]); + if (!lastElement) { + // TODO:del + console.error('no, impossible'); + return; + } + // TODO:del + console.debug('oh, try to restore topmost'); + focusElement(lastElement, shouldIgnoreFocused); + focusMap.delete(lastKey); +} + +function resetReadyToFocus(id) { + // TODO:del + console.debug('reset ready to focus', id); + const obj = {}; + obj.ready = new Promise((resolve) => { + obj.resolve = resolve; + }); + promiseMap.set(id, obj); +} + +function getKey(id) { + if (id) { + return id; + } + if (promiseMap.size < 1) { + return 0; + } + return _.last([...promiseMap.keys()]); +} + +function setReadyToFocus(id) { + const key = getKey(id); + const promise = promiseMap.get(key); + // TODO:del + console.debug('set ready to focus', id, key); + if (!promise) { + return; + } + promise.resolve(); + promiseMap.delete(key); +} + +function removePromise(id) { + // TODO:del + console.debug('remove promise', id); + const key = getKey(id); + promiseMap.delete(key); +} + +function isReadyToFocus(id) { + const key = getKey(id); + const promise = promiseMap.get(key); + // TODO:del + console.debug('is ready to focus', id, key, promise); + if (!promise) { + return Promise.resolve(); + } + return promise.ready; +} + +function tryRestoreFocusAfterClosedCompletely(id, restoreType) { + isReadyToFocus(id).then(() => restoreFocusState(id, restoreType)); +} + +function tryRestoreFocusByExternal() { + if (focusMap.size < 1) { + return; + } + const [key, input] = _.last([...focusMap]); + console.debug('oh, try to restore by external'); + input.focus(); + focusMap.delete(key); } export default { + getId, + saveFocusedElement, + removeFocusedElement, + releaseElement, + saveFocusState, + restoreFocusState, resetReadyToFocus, setReadyToFocus, isReadyToFocus, + tryRestoreFocusAfterClosedCompletely, + removePromise, + tryRestoreFocusByExternal, }; diff --git a/src/libs/Navigation/AppNavigator/AuthScreens.js b/src/libs/Navigation/AppNavigator/AuthScreens.js index aedb2fa8d74..314ca856d60 100644 --- a/src/libs/Navigation/AppNavigator/AuthScreens.js +++ b/src/libs/Navigation/AppNavigator/AuthScreens.js @@ -5,6 +5,7 @@ import {View} from 'react-native'; import Onyx, {withOnyx} from 'react-native-onyx'; import _ from 'underscore'; import useWindowDimensions from '@hooks/useWindowDimensions'; +import ComposerFocusManager from '@libs/ComposerFocusManager'; import KeyboardShortcut from '@libs/KeyboardShortcut'; import getCurrentUrl from '@libs/Navigation/currentUrl'; import Navigation from '@libs/Navigation/Navigation'; @@ -94,6 +95,8 @@ Onyx.connect({ }); const RootStack = createCustomStackNavigator(); +const modalId = ComposerFocusManager.getId(); + // We want to delay the re-rendering for components(e.g. ReportActionCompose) // that depends on modal visibility until Modal is completely closed and its focused // When modal screen is focused, update modal visibility in Onyx @@ -101,9 +104,11 @@ const RootStack = createCustomStackNavigator(); const modalScreenListeners = { focus: () => { + ComposerFocusManager.saveFocusState(modalId); Modal.setModalVisibility(true); }, beforeRemove: () => { + ComposerFocusManager.restoreFocusState(modalId, CONST.MODAL.RESTORE_TYPE.DEFAULT, true); Modal.setModalVisibility(false); }, }; @@ -322,7 +327,6 @@ function AuthScreens({isUsingMemoryOnlyKeys, lastUpdateIDAppliedToClient, sessio presentation: 'transparentModal', }} getComponent={loadReportAttachments} - listeners={modalScreenListeners} /> ({ // Excess space should be on the left so we need to position from right. right: 0, }, + keyboardHandlingEnabled: false, }, homeScreen: { diff --git a/src/pages/home/report/ContextMenu/BaseReportActionContextMenu.js b/src/pages/home/report/ContextMenu/BaseReportActionContextMenu.js index 18351f86a40..264011dcbd8 100755 --- a/src/pages/home/report/ContextMenu/BaseReportActionContextMenu.js +++ b/src/pages/home/report/ContextMenu/BaseReportActionContextMenu.js @@ -164,7 +164,10 @@ function BaseReportActionContextMenu(props) { successText={contextAction.successTextTranslateKey ? props.translate(contextAction.successTextTranslateKey) : undefined} isMini={props.isMini} key={contextAction.textTranslateKey} - onPress={() => interceptAnonymousUser(() => contextAction.onPress(closePopup, payload), contextAction.isAnonymousAction)} + onPress={() => { + props.onItemSelected(contextAction); + interceptAnonymousUser(() => contextAction.onPress(closePopup, payload), contextAction.isAnonymousAction); + }} description={contextAction.getDescription(props.selection, props.isSmallScreenWidth)} isAnonymousAction={contextAction.isAnonymousAction} isFocused={focusedIndex === index} diff --git a/src/pages/home/report/ContextMenu/ContextMenuActions.js b/src/pages/home/report/ContextMenu/ContextMenuActions.js index 4f35926c595..49c294ca704 100644 --- a/src/pages/home/report/ContextMenu/ContextMenuActions.js +++ b/src/pages/home/report/ContextMenu/ContextMenuActions.js @@ -132,6 +132,7 @@ export default [ const isTaskAction = ReportActionsUtils.isTaskAction(reportAction); return (isCommentAction || isReportPreviewAction || isIOUAction || isModifiedExpenseAction || isTaskAction) && !ReportUtils.isThreadFirstChat(reportAction, reportID); }, + restoreType: CONST.MODAL.RESTORE_TYPE.DELETE, onPress: (closePopover, {reportAction, reportID}) => { if (closePopover) { hideContextMenu(false, () => { @@ -362,6 +363,7 @@ export default [ icon: Expensicons.Pencil, shouldShow: (type, reportAction, isArchivedRoom, betas, menuTarget, isChronosReport) => type === CONTEXT_MENU_TYPES.REPORT_ACTION && ReportUtils.canEditReportAction(reportAction) && !isArchivedRoom && !isChronosReport, + restoreType: CONST.MODAL.RESTORE_TYPE.DELETE, onPress: (closePopover, {reportID, reportAction, draftMessage}) => { if (ReportActionsUtils.isMoneyRequestAction(reportAction)) { hideContextMenu(false); @@ -401,6 +403,7 @@ export default [ !isArchivedRoom && !isChronosReport && !ReportActionsUtils.isMessageDeleted(reportAction), + restoreType: CONST.MODAL.RESTORE_TYPE.PRESERVE, onPress: (closePopover, {reportID, reportAction}) => { if (closePopover) { // Hide popover, then call showDeleteConfirmModal diff --git a/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.js b/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.js index 7f60b9d9b4d..1bbbe0734a3 100644 --- a/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.js +++ b/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.js @@ -7,6 +7,7 @@ import useLocalize from '@hooks/useLocalize'; import * as ReportActionsUtils from '@libs/ReportActionsUtils'; import * as IOU from '@userActions/IOU'; import * as Report from '@userActions/Report'; +import CONST from '@src/CONST'; import BaseReportActionContextMenu from './BaseReportActionContextMenu'; function PopoverReportActionContextMenu(_props, ref) { @@ -40,6 +41,7 @@ function PopoverReportActionContextMenu(_props, ref) { const [isChronosReportEnabled, setIsChronosReportEnabled] = useState(false); const [isChatPinned, setIsChatPinned] = useState(false); const [hasUnreadMessages, setHasUnreadMessages] = useState(false); + const [restoreFocusType, setRestoreFocusType] = useState(undefined); const contentRef = useRef(null); const anchorRef = useRef(null); @@ -188,6 +190,7 @@ function PopoverReportActionContextMenu(_props, ref) { * After Popover shows, call the registered onPopoverShow callback and reset it */ const runAndResetOnPopoverShow = () => { + setRestoreFocusType(CONST.MODAL.RESTORE_TYPE.DEFAULT); onPopoverShow.current(); // After we have called the action, reset it. @@ -298,6 +301,7 @@ function PopoverReportActionContextMenu(_props, ref) { fullscreen withoutOverlay anchorRef={anchorRef} + restoreFocusType={restoreFocusType} > setRestoreFocusType(action.restoreType || CONST.MODAL.RESTORE_TYPE.DEFAULT)} originalReportID={originalReportIDRef.current} /> diff --git a/src/pages/home/report/ContextMenu/genericReportActionContextMenuPropTypes.js b/src/pages/home/report/ContextMenu/genericReportActionContextMenuPropTypes.js index 3d8667e44e6..5b608d9677d 100644 --- a/src/pages/home/report/ContextMenu/genericReportActionContextMenuPropTypes.js +++ b/src/pages/home/report/ContextMenu/genericReportActionContextMenuPropTypes.js @@ -22,6 +22,9 @@ const propTypes = { /** Draft message - if this is set the comment is in 'edit' mode */ draftMessage: PropTypes.string, + + /** Callback to fire when a menu item is selected */ + onItemSelected: PropTypes.func, }; const defaultProps = { @@ -29,6 +32,7 @@ const defaultProps = { isVisible: false, selection: '', draftMessage: '', + onItemSelected: () => {}, }; export {propTypes, defaultProps}; diff --git a/src/pages/home/report/ReportActionCompose/AttachmentPickerWithMenuItems.js b/src/pages/home/report/ReportActionCompose/AttachmentPickerWithMenuItems.js index ec9421bfa1c..7dda29bba7a 100644 --- a/src/pages/home/report/ReportActionCompose/AttachmentPickerWithMenuItems.js +++ b/src/pages/home/report/ReportActionCompose/AttachmentPickerWithMenuItems.js @@ -1,5 +1,5 @@ import PropTypes from 'prop-types'; -import React, {useMemo} from 'react'; +import React, {useMemo, useState} from 'react'; import {View} from 'react-native'; import _ from 'underscore'; import AttachmentPicker from '@components/AttachmentPicker'; @@ -11,6 +11,7 @@ import Tooltip from '@components/Tooltip/PopoverAnchorTooltip'; import useLocalize from '@hooks/useLocalize'; import useWindowDimensions from '@hooks/useWindowDimensions'; import * as Browser from '@libs/Browser'; +import ComposerFocusManager from '@libs/ComposerFocusManager'; import * as ReportUtils from '@libs/ReportUtils'; import useThemeStyles from '@styles/useThemeStyles'; import * as IOU from '@userActions/IOU'; @@ -61,18 +62,9 @@ const propTypes = { /** Called when opening the attachment picker */ onTriggerAttachmentPicker: PropTypes.func.isRequired, - /** Called when cancelling the attachment picker */ - onCanceledAttachmentPicker: PropTypes.func.isRequired, - /** Called when the menu with the items is closed after it was open */ onMenuClosed: PropTypes.func.isRequired, - /** Called when the add action button is pressed */ - onAddActionPressed: PropTypes.func.isRequired, - - /** Called when the menu item is selected */ - onItemSelected: PropTypes.func.isRequired, - /** A ref for the add action button */ actionButtonRef: PropTypes.shape({ // eslint-disable-next-line react/forbid-prop-types @@ -103,15 +95,13 @@ function AttachmentPickerWithMenuItems({ setMenuVisibility, isMenuVisible, onTriggerAttachmentPicker, - onCanceledAttachmentPicker, onMenuClosed, - onAddActionPressed, - onItemSelected, actionButtonRef, }) { const styles = useThemeStyles(); const {translate} = useLocalize(); const {windowHeight} = useWindowDimensions(); + const [restoreFocusType, setRestoreFocusType] = useState(undefined); /** * Returns the list of IOU Options @@ -169,7 +159,7 @@ function AttachmentPickerWithMenuItems({ onTriggerAttachmentPicker(); openPicker({ onPicked: displayFileInModal, - onCanceled: onCanceledAttachmentPicker, + onCanceled: () => ComposerFocusManager.tryRestoreFocusByExternal(), }); }; const menuItems = [ @@ -232,7 +222,6 @@ function AttachmentPickerWithMenuItems({ ref={actionButtonRef} onPress={(e) => { e.preventDefault(); - onAddActionPressed(); // Drop focus to avoid blue focus ring. actionButtonRef.current.blur(); @@ -248,17 +237,30 @@ function AttachmentPickerWithMenuItems({ setRestoreFocusType(CONST.MODAL.RESTORE_TYPE.DEFAULT)} onClose={onPopoverMenuClose} onItemSelected={(item, index) => { + if (index !== menuItems.length - 1) { + setMenuVisibility(false); + return; + } + // TODO:refine + let type = CONST.MODAL.RESTORE_TYPE.DEFAULT; + if (Browser.isFileCancelSupported()) { + type = CONST.MODAL.RESTORE_TYPE.PRESERVE; + } else if (Browser.isMobile()) { + type = CONST.MODAL.RESTORE_TYPE.DELETE; + } + setRestoreFocusType(type); setMenuVisibility(false); - onItemSelected(); // In order for the file picker to open dynamically, the click // function must be called from within a event handler that was initiated // by the user on Safari. - if (index === menuItems.length - 1 && Browser.isSafari()) { + if (Browser.isSafari()) { triggerAttachmentPicker(); } }} diff --git a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.js b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.js index 663db82a606..d2d74e8714a 100644 --- a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.js +++ b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.js @@ -8,7 +8,6 @@ import Composer from '@components/Composer'; import withKeyboardState from '@components/withKeyboardState'; import useDebounce from '@hooks/useDebounce'; import useLocalize from '@hooks/useLocalize'; -import usePrevious from '@hooks/usePrevious'; import useWindowDimensions from '@hooks/useWindowDimensions'; import * as Browser from '@libs/Browser'; import canFocusInputOnScreenFocus from '@libs/canFocusInputOnScreenFocus'; @@ -17,14 +16,12 @@ import * as ComposerUtils from '@libs/ComposerUtils'; import getDraftComment from '@libs/ComposerUtils/getDraftComment'; import convertToLTRForComposer from '@libs/convertToLTRForComposer'; import * as EmojiUtils from '@libs/EmojiUtils'; -import focusComposerWithDelay from '@libs/focusComposerWithDelay'; import * as KeyDownListener from '@libs/KeyboardShortcut/KeyDownPressListener'; import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager'; import * as ReportActionsUtils from '@libs/ReportActionsUtils'; import * as ReportUtils from '@libs/ReportUtils'; import * as SuggestionUtils from '@libs/SuggestionUtils'; import updateMultilineInputRange from '@libs/UpdateMultilineInputRange'; -import willBlurTextInputOnTapOutsideFunc from '@libs/willBlurTextInputOnTapOutside'; import SilentCommentUpdater from '@pages/home/report/ReportActionCompose/SilentCommentUpdater'; import Suggestions from '@pages/home/report/ReportActionCompose/Suggestions'; import containerComposeStyles from '@styles/containerComposeStyles'; @@ -53,8 +50,6 @@ const debouncedBroadcastUserIsTyping = _.debounce((reportID) => { Report.broadcastUserIsTyping(reportID); }, 100); -const willBlurTextInputOnTapOutside = willBlurTextInputOnTapOutsideFunc(); - // We want consistent auto focus behavior on input between native and mWeb so we have some auto focus management code that will // prevent auto focus on existing chat for mobile device const shouldFocusInputOnScreenFocus = canFocusInputOnScreenFocus(); @@ -102,8 +97,6 @@ function ComposerWithSuggestions({ suggestionsRef, animatedRef, forwardedRef, - isNextModalWillOpenRef, - editFocused, // For testing children, }) { @@ -401,24 +394,14 @@ function ComposerWithSuggestions({ /** * Focus the composer text input - * @param {Boolean} [shouldDelay=false] Impose delay before focusing the composer - * @memberof ReportActionCompose */ - const focus = useCallback((shouldDelay = false) => { - focusComposerWithDelay(textInputRef.current)(shouldDelay); + const focus = useCallback(() => { + if (!textInputRef.current) { + return; + } + textInputRef.current.focus(); }, []); - const setUpComposeFocusManager = useCallback(() => { - // This callback is used in the contextMenuActions to manage giving focus back to the compose input. - ReportActionComposeFocusManager.onComposerFocus(() => { - if (!willBlurTextInputOnTapOutside || !isFocused) { - return; - } - - focus(false); - }, true); - }, [focus, isFocused]); - /** * Check if the composer is visible. Returns true if the composer is not covered up by emoji picker or menu. False otherwise. * @returns {Boolean} @@ -473,12 +456,9 @@ function ComposerWithSuggestions({ const unsubscribeNavigationBlur = navigation.addListener('blur', () => KeyDownListener.removeKeyDownPressListener(focusComposerOnKeyPress)); const unsubscribeNavigationFocus = navigation.addListener('focus', () => { KeyDownListener.addKeyDownPressListener(focusComposerOnKeyPress); - setUpComposeFocusManager(); }); KeyDownListener.addKeyDownPressListener(focusComposerOnKeyPress); - setUpComposeFocusManager(); - return () => { ReportActionComposeFocusManager.clear(true); @@ -486,28 +466,8 @@ function ComposerWithSuggestions({ unsubscribeNavigationBlur(); unsubscribeNavigationFocus(); }; - }, [focusComposerOnKeyPress, navigation, setUpComposeFocusManager]); + }, [focusComposerOnKeyPress, navigation]); - const prevIsModalVisible = usePrevious(modal.isVisible); - const prevIsFocused = usePrevious(isFocused); - useEffect(() => { - if (modal.isVisible && !prevIsModalVisible) { - // eslint-disable-next-line no-param-reassign - isNextModalWillOpenRef.current = false; - } - // We want to focus or refocus the input when a modal has been closed or the underlying screen is refocused. - // We avoid doing this on native platforms since the software keyboard popping - // open creates a jarring and broken UX. - if (!(willBlurTextInputOnTapOutside && !isNextModalWillOpenRef.current && !modal.isVisible && isFocused && (prevIsModalVisible || !prevIsFocused))) { - return; - } - - if (editFocused) { - InputFocus.inputFocusChange(false); - return; - } - focus(true); - }, [focus, prevIsFocused, editFocused, prevIsModalVisible, isFocused, modal.isVisible, isNextModalWillOpenRef]); useEffect(() => { // Scrolls the composer to the bottom and sets the selection to the end, so that longer drafts are easier to edit updateMultilineInputRange(textInputRef.current, shouldAutoFocus); @@ -637,9 +597,6 @@ export default compose( key: ONYXKEYS.PREFERRED_EMOJI_SKIN_TONE, selector: EmojiUtils.getPreferredSkinToneIndex, }, - editFocused: { - key: ONYXKEYS.INPUT_FOCUSED, - }, parentReportActions: { key: ({report}) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.parentReportID}`, canEvict: false, diff --git a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/composerWithSuggestionsProps.js b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/composerWithSuggestionsProps.js index 46d34d407b1..fd485fba063 100644 --- a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/composerWithSuggestionsProps.js +++ b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/composerWithSuggestionsProps.js @@ -102,9 +102,6 @@ const propTypes = { /** Ref for the composer */ forwardedRef: PropTypes.shape({current: PropTypes.shape({})}), - - /** Ref for the isNextModalWillOpen */ - isNextModalWillOpenRef: PropTypes.shape({current: PropTypes.bool.isRequired}).isRequired, }; const defaultProps = { diff --git a/src/pages/home/report/ReportActionCompose/ReportActionCompose.js b/src/pages/home/report/ReportActionCompose/ReportActionCompose.js index 7bce37dc382..dd85bd847ff 100644 --- a/src/pages/home/report/ReportActionCompose/ReportActionCompose.js +++ b/src/pages/home/report/ReportActionCompose/ReportActionCompose.js @@ -187,21 +187,7 @@ function ReportActionCompose({ return translate('reportActionCompose.writeSomething'); }, [report, blockedFromConcierge, translate, conciergePlaceholderRandomIndex]); - const focus = () => { - if (composerRef === null || composerRef.current === null) { - return; - } - composerRef.current.focus(true); - }; - - const isKeyboardVisibleWhenShowingModalRef = useRef(false); - const restoreKeyboardState = useCallback(() => { - if (!isKeyboardVisibleWhenShowingModalRef.current) { - return; - } - focus(); - isKeyboardVisibleWhenShowingModalRef.current = false; - }, []); + const restoreKeyboardState = useCallback(() => {}, []); const containerRef = useRef(null); const measureContainer = useCallback((callback) => { @@ -211,17 +197,6 @@ function ReportActionCompose({ containerRef.current.measureInWindow(callback); }, []); - const onAddActionPressed = useCallback(() => { - if (!willBlurTextInputOnTapOutside) { - isKeyboardVisibleWhenShowingModalRef.current = composerRef.current.isFocused(); - } - composerRef.current.blur(); - }, []); - - const onItemSelected = useCallback(() => { - isKeyboardVisibleWhenShowingModalRef.current = false; - }, []); - const updateShouldShowSuggestionMenuToFalse = useCallback(() => { if (!suggestionsRef.current) { return; @@ -247,8 +222,7 @@ function ReportActionCompose({ const onAttachmentPreviewClose = useCallback(() => { updateShouldShowSuggestionMenuToFalse(); setIsAttachmentPreviewActive(false); - restoreKeyboardState(); - }, [updateShouldShowSuggestionMenuToFalse, restoreKeyboardState]); + }, [updateShouldShowSuggestionMenuToFalse]); /** * Add a new comment to this chat @@ -271,25 +245,20 @@ function ReportActionCompose({ [onSubmit], ); - const isNextModalWillOpenRef = useRef(false); const onTriggerAttachmentPicker = useCallback(() => { // Set a flag to block suggestion calculation until we're finished using the file picker, // which will stop any flickering as the file picker opens on non-native devices. - if (willBlurTextInputOnTapOutside) { - suggestionsRef.current.setShouldBlockSuggestionCalc(true); + if (!willBlurTextInputOnTapOutside) { + return; } - isNextModalWillOpenRef.current = true; - isKeyboardVisibleWhenShowingModalRef.current = true; + suggestionsRef.current.setShouldBlockSuggestionCalc(true); }, []); - const onBlur = useCallback((e) => { + const onBlur = useCallback(() => { setIsFocused(false); if (suggestionsRef.current) { suggestionsRef.current.resetSuggestions(); } - if (e.relatedTarget && e.relatedTarget === actionButtonRef.current) { - isKeyboardVisibleWhenShowingModalRef.current = true; - } }, []); const onFocus = useCallback(() => { @@ -387,15 +356,12 @@ function ReportActionCompose({ onTriggerAttachmentPicker={onTriggerAttachmentPicker} onCanceledAttachmentPicker={restoreKeyboardState} onMenuClosed={restoreKeyboardState} - onAddActionPressed={onAddActionPressed} - onItemSelected={onItemSelected} actionButtonRef={actionButtonRef} /> composerRef.current.replaceSelectionWithText(...args)} emojiPickerID={report.reportID} /> diff --git a/src/pages/home/report/ReportActionItemMessageEdit.js b/src/pages/home/report/ReportActionItemMessageEdit.js index c8ea0d5e351..4b762c4e229 100644 --- a/src/pages/home/report/ReportActionItemMessageEdit.js +++ b/src/pages/home/report/ReportActionItemMessageEdit.js @@ -20,7 +20,6 @@ import useWindowDimensions from '@hooks/useWindowDimensions'; import * as Browser from '@libs/Browser'; import * as ComposerUtils from '@libs/ComposerUtils'; import * as EmojiUtils from '@libs/EmojiUtils'; -import focusComposerWithDelay from '@libs/focusComposerWithDelay'; import onyxSubscribe from '@libs/onyxSubscribe'; import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager'; import * as ReportActionsUtils from '@libs/ReportActionsUtils'; @@ -363,11 +362,6 @@ function ReportActionItemMessageEdit(props) { [deleteDraft, isKeyboardShown, isSmallScreenWidth, publishDraft], ); - /** - * Focus the composer text input - */ - const focus = focusComposerWithDelay(textInputRef.current); - return ( <> @@ -440,7 +434,6 @@ function ReportActionItemMessageEdit(props) { focus(true)} onEmojiSelected={addEmojiToTextBox} id={emojiButtonID} emojiPickerID={props.action.reportActionID} diff --git a/src/pages/iou/WaypointEditor.js b/src/pages/iou/WaypointEditor.js index 74a3a353ef5..1231fee532d 100644 --- a/src/pages/iou/WaypointEditor.js +++ b/src/pages/iou/WaypointEditor.js @@ -88,6 +88,7 @@ function WaypointEditor({route: {params: {iouType = '', transactionID = '', wayp const parsedWaypointIndex = parseInt(waypointIndex, 10); const allWaypoints = lodashGet(transaction, 'comment.waypoints', {}); const currentWaypoint = lodashGet(allWaypoints, `waypoint${waypointIndex}`, {}); + const [restoreFocusType, setRestoreFocusType] = useState(); const waypointCount = _.size(allWaypoints); const filledWaypointCount = _.size(_.filter(allWaypoints, (waypoint) => !_.isEmpty(waypoint))); @@ -153,6 +154,7 @@ function WaypointEditor({route: {params: {iouType = '', transactionID = '', wayp }; const deleteStopAndHideModal = () => { + setRestoreFocusType(CONST.MODAL.RESTORE_TYPE.DELETE); Transaction.removeWaypoint(transactionID, waypointIndex); setIsDeleteStopModalOpen(false); Navigation.goBack(ROUTES.MONEY_REQUEST_DISTANCE_TAB.getRoute(iouType)); @@ -201,8 +203,12 @@ function WaypointEditor({route: {params: {iouType = '', transactionID = '', wayp setIsDeleteStopModalOpen(false)} + onCancel={() => { + setRestoreFocusType(undefined); + setIsDeleteStopModalOpen(false); + }} prompt={translate('distance.deleteWaypointConfirmation')} confirmText={translate('common.delete')} cancelText={translate('common.cancel')} From 7655502a8b0664c21d19f5c6d1e782f2a8c9d629 Mon Sep 17 00:00:00 2001 From: ntdiary <2471314@gmail.com> Date: Wed, 29 Nov 2023 22:03:29 +0800 Subject: [PATCH 02/28] fix map iterator --- src/libs/ComposerFocusManager.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/libs/ComposerFocusManager.js b/src/libs/ComposerFocusManager.js index 79221a58a06..829ffa07b7a 100644 --- a/src/libs/ComposerFocusManager.js +++ b/src/libs/ComposerFocusManager.js @@ -1,4 +1,3 @@ -import {forEach} from 'lodash'; import {TextInput} from 'react-native'; import _ from 'underscore'; import CONST from '@src/CONST'; @@ -54,7 +53,7 @@ function releaseElement(el) { if (el === focusedElement) { focusedElement = null; } - forEach(focusMap, ([key, value]) => { + [...focusMap].forEach(([key, value]) => { if (value !== el) { return; } From 8a7ce9d88ad1ba72a2cf052f7330dace0b897040 Mon Sep 17 00:00:00 2001 From: ntdiary <2471314@gmail.com> Date: Mon, 18 Dec 2023 23:57:48 +0800 Subject: [PATCH 03/28] fix merge error --- src/components/PopoverWithoutOverlay/index.js | 1 + src/libs/Navigation/AppNavigator/ModalStackNavigators.js | 0 2 files changed, 1 insertion(+) delete mode 100644 src/libs/Navigation/AppNavigator/ModalStackNavigators.js diff --git a/src/components/PopoverWithoutOverlay/index.js b/src/components/PopoverWithoutOverlay/index.js index eb7461678b4..104d885f425 100644 --- a/src/components/PopoverWithoutOverlay/index.js +++ b/src/components/PopoverWithoutOverlay/index.js @@ -1,5 +1,6 @@ import React, {useMemo, useRef} from 'react'; import {View} from 'react-native'; +import {isFunction, isObject} from 'underscore'; import ColorSchemeWrapper from '@components/ColorSchemeWrapper'; import ModalContent from '@components/Modal/ModalContent'; import {defaultProps, propTypes} from '@components/Popover/popoverPropTypes'; diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators.js b/src/libs/Navigation/AppNavigator/ModalStackNavigators.js deleted file mode 100644 index e69de29bb2d..00000000000 From 44b57f58922794386cddd48eee49137cff165122 Mon Sep 17 00:00:00 2001 From: ntdiary <2471314@gmail.com> Date: Tue, 19 Dec 2023 17:50:40 +0800 Subject: [PATCH 04/28] fix RHP refocus --- src/libs/Navigation/AppNavigator/ModalStackNavigators.tsx | 1 + .../AppNavigator/Navigators/RightModalNavigator.tsx | 8 +++++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators.tsx index 256ea6d4ece..abcd5e660a4 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators.tsx +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators.tsx @@ -50,6 +50,7 @@ function createModalStackNavigator(screens: cardStyle: styles.navigationScreenCardStyle, headerShown: false, cardStyleInterpolator: CardStyleInterpolators.forHorizontalIOS, + keyboardHandlingEnabled: false, }), [styles], ); diff --git a/src/libs/Navigation/AppNavigator/Navigators/RightModalNavigator.tsx b/src/libs/Navigation/AppNavigator/Navigators/RightModalNavigator.tsx index d7c31bcae7d..db1d57d0aaa 100644 --- a/src/libs/Navigation/AppNavigator/Navigators/RightModalNavigator.tsx +++ b/src/libs/Navigation/AppNavigator/Navigators/RightModalNavigator.tsx @@ -18,7 +18,13 @@ const Stack = createStackNavigator(); function RightModalNavigator({navigation}: RightModalNavigatorProps) { const styles = useThemeStyles(); const {isSmallScreenWidth} = useWindowDimensions(); - const screenOptions = useMemo(() => ModalNavigatorScreenOptions(styles), [styles]); + const screenOptions = useMemo( + () => ({ + ...ModalNavigatorScreenOptions(styles), + keyboardHandlingEnabled: false, + }), + [styles], + ); return ( From 5dfca1b4489ee0d2768704268eeda6fda50096ce Mon Sep 17 00:00:00 2001 From: ntdiary <2471314@gmail.com> Date: Tue, 19 Dec 2023 19:04:29 +0800 Subject: [PATCH 05/28] fix ts check --- src/components/Modal/index.android.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Modal/index.android.tsx b/src/components/Modal/index.android.tsx index bd725549d1f..76a7f57882f 100644 --- a/src/components/Modal/index.android.tsx +++ b/src/components/Modal/index.android.tsx @@ -10,7 +10,7 @@ import BaseModalProps, {ModalRef} from './types'; function Modal({useNativeDriver = true, restoreFocusType, onModalHide, ...rest}: BaseModalProps) { const modalRef = useRef(null); const hideModal = () => { - onModalHide(); + onModalHide?.(); if (restoreFocusType && restoreFocusType !== CONST.MODAL.RESTORE_TYPE.DEFAULT) { modalRef?.current?.removePromise(); return; From b73e737cfd5af79a7305f5afcfdac399ccddabde Mon Sep 17 00:00:00 2001 From: ntdiary <2471314@gmail.com> Date: Tue, 19 Dec 2023 23:29:19 +0800 Subject: [PATCH 06/28] optimize based on the comments --- src/CONST.ts | 2 +- .../AttachmentPicker/index.native.js | 4 +- src/components/Modal/index.android.tsx | 2 +- src/components/Modal/modalPropTypes.js | 2 +- src/components/Modal/types.ts | 2 +- .../GenericPressable/BaseGenericPressable.tsx | 3 +- .../Pressable/GenericPressable/index.tsx | 16 +++- src/components/RNTextInput.tsx | 2 +- src/libs/ComposerFocusManager.js | 76 +++++++++---------- .../Navigation/AppNavigator/AuthScreens.tsx | 2 +- .../report/ContextMenu/ContextMenuActions.js | 6 +- .../PopoverReportActionContextMenu.js | 4 +- .../AttachmentPickerWithMenuItems.js | 8 +- src/pages/iou/WaypointEditor.js | 2 +- 14 files changed, 70 insertions(+), 61 deletions(-) diff --git a/src/CONST.ts b/src/CONST.ts index e5978142ca1..2402d14aada 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -693,7 +693,7 @@ const CONST = { RIGHT: 'right', }, POPOVER_MENU_PADDING: 8, - RESTORE_TYPE: { + RESTORE_FOCUS_TYPE: { DEFAULT: 'default', DELETE: 'delete', PRESERVE: 'preserve', diff --git a/src/components/AttachmentPicker/index.native.js b/src/components/AttachmentPicker/index.native.js index 2442009bcce..9bd6426686b 100644 --- a/src/components/AttachmentPicker/index.native.js +++ b/src/components/AttachmentPicker/index.native.js @@ -313,7 +313,7 @@ function AttachmentPicker({type, children, shouldHideCameraOption}) { }} isVisible={isVisible} anchorPosition={styles.createMenuPosition} - onModalShow={() => setRestoreFocusType(CONST.MODAL.RESTORE_TYPE.DEFAULT)} + onModalShow={() => setRestoreFocusType(CONST.MODAL.RESTORE_FOCUS_TYPE.DEFAULT)} onModalHide={onModalHide.current} > @@ -323,7 +323,7 @@ function AttachmentPicker({type, children, shouldHideCameraOption}) { icon={item.icon} title={translate(item.textTranslationKey)} onPress={() => { - setRestoreFocusType(CONST.MODAL.RESTORE_TYPE.PRESERVE); + setRestoreFocusType(CONST.MODAL.RESTORE_FOCUS_TYPE.PRESERVE); selectItem(item); }} focused={focusedIndex === menuIndex} diff --git a/src/components/Modal/index.android.tsx b/src/components/Modal/index.android.tsx index 76a7f57882f..06ead67367f 100644 --- a/src/components/Modal/index.android.tsx +++ b/src/components/Modal/index.android.tsx @@ -11,7 +11,7 @@ function Modal({useNativeDriver = true, restoreFocusType, onModalHide, ...rest}: const modalRef = useRef(null); const hideModal = () => { onModalHide?.(); - if (restoreFocusType && restoreFocusType !== CONST.MODAL.RESTORE_TYPE.DEFAULT) { + if (restoreFocusType && restoreFocusType !== CONST.MODAL.RESTORE_FOCUS_TYPE.DEFAULT) { modalRef?.current?.removePromise(); return; } diff --git a/src/components/Modal/modalPropTypes.js b/src/components/Modal/modalPropTypes.js index a4af2ca0e4d..e8e5a9d141c 100644 --- a/src/components/Modal/modalPropTypes.js +++ b/src/components/Modal/modalPropTypes.js @@ -67,7 +67,7 @@ const propTypes = { hideModalContentWhileAnimating: PropTypes.bool, /** how to re-focus after the modal is dismissed */ - restoreFocusType: PropTypes.oneOf(_.values(CONST.MODAL.RESTORE_TYPE)), + restoreFocusType: PropTypes.oneOf(_.values(CONST.MODAL.RESTORE_FOCUS_TYPE)), ...windowDimensionsPropTypes, }; diff --git a/src/components/Modal/types.ts b/src/components/Modal/types.ts index 9a50a0d58ba..e714c6c51c9 100644 --- a/src/components/Modal/types.ts +++ b/src/components/Modal/types.ts @@ -63,7 +63,7 @@ type BaseModalProps = WindowDimensionsProps & hideModalContentWhileAnimating?: boolean; /** how to re-focus after the modal is dismissed */ - restoreFocusType?: ValueOf; + restoreFocusType?: ValueOf; }; type ModalRef = { diff --git a/src/components/Pressable/GenericPressable/BaseGenericPressable.tsx b/src/components/Pressable/GenericPressable/BaseGenericPressable.tsx index b295d19bfa8..f9a8a78905a 100644 --- a/src/components/Pressable/GenericPressable/BaseGenericPressable.tsx +++ b/src/components/Pressable/GenericPressable/BaseGenericPressable.tsx @@ -108,7 +108,8 @@ function GenericPressable( ref.current?.blur(); } onPress(event); - ComposerFocusManager.removeFocusedElement(); + + ComposerFocusManager.clearFocusedInput(); Accessibility.moveAccessibilityFocus(nextFocusRef); }, diff --git a/src/components/Pressable/GenericPressable/index.tsx b/src/components/Pressable/GenericPressable/index.tsx index d5cf2a062c6..d6d732ad1dd 100644 --- a/src/components/Pressable/GenericPressable/index.tsx +++ b/src/components/Pressable/GenericPressable/index.tsx @@ -1,9 +1,21 @@ import React, {forwardRef} from 'react'; -import {Role} from 'react-native'; +import {NativePointerEvent, NativeSyntheticEvent, Role} from 'react-native'; import ComposerFocusManager from '@libs/ComposerFocusManager'; import GenericPressable from './BaseGenericPressable'; import PressableProps, {PressableRef} from './types'; +function saveFocusedInput(e: NativeSyntheticEvent) { + const target = e.target as unknown as EventTarget; + if (!target) { + return; + } + // If an input is clicked, it usually doesn't show a modal, so there's no need to save the focused input. + if (target instanceof HTMLTextAreaElement || target instanceof HTMLInputElement) { + return; + } + ComposerFocusManager.saveFocusedInput(); +} + function WebGenericPressable({focusable = true, ...props}: PressableProps, ref: PressableRef) { const accessible = props.accessible ?? props.accessible === undefined ? true : props.accessible; @@ -11,7 +23,7 @@ function WebGenericPressable({focusable = true, ...props}: PressableProps, ref: > | null>(null); - useEffect(() => () => ComposerFocusManager.releaseElement(inputRef.current), []); + useEffect(() => () => ComposerFocusManager.releaseInput(inputRef.current), []); return ( (focusedElement = null), CONST.ANIMATION_IN_TIMING); + setTimeout(() => (focusedInput = null), CONST.ANIMATION_IN_TIMING); } let uniqueModalId = 1; @@ -39,22 +35,22 @@ global.demo = new Proxy( obj: () => [...focusMap.values()], arr: () => activeModals, promise: () => [...promiseMap], - el: () => focusedElement, + el: () => focusedInput, }, { get: (target, key) => target[key] && target[key](), }, ); -function releaseElement(el) { - if (!el) { +function releaseInput(input) { + if (!input) { return; } - if (el === focusedElement) { - focusedElement = null; + if (input === focusedInput) { + focusedInput = null; } [...focusMap].forEach(([key, value]) => { - if (value !== el) { + if (value !== input) { return; } focusMap.delete(key); @@ -67,8 +63,8 @@ function getId() { function saveFocusState(id, container) { // For popoverWithoutOverlay, react calls autofocus before useEffect. - const input = focusedElement || getActiveElement(); - focusedElement = null; + const input = focusedInput || getActiveInput(); + focusedInput = null; if (activeModals.indexOf(id) < 0) { activeModals.push(id); } @@ -87,22 +83,22 @@ function saveFocusState(id, container) { * If we intentionally clicked on another input box, there is no need to restore focus. * But if we are closing the RHP, we can ignore the focused input. * - * @param {Element} element + * @param {TextInput} input * @param {Boolean} shouldIgnoreFocused */ -function focusElement(element, shouldIgnoreFocused = false) { +function focus(input, shouldIgnoreFocused = false) { if (shouldIgnoreFocused) { - element.focus(); + input.focus(); return; } - const focused = getActiveElement(); - if (focused) { + const activeInput = getActiveInput(); + if (activeInput) { return; } - element.focus(); + input.focus(); } -function restoreFocusState(id, type = CONST.MODAL.RESTORE_TYPE.DEFAULT, shouldIgnoreFocused = false) { +function restoreFocusState(id, type = CONST.MODAL.RESTORE_FOCUS_TYPE.DEFAULT, shouldIgnoreFocused = false) { // TODO:del console.debug(`restore ${id}, type is ${type}, active modals are`, activeModals.join()); if (!id) { @@ -122,12 +118,12 @@ function restoreFocusState(id, type = CONST.MODAL.RESTORE_TYPE.DEFAULT, shouldIg return; } activeModals.splice(index, 1); - if (type === CONST.MODAL.RESTORE_TYPE.PRESERVE) { + if (type === CONST.MODAL.RESTORE_FOCUS_TYPE.PRESERVE) { // TODO:del console.debug('preserve input focus'); return; } - if (type === CONST.MODAL.RESTORE_TYPE.DELETE) { + if (type === CONST.MODAL.RESTORE_FOCUS_TYPE.DELETE) { // TODO:del console.debug('delete, no restore'); focusMap.delete(id); @@ -139,9 +135,9 @@ function restoreFocusState(id, type = CONST.MODAL.RESTORE_TYPE.DEFAULT, shouldIg console.debug('modal is not the topmost one'); return; } - const element = focusMap.get(id); - if (element) { - focusElement(element, shouldIgnoreFocused); + const input = focusMap.get(id); + if (input) { + focus(input, shouldIgnoreFocused); focusMap.delete(id); return; } @@ -153,15 +149,15 @@ function restoreFocusState(id, type = CONST.MODAL.RESTORE_TYPE.DEFAULT, shouldIg } // find the topmost one - const [lastKey, lastElement] = _.last([...focusMap]); - if (!lastElement) { + const [lastKey, lastInput] = _.last([...focusMap]); + if (!lastInput) { // TODO:del console.error('no, impossible'); return; } // TODO:del console.debug('oh, try to restore topmost'); - focusElement(lastElement, shouldIgnoreFocused); + focus(lastInput, shouldIgnoreFocused); focusMap.delete(lastKey); } @@ -231,9 +227,9 @@ function tryRestoreFocusByExternal() { export default { getId, - saveFocusedElement, - removeFocusedElement, - releaseElement, + saveFocusedInput, + clearFocusedInput, + releaseInput, saveFocusState, restoreFocusState, resetReadyToFocus, diff --git a/src/libs/Navigation/AppNavigator/AuthScreens.tsx b/src/libs/Navigation/AppNavigator/AuthScreens.tsx index 365d7f0c65d..f9a37737138 100644 --- a/src/libs/Navigation/AppNavigator/AuthScreens.tsx +++ b/src/libs/Navigation/AppNavigator/AuthScreens.tsx @@ -129,7 +129,7 @@ const modalScreenListeners = { Modal.setModalVisibility(true); }, beforeRemove: () => { - ComposerFocusManager.restoreFocusState(modalId, CONST.MODAL.RESTORE_TYPE.DEFAULT, true); + ComposerFocusManager.restoreFocusState(modalId, CONST.MODAL.RESTORE_FOCUS_TYPE.DEFAULT, true); // Clear search input (WorkspaceInvitePage) when modal is closed SearchInputManager.searchInput = ''; diff --git a/src/pages/home/report/ContextMenu/ContextMenuActions.js b/src/pages/home/report/ContextMenu/ContextMenuActions.js index a4bcfe2b4ca..2d87fd57a2d 100644 --- a/src/pages/home/report/ContextMenu/ContextMenuActions.js +++ b/src/pages/home/report/ContextMenu/ContextMenuActions.js @@ -138,7 +138,7 @@ export default [ !ReportUtils.isThreadFirstChat(reportAction, reportID) ); }, - restoreType: CONST.MODAL.RESTORE_TYPE.DELETE, + restoreType: CONST.MODAL.RESTORE_FOCUS_TYPE.DELETE, onPress: (closePopover, {reportAction, reportID}) => { if (closePopover) { hideContextMenu(false, () => { @@ -374,7 +374,7 @@ export default [ icon: Expensicons.Pencil, shouldShow: (type, reportAction, isArchivedRoom, betas, menuTarget, isChronosReport) => type === CONTEXT_MENU_TYPES.REPORT_ACTION && ReportUtils.canEditReportAction(reportAction) && !isArchivedRoom && !isChronosReport, - restoreType: CONST.MODAL.RESTORE_TYPE.DELETE, + restoreType: CONST.MODAL.RESTORE_FOCUS_TYPE.DELETE, onPress: (closePopover, {reportID, reportAction, draftMessage}) => { if (ReportActionsUtils.isMoneyRequestAction(reportAction)) { hideContextMenu(false); @@ -414,7 +414,7 @@ export default [ !isArchivedRoom && !isChronosReport && !ReportActionsUtils.isMessageDeleted(reportAction), - restoreType: CONST.MODAL.RESTORE_TYPE.PRESERVE, + restoreType: CONST.MODAL.RESTORE_FOCUS_TYPE.PRESERVE, onPress: (closePopover, {reportID, reportAction}) => { if (closePopover) { // Hide popover, then call showDeleteConfirmModal diff --git a/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.js b/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.js index 1bbbe0734a3..6eacaf6d526 100644 --- a/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.js +++ b/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.js @@ -190,7 +190,7 @@ function PopoverReportActionContextMenu(_props, ref) { * After Popover shows, call the registered onPopoverShow callback and reset it */ const runAndResetOnPopoverShow = () => { - setRestoreFocusType(CONST.MODAL.RESTORE_TYPE.DEFAULT); + setRestoreFocusType(CONST.MODAL.RESTORE_FOCUS_TYPE.DEFAULT); onPopoverShow.current(); // After we have called the action, reset it. @@ -316,7 +316,7 @@ function PopoverReportActionContextMenu(_props, ref) { isUnreadChat={hasUnreadMessages} anchor={contextMenuTargetNode} contentRef={contentRef} - onItemSelected={(action) => setRestoreFocusType(action.restoreType || CONST.MODAL.RESTORE_TYPE.DEFAULT)} + onItemSelected={(action) => setRestoreFocusType(action.restoreType || CONST.MODAL.RESTORE_FOCUS_TYPE.DEFAULT)} originalReportID={originalReportIDRef.current} /> diff --git a/src/pages/home/report/ReportActionCompose/AttachmentPickerWithMenuItems.js b/src/pages/home/report/ReportActionCompose/AttachmentPickerWithMenuItems.js index e50bcae0b5d..86adac9e520 100644 --- a/src/pages/home/report/ReportActionCompose/AttachmentPickerWithMenuItems.js +++ b/src/pages/home/report/ReportActionCompose/AttachmentPickerWithMenuItems.js @@ -271,7 +271,7 @@ function AttachmentPickerWithMenuItems({ restoreFocusType={restoreFocusType} animationInTiming={CONST.ANIMATION_IN_TIMING} isVisible={isMenuVisible && isFocused} - onModalShow={() => setRestoreFocusType(CONST.MODAL.RESTORE_TYPE.DEFAULT)} + onModalShow={() => setRestoreFocusType(CONST.MODAL.RESTORE_FOCUS_TYPE.DEFAULT)} onClose={onPopoverMenuClose} onItemSelected={(item, index) => { if (index !== menuItems.length - 1) { @@ -279,11 +279,11 @@ function AttachmentPickerWithMenuItems({ return; } // TODO:refine - let type = CONST.MODAL.RESTORE_TYPE.DEFAULT; + let type = CONST.MODAL.RESTORE_FOCUS_TYPE.DEFAULT; if (Browser.isFileCancelSupported()) { - type = CONST.MODAL.RESTORE_TYPE.PRESERVE; + type = CONST.MODAL.RESTORE_FOCUS_TYPE.PRESERVE; } else if (Browser.isMobile()) { - type = CONST.MODAL.RESTORE_TYPE.DELETE; + type = CONST.MODAL.RESTORE_FOCUS_TYPE.DELETE; } setRestoreFocusType(type); setMenuVisibility(false); diff --git a/src/pages/iou/WaypointEditor.js b/src/pages/iou/WaypointEditor.js index 0dd4842cede..ae576073a83 100644 --- a/src/pages/iou/WaypointEditor.js +++ b/src/pages/iou/WaypointEditor.js @@ -155,7 +155,7 @@ function WaypointEditor({route: {params: {iouType = '', transactionID = '', wayp }; const deleteStopAndHideModal = () => { - setRestoreFocusType(CONST.MODAL.RESTORE_TYPE.DELETE); + setRestoreFocusType(CONST.MODAL.RESTORE_FOCUS_TYPE.DELETE); Transaction.removeWaypoint(transaction, waypointIndex); setIsDeleteStopModalOpen(false); Navigation.goBack(ROUTES.MONEY_REQUEST_DISTANCE_TAB.getRoute(iouType)); From 2d85663ec2ee0f55f44b3169e3828caff42c11ee Mon Sep 17 00:00:00 2001 From: ntdiary <2471314@gmail.com> Date: Wed, 20 Dec 2023 18:07:26 +0800 Subject: [PATCH 07/28] delete outdated file --- src/libs/Navigation/AppNavigator/RHPScreenOptions.js | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 src/libs/Navigation/AppNavigator/RHPScreenOptions.js diff --git a/src/libs/Navigation/AppNavigator/RHPScreenOptions.js b/src/libs/Navigation/AppNavigator/RHPScreenOptions.js deleted file mode 100644 index e69de29bb2d..00000000000 From 7da38aeb2b640b66f285c190202006fb8563e2e6 Mon Sep 17 00:00:00 2001 From: ntdiary <2471314@gmail.com> Date: Thu, 21 Dec 2023 18:57:06 +0800 Subject: [PATCH 08/28] fix lint error --- .../ComposerWithSuggestions/ComposerWithSuggestions.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.js b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.js index 8036c022d1a..8572976d4c0 100644 --- a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.js +++ b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.js @@ -8,7 +8,6 @@ import Composer from '@components/Composer'; import withKeyboardState from '@components/withKeyboardState'; import useDebounce from '@hooks/useDebounce'; import useLocalize from '@hooks/useLocalize'; -import usePrevious from '@hooks/usePrevious'; import useStyleUtils from '@hooks/useStyleUtils'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -26,7 +25,6 @@ import * as ReportActionsUtils from '@libs/ReportActionsUtils'; import * as ReportUtils from '@libs/ReportUtils'; import * as SuggestionUtils from '@libs/SuggestionUtils'; import updateMultilineInputRange from '@libs/updateMultilineInputRange'; -import willBlurTextInputOnTapOutsideFunc from '@libs/willBlurTextInputOnTapOutside'; import SilentCommentUpdater from '@pages/home/report/ReportActionCompose/SilentCommentUpdater'; import Suggestions from '@pages/home/report/ReportActionCompose/Suggestions'; import * as EmojiPickerActions from '@userActions/EmojiPickerAction'; From 93972b09fda74e871126a37e3c39661fa44d9c3d Mon Sep 17 00:00:00 2001 From: ntdiary <2471314@gmail.com> Date: Tue, 26 Dec 2023 20:45:30 +0800 Subject: [PATCH 09/28] add some comments --- src/components/PopoverWithoutOverlay/index.js | 9 ++-- .../GenericPressable/BaseGenericPressable.tsx | 4 +- src/libs/ComposerFocusManager.js | 51 +++++++++++++++---- 3 files changed, 48 insertions(+), 16 deletions(-) diff --git a/src/components/PopoverWithoutOverlay/index.js b/src/components/PopoverWithoutOverlay/index.js index 104d885f425..a309bfb8db5 100644 --- a/src/components/PopoverWithoutOverlay/index.js +++ b/src/components/PopoverWithoutOverlay/index.js @@ -11,6 +11,7 @@ import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; import ComposerFocusManager from '@libs/ComposerFocusManager'; import * as Modal from '@userActions/Modal'; +import CONST from '@src/CONST'; function Popover(props) { const styles = useThemeStyles(); @@ -107,9 +108,11 @@ function Popover(props) { restoreFocusTypeRef.current = props.restoreFocusType; const handleDismissContent = () => { ComposerFocusManager.tryRestoreFocusAfterClosedCompletely(modalId, restoreFocusTypeRef.current); - // PopoverWithMeasuredContent delays the mounting of this popover, so here we also need to defer the restoration. - // In a follow-up PR, we can also consider how to improve the former. - setImmediate(() => ComposerFocusManager.setReadyToFocus(modalId)); + + // On the web platform, because there is no overlay, modal can be closed and opened instantly and randomly, + // this will cause the input box to gain and lose focus instantly while the subsequent modal is opened. + // The RESTORE_FOCUS_TYPE cannot address this randomness case, so we have to delay the refocusing here. + setTimeout(() => ComposerFocusManager.setReadyToFocus(modalId), CONST.ANIMATION_IN_TIMING); }; if (!props.isVisible) { diff --git a/src/components/Pressable/GenericPressable/BaseGenericPressable.tsx b/src/components/Pressable/GenericPressable/BaseGenericPressable.tsx index f9a8a78905a..af32ebc23b6 100644 --- a/src/components/Pressable/GenericPressable/BaseGenericPressable.tsx +++ b/src/components/Pressable/GenericPressable/BaseGenericPressable.tsx @@ -95,6 +95,8 @@ function GenericPressable( const onPressHandler = useCallback( (event?: GestureResponderEvent | KeyboardEvent) => { + ComposerFocusManager.clearFocusedInput(); + if (isDisabled) { return; } @@ -109,8 +111,6 @@ function GenericPressable( } onPress(event); - ComposerFocusManager.clearFocusedInput(); - Accessibility.moveAccessibilityFocus(nextFocusRef); }, [shouldUseHapticsOnPress, onPress, nextFocusRef, ref, isDisabled], diff --git a/src/libs/ComposerFocusManager.js b/src/libs/ComposerFocusManager.js index d6bb5099a31..8fb7b8fc2b4 100644 --- a/src/libs/ComposerFocusManager.js +++ b/src/libs/ComposerFocusManager.js @@ -1,3 +1,4 @@ +/* eslint-disable es/no-optional-chaining */ import {TextInput} from 'react-native'; import _ from 'underscore'; import CONST from '@src/CONST'; @@ -8,18 +9,21 @@ function getActiveInput() { return TextInput.State.currentlyFocusedInput ? TextInput.State.currentlyFocusedInput() : TextInput.State.currentlyFocusedField(); } +/** + * On web platform, if the modal is displayed by a click, the blur event is fired before the modal appears, + * so we need to cache the focused input in the pointerdown handler, which is fired before the blur event. + */ function saveFocusedInput() { - const activeInput = getActiveInput(); - if (activeInput) { - focusedInput = activeInput; - } + focusedInput = getActiveInput(); } +/** + * If a click does not display the modal, we also should clear the cached value to avoid potential issues. + */ function clearFocusedInput() { if (!focusedInput) { return; } - // we have to use timeout because of measureLayout setTimeout(() => (focusedInput = null), CONST.ANIMATION_IN_TIMING); } @@ -42,6 +46,11 @@ global.demo = new Proxy( }, ); +/** + * When a TextInput is unmounted, we also should release the reference here to avoid potential issues. + * + * @param {TextInput | Falsy} input + */ function releaseInput(input) { if (!input) { return; @@ -61,9 +70,17 @@ function getId() { return uniqueModalId++; } -function saveFocusState(id, container) { +/** + * Cache the focus state before the modal appears. + * + * @param {Number} id + * @param {HTMLElement} container + */ +function saveFocusState(id, container = undefined) { + const activeInput = getActiveInput(); + // For popoverWithoutOverlay, react calls autofocus before useEffect. - const input = focusedInput || getActiveInput(); + const input = focusedInput || activeInput; focusedInput = null; if (activeModals.indexOf(id) < 0) { activeModals.push(id); @@ -80,7 +97,7 @@ function saveFocusState(id, container) { } /** - * If we intentionally clicked on another input box, there is no need to restore focus. + * On web platform, if we intentionally click on another input box, there is no need to restore focus. * But if we are closing the RHP, we can ignore the focused input. * * @param {TextInput} input @@ -98,6 +115,13 @@ function focus(input, shouldIgnoreFocused = false) { input.focus(); } +/** + * Restore the focus state after the modal is dismissed. + * + * @param {Number} id + * @param {String} type + * @param {Boolean} shouldIgnoreFocused + */ function restoreFocusState(id, type = CONST.MODAL.RESTORE_FOCUS_TYPE.DEFAULT, shouldIgnoreFocused = false) { // TODO:del console.debug(`restore ${id}, type is ${type}, active modals are`, activeModals.join()); @@ -149,16 +173,21 @@ function restoreFocusState(id, type = CONST.MODAL.RESTORE_FOCUS_TYPE.DEFAULT, sh } // find the topmost one - const [lastKey, lastInput] = _.last([...focusMap]); + const [lastId, lastInput] = _.last([...focusMap]); if (!lastInput) { // TODO:del console.error('no, impossible'); return; } + if (activeModals.indexOf(lastId) >= 0) { + // TODO:del + console.debug('the previous modal is still active'); + return; + } // TODO:del - console.debug('oh, try to restore topmost'); + console.debug('ok, try to restore topmost'); focus(lastInput, shouldIgnoreFocused); - focusMap.delete(lastKey); + focusMap.delete(lastId); } function resetReadyToFocus(id) { From 532319f87e6c85714cf945a5ce52d00b02176d70 Mon Sep 17 00:00:00 2001 From: ntdiary <2471314@gmail.com> Date: Fri, 5 Jan 2024 20:35:57 +0800 Subject: [PATCH 10/28] add some missed changes --- src/components/PopoverMenu.tsx | 8 +++++++- .../AppNavigator/ModalNavigatorScreenOptions.ts | 1 + 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/components/PopoverMenu.tsx b/src/components/PopoverMenu.tsx index adbc33f4edd..b5f6aa1c8cc 100644 --- a/src/components/PopoverMenu.tsx +++ b/src/components/PopoverMenu.tsx @@ -4,6 +4,7 @@ import React, {useRef} from 'react'; import {View} from 'react-native'; import type {ModalProps} from 'react-native-modal'; import type {SvgProps} from 'react-native-svg'; +import type {ValueOf} from 'type-fest'; import useArrowKeyFocusManager from '@hooks/useArrowKeyFocusManager'; import useKeyboardShortcut from '@hooks/useKeyboardShortcut'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -50,9 +51,12 @@ type PopoverMenuProps = PopoverModalProps & { /** Callback method fired when the user requests to close the modal */ onClose: () => void; - /** Callback method fired when the modal is displayed */ + /** Callback method fired when the modal is shown */ onModalShow: () => void; + /** how to re-focus after the modal is dismissed */ + restoreFocusType?: ValueOf; + /** State that determines whether to display the modal or not */ isVisible: boolean; @@ -94,6 +98,7 @@ function PopoverMenu({ anchorPosition, anchorRef, onClose, + restoreFocusType, onModalShow, headerText, fromSidebarMediumScreen, @@ -138,6 +143,7 @@ function PopoverMenu({ anchorAlignment={anchorAlignment} onClose={onClose} isVisible={isVisible} + restoreFocusType={restoreFocusType} onModalShow={onModalShow} onModalHide={() => { setFocusedIndex(-1); diff --git a/src/libs/Navigation/AppNavigator/ModalNavigatorScreenOptions.ts b/src/libs/Navigation/AppNavigator/ModalNavigatorScreenOptions.ts index e49a94bf4ed..871b56d0263 100644 --- a/src/libs/Navigation/AppNavigator/ModalNavigatorScreenOptions.ts +++ b/src/libs/Navigation/AppNavigator/ModalNavigatorScreenOptions.ts @@ -13,6 +13,7 @@ const ModalNavigatorScreenOptions = (themeStyles: ThemeStyles): StackNavigationO gestureDirection: 'horizontal', cardStyle: themeStyles.navigationScreenCardStyle, cardStyleInterpolator: CardStyleInterpolators.forHorizontalIOS, + keyboardHandlingEnabled: false, }); export default ModalNavigatorScreenOptions; From 89cc964fe03de00ea1dff74125f96aceb9720b36 Mon Sep 17 00:00:00 2001 From: ntdiary <2471314@gmail.com> Date: Fri, 5 Jan 2024 21:41:31 +0800 Subject: [PATCH 11/28] clean up --- src/components/Composer/index.tsx | 7 +------ src/components/RNTextInput.tsx | 2 +- src/libs/Browser/index.website.ts | 6 +++--- src/libs/ComposerFocusManager.js | 8 ++++---- .../report/ReportActionCompose/ReportActionCompose.js | 9 +-------- 5 files changed, 10 insertions(+), 22 deletions(-) diff --git a/src/components/Composer/index.tsx b/src/components/Composer/index.tsx index c60bc3665e9..7e997a933f7 100755 --- a/src/components/Composer/index.tsx +++ b/src/components/Composer/index.tsx @@ -370,12 +370,7 @@ function Composer( rows={numberOfLines} disabled={isDisabled} onKeyPress={handleKeyPress} - onFocus={(e) => { - if (!props.onFocus) { - return; - } - props.onFocus?.(e); - }} + onFocus={(e) => props.onFocus?.(e)} /> {shouldCalculateCaretPosition && renderElementForCaretPosition} diff --git a/src/components/RNTextInput.tsx b/src/components/RNTextInput.tsx index 6530b19bbda..6117bf3f730 100644 --- a/src/components/RNTextInput.tsx +++ b/src/components/RNTextInput.tsx @@ -15,7 +15,7 @@ const AnimatedTextInput = Animated.createAnimatedComponent(TextInput); function RNTextInputWithRef(props: TextInputProps, ref: ForwardedRef>>) { const theme = useTheme(); - const inputRef = React.useRef> | null>(null); + const inputRef = React.useRef> | null>(null); React.useEffect(() => () => ComposerFocusManager.releaseInput(inputRef.current), []); diff --git a/src/libs/Browser/index.website.ts b/src/libs/Browser/index.website.ts index c09c24e69b5..4641e933090 100644 --- a/src/libs/Browser/index.website.ts +++ b/src/libs/Browser/index.website.ts @@ -101,7 +101,7 @@ const openRouteInDesktopApp: OpenRouteInDesktopApp = (shortLivedAuthToken = '', } }; -const isFileCancelSupported: IsFileCancelSupported = () => - // Chrome: 113 / Firefox: 91 / Safari 16.4 - false; +// Chrome: 113 / Firefox: 91 / Safari 16.4 +const isFileCancelSupported: IsFileCancelSupported = () => false; + export {getBrowser, isMobile, isMobileSafari, isSafari, isMobileChrome, openRouteInDesktopApp, isFileCancelSupported}; diff --git a/src/libs/ComposerFocusManager.js b/src/libs/ComposerFocusManager.js index c1d922f9f55..b6c271f6cf7 100644 --- a/src/libs/ComposerFocusManager.js +++ b/src/libs/ComposerFocusManager.js @@ -1,6 +1,6 @@ /* eslint-disable es/no-optional-chaining */ +import {last} from 'lodash'; import {TextInput} from 'react-native'; -import _ from 'underscore'; import CONST from '@src/CONST'; let focusedInput = null; @@ -173,7 +173,7 @@ function restoreFocusState(id, type = CONST.MODAL.RESTORE_FOCUS_TYPE.DEFAULT, sh } // find the topmost one - const [lastId, lastInput] = _.last([...focusMap]); + const [lastId, lastInput] = last([...focusMap]); if (!lastInput) { // TODO:del console.error('no, impossible'); @@ -207,7 +207,7 @@ function getKey(id) { if (promiseMap.size < 1) { return 0; } - return _.last([...promiseMap.keys()]); + return last([...promiseMap.keys()]); } function setReadyToFocus(id) { @@ -248,7 +248,7 @@ function tryRestoreFocusByExternal() { if (focusMap.size < 1) { return; } - const [key, input] = _.last([...focusMap]); + const [key, input] = last([...focusMap]); console.debug('oh, try to restore by external'); input.focus(); focusMap.delete(key); diff --git a/src/pages/home/report/ReportActionCompose/ReportActionCompose.js b/src/pages/home/report/ReportActionCompose/ReportActionCompose.js index 37b3466e709..7b680edc32d 100644 --- a/src/pages/home/report/ReportActionCompose/ReportActionCompose.js +++ b/src/pages/home/report/ReportActionCompose/ReportActionCompose.js @@ -266,14 +266,7 @@ function ReportActionCompose({ [onSubmit], ); - const onTriggerAttachmentPicker = useCallback(() => { - // Set a flag to block suggestion calculation until we're finished using the file picker, - // which will stop any flickering as the file picker opens on non-native devices. - if (!willBlurTextInputOnTapOutside) { - return; - } - suggestionsRef.current.setShouldBlockSuggestionCalc(true); - }, []); + const onTriggerAttachmentPicker = useCallback(() => {}, []); const onBlur = useCallback(() => { setIsFocused(false); From c200021bef3d8b11a26631ea74f6933c9d021ae0 Mon Sep 17 00:00:00 2001 From: ntdiary <2471314@gmail.com> Date: Fri, 5 Jan 2024 21:46:23 +0800 Subject: [PATCH 12/28] clean up --- .../home/report/ReportActionCompose/ReportActionCompose.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/pages/home/report/ReportActionCompose/ReportActionCompose.js b/src/pages/home/report/ReportActionCompose/ReportActionCompose.js index 7b680edc32d..cb20c2d2539 100644 --- a/src/pages/home/report/ReportActionCompose/ReportActionCompose.js +++ b/src/pages/home/report/ReportActionCompose/ReportActionCompose.js @@ -25,7 +25,6 @@ import * as DeviceCapabilities from '@libs/DeviceCapabilities'; import getModalState from '@libs/getModalState'; import * as ReportUtils from '@libs/ReportUtils'; import updatePropsPaperWorklet from '@libs/updatePropsPaperWorklet'; -import willBlurTextInputOnTapOutsideFunc from '@libs/willBlurTextInputOnTapOutside'; import ParticipantLocalTime from '@pages/home/report/ParticipantLocalTime'; import reportActionPropTypes from '@pages/home/report/reportActionPropTypes'; import ReportDropUI from '@pages/home/report/ReportDropUI'; @@ -95,8 +94,6 @@ const defaultProps = { // prevent auto focus on existing chat for mobile device const shouldFocusInputOnScreenFocus = canFocusInputOnScreenFocus(); -const willBlurTextInputOnTapOutside = willBlurTextInputOnTapOutsideFunc(); - function ReportActionCompose({ blockedFromConcierge, currentUserPersonalDetails, From 016521d8e7dd552f0c1a36901576dbaba879ae8f Mon Sep 17 00:00:00 2001 From: ntdiary <2471314@gmail.com> Date: Mon, 8 Jan 2024 15:54:20 +0800 Subject: [PATCH 13/28] clean up the console log --- src/libs/ComposerFocusManager.js | 73 +++++++------------------------- 1 file changed, 15 insertions(+), 58 deletions(-) diff --git a/src/libs/ComposerFocusManager.js b/src/libs/ComposerFocusManager.js index b6c271f6cf7..506c8fd7672 100644 --- a/src/libs/ComposerFocusManager.js +++ b/src/libs/ComposerFocusManager.js @@ -1,9 +1,12 @@ -/* eslint-disable es/no-optional-chaining */ import {last} from 'lodash'; import {TextInput} from 'react-native'; import CONST from '@src/CONST'; let focusedInput = null; +let uniqueModalId = 1; +const focusMap = new Map(); +const activeModals = []; +const promiseMap = new Map(); function getActiveInput() { return TextInput.State.currentlyFocusedInput ? TextInput.State.currentlyFocusedInput() : TextInput.State.currentlyFocusedField(); @@ -24,28 +27,11 @@ function clearFocusedInput() { if (!focusedInput) { return; } + // we have to use timeout because of measureLayout setTimeout(() => (focusedInput = null), CONST.ANIMATION_IN_TIMING); } -let uniqueModalId = 1; -const focusMap = new Map(); -const activeModals = []; -const promiseMap = new Map(); - -// TODO:debug -global.demo = new Proxy( - { - obj: () => [...focusMap.values()], - arr: () => activeModals, - promise: () => [...promiseMap], - el: () => focusedInput, - }, - { - get: (target, key) => target[key] && target[key](), - }, -); - /** * When a TextInput is unmounted, we also should release the reference here to avoid potential issues. * @@ -71,7 +57,7 @@ function getId() { } /** - * Cache the focus state before the modal appears. + * Save the focus state when opening the modal. * * @param {Number} id * @param {any} container @@ -88,7 +74,6 @@ function saveFocusState(id, container = undefined) { if (!input) { return; } - // TODO: can we refine this logic? if (container && container.contains(input)) { return; } @@ -123,40 +108,31 @@ function focus(input, shouldIgnoreFocused = false) { * @param {Boolean} shouldIgnoreFocused */ function restoreFocusState(id, type = CONST.MODAL.RESTORE_FOCUS_TYPE.DEFAULT, shouldIgnoreFocused = false) { - // TODO:del - console.debug(`restore ${id}, type is ${type}, active modals are`, activeModals.join()); if (!id) { - // TODO:del - console.debug('todo id empty'); return; } + + // The stack is empty if (activeModals.length < 1) { - // TODO:del - console.debug('stack is empty'); return; } const index = activeModals.indexOf(id); + + // This id has been removed from the stack. if (index < 0) { - // TODO:del - console.debug('activeModals does not contain this id'); return; } activeModals.splice(index, 1); if (type === CONST.MODAL.RESTORE_FOCUS_TYPE.PRESERVE) { - // TODO:del - console.debug('preserve input focus'); return; } if (type === CONST.MODAL.RESTORE_FOCUS_TYPE.DELETE) { - // TODO:del - console.debug('delete, no restore'); focusMap.delete(id); return; } + + // This modal is not the topmost one, do not restore it. if (activeModals.length > index) { - // this modal is not the topmost one, do not restore it. - // TODO:del - console.debug('modal is not the topmost one'); return; } const input = focusMap.get(id); @@ -166,33 +142,21 @@ function restoreFocusState(id, type = CONST.MODAL.RESTORE_FOCUS_TYPE.DEFAULT, sh return; } + // Try to find the topmost one and restore it if (focusMap.size < 1) { - // TODO:del - console.debug('obj is also empty, so return'); return; } - - // find the topmost one const [lastId, lastInput] = last([...focusMap]); - if (!lastInput) { - // TODO:del - console.error('no, impossible'); - return; - } + + // The previous modal is still active if (activeModals.indexOf(lastId) >= 0) { - // TODO:del - console.debug('the previous modal is still active'); return; } - // TODO:del - console.debug('ok, try to restore topmost'); focus(lastInput, shouldIgnoreFocused); focusMap.delete(lastId); } function resetReadyToFocus(id) { - // TODO:del - console.debug('reset ready to focus', id); const obj = {}; obj.ready = new Promise((resolve) => { obj.resolve = resolve; @@ -213,8 +177,6 @@ function getKey(id) { function setReadyToFocus(id) { const key = getKey(id); const promise = promiseMap.get(key); - // TODO:del - console.debug('set ready to focus', id, key); if (!promise) { return; } @@ -223,8 +185,6 @@ function setReadyToFocus(id) { } function removePromise(id) { - // TODO:del - console.debug('remove promise', id); const key = getKey(id); promiseMap.delete(key); } @@ -232,8 +192,6 @@ function removePromise(id) { function isReadyToFocus(id) { const key = getKey(id); const promise = promiseMap.get(key); - // TODO:del - console.debug('is ready to focus', id, key, promise); if (!promise) { return Promise.resolve(); } @@ -249,7 +207,6 @@ function tryRestoreFocusByExternal() { return; } const [key, input] = last([...focusMap]); - console.debug('oh, try to restore by external'); input.focus(); focusMap.delete(key); } From 40380d14f6fdbba33ffbb839cee445cdd93c2722 Mon Sep 17 00:00:00 2001 From: ntdiary <2471314@gmail.com> Date: Sat, 13 Jan 2024 00:03:53 +0800 Subject: [PATCH 14/28] use typescript --- src/components/Modal/ModalContent.js | 18 ---------------- src/components/Modal/ModalContent.web.js | 21 ------------------- .../Modal/ModalContent/index.native.tsx | 8 +++++++ src/components/Modal/ModalContent/index.tsx | 11 ++++++++++ src/components/Modal/ModalContent/types.ts | 11 ++++++++++ 5 files changed, 30 insertions(+), 39 deletions(-) delete mode 100644 src/components/Modal/ModalContent.js delete mode 100644 src/components/Modal/ModalContent.web.js create mode 100644 src/components/Modal/ModalContent/index.native.tsx create mode 100644 src/components/Modal/ModalContent/index.tsx create mode 100644 src/components/Modal/ModalContent/types.ts diff --git a/src/components/Modal/ModalContent.js b/src/components/Modal/ModalContent.js deleted file mode 100644 index a1be2765cd8..00000000000 --- a/src/components/Modal/ModalContent.js +++ /dev/null @@ -1,18 +0,0 @@ -import PropTypes from 'prop-types'; - -const propTypes = { - /** Modal contents */ - children: PropTypes.node.isRequired, - - /** called after modal content is dismissed */ - onDismiss: PropTypes.func, -}; - -function ModalContent({children}) { - return children; -} - -ModalContent.propTypes = propTypes; -ModalContent.displayName = 'ModalContent'; - -export default ModalContent; diff --git a/src/components/Modal/ModalContent.web.js b/src/components/Modal/ModalContent.web.js deleted file mode 100644 index 5aaba122d0d..00000000000 --- a/src/components/Modal/ModalContent.web.js +++ /dev/null @@ -1,21 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; - -const propTypes = { - /** Modal contents */ - children: PropTypes.node.isRequired, - - /** called after modal content is dismissed */ - onDismiss: PropTypes.func, -}; - -function ModalContent({children, onDismiss = () => {}}) { - // eslint-disable-next-line react-hooks/exhaustive-deps - React.useEffect(() => () => onDismiss(), []); - return children; -} - -ModalContent.propTypes = propTypes; -ModalContent.displayName = 'ModalContent'; - -export default ModalContent; diff --git a/src/components/Modal/ModalContent/index.native.tsx b/src/components/Modal/ModalContent/index.native.tsx new file mode 100644 index 00000000000..2f950495200 --- /dev/null +++ b/src/components/Modal/ModalContent/index.native.tsx @@ -0,0 +1,8 @@ +import type ModalContentProps from './types'; + +function ModalContent({children}: ModalContentProps) { + return children; +} +ModalContent.displayName = 'ModalContent'; + +export default ModalContent; diff --git a/src/components/Modal/ModalContent/index.tsx b/src/components/Modal/ModalContent/index.tsx new file mode 100644 index 00000000000..1c8afb8ce77 --- /dev/null +++ b/src/components/Modal/ModalContent/index.tsx @@ -0,0 +1,11 @@ +import {useEffect} from 'react'; +import type ModalContentProps from './types'; + +function ModalContent({children, onDismiss = () => {}}: ModalContentProps) { + // eslint-disable-next-line react-hooks/exhaustive-deps + useEffect(() => () => onDismiss(), []); + return children; +} +ModalContent.displayName = 'ModalContent'; + +export default ModalContent; diff --git a/src/components/Modal/ModalContent/types.ts b/src/components/Modal/ModalContent/types.ts new file mode 100644 index 00000000000..66c3ad2980d --- /dev/null +++ b/src/components/Modal/ModalContent/types.ts @@ -0,0 +1,11 @@ +import type {ReactNode} from 'react'; + +type ModalContentProps = { + /** Modal contents */ + children: ReactNode; + + /** called after modal content is dismissed */ + onDismiss: () => void; +}; + +export default ModalContentProps; From 716e20c3971ba0eb157bc289585635f6bc045300 Mon Sep 17 00:00:00 2001 From: ntdiary <2471314@gmail.com> Date: Mon, 15 Jan 2024 15:49:07 +0800 Subject: [PATCH 15/28] introduce businessType to remove dependence on file cancel event detection --- src/CONST.ts | 4 + .../Modal/ActiveModalProvider/index.tsx | 20 +++ .../Modal/ActiveModalProvider/types.ts | 21 +++ src/components/Modal/BaseModal.tsx | 12 +- src/components/Modal/ModalContent/index.tsx | 15 +- src/components/Modal/types.ts | 5 +- src/components/PopoverMenu.tsx | 7 +- .../PopoverWithoutOverlay/index.tsx | 8 +- src/libs/Browser/index.ts | 6 +- src/libs/Browser/index.website.ts | 7 +- src/libs/Browser/types.ts | 4 +- src/libs/ComposerFocusManager.js | 52 ++++--- .../Navigation/AppNavigator/AuthScreens.tsx | 2 +- .../AttachmentPickerWithMenuItems.js | 12 +- .../ReportActionCompose.js | 138 +++++++++--------- 15 files changed, 192 insertions(+), 121 deletions(-) create mode 100644 src/components/Modal/ActiveModalProvider/index.tsx create mode 100644 src/components/Modal/ActiveModalProvider/types.ts diff --git a/src/CONST.ts b/src/CONST.ts index 88563b87bed..e4264b947ad 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -710,6 +710,10 @@ const CONST = { RIGHT: 'right', }, POPOVER_MENU_PADDING: 8, + BUSINESS_TYPE: { + DEFAULT: 'default', + ATTACHMENT: 'attachment', + }, RESTORE_FOCUS_TYPE: { DEFAULT: 'default', DELETE: 'delete', diff --git a/src/components/Modal/ActiveModalProvider/index.tsx b/src/components/Modal/ActiveModalProvider/index.tsx new file mode 100644 index 00000000000..f6d579f7e99 --- /dev/null +++ b/src/components/Modal/ActiveModalProvider/index.tsx @@ -0,0 +1,20 @@ +import React, {useMemo} from 'react'; +import CONST from '@src/CONST'; +import type {ActiveModalContextProps, ActiveModalContextValue} from './types'; + +const ActiveModalContext = React.createContext({ + businessType: CONST.MODAL.BUSINESS_TYPE.DEFAULT, +}); + +function ActiveModalProvider({businessType, children}: ActiveModalContextProps) { + const contextValue = useMemo( + () => ({ + businessType, + }), + [businessType], + ); + return {children}; +} + +export default ActiveModalProvider; +export {ActiveModalContext}; diff --git a/src/components/Modal/ActiveModalProvider/types.ts b/src/components/Modal/ActiveModalProvider/types.ts new file mode 100644 index 00000000000..1b14417a7fd --- /dev/null +++ b/src/components/Modal/ActiveModalProvider/types.ts @@ -0,0 +1,21 @@ +import type {ReactNode} from 'react'; +import type {ValueOf} from 'type-fest'; +import type CONST from '@src/CONST'; + +type ActiveModalContextProps = { + /** + * So far, modern browsers only support the file cancel event in some newer versions + * (i.e., Chrome: 113 / Firefox: 91 / Safari 16.4), and there is no standard feature detection method available. + * Therefore, we introduce this prop to isolate the impact of the file upload modal on the focus stack. + */ + businessType: ValueOf; + + /** Children to render */ + children: ReactNode; +}; + +type ActiveModalContextValue = { + businessType: ValueOf; +}; + +export type {ActiveModalContextProps, ActiveModalContextValue}; diff --git a/src/components/Modal/BaseModal.tsx b/src/components/Modal/BaseModal.tsx index 9a207cb81d8..71f0cfd52e3 100644 --- a/src/components/Modal/BaseModal.tsx +++ b/src/components/Modal/BaseModal.tsx @@ -1,7 +1,8 @@ -import React, {forwardRef, useCallback, useEffect, useImperativeHandle, useMemo, useRef} from 'react'; +import React, {forwardRef, useCallback, useContext, useEffect, useImperativeHandle, useMemo, useRef} from 'react'; import {View} from 'react-native'; import ReactNativeModal from 'react-native-modal'; import ColorSchemeWrapper from '@components/ColorSchemeWrapper'; +import {ActiveModalContext} from '@components/Modal/ActiveModalProvider'; import usePrevious from '@hooks/usePrevious'; import useSafeAreaInsets from '@hooks/useSafeAreaInsets'; import useStyleUtils from '@hooks/useStyleUtils'; @@ -39,6 +40,7 @@ function BaseModal( statusBarTranslucent = true, onLayout, restoreFocusType, + shouldClearFocusWithType = false, avoidKeyboard = false, children, }: BaseModalProps, @@ -55,9 +57,9 @@ function BaseModal( const wasVisible = usePrevious(isVisible); const modalId = useMemo(() => ComposerFocusManager.getId(), []); - + const {businessType} = useContext(ActiveModalContext); const saveFocusState = () => { - ComposerFocusManager.saveFocusState(modalId); + ComposerFocusManager.saveFocusState(modalId, businessType, shouldClearFocusWithType); ComposerFocusManager.resetReadyToFocus(modalId); }; @@ -75,9 +77,9 @@ function BaseModal( onModalHide(); } Modal.onModalDidClose(); - ComposerFocusManager.tryRestoreFocusAfterClosedCompletely(modalId, restoreFocusType); + ComposerFocusManager.tryRestoreFocusAfterClosedCompletely(modalId, businessType, restoreFocusType); }, - [shouldSetModalVisibility, onModalHide, restoreFocusType, modalId], + [shouldSetModalVisibility, onModalHide, businessType, restoreFocusType, modalId], ); useEffect(() => { diff --git a/src/components/Modal/ModalContent/index.tsx b/src/components/Modal/ModalContent/index.tsx index 1c8afb8ce77..def24137d10 100644 --- a/src/components/Modal/ModalContent/index.tsx +++ b/src/components/Modal/ModalContent/index.tsx @@ -1,9 +1,18 @@ -import {useEffect} from 'react'; +import {useEffect, useRef} from 'react'; import type ModalContentProps from './types'; function ModalContent({children, onDismiss = () => {}}: ModalContentProps) { - // eslint-disable-next-line react-hooks/exhaustive-deps - useEffect(() => () => onDismiss(), []); + const dismissRef = useRef(onDismiss); + dismissRef.current = onDismiss; + useEffect( + () => () => { + if (typeof dismissRef.current !== 'function') { + return; + } + dismissRef.current(); + }, + [], + ); return children; } ModalContent.displayName = 'ModalContent'; diff --git a/src/components/Modal/types.ts b/src/components/Modal/types.ts index 571841f41a3..7871d28ea13 100644 --- a/src/components/Modal/types.ts +++ b/src/components/Modal/types.ts @@ -62,7 +62,10 @@ type BaseModalProps = WindowDimensionsProps & * */ hideModalContentWhileAnimating?: boolean; - /** how to re-focus after the modal is dismissed */ + /** Whether the modal should clear the focus record for the current business type. */ + shouldClearFocusWithType?: boolean; + + /** How to re-focus after the modal is dismissed */ restoreFocusType?: ValueOf; }; diff --git a/src/components/PopoverMenu.tsx b/src/components/PopoverMenu.tsx index 34a9fab2da4..e15ca602c5c 100644 --- a/src/components/PopoverMenu.tsx +++ b/src/components/PopoverMenu.tsx @@ -54,7 +54,10 @@ type PopoverMenuProps = PopoverModalProps & { /** Callback method fired when the modal is shown */ onModalShow: () => void; - /** how to re-focus after the modal is dismissed */ + /** Whether the modal should clear the focus record for the current business type. */ + shouldClearFocusWithType?: boolean; + + /** How to re-focus after the modal is dismissed */ restoreFocusType?: ValueOf; /** State that determines whether to display the modal or not */ @@ -98,6 +101,7 @@ function PopoverMenu({ anchorPosition, anchorRef, onClose, + shouldClearFocusWithType, restoreFocusType, onModalShow, headerText, @@ -151,6 +155,7 @@ function PopoverMenu({ anchorAlignment={anchorAlignment} onClose={onClose} isVisible={isVisible} + shouldClearFocusWithType={shouldClearFocusWithType} restoreFocusType={restoreFocusType} onModalShow={onModalShow} onModalHide={onModalHide} diff --git a/src/components/PopoverWithoutOverlay/index.tsx b/src/components/PopoverWithoutOverlay/index.tsx index d6236307eed..fdc63388e95 100644 --- a/src/components/PopoverWithoutOverlay/index.tsx +++ b/src/components/PopoverWithoutOverlay/index.tsx @@ -1,5 +1,5 @@ import type {ForwardedRef} from 'react'; -import React, {forwardRef, useContext, useEffect, useMemo, useRef} from 'react'; +import React, {forwardRef, useContext, useEffect, useMemo} from 'react'; import {View} from 'react-native'; import ColorSchemeWrapper from '@components/ColorSchemeWrapper'; import ModalContent from '@components/Modal/ModalContent'; @@ -58,7 +58,7 @@ function PopoverWithoutOverlay( anchorRef, }); removeOnClose = Modal.setCloseModal(() => onClose(anchorRef)); - ComposerFocusManager.saveFocusState(modalId, withoutOverlayRef.current); + ComposerFocusManager.saveFocusState(modalId, undefined, undefined, withoutOverlayRef.current); ComposerFocusManager.resetReadyToFocus(modalId); } else { onModalHide(); @@ -118,10 +118,8 @@ function PopoverWithoutOverlay( shouldAddTopSafeAreaPadding, ], ); - const restoreFocusTypeRef = useRef(); - restoreFocusTypeRef.current = restoreFocusType; const handleDismissContent = () => { - ComposerFocusManager.tryRestoreFocusAfterClosedCompletely(modalId, restoreFocusTypeRef.current); + ComposerFocusManager.tryRestoreFocusAfterClosedCompletely(modalId, undefined, restoreFocusType); // On the web platform, because there is no overlay, modal can be closed and opened instantly and randomly, // this will cause the input box to gain and lose focus instantly while the subsequent modal is opened. diff --git a/src/libs/Browser/index.ts b/src/libs/Browser/index.ts index deb02bfc25c..9569d358380 100644 --- a/src/libs/Browser/index.ts +++ b/src/libs/Browser/index.ts @@ -1,4 +1,4 @@ -import type {GetBrowser, IsFileCancelSupported, IsMobile, IsMobileChrome, IsMobileSafari, IsSafari, OpenRouteInDesktopApp} from './types'; +import type {GetBrowser, IsMobile, IsMobileChrome, IsMobileSafari, IsSafari, OpenRouteInDesktopApp} from './types'; const getBrowser: GetBrowser = () => ''; @@ -12,6 +12,4 @@ const isSafari: IsSafari = () => false; const openRouteInDesktopApp: OpenRouteInDesktopApp = () => {}; -const isFileCancelSupported: IsFileCancelSupported = () => true; - -export {getBrowser, isMobile, isMobileSafari, isSafari, isMobileChrome, openRouteInDesktopApp, isFileCancelSupported}; +export {getBrowser, isMobile, isMobileSafari, isSafari, isMobileChrome, openRouteInDesktopApp}; diff --git a/src/libs/Browser/index.website.ts b/src/libs/Browser/index.website.ts index 4641e933090..7e88e2bbd5f 100644 --- a/src/libs/Browser/index.website.ts +++ b/src/libs/Browser/index.website.ts @@ -1,7 +1,7 @@ import CONFIG from '@src/CONFIG'; import CONST from '@src/CONST'; import ROUTES from '@src/ROUTES'; -import type {GetBrowser, IsFileCancelSupported, IsMobile, IsMobileChrome, IsMobileSafari, IsSafari, OpenRouteInDesktopApp} from './types'; +import type {GetBrowser, IsMobile, IsMobileChrome, IsMobileSafari, IsSafari, OpenRouteInDesktopApp} from './types'; /** * Fetch browser name from UA string @@ -101,7 +101,4 @@ const openRouteInDesktopApp: OpenRouteInDesktopApp = (shortLivedAuthToken = '', } }; -// Chrome: 113 / Firefox: 91 / Safari 16.4 -const isFileCancelSupported: IsFileCancelSupported = () => false; - -export {getBrowser, isMobile, isMobileSafari, isSafari, isMobileChrome, openRouteInDesktopApp, isFileCancelSupported}; +export {getBrowser, isMobile, isMobileSafari, isSafari, isMobileChrome, openRouteInDesktopApp}; diff --git a/src/libs/Browser/types.ts b/src/libs/Browser/types.ts index 9bf81a4b87b..2e84dca3fd7 100644 --- a/src/libs/Browser/types.ts +++ b/src/libs/Browser/types.ts @@ -10,6 +10,4 @@ type IsSafari = () => boolean; type OpenRouteInDesktopApp = (shortLivedAuthToken?: string, email?: string) => void; -type IsFileCancelSupported = () => boolean; - -export type {GetBrowser, IsMobile, IsMobileSafari, IsMobileChrome, IsSafari, OpenRouteInDesktopApp, IsFileCancelSupported}; +export type {GetBrowser, IsMobile, IsMobileSafari, IsMobileChrome, IsSafari, OpenRouteInDesktopApp}; diff --git a/src/libs/ComposerFocusManager.js b/src/libs/ComposerFocusManager.js index 506c8fd7672..2d7f57bc60a 100644 --- a/src/libs/ComposerFocusManager.js +++ b/src/libs/ComposerFocusManager.js @@ -1,4 +1,4 @@ -import {last} from 'lodash'; +import {filter, last} from 'lodash'; import {TextInput} from 'react-native'; import CONST from '@src/CONST'; @@ -45,7 +45,7 @@ function releaseInput(input) { focusedInput = null; } [...focusMap].forEach(([key, value]) => { - if (value !== input) { + if (value.input !== input) { return; } focusMap.delete(key); @@ -60,9 +60,11 @@ function getId() { * Save the focus state when opening the modal. * * @param {Number} id + * @param {String} businessType + * @param {Boolean} shouldClearFocusWithType * @param {any} container */ -function saveFocusState(id, container = undefined) { +function saveFocusState(id, businessType = CONST.MODAL.BUSINESS_TYPE.DEFAULT, shouldClearFocusWithType = false, container = undefined) { const activeInput = getActiveInput(); // For popoverWithoutOverlay, react calls autofocus before useEffect. @@ -71,13 +73,23 @@ function saveFocusState(id, container = undefined) { if (activeModals.indexOf(id) < 0) { activeModals.push(id); } + + if (shouldClearFocusWithType) { + [...focusMap].forEach(([key, value]) => { + if (value.businessType !== businessType) { + return; + } + focusMap.delete(key); + }); + } + if (!input) { return; } if (container && container.contains(input)) { return; } - focusMap.set(id, input); + focusMap.set(id, {input, businessType}); input.blur(); } @@ -104,10 +116,11 @@ function focus(input, shouldIgnoreFocused = false) { * Restore the focus state after the modal is dismissed. * * @param {Number} id - * @param {String} type * @param {Boolean} shouldIgnoreFocused + * @param {String} businessType + * @param {String} ReFocusType */ -function restoreFocusState(id, type = CONST.MODAL.RESTORE_FOCUS_TYPE.DEFAULT, shouldIgnoreFocused = false) { +function restoreFocusState(id, shouldIgnoreFocused = false, businessType = CONST.MODAL.BUSINESS_TYPE.DEFAULT, ReFocusType = CONST.MODAL.RESTORE_FOCUS_TYPE.DEFAULT) { if (!id) { return; } @@ -123,10 +136,10 @@ function restoreFocusState(id, type = CONST.MODAL.RESTORE_FOCUS_TYPE.DEFAULT, sh return; } activeModals.splice(index, 1); - if (type === CONST.MODAL.RESTORE_FOCUS_TYPE.PRESERVE) { + if (ReFocusType === CONST.MODAL.RESTORE_FOCUS_TYPE.PRESERVE) { return; } - if (type === CONST.MODAL.RESTORE_FOCUS_TYPE.DELETE) { + if (ReFocusType === CONST.MODAL.RESTORE_FOCUS_TYPE.DELETE) { focusMap.delete(id); return; } @@ -135,18 +148,19 @@ function restoreFocusState(id, type = CONST.MODAL.RESTORE_FOCUS_TYPE.DEFAULT, sh if (activeModals.length > index) { return; } - const input = focusMap.get(id); - if (input) { - focus(input, shouldIgnoreFocused); + const value = focusMap.get(id); + if (value) { + focus(value.input, shouldIgnoreFocused); focusMap.delete(id); return; } // Try to find the topmost one and restore it - if (focusMap.size < 1) { + const stack = filter([...focusMap], ([, v]) => v.businessType === businessType); + if (stack.length < 1) { return; } - const [lastId, lastInput] = last([...focusMap]); + const [lastId, {input: lastInput}] = last(stack); // The previous modal is still active if (activeModals.indexOf(lastId) >= 0) { @@ -198,15 +212,19 @@ function isReadyToFocus(id) { return promise.ready; } -function tryRestoreFocusAfterClosedCompletely(id, restoreType) { - isReadyToFocus(id).then(() => restoreFocusState(id, restoreType)); +function tryRestoreFocusAfterClosedCompletely(id, businessType, restoreType) { + isReadyToFocus(id).then(() => restoreFocusState(id, false, businessType, restoreType)); } -function tryRestoreFocusByExternal() { +/** + * So far, this will only be called in file canceled event handler. + * @param {String} businessType + */ +function tryRestoreFocusByExternal(businessType) { if (focusMap.size < 1) { return; } - const [key, input] = last([...focusMap]); + const [key, {input}] = last(filter([...focusMap], ([, value]) => value.businessType === businessType)); input.focus(); focusMap.delete(key); } diff --git a/src/libs/Navigation/AppNavigator/AuthScreens.tsx b/src/libs/Navigation/AppNavigator/AuthScreens.tsx index 28dfbf3e6cf..e1c306dc52d 100644 --- a/src/libs/Navigation/AppNavigator/AuthScreens.tsx +++ b/src/libs/Navigation/AppNavigator/AuthScreens.tsx @@ -143,7 +143,7 @@ const modalScreenListeners = { Modal.setModalVisibility(true); }, beforeRemove: () => { - ComposerFocusManager.restoreFocusState(modalId, CONST.MODAL.RESTORE_FOCUS_TYPE.DEFAULT, true); + ComposerFocusManager.restoreFocusState(modalId, true); // Clear search input (WorkspaceInvitePage) when modal is closed SearchInputManager.searchInput = ''; diff --git a/src/pages/home/report/ReportActionCompose/AttachmentPickerWithMenuItems.js b/src/pages/home/report/ReportActionCompose/AttachmentPickerWithMenuItems.js index 09839ed904f..26d44f393f6 100644 --- a/src/pages/home/report/ReportActionCompose/AttachmentPickerWithMenuItems.js +++ b/src/pages/home/report/ReportActionCompose/AttachmentPickerWithMenuItems.js @@ -201,7 +201,7 @@ function AttachmentPickerWithMenuItems({ onTriggerAttachmentPicker(); openPicker({ onPicked: displayFileInModal, - onCanceled: () => ComposerFocusManager.tryRestoreFocusByExternal(), + onCanceled: () => ComposerFocusManager.tryRestoreFocusByExternal(CONST.MODAL.BUSINESS_TYPE.ATTACHMENT), }); }; const menuItems = [ @@ -291,6 +291,7 @@ function AttachmentPickerWithMenuItems({ - setIsAttachmentPreviewActive(true)} - onModalHide={onAttachmentPreviewClose} - > - {({displayFileInModal}) => ( - <> - { - restoreKeyboardState(); - }} - onMenuClosed={restoreKeyboardState} - actionButtonRef={actionButtonRef} - /> - - { - if (isAttachmentPreviewActive) { - return; - } - const data = lodashGet(e, ['dataTransfer', 'items', 0]); - displayFileInModal(data); - }} - /> - - )} - + + setIsAttachmentPreviewActive(true)} + onModalHide={onAttachmentPreviewClose} + > + {({displayFileInModal}) => ( + <> + { + restoreKeyboardState(); + }} + onMenuClosed={restoreKeyboardState} + actionButtonRef={actionButtonRef} + /> + + { + if (isAttachmentPreviewActive) { + return; + } + const data = lodashGet(e, ['dataTransfer', 'items', 0]); + displayFileInModal(data); + }} + /> + + )} + + + {DeviceCapabilities.canUseTouchScreen() && isMediumScreenWidth ? null : ( Date: Mon, 15 Jan 2024 18:14:25 +0800 Subject: [PATCH 16/28] fix lint error --- src/components/Modal/BaseModal.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Modal/BaseModal.tsx b/src/components/Modal/BaseModal.tsx index 71f0cfd52e3..dbaddfc8d0c 100644 --- a/src/components/Modal/BaseModal.tsx +++ b/src/components/Modal/BaseModal.tsx @@ -2,7 +2,6 @@ import React, {forwardRef, useCallback, useContext, useEffect, useImperativeHand import {View} from 'react-native'; import ReactNativeModal from 'react-native-modal'; import ColorSchemeWrapper from '@components/ColorSchemeWrapper'; -import {ActiveModalContext} from '@components/Modal/ActiveModalProvider'; import usePrevious from '@hooks/usePrevious'; import useSafeAreaInsets from '@hooks/useSafeAreaInsets'; import useStyleUtils from '@hooks/useStyleUtils'; @@ -14,6 +13,7 @@ import useNativeDriver from '@libs/useNativeDriver'; import variables from '@styles/variables'; import * as Modal from '@userActions/Modal'; import CONST from '@src/CONST'; +import {ActiveModalContext} from './ActiveModalProvider'; import ModalContent from './ModalContent'; import type BaseModalProps from './types'; import type {ModalRef} from './types'; From e6f1f007ab4fd3900d656c3f140cc1e2582bfb43 Mon Sep 17 00:00:00 2001 From: ntdiary <2471314@gmail.com> Date: Tue, 16 Jan 2024 16:55:12 +0800 Subject: [PATCH 17/28] improve business type detection --- src/components/Modal/ModalContent/index.tsx | 15 ++----- .../PopoverWithoutOverlay/index.tsx | 12 ++++-- src/libs/ComposerFocusManager.js | 39 +++++++++++-------- .../AttachmentPickerWithMenuItems.js | 3 +- 4 files changed, 36 insertions(+), 33 deletions(-) diff --git a/src/components/Modal/ModalContent/index.tsx b/src/components/Modal/ModalContent/index.tsx index def24137d10..4c9b03d428f 100644 --- a/src/components/Modal/ModalContent/index.tsx +++ b/src/components/Modal/ModalContent/index.tsx @@ -1,18 +1,9 @@ -import {useEffect, useRef} from 'react'; +import React from 'react'; import type ModalContentProps from './types'; function ModalContent({children, onDismiss = () => {}}: ModalContentProps) { - const dismissRef = useRef(onDismiss); - dismissRef.current = onDismiss; - useEffect( - () => () => { - if (typeof dismissRef.current !== 'function') { - return; - } - dismissRef.current(); - }, - [], - ); + // eslint-disable-next-line react-hooks/exhaustive-deps + React.useEffect(() => () => onDismiss?.(), []); return children; } ModalContent.displayName = 'ModalContent'; diff --git a/src/components/PopoverWithoutOverlay/index.tsx b/src/components/PopoverWithoutOverlay/index.tsx index fdc63388e95..086391a5994 100644 --- a/src/components/PopoverWithoutOverlay/index.tsx +++ b/src/components/PopoverWithoutOverlay/index.tsx @@ -1,7 +1,8 @@ import type {ForwardedRef} from 'react'; -import React, {forwardRef, useContext, useEffect, useMemo} from 'react'; +import React, {forwardRef, useContext, useEffect, useMemo, useRef} from 'react'; import {View} from 'react-native'; import ColorSchemeWrapper from '@components/ColorSchemeWrapper'; +import {ActiveModalContext} from '@components/Modal/ActiveModalProvider'; import ModalContent from '@components/Modal/ModalContent'; import {PopoverContext} from '@components/PopoverProvider'; import useSafeAreaInsets from '@hooks/useSafeAreaInsets'; @@ -24,6 +25,7 @@ function PopoverWithoutOverlay( isVisible, onClose, onModalHide = () => {}, + shouldClearFocusWithType, restoreFocusType, children, }: PopoverWithoutOverlayProps, @@ -35,6 +37,7 @@ function PopoverWithoutOverlay( const {windowWidth, windowHeight} = useWindowDimensions(); const modalId = useMemo(() => ComposerFocusManager.getId(), []); const insets = useSafeAreaInsets(); + const {businessType} = useContext(ActiveModalContext); const {modalStyle, modalContainerStyle, shouldAddTopSafeAreaMargin, shouldAddBottomSafeAreaMargin, shouldAddTopSafeAreaPadding, shouldAddBottomSafeAreaPadding} = StyleUtils.getModalStyles( 'popover', @@ -58,7 +61,7 @@ function PopoverWithoutOverlay( anchorRef, }); removeOnClose = Modal.setCloseModal(() => onClose(anchorRef)); - ComposerFocusManager.saveFocusState(modalId, undefined, undefined, withoutOverlayRef.current); + ComposerFocusManager.saveFocusState(modalId, businessType, shouldClearFocusWithType, withoutOverlayRef.current); ComposerFocusManager.resetReadyToFocus(modalId); } else { onModalHide(); @@ -118,8 +121,11 @@ function PopoverWithoutOverlay( shouldAddTopSafeAreaPadding, ], ); + + const restoreFocusTypeRef = useRef(); + restoreFocusTypeRef.current = restoreFocusType; const handleDismissContent = () => { - ComposerFocusManager.tryRestoreFocusAfterClosedCompletely(modalId, undefined, restoreFocusType); + ComposerFocusManager.tryRestoreFocusAfterClosedCompletely(modalId, businessType, restoreFocusTypeRef.current); // On the web platform, because there is no overlay, modal can be closed and opened instantly and randomly, // this will cause the input box to gain and lose focus instantly while the subsequent modal is opened. diff --git a/src/libs/ComposerFocusManager.js b/src/libs/ComposerFocusManager.js index 2d7f57bc60a..c336001c48a 100644 --- a/src/libs/ComposerFocusManager.js +++ b/src/libs/ComposerFocusManager.js @@ -83,13 +83,13 @@ function saveFocusState(id, businessType = CONST.MODAL.BUSINESS_TYPE.DEFAULT, sh }); } - if (!input) { - return; - } if (container && container.contains(input)) { return; } focusMap.set(id, {input, businessType}); + if (!input) { + return; + } input.blur(); } @@ -101,6 +101,9 @@ function saveFocusState(id, businessType = CONST.MODAL.BUSINESS_TYPE.DEFAULT, sh * @param {Boolean} shouldIgnoreFocused */ function focus(input, shouldIgnoreFocused = false) { + if (!input) { + return; + } if (shouldIgnoreFocused) { input.focus(); return; @@ -118,9 +121,9 @@ function focus(input, shouldIgnoreFocused = false) { * @param {Number} id * @param {Boolean} shouldIgnoreFocused * @param {String} businessType - * @param {String} ReFocusType + * @param {String} restoreFocusType */ -function restoreFocusState(id, shouldIgnoreFocused = false, businessType = CONST.MODAL.BUSINESS_TYPE.DEFAULT, ReFocusType = CONST.MODAL.RESTORE_FOCUS_TYPE.DEFAULT) { +function restoreFocusState(id, shouldIgnoreFocused = false, businessType = CONST.MODAL.BUSINESS_TYPE.DEFAULT, restoreFocusType = CONST.MODAL.RESTORE_FOCUS_TYPE.DEFAULT) { if (!id) { return; } @@ -136,27 +139,31 @@ function restoreFocusState(id, shouldIgnoreFocused = false, businessType = CONST return; } activeModals.splice(index, 1); - if (ReFocusType === CONST.MODAL.RESTORE_FOCUS_TYPE.PRESERVE) { + if (restoreFocusType === CONST.MODAL.RESTORE_FOCUS_TYPE.PRESERVE) { return; } - if (ReFocusType === CONST.MODAL.RESTORE_FOCUS_TYPE.DELETE) { - focusMap.delete(id); + + const {input} = focusMap.get(id) || {}; + focusMap.delete(id); + if (restoreFocusType === CONST.MODAL.RESTORE_FOCUS_TYPE.DELETE) { return; } // This modal is not the topmost one, do not restore it. if (activeModals.length > index) { + if (input) { + const lastId = last(activeModals); + focusMap.set(lastId, {...focusMap.get(lastId), input}); + } return; } - const value = focusMap.get(id); - if (value) { - focus(value.input, shouldIgnoreFocused); - focusMap.delete(id); + if (input) { + focus(input, shouldIgnoreFocused); return; } // Try to find the topmost one and restore it - const stack = filter([...focusMap], ([, v]) => v.businessType === businessType); + const stack = filter([...focusMap], ([, v]) => v.input && v.businessType === businessType); if (stack.length < 1) { return; } @@ -221,12 +228,12 @@ function tryRestoreFocusAfterClosedCompletely(id, businessType, restoreType) { * @param {String} businessType */ function tryRestoreFocusByExternal(businessType) { - if (focusMap.size < 1) { + const [key, {input}] = last(filter([...focusMap], ([, value]) => value.businessType === businessType)); + focusMap.delete(key); + if (!input) { return; } - const [key, {input}] = last(filter([...focusMap], ([, value]) => value.businessType === businessType)); input.focus(); - focusMap.delete(key); } export default { diff --git a/src/pages/home/report/ReportActionCompose/AttachmentPickerWithMenuItems.js b/src/pages/home/report/ReportActionCompose/AttachmentPickerWithMenuItems.js index 26d44f393f6..59cff4d0f48 100644 --- a/src/pages/home/report/ReportActionCompose/AttachmentPickerWithMenuItems.js +++ b/src/pages/home/report/ReportActionCompose/AttachmentPickerWithMenuItems.js @@ -298,12 +298,11 @@ function AttachmentPickerWithMenuItems({ onModalShow={() => setRestoreFocusType(CONST.MODAL.RESTORE_FOCUS_TYPE.DEFAULT)} onClose={onPopoverMenuClose} onItemSelected={(item, index) => { + setMenuVisibility(false); if (index !== menuItems.length - 1) { - setMenuVisibility(false); return; } setRestoreFocusType(CONST.MODAL.RESTORE_FOCUS_TYPE.PRESERVE); - setMenuVisibility(false); // In order for the file picker to open dynamically, the click // function must be called from within a event handler that was initiated From 1f6b9b8ec5e812872e2a5d3ddf6bda5d7c90e291 Mon Sep 17 00:00:00 2001 From: ntdiary <2471314@gmail.com> Date: Tue, 16 Jan 2024 17:04:35 +0800 Subject: [PATCH 18/28] waypoint edit migration --- src/pages/iou/request/step/IOURequestStepWaypoint.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/pages/iou/request/step/IOURequestStepWaypoint.js b/src/pages/iou/request/step/IOURequestStepWaypoint.js index 1087018eeed..c0aa405e8db 100644 --- a/src/pages/iou/request/step/IOURequestStepWaypoint.js +++ b/src/pages/iou/request/step/IOURequestStepWaypoint.js @@ -99,6 +99,8 @@ function IOURequestStepWaypoint({ const currentWaypoint = lodashGet(allWaypoints, `waypoint${pageIndex}`, {}); const waypointCount = _.size(allWaypoints); const filledWaypointCount = _.size(_.filter(allWaypoints, (waypoint) => !_.isEmpty(waypoint))); + + const [restoreFocusType, setRestoreFocusType] = useState(); const waypointDescriptionKey = useMemo(() => { switch (parsedWaypointIndex) { @@ -162,6 +164,7 @@ function IOURequestStepWaypoint({ }; const deleteStopAndHideModal = () => { + setRestoreFocusType(CONST.MODAL.RESTORE_FOCUS_TYPE.DELETE); Transaction.removeWaypoint(transaction, pageIndex, true); setIsDeleteStopModalOpen(false); Navigation.goBack(ROUTES.MONEY_REQUEST_DISTANCE_TAB.getRoute(iouType)); @@ -208,7 +211,10 @@ function IOURequestStepWaypoint({ { icon: Expensicons.Trashcan, text: translate('distance.deleteWaypoint'), - onSelected: () => setIsDeleteStopModalOpen(true), + onSelected: () => { + setRestoreFocusType(CONST.MODAL.RESTORE_FOCUS_TYPE.DEFAULT); + setIsDeleteStopModalOpen(true); + }, }, ]} /> @@ -221,6 +227,7 @@ function IOURequestStepWaypoint({ confirmText={translate('common.delete')} cancelText={translate('common.cancel')} danger + restoreFocusType={restoreFocusType} /> Date: Thu, 18 Jan 2024 00:04:16 +0800 Subject: [PATCH 19/28] fix ts check --- src/components/PopoverMenu.tsx | 4 ++-- .../home/report/ContextMenu/BaseReportActionContextMenu.tsx | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/PopoverMenu.tsx b/src/components/PopoverMenu.tsx index 5308ca3b74d..72e282b7542 100644 --- a/src/components/PopoverMenu.tsx +++ b/src/components/PopoverMenu.tsx @@ -52,7 +52,7 @@ type PopoverMenuProps = Partial & { onClose: () => void; /** Callback method fired when the modal is shown */ - onModalShow: () => void; + onModalShow?: () => void; /** Whether the modal should clear the focus record for the current business type. */ shouldClearFocusWithType?: boolean; @@ -103,7 +103,7 @@ function PopoverMenu({ onClose, shouldClearFocusWithType, restoreFocusType, - onModalShow, + onModalShow = () => {}, headerText, fromSidebarMediumScreen, anchorAlignment = { diff --git a/src/pages/home/report/ContextMenu/BaseReportActionContextMenu.tsx b/src/pages/home/report/ContextMenu/BaseReportActionContextMenu.tsx index cb2b313b718..66e028c74cf 100755 --- a/src/pages/home/report/ContextMenu/BaseReportActionContextMenu.tsx +++ b/src/pages/home/report/ContextMenu/BaseReportActionContextMenu.tsx @@ -79,7 +79,7 @@ type BaseReportActionContextMenuProps = BaseReportActionContextMenuOnyxProps & { contentRef?: RefObject; /** Callback to fire when a menu item is selected */ - onItemSelected: (action: ContextMenuAction) => void; + onItemSelected?: (action: ContextMenuAction) => void; }; type MenuItemRefs = Record; From 68e8e646da1b1287e2df2ae86e9de01cc49f23c4 Mon Sep 17 00:00:00 2001 From: ntdiary <2471314@gmail.com> Date: Tue, 23 Jan 2024 17:22:33 +0800 Subject: [PATCH 20/28] polish --- .../Modal/ActiveModalProvider/index.tsx | 20 ------------- src/components/Modal/BaseModal.tsx | 23 ++++++--------- .../Modal/ModalBusinessTypeProvider/index.tsx | 20 +++++++++++++ .../types.ts | 8 +++--- .../index.tsx => ModalContent.tsx} | 10 ++++++- .../Modal/ModalContent/index.native.tsx | 8 ------ src/components/Modal/ModalContent/types.ts | 11 -------- src/components/Modal/index.android.tsx | 25 ++--------------- src/components/Modal/types.ts | 6 ---- .../PopoverWithoutOverlay/index.tsx | 4 +-- src/libs/ComposerFocusManager.js | 28 +++++++++++-------- .../isWindowReadyToFocus/index.android.ts | 27 ++++++++++++++++++ src/libs/isWindowReadyToFocus/index.ts | 5 ++++ .../ReportActionCompose.js | 6 ++-- 14 files changed, 96 insertions(+), 105 deletions(-) delete mode 100644 src/components/Modal/ActiveModalProvider/index.tsx create mode 100644 src/components/Modal/ModalBusinessTypeProvider/index.tsx rename src/components/Modal/{ActiveModalProvider => ModalBusinessTypeProvider}/types.ts (65%) rename src/components/Modal/{ModalContent/index.tsx => ModalContent.tsx} (61%) delete mode 100644 src/components/Modal/ModalContent/index.native.tsx delete mode 100644 src/components/Modal/ModalContent/types.ts create mode 100644 src/libs/isWindowReadyToFocus/index.android.ts create mode 100644 src/libs/isWindowReadyToFocus/index.ts diff --git a/src/components/Modal/ActiveModalProvider/index.tsx b/src/components/Modal/ActiveModalProvider/index.tsx deleted file mode 100644 index f6d579f7e99..00000000000 --- a/src/components/Modal/ActiveModalProvider/index.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import React, {useMemo} from 'react'; -import CONST from '@src/CONST'; -import type {ActiveModalContextProps, ActiveModalContextValue} from './types'; - -const ActiveModalContext = React.createContext({ - businessType: CONST.MODAL.BUSINESS_TYPE.DEFAULT, -}); - -function ActiveModalProvider({businessType, children}: ActiveModalContextProps) { - const contextValue = useMemo( - () => ({ - businessType, - }), - [businessType], - ); - return {children}; -} - -export default ActiveModalProvider; -export {ActiveModalContext}; diff --git a/src/components/Modal/BaseModal.tsx b/src/components/Modal/BaseModal.tsx index dbaddfc8d0c..61168daed3f 100644 --- a/src/components/Modal/BaseModal.tsx +++ b/src/components/Modal/BaseModal.tsx @@ -1,4 +1,4 @@ -import React, {forwardRef, useCallback, useContext, useEffect, useImperativeHandle, useMemo, useRef} from 'react'; +import React, {forwardRef, useCallback, useContext, useEffect, useMemo, useRef} from 'react'; import {View} from 'react-native'; import ReactNativeModal from 'react-native-modal'; import ColorSchemeWrapper from '@components/ColorSchemeWrapper'; @@ -13,10 +13,9 @@ import useNativeDriver from '@libs/useNativeDriver'; import variables from '@styles/variables'; import * as Modal from '@userActions/Modal'; import CONST from '@src/CONST'; -import {ActiveModalContext} from './ActiveModalProvider'; +import {ModalBusinessTypeContext} from './ModalBusinessTypeProvider'; import ModalContent from './ModalContent'; import type BaseModalProps from './types'; -import type {ModalRef} from './types'; function BaseModal( { @@ -44,7 +43,7 @@ function BaseModal( avoidKeyboard = false, children, }: BaseModalProps, - ref: React.ForwardedRef, + ref: React.ForwardedRef, ) { const theme = useTheme(); const styles = useThemeStyles(); @@ -57,7 +56,7 @@ function BaseModal( const wasVisible = usePrevious(isVisible); const modalId = useMemo(() => ComposerFocusManager.getId(), []); - const {businessType} = useContext(ActiveModalContext); + const {businessType} = useContext(ModalBusinessTypeContext); const saveFocusState = () => { ComposerFocusManager.saveFocusState(modalId, businessType, shouldClearFocusWithType); ComposerFocusManager.resetReadyToFocus(modalId); @@ -130,15 +129,6 @@ function BaseModal( ComposerFocusManager.setReadyToFocus(modalId); }; - useImperativeHandle( - ref, - () => ({ - removePromise: () => ComposerFocusManager.removePromise(modalId), - setReadyToFocus: () => ComposerFocusManager.setReadyToFocus(modalId), - }), - [modalId], - ); - const { modalStyle, modalContainerStyle, @@ -222,7 +212,10 @@ function BaseModal( avoidKeyboard={avoidKeyboard} > - + {children} diff --git a/src/components/Modal/ModalBusinessTypeProvider/index.tsx b/src/components/Modal/ModalBusinessTypeProvider/index.tsx new file mode 100644 index 00000000000..ddeea87bbbd --- /dev/null +++ b/src/components/Modal/ModalBusinessTypeProvider/index.tsx @@ -0,0 +1,20 @@ +import React, {useMemo} from 'react'; +import CONST from '@src/CONST'; +import type {ModalBusinessTypeContextProps, ModalBusinessTypeContextValue} from './types'; + +const ModalBusinessTypeContext = React.createContext({ + businessType: CONST.MODAL.BUSINESS_TYPE.DEFAULT, +}); + +function ModalBusinessTypeProvider({businessType, children}: ModalBusinessTypeContextProps) { + const contextValue = useMemo( + () => ({ + businessType, + }), + [businessType], + ); + return {children}; +} + +export default ModalBusinessTypeProvider; +export {ModalBusinessTypeContext}; diff --git a/src/components/Modal/ActiveModalProvider/types.ts b/src/components/Modal/ModalBusinessTypeProvider/types.ts similarity index 65% rename from src/components/Modal/ActiveModalProvider/types.ts rename to src/components/Modal/ModalBusinessTypeProvider/types.ts index 1b14417a7fd..5ca3015ca08 100644 --- a/src/components/Modal/ActiveModalProvider/types.ts +++ b/src/components/Modal/ModalBusinessTypeProvider/types.ts @@ -2,10 +2,10 @@ import type {ReactNode} from 'react'; import type {ValueOf} from 'type-fest'; import type CONST from '@src/CONST'; -type ActiveModalContextProps = { +type ModalBusinessTypeContextProps = { /** * So far, modern browsers only support the file cancel event in some newer versions - * (i.e., Chrome: 113 / Firefox: 91 / Safari 16.4), and there is no standard feature detection method available. + * (i.e., Chrome: 113+ / Firefox: 91+ / Safari 16.4+), and there is no standard feature detection method available. * Therefore, we introduce this prop to isolate the impact of the file upload modal on the focus stack. */ businessType: ValueOf; @@ -14,8 +14,8 @@ type ActiveModalContextProps = { children: ReactNode; }; -type ActiveModalContextValue = { +type ModalBusinessTypeContextValue = { businessType: ValueOf; }; -export type {ActiveModalContextProps, ActiveModalContextValue}; +export type {ModalBusinessTypeContextProps, ModalBusinessTypeContextValue}; diff --git a/src/components/Modal/ModalContent/index.tsx b/src/components/Modal/ModalContent.tsx similarity index 61% rename from src/components/Modal/ModalContent/index.tsx rename to src/components/Modal/ModalContent.tsx index 4c9b03d428f..5c8e0d2ece6 100644 --- a/src/components/Modal/ModalContent/index.tsx +++ b/src/components/Modal/ModalContent.tsx @@ -1,5 +1,13 @@ +import type {ReactNode} from 'react'; import React from 'react'; -import type ModalContentProps from './types'; + +type ModalContentProps = { + /** Modal contents */ + children: ReactNode; + + /** called after modal content is dismissed */ + onDismiss: () => void; +}; function ModalContent({children, onDismiss = () => {}}: ModalContentProps) { // eslint-disable-next-line react-hooks/exhaustive-deps diff --git a/src/components/Modal/ModalContent/index.native.tsx b/src/components/Modal/ModalContent/index.native.tsx deleted file mode 100644 index 2f950495200..00000000000 --- a/src/components/Modal/ModalContent/index.native.tsx +++ /dev/null @@ -1,8 +0,0 @@ -import type ModalContentProps from './types'; - -function ModalContent({children}: ModalContentProps) { - return children; -} -ModalContent.displayName = 'ModalContent'; - -export default ModalContent; diff --git a/src/components/Modal/ModalContent/types.ts b/src/components/Modal/ModalContent/types.ts deleted file mode 100644 index 66c3ad2980d..00000000000 --- a/src/components/Modal/ModalContent/types.ts +++ /dev/null @@ -1,11 +0,0 @@ -import type {ReactNode} from 'react'; - -type ModalContentProps = { - /** Modal contents */ - children: ReactNode; - - /** called after modal content is dismissed */ - onDismiss: () => void; -}; - -export default ModalContentProps; diff --git a/src/components/Modal/index.android.tsx b/src/components/Modal/index.android.tsx index ea5f3fe2980..7cb2c608375 100644 --- a/src/components/Modal/index.android.tsx +++ b/src/components/Modal/index.android.tsx @@ -1,36 +1,15 @@ -import React, {useRef} from 'react'; -import {AppState} from 'react-native'; -import CONST from '@src/CONST'; +import React from 'react'; import BaseModal from './BaseModal'; import type BaseModalProps from './types'; -import type {ModalRef} from './types'; // Only want to use useNativeDriver on Android. It has strange flashes issue on IOS // https://github.com/react-native-modal/react-native-modal#the-modal-flashes-in-a-weird-way-when-animating -function Modal({useNativeDriver = true, restoreFocusType, onModalHide, ...rest}: BaseModalProps) { - const modalRef = useRef(null); - const hideModal = () => { - onModalHide?.(); - if (restoreFocusType && restoreFocusType !== CONST.MODAL.RESTORE_FOCUS_TYPE.DEFAULT) { - modalRef?.current?.removePromise(); - return; - } - const listener = AppState.addEventListener('focus', () => { - // TODO:del - console.debug('android is ready to focus'); - listener.remove(); - modalRef?.current?.setReadyToFocus(); - }); - }; - +function Modal({useNativeDriver = true, ...rest}: BaseModalProps) { return ( {rest.children} diff --git a/src/components/Modal/types.ts b/src/components/Modal/types.ts index f73de6bcb5c..febcb814de8 100644 --- a/src/components/Modal/types.ts +++ b/src/components/Modal/types.ts @@ -67,11 +67,5 @@ type BaseModalProps = Partial & { restoreFocusType?: ValueOf; }; -type ModalRef = { - removePromise: () => void; - setReadyToFocus: () => void; -}; - -export type {ModalRef}; export default BaseModalProps; export type {PopoverAnchorPosition}; diff --git a/src/components/PopoverWithoutOverlay/index.tsx b/src/components/PopoverWithoutOverlay/index.tsx index 5a911db576e..a7405f24afc 100644 --- a/src/components/PopoverWithoutOverlay/index.tsx +++ b/src/components/PopoverWithoutOverlay/index.tsx @@ -2,7 +2,7 @@ import type {ForwardedRef} from 'react'; import React, {forwardRef, useContext, useEffect, useMemo, useRef} from 'react'; import {View} from 'react-native'; import ColorSchemeWrapper from '@components/ColorSchemeWrapper'; -import {ActiveModalContext} from '@components/Modal/ActiveModalProvider'; +import {ModalBusinessTypeContext} from '@components/Modal/ModalBusinessTypeProvider'; import ModalContent from '@components/Modal/ModalContent'; import {PopoverContext} from '@components/PopoverProvider'; import useSafeAreaInsets from '@hooks/useSafeAreaInsets'; @@ -38,7 +38,7 @@ function PopoverWithoutOverlay( const {windowWidth, windowHeight} = useWindowDimensions(); const modalId = useMemo(() => ComposerFocusManager.getId(), []); const insets = useSafeAreaInsets(); - const {businessType} = useContext(ActiveModalContext); + const {businessType} = useContext(ModalBusinessTypeContext); const {modalStyle, modalContainerStyle, shouldAddTopSafeAreaMargin, shouldAddBottomSafeAreaMargin, shouldAddTopSafeAreaPadding, shouldAddBottomSafeAreaPadding} = StyleUtils.getModalStyles( 'popover', diff --git a/src/libs/ComposerFocusManager.js b/src/libs/ComposerFocusManager.js index c336001c48a..7bd1b616fb8 100644 --- a/src/libs/ComposerFocusManager.js +++ b/src/libs/ComposerFocusManager.js @@ -1,6 +1,7 @@ import {filter, last} from 'lodash'; import {TextInput} from 'react-native'; import CONST from '@src/CONST'; +import isWindowReadyToFocus from './isWindowReadyToFocus'; let focusedInput = null; let uniqueModalId = 1; @@ -8,6 +9,11 @@ const focusMap = new Map(); const activeModals = []; const promiseMap = new Map(); +/** + * react-native-web doesn't support `currentlyFocusedInput`, so we need to make it compatible. + * + * @return {React.ElementRef|HTMLElement} + */ function getActiveInput() { return TextInput.State.currentlyFocusedInput ? TextInput.State.currentlyFocusedInput() : TextInput.State.currentlyFocusedField(); } @@ -35,7 +41,7 @@ function clearFocusedInput() { /** * When a TextInput is unmounted, we also should release the reference here to avoid potential issues. * - * @param {TextInput | Falsy} input + * @param {Component|null} input */ function releaseInput(input) { if (!input) { @@ -95,7 +101,7 @@ function saveFocusState(id, businessType = CONST.MODAL.BUSINESS_TYPE.DEFAULT, sh /** * On web platform, if we intentionally click on another input box, there is no need to restore focus. - * But if we are closing the RHP, we can ignore the focused input. + * Additionally, if we are closing the RHP, we can ignore the focused input. * * @param {TextInput} input * @param {Boolean} shouldIgnoreFocused @@ -105,14 +111,14 @@ function focus(input, shouldIgnoreFocused = false) { return; } if (shouldIgnoreFocused) { - input.focus(); + isWindowReadyToFocus().then(() => input.focus()); return; } const activeInput = getActiveInput(); if (activeInput) { return; } - input.focus(); + isWindowReadyToFocus().then(() => input.focus()); } /** @@ -205,11 +211,6 @@ function setReadyToFocus(id) { promiseMap.delete(key); } -function removePromise(id) { - const key = getKey(id); - promiseMap.delete(key); -} - function isReadyToFocus(id) { const key = getKey(id); const promise = promiseMap.get(key); @@ -228,12 +229,16 @@ function tryRestoreFocusAfterClosedCompletely(id, businessType, restoreType) { * @param {String} businessType */ function tryRestoreFocusByExternal(businessType) { - const [key, {input}] = last(filter([...focusMap], ([, value]) => value.businessType === businessType)); + const stack = filter([...focusMap], ([, value]) => value.businessType === businessType && value.input); + if (stack.length < 1) { + return; + } + const [key, {input}] = last(stack); focusMap.delete(key); if (!input) { return; } - input.focus(); + focus(input); } export default { @@ -247,6 +252,5 @@ export default { setReadyToFocus, isReadyToFocus, tryRestoreFocusAfterClosedCompletely, - removePromise, tryRestoreFocusByExternal, }; diff --git a/src/libs/isWindowReadyToFocus/index.android.ts b/src/libs/isWindowReadyToFocus/index.android.ts new file mode 100644 index 00000000000..b9cca1b5a29 --- /dev/null +++ b/src/libs/isWindowReadyToFocus/index.android.ts @@ -0,0 +1,27 @@ +import {AppState} from 'react-native'; + +let isWindowReadyPromise = Promise.resolve(); +let resolveWindowReadyToFocus: () => void; + +AppState.addEventListener('focus', () => { + if (!resolveWindowReadyToFocus) { + return; + } + resolveWindowReadyToFocus(); +}); + +AppState.addEventListener('blur', () => { + isWindowReadyPromise = new Promise((resolve) => { + resolveWindowReadyToFocus = resolve; + }); +}); + +/** + * If we want to show the soft keyboard reliably, we need to ensure that the input's window gains focus first. + * Fortunately, we only need to manage the focus of the app window now, + * so we can achieve this by listening to the 'focus' event of the AppState. + * See {@link https://developer.android.com/develop/ui/views/touch-and-input/keyboard-input/visibility#ShowReliably} + */ +const isWindowReadyToFocus = () => isWindowReadyPromise; + +export default isWindowReadyToFocus; diff --git a/src/libs/isWindowReadyToFocus/index.ts b/src/libs/isWindowReadyToFocus/index.ts new file mode 100644 index 00000000000..666ab096cf0 --- /dev/null +++ b/src/libs/isWindowReadyToFocus/index.ts @@ -0,0 +1,5 @@ +const isWindowReadyToFocus = () => ({ + then: (callback: () => void) => callback?.(), +}); + +export default isWindowReadyToFocus; diff --git a/src/pages/home/report/ReportActionCompose/ReportActionCompose.js b/src/pages/home/report/ReportActionCompose/ReportActionCompose.js index 573dbd43a98..bd40af1fcff 100644 --- a/src/pages/home/report/ReportActionCompose/ReportActionCompose.js +++ b/src/pages/home/report/ReportActionCompose/ReportActionCompose.js @@ -9,7 +9,7 @@ import _ from 'underscore'; import AttachmentModal from '@components/AttachmentModal'; import EmojiPickerButton from '@components/EmojiPicker/EmojiPickerButton'; import ExceededCommentLength from '@components/ExceededCommentLength'; -import ActiveModalProvider from '@components/Modal/ActiveModalProvider'; +import ModalBusinessTypeProvider from '@components/Modal/ModalBusinessTypeProvider'; import OfflineIndicator from '@components/OfflineIndicator'; import OfflineWithFeedback from '@components/OfflineWithFeedback'; import {usePersonalDetails, withNetwork} from '@components/OnyxProvider'; @@ -341,7 +341,7 @@ function ReportActionCompose({ hasExceededMaxCommentLength && styles.borderColorDanger, ]} > - + )} - + {DeviceCapabilities.canUseTouchScreen() && isMediumScreenWidth ? null : ( Date: Wed, 24 Jan 2024 23:00:54 +0800 Subject: [PATCH 21/28] fix conflicts --- src/components/ConfirmModal.tsx | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/components/ConfirmModal.tsx b/src/components/ConfirmModal.tsx index f25fc978e3e..48813717707 100755 --- a/src/components/ConfirmModal.tsx +++ b/src/components/ConfirmModal.tsx @@ -4,6 +4,7 @@ import type {StyleProp, TextStyle, ViewStyle} from 'react-native'; import useWindowDimensions from '@hooks/useWindowDimensions'; import CONST from '@src/CONST'; import type IconAsset from '@src/types/utils/IconAsset'; +import type {ValueOf} from "type-fest"; import ConfirmContent from './ConfirmContent'; import Modal from './Modal'; @@ -64,6 +65,9 @@ type ConfirmModalProps = { /** Whether to stack the buttons */ shouldStackButtons?: boolean; + + /** How to re-focus after the modal is dismissed */ + restoreFocusType?: ValueOf; }; function ConfirmModal({ @@ -86,6 +90,7 @@ function ConfirmModal({ shouldStackButtons = true, isVisible, onConfirm, + restoreFocusType }: ConfirmModalProps) { const {isSmallScreenWidth} = useWindowDimensions(); @@ -97,6 +102,7 @@ function ConfirmModal({ shouldSetModalVisibility={shouldSetModalVisibility} onModalHide={onModalHide} type={isSmallScreenWidth ? CONST.MODAL.MODAL_TYPE.BOTTOM_DOCKED : CONST.MODAL.MODAL_TYPE.CONFIRM} + restoreFocusType={restoreFocusType} > Date: Thu, 25 Jan 2024 23:09:57 +0800 Subject: [PATCH 22/28] fix ts check --- src/libs/ComposerFocusManager.ts | 26 ++++++++++++--------- src/pages/home/report/ReportAttachments.tsx | 6 +---- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/src/libs/ComposerFocusManager.ts b/src/libs/ComposerFocusManager.ts index 2aaa0832a49..ec6a797e616 100644 --- a/src/libs/ComposerFocusManager.ts +++ b/src/libs/ComposerFocusManager.ts @@ -1,17 +1,18 @@ +import type { View} from 'react-native'; import {TextInput} from 'react-native'; import CONST from '@src/CONST'; import type {ValueOf} from "type-fest"; import isWindowReadyToFocus from './isWindowReadyToFocus'; -type ModalId = number; +type ModalId = number | undefined; type InputElement = TextInput & HTMLElement | null; -type BusinessType = ValueOf; +type BusinessType = ValueOf | undefined; -type RestoreFocusType = ValueOf; +type RestoreFocusType = ValueOf | undefined; -type ModalContainer = HTMLElement | undefined; +type ModalContainer = View | HTMLElement | undefined | null; type FocusMapValue = { input: InputElement, @@ -19,12 +20,12 @@ type FocusMapValue = { } type PromiseMapValue = { - ready?: Promise, - resolve?: () => void, + ready: Promise, + resolve: () => void, } let focusedInput: InputElement = null; -let uniqueModalId: ModalId = 1; +let uniqueModalId = 1; const focusMap = new Map(); const activeModals: ModalId[] = []; const promiseMap = new Map(); @@ -82,7 +83,7 @@ function getId() { /** * Save the focus state when opening the modal. */ -function saveFocusState(id: ModalId, businessType = CONST.MODAL.BUSINESS_TYPE.DEFAULT, shouldClearFocusWithType = false, container: ModalContainer = undefined) { +function saveFocusState(id: ModalId, businessType: BusinessType = CONST.MODAL.BUSINESS_TYPE.DEFAULT, shouldClearFocusWithType = false, container: ModalContainer = undefined) { const activeInput = getActiveInput(); // For popoverWithoutOverlay, react calls autofocus before useEffect. @@ -101,7 +102,7 @@ function saveFocusState(id: ModalId, businessType = CONST.MODAL.BUSINESS_TYPE.DE }); } - if (container && container.contains(input)) { + if (container instanceof HTMLElement && container?.contains(input)) { return; } focusMap.set(id, {input, businessType}); @@ -188,7 +189,10 @@ function restoreFocusState(id: ModalId, shouldIgnoreFocused = false, businessTyp } function resetReadyToFocus(id: ModalId) { - const promise: PromiseMapValue = {}; + const promise: PromiseMapValue = { + ready: Promise.resolve(), + resolve: () => {}, + }; promise.ready = new Promise((resolve) => { promise.resolve = resolve; }); @@ -215,7 +219,7 @@ function setReadyToFocus(id: ModalId) { promiseMap.delete(key); } -function isReadyToFocus(id: ModalId) { +function isReadyToFocus(id: ModalId = undefined) { const key = getKey(id); const promise = promiseMap.get(key); if (!promise) { diff --git a/src/pages/home/report/ReportAttachments.tsx b/src/pages/home/report/ReportAttachments.tsx index 42ca51cabe0..0c2f407dd7d 100644 --- a/src/pages/home/report/ReportAttachments.tsx +++ b/src/pages/home/report/ReportAttachments.tsx @@ -46,11 +46,7 @@ function ReportAttachments({route}: ReportAttachmentsProps) { defaultOpen report={report} source={source} - onModalHide={() => { - Navigation.dismissModal(); - // This enables Composer refocus when the attachments modal is closed by the browser navigation - ComposerFocusManager.setReadyToFocus(); - }} + onModalHide={() => Navigation.dismissModal()} onCarouselAttachmentChange={onCarouselAttachmentChange} /> ); From b826eca40413146b6d9d71a261175beb7c9d4b54 Mon Sep 17 00:00:00 2001 From: ntdiary <2471314@gmail.com> Date: Thu, 25 Jan 2024 23:11:32 +0800 Subject: [PATCH 23/28] fix lint error --- src/components/ConfirmModal.tsx | 4 ++-- src/libs/ComposerFocusManager.ts | 29 ++++++++++++++++------------- 2 files changed, 18 insertions(+), 15 deletions(-) diff --git a/src/components/ConfirmModal.tsx b/src/components/ConfirmModal.tsx index 48813717707..5cd0b60ce3f 100755 --- a/src/components/ConfirmModal.tsx +++ b/src/components/ConfirmModal.tsx @@ -1,10 +1,10 @@ import type {ReactNode} from 'react'; import React from 'react'; import type {StyleProp, TextStyle, ViewStyle} from 'react-native'; +import type {ValueOf} from 'type-fest'; import useWindowDimensions from '@hooks/useWindowDimensions'; import CONST from '@src/CONST'; import type IconAsset from '@src/types/utils/IconAsset'; -import type {ValueOf} from "type-fest"; import ConfirmContent from './ConfirmContent'; import Modal from './Modal'; @@ -90,7 +90,7 @@ function ConfirmModal({ shouldStackButtons = true, isVisible, onConfirm, - restoreFocusType + restoreFocusType, }: ConfirmModalProps) { const {isSmallScreenWidth} = useWindowDimensions(); diff --git a/src/libs/ComposerFocusManager.ts b/src/libs/ComposerFocusManager.ts index ec6a797e616..767a3242861 100644 --- a/src/libs/ComposerFocusManager.ts +++ b/src/libs/ComposerFocusManager.ts @@ -1,12 +1,12 @@ -import type { View} from 'react-native'; +import type {View} from 'react-native'; import {TextInput} from 'react-native'; +import type {ValueOf} from 'type-fest'; import CONST from '@src/CONST'; -import type {ValueOf} from "type-fest"; import isWindowReadyToFocus from './isWindowReadyToFocus'; type ModalId = number | undefined; -type InputElement = TextInput & HTMLElement | null; +type InputElement = (TextInput & HTMLElement) | null; type BusinessType = ValueOf | undefined; @@ -15,14 +15,14 @@ type RestoreFocusType = ValueOf | undefin type ModalContainer = View | HTMLElement | undefined | null; type FocusMapValue = { - input: InputElement, - businessType?: BusinessType, -} + input: InputElement; + businessType?: BusinessType; +}; type PromiseMapValue = { - ready: Promise, - resolve: () => void, -} + ready: Promise; + resolve: () => void; +}; let focusedInput: InputElement = null; let uniqueModalId = 1; @@ -134,7 +134,12 @@ function focus(input: InputElement, shouldIgnoreFocused = false) { /** * Restore the focus state after the modal is dismissed. */ -function restoreFocusState(id: ModalId, shouldIgnoreFocused = false, businessType: BusinessType = CONST.MODAL.BUSINESS_TYPE.DEFAULT, restoreFocusType: RestoreFocusType = CONST.MODAL.RESTORE_FOCUS_TYPE.DEFAULT) { +function restoreFocusState( + id: ModalId, + shouldIgnoreFocused = false, + businessType: BusinessType = CONST.MODAL.BUSINESS_TYPE.DEFAULT, + restoreFocusType: RestoreFocusType = CONST.MODAL.RESTORE_FOCUS_TYPE.DEFAULT, +) { if (!id) { return; } @@ -248,9 +253,7 @@ function tryRestoreFocusByExternal(businessType: BusinessType) { focus(input); } -export type { - InputElement -} +export type {InputElement}; export default { getId, From 51d8928209c5cb15f6d363bd39c746f0eebec6b3 Mon Sep 17 00:00:00 2001 From: ntdiary <2471314@gmail.com> Date: Thu, 25 Jan 2024 23:15:37 +0800 Subject: [PATCH 24/28] remove unused import --- src/pages/home/report/ReportAttachments.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/pages/home/report/ReportAttachments.tsx b/src/pages/home/report/ReportAttachments.tsx index 0c2f407dd7d..7a804dc9efb 100644 --- a/src/pages/home/report/ReportAttachments.tsx +++ b/src/pages/home/report/ReportAttachments.tsx @@ -1,7 +1,6 @@ import type {StackScreenProps} from '@react-navigation/stack'; import React, {useCallback} from 'react'; import AttachmentModal from '@components/AttachmentModal'; -import ComposerFocusManager from '@libs/ComposerFocusManager'; import Navigation from '@libs/Navigation/Navigation'; import type {AuthScreensParamList} from '@libs/Navigation/types'; import * as ReportUtils from '@libs/ReportUtils'; From cc009cc1946fcd1b6ab674326f17050979485812 Mon Sep 17 00:00:00 2001 From: ntdiary <2471314@gmail.com> Date: Mon, 29 Jan 2024 17:26:14 +0800 Subject: [PATCH 25/28] fix app crash --- src/libs/ComposerFocusManager.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/ComposerFocusManager.ts b/src/libs/ComposerFocusManager.ts index 767a3242861..c028b2dee0c 100644 --- a/src/libs/ComposerFocusManager.ts +++ b/src/libs/ComposerFocusManager.ts @@ -102,7 +102,7 @@ function saveFocusState(id: ModalId, businessType: BusinessType = CONST.MODAL.BU }); } - if (container instanceof HTMLElement && container?.contains(input)) { + if (container && ('contains' in container) && container.contains(input)) { return; } focusMap.set(id, {input, businessType}); From f853d1383bac4a76de6aaf764394a1574a73145d Mon Sep 17 00:00:00 2001 From: ntdiary <2471314@gmail.com> Date: Mon, 29 Jan 2024 22:34:38 +0800 Subject: [PATCH 26/28] fix android preview modal --- src/components/Composer/index.native.tsx | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/components/Composer/index.native.tsx b/src/components/Composer/index.native.tsx index c7b020a5c6d..db2e7e38480 100644 --- a/src/components/Composer/index.native.tsx +++ b/src/components/Composer/index.native.tsx @@ -4,7 +4,6 @@ import type {TextInput} from 'react-native'; import {StyleSheet} from 'react-native'; import type {AnimatedTextInputRef} from '@components/RNTextInput'; import RNTextInput from '@components/RNTextInput'; -import useResetComposerFocus from '@hooks/useResetComposerFocus'; import useStyleUtils from '@hooks/useStyleUtils'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -31,7 +30,6 @@ function Composer( ref: ForwardedRef, ) { const textInput = useRef(null); - const {isFocused, shouldResetFocus} = useResetComposerFocus(textInput); const theme = useTheme(); const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); @@ -81,12 +79,6 @@ function Composer( /* eslint-disable-next-line react/jsx-props-no-spreading */ {...props} readOnly={isDisabled} - onBlur={(e) => { - if (!isFocused) { - shouldResetFocus.current = true; // detect the input is blurred when the page is hidden - } - props?.onBlur?.(e); - }} /> ); } From 40ff044a58a24937128c8f066f0a00a4eacf55d5 Mon Sep 17 00:00:00 2001 From: ntdiary <2471314@gmail.com> Date: Mon, 29 Jan 2024 22:35:04 +0800 Subject: [PATCH 27/28] fix android AddReaction button --- .../report/ContextMenu/BaseReportActionContextMenu.tsx | 1 + src/pages/home/report/ContextMenu/ContextMenuActions.tsx | 9 +++++++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/pages/home/report/ContextMenu/BaseReportActionContextMenu.tsx b/src/pages/home/report/ContextMenu/BaseReportActionContextMenu.tsx index 69eedbd1fb3..65cab49d41c 100755 --- a/src/pages/home/report/ContextMenu/BaseReportActionContextMenu.tsx +++ b/src/pages/home/report/ContextMenu/BaseReportActionContextMenu.tsx @@ -232,6 +232,7 @@ function BaseReportActionContextMenu({ openContextMenu: () => setShouldKeepOpen(true), interceptAnonymousUser, openOverflowMenu, + onPressAddReaction: () => onItemSelected(contextAction), }; if ('renderContent' in contextAction) { diff --git a/src/pages/home/report/ContextMenu/ContextMenuActions.tsx b/src/pages/home/report/ContextMenu/ContextMenuActions.tsx index 4cd8cb30aff..ebc59eee638 100644 --- a/src/pages/home/report/ContextMenu/ContextMenuActions.tsx +++ b/src/pages/home/report/ContextMenu/ContextMenuActions.tsx @@ -77,6 +77,7 @@ type ContextMenuActionPayload = { interceptAnonymousUser: (callback: () => void, isAnonymousAction?: boolean) => void; openOverflowMenu: (event: GestureResponderEvent | MouseEvent) => void; event?: GestureResponderEvent | MouseEvent | KeyboardEvent; + onPressAddReaction?: () => void; }; type OnPress = (closePopover: boolean, payload: ContextMenuActionPayload, selection?: string, reportID?: string, draftMessage?: string) => void; @@ -108,9 +109,10 @@ type ContextMenuAction = (ContextMenuActionWithContent | ContextMenuActionWithIc const ContextMenuActions: ContextMenuAction[] = [ { isAnonymousAction: false, + restoreType: CONST.MODAL.RESTORE_FOCUS_TYPE.PRESERVE, shouldShow: (type, reportAction): reportAction is ReportAction => type === CONST.CONTEXT_MENU_TYPES.REPORT_ACTION && !!reportAction && 'message' in reportAction && !ReportActionsUtils.isMessageDeleted(reportAction), - renderContent: (closePopover, {reportID, reportAction, close: closeManually, openContextMenu}) => { + renderContent: (closePopover, {reportID, reportAction, close: closeManually, openContextMenu, onPressAddReaction}) => { const isMini = !closePopover; const closeContextMenu = (onHideCallback?: () => void) => { @@ -145,7 +147,10 @@ const ContextMenuActions: ContextMenuAction[] = [ return ( { + onPressAddReaction?.(); + closeContextMenu(onHideCallback); + }} onEmojiSelected={toggleEmojiAndCloseMenu} reportActionID={reportAction?.reportActionID} reportAction={reportAction} From bd84c65080b99754be625536efb20092f90a51ac Mon Sep 17 00:00:00 2001 From: ntdiary <2471314@gmail.com> Date: Tue, 30 Jan 2024 00:15:26 +0800 Subject: [PATCH 28/28] lint error --- src/components/RNTextInput.tsx | 5 +++-- src/libs/ComposerFocusManager.ts | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/components/RNTextInput.tsx b/src/components/RNTextInput.tsx index e4509e413be..7f75e6b2d83 100644 --- a/src/components/RNTextInput.tsx +++ b/src/components/RNTextInput.tsx @@ -1,8 +1,9 @@ -import type {ForwardedRef} from 'react'; +import type {Component, ForwardedRef} from 'react'; import React from 'react'; // eslint-disable-next-line no-restricted-imports import type {TextInputProps} from 'react-native'; import {TextInput} from 'react-native'; +import type {AnimatedProps} from 'react-native-reanimated'; import Animated from 'react-native-reanimated'; import useTheme from '@hooks/useTheme'; import type {InputElement} from '@libs/ComposerFocusManager'; @@ -27,7 +28,7 @@ function RNTextInputWithRef(props: TextInputProps, ref: ForwardedRef { if (refHandle) { - (inputRef.current as AnimatedTextInputRef) = refHandle; + (inputRef.current as Component>) = refHandle; } if (typeof ref !== 'function') { return; diff --git a/src/libs/ComposerFocusManager.ts b/src/libs/ComposerFocusManager.ts index c028b2dee0c..03219c7ca2a 100644 --- a/src/libs/ComposerFocusManager.ts +++ b/src/libs/ComposerFocusManager.ts @@ -102,7 +102,7 @@ function saveFocusState(id: ModalId, businessType: BusinessType = CONST.MODAL.BU }); } - if (container && ('contains' in container) && container.contains(input)) { + if (container && 'contains' in container && container.contains(input)) { return; } focusMap.set(id, {input, businessType});