From 8250f0590e2e0de7fa53527676f756d7c2cd0eb6 Mon Sep 17 00:00:00 2001 From: Huu Le <20178761+huult@users.noreply.github.com> Date: Tue, 3 Dec 2024 14:13:48 +0700 Subject: [PATCH 01/10] prevent submit button from jumping when proceeding to the confirmation page --- .../step/IOURequestStepParticipants.tsx | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/src/pages/iou/request/step/IOURequestStepParticipants.tsx b/src/pages/iou/request/step/IOURequestStepParticipants.tsx index fb2484ea414f..4552e94b3b5d 100644 --- a/src/pages/iou/request/step/IOURequestStepParticipants.tsx +++ b/src/pages/iou/request/step/IOURequestStepParticipants.tsx @@ -18,6 +18,7 @@ import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type SCREENS from '@src/SCREENS'; import type {Participant} from '@src/types/onyx/IOU'; +import KeyboardUtils from '@src/utils/keyboard'; import StepScreenWrapper from './StepScreenWrapper'; import type {WithFullTransactionOrNotFoundProps} from './withFullTransactionOrNotFound'; import withFullTransactionOrNotFound from './withFullTransactionOrNotFound'; @@ -131,11 +132,14 @@ function IOURequestStepParticipants({ transactionID, selectedReportID.current || reportID, ); - if (isCategorizing) { - Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_CATEGORY.getRoute(action, iouType, transactionID, selectedReportID.current || reportID, iouConfirmationPageRoute)); - } else { - Navigation.navigate(iouConfirmationPageRoute); - } + + const route = isCategorizing + ? ROUTES.MONEY_REQUEST_STEP_CATEGORY.getRoute(action, iouType, transactionID, selectedReportID.current || reportID, iouConfirmationPageRoute) + : iouConfirmationPageRoute; + + KeyboardUtils.dismiss().then(() => { + Navigation.navigate(route); + }); }, [iouType, transactionID, transaction, reportID, action, participants]); const navigateBack = useCallback(() => { @@ -153,7 +157,9 @@ function IOURequestStepParticipants({ IOU.setCustomUnitRateID(transactionID, rateID); IOU.setMoneyRequestParticipantsFromReport(transactionID, ReportUtils.getReport(selfDMReportID)); const iouConfirmationPageRoute = ROUTES.MONEY_REQUEST_STEP_CONFIRMATION.getRoute(action, CONST.IOU.TYPE.TRACK, transactionID, selfDMReportID); - Navigation.navigate(iouConfirmationPageRoute); + KeyboardUtils.dismiss().then(() => { + Navigation.navigate(iouConfirmationPageRoute); + }); }; useEffect(() => { From c0d30f792930f1757d933f7370a618876b0f1335 Mon Sep 17 00:00:00 2001 From: Huu Le <20178761+huult@users.noreply.github.com> Date: Wed, 4 Dec 2024 16:04:24 +0700 Subject: [PATCH 02/10] Update dismiss keyboard for web --- src/utils/keyboard.ts | 83 +++++++++++++++++++++++++++++++++++++------ 1 file changed, 72 insertions(+), 11 deletions(-) diff --git a/src/utils/keyboard.ts b/src/utils/keyboard.ts index a2b1d329aa0a..6cea7dc727cf 100644 --- a/src/utils/keyboard.ts +++ b/src/utils/keyboard.ts @@ -1,25 +1,82 @@ -import {Keyboard} from 'react-native'; +import {InteractionManager, Keyboard} from 'react-native'; +import getPlatform from '@libs/getPlatform'; +import CONST from '@src/CONST'; -let isVisible = false; +let isNativeKeyboardVisible = false; // Native keyboard visibility +let isWebKeyboardOpen = false; // Web keyboard visibility +const isWeb = getPlatform() === CONST.PLATFORM.WEB; +/** + * Initializes native keyboard visibility listeners + */ +const initializeNativeKeyboardListeners = () => { + Keyboard.addListener('keyboardDidHide', () => { + isNativeKeyboardVisible = false; + }); + + Keyboard.addListener('keyboardDidShow', () => { + isNativeKeyboardVisible = true; + }); +}; + +/** + * Checks if the given HTML element is a keyboard-related input + */ +const isKeyboardInput = (elem: HTMLElement): boolean => + (elem.tagName === 'INPUT' && !['button', 'submit', 'checkbox', 'file', 'image'].includes((elem as HTMLInputElement).type)) || elem.hasAttribute('contenteditable'); -Keyboard.addListener('keyboardDidHide', () => { - isVisible = false; -}); +/** + * Initializes web-specific keyboard visibility listeners + */ +const initializeWebKeyboardListeners = () => { + if (typeof document === 'undefined' || !isWeb) { + return; + } + + const handleFocusIn = (e: FocusEvent) => { + const target = e.target as HTMLElement; + if (target && isKeyboardInput(target)) { + isWebKeyboardOpen = true; + } + }; + + const handleFocusOut = (e: FocusEvent) => { + const target = e.target as HTMLElement; + if (target && isKeyboardInput(target)) { + isWebKeyboardOpen = false; + } + }; -Keyboard.addListener('keyboardDidShow', () => { - isVisible = true; -}); + document.addEventListener('focusin', handleFocusIn); + document.addEventListener('focusout', handleFocusOut); +}; +/** + * Dismisses the keyboard and resolves the promise when the dismissal is complete + */ const dismiss = (): Promise => { return new Promise((resolve) => { - if (!isVisible) { - resolve(); + if (isWeb) { + if (!isWebKeyboardOpen) { + resolve(); + return; + } + + Keyboard.dismiss(); + InteractionManager.runAfterInteractions(() => { + isWebKeyboardOpen = false; + resolve(); + }); return; } + if (!isNativeKeyboardVisible) { + resolve(); + return; + } + const subscription = Keyboard.addListener('keyboardDidHide', () => { - resolve(undefined); + resolve(); subscription.remove(); }); @@ -27,6 +84,10 @@ const dismiss = (): Promise => { }); }; +// Initialize listeners for native and web +initializeNativeKeyboardListeners(); +initializeWebKeyboardListeners(); + const utils = {dismiss}; export default utils; From ff74004bb4fec9c63b3cadf343da23746b45dcb4 Mon Sep 17 00:00:00 2001 From: Huu Le <20178761+huult@users.noreply.github.com> Date: Thu, 5 Dec 2024 09:08:02 +0700 Subject: [PATCH 03/10] Add keyboard dismiss for web --- src/utils/keyboard.ts | 93 ----------------------------- src/utils/keyboard/index.ts | 32 ++++++++++ src/utils/keyboard/index.website.ts | 46 ++++++++++++++ 3 files changed, 78 insertions(+), 93 deletions(-) delete mode 100644 src/utils/keyboard.ts create mode 100644 src/utils/keyboard/index.ts create mode 100644 src/utils/keyboard/index.website.ts diff --git a/src/utils/keyboard.ts b/src/utils/keyboard.ts deleted file mode 100644 index 6cea7dc727cf..000000000000 --- a/src/utils/keyboard.ts +++ /dev/null @@ -1,93 +0,0 @@ -import {InteractionManager, Keyboard} from 'react-native'; -import getPlatform from '@libs/getPlatform'; -import CONST from '@src/CONST'; - -let isNativeKeyboardVisible = false; // Native keyboard visibility -let isWebKeyboardOpen = false; // Web keyboard visibility -const isWeb = getPlatform() === CONST.PLATFORM.WEB; -/** - * Initializes native keyboard visibility listeners - */ -const initializeNativeKeyboardListeners = () => { - Keyboard.addListener('keyboardDidHide', () => { - isNativeKeyboardVisible = false; - }); - - Keyboard.addListener('keyboardDidShow', () => { - isNativeKeyboardVisible = true; - }); -}; - -/** - * Checks if the given HTML element is a keyboard-related input - */ -const isKeyboardInput = (elem: HTMLElement): boolean => - (elem.tagName === 'INPUT' && !['button', 'submit', 'checkbox', 'file', 'image'].includes((elem as HTMLInputElement).type)) || elem.hasAttribute('contenteditable'); - -/** - * Initializes web-specific keyboard visibility listeners - */ -const initializeWebKeyboardListeners = () => { - if (typeof document === 'undefined' || !isWeb) { - return; - } - - const handleFocusIn = (e: FocusEvent) => { - const target = e.target as HTMLElement; - if (target && isKeyboardInput(target)) { - isWebKeyboardOpen = true; - } - }; - - const handleFocusOut = (e: FocusEvent) => { - const target = e.target as HTMLElement; - if (target && isKeyboardInput(target)) { - isWebKeyboardOpen = false; - } - }; - - document.addEventListener('focusin', handleFocusIn); - document.addEventListener('focusout', handleFocusOut); -}; - -/** - * Dismisses the keyboard and resolves the promise when the dismissal is complete - */ -const dismiss = (): Promise => { - return new Promise((resolve) => { - if (isWeb) { - if (!isWebKeyboardOpen) { - resolve(); - return; - } - - Keyboard.dismiss(); - InteractionManager.runAfterInteractions(() => { - isWebKeyboardOpen = false; - resolve(); - }); - - return; - } - - if (!isNativeKeyboardVisible) { - resolve(); - return; - } - - const subscription = Keyboard.addListener('keyboardDidHide', () => { - resolve(); - subscription.remove(); - }); - - Keyboard.dismiss(); - }); -}; - -// Initialize listeners for native and web -initializeNativeKeyboardListeners(); -initializeWebKeyboardListeners(); - -const utils = {dismiss}; - -export default utils; diff --git a/src/utils/keyboard/index.ts b/src/utils/keyboard/index.ts new file mode 100644 index 000000000000..a2b1d329aa0a --- /dev/null +++ b/src/utils/keyboard/index.ts @@ -0,0 +1,32 @@ +import {Keyboard} from 'react-native'; + +let isVisible = false; + +Keyboard.addListener('keyboardDidHide', () => { + isVisible = false; +}); + +Keyboard.addListener('keyboardDidShow', () => { + isVisible = true; +}); + +const dismiss = (): Promise => { + return new Promise((resolve) => { + if (!isVisible) { + resolve(); + + return; + } + + const subscription = Keyboard.addListener('keyboardDidHide', () => { + resolve(undefined); + subscription.remove(); + }); + + Keyboard.dismiss(); + }); +}; + +const utils = {dismiss}; + +export default utils; diff --git a/src/utils/keyboard/index.website.ts b/src/utils/keyboard/index.website.ts new file mode 100644 index 000000000000..de7e89b9d660 --- /dev/null +++ b/src/utils/keyboard/index.website.ts @@ -0,0 +1,46 @@ +import {InteractionManager, Keyboard} from 'react-native'; + +let isVisible = false; + +const isKeyboardInput = (elem: HTMLElement): boolean => { + const inputTypesToIgnore = ['button', 'submit', 'checkbox', 'file', 'image']; + return (elem.tagName === 'INPUT' && !inputTypesToIgnore.includes((elem as HTMLInputElement).type)) || elem.tagName === 'TEXTAREA' || elem.hasAttribute('contenteditable'); +}; + +const handleFocusIn = (event: FocusEvent): void => { + const target = event.target as HTMLElement; + if (target && isKeyboardInput(target)) { + isVisible = true; + } +}; + +const handleFocusOut = (event: FocusEvent): void => { + const target = event.target as HTMLElement; + if (target && isKeyboardInput(target)) { + isVisible = false; + } +}; + +document.addEventListener('focusin', handleFocusIn); +document.addEventListener('focusout', handleFocusOut); + +const dismiss = (): Promise => { + return new Promise((resolve) => { + if (!isVisible) { + resolve(); + return; + } + + Keyboard.dismiss(); + InteractionManager.runAfterInteractions(() => { + isVisible = false; + resolve(); + }); + }); +}; + +const utils = { + dismiss, +}; + +export default utils; From c01e82cd6e016fa234afcddf4aa4dd36bea40710 Mon Sep 17 00:00:00 2001 From: Huu Le <20178761+huult@users.noreply.github.com> Date: Mon, 9 Dec 2024 10:08:54 +0700 Subject: [PATCH 04/10] Specify dismiss keyboard behavior for Android native and Safari on mobile web --- .../step/IOURequestStepParticipants.tsx | 29 ++++++++++++++----- 1 file changed, 22 insertions(+), 7 deletions(-) diff --git a/src/pages/iou/request/step/IOURequestStepParticipants.tsx b/src/pages/iou/request/step/IOURequestStepParticipants.tsx index 4552e94b3b5d..c0f90dea0afd 100644 --- a/src/pages/iou/request/step/IOURequestStepParticipants.tsx +++ b/src/pages/iou/request/step/IOURequestStepParticipants.tsx @@ -5,7 +5,9 @@ import FormHelpMessage from '@components/FormHelpMessage'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import {READ_COMMANDS} from '@libs/API/types'; +import * as Browser from '@libs/Browser'; import DistanceRequestUtils from '@libs/DistanceRequestUtils'; +import getPlatform from '@libs/getPlatform'; import HttpUtils from '@libs/HttpUtils'; import * as IOUUtils from '@libs/IOUUtils'; import Navigation from '@libs/Navigation/Navigation'; @@ -15,6 +17,7 @@ import MoneyRequestParticipantsSelector from '@pages/iou/request/MoneyRequestPar import * as IOU from '@userActions/IOU'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; +import type {Route} from '@src/ROUTES'; import ROUTES from '@src/ROUTES'; import type SCREENS from '@src/SCREENS'; import type {Participant} from '@src/types/onyx/IOU'; @@ -70,6 +73,8 @@ function IOURequestStepParticipants({ const receiptFilename = transaction?.filename; const receiptPath = transaction?.receipt?.source; const receiptType = transaction?.receipt?.type; + const isAndroidNative = getPlatform() === CONST.PLATFORM.ANDROID; + const isMobileSafari = Browser.isMobileSafari(); // When the component mounts, if there is a receipt, see if the image can be read from the disk. If not, redirect the user to the starting step of the flow. // This is because until the expense is saved, the receipt file is only stored in the browsers memory as a blob:// and if the browser is refreshed, then @@ -107,6 +112,19 @@ function IOURequestStepParticipants({ [iouType, reportID, transactionID], ); + const handleNavigation = useCallback( + (route: Route) => { + if (isAndroidNative || isMobileSafari) { + KeyboardUtils.dismiss().then(() => { + Navigation.navigate(route); + }); + } else { + Navigation.navigate(route); + } + }, + [isAndroidNative, isMobileSafari], + ); + const goToNextStep = useCallback(() => { const isCategorizing = action === CONST.IOU.ACTION.CATEGORIZE; const isShareAction = action === CONST.IOU.ACTION.SHARE; @@ -137,10 +155,8 @@ function IOURequestStepParticipants({ ? ROUTES.MONEY_REQUEST_STEP_CATEGORY.getRoute(action, iouType, transactionID, selectedReportID.current || reportID, iouConfirmationPageRoute) : iouConfirmationPageRoute; - KeyboardUtils.dismiss().then(() => { - Navigation.navigate(route); - }); - }, [iouType, transactionID, transaction, reportID, action, participants]); + handleNavigation(route); + }, [action, participants, iouType, transaction, transactionID, reportID, handleNavigation]); const navigateBack = useCallback(() => { IOUUtils.navigateToStartMoneyRequestStep(iouRequestType, iouType, transactionID, reportID, action); @@ -157,9 +173,8 @@ function IOURequestStepParticipants({ IOU.setCustomUnitRateID(transactionID, rateID); IOU.setMoneyRequestParticipantsFromReport(transactionID, ReportUtils.getReport(selfDMReportID)); const iouConfirmationPageRoute = ROUTES.MONEY_REQUEST_STEP_CONFIRMATION.getRoute(action, CONST.IOU.TYPE.TRACK, transactionID, selfDMReportID); - KeyboardUtils.dismiss().then(() => { - Navigation.navigate(iouConfirmationPageRoute); - }); + + handleNavigation(iouConfirmationPageRoute); }; useEffect(() => { From ff4fb89d09308e54a4fea4b6725011e570365066 Mon Sep 17 00:00:00 2001 From: Huu Le <20178761+huult@users.noreply.github.com> Date: Sat, 14 Dec 2024 17:21:31 +0700 Subject: [PATCH 05/10] add listen dimention changes --- src/CONST.ts | 2 + src/components/ScreenWrapper.tsx | 9 +++- src/hooks/useReadyWithDimensions.ts | 31 +++++++++++++ .../step/IOURequestStepConfirmation.tsx | 1 + src/utils/{keyboard/index.ts => keyboard.ts} | 0 src/utils/keyboard/index.website.ts | 46 ------------------- 6 files changed, 42 insertions(+), 47 deletions(-) create mode 100644 src/hooks/useReadyWithDimensions.ts rename src/utils/{keyboard/index.ts => keyboard.ts} (100%) delete mode 100644 src/utils/keyboard/index.website.ts diff --git a/src/CONST.ts b/src/CONST.ts index 204ccaccf394..5e7aa0abc6eb 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -319,6 +319,8 @@ const CONST = { NON_BILLABLE: 'nonBillable', }, + DIMENSIONS_CHANGED_DELAY: 1000, + // Note: Group and Self-DM excluded as these are not tied to a Workspace WORKSPACE_ROOM_TYPES: [chatTypes.POLICY_ADMINS, chatTypes.POLICY_ANNOUNCE, chatTypes.DOMAIN_ALL, chatTypes.POLICY_ROOM, chatTypes.POLICY_EXPENSE_CHAT, chatTypes.INVOICE], ANDROID_PACKAGE_NAME, diff --git a/src/components/ScreenWrapper.tsx b/src/components/ScreenWrapper.tsx index 09315bfb8a8e..500aa505228c 100644 --- a/src/components/ScreenWrapper.tsx +++ b/src/components/ScreenWrapper.tsx @@ -9,6 +9,7 @@ import useEnvironment from '@hooks/useEnvironment'; import useInitialDimensions from '@hooks/useInitialWindowDimensions'; import useKeyboardState from '@hooks/useKeyboardState'; import useNetwork from '@hooks/useNetwork'; +import useReadyWithDimensions from '@hooks/useReadyWithDimensions'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useStyledSafeAreaInsets from '@hooks/useStyledSafeAreaInsets'; import useTackInputFocus from '@hooks/useTackInputFocus'; @@ -104,6 +105,8 @@ type ScreenWrapperProps = { /** Overrides the focus trap default settings */ focusTrapSettings?: FocusTrapForScreenProps['focusTrapSettings']; + + shouldListenToDimensionChanges?: boolean; }; type ScreenWrapperStatusContextType = { @@ -136,6 +139,7 @@ function ScreenWrapper( shouldShowOfflineIndicatorInWideScreen = false, shouldUseCachedViewportHeight = false, focusTrapSettings, + shouldListenToDimensionChanges = false, }: ScreenWrapperProps, ref: ForwardedRef, ) { @@ -161,8 +165,11 @@ function ScreenWrapper( const keyboardState = useKeyboardState(); const {isDevelopment} = useEnvironment(); const {isOffline} = useNetwork(); + + const {isReady} = useReadyWithDimensions(Browser.isMobileSafari() && shouldListenToDimensionChanges); const [didScreenTransitionEnd, setDidScreenTransitionEnd] = useState(false); - const maxHeight = shouldEnableMaxHeight ? windowHeight : undefined; + + const maxHeight = shouldEnableMaxHeight && isReady ? windowHeight : undefined; const minHeight = shouldEnableMinHeight && !Browser.isSafari() ? initialHeight : undefined; const isKeyboardShown = keyboardState?.isKeyboardShown ?? false; diff --git a/src/hooks/useReadyWithDimensions.ts b/src/hooks/useReadyWithDimensions.ts new file mode 100644 index 000000000000..897be8c558df --- /dev/null +++ b/src/hooks/useReadyWithDimensions.ts @@ -0,0 +1,31 @@ +import {useEffect, useState} from 'react'; +import {Dimensions} from 'react-native'; +import CONST from '@src/CONST'; + +const useReadyWithDimensions = (isEnabled = true) => { + const [isReady, setIsReady] = useState(false); + + useEffect(() => { + if (!isEnabled) { + return; + } + const timer = setTimeout(() => { + setIsReady(true); + }, CONST.DIMENSIONS_CHANGED_DELAY); + + const handleDimensionChange = () => { + setIsReady(true); + }; + + const subscription = Dimensions.addEventListener('change', handleDimensionChange); + + return () => { + clearTimeout(timer); + subscription?.remove(); + }; + }, [isEnabled]); + + return {isReady}; +}; + +export default useReadyWithDimensions; diff --git a/src/pages/iou/request/step/IOURequestStepConfirmation.tsx b/src/pages/iou/request/step/IOURequestStepConfirmation.tsx index 989277bb5fc1..5fbc536144fa 100644 --- a/src/pages/iou/request/step/IOURequestStepConfirmation.tsx +++ b/src/pages/iou/request/step/IOURequestStepConfirmation.tsx @@ -603,6 +603,7 @@ function IOURequestStepConfirmation({ { - const inputTypesToIgnore = ['button', 'submit', 'checkbox', 'file', 'image']; - return (elem.tagName === 'INPUT' && !inputTypesToIgnore.includes((elem as HTMLInputElement).type)) || elem.tagName === 'TEXTAREA' || elem.hasAttribute('contenteditable'); -}; - -const handleFocusIn = (event: FocusEvent): void => { - const target = event.target as HTMLElement; - if (target && isKeyboardInput(target)) { - isVisible = true; - } -}; - -const handleFocusOut = (event: FocusEvent): void => { - const target = event.target as HTMLElement; - if (target && isKeyboardInput(target)) { - isVisible = false; - } -}; - -document.addEventListener('focusin', handleFocusIn); -document.addEventListener('focusout', handleFocusOut); - -const dismiss = (): Promise => { - return new Promise((resolve) => { - if (!isVisible) { - resolve(); - return; - } - - Keyboard.dismiss(); - InteractionManager.runAfterInteractions(() => { - isVisible = false; - resolve(); - }); - }); -}; - -const utils = { - dismiss, -}; - -export default utils; From 65345c428110422bee470eefd817345f16fb4e43 Mon Sep 17 00:00:00 2001 From: Huu Le <20178761+huult@users.noreply.github.com> Date: Thu, 19 Dec 2024 12:17:09 +0700 Subject: [PATCH 06/10] Check visual viewport before setting maxHeight --- src/CONST.ts | 2 - src/components/ScreenWrapper.tsx | 37 +++++++++++++++---- src/hooks/useReadyWithDimensions.ts | 31 ---------------- .../step/IOURequestStepConfirmation.tsx | 1 - 4 files changed, 30 insertions(+), 41 deletions(-) delete mode 100644 src/hooks/useReadyWithDimensions.ts diff --git a/src/CONST.ts b/src/CONST.ts index 06e34b518c5c..e5c070676e49 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -319,8 +319,6 @@ const CONST = { NON_BILLABLE: 'nonBillable', }, - DIMENSIONS_CHANGED_DELAY: 1000, - // Note: Group and Self-DM excluded as these are not tied to a Workspace WORKSPACE_ROOM_TYPES: [chatTypes.POLICY_ADMINS, chatTypes.POLICY_ANNOUNCE, chatTypes.DOMAIN_ALL, chatTypes.POLICY_ROOM, chatTypes.POLICY_EXPENSE_CHAT, chatTypes.INVOICE], ANDROID_PACKAGE_NAME, diff --git a/src/components/ScreenWrapper.tsx b/src/components/ScreenWrapper.tsx index d4f86b26c81f..20e914ef6b5a 100644 --- a/src/components/ScreenWrapper.tsx +++ b/src/components/ScreenWrapper.tsx @@ -8,7 +8,6 @@ import type {EdgeInsets} from 'react-native-safe-area-context'; import useEnvironment from '@hooks/useEnvironment'; import useInitialDimensions from '@hooks/useInitialWindowDimensions'; import useNetwork from '@hooks/useNetwork'; -import useReadyWithDimensions from '@hooks/useReadyWithDimensions'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useStyledSafeAreaInsets from '@hooks/useStyledSafeAreaInsets'; import useTackInputFocus from '@hooks/useTackInputFocus'; @@ -17,6 +16,7 @@ import useWindowDimensions from '@hooks/useWindowDimensions'; import * as Browser from '@libs/Browser'; import type {PlatformStackNavigationProp} from '@libs/Navigation/PlatformStackNavigation/types'; import type {AuthScreensParamList, RootStackParamList} from '@libs/Navigation/types'; +import addViewportResizeListener from '@libs/VisualViewport'; import toggleTestToolsModal from '@userActions/TestTool'; import CONST from '@src/CONST'; import CustomDevMenu from './CustomDevMenu'; @@ -104,8 +104,6 @@ type ScreenWrapperProps = { /** Overrides the focus trap default settings */ focusTrapSettings?: FocusTrapForScreenProps['focusTrapSettings']; - - shouldListenToDimensionChanges?: boolean; }; type ScreenWrapperStatusContextType = { @@ -138,7 +136,6 @@ function ScreenWrapper( shouldShowOfflineIndicatorInWideScreen = false, shouldUseCachedViewportHeight = false, focusTrapSettings, - shouldListenToDimensionChanges = false, }: ScreenWrapperProps, ref: ForwardedRef, ) { @@ -164,10 +161,9 @@ function ScreenWrapper( const {isDevelopment} = useEnvironment(); const {isOffline} = useNetwork(); - const {isReady} = useReadyWithDimensions(Browser.isMobileSafari() && shouldListenToDimensionChanges); const [didScreenTransitionEnd, setDidScreenTransitionEnd] = useState(false); - const maxHeight = shouldEnableMaxHeight && isReady ? windowHeight : undefined; + const maxHeight = shouldEnableMaxHeight ? windowHeight : undefined; const minHeight = shouldEnableMinHeight && !Browser.isSafari() ? initialHeight : undefined; const route = useRoute(); @@ -175,6 +171,8 @@ function ScreenWrapper( return !!route?.params && 'singleNewDotEntry' in route.params && route.params.singleNewDotEntry === 'true'; }, [route?.params]); + const initVisualViewport = Browser.isSafari() && window.visualViewport ? window.visualViewport.height : undefined; + const [isMaxHeightReady, setIsMaxHeightReady] = useState(!Browser.isSafari()); UNSTABLE_usePreventRemove(shouldReturnToOldDot, () => { NativeModules.HybridAppModule?.closeReactNativeApp(false, false); }); @@ -198,6 +196,26 @@ function ScreenWrapper( }), ).current; + useEffect(() => { + if (!Browser.isMobileSafari()) { + return; + } + + const handleViewportResize = () => { + if (!window.visualViewport) { + return; + } + + setIsMaxHeightReady(window.visualViewport.height === initVisualViewport); + }; + + const removeViewportResizeListener = addViewportResizeListener(handleViewportResize); + + return () => { + removeViewportResizeListener(); + }; + }, [initVisualViewport]); + useEffect(() => { // On iOS, the transitionEnd event doesn't trigger some times. As such, we need to set a timeout const timeout = setTimeout(() => { @@ -281,7 +299,12 @@ function ScreenWrapper( {...keyboardDismissPanResponder.panHandlers} > diff --git a/src/hooks/useReadyWithDimensions.ts b/src/hooks/useReadyWithDimensions.ts deleted file mode 100644 index 897be8c558df..000000000000 --- a/src/hooks/useReadyWithDimensions.ts +++ /dev/null @@ -1,31 +0,0 @@ -import {useEffect, useState} from 'react'; -import {Dimensions} from 'react-native'; -import CONST from '@src/CONST'; - -const useReadyWithDimensions = (isEnabled = true) => { - const [isReady, setIsReady] = useState(false); - - useEffect(() => { - if (!isEnabled) { - return; - } - const timer = setTimeout(() => { - setIsReady(true); - }, CONST.DIMENSIONS_CHANGED_DELAY); - - const handleDimensionChange = () => { - setIsReady(true); - }; - - const subscription = Dimensions.addEventListener('change', handleDimensionChange); - - return () => { - clearTimeout(timer); - subscription?.remove(); - }; - }, [isEnabled]); - - return {isReady}; -}; - -export default useReadyWithDimensions; diff --git a/src/pages/iou/request/step/IOURequestStepConfirmation.tsx b/src/pages/iou/request/step/IOURequestStepConfirmation.tsx index ac335f90dd79..15aef60bf3d3 100644 --- a/src/pages/iou/request/step/IOURequestStepConfirmation.tsx +++ b/src/pages/iou/request/step/IOURequestStepConfirmation.tsx @@ -607,7 +607,6 @@ function IOURequestStepConfirmation({ Date: Thu, 19 Dec 2024 13:35:58 +0700 Subject: [PATCH 07/10] Use visual viewport to detect keyboard show/hide --- src/components/ScreenWrapper.tsx | 30 +----------- src/utils/{keyboard.ts => keyboard/index.ts} | 0 src/utils/keyboard/index.website.ts | 48 ++++++++++++++++++++ 3 files changed, 49 insertions(+), 29 deletions(-) rename src/utils/{keyboard.ts => keyboard/index.ts} (100%) create mode 100644 src/utils/keyboard/index.website.ts diff --git a/src/components/ScreenWrapper.tsx b/src/components/ScreenWrapper.tsx index 20e914ef6b5a..b57b12117736 100644 --- a/src/components/ScreenWrapper.tsx +++ b/src/components/ScreenWrapper.tsx @@ -16,7 +16,6 @@ import useWindowDimensions from '@hooks/useWindowDimensions'; import * as Browser from '@libs/Browser'; import type {PlatformStackNavigationProp} from '@libs/Navigation/PlatformStackNavigation/types'; import type {AuthScreensParamList, RootStackParamList} from '@libs/Navigation/types'; -import addViewportResizeListener from '@libs/VisualViewport'; import toggleTestToolsModal from '@userActions/TestTool'; import CONST from '@src/CONST'; import CustomDevMenu from './CustomDevMenu'; @@ -171,8 +170,6 @@ function ScreenWrapper( return !!route?.params && 'singleNewDotEntry' in route.params && route.params.singleNewDotEntry === 'true'; }, [route?.params]); - const initVisualViewport = Browser.isSafari() && window.visualViewport ? window.visualViewport.height : undefined; - const [isMaxHeightReady, setIsMaxHeightReady] = useState(!Browser.isSafari()); UNSTABLE_usePreventRemove(shouldReturnToOldDot, () => { NativeModules.HybridAppModule?.closeReactNativeApp(false, false); }); @@ -196,26 +193,6 @@ function ScreenWrapper( }), ).current; - useEffect(() => { - if (!Browser.isMobileSafari()) { - return; - } - - const handleViewportResize = () => { - if (!window.visualViewport) { - return; - } - - setIsMaxHeightReady(window.visualViewport.height === initVisualViewport); - }; - - const removeViewportResizeListener = addViewportResizeListener(handleViewportResize); - - return () => { - removeViewportResizeListener(); - }; - }, [initVisualViewport]); - useEffect(() => { // On iOS, the transitionEnd event doesn't trigger some times. As such, we need to set a timeout const timeout = setTimeout(() => { @@ -299,12 +276,7 @@ function ScreenWrapper( {...keyboardDismissPanResponder.panHandlers} > diff --git a/src/utils/keyboard.ts b/src/utils/keyboard/index.ts similarity index 100% rename from src/utils/keyboard.ts rename to src/utils/keyboard/index.ts diff --git a/src/utils/keyboard/index.website.ts b/src/utils/keyboard/index.website.ts new file mode 100644 index 000000000000..f2ea9c673fdf --- /dev/null +++ b/src/utils/keyboard/index.website.ts @@ -0,0 +1,48 @@ +import {Keyboard} from 'react-native'; + +let isVisible = false; +const initialViewportHeight = window?.visualViewport?.height; + +const handleResize = () => { + const currentHeight = window?.visualViewport?.height; + + if (!currentHeight || !initialViewportHeight) { + return; + } + + if (currentHeight < initialViewportHeight) { + isVisible = true; + return; + } + + if (currentHeight === initialViewportHeight) { + isVisible = false; + } +}; + +window.visualViewport?.addEventListener('resize', handleResize); + +const dismiss = (): Promise => { + return new Promise((resolve) => { + if (!isVisible) { + resolve(); + return; + } + + const handleDismissResize = () => { + if (window.visualViewport?.height !== initialViewportHeight) { + return; + } + + window.visualViewport?.removeEventListener('resize', handleDismissResize); + return resolve(); + }; + + window.visualViewport?.addEventListener('resize', handleDismissResize); + Keyboard.dismiss(); + }); +}; + +const utils = {dismiss}; + +export default utils; From 9917a1baafb56a65c423c25b4b12f8987c61c3fe Mon Sep 17 00:00:00 2001 From: Huu Le <20178761+huult@users.noreply.github.com> Date: Thu, 19 Dec 2024 13:37:29 +0700 Subject: [PATCH 08/10] Remove unused break line --- src/components/ScreenWrapper.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/components/ScreenWrapper.tsx b/src/components/ScreenWrapper.tsx index b57b12117736..bb20b4abae11 100644 --- a/src/components/ScreenWrapper.tsx +++ b/src/components/ScreenWrapper.tsx @@ -159,9 +159,7 @@ function ScreenWrapper( const styles = useThemeStyles(); const {isDevelopment} = useEnvironment(); const {isOffline} = useNetwork(); - const [didScreenTransitionEnd, setDidScreenTransitionEnd] = useState(false); - const maxHeight = shouldEnableMaxHeight ? windowHeight : undefined; const minHeight = shouldEnableMinHeight && !Browser.isSafari() ? initialHeight : undefined; From c25b46012ad22983cae775e5bc5bc0f0448fe4b8 Mon Sep 17 00:00:00 2001 From: Huu Le <20178761+huult@users.noreply.github.com> Date: Tue, 24 Dec 2024 22:14:11 +0700 Subject: [PATCH 09/10] fix eslint --- src/libs/DistanceRequestUtils.ts | 5 ++++- src/pages/iou/request/step/IOURequestStepParticipants.tsx | 6 +++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/libs/DistanceRequestUtils.ts b/src/libs/DistanceRequestUtils.ts index c41b33873a8a..e69b6754aeb2 100644 --- a/src/libs/DistanceRequestUtils.ts +++ b/src/libs/DistanceRequestUtils.ts @@ -289,7 +289,10 @@ function convertToDistanceInMeters(distance: number, unit: Unit): number { /** * Returns custom unit rate ID for the distance transaction */ -function getCustomUnitRateID(reportID: string) { +function getCustomUnitRateID(reportID?: string) { + if (!reportID) { + return ''; + } const report = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`]; const parentReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${report?.parentReportID}`]; const policy = PolicyUtils.getPolicy(report?.policyID ?? parentReport?.policyID); diff --git a/src/pages/iou/request/step/IOURequestStepParticipants.tsx b/src/pages/iou/request/step/IOURequestStepParticipants.tsx index af4d674f08d7..6a67a1040f1b 100644 --- a/src/pages/iou/request/step/IOURequestStepParticipants.tsx +++ b/src/pages/iou/request/step/IOURequestStepParticipants.tsx @@ -41,7 +41,7 @@ function IOURequestStepParticipants({ const {translate} = useLocalize(); const styles = useThemeStyles(); const isFocused = useIsFocused(); - const [skipConfirmation] = useOnyx(`${ONYXKEYS.COLLECTION.SKIP_CONFIRMATION}${transactionID ?? -1}`); + const [skipConfirmation] = useOnyx(`${ONYXKEYS.COLLECTION.SKIP_CONFIRMATION}${transactionID ?? CONST.DEFAULT_NUMBER_ID}`); // We need to set selectedReportID if user has navigated back from confirmation page and navigates to confirmation page with already selected participant const selectedReportID = useRef(participants?.length === 1 ? participants.at(0)?.reportID ?? reportID : reportID); @@ -92,7 +92,7 @@ function IOURequestStepParticipants({ (val: Participant[]) => { HttpUtils.cancelPendingRequests(READ_COMMANDS.SEARCH_FOR_REPORTS); - const firstParticipantReportID = val.at(0)?.reportID ?? ''; + const firstParticipantReportID = val.at(0)?.reportID; const rateID = DistanceRequestUtils.getCustomUnitRateID(firstParticipantReportID); const isInvoice = iouType === CONST.IOU.TYPE.INVOICE && ReportUtils.isInvoiceRoomWithID(firstParticipantReportID); numberOfParticipants.current = val.length; @@ -108,7 +108,7 @@ function IOURequestStepParticipants({ } // When a participant is selected, the reportID needs to be saved because that's the reportID that will be used in the confirmation step. - selectedReportID.current = firstParticipantReportID || reportID; + selectedReportID.current = firstParticipantReportID ?? reportID; }, [iouType, reportID, transactionID], ); From 670440364067251e855d0a9cd7a7aad5c5a3a666 Mon Sep 17 00:00:00 2001 From: Huu Le <20178761+huult@users.noreply.github.com> Date: Tue, 24 Dec 2024 23:52:45 +0700 Subject: [PATCH 10/10] Update return default customUnitRateID when report ID is undefined --- src/libs/DistanceRequestUtils.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/libs/DistanceRequestUtils.ts b/src/libs/DistanceRequestUtils.ts index e69b6754aeb2..94167b382d49 100644 --- a/src/libs/DistanceRequestUtils.ts +++ b/src/libs/DistanceRequestUtils.ts @@ -290,13 +290,14 @@ function convertToDistanceInMeters(distance: number, unit: Unit): number { * Returns custom unit rate ID for the distance transaction */ function getCustomUnitRateID(reportID?: string) { + let customUnitRateID: string = CONST.CUSTOM_UNITS.FAKE_P2P_ID; + if (!reportID) { - return ''; + return customUnitRateID; } const report = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`]; const parentReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${report?.parentReportID}`]; const policy = PolicyUtils.getPolicy(report?.policyID ?? parentReport?.policyID); - let customUnitRateID: string = CONST.CUSTOM_UNITS.FAKE_P2P_ID; if (isEmptyObject(policy)) { return customUnitRateID;