Safari in ios8 is scrolling screen when fixed elements get focus

前端 未结 12 1079
失恋的感觉
失恋的感觉 2020-12-12 11:24

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.

T

相关标签:
12条回答
  • 2020-12-12 11:45

    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.

    0 讨论(0)
  • 2020-12-12 11:49

    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)});
    
    0 讨论(0)
  • 2020-12-12 11:52

    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.

    0 讨论(0)
  • 2020-12-12 11:53

    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.

    0 讨论(0)
  • 2020-12-12 11:54

    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

    0 讨论(0)
  • 2020-12-12 11:56

    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://stackoverflow.com/a/9039885/1611058)

    //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);
        }
    });
    
    0 讨论(0)
提交回复
热议问题