When I was working as a frontend developer for a retailer, developing a web shop, I stumbled upon a tricky problem.
My customer was pretty picky, and didn’t want any edge case flaws.
We had a fixed (or sticky of you supported that) navigation that was always attached from top to bottom.
The nav did only cover about 2/3 of the screen’s width though, so when the user reached the end of the content,
you could see how the background started scrolling instead. The scrolling was leaking upwards to the body.
Note: This has been fixed in iOS 8 but is still a valid defect in iOS 7 and below, as well as Android.
To fix this you need to listen to the touchstart and touchmove events and check where you are in the content and which direction you are scrolling.
When you have reached the bottom of the container and the direction you are scrolling is downwards we simple use preventDefault() to stop the scroll event,
to avoid it leaking to the body content.
The wrapper covers the whole screen so when user scrolls outside of the menu, it will still scroll the menu.
It’s using a semi-transparent color to appear modular.
The content covers the width of the menu.
I also need to mention that this solution is not perfect on Android devices.
If you scroll almost all the way to the bottom, and then scrolling past the bottom without lifting you finger, the scroll will leak with the following error message in the console:
Ignored attempt to cancel a touchmove event with cancelable=false, for example because scrolling is in progress and cannot be interrupted.
A way around this, is to implement a custom scroll like iScroll, this is what we ended up doing, although not optimal, of course.
Here’s the JavaScript fixing the problem, I have explained the code with comments inside the script itself.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 |
$leftNavWrap = $('.leftNavWrapper'); $leftNavContent = $('.leftNavContent'); var enableFix = function () { var scrolling = false, ts = null; //Attach a listener to touchstart so we can get the direction $(document).on('touchstart.scrollable', $leftNavWrap, function (e) { if (!scrolling) { scrolling = true; //If we're at the top we move the scroll 1 pixel to "unlock" it. if (e.currentTarget.scrollTop === 0) { e.currentTarget.scrollTop = 1; } else if (e.currentTarget.scrollHeight === e.currentTarget.scrollTop + e.currentTarget.offsetHeight) { e.currentTarget.scrollTop -= 1; } scrolling = false; } //Save touch event so we can compare it with the next event ts = e.originalEvent.touches[0].clientY; }); //Attach a touchmove listener where we calculate the direction and stop the scrolling if necessary $(document).on('touchmove.scrollable', $leftNavWrap, function (e) { //Get the touch event and compare it with our previous touchstart event. //If the current clientY is greater than the previous one we're moving downwards, otherwise upwards. var te = e.originalEvent.changedTouches[0].clientY, direction = ts > te ? 'down' : 'up'; //Specify wrapper and container $container = $leftNavWrap; $content = $leftNavContent; if ($content.height() <= $container.height()) { //If wrapper height is greater than content height we always prevent the scrolling e.preventDefault(); } else if ($container.scrollTop() <= 1 && direction == 'up') { //If user has scrolled all the way to the top and the direction is up, we prevent the scroll. e.preventDefault(); } else if ($container.scrollTop() > 0 && $container.scrollTop() + $container.height() >= $content.height() - 1 && direction == 'down') { //If user has scrolled all the way to the bottom and the direction is down we also prevent the scroll. e.preventDefault(); } }); } |
Try it out on your device:
https://jonwallsten.com/codepen-examples/example1/
See the Pen RNMrmP by Jon WAllsten (@JonWallsten) on CodePen.
Very nice work. A few issues I have noticed:
1. On a Samsung S5 in Firefox mobile, I am able to induce scrolling of your background using long presses followed by a drag/swipe. I presume this is because the touchmove is not seeing this as a touchmove event, and therefore, not applying preventDefault to halt scrolling of the background. Is there any easy way to deal with this?
2. On a Samsung S5 in Chrome mobile, I see what you mean about scrolling near the bottom (I don’t see this in Firefox mobile BTW). I see that the main content continues scrolling and also that the menu does not stay at the bottom but instead rises up (the height of the address bar) and then snaps back down when the touch is released. After applying your code to my site, I don’t have the first problem (CSS differences?), but I do have the menu rise/snap back issue. One thing that I noticed on both your menu and mine is that the address bar hides/reveals before any scrolling on the main content, but hides/reveals after the scrolling (e.g., when the menu has reached the top or bottom) on the menu. I’m thinking that an easy fix to the rise/snap back issue would be to cause the browser to apply the same scroll behavior for the menu as the main content; this should fix it since the address bar will be fully hidden or revealed before the end of the scroll. Any idea how to do this or otherwise address this problem?
Thanks,
Bryan
Hi there
I too have been trying to resolve the ‘scroll leak’ (liking this term as it fits very well). I tested your use case on my Motorola Moto G 4G (2nd gen) on Lollipop and the fix disables scrolling of the main content :s
If I find any resolution to this I will let you know.
Dave