Skip to content
Merged
15 changes: 8 additions & 7 deletions src/components/DestinationPicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@ import useDebouncedState from '@hooks/useDebouncedState';
import useLocalize from '@hooks/useLocalize';
import useOnyx from '@hooks/useOnyx';
import usePolicy from '@hooks/usePolicy';
import * as OptionsListUtils from '@libs/OptionsListUtils';
import * as PerDiemRequestUtils from '@libs/PerDiemRequestUtils';
import {getHeaderMessageForNonUserList} from '@libs/OptionsListUtils';
import {getDestinationListSections} from '@libs/PerDiemRequestUtils';
import type {Destination} from '@libs/PerDiemRequestUtils';
import * as PolicyUtils from '@libs/PolicyUtils';
import {getPerDiemCustomUnit} from '@libs/PolicyUtils';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import SelectionList from './SelectionList';
Expand All @@ -21,8 +21,8 @@ type DestinationPickerProps = {

function DestinationPicker({selectedDestination, policyID, onSubmit}: DestinationPickerProps) {
const policy = usePolicy(policyID);
const customUnit = PolicyUtils.getPerDiemCustomUnit(policy);
const [policyRecentlyUsedDestinations] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_RECENTLY_USED_DESTINATIONS}${policyID}`);
const customUnit = getPerDiemCustomUnit(policy);
const [policyRecentlyUsedDestinations] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_RECENTLY_USED_DESTINATIONS}${policyID}`, {canBeMissing: true});

const {translate} = useLocalize();
const [searchValue, debouncedSearchValue, setSearchValue] = useDebouncedState('');
Expand All @@ -49,15 +49,15 @@ function DestinationPicker({selectedDestination, policyID, onSubmit}: Destinatio
}, [customUnit?.rates, selectedDestination]);

const [sections, headerMessage, shouldShowTextInput] = useMemo(() => {
const destinationOptions = PerDiemRequestUtils.getDestinationListSections({
const destinationOptions = getDestinationListSections({
searchValue: debouncedSearchValue,
selectedOptions,
destinations: Object.values(customUnit?.rates ?? {}),
recentlyUsedDestinations: policyRecentlyUsedDestinations,
});

const destinationData = destinationOptions?.at(0)?.data ?? [];
const header = OptionsListUtils.getHeaderMessageForNonUserList(destinationData.length > 0, debouncedSearchValue);
const header = getHeaderMessageForNonUserList(destinationData.length > 0, debouncedSearchValue);
const destinationsCount = Object.values(customUnit?.rates ?? {}).length;
const isDestinationsCountBelowThreshold = destinationsCount < CONST.STANDARD_LIST_ITEM_LIMIT;
const showInput = !isDestinationsCountBelowThreshold;
Expand All @@ -81,6 +81,7 @@ function DestinationPicker({selectedDestination, policyID, onSubmit}: Destinatio
ListItem={RadioListItem}
initiallyFocusedOptionKey={selectedOptionKey ?? undefined}
isRowMultilineSupported
shouldHideKeyboardOnScroll={false}
/>
);
}
Expand Down
9 changes: 7 additions & 2 deletions src/components/SelectionList/index.native.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,18 @@ import {Keyboard} from 'react-native';
import BaseSelectionList from './BaseSelectionList';
import type {ListItem, SelectionListHandle, SelectionListProps} from './types';

function SelectionList<TItem extends ListItem>(props: SelectionListProps<TItem>, ref: ForwardedRef<SelectionListHandle>) {
function SelectionList<TItem extends ListItem>({shouldHideKeyboardOnScroll = true, ...props}: SelectionListProps<TItem>, ref: ForwardedRef<SelectionListHandle>) {
return (
<BaseSelectionList
// eslint-disable-next-line react/jsx-props-no-spreading
{...props}
ref={ref}
onScrollBeginDrag={() => Keyboard.dismiss()}
onScrollBeginDrag={() => {
if (!shouldHideKeyboardOnScroll) {
return;
}
Keyboard.dismiss();
}}
/>
);
}
Expand Down
6 changes: 3 additions & 3 deletions src/components/SelectionList/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import CONST from '@src/CONST';
import BaseSelectionList from './BaseSelectionList';
import type {ListItem, SelectionListHandle, SelectionListProps} from './types';

function SelectionList<TItem extends ListItem>({onScroll, ...props}: SelectionListProps<TItem>, ref: ForwardedRef<SelectionListHandle>) {
function SelectionList<TItem extends ListItem>({onScroll, shouldHideKeyboardOnScroll = true, ...props}: SelectionListProps<TItem>, ref: ForwardedRef<SelectionListHandle>) {
const [isScreenTouched, setIsScreenTouched] = useState(false);

const touchStart = () => setIsScreenTouched(true);
Expand Down Expand Up @@ -58,8 +58,8 @@ function SelectionList<TItem extends ListItem>({onScroll, ...props}: SelectionLi

// In SearchPageBottomTab we use useAnimatedScrollHandler from reanimated(for performance reasons) and it returns object instead of function. In that case we cannot change it to a function call, that's why we have to choose between onScroll and defaultOnScroll.
const defaultOnScroll = () => {
// Only dismiss the keyboard whenever the user scrolls the screen
if (!isScreenTouched) {
// Only dismiss the keyboard whenever the user scrolls the screen or `shouldHideKeyboardOnScroll` is true
if (!isScreenTouched || !shouldHideKeyboardOnScroll) {
return;
}
Keyboard.dismiss();
Expand Down
3 changes: 3 additions & 0 deletions src/components/SelectionList/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -826,6 +826,9 @@ type SelectionListProps<TItem extends ListItem> = Partial<ChildrenProps> & {

/** Whether product training tooltips can be displayed */
canShowProductTrainingTooltip?: boolean;

/** Whether to hide the keyboard when scrolling a list */
shouldHideKeyboardOnScroll?: boolean;
} & TRightHandSideComponent<TItem>;

type SelectionListHandle = {
Expand Down
8 changes: 6 additions & 2 deletions src/pages/iou/request/IOURequestStartPage.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import {useFocusEffect} from '@react-navigation/native';
import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react';
import {View} from 'react-native';
import {Keyboard, View} from 'react-native';
import DragAndDropProvider from '@components/DragAndDrop/Provider';
import FocusTrapContainerElement from '@components/FocusTrap/FocusTrapContainerElement';
import HeaderWithBackButton from '@components/HeaderWithBackButton';
Expand All @@ -14,6 +14,7 @@ import usePolicy from '@hooks/usePolicy';
import usePrevious from '@hooks/usePrevious';
import useThemeStyles from '@hooks/useThemeStyles';
import {dismissProductTraining} from '@libs/actions/Welcome';
import {isMobile} from '@libs/Browser';
import {canUseTouchScreen} from '@libs/DeviceCapabilities';
import Navigation from '@libs/Navigation/Navigation';
import OnyxTabNavigator, {TabScreenWithFocusTrapWrapper, TopTab} from '@libs/Navigation/OnyxTabNavigator';
Expand Down Expand Up @@ -115,6 +116,7 @@ function IOURequestStartPage({

const resetIOUTypeIfChanged = useCallback(
(newIOUType: IOURequestType) => {
Keyboard.dismiss();
if (transaction?.iouRequestType === newIOUType) {
return;
}
Expand Down Expand Up @@ -175,6 +177,7 @@ function IOURequestStartPage({
>
<ScreenWrapper
shouldEnableKeyboardAvoidingView={false}
shouldEnableMaxHeight={selectedTab === CONST.TAB_REQUEST.PER_DIEM}
shouldEnableMinHeight={canUseTouchScreen()}
headerGapStyles={isDraggingOver ? styles.dropWrapper : []}
testID={IOURequestStartPage.displayName}
Expand Down Expand Up @@ -207,7 +210,8 @@ function IOURequestStartPage({
shouldShowProductTrainingTooltip={shouldShowProductTrainingTooltip}
renderProductTrainingTooltip={renderProductTrainingTooltip}
lazyLoadEnabled
disableSwipe={isMultiScanEnabled && selectedTab === CONST.TAB_REQUEST.SCAN}
// We're disabling swipe on mWeb fo the Per Diem tab because the keyboard will hang on the other tab after switching
disableSwipe={(isMultiScanEnabled && selectedTab === CONST.TAB_REQUEST.SCAN) || (selectedTab === CONST.TAB_REQUEST.PER_DIEM && isMobile())}
>
<TopTab.Screen name={CONST.TAB_REQUEST.MANUAL}>
{() => (
Expand Down
107 changes: 58 additions & 49 deletions src/pages/iou/request/step/IOURequestStepDestination.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,20 @@ import Button from '@components/Button';
import DestinationPicker from '@components/DestinationPicker';
import FixedFooter from '@components/FixedFooter';
import * as Illustrations from '@components/Icon/Illustrations';
import ScreenWrapper from '@components/ScreenWrapper';
import type {ListItem} from '@components/SelectionList/types';
import WorkspaceEmptyStateSection from '@components/WorkspaceEmptyStateSection';
import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails';
import useLocalize from '@hooks/useLocalize';
import useNetwork from '@hooks/useNetwork';
import useOnyx from '@hooks/useOnyx';
import useSafeAreaInsets from '@hooks/useSafeAreaInsets';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
import Navigation from '@libs/Navigation/Navigation';
import {getPerDiemCustomUnit, isPolicyAdmin} from '@libs/PolicyUtils';
import {getPolicyExpenseChat} from '@libs/ReportUtils';
import variables from '@styles/variables';
import {
clearSubrates,
getIOURequestPolicyID,
Expand Down Expand Up @@ -55,7 +58,7 @@ function IOURequestStepDestination({
const [policy, policyMetadata] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${explicitPolicyID ?? getIOURequestPolicyID(transaction, report)}`, {canBeMissing: false});
const {accountID} = useCurrentUserPersonalDetails();
const policyExpenseReport = policy?.id ? getPolicyExpenseChat(accountID, policy.id) : undefined;

const {top} = useSafeAreaInsets();
const customUnit = getPerDiemCustomUnit(policy);
const selectedDestination = transaction?.comment?.customUnit?.customUnitRateID;

Expand Down Expand Up @@ -112,56 +115,62 @@ function IOURequestStepDestination({
};

return (
<StepScreenWrapper
headerTitle={backTo ? translate('common.destination') : tabTitles[iouType]}
onBackButtonPress={navigateBack}
shouldShowWrapper={!openedFromStartPage}
shouldShowNotFoundPage={shouldShowNotFoundPage}
testID={IOURequestStepDestination.displayName}
<ScreenWrapper
includePaddingTop={false}
keyboardVerticalOffset={variables.contentHeaderHeight + top + variables.tabSelectorButtonHeight + variables.tabSelectorButtonPadding}
testID={`${IOURequestStepDestination.displayName}-container`}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Coming from this issue #72833, we’re wrapping the StepScreenWrapper with ScreenWrapper. However, since StepScreenWrapper has its own offline indicator and ScreenWrapper does too, the offline indicator was appearing twice. We’ve fixed that here: #72833 (comment)

>
{isLoading && (
<ActivityIndicator
size={CONST.ACTIVITY_INDICATOR_SIZE.LARGE}
style={[styles.flex1]}
color={theme.spinner}
/>
)}
{shouldShowOfflineView && <FullPageOfflineBlockingView>{null}</FullPageOfflineBlockingView>}
{shouldShowEmptyState && (
<View style={[styles.flex1]}>
<WorkspaceEmptyStateSection
shouldStyleAsCard={false}
icon={Illustrations.EmptyStateExpenses}
title={translate('workspace.perDiem.emptyList.title')}
subtitle={translate('workspace.perDiem.emptyList.subtitle')}
containerStyle={[styles.flex1, styles.justifyContentCenter]}
<StepScreenWrapper
headerTitle={backTo ? translate('common.destination') : tabTitles[iouType]}
onBackButtonPress={navigateBack}
shouldShowWrapper={!openedFromStartPage}
shouldShowNotFoundPage={shouldShowNotFoundPage}
testID={IOURequestStepDestination.displayName}
>
{isLoading && (
<ActivityIndicator
size={CONST.ACTIVITY_INDICATOR_SIZE.LARGE}
style={[styles.flex1]}
color={theme.spinner}
/>
)}
{shouldShowOfflineView && <FullPageOfflineBlockingView>{null}</FullPageOfflineBlockingView>}
{shouldShowEmptyState && (
<View style={[styles.flex1]}>
<WorkspaceEmptyStateSection
shouldStyleAsCard={false}
icon={Illustrations.EmptyStateExpenses}
title={translate('workspace.perDiem.emptyList.title')}
subtitle={translate('workspace.perDiem.emptyList.subtitle')}
containerStyle={[styles.flex1, styles.justifyContentCenter]}
/>
{isPolicyAdmin(policy) && !!policy?.areCategoriesEnabled && (
<FixedFooter style={[styles.mtAuto, styles.pt5]}>
<Button
large
success
style={[styles.w100]}
onPress={() => {
InteractionManager.runAfterInteractions(() => {
Navigation.navigate(ROUTES.WORKSPACE_PER_DIEM.getRoute(policy.id, Navigation.getActiveRoute()));
});
}}
text={translate('workspace.perDiem.editPerDiemRates')}
pressOnEnter
/>
</FixedFooter>
)}
</View>
)}
{!shouldShowEmptyState && !isLoading && !shouldShowOfflineView && !!policy?.id && (
<DestinationPicker
selectedDestination={selectedDestination}
policyID={policy.id}
onSubmit={updateDestination}
/>
{isPolicyAdmin(policy) && !!policy?.areCategoriesEnabled && (
<FixedFooter style={[styles.mtAuto, styles.pt5]}>
<Button
large
success
style={[styles.w100]}
onPress={() => {
InteractionManager.runAfterInteractions(() => {
Navigation.navigate(ROUTES.WORKSPACE_PER_DIEM.getRoute(policy.id, Navigation.getActiveRoute()));
});
}}
text={translate('workspace.perDiem.editPerDiemRates')}
pressOnEnter
/>
</FixedFooter>
)}
</View>
)}
{!shouldShowEmptyState && !isLoading && !shouldShowOfflineView && !!policy?.id && (
<DestinationPicker
selectedDestination={selectedDestination}
policyID={policy.id}
onSubmit={updateDestination}
/>
)}
</StepScreenWrapper>
)}
</StepScreenWrapper>
</ScreenWrapper>
);
}

Expand Down
Loading