From 515aa3e727f006bef62ee8341df1796643288f4b Mon Sep 17 00:00:00 2001 From: GCyganek Date: Mon, 27 Jul 2026 16:03:19 +0200 Subject: [PATCH] Fix mWeb landscape layout --- .../SidePanel/SidePanelModal/index.tsx | 4 +- src/hooks/useIsInLandscapeMode/index.ts | 11 ++++- src/hooks/useResponsiveLayout/index.ts | 12 ++++-- src/hooks/useSafeAreaPaddings.ts | 6 ++- .../useModalStackScreenOptions.ts | 4 +- .../useRootNavigatorScreenOptions.ts | 6 ++- .../useSplitNavigatorScreenOptions.ts | 7 +++- src/libs/getIsNarrowLayout/index.ts | 3 +- src/libs/isInLandscapeMode/index.ts | 12 +++--- .../step/IOURequestStepDistanceGPS/index.tsx | 41 ++++++++++--------- .../step/IOURequestStepDistanceOdometer.tsx | 5 ++- 11 files changed, 71 insertions(+), 40 deletions(-) diff --git a/src/components/SidePanel/SidePanelModal/index.tsx b/src/components/SidePanel/SidePanelModal/index.tsx index cd6817940d7c..7df699dad6d0 100644 --- a/src/components/SidePanel/SidePanelModal/index.tsx +++ b/src/components/SidePanel/SidePanelModal/index.tsx @@ -27,7 +27,7 @@ import type SidePanelModalProps from './types'; function SidePanelModal({children, sidePanelTranslateX, closeSidePanel, shouldHideSidePanelBackdrop}: SidePanelModalProps) { const styles = useThemeStyles(); const {isExtraLargeScreenWidth, shouldUseNarrowLayout} = useResponsiveLayout(); - const {paddingTop, paddingBottom} = useSafeAreaPaddings(); + const {paddingTop, paddingBottom, paddingLeft, paddingRight} = useSafeAreaPaddings(); const [isRHPVisible = false] = useOnyx(ONYXKEYS.MODAL, {selector: isRHPVisibleSelector}); const uniqueModalId = ComposerFocusManager.getId(); @@ -85,7 +85,7 @@ function SidePanelModal({children, sidePanelTranslateX, closeSidePanel, shouldHi styles.sidePanelContent, styles.sidePanelContentWidth(shouldUseNarrowLayout), styles.sidePanelContentBorderWidth(isExtraLargeScreenWidth), - {transform: [{translateX: sidePanelTranslateX.current}], paddingTop, paddingBottom}, + {transform: [{translateX: sidePanelTranslateX.current}], paddingTop, paddingBottom, paddingLeft, paddingRight}, ]} > {children} diff --git a/src/hooks/useIsInLandscapeMode/index.ts b/src/hooks/useIsInLandscapeMode/index.ts index 01775a6d2c34..b2b014a18625 100644 --- a/src/hooks/useIsInLandscapeMode/index.ts +++ b/src/hooks/useIsInLandscapeMode/index.ts @@ -1,8 +1,15 @@ +import useWindowDimensions from '@hooks/useWindowDimensions'; + +import isInLandscapeMode from '@libs/isInLandscapeMode'; /** - * We only want to change UI according to the landscape mode on native platforms. + * Returns whether mWeb is currently in landscape orientation. + * If component already uses useResponsiveLayout, it will return the value from that hook. + * If component already uses useWindowDimensions, use @libs/isInLandscapeMode instead. */ function useIsInLandscapeMode(): boolean { - return false; + const {windowWidth, windowHeight} = useWindowDimensions(); + + return isInLandscapeMode(windowWidth, windowHeight); } export default useIsInLandscapeMode; diff --git a/src/hooks/useResponsiveLayout/index.ts b/src/hooks/useResponsiveLayout/index.ts index ba8e6a046244..882eba412fe7 100644 --- a/src/hooks/useResponsiveLayout/index.ts +++ b/src/hooks/useResponsiveLayout/index.ts @@ -2,6 +2,8 @@ import ModalContext from '@components/Modal/ModalContext'; import useWindowDimensions from '@hooks/useWindowDimensions'; +import isInLandscapeModeUtil from '@libs/isInLandscapeMode'; + import variables from '@styles/variables'; import CONST from '@src/CONST'; @@ -31,12 +33,14 @@ import type ResponsiveLayoutResult from './types'; export default function useResponsiveLayout(): ResponsiveLayoutResult { const {windowWidth, windowHeight} = useWindowDimensions(); + const isInLandscapeMode = isInLandscapeModeUtil(windowWidth, windowHeight); + // When the soft keyboard opens on mWeb, the window height changes. Use static screen height instead to get real screenHeight. const screenHeight = Dimensions.get('screen').height; const isExtraSmallScreenHeight = screenHeight <= variables.extraSmallMobileResponsiveHeightBreakpoint; - const isSmallScreenWidth = windowWidth <= variables.mobileResponsiveWidthBreakpoint; + const isSmallScreenWidth = windowWidth <= variables.mobileResponsiveWidthBreakpoint || isInLandscapeMode; const isMediumScreenWidth = windowWidth > variables.mobileResponsiveWidthBreakpoint && windowWidth <= variables.tabletResponsiveWidthBreakpoint; - const onboardingIsMediumOrLargerScreenWidth = windowWidth > variables.mobileResponsiveWidthBreakpoint; + const onboardingIsMediumOrLargerScreenWidth = !isInLandscapeMode && windowWidth > variables.mobileResponsiveWidthBreakpoint; const isLargeScreenWidth = windowWidth > variables.tabletResponsiveWidthBreakpoint; const isExtraLargeScreenWidth = windowWidth > variables.sidePanelResponsiveWidthBreakpoint; const isExtraSmallScreenWidth = windowWidth <= variables.extraSmallMobileResponsiveWidthBreakpoint; @@ -66,7 +70,7 @@ export default function useResponsiveLayout(): ResponsiveLayoutResult { // and the component calling this hook is not the child of another modal type, such as a confirm modal (isDisplayedInNarrowModalNavigator && !activeModalType); - const shouldUseNarrowLayout = isSmallScreenWidth || isInNarrowPaneModal; + const shouldUseNarrowLayout = isSmallScreenWidth || isInNarrowPaneModal || isInLandscapeMode; return { shouldUseNarrowLayout, @@ -79,6 +83,6 @@ export default function useResponsiveLayout(): ResponsiveLayoutResult { isLargeScreenWidth, isExtraLargeScreenWidth, isSmallScreen, - isInLandscapeMode: false, + isInLandscapeMode, }; } diff --git a/src/hooks/useSafeAreaPaddings.ts b/src/hooks/useSafeAreaPaddings.ts index 151da9ae061a..3b64e03cd86b 100644 --- a/src/hooks/useSafeAreaPaddings.ts +++ b/src/hooks/useSafeAreaPaddings.ts @@ -35,7 +35,7 @@ import useStyleUtils from './useStyleUtils'; function useSafeAreaPaddings(isUsingEdgeToEdgeBottomSafeAreaPadding = false) { const StyleUtils = useStyleUtils(); const insets = useSafeAreaInsets(); - const {paddingTop, paddingBottom} = useMemo(() => StyleUtils.getPlatformSafeAreaPadding(insets), [StyleUtils, insets]); + const {paddingTop, paddingBottom, paddingLeft, paddingRight} = useMemo(() => StyleUtils.getPlatformSafeAreaPadding(insets), [StyleUtils, insets]); const screenWrapperStatusContext = useContext(ScreenWrapperStatusContext); const isSafeAreaTopPaddingApplied = screenWrapperStatusContext?.isSafeAreaTopPaddingApplied ?? false; @@ -66,9 +66,13 @@ function useSafeAreaPaddings(isUsingEdgeToEdgeBottomSafeAreaPadding = false) { return { paddingTop: isSafeAreaTopPaddingApplied ? 0 : paddingTop, paddingBottom: adaptedPaddingBottom, + paddingLeft, + paddingRight, unmodifiedPaddings: { top: paddingTop, bottom: paddingBottom, + left: paddingLeft, + right: paddingRight, }, insets: adaptedInsets, safeAreaPaddingBottomStyle, diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators/useModalStackScreenOptions.ts b/src/libs/Navigation/AppNavigator/ModalStackNavigators/useModalStackScreenOptions.ts index a1e6e23c1410..aed188ee9233 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators/useModalStackScreenOptions.ts +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators/useModalStackScreenOptions.ts @@ -61,7 +61,9 @@ function useWideModalStackScreenOptions() { contentStyle: styles.navigationScreenCardStyle, }, web: { - cardStyle: styles.navigationScreenCardStyle, + cardStyle: isSmallScreenWidth + ? {...styles.navigationScreenCardStyle, paddingLeft: 'env(safe-area-inset-left)', paddingRight: 'env(safe-area-inset-right)'} + : styles.navigationScreenCardStyle, cardStyleInterpolator, transitionSpec: isSmallScreenWidth ? undefined : RHP_WEB_TRANSITION_SPEC, }, diff --git a/src/libs/Navigation/AppNavigator/useRootNavigatorScreenOptions.ts b/src/libs/Navigation/AppNavigator/useRootNavigatorScreenOptions.ts index 2c678754b464..fc481fa3e219 100644 --- a/src/libs/Navigation/AppNavigator/useRootNavigatorScreenOptions.ts +++ b/src/libs/Navigation/AppNavigator/useRootNavigatorScreenOptions.ts @@ -74,6 +74,8 @@ const useRootNavigatorScreenOptions = () => { top: 0, left: 0, position: 'fixed', + paddingLeft: 'env(safe-area-inset-left)', + paddingRight: 'env(safe-area-inset-right)', }, cardStyleInterpolator: (props: StackCardInterpolationProps) => modalCardStyleInterpolator({props, enter: onboardingEnter}), }, @@ -113,7 +115,9 @@ const useRootNavigatorScreenOptions = () => { animation: Animations.NONE, web: { cardStyleInterpolator: (props: StackCardInterpolationProps) => modalCardStyleInterpolator({props, enter: {kind: 'none'}, applySidePanelOffset: true}), - cardStyle: shouldUseNarrowLayout ? {...StyleUtils.getNavigationModalCardStyle(), paddingLeft: 0} : {...themeStyles.h100, width: '100%'}, + cardStyle: shouldUseNarrowLayout + ? {...StyleUtils.getNavigationModalCardStyle(), paddingLeft: 'env(safe-area-inset-left)', paddingRight: 'env(safe-area-inset-right)'} + : {...themeStyles.h100, width: '100%'}, }, }, } satisfies RootNavigatorScreenOptions; diff --git a/src/libs/Navigation/AppNavigator/useSplitNavigatorScreenOptions.ts b/src/libs/Navigation/AppNavigator/useSplitNavigatorScreenOptions.ts index e375f26b24da..842b86230ef4 100644 --- a/src/libs/Navigation/AppNavigator/useSplitNavigatorScreenOptions.ts +++ b/src/libs/Navigation/AppNavigator/useSplitNavigatorScreenOptions.ts @@ -19,6 +19,11 @@ import useModalCardStyleInterpolator from './useModalCardStyleInterpolator'; const IS_MOBILE_SAFARI = isMobileSafari(); +// On mWeb the centralScreen card is `position: fixed`, so `width: 100%` resolves against the viewport +// (including the landscape notch gutters), not the layout parent. Subtract the safe-area +// insets so the card is sized to the usable safe region instead of overflowing. +const NARROW_CARD_SAFE_AREA_WIDTH = 'calc(100% - env(safe-area-inset-left) - env(safe-area-inset-right))'; + type SplitNavigatorScreenOptions = { sidebarScreen: PlatformStackNavigationOptions; centralScreen: PlatformStackNavigationOptions; @@ -66,7 +71,7 @@ const useSplitNavigatorScreenOptions = () => { web: { cardStyleInterpolator: (props: StackCardInterpolationProps) => modalCardStyleInterpolator({props, enter: centralScreenEnter}), cardStyle: shouldUseNarrowLayout - ? StyleUtils.getNavigationModalCardStyle() + ? {...StyleUtils.getNavigationModalCardStyle(), width: NARROW_CARD_SAFE_AREA_WIDTH} : { ...themeStyles.h100, }, diff --git a/src/libs/getIsNarrowLayout/index.ts b/src/libs/getIsNarrowLayout/index.ts index e901f04e7d26..f483b228da22 100644 --- a/src/libs/getIsNarrowLayout/index.ts +++ b/src/libs/getIsNarrowLayout/index.ts @@ -1,5 +1,6 @@ import getIsSmallScreenWidth from '@libs/getIsSmallScreenWidth'; +import isInLandscapeMode from '@libs/isInLandscapeMode'; export default function getIsNarrowLayout() { - return getIsSmallScreenWidth(); + return getIsSmallScreenWidth() || isInLandscapeMode(); } diff --git a/src/libs/isInLandscapeMode/index.ts b/src/libs/isInLandscapeMode/index.ts index b1090972fbd0..358499a8515b 100644 --- a/src/libs/isInLandscapeMode/index.ts +++ b/src/libs/isInLandscapeMode/index.ts @@ -1,7 +1,7 @@ -/** - * We only want to change UI according to the landscape mode on native platforms. - */ -// eslint-disable-next-line @typescript-eslint/no-unused-vars -export default function isInLandscapeMode(windowWidth: number, windowHeight: number): boolean { - return false; +import {isMobile} from '@libs/Browser'; + +import {Dimensions} from 'react-native'; + +export default function isInLandscapeMode(windowWidth = Dimensions.get('window').width, windowHeight = Dimensions.get('window').height): boolean { + return isMobile() && windowWidth > windowHeight; } diff --git a/src/pages/iou/request/step/IOURequestStepDistanceGPS/index.tsx b/src/pages/iou/request/step/IOURequestStepDistanceGPS/index.tsx index a1ae718352d9..1937a37f866c 100644 --- a/src/pages/iou/request/step/IOURequestStepDistanceGPS/index.tsx +++ b/src/pages/iou/request/step/IOURequestStepDistanceGPS/index.tsx @@ -1,6 +1,7 @@ import Button from '@components/ButtonComposed'; import {loadIllustration} from '@components/Icon/IllustrationLoader'; import ImageSVG from '@components/ImageSVG'; +import ScrollView from '@components/ScrollView'; import Text from '@components/Text'; import withCurrentUserPersonalDetails from '@components/withCurrentUserPersonalDetails'; @@ -25,26 +26,28 @@ function IOURequestStepDistanceGPS(props: IOURequestStepDistanceGPSProps) { const {translate} = useLocalize(); return ( - - - + + + + + + + {translate('gps.desktop.title')} + {translate('gps.desktop.subtitle')} + + + - - {translate('gps.desktop.title')} - {translate('gps.desktop.subtitle')} - - - - + ); } diff --git a/src/pages/iou/request/step/IOURequestStepDistanceOdometer.tsx b/src/pages/iou/request/step/IOURequestStepDistanceOdometer.tsx index 09975d9da924..e26fcfb85bd5 100644 --- a/src/pages/iou/request/step/IOURequestStepDistanceOdometer.tsx +++ b/src/pages/iou/request/step/IOURequestStepDistanceOdometer.tsx @@ -3,6 +3,7 @@ import FormHelpMessage from '@components/FormHelpMessage'; import PressableWithFeedback from '@components/Pressable/PressableWithFeedback'; import ReceiptImage from '@components/ReceiptImage'; import type {AnimatedTextInputRef} from '@components/RNTextInput'; +import ScrollView from '@components/ScrollView'; import Text from '@components/Text'; import TextInput from '@components/TextInput'; import type {BaseTextInputRef} from '@components/TextInput/BaseTextInput/types'; @@ -603,7 +604,7 @@ function IOURequestStepDistanceOdometer({ shouldShowWrapper={!isCreatingNewRequest} includeSafeAreaPaddingBottom > - + {/* Start Reading */} @@ -740,7 +741,7 @@ function IOURequestStepDistanceOdometer({ sentryLabel={CONST.SENTRY_LABEL.IOU_REQUEST_STEP.DISTANCE_ODOMETER_NEXT_BUTTON} /> - + ); }