Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Mobile Web Safari: Adds auto scroll back - Issue 5894 #6413

Merged
merged 7 commits into from
Nov 30, 2021
5 changes: 5 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
},
Expand Down
3 changes: 3 additions & 0 deletions src/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -40,4 +41,6 @@ const App = () => (

App.displayName = 'App';

initializeiOSSafariAutoScrollback();

Comment on lines +44 to +45
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this is something that's only intended to run on web, maybe you should consider importing and calling it only inside src/setup/platformSetup/index.website.js

I'll would even go as far as

export default function () {
  // AppRegistry.runApplication(
  // ...

    if (getBrowser() === CONST.BROWSER.SAFARI || Str.contains(userAgent, 'iphone os 1')) {
        const applyScrollFix = require('../libs/iOSSafariAutoScrollback');
        applyScrollFix();
    }

This way extra code is imported only when needed

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@kidroca it is merged already... but that's a GREAT suggestion!

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great suggestion @kidroca! If either of you wouldn't mind submitting a quick PR, I'll review it.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks guys!
@roryabraham I'll open a PR about this today, when I get the chance

export default App;
3 changes: 3 additions & 0 deletions src/libs/iOSSafariAutoScrollback/index.js
Original file line number Diff line number Diff line change
@@ -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 () { }
87 changes: 87 additions & 0 deletions src/libs/iOSSafariAutoScrollback/index.web.js
Original file line number Diff line number Diff line change
@@ -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')) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't this supposed to be

getBrowser() !== CONST.BROWSER.SAFARI

!getBrowser() would be evaluated to boolean otherwise, so it's most probably the second part of the check that is doing all the work here

Copy link
Member

@parasharrajat parasharrajat Dec 1, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WOW. It should be getBrowser() == CONST.BROWSER.SAFARI

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So we want to exist early if it regular safari, and only apply the polyfill for mobile ?

Copy link
Contributor

@kidroca kidroca Dec 1, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is an early return condition
I though it tried to exist early if the browser is not Safari or mobile safari

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IT should only work on Safari web.

Copy link
Contributor Author

@sidferreira sidferreira Dec 1, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OMG 🤦‍♂️ how I did that?!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@kidroca if possible, load that file only for Website + Safari

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey I'm slightly confused I thought the issue is "Mobile Web Safari: Adds auto scroll back"
Which mean we're only targeting ios Safari (iPhone & iPads) and should apply the fix there, correct?

My plan is to use this expression

if (_.every([/iP(ad|hone)/i, /WebKit/i, /CriOS/i], re => re.test(navigator.userAgent))) {
   const applyAutoScrollbackFix = require('../../libs/iOSSafariAutoScrollback');
   applyAutoScrollbackFix();
}

The check is based on: https://stackoverflow.com/a/29696509/4834103

I can extract

const isMobileSafari = () => _.every([/iP(ad|hone)/i, /WebKit/i, /CriOS/i], re => re.test(navigator.userAgent));

But I don't know where to put it
Since it's only one time usage so far, I think I'll just keep it inline
Otherwise getBrowser seems to be a nice place, but I'm not sure how exactly to fit this expression there, or just add a isMobileSafari check

return;
}
smoothscrollPolyfill.polyfill();
document.addEventListener('touchstart', touchStarted);
document.addEventListener('touchend', scrollbackAfterTouch);
document.addEventListener('scroll', scrollbackAfterScroll);
document.addEventListener('focusin', startWaitingForScroll);
document.addEventListener('focusout', stopWaitingForScroll);
}