diff --git a/src/components/MoneyReportHeaderActions/MoneyReportHeaderSecondaryActions.tsx b/src/components/MoneyReportHeaderActions/MoneyReportHeaderSecondaryActions.tsx index a40dfdd7d9c8..7297ab98cf59 100644 --- a/src/components/MoneyReportHeaderActions/MoneyReportHeaderSecondaryActions.tsx +++ b/src/components/MoneyReportHeaderActions/MoneyReportHeaderSecondaryActions.tsx @@ -2,8 +2,7 @@ import {delegateEmailSelector, isUserValidatedSelector} from '@selectors/Account import {hasSeenTourSelector} from '@selectors/Onboarding'; import truncate from 'lodash/truncate'; import React, {useContext, useEffect} from 'react'; -// eslint-disable-next-line no-restricted-imports -import {InteractionManager, View} from 'react-native'; +import {View} from 'react-native'; import type {ValueOf} from 'type-fest'; import Button from '@components/Button'; import type {ButtonWithDropdownMenuRef} from '@components/ButtonWithDropdownMenu/types'; @@ -45,6 +44,7 @@ import {search} from '@libs/actions/Search'; import getNonEmptyStringOnyxID from '@libs/getNonEmptyStringOnyxID'; import getPlatform from '@libs/getPlatform'; import {getTotalAmountForIOUReportPreviewButton} from '@libs/MoneyRequestReportUtils'; +import TransitionTracker from '@libs/Navigation/TransitionTracker'; import type {KYCFlowEvent, TriggerKYCFlow, WorkspacePolicyPaymentOption} from '@libs/PaymentUtils'; import {selectPaymentType} from '@libs/PaymentUtils'; import {sortPoliciesByName} from '@libs/PolicyUtils'; @@ -156,8 +156,13 @@ function MoneyReportHeaderSecondaryActionsInner({reportID, primaryAction, isRepo onConfirm: () => startAnimation(), }; if (getPlatform() === CONST.PLATFORM.IOS) { - // InteractionManager delays modal until current interaction completes, preventing visual glitches on iOS - InteractionManager.runAfterInteractions(() => openHoldMenu(holdMenuParams)); + // TransitionTracker.runAfterTransitions delays modal until current interaction completes, preventing visual glitches on iOS + TransitionTracker.runAfterTransitions({ + callback: () => { + openHoldMenu(holdMenuParams); + }, + waitForUpcomingTransition: true, + }); } else { openHoldMenu(holdMenuParams); } diff --git a/src/components/MoneyReportHeaderModals.tsx b/src/components/MoneyReportHeaderModals.tsx index 95c23aa35315..3834367920ad 100644 --- a/src/components/MoneyReportHeaderModals.tsx +++ b/src/components/MoneyReportHeaderModals.tsx @@ -1,7 +1,5 @@ import React, {useRef, useState} from 'react'; import type {ReactNode} from 'react'; -// eslint-disable-next-line no-restricted-imports -import {InteractionManager} from 'react-native'; import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; import useDecisionModal from '@hooks/useDecisionModal'; import useHoldMenuModal from '@hooks/useHoldMenuModal'; @@ -87,10 +85,10 @@ function MoneyReportHeaderModals({reportID, children}: MoneyReportHeaderModalsPr onConfirm, }); - // On iOS, delay opening the hold menu until active touch interactions finish to prevent visual glitches + // On iOS, defer by one frame so the current touch animation finishes before the modal opens if (getPlatform() === CONST.PLATFORM.IOS) { return new Promise((resolve) => { - InteractionManager.runAfterInteractions(() => { + requestAnimationFrame(() => { open().then(() => resolve()); }); }); diff --git a/src/components/OptionRow.tsx b/src/components/OptionRow.tsx index 9c3412953d91..e64a266f12c1 100644 --- a/src/components/OptionRow.tsx +++ b/src/components/OptionRow.tsx @@ -1,8 +1,7 @@ import {deepEqual} from 'fast-equals'; import React, {useEffect, useRef, useState} from 'react'; import type {StyleProp, TextStyle, ViewStyle} from 'react-native'; -// eslint-disable-next-line no-restricted-imports -import {InteractionManager, StyleSheet, View} from 'react-native'; +import {StyleSheet, View} from 'react-native'; import {useCurrencyListActions} from '@hooks/useCurrencyList'; import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; @@ -35,8 +34,8 @@ type OptionRowProps = { /** Whether this option is currently in focus so we can modify its style */ optionIsFocused?: boolean; - /** A function that is called when an option is selected. Selected option is passed as a param */ - onSelectRow?: (option: OptionDataWithOptionalReportID, refElement: View | HTMLDivElement | null) => void | Promise; + /** A function that is called when an option is selected */ + onSelectRow?: () => void; /** Whether this item is selected */ isSelected?: boolean; @@ -171,17 +170,8 @@ function OptionRow({ } setIsDisabled(true); - if (e) { - e.preventDefault(); - } - let result = onSelectRow(option, pressableRef.current); - if (!(result instanceof Promise)) { - result = Promise.resolve(); - } - - InteractionManager.runAfterInteractions(() => { - result?.finally(() => setIsDisabled(isOptionDisabled)); - }); + e?.preventDefault(); + onSelectRow(); }} disabled={isDisabled} style={[ diff --git a/src/pages/iou/request/step/IOURequestStepSubrate.tsx b/src/pages/iou/request/step/IOURequestStepSubrate.tsx index fdc5c5278fa8..db339dbf3b2a 100644 --- a/src/pages/iou/request/step/IOURequestStepSubrate.tsx +++ b/src/pages/iou/request/step/IOURequestStepSubrate.tsx @@ -1,7 +1,6 @@ import {useNavigation} from '@react-navigation/native'; import React, {useCallback, useEffect, useRef, useState} from 'react'; -// eslint-disable-next-line no-restricted-imports -import {InteractionManager, View} from 'react-native'; +import {View} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView'; import FormProvider from '@components/Form/FormProvider'; @@ -21,6 +20,7 @@ import usePolicyForTransaction from '@hooks/usePolicyForTransaction'; import useThemeStyles from '@hooks/useThemeStyles'; import {addErrorMessage} from '@libs/ErrorUtils'; import Navigation from '@libs/Navigation/Navigation'; +import TransitionTracker from '@libs/Navigation/TransitionTracker'; import {getPerDiemCustomUnit} from '@libs/PolicyUtils'; import {getIOURequestPolicyID} from '@userActions/IOU/MoneyRequest'; import {addSubrate, removeSubrate, updateSubrate} from '@userActions/IOU/PerDiem'; @@ -239,8 +239,17 @@ function IOURequestStepSubrate({ items={validOptions} onValueChange={(value) => { setSubrateValue(value as string); - InteractionManager.runAfterInteractions(() => { - textInputRef.current?.focus(); + + // Focus the Quantity input after the ValuePicker modal closes. + // TransitionTracker's callback fires synchronously inside Reanimated's animation + // callback (outside React's event handler), so React flushes state updates async + // via MessageChannel. requestIdleCallback ensures focus() runs after React commits + // and the modal's FocusTrap (web) deactivates, preventing focus from being stolen. + TransitionTracker.runAfterTransitions({ + callback: () => { + requestIdleCallback(() => textInputRef.current?.focus()); + }, + waitForUpcomingTransition: true, }); }} /> diff --git a/src/pages/tasks/NewTaskPage.tsx b/src/pages/tasks/NewTaskPage.tsx index 5785d2e46cd7..d5e276296100 100644 --- a/src/pages/tasks/NewTaskPage.tsx +++ b/src/pages/tasks/NewTaskPage.tsx @@ -1,7 +1,5 @@ -import {useFocusEffect} from '@react-navigation/native'; import React, {useEffect, useRef, useState} from 'react'; -// eslint-disable-next-line no-restricted-imports -import {InteractionManager, View} from 'react-native'; +import {View} from 'react-native'; import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView'; import FormAlertWithSubmitButton from '@components/FormAlertWithSubmitButton'; import FormHelpMessage from '@components/FormHelpMessage'; @@ -18,7 +16,6 @@ import usePolicy from '@hooks/usePolicy'; import useReportAttributes from '@hooks/useReportAttributes'; import useSafeAreaPaddings from '@hooks/useSafeAreaPaddings'; import useThemeStyles from '@hooks/useThemeStyles'; -import blurActiveElement from '@libs/Accessibility/blurActiveElement'; import {createTaskAndNavigate, dismissModalAndClearOutTaskInfo, getAssignee, getShareDestination, setShareDestinationValue} from '@libs/actions/Task'; import Navigation from '@libs/Navigation/Navigation'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; @@ -67,15 +64,6 @@ function NewTaskPage({route}: NewTaskPageProps) { const backTo = route.params?.backTo; const confirmButtonRef = useRef(null); - const focusTimeoutRef = useRef(null); - useFocusEffect(() => { - focusTimeoutRef.current = setTimeout(() => { - InteractionManager.runAfterInteractions(() => { - blurActiveElement(); - }); - }, CONST.ANIMATED_TRANSITION); - return () => focusTimeoutRef.current && clearTimeout(focusTimeoutRef.current); - }); useEffect(() => { if (!task?.parentReportID) { @@ -137,6 +125,8 @@ function NewTaskPage({route}: NewTaskPageProps) { onBackButtonPress={() => { Navigation.goBack(ROUTES.NEW_TASK_DETAILS.getRoute(backTo)); }} + /** Skip focus of the first interactive element in the header to make sure that Enter key confirms the task instead of navigating back. */ + shouldSkipFocusAfterTransition /> {!!hasDestinationError && ( { - requestAnimationFrame(() => { - addPolicyReport(policyReport); - Navigation.dismissModalWithReport({reportID: policyReport.reportID}); - }); - }); + addPolicyReport(policyReport); + Navigation.dismissModalWithReport({reportID: policyReport.reportID}); }; useEffect(() => {