Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP

Loading…

Add iOS support to position:fixed sample code #167

Open
matthewbuchanan opened this Issue · 38 comments
@matthewbuchanan

The sample code provided in the addTest() documentation for detecting support for position:fixed reports a false positive in Mobile Safari. (This browser correctly positions objects with that style applied, but does not keep them fixed in the viewport when the page scrolls.)

@paulirish
Owner

matt,

i'd be curious if you tried out this code:
https://github.com/jquery/jquery/blob/eed380/src/offset.js#L125-143

if it rectifies the iOS false positive.

cheers

@matthewbuchanan

Hi Paul, sorry for the delay. Just tested that code and it returns the same false positive on both iPhone and iPad :(

@paulirish
Owner

Just a note that Marc Grabanski told me he spent 7 hours trying to solve this one without a successful resolution. :/

@torbs
// Tested on iphone with IOS 5 beta and IOS 4

var IOS_FIXED_POSITION;

window.onload = function () { // Should be addEventListener etc...

if (typeof document.body.scrollIntoViewIfNeeded === 'function') {

    (function () {
        var container = document.body,
        el = document.createElement('div'),
        originalHeight = container.style.height, 
        originalScrollTop = window.pageYOffset;
        testScrollTop = 20;
        el.style.cssText = 'position:fixed;top:0px;height:10px;';

        container.appendChild(el);
        container.style.height="3000px";

        window.addEventListener('scroll', testScrollTop, false);
        window.setTimeout(testScroll, 20); // ios 4 does'n publish the scroll event on scrollto
        window.scrollTo(0,testScrollTop );

        function testScroll() {
            if (IOS_FIXED_POSITION === undefined) {
                el.scrollIntoViewIfNeeded();
                if (window.pageYOffset === testScrollTop) {
                    IOS_FIXED_POSITION = true;
                } else {
                    IOS_FIXED_POSITION = false;
                }
            }
            window.removeEventListener('scroll', testScroll, false);
            container.style.height=originalHeight;
            container.removeChild(el);
            window.scrollTo(0,originalScrollTop);
        }

    }());
} else {
    IOS_FIXED_POSITION = false;
}
}
@bobslaede

Fixed it a bit, and used the original positionfixed test from the docs for regular browsers, it could be cleaned up a bit more, but it works.
Tested in Chrome, Firefox, IE9, iOS4, The Android 2.3.3 browser, and Firefox for Android

Modernizr.addTest('iospositionfixed', function () {
    var test  = document.createElement('div'),
        ret,
        fake = false,
        root = document.body || (function () {
            fake = true;
            return document.documentElement.appendChild(document.createElement('body'));
        }());

    if (typeof document.body.scrollIntoViewIfNeeded === 'function') {

        var oldCssText = root.style.cssText,
            testScrollTop = 20,
            originalScrollTop = window.pageYOffset;

        root.appendChild(test);

        test.style.cssText = 'position:fixed;top:0px;height:10px;';

        root.style.height="3000px";

        /* avoided hoisting for clarity */
        var testScroll = function() {
            if (ret === undefined) {
                test.scrollIntoViewIfNeeded();
                if (window.pageYOffset === testScrollTop) {
                    ret = true;
                } else {
                    ret = false;
                }
            }
            window.removeEventListener('scroll', testScroll, false);
        }

        window.addEventListener('scroll', testScrollTop, false);
        window.setTimeout(testScroll, 20); // ios 4 does'nt publish the scroll event on scrollto
        window.scrollTo(0, testScrollTop);
        testScroll();

        root.removeChild(test);
        root.style.cssText = oldCssText;
        window.scrollTo(0, originalScrollTop);

    } else {
        ret = Modernizr.positionfixed; // firefox and IE doesnt have document.body.scrollIntoViewIfNeeded, so we test with the original modernizr test
    }

    if (fake) {
        document.documentElement.removeChild(root);
    }

    return ret;
});

EDIT: Here's a gist of them both: https://gist.github.com/1221602

@torbs

I did the timeout/eventlistener to test on Android devices for scrollIntoViewIfNeeded. If testScroll() is called right after scrollTo() and it works, the timeout and eventlistener is not needed. I will test this later. I don't have an Android available right now.

I found out after publishing my code here, that we need to test further on Android devices with user-scalable set to "yes". My test returns true if the timeout is set to a higher number, but Android doesn't render posistion fixed on scalable viewports.

@bobslaede

Very good to know. My test on android was with user-scalable set to "no".

@torbs

Just tested on an IOS beta 5. The eventlistener is necessary for iPhone IOS5 to return true. Calling testScroll() directly will return false.

@torbs

It is also necesarry to do the test onload.

@paulirish
Owner

torbs, is your code above final?

we should start a pull request if so

@torbs

paulirish, this works for ios:

    var mno = {};
    mno.features = (function () {
        var features = {
            positionFixed: function (callback) {
                if (typeof fixed.vaule === 'undefined') {
                    fixed.callbacks.push(callback);
                } else {
                    callback(fixed.value);
                }
            }
        },
        fixed = {
            value:undefined,
            callbacks:[]
        };

        function runFixedCallbacks() {
            for (var i = 0; i < fixed.callbacks.length;i++) {
                fixed.callbacks[i](fixed.value);
            }
        }

       $(window).load(function () {
           var test  = document.createElement('div'),
               control = test.cloneNode(false),
               root = document.body,
               oldCssText = root.style.cssText;

           if (typeof root.scrollIntoViewIfNeeded === 'function') {

               var testScrollTop = 42,
                   originalScrollTop = window.pageYOffset;

               root.appendChild(test);

               test.style.cssText = 'position:fixed;top:0px;height:10px;width:100%;background:#900';

               root.style.height="3000px";

               /* avoided hoisting for clarity */
               var testScroll = function() {
                   window.removeEventListener('scroll', testScroll, false);
                   if (fixed.value === undefined) {
                       test.scrollIntoViewIfNeeded();
                       fixed.value = (window.pageYOffset === testScrollTop);
                   }

                   root.removeChild(test);
                   root.style.cssText = oldCssText;
                   window.scrollTo(0, originalScrollTop);
                   runFixedCallbacks();
               };

               window.addEventListener('scroll', testScrollTop, false);
               window.scrollTo(0, testScrollTop);
               window.setTimeout(testScroll, 15); // ios 4 doesn't publish the scroll event on scrollto

           } else {
               root.style.cssText = 'padding:0;margin:0';
               test.style.cssText = 'position:fixed;top:42px';

               root.appendChild(test);
               root.appendChild(control);

               fixed.value = test.offsetTop !== control.offsetTop;
               root.removeChild(control);
               root.removeChild(test);
               root.style.cssText = oldCssText;
               runFixedCallbacks();
           }



       });

       return features;
    }());
@jokeyrhyme

So this is part of jQuery 1.7. Cool.

Is it worth programming Modernizr to detect jQuery and use the result from its tests (and vice versa) where they overlap and one is loaded before the other? Or is it faster to mess with the DOM twice over for this feature-detect than it would be to test for an existing result?

@paulirish
Owner

the detect in jquery 1.7 gives a false positive for mobile webkit.

@arxpoetica

BUMP! BUMP! ;)

@paulirish
Owner

if anyone wants to try torb's latest code.. and test it against some ios/android/op mobile devices.. that'd be great.

then we can bring it in.

its all you, rob. :)

@arxpoetica

I sort of half tested it, but achieved !victory.

@lmeurs

torbs code needs a small correction before one can test it, at line 5 it says 'vaule' instead of 'value':

if (typeof fixed.vaule === 'undefined') {

After the correction I tested it on Chrome v16 and the iOS simulator (iOS v5.0 and v4.3), but unfortunately all returned a false value, while iOS v4.3 is the only non-supportive.

@RyanS

So I ran in to a problem where it was ok for position: fixed not to work but if it did I made some layout calculations based on that. There was an issue with browsers that did not support position fixed as they would still effect the document layout rather than acting like a position fixed element.

I was able to write a test that allowed me to see if a position fixed element was taken out of the document flow. I tested on: Mac chrome, FF, and safari, Windows IE, Blackberry Torch, Android 2.2.3 and iOS 5. I don't have easy access to an iOS4 device. Dunno if this helps anyone out or maybe I am being foolish but just thought i'd share: http://jsfiddle.net/s7TUs/4/

@lmeurs

@RyanS: I tested your script on my Android 2.3.3 phone and iOS 4.3 and 5.0 simulator, all resulted in true values, while iOS 4 is not supporting position: fixed

@RyanS

bummer. Thanks for the heads up

@jokeyrhyme

While it worries me, this is actually something I'm currently sniffing the user agent for. It sucks, but it seems really tricky to come up with a true feature-detect for this. Is it possible to feature-detect for iOS via iOS-specific JavaScript features/values (to help guard against spoofed user agents) and then trust the user agent to be reporting the correct version?

How about if we use 2 different tests that together have 100% coverage, and then just detect when it is appropriate to use one test instead of the other?

@lmeurs

The Filament Group published a sort of proof of concept for a similar problem, cross platform support of the CSS overflow property. The partially ua sniffing part of this script might be of any help, see http://filamentgroup.com/lab/overthrow/.

@grandecomplex

You don't need two tests.. let me see if I can do a pull request...

@paulirish
Owner

attention to people who want a pos:fixed test.. mr @grandecomplex has a draft ready in #539 but is stuck with some iOS4 scroll logic. could you take a look and see if we can get this squared away?

@keeyipchan

If anyone's interested, I'm playing with an alternative method that works for ios 4, android 2.2, and desktop chrome. I haven't had time to test on ios 5 yet.
http://jsfiddle.net/2yS4L/embedded/result/
You will need to move the code outside of the jsfiddle environment.

@matthewbuchanan

@Kee-Yip iOS5 supports position fixed but your script appears to report ‘false’ ten times and then set givingUp to ‘true’.

@grandecomplex

The pull request I proposed also works outside of Modernizr. In a nutshell, the problem is Modernizr doesn't like async (including timeouts) stuff.

Do you think that your script can work without timeouts?

@keeyipchan

I'll test it out some more, but sadly I think the scrolling/timeout is necessary. Android, for example, takes roughly 400ms to finish the detection.
I also realize that there are some assumptions with my script, such as the meta viewport and z-indexes, that may not fit everyone's needs.

@arxpoetica

Thoughts on why this has died? Is it because of issue #539 ?

@grandecomplex

Mr. Irish can touch on this more maybe.

I think it died because Modernizr needs async testing ability similar to async tests in jQuery's qunit in order for position fixed to be tested.

Happy new year!

@ChrisWojcik

Am curious if people are using UA-sniffing for this, or are avoiding position:fixed in situations where you'd need some sort of code-branching because they're an "undetectable"?

My (probably naive) takeaway was that in the proposed tests, the "hack" they were using needed to be patched up and became very specific to certain browsers, and then still reported false-positives/negatives.

If a feature-detect is unreliable and would require a lot of maintenance as new browsers/devices come out, aren't you getting most of the same downsides of UA-sniffing anyway?

http://caniuse.com/css-fixed - Appears the support is getting much better in the very latest mobile versions. And I think you could UA-sniff only to "blacklist" the categories of UA's that don't pretty easily using regex similar to how the filament group did as part of their solution. One line and you can knock out a whole group of them (e.g. iOS <= 5).

That probably isn't in-line with Modernizr, but any feature detect just seems like it would have to be "overly-elegant" at best.

@stucox
Owner

@americanyak - the async stuff's being done "properly" in the big AMD re-rub (Modernizr v3.0) - see #622 and #713 - so I guess this is blocked by those.

@ChrisWojcik - tbh there are a few Modernizr tests which give false positives/negatives; in some places, Modernizr covers these with UA sniffing itself. In this particular case, I gather the consensus is that @grandecomplex's test does the trick in all browsers(?) if run after a timeout - but Modernizr doesn't support that properly... yet. If I was desperate to use position: fixed, I'd probably grab his test and wrap it in some code to run it async outside Modernizr and add an appropriate class to <html> if successful - assuming it's just styles that are dependent on the presence of this feature.

In the words of @aFarkas - "innovative frontend development is hacky and always will be hacky!"

@ChrisWojcik

@stucox Thanks for the clarification. I was under the impression that the creator of Modernizr was against including UA sniffing into the library as a rule. (Only because feature-detection is intended to be the alternative). I thought I heard Paul Irish say that at some point, but I may just be misremembering.

The issue I'm working on involves javascript and not just styles, but it's not the most complex thing ever.

@stucox
Owner

It's a last resort really, usually to filter out buggy implementations which can't otherwise be detected.

@patrickkettner

Another reminder, because I keep forgetting myself, @grandecomplex has a pull request for this in #539, and are just waiting for an update to the code style before we finally close out this ancient bug :D

@grandecomplex
@patrickkettner

Oh, sorry, that was a reminder for me, not for you. I keep on forgetting there is an open pull request, spend 10 minutes hacking on it, then remember to scroll up.

@patrickkettner

@grandecomplex hows it goin? :]

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Something went wrong with that request. Please try again.