Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 57 additions & 26 deletions src/libs/Navigation/OnyxTabNavigator.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,15 @@ import ONYXKEYS from '@src/ONYXKEYS';
import type {SelectedTabRequest} from '@src/types/onyx';
import type ChildrenProps from '@src/types/utils/ChildrenProps';
import isLoadingOnyxValue from '@src/types/utils/isLoadingOnyxValue';
import KeyboardUtils from '@src/utils/keyboard';

import type {MaterialTopTabNavigationEventMap} from '@react-navigation/material-top-tabs';
import type {EventArg, EventMapCore, NavigationProp, NavigationState, ParamListBase, ScreenListeners} from '@react-navigation/native';

import {createMaterialTopTabNavigator} from '@react-navigation/material-top-tabs';
import {TabActions, useRoute} from '@react-navigation/native';
import React, {useCallback, useContext, useEffect, useRef, useState} from 'react';
import {StyleSheet, View} from 'react-native';
import {Keyboard, StyleSheet, View} from 'react-native';

import type {RegisterTabSwitchGuard, TabSwitchGuard} from './TabSwitchGuardContext';

Expand Down Expand Up @@ -71,6 +72,9 @@ type OnyxTabNavigatorProps<TTabName extends string = SelectedTabRequest> = Child

/** Whether tabs should have equal width */
equalWidth?: boolean;

/** Whether to wait for the keyboard to close before switching tabs */
shouldDismissKeyboardBeforeTabSwitch?: boolean;
};

const TopTab = createMaterialTopTabNavigator<ParamListBase, string>();
Expand Down Expand Up @@ -113,6 +117,7 @@ function OnyxTabNavigator<TTabName extends string = SelectedTabRequest>({
lazyLoadEnabled = false,
onTabSelect,
equalWidth = false,
shouldDismissKeyboardBeforeTabSwitch = false,
...rest
}: OnyxTabNavigatorProps<TTabName>) {
const styles = useThemeStyles();
Expand Down Expand Up @@ -154,6 +159,7 @@ function OnyxTabNavigator<TTabName extends string = SelectedTabRequest>({
// Tab-switch discard guards, keyed by tab name. Tab screens register via `useDiscardChangesConfirmation`.
const guardsRef = useRef<Map<string, TabSwitchGuard>>(new Map());
const isDiscardModalOpenRef = useRef(false);
const isTabSwitchPendingRef = useRef(false);

const registerTabGuard: RegisterTabSwitchGuard = (guard) => {
guardsRef.current.set(guard.tabName, guard);
Expand All @@ -166,43 +172,68 @@ function OnyxTabNavigator<TTabName extends string = SelectedTabRequest>({
};
};

const runAfterKeyboardDismiss = (callback: () => void) => {
if (!shouldDismissKeyboardBeforeTabSwitch || !Keyboard.isVisible()) {
callback();
return;
}

isTabSwitchPendingRef.current = true;
KeyboardUtils.dismiss()
.then(callback)
.finally(() => {
isTabSwitchPendingRef.current = false;
});
};

const handleTabPress = (navigation: NavigationProp<ParamListBase>, event: EventArg<'tabPress', true, undefined>) => {
if (isDiscardModalOpenRef.current) {
if (isDiscardModalOpenRef.current || isTabSwitchPendingRef.current) {
event.preventDefault();
return;
}
const navState = navigation.getState();
const currentRouteName = navState.routes.at(navState.index)?.name;
const guard = currentRouteName ? guardsRef.current.get(currentRouteName) : undefined;
if (!guard || !guard.getHasUnsavedChanges()) {
return;
}
const targetRoute = navState.routes.find((tabRoute) => tabRoute.key === event.target);
if (!targetRoute || targetRoute.name === currentRouteName) {
return;
}
event.preventDefault();
isDiscardModalOpenRef.current = true;
showConfirmModal({
...getDiscardChangesModalConfig(translate),
shouldIgnoreBackHandlerDuringTransition: true,
}).then((result) => {
isDiscardModalOpenRef.current = false;
if (result.action !== ModalActions.CONFIRM) {
guard.onCancel?.();
const guard = currentRouteName ? guardsRef.current.get(currentRouteName) : undefined;
if (!guard || !guard.getHasUnsavedChanges()) {
if (!shouldDismissKeyboardBeforeTabSwitch || !Keyboard.isVisible()) {
return;
}
// User confirmed: always jump to the target tab, even if onDiscard fails, rather than stranding them with no feedback.
Promise.resolve()
.then(() => guard.onDiscard())
.catch((error: unknown) => {
Log.warn('[OnyxTabNavigator] Failed to run tab-switch onDiscard callback', {error});
Growl.error(translate('common.genericErrorMessage'));
})
.then(() => {
navigation.dispatch(TabActions.jumpTo(targetRoute.name));
});
});

event.preventDefault();
runAfterKeyboardDismiss(() => navigation.dispatch(TabActions.jumpTo(targetRoute.name)));
return;
}
event.preventDefault();
isDiscardModalOpenRef.current = true;

const showDiscardModal = () => {
showConfirmModal({
...getDiscardChangesModalConfig(translate),
shouldIgnoreBackHandlerDuringTransition: true,
}).then((result) => {
isDiscardModalOpenRef.current = false;
if (result.action !== ModalActions.CONFIRM) {
guard.onCancel?.();
return;
}
// User confirmed: always jump to the target tab, even if onDiscard fails, rather than stranding them with no feedback.
Promise.resolve()
.then(() => guard.onDiscard())
.catch((error: unknown) => {
Log.warn('[OnyxTabNavigator] Failed to run tab-switch onDiscard callback', {error});
Growl.error(translate('common.genericErrorMessage'));
})
.then(() => {
runAfterKeyboardDismiss(() => navigation.dispatch(TabActions.jumpTo(targetRoute.name)));
});
});
};

runAfterKeyboardDismiss(showDiscardModal);
};

/**
Expand Down
5 changes: 4 additions & 1 deletion src/pages/iou/request/DistanceRequestStartPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import HeaderWithBackButton from '@components/HeaderWithBackButton';
import ScreenWrapper from '@components/ScreenWrapper';
import TabSelector from '@components/TabSelector/TabSelector';

import useKeyboardState from '@hooks/useKeyboardState';
import useLocalize from '@hooks/useLocalize';
import useOnyx from '@hooks/useOnyx';
import usePolicyForTransaction from '@hooks/usePolicyForTransaction';
Expand Down Expand Up @@ -56,6 +57,7 @@ function DistanceRequestStartPage({
const [lastDistanceExpenseType] = useOnyx(ONYXKEYS.NVP_LAST_DISTANCE_EXPENSE_TYPE);
const isLoadingSelectedTab = isLoadingOnyxValue(selectedTabResult);
const isTrackDistanceExpense = iouType === CONST.IOU.TYPE.TRACK;
const {isKeyboardShown} = useKeyboardState();

const tabTitles = {
[CONST.IOU.TYPE.REQUEST]: translate('iou.trackDistance'),
Expand Down Expand Up @@ -112,7 +114,7 @@ function DistanceRequestStartPage({
accessVariants={[CONST.IOU.ACCESS_VARIANTS.CREATE]}
>
<ScreenWrapper
shouldEnableKeyboardAvoidingView={selectedTab === CONST.TAB_REQUEST.DISTANCE_ODOMETER}
shouldEnableKeyboardAvoidingView={selectedTab === CONST.TAB_REQUEST.DISTANCE_ODOMETER || isKeyboardShown}
shouldEnableMinHeight={canUseTouchScreen()}
testID="DistanceRequestStartPage"
focusTrapSettings={{containerElements: focusTrapContainerElements}}
Expand All @@ -135,6 +137,7 @@ function DistanceRequestStartPage({
onTabBarFocusTrapContainerElementChanged={setTabBarContainerElement}
onActiveTabFocusTrapContainerElementChanged={setActiveTabContainerElement}
lazyLoadEnabled
shouldDismissKeyboardBeforeTabSwitch
>
<TopTab.Screen name={CONST.TAB_REQUEST.DISTANCE_MAP}>
{() => (
Expand Down
Loading