Mobile Webkit reflow issue
Asked Answered
S

6

14

I've been experiencing an issue in mobile versions of webkit (specifically Webkit 534.46 on iOS 5.1.1 as mobile Safari, and now Chrome for iOS) which doesn't happen on any desktop browser that I've seen. (i.e. the demos below should be viewed on a mobile version of webkit.)

Here is a live example of the issue. The core of the CSS is extremely straight forward. It positions an alphabet index along the left of the page:

#index {
    left:0; margin:0; padding:0; position:fixed; top:0; width:3em;
}

The issue happens when an element is fixed position over the top of the body. It is fully able to be interacted with until the scroll changes and then it stops accepting input. If I (manually) jiggle the scroll even one pixel then it becomes active again. The example was kept as simple as possible and does not use any JavaScript. After really hammering on it, I've discovered that it appears that the element thinks it is scrolled but has been visually fixed. In other words, if you click on 'A' then try to click on 'A' again, sometimes you will get a second click in but it will be further down the list. This seemed like a CSS reflow issue to me. I know that mobile webkit attempts to reduce the number of reflows.

Here is a live example of the workaround.

I am able to use JS to force the CSS of the entire document to reflow on scroll (with a throttle which prevents it from happening until 100ms after scrolling) which seems to workaround this issue in the simple example. Unfortunately, this does not help the real world version of this issue.

This is the code for the issue page and the workaround script.

My question is what is happening here and is there a CSS workaround that I am missing? Specifically, I'm curious if any CSS guru can figure out what the layout situation is that prevents the clicks from hitting the correct place on the fixed element? A better understanding might help find a real fix.

Edit: I forgot to mention that the example explicitly forces the viewport to the size of the window. So the user cannot zoom in/out, meaning that the position:fixed should anchor the element to the left side of the window.

Update (2012-09-20): This appears to be fixed in Mobile Safari on iOS 6 (as well as UIWebView). Any workaround should first check to make sure it is on iOS < 6. For example, using CssUserAgent this would look like:

if (parseFloat(cssua.ua.ios) < 6) { /* ... */ }
Shaver answered 2/7, 2012 at 16:31 Comment(2)
I am still going through the various workarounds in the two answers to find the "best". I gave both of you a +1 for the links. I'll award the bounty to whichever actually fixes my issue. Thanks!Shaver
I'm still hunting for a solution which works in my particular case, but the bounty window is running out so I'm going to award it to @Paul Sweatte since he answered first and his links have more solution variations. Thanks everyone!Shaver
S
9

The answer that actually solved my particular issue was a variation of a solution found in one of @Paul Sweatte's links:

Essentially, a plain div which is taller than the body is added. When it is removed, it causes the body to effectively scroll or reflow. Setting the delay to 0ms between adding/removing is enough to allow the DOM to recalculate without causing any flickering. This was the minimal script I could find which fully solved the problem for all position:fixed elements on my particular instance of this issue.

var hack = document.createElement("div");
hack.style.height = "101%";
document.body.appendChild(hack);
setTimeout(function(){
    document.body.removeChild(hack);
    hack = null;
}, 0);
Shaver answered 13/7, 2012 at 21:51 Comment(0)
S
6

Ironically, my original reflow fix (linked to in the question) is now working in my real app, too. Putting a variant of it here in case is useful to anyone else. It can be called on any container element, or if nothing is passed in it reflows the whole document.

var forceReflow = function(elem){
    elem = elem || document.documentElement;

    // force a reflow by increasing size 1px
    var width = elem.style.width,
        px = elem.offsetWidth+1;

    elem.style.width = px+'px';

    setTimeout(function(){
        // undo resize, unfortunately forces another reflow
        elem.style.width = width;
        elem = null;
    }, 0);
};

The nice thing about this is that it doesn't require creating / adding / removing elements, just tweaking the container.

Shaver answered 13/7, 2012 at 22:20 Comment(3)
Do you maybe know how to prevent from showing broken content before reflow? It's visible for a very short time ~50ms, but is noticable for users.Portend
Do you see the broken content even when setTimeout uses a value of 0? If so, you might need to do general optimizations that improve rendering speed of the page. Try some of the suggestions here. Typically what I've seen is people using jQuery ready event to build up their UI which is way slow.Shaver
IIRC, adding a dummy class to an element (e.g., 'foo' + Date.now()) should cause a reflow, and you don't need to remove the class (unless you care about tidiness).Theft
O
2

Looks like this is a known bug:

the core problem is: if the page moves programatically (i.e. the user didn’t cause the scroll) the elements inside the fix element are unavailable.

Use absolute positioning, change the markup, or use one of the hybrid workarounds.

Opine answered 10/7, 2012 at 22:46 Comment(1)
Note that the example I posted specifically does not use JavaScript to scroll. So this is purely a CSS issue.Shaver
F
2

My install of iWebInspector is pretty busted right now, but after messing around with jsfiddle and the iOS sim it seems like your hunch is correct - despite being position:fixed, the browser thinks the page has scrolled, and screws up the click targets.

It looks a lot like this is the same issue as iOS Safari: Anchors within a fixed positioned element only work once, which also hasn't been solved with pure CSS. Also related: Fixed position navbar only clickable once in Mobile Safari on iOS5.

Tangentially, and I'm sure it's been noticed already, it's not possible to scroll the left side, so on an iPhone the index only shows A-M.

Fisticuffs answered 11/7, 2012 at 22:7 Comment(1)
Thanks. Yes I didn't bother making the left side responsive layout, I tried to remove everything that wasn't related to the bug.Shaver
H
0

I believe this is better, and achieves the same effect, allowing links to be clickable in fixed footers. Somehow, doing urlbar hiding causes links in the fixed footer to be unclickable until you scroll a little bit. I have seen this too when focusing inputs, and I attach an event handler to all focus events to fire this off as well. I do this with dojo to attach the events.

        if(navigator.userAgent.match(/iPhone/i)){
        /* The famous iOS can't-click-links until touch fix, I attach onfocus */
            query('input,textarea,select', this.domNode).on('focus', function(el){
                document.documentElement.style.paddingRight = '1px';
                setTimeout(function () {
                document.documentElement.style.paddingRight = '';
                }, 0);
            });
        }
Heady answered 27/8, 2012 at 19:15 Comment(1)
Better than what? It is the same solution as my original but with hard-coded values. What needs to happen is forcing the reflow.Shaver
A
0

Here's a variation of McKamey's workaround. It avoids reflowing twice, and may help with flickering (depending on your app):

setTimeout(function(){
    document.body.style.borderBottom = 
        document.body.style.borderBottom === 'none' ? '1px solid white' : 'none';
}, 0);
Anemophilous answered 10/9, 2012 at 9:16 Comment(2)
Don't you still need to remove the white line (esp. on a non-white background)? That would be your 2nd reflow.Shaver
You can set the color to transparent on a non-white background to avoid a second reflow.Anemophilous

© 2022 - 2024 — McMap. All rights reserved.