diff --git a/src/components/ScreenWrapper.tsx b/src/components/ScreenWrapper.tsx index a7d05a335d43..2fc3efbb4f26 100644 --- a/src/components/ScreenWrapper.tsx +++ b/src/components/ScreenWrapper.tsx @@ -10,6 +10,7 @@ import useEnvironment from '@hooks/useEnvironment'; import useInitialDimensions from '@hooks/useInitialWindowDimensions'; import useKeyboardState from '@hooks/useKeyboardState'; import useNetwork from '@hooks/useNetwork'; +import useTackInputFocus from '@hooks/useTackInputFocus'; import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; import * as Browser from '@libs/Browser'; @@ -79,6 +80,9 @@ type ScreenWrapperProps = { /** Whether to show offline indicator */ shouldShowOfflineIndicator?: boolean; + /** Whether to avoid scroll on virtual viewport */ + shouldAvoidScrollOnVirtualViewport?: boolean; + /** * The navigation prop is passed by the navigator. It is used to trigger the onEntryTransitionEnd callback * when the screen transition ends. @@ -109,6 +113,7 @@ function ScreenWrapper( onEntryTransitionEnd, testID, navigation: navigationProp, + shouldAvoidScrollOnVirtualViewport = true, shouldShowOfflineIndicatorInWideScreen = false, }: ScreenWrapperProps, ref: ForwardedRef, @@ -192,6 +197,8 @@ function ScreenWrapper( // eslint-disable-next-line react-hooks/exhaustive-deps }, []); + const isAvoidingViewportScroll = useTackInputFocus(shouldEnableMaxHeight && shouldAvoidScrollOnVirtualViewport && Browser.isMobileSafari()); + return ( {({insets, paddingTop, paddingBottom, safeAreaPaddingBottomStyle}) => { @@ -220,12 +227,12 @@ function ScreenWrapper( {...keyboardDissmissPanResponder.panHandlers} > diff --git a/src/hooks/useTackInputFocus/index.native.ts b/src/hooks/useTackInputFocus/index.native.ts new file mode 100644 index 000000000000..683040d7421a --- /dev/null +++ b/src/hooks/useTackInputFocus/index.native.ts @@ -0,0 +1,6 @@ +/** + * Detects input or text area focus on browser. Native doesn't support DOM so default to false + */ +export default function useTackInputFocus(): boolean { + return false; +} diff --git a/src/hooks/useTackInputFocus/index.ts b/src/hooks/useTackInputFocus/index.ts new file mode 100644 index 000000000000..124f8460127c --- /dev/null +++ b/src/hooks/useTackInputFocus/index.ts @@ -0,0 +1,49 @@ +import {useCallback, useEffect} from 'react'; +import useDebouncedState from '@hooks/useDebouncedState'; + +/** + * Detects input or text area focus on browsers, to avoid scrolling on virtual viewports + */ +export default function useTackInputFocus(enable = false): boolean { + const [, isInputFocusDebounced, setIsInputFocus] = useDebouncedState(false); + + const handleFocusIn = useCallback( + (event: FocusEvent) => { + const targetElement = event.target as HTMLElement; + if (targetElement.tagName === 'INPUT' || targetElement.tagName === 'TEXTAREA') { + setIsInputFocus(true); + } + }, + [setIsInputFocus], + ); + + const handleFocusOut = useCallback( + (event: FocusEvent) => { + const targetElement = event.target as HTMLElement; + if (targetElement.tagName === 'INPUT' || targetElement.tagName === 'TEXTAREA') { + setIsInputFocus(false); + } + }, + [setIsInputFocus], + ); + + const resetScrollPositionOnVisualViewport = useCallback(() => { + window.scrollTo({top: 0}); + }, []); + + useEffect(() => { + if (!enable) { + return; + } + window.addEventListener('focusin', handleFocusIn); + window.addEventListener('focusout', handleFocusOut); + window.visualViewport?.addEventListener('scroll', resetScrollPositionOnVisualViewport); + return () => { + window.removeEventListener('focusin', handleFocusIn); + window.removeEventListener('focusout', handleFocusOut); + window.visualViewport?.removeEventListener('scroll', resetScrollPositionOnVisualViewport); + }; + }, [enable, handleFocusIn, handleFocusOut, resetScrollPositionOnVisualViewport]); + + return isInputFocusDebounced; +}