diff --git a/package-lock.json b/package-lock.json index 1242191caded..60a8d5ad13b0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -38956,6 +38956,11 @@ "dev": true, "optional": true }, + "smoothscroll-polyfill": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/smoothscroll-polyfill/-/smoothscroll-polyfill-0.4.4.tgz", + "integrity": "sha512-TK5ZA9U5RqCwMpfoMq/l1mrH0JAR7y7KRvOBx0n2869aLxch+gT9GhN3yUfjiw+d/DiF1mKo14+hd62JyMmoBg==" + }, "snapdragon": { "version": "0.8.2", "resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz", diff --git a/package.json b/package.json index c7284a3ff62e..258a17b97739 100644 --- a/package.json +++ b/package.json @@ -104,6 +104,7 @@ "react-web-config": "^1.0.0", "rn-fetch-blob": "^0.12.0", "save": "^2.4.0", + "smoothscroll-polyfill": "^0.4.4", "underscore": "^1.13.1", "urbanairship-react-native": "^11.0.2" }, diff --git a/src/App.js b/src/App.js index bcbd148aa63d..677e93ad7a32 100644 --- a/src/App.js +++ b/src/App.js @@ -10,6 +10,7 @@ import OnyxProvider from './components/OnyxProvider'; import HTMLEngineProvider from './components/HTMLEngineProvider'; import ComposeProviders from './components/ComposeProviders'; import SafeArea from './components/SafeArea'; +import initializeiOSSafariAutoScrollback from './libs/iOSSafariAutoScrollback'; LogBox.ignoreLogs([ // Basically it means that if the app goes in the background and back to foreground on Android, @@ -40,4 +41,6 @@ const App = () => ( App.displayName = 'App'; +initializeiOSSafariAutoScrollback(); + export default App; diff --git a/src/libs/iOSSafariAutoScrollback/index.js b/src/libs/iOSSafariAutoScrollback/index.js new file mode 100644 index 000000000000..a0d28c7d9df6 --- /dev/null +++ b/src/libs/iOSSafariAutoScrollback/index.js @@ -0,0 +1,3 @@ +/* The autoScrollBack address Mobile Safari-specific issues when the user overscrolls the window while the keyboard is visible */ +/* It has no effect to other platforms */ +export default function () { } diff --git a/src/libs/iOSSafariAutoScrollback/index.web.js b/src/libs/iOSSafariAutoScrollback/index.web.js new file mode 100644 index 000000000000..6f0aee5347ca --- /dev/null +++ b/src/libs/iOSSafariAutoScrollback/index.web.js @@ -0,0 +1,87 @@ +/* The autoScrollBack address Mobile Safari-specific issues when the user overscrolls the window while the keyboard is visible */ +import Str from 'expensify-common/lib/str'; +import smoothscrollPolyfill from 'smoothscroll-polyfill'; +import CONST from '../../CONST'; +import getBrowser from '../getBrowser'; + +const userAgent = navigator.userAgent.toLowerCase(); + +// The innerHeight when the keyboard is not visible +const baseInnerHeight = window.innerHeight; + +// Control flag if an input/text area was focused and we're waiting the "focus scroll" to identify the screen size +let isWaitingForScroll = false; + +// Calculated value of the maximum value to the screen to scroll when keyboard is visible +let maxScrollY = 0; + +let isTouching = false; + +let scrollbackTimeout; + +const isIOS15 = Str.contains(userAgent, 'iphone os 15_'); + +function scrollback() { + if (isTouching || maxScrollY >= window.scrollY) { + return; + } + window.scrollTo({top: maxScrollY, behavior: 'smooth'}); +} + +function scheduleScrollback() { + if (!maxScrollY) { + return; + } + + if (scrollbackTimeout) { + clearTimeout(scrollbackTimeout); + scrollbackTimeout = undefined; + } + + if (!isTouching && window.scrollY > maxScrollY) { + scrollbackTimeout = setTimeout(scrollback, 34); + } +} + + +function touchStarted() { + isTouching = true; +} + +function scrollbackAfterTouch() { + isTouching = false; + scrollback(); +} + +function scrollbackAfterScroll() { + if (isWaitingForScroll && !maxScrollY) { + isWaitingForScroll = false; + const keyboardHeight = baseInnerHeight - window.visualViewport.height; + + // The iOS 15 Safari has a 52 pixel tall address label that must be manually added + maxScrollY = keyboardHeight + (isIOS15 ? 52 : 0); + } + + scheduleScrollback(); +} + +function startWaitingForScroll() { + isWaitingForScroll = true; +} + +function stopWaitingForScroll() { + isWaitingForScroll = false; +} + + +export default function () { + if (!getBrowser() === CONST.BROWSER.SAFARI || !Str.contains(userAgent, 'iphone os 1')) { + return; + } + smoothscrollPolyfill.polyfill(); + document.addEventListener('touchstart', touchStarted); + document.addEventListener('touchend', scrollbackAfterTouch); + document.addEventListener('scroll', scrollbackAfterScroll); + document.addEventListener('focusin', startWaitingForScroll); + document.addEventListener('focusout', stopWaitingForScroll); +}