diff --git a/src/components/AttachmentModal.tsx b/src/components/AttachmentModal.tsx index f76ade0db879..a966d9f33a95 100644 --- a/src/components/AttachmentModal.tsx +++ b/src/components/AttachmentModal.tsx @@ -534,6 +534,11 @@ function AttachmentModal({ onCloseButtonPress={closeModal} shouldShowThreeDotsButton={shouldShowThreeDotsButton} threeDotsAnchorPosition={styles.threeDotsPopoverOffsetAttachmentModal(windowWidth)} + threeDotsAnchorAlignment={{ + horizontal: CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL.LEFT, + vertical: CONST.MODAL.ANCHOR_ORIGIN_VERTICAL.TOP, + }} + shouldSetModalVisibility={false} threeDotsMenuItems={threeDotsMenuItems} shouldOverlayDots subTitleLink={currentAttachmentLink ?? ''} diff --git a/src/components/SidePane/HelpComponents/HelpButton.tsx b/src/components/SidePane/HelpComponents/HelpButton.tsx index 3f45e31b0d77..4d1e89f34da3 100644 --- a/src/components/SidePane/HelpComponents/HelpButton.tsx +++ b/src/components/SidePane/HelpComponents/HelpButton.tsx @@ -5,12 +5,9 @@ import * as Expensicons from '@components/Icon/Expensicons'; import {PressableWithoutFeedback} from '@components/Pressable'; import Tooltip from '@components/Tooltip'; import useLocalize from '@hooks/useLocalize'; -import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useSidePane from '@hooks/useSidePane'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; -import {triggerSidePane} from '@libs/actions/SidePane'; -import KeyboardUtils from '@src/utils/keyboard'; type HelpButtonProps = { style?: StyleProp; @@ -20,8 +17,7 @@ function HelpButton({style}: HelpButtonProps) { const styles = useThemeStyles(); const theme = useTheme(); const {translate} = useLocalize(); - const {isExtraLargeScreenWidth} = useResponsiveLayout(); - const {sidePane, shouldHideHelpButton} = useSidePane(); + const {openSidePane, shouldHideHelpButton} = useSidePane(); if (shouldHideHelpButton) { return null; @@ -32,13 +28,7 @@ function HelpButton({style}: HelpButtonProps) { { - KeyboardUtils.dismiss(); - triggerSidePane({ - isOpen: isExtraLargeScreenWidth ? !sidePane?.open : !sidePane?.openNarrowScreen, - isOpenNarrowScreen: isExtraLargeScreenWidth ? undefined : !sidePane?.openNarrowScreen, - }); - }} + onPress={openSidePane} > { @@ -23,7 +23,7 @@ function Help({isPaneHidden, closeSidePane}: HelpProps) { return ( closeSidePane()} - isVisible={!isPaneHidden} + isVisible={!shouldHideSidePane} type={CONST.MODAL.MODAL_TYPE.RIGHT_DOCKED} shouldHandleNavigationBack > diff --git a/src/components/SidePane/HelpModal/index.ios.tsx b/src/components/SidePane/HelpModal/index.ios.tsx index d249bdf316a9..9939cac5fc19 100644 --- a/src/components/SidePane/HelpModal/index.ios.tsx +++ b/src/components/SidePane/HelpModal/index.ios.tsx @@ -4,11 +4,11 @@ import HelpContent from '@components/SidePane/HelpComponents/HelpContent'; import CONST from '@src/CONST'; import type HelpProps from './types'; -function Help({isPaneHidden, closeSidePane}: HelpProps) { +function Help({shouldHideSidePane, closeSidePane}: HelpProps) { return ( closeSidePane()} - isVisible={!isPaneHidden} + isVisible={!shouldHideSidePane} type={CONST.MODAL.MODAL_TYPE.RIGHT_DOCKED} shouldHandleNavigationBack propagateSwipe diff --git a/src/components/SidePane/HelpModal/index.tsx b/src/components/SidePane/HelpModal/index.tsx index eb4d6cf0ca2b..5ada2302e17d 100644 --- a/src/components/SidePane/HelpModal/index.tsx +++ b/src/components/SidePane/HelpModal/index.tsx @@ -56,7 +56,7 @@ function Help({sidePaneTranslateX, closeSidePane, shouldHideSidePaneBackdrop}: H return ( - + {!shouldHideSidePaneBackdrop && ( diff --git a/src/components/SidePane/HelpModal/types.ts b/src/components/SidePane/HelpModal/types.ts index f54cff0d3d5c..6c7cccdcae6c 100644 --- a/src/components/SidePane/HelpModal/types.ts +++ b/src/components/SidePane/HelpModal/types.ts @@ -3,7 +3,7 @@ import type {MutableRefObject} from 'react'; import type {Animated} from 'react-native'; type HelpProps = { - isPaneHidden: boolean; + shouldHideSidePane: boolean; sidePaneTranslateX: MutableRefObject; shouldHideSidePaneBackdrop: boolean; closeSidePane: (shouldUpdateNarrow?: boolean) => void; diff --git a/src/components/SidePane/index.tsx b/src/components/SidePane/index.tsx index 73aeab56434f..d463de34e1e8 100644 --- a/src/components/SidePane/index.tsx +++ b/src/components/SidePane/index.tsx @@ -3,15 +3,15 @@ import useSidePane from '@hooks/useSidePane'; import Help from './HelpModal'; function SidePane() { - const {shouldHideSidePane, isPaneHidden, sidePaneTranslateX, shouldHideSidePaneBackdrop, closeSidePane} = useSidePane(); + const {isSidePaneTransitionEnded, shouldHideSidePane, sidePaneTranslateX, shouldHideSidePaneBackdrop, closeSidePane} = useSidePane(); - if (shouldHideSidePane) { + if (isSidePaneTransitionEnded && shouldHideSidePane) { return null; } return ( void; @@ -53,6 +54,16 @@ export default function useAutoFocusInput(isMultiline = false): UseAutoFocusInpu }, []), ); + // Trigger focus when side pane transition ends + const {isSidePaneTransitionEnded, shouldHideSidePane} = useSidePane(); + useEffect(() => { + if (!shouldHideSidePane) { + return; + } + + setIsScreenTransitionEnded(isSidePaneTransitionEnded); + }, [isSidePaneTransitionEnded, shouldHideSidePane]); + const inputCallbackRef = (ref: TextInput | null) => { inputRef.current = ref; if (isInputInitialized) { diff --git a/src/hooks/useSidePane.ts b/src/hooks/useSidePane.ts index d645f9bd55fa..d0d579086bd6 100644 --- a/src/hooks/useSidePane.ts +++ b/src/hooks/useSidePane.ts @@ -2,7 +2,6 @@ import {useCallback, useEffect, useRef, useState} from 'react'; // Import Animated directly from 'react-native' as animations are used with navigation. // eslint-disable-next-line no-restricted-imports import {Animated} from 'react-native'; -import type {OnyxEntry} from 'react-native-onyx'; import {useOnyx} from 'react-native-onyx'; import {triggerSidePane} from '@libs/actions/SidePane'; import focusComposerWithDelay from '@libs/focusComposerWithDelay'; @@ -10,61 +9,63 @@ import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManag import variables from '@styles/variables'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import type * as OnyxTypes from '@src/types/onyx'; +import KeyboardUtils from '@src/utils/keyboard'; import useResponsiveLayout from './useResponsiveLayout'; import useWindowDimensions from './useWindowDimensions'; -function isSidePaneHidden(sidePane: OnyxEntry, isExtraLargeScreenWidth: boolean) { - if (!isExtraLargeScreenWidth && !sidePane?.openNarrowScreen) { - return true; - } - - return isExtraLargeScreenWidth && !sidePane?.open; -} - /** - * Hook to get the animated position of the side pane and the margin of the navigator + * Hook to get the display status of the side pane */ -function useSidePane() { +function useSidePaneDisplayStatus() { const {isExtraLargeScreenWidth, shouldUseNarrowLayout} = useResponsiveLayout(); - const {windowWidth} = useWindowDimensions(); - const [sidePaneNVP] = useOnyx(ONYXKEYS.NVP_SIDE_PANE); const [language] = useOnyx(ONYXKEYS.NVP_PREFERRED_LOCALE); const [isModalCenteredVisible = false] = useOnyx(ONYXKEYS.MODAL, { selector: (modal) => modal?.type === CONST.MODAL.MODAL_TYPE.CENTERED_SWIPABLE_TO_RIGHT || modal?.type === CONST.MODAL.MODAL_TYPE.CENTERED_UNSWIPEABLE || - modal?.type === CONST.MODAL.MODAL_TYPE.CENTERED_SMALL, + modal?.type === CONST.MODAL.MODAL_TYPE.CENTERED_SMALL || + modal?.type === CONST.MODAL.MODAL_TYPE.CENTERED, }); - const isLanguageUnsupported = language !== CONST.LOCALES.EN; - const isPaneHidden = isSidePaneHidden(sidePaneNVP, isExtraLargeScreenWidth) || isLanguageUnsupported || isModalCenteredVisible; - const sidePaneWidth = shouldUseNarrowLayout ? windowWidth : variables.sideBarWidth; - const shouldApplySidePaneOffset = isExtraLargeScreenWidth && !isPaneHidden; - - const [shouldHideSidePane, setShouldHideSidePane] = useState(true); - const [isAnimatingExtraLargeScree, setIsAnimatingExtraLargeScreen] = useState(false); + const isLanguageUnsupported = language !== CONST.LOCALES.EN; + const isSidePaneVisible = isExtraLargeScreenWidth ? sidePaneNVP?.open : sidePaneNVP?.openNarrowScreen; - const shouldHideSidePaneBackdrop = isPaneHidden || isExtraLargeScreenWidth || shouldUseNarrowLayout; - const shouldHideToolTip = isExtraLargeScreenWidth ? isAnimatingExtraLargeScree : !shouldHideSidePane; + // The side pane is hidden when: + // - NVP is not set or it is false + // - language is unsupported + // - modal centered is visible + const shouldHideSidePane = !isSidePaneVisible || isLanguageUnsupported || isModalCenteredVisible; + const isSidePaneHiddenOrLargeScreen = !isSidePaneVisible || isLanguageUnsupported || isExtraLargeScreenWidth; // The help button is hidden when: // - side pane nvp is not set // - side pane is displayed currently // - language is unsupported - const shouldHideHelpButton = !sidePaneNVP || !isPaneHidden || isLanguageUnsupported; + const shouldHideHelpButton = !sidePaneNVP || !shouldHideSidePane || isLanguageUnsupported; + const shouldHideSidePaneBackdrop = shouldHideSidePane || isExtraLargeScreenWidth || shouldUseNarrowLayout; + return {shouldHideSidePane, isSidePaneHiddenOrLargeScreen, shouldHideHelpButton, shouldHideSidePaneBackdrop, sidePaneNVP}; +} + +/** + * Hook to get the animated position of the side pane and the margin of the navigator + */ +function useSidePane() { + const {isExtraLargeScreenWidth, shouldUseNarrowLayout} = useResponsiveLayout(); + const {windowWidth} = useWindowDimensions(); + const sidePaneWidth = shouldUseNarrowLayout ? windowWidth : variables.sideBarWidth; + + const [isSidePaneTransitionEnded, setIsSidePaneTransitionEnded] = useState(true); + const {shouldHideSidePane, shouldHideSidePaneBackdrop, shouldHideHelpButton, sidePaneNVP} = useSidePaneDisplayStatus(); + const shouldHideToolTip = isExtraLargeScreenWidth ? !isSidePaneTransitionEnded : !shouldHideSidePane; + + const shouldApplySidePaneOffset = isExtraLargeScreenWidth && !shouldHideSidePane; const sidePaneOffset = useRef(new Animated.Value(shouldApplySidePaneOffset ? variables.sideBarWidth : 0)); - const sidePaneTranslateX = useRef(new Animated.Value(isPaneHidden ? sidePaneWidth : 0)); + const sidePaneTranslateX = useRef(new Animated.Value(shouldHideSidePane ? sidePaneWidth : 0)); useEffect(() => { - if (!isPaneHidden) { - setShouldHideSidePane(false); - } - if (isExtraLargeScreenWidth) { - setIsAnimatingExtraLargeScreen(true); - } + setIsSidePaneTransitionEnded(false); Animated.parallel([ Animated.timing(sidePaneOffset.current, { @@ -73,15 +74,26 @@ function useSidePane() { useNativeDriver: true, }), Animated.timing(sidePaneTranslateX.current, { - toValue: isPaneHidden ? sidePaneWidth : 0, + toValue: shouldHideSidePane ? sidePaneWidth : 0, duration: CONST.ANIMATED_TRANSITION, useNativeDriver: true, }), - ]).start(() => { - setShouldHideSidePane(isPaneHidden); - setIsAnimatingExtraLargeScreen(false); + ]).start(() => setIsSidePaneTransitionEnded(true)); + }, [shouldHideSidePane, shouldApplySidePaneOffset, sidePaneWidth]); + + const openSidePane = useCallback(() => { + if (!sidePaneNVP) { + return; + } + + setIsSidePaneTransitionEnded(false); + KeyboardUtils.dismiss(); + + triggerSidePane({ + isOpen: true, + isOpenNarrowScreen: isExtraLargeScreenWidth ? undefined : true, }); - }, [isPaneHidden, shouldApplySidePaneOffset, shouldUseNarrowLayout, sidePaneWidth, isExtraLargeScreenWidth]); + }, [isExtraLargeScreenWidth, sidePaneNVP]); const closeSidePane = useCallback( (shouldUpdateNarrow = false) => { @@ -89,6 +101,7 @@ function useSidePane() { return; } + setIsSidePaneTransitionEnded(false); const shouldOnlyUpdateNarrowLayout = !isExtraLargeScreenWidth || shouldUpdateNarrow; triggerSidePane({ isOpen: shouldOnlyUpdateNarrowLayout ? undefined : false, @@ -103,15 +116,17 @@ function useSidePane() { return { sidePane: sidePaneNVP, - isPaneHidden, + isSidePaneTransitionEnded, shouldHideSidePane, shouldHideSidePaneBackdrop, shouldHideHelpButton, + shouldHideToolTip, sidePaneOffset, sidePaneTranslateX, - shouldHideToolTip, + openSidePane, closeSidePane, }; } export default useSidePane; +export {useSidePaneDisplayStatus}; diff --git a/src/hooks/useThreeDotsAnchorPosition.ts b/src/hooks/useThreeDotsAnchorPosition.ts index 1201cdbce675..e4e02abffc12 100644 --- a/src/hooks/useThreeDotsAnchorPosition.ts +++ b/src/hooks/useThreeDotsAnchorPosition.ts @@ -1,6 +1,6 @@ import type {AnchorPosition} from '@styles/index'; import variables from '@styles/variables'; -import useSidePane from './useSidePane'; +import {useSidePaneDisplayStatus} from './useSidePane'; import useWindowDimensions from './useWindowDimensions'; /** @@ -9,9 +9,9 @@ import useWindowDimensions from './useWindowDimensions'; */ function useThreeDotsAnchorPosition(anchorPositionStyle: (screenWidth: number) => AnchorPosition) { const {windowWidth} = useWindowDimensions(); - const {isPaneHidden} = useSidePane(); + const {shouldHideSidePane} = useSidePaneDisplayStatus(); - return anchorPositionStyle(isPaneHidden ? windowWidth : windowWidth - variables.sideBarWidth); + return anchorPositionStyle(shouldHideSidePane ? windowWidth : windowWidth - variables.sideBarWidth); } export default useThreeDotsAnchorPosition; diff --git a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx index e5c32117d9a8..c3a3f9e5ae1e 100644 --- a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx +++ b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx @@ -26,6 +26,7 @@ import useKeyboardState from '@hooks/useKeyboardState'; import useLocalize from '@hooks/useLocalize'; import usePrevious from '@hooks/usePrevious'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; +import {useSidePaneDisplayStatus} from '@hooks/useSidePane'; import useStyleUtils from '@hooks/useStyleUtils'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -240,6 +241,7 @@ function ComposerWithSuggestions( const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); const {preferredLocale} = useLocalize(); + const {isSidePaneHiddenOrLargeScreen} = useSidePaneDisplayStatus(); const isFocused = useIsFocused(); const navigation = useNavigation(); const emojisPresentBefore = useRef([]); @@ -565,14 +567,14 @@ function ComposerWithSuggestions( const setUpComposeFocusManager = useCallback( (shouldTakeOverFocus = false) => { ReportActionComposeFocusManager.onComposerFocus((shouldFocusForNonBlurInputOnTapOutside = false) => { - if ((!willBlurTextInputOnTapOutside && !shouldFocusForNonBlurInputOnTapOutside) || !isFocused) { + if ((!willBlurTextInputOnTapOutside && !shouldFocusForNonBlurInputOnTapOutside) || !isFocused || !isSidePaneHiddenOrLargeScreen) { return; } focus(true); }, shouldTakeOverFocus); }, - [focus, isFocused], + [focus, isFocused, isSidePaneHiddenOrLargeScreen], ); /** @@ -592,6 +594,11 @@ function ComposerWithSuggestions( return; } + // Do not focus the composer if the side pane is visible + if (!isSidePaneHiddenOrLargeScreen) { + return; + } + if (!shouldAutoFocusOnKeyPress(e)) { return; } @@ -603,7 +610,7 @@ function ComposerWithSuggestions( focus(); }, - [checkComposerVisibility, focus], + [checkComposerVisibility, focus, isSidePaneHiddenOrLargeScreen], ); const blur = useCallback(() => { @@ -642,7 +649,7 @@ function ComposerWithSuggestions( unsubscribeNavigationBlur(); unsubscribeNavigationFocus(); }; - }, [focusComposerOnKeyPress, navigation, setUpComposeFocusManager]); + }, [focusComposerOnKeyPress, navigation, setUpComposeFocusManager, isSidePaneHiddenOrLargeScreen]); const prevIsModalVisible = usePrevious(modal?.isVisible); const prevIsFocused = usePrevious(isFocused); @@ -660,6 +667,11 @@ function ComposerWithSuggestions( return; } + // Do not focus the composer if the side pane is visible + if (!isSidePaneHiddenOrLargeScreen) { + return; + } + // 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. @@ -672,7 +684,7 @@ function ComposerWithSuggestions( return; } focus(true); - }, [focus, prevIsFocused, editFocused, prevIsModalVisible, isFocused, modal?.isVisible, isNextModalWillOpenRef, shouldAutoFocus]); + }, [focus, prevIsFocused, editFocused, prevIsModalVisible, isFocused, modal?.isVisible, isNextModalWillOpenRef, shouldAutoFocus, isSidePaneHiddenOrLargeScreen]); useEffect(() => { // Scrolls the composer to the bottom and sets the selection to the end, so that longer drafts are easier to edit