Skip to content
75 changes: 49 additions & 26 deletions src/components/TimeModalPicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,26 @@ import type {ForwardedRef} from 'react';
import {View} from 'react-native';
import useThemeStyles from '@hooks/useThemeStyles';
import DateUtils from '@libs/DateUtils';
import useIsCenteredRHPModal from '@libs/Navigation/AppNavigator/useIsCenteredRHPModal';
import CONST from '@src/CONST';
import HeaderWithBackButton from './HeaderWithBackButton';
import MenuItemWithTopDescription from './MenuItemWithTopDescription';
import Modal from './Modal';
import ScreenWrapper from './ScreenWrapper';
import TimePicker from './TimePicker/TimePicker';

/** Configuration handed to the parent step so it can render the picker inline within the same modal. */
type InlineTimePickerConfig = {
/** The currently selected value (ISO datetime string). */
value?: string;

/** Header label for the picker. */
label: string;

/** Called with the selected 12-hour time string when the user saves. */
onSubmit: (time: string) => void;
};

type TimeModalPickerProps = {
/** Current value of the selected item */
value?: string;
Expand All @@ -23,12 +36,16 @@ type TimeModalPickerProps = {
/** Label for the picker */
label: string;

/** When provided (centered RHP modal), tapping the row asks the parent step to render the picker inline instead of opening a separate modal. */
onRequestOpenInline?: (config: InlineTimePickerConfig) => void;

/** Reference to the outer element */
ref?: ForwardedRef<View>;
};

function TimeModalPicker({value, errorText, label, onInputChange = () => {}, ref}: TimeModalPickerProps) {
function TimeModalPicker({value, errorText, label, onInputChange = () => {}, onRequestOpenInline, ref}: TimeModalPickerProps) {
const styles = useThemeStyles();
const isCenteredModal = useIsCenteredRHPModal();
const [isPickerVisible, setIsPickerVisible] = useState(false);
const currentTime = value ? DateUtils.extractTime12Hour(value) : undefined;

Expand All @@ -42,45 +59,51 @@ function TimeModalPicker({value, errorText, label, onInputChange = () => {}, ref
hidePickerModal();
};

// Inside a centered RHP modal, render the picker inline via the parent step instead of opening a second modal.
const shouldRenderInline = isCenteredModal && !!onRequestOpenInline;

return (
<>
<MenuItemWithTopDescription
shouldShowRightIcon
title={currentTime}
description={label}
onPress={() => setIsPickerVisible(true)}
onPress={() => (shouldRenderInline ? onRequestOpenInline?.({value, label, onSubmit: updateInput}) : setIsPickerVisible(true))}
brickRoadIndicator={errorText ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined}
errorText={errorText}
ref={ref}
/>
<Modal
type={CONST.MODAL.MODAL_TYPE.RIGHT_DOCKED}
isVisible={isPickerVisible}
onClose={hidePickerModal}
onModalHide={hidePickerModal}
enableEdgeToEdgeBottomSafeAreaPadding
>
<ScreenWrapper
style={styles.pb0}
includePaddingTop={false}
includeSafeAreaPaddingBottom
testID="TimeModalPicker"
{!shouldRenderInline && (
<Modal
type={CONST.MODAL.MODAL_TYPE.RIGHT_DOCKED}
isVisible={isPickerVisible}
onClose={hidePickerModal}
onModalHide={hidePickerModal}
enableEdgeToEdgeBottomSafeAreaPadding
>
<HeaderWithBackButton
title={label}
onBackButtonPress={hidePickerModal}
/>
<View style={styles.flex1}>
<TimePicker
defaultValue={value}
onSubmit={updateInput}
shouldValidateFutureTime={false}
<ScreenWrapper
style={styles.pb0}
includePaddingTop={false}
includeSafeAreaPaddingBottom
testID="TimeModalPicker"
>
<HeaderWithBackButton
title={label}
onBackButtonPress={hidePickerModal}
/>
</View>
</ScreenWrapper>
</Modal>
<View style={styles.flex1}>
<TimePicker
defaultValue={value}
onSubmit={updateInput}
shouldValidateFutureTime={false}
/>
</View>
</ScreenWrapper>
</Modal>
)}
</>
);
}

export default TimeModalPicker;
export type {InlineTimePickerConfig};
36 changes: 22 additions & 14 deletions src/components/ValuePicker/index.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React, {useState} from 'react';
import {View} from 'react-native';
import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription';
import useIsCenteredRHPModal from '@libs/Navigation/AppNavigator/useIsCenteredRHPModal';
import Navigation from '@libs/Navigation/Navigation';
import CONST from '@src/CONST';
import type {ValuePickerItem, ValuePickerProps} from './types';
Expand All @@ -22,7 +23,9 @@ function ValuePicker({
addBottomSafeAreaPadding = true,
disableKeyboardShortcuts = false,
alternateNumberOfSupportedLines,
onRequestOpenInline,
}: ValuePickerProps) {
const isCenteredModal = useIsCenteredRHPModal();
const [isPickerVisible, setIsPickerVisible] = useState(false);

const showPickerModal = () => {
Expand All @@ -42,6 +45,9 @@ function ValuePicker({

const selectedItem = items?.find((item) => item.value === value);

// Inside a centered RHP modal, render the selection list inline via the parent step instead of opening a second modal.
const shouldRenderInline = isCenteredModal && !!onRequestOpenInline;

return (
<View>
{shouldShowModal ? (
Expand All @@ -52,25 +58,27 @@ function ValuePicker({
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
title={selectedItem?.label || placeholder || ''}
description={label}
onPress={showPickerModal}
onPress={() => (shouldRenderInline ? onRequestOpenInline?.({label, items, selectedItem, onItemSelected: updateInput, shouldShowTooltips}) : showPickerModal())}
furtherDetails={furtherDetails}
brickRoadIndicator={errorText ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined}
errorText={errorText}
forwardedFSClass={forwardedFSClass}
/>
<ValueSelectorModal
isVisible={isPickerVisible}
label={label}
selectedItem={selectedItem}
items={items}
onClose={hidePickerModal}
onItemSelected={updateInput}
shouldShowTooltips={shouldShowTooltips}
onBackdropPress={Navigation.dismissModal}
shouldEnableKeyboardAvoidingView={false}
addBottomSafeAreaPadding={addBottomSafeAreaPadding}
alternateNumberOfSupportedLines={alternateNumberOfSupportedLines}
/>
{!shouldRenderInline && (
<ValueSelectorModal
isVisible={isPickerVisible}
label={label}
selectedItem={selectedItem}
items={items}
onClose={hidePickerModal}
onItemSelected={updateInput}
shouldShowTooltips={shouldShowTooltips}
onBackdropPress={Navigation.dismissModal}
shouldEnableKeyboardAvoidingView={false}
addBottomSafeAreaPadding={addBottomSafeAreaPadding}
alternateNumberOfSupportedLines={alternateNumberOfSupportedLines}
/>
)}
</>
) : (
<ValueSelectionList
Expand Down
8 changes: 7 additions & 1 deletion src/components/ValuePicker/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,9 @@ type ValueSelectionListProps = Pick<
isVisible?: boolean;
};

/** Config handed to the parent step so it can render the value selection list inline within the same centered modal. */
type InlineValuePickerConfig = Pick<ValueSelectorModalProps, 'label' | 'items' | 'selectedItem' | 'onItemSelected' | 'shouldShowTooltips'>;

type ValuePickerProps = ForwardedFSClassProps & {
/** Item to display */
value?: string;
Expand Down Expand Up @@ -98,6 +101,9 @@ type ValuePickerProps = ForwardedFSClassProps & {

/** Number of lines to show for alternate text */
alternateNumberOfSupportedLines?: number;

/** When provided (centered RHP modal), tapping the row asks the parent step to render the selection list inline instead of opening a separate modal. */
onRequestOpenInline?: (config: InlineValuePickerConfig) => void;
};

export type {ValuePickerItem, ValueSelectorModalProps, ValuePickerProps, ValueSelectionListProps};
export type {ValuePickerItem, ValueSelectorModalProps, ValuePickerProps, ValueSelectionListProps, InlineValuePickerConfig};
8 changes: 0 additions & 8 deletions src/components/WideRHPContextProvider/index.native.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,11 @@ import {defaultWideRHPActionsContextValue, defaultWideRHPStateContextValue} from
import type {WideRHPActionsContextType, WideRHPStateContextType} from './types';

const secondOverlayWideRHPProgress = new Animated.Value(0);
const secondOverlayRHPOnWideRHPProgress = new Animated.Value(0);
const secondOverlayRHPOnSuperWideRHPProgress = new Animated.Value(0);
const thirdOverlayProgress = new Animated.Value(0);

const animatedReceiptPaneRHPWidth = new Animated.Value(0);
const animatedWideRHPWidth = new Animated.Value(0);
const animatedSuperWideRHPWidth = new Animated.Value(0);

const modalStackOverlaySuperWideRHPPositionLeft = new Animated.Value(0);
const modalStackOverlayWideRHPPositionLeft = new Animated.Value(0);

const expandedRHPProgress = new Animated.Value(0);
Expand Down Expand Up @@ -45,12 +41,8 @@ export {
animatedSuperWideRHPWidth,
animatedWideRHPWidth,
expandedRHPProgress,
modalStackOverlaySuperWideRHPPositionLeft,
modalStackOverlayWideRHPPositionLeft,
secondOverlayRHPOnSuperWideRHPProgress,
secondOverlayRHPOnWideRHPProgress,
secondOverlayWideRHPProgress,
thirdOverlayProgress,
useWideRHPState,
useWideRHPActions,
};
Expand Down
4 changes: 0 additions & 4 deletions src/components/WideRHPContextProvider/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -357,12 +357,8 @@ export {
animatedSuperWideRHPWidth,
animatedWideRHPWidth,
expandedRHPProgress,
modalStackOverlaySuperWideRHPPositionLeft,
modalStackOverlayWideRHPPositionLeft,
secondOverlayWideRHPProgress,
secondOverlayRHPOnWideRHPProgress,
secondOverlayRHPOnSuperWideRHPProgress,
thirdOverlayProgress,
useWideRHPState,
useWideRHPActions,
};
48 changes: 8 additions & 40 deletions src/components/WideRHPOverlayWrapper/index.tsx
Original file line number Diff line number Diff line change
@@ -1,52 +1,29 @@
import {useRoute} from '@react-navigation/native';
import React from 'react';
import {
animatedReceiptPaneRHPWidth,
modalStackOverlaySuperWideRHPPositionLeft,
modalStackOverlayWideRHPPositionLeft,
secondOverlayRHPOnSuperWideRHPProgress,
secondOverlayRHPOnWideRHPProgress,
secondOverlayWideRHPProgress,
useWideRHPState,
} from '@components/WideRHPContextProvider';
import {modalStackOverlayWideRHPPositionLeft, secondOverlayWideRHPProgress, useWideRHPState} from '@components/WideRHPContextProvider';
import useResponsiveLayout from '@hooks/useResponsiveLayout';
import Overlay from '@libs/Navigation/AppNavigator/Navigators/Overlay';

function SecondaryOverlay() {
const {shouldRenderSecondaryOverlayForRHPOnSuperWideRHP, shouldRenderSecondaryOverlayForRHPOnWideRHP, shouldRenderSecondaryOverlayForWideRHP, superWideRHPRouteKeys, wideRHPRouteKeys} =
useWideRHPState();
const {shouldRenderSecondaryOverlayForWideRHP, superWideRHPRouteKeys} = useWideRHPState();

const route = useRoute();

const isWide = !!route?.key && wideRHPRouteKeys.includes(route.key);
const isSuperWide = !!route?.key && superWideRHPRouteKeys.includes(route.key);

const isRHPDisplayedOnWideRHP = shouldRenderSecondaryOverlayForRHPOnWideRHP && isWide;
const isRHPDisplayedOnSuperWideRHP = shouldRenderSecondaryOverlayForRHPOnSuperWideRHP && isSuperWide;
const isWideRHPDisplayedOnSuperWideRHP = shouldRenderSecondaryOverlayForWideRHP && isSuperWide;

/**
* These overlays are used to cover the space under the narrower RHP screen when more than one RHP width is displayed on the screen
* Their position is calculated as follows:
* This overlay is used to cover the space under the narrower RHP screen when more than one RHP width is displayed on the screen.
* Its position is calculated as follows:
* The width of the window for which we calculate the overlay positions is the width of the RHP window, for example for Super Wide RHP it will be 1260 px on a wide layout.
* We need to move the overlay left from the left edge of the RHP below to the left edge of the RHP above.
* To calculate this, subtract the width of the widest RHP from the width of the RHP above.
* Please note that in these cases, the overlay is rendered from the RHP screen displayed below. For example, if we display RHP on Wide RHP, the secondary overlay is rendered from Wide RHP, etc.
* Three cases were described for the secondary overlay:
* 1. Single RHP is displayed on Wide RHP
* 2. Single RHP is displayed on Super Wide RHP
* 3. Wide RHP is displayed on Super Wide RHP route.
* Please note that in this case, the overlay is rendered from the RHP screen displayed below (the Wide RHP).
*
* The "single RHP displayed on (super) wide RHP" cases are not handled here: those small RHPs are centered modals with their
* own dim (see ModalStackNavigators index). Only "Wide RHP displayed on Super Wide RHP" remains.
* */
if (isRHPDisplayedOnWideRHP) {
return (
<Overlay
progress={secondOverlayRHPOnWideRHPProgress}
// If RHP is displayed on Wide RHP which is displayed above the Super Wide RHP, the secondary overlay's position left should be calculated from the left edge of the super wide RHP.
positionLeftValue={animatedReceiptPaneRHPWidth}
/>
);
}

if (isWideRHPDisplayedOnSuperWideRHP) {
return (
<Overlay
Expand All @@ -56,15 +33,6 @@ function SecondaryOverlay() {
);
}

if (isRHPDisplayedOnSuperWideRHP) {
return (
<Overlay
progress={secondOverlayRHPOnSuperWideRHPProgress}
positionLeftValue={modalStackOverlaySuperWideRHPPositionLeft}
/>
);
}

return null;
}

Expand Down
22 changes: 21 additions & 1 deletion src/libs/Navigation/AppNavigator/AuthScreens.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ import createRootStackNavigator from './createRootStackNavigator';
import {screensWithEnteringAnimation} from './createRootStackNavigator/GetStateForActionHandlers';
import defaultScreenOptions from './defaultScreenOptions';
import DelegatorConnectGuard from './DelegatorConnectGate';
import doesRHPStackHaveWidePane from './doesRHPStackHaveWidePane';
import hideKeyboardOnSwipe from './hideKeyboardOnSwipe';
import KeyboardShortcutsHandler from './KeyboardShortcutsHandler';
import {ShareModalStackNavigator} from './ModalStackNavigators';
Expand All @@ -65,6 +66,7 @@ import TestDriveModalNavigator from './Navigators/TestDriveModalNavigator';
import TestToolsModalNavigator from './Navigators/TestToolsModalNavigator';
import TestDriveDemoNavigator from './TestDriveDemoNavigator';
import ThreeDSAuthHandler from './ThreeDSAuthHandler';
import useIsCenteredRHPModal from './useIsCenteredRHPModal';
import useModalCardStyleInterpolator from './useModalCardStyleInterpolator';
import useRootNavigatorScreenOptions from './useRootNavigatorScreenOptions';
import UserStatusHandler from './UserStatusHandler';
Expand Down Expand Up @@ -125,6 +127,7 @@ function AuthScreens() {
const {shouldUseNarrowLayout} = useResponsiveLayout();
const rootNavigatorScreenOptions = useRootNavigatorScreenOptions();
const modalCardStyleInterpolator = useModalCardStyleInterpolator();
const isCenteredRHPModal = useIsCenteredRHPModal();
const {isOnboardingCompleted} = useOnboardingFlowRouter();

useEffect(() => {
Expand All @@ -135,6 +138,23 @@ function AuthScreens() {
};
}, [theme]);

// Dynamic options for RIGHT_MODAL_NAVIGATOR: on wide layout a standalone small RHP fades the whole navigator in place
// (matching centered/alert modals, so there's no lateral movement). Wide expense/report panes and narrow layout keep the slide entrance.
const getRightModalNavigatorOptions = ({route}: {route: RouteProp<AuthScreensParamList, typeof NAVIGATORS.RIGHT_MODAL_NAVIGATOR>}) => {
const baseOptions = rootNavigatorScreenOptions.rightModalNavigator;
if (!isCenteredRHPModal || doesRHPStackHaveWidePane(route)) {
return baseOptions;
}
return {
...baseOptions,
web: {
...baseOptions.web,
// A centered modal is positioned independently (fixed, centered box), so the right-docked side-panel offset doesn't apply.
cardStyleInterpolator: (props: StackCardInterpolationProps) => modalCardStyleInterpolator({props, enter: {kind: 'fade'}}),
},
};
};

// Dynamic options for TAB_NAVIGATOR: supports entering animation for pushed instances
const getTabNavigatorOptions = ({route}: {route: RouteProp<AuthScreensParamList>}) => {
if (!shouldUseNarrowLayout) {
Expand Down Expand Up @@ -292,7 +312,7 @@ function AuthScreens() {
/>
<RootStack.Screen
name={NAVIGATORS.RIGHT_MODAL_NAVIGATOR}
options={rootNavigatorScreenOptions.rightModalNavigator}
options={getRightModalNavigatorOptions}
getComponent={loadRightModalNavigator}
listeners={modalScreenListenersWithCancelSearch}
/>
Expand Down
Loading
Loading