Skip to content

Commit

Permalink
Update useResponsiveLayout to correctly account for react-navigation …
Browse files Browse the repository at this point in the history
…and react-native-modal
  • Loading branch information
roryabraham committed Jun 3, 2024
1 parent a935606 commit 2ebe8da
Show file tree
Hide file tree
Showing 5 changed files with 54 additions and 44 deletions.
75 changes: 41 additions & 34 deletions src/hooks/useResponsiveLayout.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
import {useEffect, useRef, useState} from 'react';
import type {OnyxEntry} from 'react-native-onyx';
import {useOnyx} from 'react-native-onyx';
import Navigation, {navigationRef} from '@libs/Navigation/Navigation';
import ONYXKEYS from '@src/ONYXKEYS';
import type {Modal} from '@src/types/onyx';
import {useContext} from 'react';
import ModalContext from '@components/Modal/ModalContext';
import Navigation from '@libs/Navigation/Navigation';
import CONST from '@src/CONST';
import useRootNavigationState from './useRootNavigationState';
import useWindowDimensions from './useWindowDimensions';

type ResponsiveLayoutResult = {
shouldUseNarrowLayout: boolean;
isSmallScreenWidth: boolean;
isInModal: boolean;
isInNarrowPaneModal: boolean;
isExtraSmallScreenHeight: boolean;
isMediumScreenWidth: boolean;
isLargeScreenWidth: boolean;
Expand All @@ -19,39 +18,47 @@ type ResponsiveLayoutResult = {

/**
* Hook to determine if we are on mobile devices or in the Modal Navigator.
* Use "shouldUseNarrowLayout" for "on mobile or in RHP/LHP", "isSmallScreenWidth" for "on mobile", "isInModal" for "in RHP/LHP".
* Use "shouldUseNarrowLayout" for "on mobile or in RHP/LHP", "isSmallScreenWidth" for "on mobile", "isInNarrowPaneModal" for "in RHP/LHP".
*
* There are two kinds of modals in this app:
* 1. Modal stack navigators from react-navigation
* 2. Modal components that use react-native-modal
*
* This hook is designed to handle both. `shouldUseNarrowLayout` will return `true` if any of the following are true:
* 1. The device screen width is narrow
* 2. The consuming component is the child of a "right docked" react-native-modal component
* 3. The consuming component is a screen in a modal stack navigator and not a child of a "non-right-docked" react-native-modal component.
*
* For more details on the various modal types we've defined for this app and implemented using react-native-modal, see `ModalType`.
*/
export default function useResponsiveLayout(): ResponsiveLayoutResult {
const {isSmallScreenWidth, isExtraSmallScreenHeight, isExtraSmallScreenWidth, isMediumScreenWidth, isLargeScreenWidth, isSmallScreen} = useWindowDimensions();

const [willAlertModalBecomeVisible] = useOnyx(ONYXKEYS.MODAL, {selector: (value: OnyxEntry<Modal>) => value?.willAlertModalBecomeVisible ?? false});
// Note: activeModalType refers to our react-native-modal component wrapper, not react-navigation's modal stack navigators.
// This means it will only be defined if the component calling this hook is a child of a modal component. See BaseModal for the provider.
const {activeModalType} = useContext(ModalContext);

const [isInModal, setIsInModal] = useState(false);
const hasSetIsInModal = useRef(false);
const updateModalStatus = () => {
if (hasSetIsInModal.current) {
return;
}
const isDisplayedInModal = Navigation.isDisplayedInModal();
if (isInModal !== isDisplayedInModal) {
setIsInModal(isDisplayedInModal);
}
hasSetIsInModal.current = true;
};

useEffect(() => {
const unsubscribe = navigationRef?.current?.addListener('state', updateModalStatus);
// This refers to the state of the root navigator, and is true if and only if the topmost navigator is the "left modal navigator" or the "right modal navigator"
const isDisplayedInModalNavigator = !!useRootNavigationState(Navigation.isModalNavigatorActive);

if (navigationRef?.current?.isReady()) {
updateModalStatus();
}
// The component calling this hook is in a "narrow pane modal" if:
const isInNarrowPaneModal =
// it's a child of the right-docked modal
activeModalType === CONST.MODAL.MODAL_TYPE.RIGHT_DOCKED ||
// or there's a "right modal navigator" or "left modal navigator" on the top of the root navigation stack
// and the component calling this hook is not the child of another modal type, such as a confirm modal
(isDisplayedInModalNavigator && !activeModalType);

return () => {
unsubscribe?.();
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const shouldUseNarrowLayout = isSmallScreenWidth || isInNarrowPaneModal;

const shouldUseNarrowLayout = willAlertModalBecomeVisible ? isSmallScreenWidth : isSmallScreenWidth || isInModal;
return {shouldUseNarrowLayout, isSmallScreenWidth, isInModal, isExtraSmallScreenHeight, isExtraSmallScreenWidth, isMediumScreenWidth, isLargeScreenWidth, isSmallScreen};
return {
shouldUseNarrowLayout,
isSmallScreenWidth,
isInNarrowPaneModal,
isExtraSmallScreenHeight,
isExtraSmallScreenWidth,
isMediumScreenWidth,
isLargeScreenWidth,
isSmallScreen,
};
}
2 changes: 1 addition & 1 deletion src/hooks/useRootNavigationState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {navigationRef} from '@libs/Navigation/Navigation';
* Use with caution, because re-rendering any component every time the root navigation state changes can be very costly for performance.
* That's why the selector is mandatory.
*/
function useRootNavigationState(selector: (state: NavigationState) => unknown) {
function useRootNavigationState<T>(selector: (state: NavigationState) => T): T | undefined {
const getSnapshot = useCallback(() => {
if (!navigationRef.current) {
return;
Expand Down
13 changes: 8 additions & 5 deletions src/libs/Navigation/Navigation.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {findFocusedRoute} from '@react-navigation/core';
import type {EventArg, NavigationContainerEventMap} from '@react-navigation/native';
import type {EventArg, NavigationContainerEventMap, NavigationState} from '@react-navigation/native';
import {CommonActions, getPathFromState, StackActions} from '@react-navigation/native';
import Log from '@libs/Log';
import * as ReportUtils from '@libs/ReportUtils';
Expand Down Expand Up @@ -356,9 +356,12 @@ function navigateWithSwitchPolicyID(params: SwitchPolicyIDParams) {
return switchPolicyID(navigationRef.current, params);
}

/** Check if the modal is being displayed */
function isDisplayedInModal() {
const state = navigationRef?.current?.getRootState();
/**
* Check if the modal is being displayed.
*
* @param state - MUST be the state of the root navigator for this to work. Do not use a child navigator state.
*/
function isModalNavigatorActive(state: NavigationState) {
const lastRoute = state?.routes?.at(-1);
const lastRouteName = lastRoute?.name;
return lastRouteName === NAVIGATORS.LEFT_MODAL_NAVIGATOR || lastRouteName === NAVIGATORS.RIGHT_MODAL_NAVIGATOR;
Expand All @@ -383,7 +386,7 @@ export default {
parseHybridAppUrl,
navigateWithSwitchPolicyID,
resetToHome,
isDisplayedInModal,
isModalNavigatorActive,
closeRHPFlow,
setNavigationActionToMicrotaskQueue,
};
Expand Down
4 changes: 2 additions & 2 deletions src/pages/signin/LoginForm/BaseLoginForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ function BaseLoginForm({account, credentials, closeAccount, blurOnSubmit = false
const firstBlurred = useRef(false);
const isFocused = useIsFocused();
const isLoading = useRef(false);
const {shouldUseNarrowLayout, isInModal} = useResponsiveLayout();
const {shouldUseNarrowLayout, isInNarrowPaneModal} = useResponsiveLayout();

/**
* Validate the input value and set the error for formError
Expand Down Expand Up @@ -167,7 +167,7 @@ function BaseLoginForm({account, credentials, closeAccount, blurOnSubmit = false
return;
}
let focusTimeout: NodeJS.Timeout;
if (isInModal) {
if (isInNarrowPaneModal) {
focusTimeout = setTimeout(() => input.current?.focus(), CONST.ANIMATED_TRANSITION);
} else {
input.current.focus();
Expand Down
4 changes: 2 additions & 2 deletions src/pages/signin/SignInPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ function SignInPageInner({credentials, account, activeClients = [], preferredLoc
const styles = useThemeStyles();
const StyleUtils = useStyleUtils();
const {translate, formatPhoneNumber} = useLocalize();
const {shouldUseNarrowLayout, isInModal} = useResponsiveLayout();
const {shouldUseNarrowLayout, isInNarrowPaneModal} = useResponsiveLayout();
const safeAreaInsets = useSafeAreaInsets();
const signInPageLayoutRef = useRef<SignInPageLayoutRef>(null);
const loginFormRef = useRef<InputHandle>(null);
Expand Down Expand Up @@ -250,7 +250,7 @@ function SignInPageInner({credentials, account, activeClients = [], preferredLoc
<ScreenWrapper
shouldShowOfflineIndicator={false}
shouldEnableMaxHeight={shouldEnableMaxHeight}
style={[styles.signInPage, StyleUtils.getSafeAreaPadding({...safeAreaInsets, bottom: 0, top: isInModal ? 0 : safeAreaInsets.top}, 1)]}
style={[styles.signInPage, StyleUtils.getSafeAreaPadding({...safeAreaInsets, bottom: 0, top: isInNarrowPaneModal ? 0 : safeAreaInsets.top}, 1)]}
testID={SignInPageInner.displayName}
>
<SignInPageLayout
Expand Down

0 comments on commit 2ebe8da

Please sign in to comment.