diff --git a/src/components/DatePicker/index.tsx b/src/components/DatePicker/index.tsx index f91c0816c12f..95f533ede2d6 100644 --- a/src/components/DatePicker/index.tsx +++ b/src/components/DatePicker/index.tsx @@ -3,6 +3,7 @@ import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; import {InteractionManager, View} from 'react-native'; import TextInput from '@components/TextInput'; import type {BaseTextInputRef} from '@components/TextInput/BaseTextInput/types'; +import useAutoFocusInput from '@hooks/useAutoFocusInput'; import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -41,10 +42,13 @@ function DatePicker({ const [isModalVisible, setIsModalVisible] = useState(false); const [selectedDate, setSelectedDate] = useState(() => value ?? defaultValue ?? ''); const [popoverPosition, setPopoverPosition] = useState({horizontal: 0, vertical: 0}); - const textInputRef = useRef(null); + const textInputRef = useRef(null); const anchorRef = useRef(null); const [isInverted, setIsInverted] = useState(false); - const isAutoFocused = useRef(false); + + const {inputCallbackRef: autoFocusCallbackRef} = useAutoFocusInput(); + const autoFocusCallbackRefRef = useRef(autoFocusCallbackRef); + autoFocusCallbackRefRef.current = autoFocusCallbackRef; useEffect(() => { if (shouldSaveDraft && formID) { @@ -103,16 +107,20 @@ function DatePicker({ }); }, [calculatePopoverPosition, windowWidth]); - useEffect(() => { - if (!autoFocus || isAutoFocused.current) { - return; - } - isAutoFocused.current = true; - // eslint-disable-next-line @typescript-eslint/no-deprecated - InteractionManager.runAfterInteractions(() => { - textInputRef.current?.focus(); - }); - }, [autoFocus]); + // Combined ref: updates textInputRef (needed for blur() in showDatePickerModal) and connects + // autoFocusCallbackRef only when autoFocus=true so useAutoFocusInput's useFocusEffect cleanup + // can cancel any pending focus task when the screen starts closing. + const combinedTextInputRef = useCallback( + (ref: BaseTextInputRef | null) => { + textInputRef.current = ref; + if (autoFocus) { + (autoFocusCallbackRefRef.current as unknown as (ref: BaseTextInputRef | null) => void)(ref); + } + }, + // autoFocusCallbackRefRef is a stable ref — its identity never changes, so it's not a dep + // eslint-disable-next-line react-hooks/exhaustive-deps + [autoFocus], + ); const getValidDateForCalendar = useMemo(() => { if (!selectedDate) { @@ -129,7 +137,7 @@ function DatePicker({ style={styles.mv2} > ({ const theme = useTheme(); const styles = useThemeStyles(); const {translate} = useLocalize(); - const {didScreenTransitionEnd} = useScreenWrapperTransitionStatus(); - const splitItem = item as unknown as SplitListItemType; const formattedOriginalAmount = convertToDisplayStringWithoutCurrency(splitItem.originalAmount, splitItem.currency); @@ -55,7 +53,7 @@ function SplitListItem({ [splitItem], ); - const inputRef = useRef(null); + const {inputCallbackRef: autoFocusCallbackRef} = useAutoFocusInput(); // Animated highlight style for selected item const animatedHighlightStyle = useAnimatedHighlightStyle({ @@ -75,23 +73,14 @@ function SplitListItem({ onInputFocus?.(item); }, [onInputFocus, item]); - // Auto-focus input when item is selected and screen transition ends - useEffect(() => { - if (!didScreenTransitionEnd || !splitItem.isSelected || !splitItem.isEditable || !inputRef.current) { + // Only connect the auto-focus ref to the selected item so useAutoFocusInput's useFocusEffect + // cleanup can cancel any pending focus task when the screen starts closing, preventing + // the focused input from interfering with the close animation. + const inputCallbackRef: (ref: BaseTextInputRef | null) => void = (ref) => { + if (!splitItem.isSelected || !splitItem.isEditable) { return; } - - // Use InteractionManager to ensure input focus happens after all animations/interactions complete. - // This prevents focus from interrupting modal close/open animations which would cause UI glitches - // and "jumping" behavior when quickly navigating between screens. - // eslint-disable-next-line @typescript-eslint/no-deprecated - InteractionManager.runAfterInteractions(() => { - inputRef.current?.focus(); - }); - }, [didScreenTransitionEnd, splitItem.isSelected, splitItem.isEditable]); - - const inputCallbackRef = (ref: BaseTextInputRef | null) => { - inputRef.current = ref; + (autoFocusCallbackRef as unknown as (ref: BaseTextInputRef | null) => void)(ref); }; const isPercentageMode = splitItem.mode === CONST.TAB.SPLIT.PERCENTAGE; diff --git a/src/pages/iou/request/step/IOURequestEditReportCommon.tsx b/src/pages/iou/request/step/IOURequestEditReportCommon.tsx index da07d152f929..d8772eb1ac97 100644 --- a/src/pages/iou/request/step/IOURequestEditReportCommon.tsx +++ b/src/pages/iou/request/step/IOURequestEditReportCommon.tsx @@ -6,6 +6,8 @@ import {usePersonalDetails} from '@components/OnyxListItemProvider'; import SelectionList from '@components/SelectionList'; import InviteMemberListItem from '@components/SelectionList/ListItem/InviteMemberListItem'; import type {ListItem} from '@components/SelectionList/types'; +import type {BaseTextInputRef} from '@components/TextInput/BaseTextInput/types'; +import useAutoFocusInput from '@hooks/useAutoFocusInput'; import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; import useDebouncedState from '@hooks/useDebouncedState'; import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; @@ -67,6 +69,7 @@ function IOURequestEditReportCommon({ isTimeRequest = false, }: Props) { const icons = useMemoizedLazyExpensifyIcons(['Close', 'Document']); + const {inputCallbackRef} = useAutoFocusInput(); const {translate, localeCompare} = useLocalize(); const personalDetails = usePersonalDetails(); const [allPolicies] = useOnyx(ONYXKEYS.COLLECTION.POLICY); @@ -302,6 +305,8 @@ function IOURequestEditReportCommon({ label: translate('common.search'), headerMessage, onChangeText: setSearchValue, + disableAutoFocus: true, + ref: inputCallbackRef as (ref: BaseTextInputRef | null) => void, }} shouldSingleExecuteRowSelect initiallyFocusedItemKey={selectedReportID} diff --git a/tests/ui/IOURequestEditReportCommonTest.tsx b/tests/ui/IOURequestEditReportCommonTest.tsx index 7ba3322f00b0..22d2b1e746e2 100644 --- a/tests/ui/IOURequestEditReportCommonTest.tsx +++ b/tests/ui/IOURequestEditReportCommonTest.tsx @@ -1,3 +1,4 @@ +import {NavigationContainer} from '@react-navigation/native'; import {act, render, screen} from '@testing-library/react-native'; import Onyx from 'react-native-onyx'; import ComposeProviders from '@components/ComposeProviders'; @@ -44,15 +45,17 @@ jest.mock('@components/OptionListContextProvider', () => ({ */ const renderIOURequestEditReportCommon = ({selectedReportID = '', selectedPolicyID}: {selectedReportID: string; selectedPolicyID?: string}) => render( - - - , + + + + + , ); describe('IOURequestEditReportCommon', () => {