From 6f4cca75438a378d8b5fdfbd4158722fb5ff9b56 Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Mon, 17 Mar 2025 19:18:29 +0100 Subject: [PATCH 01/12] Make SidePane compatible with RightDockedModals --- src/components/Modal/BaseModal.tsx | 4 ++- src/components/Modal/index.tsx | 2 +- src/components/SidePane/Help/index.tsx | 41 ++++++++++++++++++++++---- src/components/SidePane/Help/types.ts | 1 + src/components/SidePane/index.tsx | 24 ++++----------- src/styles/index.ts | 6 +++- 6 files changed, 50 insertions(+), 28 deletions(-) diff --git a/src/components/Modal/BaseModal.tsx b/src/components/Modal/BaseModal.tsx index 3605a368d467..22f7403a5eee 100644 --- a/src/components/Modal/BaseModal.tsx +++ b/src/components/Modal/BaseModal.tsx @@ -10,6 +10,7 @@ import useKeyboardState from '@hooks/useKeyboardState'; import usePrevious from '@hooks/usePrevious'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useSafeAreaInsets from '@hooks/useSafeAreaInsets'; +import useSidePane from '@hooks/useSidePane'; import useStyleUtils from '@hooks/useStyleUtils'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -88,6 +89,7 @@ function BaseModal( // We need to use isSmallScreenWidth instead of shouldUseNarrowLayout to apply correct modal width // eslint-disable-next-line rulesdir/prefer-shouldUseNarrowLayout-instead-of-isSmallScreenWidth const {isSmallScreenWidth} = useResponsiveLayout(); + const {sidePaneOffset} = useSidePane(); const keyboardStateContextValue = useKeyboardState(); const safeAreaInsets = useSafeAreaInsets(); @@ -269,7 +271,7 @@ function BaseModal( backdropTransitionOutTiming={0} hasBackdrop={fullscreen} coverScreen={fullscreen} - style={modalStyle} + style={[modalStyle, type === CONST.MODAL.MODAL_TYPE.RIGHT_DOCKED && {paddingRight: sidePaneOffset.current}]} deviceHeight={windowHeight} deviceWidth={windowWidth} animationIn={animationIn ?? modalStyleAnimationIn} diff --git a/src/components/Modal/index.tsx b/src/components/Modal/index.tsx index fcec907d8a36..daa64070f14a 100644 --- a/src/components/Modal/index.tsx +++ b/src/components/Modal/index.tsx @@ -39,7 +39,7 @@ function Modal({fullscreen = true, onModalHide = () => {}, type, onModalShow = ( type === CONST.MODAL.MODAL_TYPE.CENTERED || type === CONST.MODAL.MODAL_TYPE.CENTERED_UNSWIPEABLE || type === CONST.MODAL.MODAL_TYPE.RIGHT_DOCKED || - CONST.MODAL.MODAL_TYPE.CENTERED_SWIPABLE_TO_RIGHT; + type === CONST.MODAL.MODAL_TYPE.CENTERED_SWIPABLE_TO_RIGHT; if (statusBarColor) { setPreviousStatusBarColor(statusBarColor); diff --git a/src/components/SidePane/Help/index.tsx b/src/components/SidePane/Help/index.tsx index 597bff5391ad..de2a147f51fb 100644 --- a/src/components/SidePane/Help/index.tsx +++ b/src/components/SidePane/Help/index.tsx @@ -1,25 +1,54 @@ import React from 'react'; // eslint-disable-next-line no-restricted-imports -import {Animated} from 'react-native'; +import {Animated, View} from 'react-native'; +import {useOnyx} from 'react-native-onyx'; +// @ts-expect-error This is a workaround to display HelpPane on top of everything, +// Modal from react-native can't be used here, as it would block interactions with the rest of the app +import ModalPortal from 'react-native-web/dist/exports/Modal/ModalPortal'; +import FocusTrapForModal from '@components/FocusTrap/FocusTrapForModal'; +import SidePaneOverlay from '@components/SidePane/SidePaneOverlay'; import useKeyboardShortcut from '@hooks/useKeyboardShortcut'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; +import useRootNavigationState from '@hooks/useRootNavigationState'; import useSafeAreaPaddings from '@hooks/useSafeAreaPaddings'; import useThemeStyles from '@hooks/useThemeStyles'; import CONST from '@src/CONST'; +import NAVIGATORS from '@src/NAVIGATORS'; +import ONYXKEYS from '@src/ONYXKEYS'; import HelpContent from './HelpContent'; import type HelpProps from './types'; -function Help({sidePaneTranslateX, closeSidePane}: HelpProps) { +function Help({sidePaneTranslateX, closeSidePane, shouldHideSidePaneBackdrop}: HelpProps) { const styles = useThemeStyles(); const {isExtraLargeScreenWidth, shouldUseNarrowLayout} = useResponsiveLayout(); const {paddingTop, paddingBottom} = useSafeAreaPaddings(); - useKeyboardShortcut(CONST.KEYBOARD_SHORTCUTS.ESCAPE, () => closeSidePane(), {isActive: !isExtraLargeScreenWidth}); + const isInRHP = useRootNavigationState((state) => state?.routes.at(-1)?.name === NAVIGATORS.RIGHT_MODAL_NAVIGATOR); + const [modal] = useOnyx(ONYXKEYS.MODAL); + const isInNarrowPaneModal = !!modal?.isVisible || isInRHP; + + useKeyboardShortcut(CONST.KEYBOARD_SHORTCUTS.ESCAPE, () => closeSidePane(), {isActive: !isExtraLargeScreenWidth, shouldBubble: false}); return ( - - - + + + + + {!shouldHideSidePaneBackdrop && ( + + )} + + + + + + + ); } diff --git a/src/components/SidePane/Help/types.ts b/src/components/SidePane/Help/types.ts index 8054cc6cd43d..acac76b6dcbf 100644 --- a/src/components/SidePane/Help/types.ts +++ b/src/components/SidePane/Help/types.ts @@ -4,6 +4,7 @@ import type {Animated} from 'react-native'; type HelpProps = { sidePaneTranslateX: MutableRefObject; + shouldHideSidePaneBackdrop: boolean; closeSidePane: (shouldUpdateNarrow?: boolean) => void; }; diff --git a/src/components/SidePane/index.tsx b/src/components/SidePane/index.tsx index 4a2f7adba8d3..d5b525f40d5a 100644 --- a/src/components/SidePane/index.tsx +++ b/src/components/SidePane/index.tsx @@ -1,34 +1,20 @@ import React from 'react'; -import {View} from 'react-native'; -import useRootNavigationState from '@hooks/useRootNavigationState'; import useSidePane from '@hooks/useSidePane'; -import NAVIGATORS from '@src/NAVIGATORS'; import Help from './Help'; -import SidePaneOverlay from './SidePaneOverlay'; function SidePane() { const {shouldHideSidePane, sidePaneTranslateX, shouldHideSidePaneBackdrop, closeSidePane} = useSidePane(); - const isInNarrowPaneModal = useRootNavigationState((state) => state?.routes.at(-1)?.name === NAVIGATORS.RIGHT_MODAL_NAVIGATOR); if (shouldHideSidePane) { return null; } return ( - <> - - {!shouldHideSidePaneBackdrop && ( - - )} - - - + ); } diff --git a/src/styles/index.ts b/src/styles/index.ts index ad7f3a5bb808..f33cdd278905 100644 --- a/src/styles/index.ts +++ b/src/styles/index.ts @@ -5480,6 +5480,8 @@ const styles = (theme: ThemeColors) => marginHorizontal: 8, alignSelf: 'center', }, + // We have to use 10000 here as sidePane has to be displayed on top of modals which have z-index of 9999 + sidePaneContainer: {zIndex: 10000}, sidePaneOverlay: (isOverlayVisible: boolean) => ({ ...positioning.pFixed, top: 0, @@ -5489,8 +5491,10 @@ const styles = (theme: ThemeColors) => backgroundColor: theme.overlay, opacity: isOverlayVisible ? 0 : variables.overlayOpacity, }), - sidePaneContainer: (shouldUseNarrowLayout: boolean, isExtraLargeScreenWidth: boolean): ViewStyle => ({ + sidePaneContent: (shouldUseNarrowLayout: boolean, isExtraLargeScreenWidth: boolean): ViewStyle => ({ position: Platform.OS === 'web' ? 'fixed' : 'absolute', + top: 0, + bottom: 0, right: 0, width: shouldUseNarrowLayout ? '100%' : variables.sideBarWidth, height: '100%', From e033219ac18e7973cd2a099ef6c3de3be30c1171 Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Mon, 17 Mar 2025 22:02:54 +0100 Subject: [PATCH 02/12] Fix physical back button on web --- src/components/SidePane/Help/index.tsx | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/src/components/SidePane/Help/index.tsx b/src/components/SidePane/Help/index.tsx index de2a147f51fb..2b853fb1301e 100644 --- a/src/components/SidePane/Help/index.tsx +++ b/src/components/SidePane/Help/index.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, {useEffect} from 'react'; // eslint-disable-next-line no-restricted-imports import {Animated, View} from 'react-native'; import {useOnyx} from 'react-native-onyx'; @@ -29,6 +29,21 @@ function Help({sidePaneTranslateX, closeSidePane, shouldHideSidePaneBackdrop}: H useKeyboardShortcut(CONST.KEYBOARD_SHORTCUTS.ESCAPE, () => closeSidePane(), {isActive: !isExtraLargeScreenWidth, shouldBubble: false}); + useEffect(() => { + window.history.pushState({isSidePaneOpen: true}, '', null); + const handlePopState = () => { + if (isExtraLargeScreenWidth) { + return; + } + + closeSidePane(); + }; + + window.addEventListener('popstate', handlePopState); + return () => window.removeEventListener('popstate', handlePopState); + // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps + }, []); + return ( From 84d1263ecf9f99bb967fb86d08b989d2bf3ac93c Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Tue, 18 Mar 2025 12:47:57 +0100 Subject: [PATCH 03/12] Little refactor, add a comment --- src/components/SidePane/Help/HelpContent.tsx | 8 +++++--- src/components/SidePane/Help/index.android.tsx | 4 ++-- src/components/SidePane/Help/index.ios.tsx | 4 ++-- src/components/SidePane/Help/index.tsx | 3 ++- 4 files changed, 11 insertions(+), 8 deletions(-) diff --git a/src/components/SidePane/Help/HelpContent.tsx b/src/components/SidePane/Help/HelpContent.tsx index 9080dd84cc32..02c226f4e443 100644 --- a/src/components/SidePane/Help/HelpContent.tsx +++ b/src/components/SidePane/Help/HelpContent.tsx @@ -8,17 +8,19 @@ import useEnvironment from '@hooks/useEnvironment'; import useLocalize from '@hooks/useLocalize'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useRootNavigationState from '@hooks/useRootNavigationState'; -import useSidePane from '@hooks/useSidePane'; import useThemeStyles from '@hooks/useThemeStyles'; import Navigation from '@libs/Navigation/Navigation'; import {substituteRouteParameters} from '@libs/SidePaneUtils'; -function HelpContent() { +type HelpContentProps = { + closeSidePane: (shouldUpdateNarrow?: boolean) => void; +}; + +function HelpContent({closeSidePane}: HelpContentProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); const {isProduction} = useEnvironment(); const {isExtraLargeScreenWidth} = useResponsiveLayout(); - const {closeSidePane} = useSidePane(); const route = useRootNavigationState((state) => { const params = (findFocusedRoute(state)?.params as Record) ?? {}; const activeRoute = Navigation.getActiveRouteWithoutParams(); diff --git a/src/components/SidePane/Help/index.android.tsx b/src/components/SidePane/Help/index.android.tsx index 8be21c35a535..6379eab1b1da 100644 --- a/src/components/SidePane/Help/index.android.tsx +++ b/src/components/SidePane/Help/index.android.tsx @@ -27,8 +27,8 @@ function Help({sidePaneTranslateX, closeSidePane}: HelpProps) { ); return ( - - + + ); } diff --git a/src/components/SidePane/Help/index.ios.tsx b/src/components/SidePane/Help/index.ios.tsx index 171b98e2c14d..69feed309dc3 100644 --- a/src/components/SidePane/Help/index.ios.tsx +++ b/src/components/SidePane/Help/index.ios.tsx @@ -46,9 +46,9 @@ function Help({sidePaneTranslateX, closeSidePane}: HelpProps) { return ( - + ); diff --git a/src/components/SidePane/Help/index.tsx b/src/components/SidePane/Help/index.tsx index 2b853fb1301e..e58cbddd7401 100644 --- a/src/components/SidePane/Help/index.tsx +++ b/src/components/SidePane/Help/index.tsx @@ -29,6 +29,7 @@ function Help({sidePaneTranslateX, closeSidePane, shouldHideSidePaneBackdrop}: H useKeyboardShortcut(CONST.KEYBOARD_SHORTCUTS.ESCAPE, () => closeSidePane(), {isActive: !isExtraLargeScreenWidth, shouldBubble: false}); + // Web back button: push history state and close side pane on popstate useEffect(() => { window.history.pushState({isSidePaneOpen: true}, '', null); const handlePopState = () => { @@ -59,7 +60,7 @@ function Help({sidePaneTranslateX, closeSidePane, shouldHideSidePaneBackdrop}: H - + From 6f4062588b360a0523791418dc1d0ad656882ac3 Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Tue, 18 Mar 2025 13:50:48 +0100 Subject: [PATCH 04/12] Handle all navigation shortcuts --- .../Search/SearchRouter/SearchRouterModal.tsx | 4 ++++ src/components/SidePane/Help/index.tsx | 14 ++++++++++++++ 2 files changed, 18 insertions(+) diff --git a/src/components/Search/SearchRouter/SearchRouterModal.tsx b/src/components/Search/SearchRouter/SearchRouterModal.tsx index 9d4841e9aa3b..56ff774581c4 100644 --- a/src/components/Search/SearchRouter/SearchRouterModal.tsx +++ b/src/components/Search/SearchRouter/SearchRouterModal.tsx @@ -4,6 +4,7 @@ import KeyboardAvoidingView from '@components/KeyboardAvoidingView'; import Modal from '@components/Modal'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useSafeAreaInsets from '@hooks/useSafeAreaInsets'; +import useSidePane from '@hooks/useSidePane'; import useThemeStyles from '@hooks/useThemeStyles'; import useViewportOffsetTop from '@hooks/useViewportOffsetTop'; import useWindowDimensions from '@hooks/useWindowDimensions'; @@ -21,11 +22,13 @@ function SearchRouterModal() { const {isSearchRouterDisplayed, closeSearchRouter} = useSearchRouterContext(); const viewportOffsetTop = useViewportOffsetTop(); const safeAreaInsets = useSafeAreaInsets(); + const {sidePaneOffset} = useSidePane(); // On mWeb Safari, the input caret stuck for a moment while the modal is animating. So, we hide the caret until the animation is done. const [shouldHideInputCaret, setShouldHideInputCaret] = useState(isMobileWebSafari); const modalType = shouldUseNarrowLayout ? CONST.MODAL.MODAL_TYPE.CENTERED_SWIPABLE_TO_RIGHT : CONST.MODAL.MODAL_TYPE.POPOVER; + const outerStyle = shouldUseNarrowLayout ? undefined : {paddingRight: sidePaneOffset.current}; return ( { + if (isExtraLargeScreenWidth) { + return; + } + + closeSidePane(); + }; + + // Close side pane on escape key press useKeyboardShortcut(CONST.KEYBOARD_SHORTCUTS.ESCAPE, () => closeSidePane(), {isActive: !isExtraLargeScreenWidth, shouldBubble: false}); + // Close side pane on small screens when navigation keyboard shortcuts are used + useKeyboardShortcut(CONST.KEYBOARD_SHORTCUTS.SEARCH, onCloseSidePaneOnSmallScreens, {shouldBubble: true}); + useKeyboardShortcut(CONST.KEYBOARD_SHORTCUTS.NEW_CHAT, onCloseSidePaneOnSmallScreens, {shouldBubble: true}); + useKeyboardShortcut(CONST.KEYBOARD_SHORTCUTS.SHORTCUTS, onCloseSidePaneOnSmallScreens, {shouldBubble: true}); + // Web back button: push history state and close side pane on popstate useEffect(() => { window.history.pushState({isSidePaneOpen: true}, '', null); From 7eacbb01bedfbf0dc3f1812c6528b8441ee31bed Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Tue, 18 Mar 2025 14:16:23 +0100 Subject: [PATCH 05/12] Focus composer after side pane closes --- src/CONST.ts | 1 + src/hooks/useSidePane.ts | 5 +++++ src/libs/focusComposerWithDelay/index.ts | 5 +++-- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/CONST.ts b/src/CONST.ts index 0ef02fcbd328..7305fd2dbae6 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -390,6 +390,7 @@ const CONST = { ANIMATED_PROGRESS_BAR_OPACITY_DURATION: 300, ANIMATED_PROGRESS_BAR_DURATION: 750, ANIMATION_IN_TIMING: 100, + COMPOSER_FOCUS_DELAY :150, ANIMATION_DIRECTION: { IN: 'in', OUT: 'out', diff --git a/src/hooks/useSidePane.ts b/src/hooks/useSidePane.ts index a879696359fc..7cb540511e95 100644 --- a/src/hooks/useSidePane.ts +++ b/src/hooks/useSidePane.ts @@ -5,6 +5,8 @@ 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'; +import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager'; import variables from '@styles/variables'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -86,6 +88,9 @@ function useSidePane() { isOpen: shouldOnlyUpdateNarrowLayout ? undefined : false, isOpenNarrowScreen: shouldOnlyUpdateNarrowLayout ? false : undefined, }); + + // Focus the composer after closing the side pane + focusComposerWithDelay(ReportActionComposeFocusManager.composerRef.current, CONST.ANIMATED_TRANSITION + CONST.COMPOSER_FOCUS_DELAY)(true); }, [isExtraLargeScreenWidth, sidePaneNVP], ); diff --git a/src/libs/focusComposerWithDelay/index.ts b/src/libs/focusComposerWithDelay/index.ts index ab251bf8403a..1f18a4b9d4bc 100644 --- a/src/libs/focusComposerWithDelay/index.ts +++ b/src/libs/focusComposerWithDelay/index.ts @@ -1,13 +1,14 @@ import ComposerFocusManager from '@libs/ComposerFocusManager'; import isWindowReadyToFocus from '@libs/isWindowReadyToFocus'; import * as EmojiPickerAction from '@userActions/EmojiPickerAction'; +import CONST from '@src/CONST'; import setTextInputSelection from './setTextInputSelection'; import type {FocusComposerWithDelay, InputType} from './types'; /** * Create a function that focuses the composer. */ -function focusComposerWithDelay(textInput: InputType | null): FocusComposerWithDelay { +function focusComposerWithDelay(textInput: InputType | null, delay: number = CONST.COMPOSER_FOCUS_DELAY): FocusComposerWithDelay { /** * Focus the text input * @param [shouldDelay] Impose delay before focusing the text input @@ -33,7 +34,7 @@ function focusComposerWithDelay(textInput: InputType | null): FocusComposerWithD } // When the closing modal has a focused text input focus() needs a delay to properly work. // Setting 150ms here is a temporary workaround for the Android HybridApp. It should be reverted once we identify the real root cause of this issue: https://github.com/Expensify/App/issues/56311. - setTimeout(() => textInput.focus(), 150); + setTimeout(() => textInput.focus(), delay); if (forcedSelectionRange) { setTextInputSelection(textInput, forcedSelectionRange); } From 3b59c9acaa049df66c4368c2aa863da003c45abb Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Tue, 18 Mar 2025 14:42:07 +0100 Subject: [PATCH 06/12] Fix lint --- src/CONST.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/CONST.ts b/src/CONST.ts index 7305fd2dbae6..00b39b57e43c 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -390,7 +390,7 @@ const CONST = { ANIMATED_PROGRESS_BAR_OPACITY_DURATION: 300, ANIMATED_PROGRESS_BAR_DURATION: 750, ANIMATION_IN_TIMING: 100, - COMPOSER_FOCUS_DELAY :150, + COMPOSER_FOCUS_DELAY: 150, ANIMATION_DIRECTION: { IN: 'in', OUT: 'out', From 4cedf682eb010b21fb94f21d3aedcce256fa1ab4 Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Tue, 18 Mar 2025 15:13:27 +0100 Subject: [PATCH 07/12] Add type to Onyx's Modal key --- src/components/Modal/BaseModal.tsx | 4 ++-- src/components/SidePane/Help/index.tsx | 7 +------ src/libs/Navigation/AppNavigator/AuthScreens.tsx | 2 +- src/libs/actions/Modal.ts | 5 +++-- src/types/onyx/Modal.ts | 5 +++++ 5 files changed, 12 insertions(+), 11 deletions(-) diff --git a/src/components/Modal/BaseModal.tsx b/src/components/Modal/BaseModal.tsx index 22f7403a5eee..b0e7377ed11f 100644 --- a/src/components/Modal/BaseModal.tsx +++ b/src/components/Modal/BaseModal.tsx @@ -156,10 +156,10 @@ function BaseModal( const handleShowModal = useCallback(() => { if (shouldSetModalVisibility) { - setModalVisibility(true); + setModalVisibility(true, type); } onModalShow(); - }, [onModalShow, shouldSetModalVisibility]); + }, [onModalShow, shouldSetModalVisibility, type]); const handleBackdropPress = (e?: KeyboardEvent) => { if (e?.key === CONST.KEYBOARD_SHORTCUTS.ENTER.shortcutKey) { diff --git a/src/components/SidePane/Help/index.tsx b/src/components/SidePane/Help/index.tsx index a0ea5505b716..e444394e554c 100644 --- a/src/components/SidePane/Help/index.tsx +++ b/src/components/SidePane/Help/index.tsx @@ -9,11 +9,9 @@ import FocusTrapForModal from '@components/FocusTrap/FocusTrapForModal'; import SidePaneOverlay from '@components/SidePane/SidePaneOverlay'; import useKeyboardShortcut from '@hooks/useKeyboardShortcut'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; -import useRootNavigationState from '@hooks/useRootNavigationState'; import useSafeAreaPaddings from '@hooks/useSafeAreaPaddings'; import useThemeStyles from '@hooks/useThemeStyles'; import CONST from '@src/CONST'; -import NAVIGATORS from '@src/NAVIGATORS'; import ONYXKEYS from '@src/ONYXKEYS'; import HelpContent from './HelpContent'; import type HelpProps from './types'; @@ -22,10 +20,7 @@ function Help({sidePaneTranslateX, closeSidePane, shouldHideSidePaneBackdrop}: H const styles = useThemeStyles(); const {isExtraLargeScreenWidth, shouldUseNarrowLayout} = useResponsiveLayout(); const {paddingTop, paddingBottom} = useSafeAreaPaddings(); - - const isInRHP = useRootNavigationState((state) => state?.routes.at(-1)?.name === NAVIGATORS.RIGHT_MODAL_NAVIGATOR); - const [modal] = useOnyx(ONYXKEYS.MODAL); - const isInNarrowPaneModal = !!modal?.isVisible || isInRHP; + const [isInNarrowPaneModal = false] = useOnyx(ONYXKEYS.MODAL, {selector: (modal) => modal?.type === CONST.MODAL.MODAL_TYPE.RIGHT_DOCKED}); const onCloseSidePaneOnSmallScreens = () => { if (isExtraLargeScreenWidth) { diff --git a/src/libs/Navigation/AppNavigator/AuthScreens.tsx b/src/libs/Navigation/AppNavigator/AuthScreens.tsx index 25ac2cc17943..bf76351bf5c9 100644 --- a/src/libs/Navigation/AppNavigator/AuthScreens.tsx +++ b/src/libs/Navigation/AppNavigator/AuthScreens.tsx @@ -180,7 +180,7 @@ const RootStack = createRootStackNavigator(); const modalScreenListeners = { focus: () => { - Modal.setModalVisibility(true); + Modal.setModalVisibility(true, CONST.MODAL.MODAL_TYPE.RIGHT_DOCKED); }, blur: () => { Modal.setModalVisibility(false); diff --git a/src/libs/actions/Modal.ts b/src/libs/actions/Modal.ts index 9f912fd888bd..34233f16b6e7 100644 --- a/src/libs/actions/Modal.ts +++ b/src/libs/actions/Modal.ts @@ -1,5 +1,6 @@ import Onyx from 'react-native-onyx'; import ONYXKEYS from '@src/ONYXKEYS'; +import type ModalType from '@src/types/utils/ModalType'; const closeModals: Array<(isNavigating?: boolean) => void> = []; @@ -69,8 +70,8 @@ function onModalDidClose() { /** * Allows other parts of the app to know when a modal has been opened or closed */ -function setModalVisibility(isVisible: boolean) { - Onyx.merge(ONYXKEYS.MODAL, {isVisible}); +function setModalVisibility(isVisible: boolean, type: ModalType | null = null) { + Onyx.merge(ONYXKEYS.MODAL, {isVisible, type}); } /** diff --git a/src/types/onyx/Modal.ts b/src/types/onyx/Modal.ts index b4b761cc8677..f5e169ae44f0 100644 --- a/src/types/onyx/Modal.ts +++ b/src/types/onyx/Modal.ts @@ -1,3 +1,5 @@ +import type ModalType from '@src/types/utils/ModalType'; + /** Modal state */ type Modal = { /** Indicates when an Alert modal is about to be visible */ @@ -9,6 +11,9 @@ type Modal = { /** Indicates if there is a modal currently visible or not */ isVisible?: boolean; + /** The type of the modal if it's visible */ + type?: ModalType; + /** Indicates if the modal is a popover */ isPopover?: boolean; }; From 87a386b6712fda8ccd2f6d4bc239bac76aea3067 Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Tue, 18 Mar 2025 15:25:05 +0100 Subject: [PATCH 08/12] Handle attachment modal --- src/components/SidePane/Help/index.tsx | 4 ++-- src/components/SidePane/SidePaneOverlay.tsx | 12 ++++++------ src/hooks/useSidePane.ts | 3 ++- 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/src/components/SidePane/Help/index.tsx b/src/components/SidePane/Help/index.tsx index e444394e554c..659ad52ba425 100644 --- a/src/components/SidePane/Help/index.tsx +++ b/src/components/SidePane/Help/index.tsx @@ -20,7 +20,7 @@ function Help({sidePaneTranslateX, closeSidePane, shouldHideSidePaneBackdrop}: H const styles = useThemeStyles(); const {isExtraLargeScreenWidth, shouldUseNarrowLayout} = useResponsiveLayout(); const {paddingTop, paddingBottom} = useSafeAreaPaddings(); - const [isInNarrowPaneModal = false] = useOnyx(ONYXKEYS.MODAL, {selector: (modal) => modal?.type === CONST.MODAL.MODAL_TYPE.RIGHT_DOCKED}); + const [isRHPVisible = false] = useOnyx(ONYXKEYS.MODAL, {selector: (modal) => modal?.type === CONST.MODAL.MODAL_TYPE.RIGHT_DOCKED}); const onCloseSidePaneOnSmallScreens = () => { if (isExtraLargeScreenWidth) { @@ -62,7 +62,7 @@ function Help({sidePaneTranslateX, closeSidePane, shouldHideSidePaneBackdrop}: H {!shouldHideSidePaneBackdrop && ( )} diff --git a/src/components/SidePane/SidePaneOverlay.tsx b/src/components/SidePane/SidePaneOverlay.tsx index 95f486b07262..a16cfb1b9084 100644 --- a/src/components/SidePane/SidePaneOverlay.tsx +++ b/src/components/SidePane/SidePaneOverlay.tsx @@ -6,8 +6,8 @@ import useThemeStyles from '@hooks/useThemeStyles'; import CONST from '@src/CONST'; type SidePaneOverlayProps = { - /** Whether the side pane is displayed inside of RHP */ - isInNarrowPaneModal: boolean; + /** Whether the side pane is displayed over RHP */ + isRHPVisible: boolean; /** Callback fired when pressing the backdrop */ onBackdropPress: () => void; @@ -15,7 +15,7 @@ type SidePaneOverlayProps = { const easing = Easing.bezier(0.76, 0.0, 0.24, 1.0).factory(); -function SidePaneOverlay({isInNarrowPaneModal, onBackdropPress}: SidePaneOverlayProps) { +function SidePaneOverlay({isRHPVisible, onBackdropPress}: SidePaneOverlayProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); @@ -37,9 +37,9 @@ function SidePaneOverlay({isInNarrowPaneModal, onBackdropPress}: SidePaneOverlay return ( modal?.type === CONST.MODAL.MODAL_TYPE.CENTERED}); const isLanguageUnsupported = language !== CONST.LOCALES.EN; - const isPaneHidden = isSidePaneHidden(sidePaneNVP, isExtraLargeScreenWidth) || isLanguageUnsupported; + const isPaneHidden = isSidePaneHidden(sidePaneNVP, isExtraLargeScreenWidth) || isLanguageUnsupported || isAttachmentModalVisible; const sidePaneWidth = shouldUseNarrowLayout ? windowWidth : variables.sideBarWidth; const shouldApplySidePaneOffset = isExtraLargeScreenWidth && !isPaneHidden; From 1b168fc362064521608e5e1abe416fafdd099f24 Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Tue, 18 Mar 2025 17:30:49 +0100 Subject: [PATCH 09/12] Move styles to BaseModal --- src/components/Modal/BaseModal.tsx | 5 ++++- src/components/Modal/types.ts | 6 ++++++ src/components/Search/SearchRouter/SearchRouterModal.tsx | 5 +---- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/src/components/Modal/BaseModal.tsx b/src/components/Modal/BaseModal.tsx index b0e7377ed11f..da4f3c7f3a73 100644 --- a/src/components/Modal/BaseModal.tsx +++ b/src/components/Modal/BaseModal.tsx @@ -79,6 +79,7 @@ function BaseModal( swipeDirection, shouldPreventScrollOnFocus = false, enableEdgeToEdgeBottomSafeAreaPadding = false, + shouldApplySidePaneOffset = type === CONST.MODAL.MODAL_TYPE.RIGHT_DOCKED, }: BaseModalProps, ref: React.ForwardedRef, ) { @@ -90,6 +91,8 @@ function BaseModal( // eslint-disable-next-line rulesdir/prefer-shouldUseNarrowLayout-instead-of-isSmallScreenWidth const {isSmallScreenWidth} = useResponsiveLayout(); const {sidePaneOffset} = useSidePane(); + const {shouldUseNarrowLayout} = useResponsiveLayout(); + const sidePaneStyle = shouldApplySidePaneOffset && !shouldUseNarrowLayout ? {paddingRight: sidePaneOffset.current} : undefined; const keyboardStateContextValue = useKeyboardState(); const safeAreaInsets = useSafeAreaInsets(); @@ -271,7 +274,7 @@ function BaseModal( backdropTransitionOutTiming={0} hasBackdrop={fullscreen} coverScreen={fullscreen} - style={[modalStyle, type === CONST.MODAL.MODAL_TYPE.RIGHT_DOCKED && {paddingRight: sidePaneOffset.current}]} + style={[modalStyle, sidePaneStyle]} deviceHeight={windowHeight} deviceWidth={windowWidth} animationIn={animationIn ?? modalStyleAnimationIn} diff --git a/src/components/Modal/types.ts b/src/components/Modal/types.ts index ddc1e2bbd0e3..38e41f2326bc 100644 --- a/src/components/Modal/types.ts +++ b/src/components/Modal/types.ts @@ -117,6 +117,12 @@ type BaseModalProps = Partial & * This flag can be removed, once all components/screens have switched to edge-to-edge safe area handling. */ enableEdgeToEdgeBottomSafeAreaPadding?: boolean; + + /** + * Whether the modal should apply the side pane offset. + * This is used to adjust the modal position when the side pane is open. + */ + shouldApplySidePaneOffset?: boolean; }; export default BaseModalProps; diff --git a/src/components/Search/SearchRouter/SearchRouterModal.tsx b/src/components/Search/SearchRouter/SearchRouterModal.tsx index 56ff774581c4..5920eb615f8a 100644 --- a/src/components/Search/SearchRouter/SearchRouterModal.tsx +++ b/src/components/Search/SearchRouter/SearchRouterModal.tsx @@ -4,7 +4,6 @@ import KeyboardAvoidingView from '@components/KeyboardAvoidingView'; import Modal from '@components/Modal'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useSafeAreaInsets from '@hooks/useSafeAreaInsets'; -import useSidePane from '@hooks/useSidePane'; import useThemeStyles from '@hooks/useThemeStyles'; import useViewportOffsetTop from '@hooks/useViewportOffsetTop'; import useWindowDimensions from '@hooks/useWindowDimensions'; @@ -22,13 +21,11 @@ function SearchRouterModal() { const {isSearchRouterDisplayed, closeSearchRouter} = useSearchRouterContext(); const viewportOffsetTop = useViewportOffsetTop(); const safeAreaInsets = useSafeAreaInsets(); - const {sidePaneOffset} = useSidePane(); // On mWeb Safari, the input caret stuck for a moment while the modal is animating. So, we hide the caret until the animation is done. const [shouldHideInputCaret, setShouldHideInputCaret] = useState(isMobileWebSafari); const modalType = shouldUseNarrowLayout ? CONST.MODAL.MODAL_TYPE.CENTERED_SWIPABLE_TO_RIGHT : CONST.MODAL.MODAL_TYPE.POPOVER; - const outerStyle = shouldUseNarrowLayout ? undefined : {paddingRight: sidePaneOffset.current}; return ( setShouldHideInputCaret(isMobileWebSafari)} onModalShow={() => setShouldHideInputCaret(false)} + shouldApplySidePaneOffset={!shouldUseNarrowLayout} > Date: Tue, 18 Mar 2025 17:31:07 +0100 Subject: [PATCH 10/12] Improve native code for side pane --- .../SidePane/Help/index.android.tsx | 23 ++++---- src/components/SidePane/Help/index.ios.tsx | 59 ++++--------------- src/components/SidePane/index.tsx | 3 +- 3 files changed, 25 insertions(+), 60 deletions(-) diff --git a/src/components/SidePane/Help/index.android.tsx b/src/components/SidePane/Help/index.android.tsx index 6379eab1b1da..5518bf1875ee 100644 --- a/src/components/SidePane/Help/index.android.tsx +++ b/src/components/SidePane/Help/index.android.tsx @@ -1,18 +1,12 @@ import {useFocusEffect} from '@react-navigation/native'; import React, {useCallback} from 'react'; -// eslint-disable-next-line no-restricted-imports -import {Animated, BackHandler} from 'react-native'; -import useResponsiveLayout from '@hooks/useResponsiveLayout'; -import useSafeAreaPaddings from '@hooks/useSafeAreaPaddings'; -import useThemeStyles from '@hooks/useThemeStyles'; +import {BackHandler} from 'react-native'; +import Modal from '@components/Modal'; +import CONST from '@src/CONST'; import HelpContent from './HelpContent'; import type HelpProps from './types'; -function Help({sidePaneTranslateX, closeSidePane}: HelpProps) { - const styles = useThemeStyles(); - const {isExtraLargeScreenWidth, shouldUseNarrowLayout} = useResponsiveLayout(); - const {paddingTop, paddingBottom} = useSafeAreaPaddings(); - +function Help({isPaneHidden, closeSidePane}: HelpProps) { // SidePane isn't a native screen, this handles the back button press on Android useFocusEffect( useCallback(() => { @@ -27,9 +21,14 @@ function Help({sidePaneTranslateX, closeSidePane}: HelpProps) { ); return ( - + closeSidePane()} + isVisible={!isPaneHidden} + type={CONST.MODAL.MODAL_TYPE.RIGHT_DOCKED} + shouldHandleNavigationBack + > - + ); } diff --git a/src/components/SidePane/Help/index.ios.tsx b/src/components/SidePane/Help/index.ios.tsx index 69feed309dc3..613040f1251c 100644 --- a/src/components/SidePane/Help/index.ios.tsx +++ b/src/components/SidePane/Help/index.ios.tsx @@ -1,56 +1,21 @@ import React from 'react'; -// eslint-disable-next-line no-restricted-imports -import {Animated, Dimensions} from 'react-native'; -import {Gesture, GestureDetector} from 'react-native-gesture-handler'; -import useResponsiveLayout from '@hooks/useResponsiveLayout'; -import useSafeAreaPaddings from '@hooks/useSafeAreaPaddings'; -import useThemeStyles from '@hooks/useThemeStyles'; +import Modal from '@components/Modal'; import CONST from '@src/CONST'; import HelpContent from './HelpContent'; import type HelpProps from './types'; -const SCREEN_WIDTH = Dimensions.get('window').width; - -function Help({sidePaneTranslateX, closeSidePane}: HelpProps) { - const styles = useThemeStyles(); - const {isExtraLargeScreenWidth, shouldUseNarrowLayout} = useResponsiveLayout(); - const {paddingTop, paddingBottom} = useSafeAreaPaddings(); - - // SidePane isn't a native screen, this simulates the 'close swipe gesture' on iOS - const panGesture = Gesture.Pan() - .runOnJS(true) - .hitSlop({left: 0, width: 20}) - .onUpdate((event) => { - if (event.translationX <= 0) { - return; - } - sidePaneTranslateX.current.setValue(event.translationX); - }) - .onEnd((event) => { - if (event.translationX > 100) { - // If swiped far enough, animate out and close - Animated.timing(sidePaneTranslateX.current, { - toValue: SCREEN_WIDTH, - duration: CONST.ANIMATED_TRANSITION, - useNativeDriver: false, - }).start(() => closeSidePane()); - } else { - // Otherwise, animate back to original position - Animated.spring(sidePaneTranslateX.current, { - toValue: 0, - useNativeDriver: false, - }).start(); - } - }); - +function Help({isPaneHidden, closeSidePane}: HelpProps) { return ( - - - - - + closeSidePane()} + isVisible={!isPaneHidden} + type={CONST.MODAL.MODAL_TYPE.RIGHT_DOCKED} + shouldHandleNavigationBack + propagateSwipe + swipeDirection={CONST.SWIPE_DIRECTION.RIGHT} + > + + ); } diff --git a/src/components/SidePane/index.tsx b/src/components/SidePane/index.tsx index d5b525f40d5a..715135a33468 100644 --- a/src/components/SidePane/index.tsx +++ b/src/components/SidePane/index.tsx @@ -3,7 +3,7 @@ import useSidePane from '@hooks/useSidePane'; import Help from './Help'; function SidePane() { - const {shouldHideSidePane, sidePaneTranslateX, shouldHideSidePaneBackdrop, closeSidePane} = useSidePane(); + const {shouldHideSidePane, isPaneHidden, sidePaneTranslateX, shouldHideSidePaneBackdrop, closeSidePane} = useSidePane(); if (shouldHideSidePane) { return null; @@ -11,6 +11,7 @@ function SidePane() { return ( Date: Tue, 18 Mar 2025 17:31:19 +0100 Subject: [PATCH 11/12] Fix errors on android --- src/components/SidePane/Help/types.ts | 1 + src/hooks/useSidePane.ts | 5 +++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/components/SidePane/Help/types.ts b/src/components/SidePane/Help/types.ts index acac76b6dcbf..f54cff0d3d5c 100644 --- a/src/components/SidePane/Help/types.ts +++ b/src/components/SidePane/Help/types.ts @@ -3,6 +3,7 @@ import type {MutableRefObject} from 'react'; import type {Animated} from 'react-native'; type HelpProps = { + isPaneHidden: boolean; sidePaneTranslateX: MutableRefObject; shouldHideSidePaneBackdrop: boolean; closeSidePane: (shouldUpdateNarrow?: boolean) => void; diff --git a/src/hooks/useSidePane.ts b/src/hooks/useSidePane.ts index 25756bdd50af..74150028c04c 100644 --- a/src/hooks/useSidePane.ts +++ b/src/hooks/useSidePane.ts @@ -65,12 +65,12 @@ function useSidePane() { Animated.timing(sidePaneOffset.current, { toValue: shouldApplySidePaneOffset ? variables.sideBarWidth : 0, duration: CONST.ANIMATED_TRANSITION, - useNativeDriver: false, + useNativeDriver: true, }), Animated.timing(sidePaneTranslateX.current, { toValue: isPaneHidden ? sidePaneWidth : 0, duration: CONST.ANIMATED_TRANSITION, - useNativeDriver: false, + useNativeDriver: true, }), ]).start(() => { setShouldHideSidePane(isPaneHidden); @@ -98,6 +98,7 @@ function useSidePane() { return { sidePane: sidePaneNVP, + isPaneHidden, shouldHideSidePane, shouldHideSidePaneBackdrop, shouldHideHelpButton, From 358d62580507c82c937d0ba48672035a89f545c6 Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Wed, 19 Mar 2025 16:19:20 +0100 Subject: [PATCH 12/12] Refactor sidePaneStyle to use isSmallScreenWidth --- src/components/Modal/BaseModal.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/components/Modal/BaseModal.tsx b/src/components/Modal/BaseModal.tsx index da4f3c7f3a73..b91eb15094e8 100644 --- a/src/components/Modal/BaseModal.tsx +++ b/src/components/Modal/BaseModal.tsx @@ -91,8 +91,7 @@ function BaseModal( // eslint-disable-next-line rulesdir/prefer-shouldUseNarrowLayout-instead-of-isSmallScreenWidth const {isSmallScreenWidth} = useResponsiveLayout(); const {sidePaneOffset} = useSidePane(); - const {shouldUseNarrowLayout} = useResponsiveLayout(); - const sidePaneStyle = shouldApplySidePaneOffset && !shouldUseNarrowLayout ? {paddingRight: sidePaneOffset.current} : undefined; + const sidePaneStyle = shouldApplySidePaneOffset && !isSmallScreenWidth ? {paddingRight: sidePaneOffset.current} : undefined; const keyboardStateContextValue = useKeyboardState(); const safeAreaInsets = useSafeAreaInsets();