Skip to content
This repository

Add iOS support to position:fixed sample code #167

Open
matthewbuchanan opened this Issue December 20, 2010 · 37 comments
Matthew Buchanan

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.)

Paul Irish
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

Matthew Buchanan

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

Paul Irish
Owner

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

Tor Brekke Skjøtskift
// 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;
}
}
Jeppe Dyrby

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

Tor Brekke Skjøtskift

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.

Jeppe Dyrby

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

Tor Brekke Skjøtskift

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

Tor Brekke Skjøtskift

It is also necesarry to do the test onload.

Paul Irish
Owner

torbs, is your code above final?

we should start a pull request if so

Tor Brekke Skjøtskift

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;
    }());
Ron Waldon

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?

Paul Irish
Owner

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

Robert Hall

BUMP! BUMP! ;)

Paul Irish
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. :)

Robert Hall

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

Laurens Meurs

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.

Ryan Szulczewski
RyanS commented March 16, 2012

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/

Laurens Meurs

@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

Ryan Szulczewski
RyanS commented March 17, 2012

bummer. Thanks for the heads up

Ron Waldon

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?

Laurens Meurs

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/.

Alex Grande

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

Paul Irish
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?

Kee-Yip Chan

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.

Matthew Buchanan

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

Alex Grande

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?

Kee-Yip Chan

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.

Robert Hall

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

Alex Grande

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!

Chris Wojcik

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.

Stu Cox
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!"

Chris Wojcik

@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.

Stu Cox
Owner

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

patrick kettner
Collaborator

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

Alex Grande
patrick kettner
Collaborator

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.

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.