Safari in ios8 is scrolling screen when fixed elements get focus
Asked Answered
S

12

98

In IOS8 Safari there is a new bug with position fixed.

If you focus a textarea that is in a fixed panel, safari will scroll you to the bottom of the page.

This makes all sorts of UIs impossible to work with, since you have no way of entering text into textareas without scrolling your page all the way down and losing your place.

Is there any way to workaround this bug cleanly?

#a {
  height: 10000px;
  background: linear-gradient(red, blue);
}
#b {
  position: fixed;
  bottom: 20px;
  left: 10%;
  width: 100%;
  height: 300px;
}

textarea {
   width: 80%;
   height: 300px;
}
<html>
   <body>
   <div id="a"></div>
   <div id="b"><textarea></textarea></div>
   </body>
</html>
Swinge answered 12/3, 2015 at 4:39 Comment(11)
Would setting a z-index on #b help?Ottie
z index is no help, maybe some fancy no op css transform would much with stack contexts, not sure.Swinge
for context here is the discussion on Discourse: meta.discourse.org/t/dealing-with-ios-8-ipad-mobile-safari-bugs/…Swinge
iOS safari is the new IEReadytowear
I am having a similar problem. When SELECT opens and needs to nudge the focused element around so it will fit on screen, the amount that it nudges the content seems to remain there, event after it closes and repaints. The content is in the right place, but the touch-points are in the nudged position. Touching a text field sets the focus to a text field somewhere else on the screen. The inspector shows elements in the nudged position instead of where they are visually on-screen. I'm going to try this fix and see how it goes.Fayfayal
@Readytowear agreed. any moronic OS which ties its default browser version down to the OS is going to fall foul of the issues which has plagued IE for the past 7 years.Pained
Damn, I was hoping Apple would have fixed this bug in iOS9 but I just upgraded and the bug is still there :(Somersault
Yeah its terrible ...Swinge
Still broken in iOS 10. Have you filed bugs?Tapestry
@Tapestry many times, I have zero confidence this will ever be fixed, reached out on twitter, tried everythingSwinge
Possible duplicate of several SO questions. See gist.github.com/avesus/… for details.Sadiron
S
0

This is now fixed in iOS 10.3!

Hacks should no longer be needed.

Swinge answered 12/6, 2017 at 18:22 Comment(4)
Can you point to any release notes that point this out as being fixed?Paleobotany
Apple are very secretive, they closed my bug report I confirmed it now works proper, that is all I got :)Swinge
I still have this issue on iOS 11Hotheaded
No, this is still an issue even on iOS 13.Iodate
T
63

Based on this good analysis of this issue, I've used this in html and body elements in css:

html,body{
    -webkit-overflow-scrolling : touch !important;
    overflow: auto !important;
    height: 100% !important;
}

I think it's working great for me.

Turnip answered 27/2, 2016 at 20:31 Comment(6)
worked for me too. This screwed up a lot of other things since I am manipulating my DOM on load, so I make this into a class, and added it to the html, body after the DOM has stabilized. Things like scrollTop dont work very well (I am doing auto scrolling), but again, you can add / remove the class while doing scrolling operations. Poor work on part of Safari team though.Groenendael
People looking at this option might also want to test transform: translateZ(0); from #7808610Derosier
This solves the problem, but if you have animations they will look very choppy. Might be better to wrap it in a media query.Glomerulonephritis
Worked for me on iOS 10.3.Vachon
It doesn't solve the problem. You need to intercept scrolling when virtual keyboard shows up and change height to specific value: https://mcmap.net/q/218801/-how-to-stop-mobile-safari-from-setting-fixed-positions-to-absolute-on-input-focusSadiron
This worked for me to fix a weird android issue affecting bootstrap-vue modal with text input. Thank you!Remarque
D
36

The best solution I could come up with is to switch to using position: absolute; on focus and calculating the position it was at when it was using position: fixed;. The trick is that the focus event fires too late, so touchstart must be used.

The solution in this answer mimics the correct behavior we had in iOS 7 very closely.

Requirements:

The body element must have positioning in order to ensure proper positioning when the element switches to absolute positioning.

body {
    position: relative;
}

The Code (Live Example):

The following code is a basic example for the provided test-case, and can be adapted for your specific use-case.

//Get the fixed element, and the input element it contains.
var fixed_el = document.getElementById('b');
var input_el = document.querySelector('textarea');
//Listen for touchstart, focus will fire too late.
input_el.addEventListener('touchstart', function() {
    //If using a non-px value, you will have to get clever, or just use 0 and live with the temporary jump.
    var bottom = parseFloat(window.getComputedStyle(fixed_el).bottom);
    //Switch to position absolute.
    fixed_el.style.position = 'absolute';
    fixed_el.style.bottom = (document.height - (window.scrollY + window.innerHeight) + bottom) + 'px';
    //Switch back when focus is lost.
    function blured() {
        fixed_el.style.position = '';
        fixed_el.style.bottom = '';
        input_el.removeEventListener('blur', blured);
    }
    input_el.addEventListener('blur', blured);
});

Here is the same code without the hack for comparison.

Caveat:

If the position: fixed; element has any other parent elements with positioning besides body, switching to position: absolute; may have unexpected behavior. Due to the nature of position: fixed; this is probably not a major issue, since nesting such elements is not common.

Recommendations:

While the use of the touchstart event will filter out most desktop environments, you will probably want to use user-agent sniffing so that this code will only run for the broken iOS 8, and not other devices such as Android and older iOS versions. Unfortunately, we don't yet know when Apple will fix this issue in iOS, but I would be surprised if it is not fixed in the next major version.

Dunedin answered 15/3, 2015 at 18:56 Comment(5)
I wonder if double wrapping with a div and setting height to %100 on transparent wrapping div could trick it into avoiding this...Swinge
@SamSaffron Could you clarify how such a technique might work? I tried a few things like this without success. Since the height of the document is ambiguous, I'm not sure how it could work.Dredger
I was thinking simply have a "fixed" 100% height wrapper may work around this, possibly notSwinge
@downvoter: Did I get something wrong? I agree this is a terrible solution, but I don't think there are any better ones.Dredger
This didn't work for me, the input field still moves.Colyer
S
8

I found a method that works without the need to change to position absolute!

Full uncommented code

var scrollPos = $(document).scrollTop();
$(window).scroll(function(){
    scrollPos = $(document).scrollTop();
});
var savedScrollPos = scrollPos;

function is_iOS() {
  var iDevices = [
    'iPad Simulator',
    'iPhone Simulator',
    'iPod Simulator',
    'iPad',
    'iPhone',
    'iPod'
  ];
  while (iDevices.length) {
    if (navigator.platform === iDevices.pop()){ return true; }
  }
  return false;
}

$('input[type=text]').on('touchstart', function(){
    if (is_iOS()){
        savedScrollPos = scrollPos;
        $('body').css({
            position: 'relative',
            top: -scrollPos
        });
        $('html').css('overflow','hidden');
    }
})
.blur(function(){
    if (is_iOS()){
        $('body, html').removeAttr('style');
        $(document).scrollTop(savedScrollPos);
    }
});

Breaking it down

First you need to have the fixed input field toward the top of the page in the HTML (it's a fixed element so it should semantically make sense to have it near the top anyway):

<!DOCTYPE HTML>

<html>

    <head>
      <title>Untitled</title>
    </head>

    <body>
        <form class="fixed-element">
            <input class="thing-causing-the-issue" type="text" />
        </form>

        <div class="everything-else">(content)</div>

    </body>

</html>

Then you need to save the current scroll position into global variables:

//Always know the current scroll position
var scrollPos = $(document).scrollTop();
$(window).scroll(function(){
    scrollPos = $(document).scrollTop();
});

//need to be able to save current scroll pos while keeping actual scroll pos up to date
var savedScrollPos = scrollPos;

Then you need a way to detect iOS devices so it doesn't affect things that don't need the fix (function taken from https://mcmap.net/q/48374/-detect-if-device-is-ios)

//function for testing if it is an iOS device
function is_iOS() {
  var iDevices = [
    'iPad Simulator',
    'iPhone Simulator',
    'iPod Simulator',
    'iPad',
    'iPhone',
    'iPod'
  ];

  while (iDevices.length) {
    if (navigator.platform === iDevices.pop()){ return true; }
  }

  return false;
}

Now that we have everything we need, here is the fix :)

//when user touches the input
$('input[type=text]').on('touchstart', function(){

    //only fire code if it's an iOS device
    if (is_iOS()){

        //set savedScrollPos to the current scroll position
        savedScrollPos = scrollPos;

        //shift the body up a number of pixels equal to the current scroll position
        $('body').css({
            position: 'relative',
            top: -scrollPos
        });

        //Hide all content outside of the top of the visible area
        //this essentially chops off the body at the position you are scrolled to so the browser can't scroll up any higher
        $('html').css('overflow','hidden');
    }
})

//when the user is done and removes focus from the input field
.blur(function(){

    //checks if it is an iOS device
    if (is_iOS()){

        //Removes the custom styling from the body and html attribute
        $('body, html').removeAttr('style');

        //instantly scrolls the page back down to where you were when you clicked on input field
        $(document).scrollTop(savedScrollPos);
    }
});
Somersault answered 4/9, 2015 at 3:2 Comment(6)
+1. This is a significantly less complicated fix than the accepted answer, if you have a non-trivial DOM hierarchy. This should have more upvotesKimberleekimberley
Could you provide this in native JS as well? Thanks so much!Kapok
@SamSaffron, is this answer really worked for u? can i have some example here. it dint worked for me?Katalin
@SamSaffron,is this answer really solved your problem, can u send some example that worked for u, am working on the same, but it dint worked for me.Katalin
@GaneshPutta It's possible that a more recent iOS update has made this not work any more. I posted this 2.5 years ago. It should still work though if you followed all the instructions exactly :/Somersault
@DanielTonon, can u please try with question, i followed all your answer but still dint worked for me. #48662628Katalin
D
4

I was able to fix this for select inputs by adding an event listener to the necessary select elements, then scrolling by an offset of one pixel when the select in question gains focus.

This isn't necessarily a good solution, but it's much simpler and more reliable than the other answers I've seen here. The browser seems to re-render/re-calculate the position: fixed; attribute based on the offset supplied in the window.scrollBy() function.

document.querySelector(".someSelect select").on("focus", function() {window.scrollBy(0, 1)});
Donaldson answered 3/1, 2017 at 20:19 Comment(0)
O
2

Much like Mark Ryan Sallee suggested, I found that dynamically changing the height and overflow of my background element is the key - this gives Safari nothing to scroll to.

So after the modal's opening animation finishes, change the background's styling:

$('body > #your-background-element').css({
  'overflow': 'hidden',
  'height': 0
});

When you close the modal change it back:

$('body > #your-background-element').css({
  'overflow': 'auto',
  'height': 'auto'
});

While other answers are useful in simpler contexts, my DOM was too complicated (thanks SharePoint) to use the absolute/fixed position swap.

Orientalism answered 15/8, 2016 at 16:32 Comment(0)
G
1

Cleanly? no.

I recently had this problem myself with a fixed search field in a sticky header, the best you can do at the moment is keep the scroll position in a variable at all times and upon selection make the fixed element's position absolute instead of fixed with a top position based on the document's scroll position.

This is however very ugly and still results in some strange back and forth scrolling before landing on the right place, but it is the closest I could get.

Any other solution would involve overriding the default scroll mechanics of the browser.

Gorges answered 12/3, 2015 at 14:59 Comment(0)
S
0

Haven't dealt with this particular bug, but maybe put an overflow: hidden; on the body when the text area is visible (or just active, depending on your design). This may have the effect of not giving the browser anywhere "down" to scroll to.

Sherellsherer answered 12/3, 2015 at 4:45 Comment(1)
I cant even seem to get touchstart to trigger early enough to even consider that hack :(Swinge
B
0

I just jumped over something like this yesterday by setting height of #a to max visible height (body height was in my case) when #b is visible

ex:

    <script>
    document.querySelector('#b').addEventListener('focus', function () {
      document.querySelector('#a').style.height = document.body.clientHeight;
    })
    </script>

ps: sorry for late example, just noticed it was needed.

Biblio answered 12/3, 2015 at 5:50 Comment(2)
Please include a code example to make it clear how your fix can helpHispaniola
@EruPenkman sorry just noticed your comment, hope that helps.Biblio
F
0

A possible solution would be to replace the input field.

  • Monitor click events on a div
  • focus a hidden input field to render the keyboard
  • replicate the content of the hidden input field into the fake input field

function focus() {
  $('#hiddeninput').focus();
}

$(document.body).load(focus);

$('.fakeinput').bind("click",function() {
    focus();
});

$("#hiddeninput").bind("keyup blur", function (){
  $('.fakeinput .placeholder').html(this.value);
});
#hiddeninput {
  position:fixed;
  top:0;left:-100vw;
  opacity:0;
  height:0px;
  width:0;
}
#hiddeninput:focus{
  outline:none;
}
.fakeinput {
  width:80vw;
  margin:15px auto;
  height:38px;
  border:1px solid #000;
  color:#000;
  font-size:18px;
  padding:12px 15px 10px;
  display:block;
  overflow:hidden;
}
.placeholder {
  opacity:0.6;
  vertical-align:middle;
}
<input type="text" id="hiddeninput"></input>

<div class="fakeinput">
    <span class="placeholder">First Name</span>
</div> 

codepen

Forestaysail answered 20/6, 2015 at 4:51 Comment(0)
M
0

None of these solutions worked for me because my DOM is complicated and I have dynamic infinite scroll pages, so I had to create my own.

Background: I am using a fixed header and an element further down that sticks below it once the user scrolls that far down. This element has a search input field. In addition, I have dynamic pages added during forward and backwards scroll.

Problem: In iOS, anytime the user clicked on the input in the fixed element, the browser would scroll all the way to the top of the page. This not only caused undesired behavior, it also triggered my dynamic page add at the top of the page.

Expected Solution: No scroll in iOS (none at all) when the user clicks on the input in the sticky element.

Solution:

     /*Returns a function, that, as long as it continues to be invoked, will not
    be triggered. The function will be called after it stops being called for
    N milliseconds. If `immediate` is passed, trigger the function on the
    leading edge, instead of the trailing.*/
    function debounce(func, wait, immediate) {
        var timeout;
        return function () {
            var context = this, args = arguments;
            var later = function () {
                timeout = null;
                if (!immediate) func.apply(context, args);
            };
            var callNow = immediate && !timeout;
            clearTimeout(timeout);
            timeout = setTimeout(later, wait);
            if (callNow) func.apply(context, args);
        };
    };

     function is_iOS() {
        var iDevices = [
          'iPad Simulator',
          'iPhone Simulator',
          'iPod Simulator',
          'iPad',
          'iPhone',
          'iPod'
        ];
        while (iDevices.length) {
            if (navigator.platform === iDevices.pop()) { return true; }
        }
        return false;
    }

    $(document).on("scrollstop", debounce(function () {
        //console.log("Stopped scrolling!");
        if (is_iOS()) {
            var yScrollPos = $(document).scrollTop();
            if (yScrollPos > 200) { //200 here to offset my fixed header (50px) and top banner (150px)
                $('#searchBarDiv').css('position', 'absolute');
                $('#searchBarDiv').css('top', yScrollPos + 50 + 'px'); //50 for fixed header
            }
            else {
                $('#searchBarDiv').css('position', 'inherit');
            }
        }
    },250,true));

    $(document).on("scrollstart", debounce(function () {
        //console.log("Started scrolling!");
        if (is_iOS()) {
            var yScrollPos = $(document).scrollTop();
            if (yScrollPos > 200) { //200 here to offset my fixed header (50px) and top banner (150px)
                $('#searchBarDiv').css('position', 'fixed');
                $('#searchBarDiv').css('width', '100%');
                $('#searchBarDiv').css('top', '50px'); //50 for fixed header
            }
        }
    },250,true));

Requirements: JQuery mobile is required for the startsroll and stopscroll functions to work.

Debounce is included to smooth out any lag created by the sticky element.

Tested in iOS10.

Membership answered 30/11, 2016 at 0:22 Comment(0)
S
0

This is now fixed in iOS 10.3!

Hacks should no longer be needed.

Swinge answered 12/6, 2017 at 18:22 Comment(4)
Can you point to any release notes that point this out as being fixed?Paleobotany
Apple are very secretive, they closed my bug report I confirmed it now works proper, that is all I got :)Swinge
I still have this issue on iOS 11Hotheaded
No, this is still an issue even on iOS 13.Iodate
A
0

I had the issue, below lines of code resolved it for me -

html{

 overflow: scroll; 
-webkit-overflow-scrolling: touch;

}
Aristippus answered 22/6, 2017 at 12:27 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.