48

I have a responsive web page that opens a modal when you tap a button. When the modal opens, it is set to take up the full width and height of the page using fixed positioning. The modal also has an input field in it.

On iOS devices, when the input field is focused, the keyboard opens. However, when it opens, it actually pushes the full document up out of the way such that half of my page goes above the top of the viewport. I can confirm that the actual html tag itself has been pushed up to compensate for the keyboard and that it has not happened via CSS or JavaScript.

Has anyone seen this before and, if so, is there a way to prevent it, or reposition things after the keyboard has opened? It's a problem because I need users to be able to see content at the top of the modal while, simultaneously, I'd like to auto-focus the input field.

rescuecreative
  • 3,607
  • 3
  • 18
  • 28
  • Did you see this: http://stackoverflow.com/questions/13820088/how-to-prevent-keyboard-push-up-webview-at-ios-app-using-phonegap – Iceman Jul 27 '16 at 17:40
  • @Iceman no I didn't. I tried searching for similar questions but didn't find that one. I'll try it. – rescuecreative Jul 27 '16 at 17:41
  • @rescuecreative . I am not to sure if this will help, as this solution is for IOS8 as i had similar issues on IOS8 and Safari. – ankurJos Jul 27 '16 at 17:48
  • @Iceman My environment is not phonegap, it's just a normal website. So I'm not sure if there's something different about phonegap, but this doesn't work for me. – rescuecreative Jul 27 '16 at 17:49
  • @rescuecreative could be issue with new version. i'll remove my answer. – Iceman Jul 27 '16 at 17:50
  • Set `document.body.style.height = window.innerHeight+'px'` on page load and try... – Rayon Jul 27 '16 at 17:58
  • This should help: https://blog.opendigerati.com/the-eccentric-ways-of-ios-safari-with-the-keyboard-b5aa3f34228d – Ruraloville Aug 07 '20 at 11:54

8 Answers8

30

first

<script type="text/javascript">
 $(document).ready(function() {
     document.ontouchmove = function(e){
          e.preventDefault();
          }
 });

then this

input.onfocus = function () {
    window.scrollTo(0, 0);
    document.body.scrollTop = 0;
}
ankurJos
  • 496
  • 4
  • 16
  • 1
    Thanks! It works perfectly. Btw, I only use `document.body.scrollTop = 0`; – Nguyen Tran Aug 01 '17 at 08:50
  • 4
    I was trying to solve a problem for 4 hours until I encountered your answer. Thank you so much. Although only the `input.onfocus = function () { window.scrollTo(0, 0); document.body.scrollTop = 0; }` is necessary. The first part freezes the page. – neophyte Mar 27 '18 at 10:58
  • Why is the first part i.e. e.preventDefault() on touchmove necessary? – Elton Lin Jul 27 '23 at 22:36
  • Also, it seems like the iOS "shove to top" behavior doesn't happen instantly, so I need to do a setTimeout() before actually resetting the window scroll. Now I'm trying to figure out the exact timing so I don't need to use setTimeout – Elton Lin Jul 27 '23 at 23:01
7

For anyone stumbling into this in React, I've managed to fix it adapting @ankurJos solution like this:

const inputElement = useRef(null);

useEffect(() => {
  inputElement.current.onfocus = () => {
    window.scrollTo(0, 0);
    document.body.scrollTop = 0;
  };
});

<input ref={inputElement} />
lewislbr
  • 1,012
  • 14
  • 23
3

I struggled with this for awhile, I couldn't find something that worked well for me.

I ended up doing some JavaScript hackery to make it work.

What I found was that Safari wouldn't push the viewport if the input element was in the top half of the screen. That was the key to my little hack:

I intercept the focus event on the input object and instead redirect the focus to a invisible (by transform: translateX(-9999px)). Then once the keyboard is on screen (usually 200ms or so) I trigger the focus event on the original element which has since animated on screen.

It's a kind of complicated interaction, but it works really well.

function ensureOffScreenInput() {
  let elem = document.querySelector("#__fake_input");
  if (!elem) {
    elem = document.createElement("input");
    elem.style.position = "fixed";
    elem.style.top = "0px";
    elem.style.opacity = "0.1";
    elem.style.width = "10px";
    elem.style.height = "10px";
    elem.style.transform = "translateX(-1000px)";
    elem.type = "text";
    elem.id = "__fake_input";
    document.body.appendChild(elem);
  }
  return elem;
}

var node = document.querySelector('#real-input')
var fakeInput = ensureOffScreenInput();

function handleFocus(event) {
  fakeInput.focus();

  let last = event.target.getBoundingClientRect().top;

  setTimeout(() => {
    function detectMovement() {
      const now = event.target.getBoundingClientRect().top;
      const dist = Math.abs(last - now);

      // Once any animations have stabilized, do your thing
      if (dist > 0.01) {
        requestAnimationFrame(detectMovement);
        last = now;
      } else {
        event.target.focus();
        event.target.addEventListener("focus", handleFocus, { once: true });
      }
    }
    requestAnimationFrame(detectMovement);
  }, 50);
}

node.addEventListener("focus", handleFocus, { once: true });

Personally I use this code in a Svelte action and it works really well in my Svelte PWA clone of Apple Maps.

Video of it working in a PWA clone of Apple Maps

You'll notice in the video that the auto-complete changes after the animation of the input into the top half of the viewport stabilizes. That's the focus switch back happening.

The only downside of this hack is that the focus handler on your original implementation will run twice, but there are ways to account for that with metadata.

  • This was the only thing worked for me. Thank you – Onat Jun 21 '21 at 22:32
  • Maybe I did the setup wrong but I couldn't get this to working with lambdatest.com real device test. The code was in an iframe attached to inputs also in the iframe and console.log prints were correct on input focus handlers and input was indeed shifting to the fake element but keyboard still pushed the page. – Glass Cannon Sep 07 '22 at 19:08
0

you could also do this if you don't want scrollTo the top(0, 0)

window.scrollBy(0, 0)
Otani Shuzo
  • 1,118
  • 1
  • 11
  • 22
0
const handleResize = () => {
  document.getElementById('header').style.top = window.visualViewport.offsetTop.toString() + 'px'
}

if (window && window.visualViewport) visualViewport.addEventListener('resize', handleResize)

Source: https://rdavis.io/articles/dealing-with-the-visual-viewport

ekimas
  • 393
  • 5
  • 10
0

CSS,

html,
body {
  overflow-y: auto;
  overflow-x: hidden;
  position: fixed;
}
Wongjn
  • 8,544
  • 2
  • 8
  • 24
  • Your answer could be improved with additional supporting information. Please [edit] to add further details, such as citations or documentation, so that others can confirm that your answer is correct. You can find more information on how to write good answers [in the help center](/help/how-to-answer). – Community May 03 '23 at 00:07
-5

Both IOS8 and Safari bowsers behave the same for input.focus() occuring after page load. They both zoom to the element and bring up the keyboard.(Not too sure if this will be help but have you tried using something like this?)

HTML IS

<input autofocus>

JS is

for (var i = 0; i < 5; i++) {
document.write("<br><button onclick='alert(this.innerHTML)'>" + i + "</button>");
}

//document.querySelector('input').focus();

CSS

button {
width: 300px;
height: 40px;
}

ALso you will have to use a user-agent workaround, you can use it for all IOS versions

if (!/iPad|iPhone|iPod/g.test(navigator.userAgent)) {
element.focus();
}
ankurJos
  • 496
  • 4
  • 16
-5

In some situations this issue can be mitigated by re-focusing the input element.

input.onfocus = function () {
  this.blur();
  this.focus();
}
malthe
  • 1,237
  • 13
  • 25