iscroll
iscroll copied to clipboard
Switching focus breaks scroll position
Hi,
Switching focus from one input field to another, which is outside viewable area, breaks scroll position.
This fiddle illustrates the issue: focus first field (Foo) and switch focus to last field (Qux) by pressing tab
key three times. Observe scrollbar while doing it.
Same happens if you focus programatically. See another fiddle.
Is this known behaviour?
Is this iOS specific or general behavior that you are describing?
General behaviour. Just checked fiddles in OS X Chrome and Firefox -- both suffer from it.
Anyone happen to solve this or made workaround? This stuff really hard to handle without native iScroll solution ...
Here's the workaround that I found. Use at your own risk:
// Listen to tab events. If we tab we need to look to see if we "natively" scrolled, in which case we need to
// reset the native scroll and use iscroll to restore our position.
element.on('keyup', function(e) {
var keyCode = e.keyCode || e.which;
if (keyCode == 9) {
setTimeout(function() {
var oldScrollY = element.scrollTop(), oldScrollX = element.scrollLeft();
if(oldScrollY || oldScrollX) {
element.scrollTop(0);
element.scrollLeft(0);
scroller.scrollBy(-oldScrollX, -oldScrollY);
}
}, 0);
}
});
Thanks @physicsrob. This works when moving forward with the focus. But when you move backward using the SHIFT key, it doesn't work.
I propose using scrollToElement() combined with jQuery focus event ... which worked for me.
Some context relative to my case:
- The class
.view-id-timeline_2
is on the DIV that wraps around my iScrollable area. -
data-center-on-timeline
is an attribute that I just happened to have on each item of my content being scrolled. This could have been anid
attribute or anything easy to match using a selector. -
myScroll
is my iScroll instance.
$('.view-id-timeline_2 a').focus(function(e) {
var $topParent = $(this).parents("[data-center-on-timeline]");
myScroll.scrollToElement('[data-center-on-timeline="' + $topParent.attr('data-center-on-timeline') + '"]', null, true, true);
});
This solution works beautifully whether you are tabbing forward or backward.
After some time searching for the culprit, I believe the reason why this is happening is the interference of browser scrolling method in iscroll's functionality. Switching focus (to an not yet visible element inside iscroll wrapper) triggers the wrapper to scroll to that element without using the way the scrolling is actually performed by iscroll, breaking the iScrollVerticalScrollbar positioning (the scrollbar exists, but is scrolled up about 2 miles). If we manually scroll at the bottom of the wrapper, the scrollbar comes into view and the positioning is repaired.
My workaround below (which I tested extensively and which saved my mental health) is based on modifying the iscroll.js
library, adding an event into the _initEvents
function that repairs this nasty bug, by forcing the rebuilding of scrollbars' positioning. Not really brain surgery, but quite effective.
And yes, it handles any scrolling action performed outside normal iscroll ... scrolling, weather triggered by pressing TAB or Shift+TAB or whatever.
_initEvents: function (remove) {
var eventType = remove ? utils.removeEvent : utils.addEvent,
target = this.options.bindToWrapper ? this.wrapper : window;
eventType(window, 'orientationchange', this);
eventType(window, 'resize', this);
if ( this.options.click ) {
eventType(this.wrapper, 'click', this, true);
}
if ( !this.options.disableMouse ) {
eventType(this.wrapper, 'mousedown', this);
eventType(target, 'mousemove', this);
eventType(target, 'mousecancel', this);
eventType(target, 'mouseup', this);
}
if ( utils.hasPointer && !this.options.disablePointer ) {
eventType(this.wrapper, utils.prefixPointerEvent('pointerdown'), this);
eventType(target, utils.prefixPointerEvent('pointermove'), this);
eventType(target, utils.prefixPointerEvent('pointercancel'), this);
eventType(target, utils.prefixPointerEvent('pointerup'), this);
}
if ( utils.hasTouch && !this.options.disableTouch ) {
eventType(this.wrapper, 'touchstart', this);
eventType(target, 'touchmove', this);
eventType(target, 'touchcancel', this);
eventType(target, 'touchend', this);
}
eventType(this.scroller, 'transitionend', this);
eventType(this.scroller, 'webkitTransitionEnd', this);
eventType(this.scroller, 'oTransitionEnd', this);
eventType(this.scroller, 'MSTransitionEnd', this);
/* until this point the function is the original */
/* START WORKAROUND */
function _isDescendant(parent, child) {
var node = child.parentNode;
while (node != null) {
if (node == parent) return true;
node = node.parentNode;
}
return false;
}
var holder = this;
function _preventScrollBug() {
var element = document.activeElement;
if (!_isDescendant(holder.wrapper, element)) return false;
holder.scrollTo(holder.maxScrollX, holder.maxScrollY);
setTimeout(function() {
holder.scrollTo(0, 0);
}, 1)
setTimeout(function() {
if (element != null && element !== document.body) holder.scrollToElement(element, null, null, true);
}, 2);
}
function _scrollTab(e) {
var keyCode = e.keyCode || e.which;
if (keyCode == 9) {
var element = document.activeElement;
if (!_isDescendant(holder.wrapper, element)) return false;
setTimeout(function() {
if (element != null && element !== document.body) holder.scrollToElement(element, null, null, true);
}, 2);
}
}
eventType(this.wrapper, 'scroll', _preventScrollBug);
eventType(this.wrapper, 'keyup', _scrollTab, true);
/* END WORKAROUND */
},