Skip to content

Commit

Permalink
fix(scroll): keyboard support for native scroll views
Browse files Browse the repository at this point in the history
Closes #3727. Closes ##3956
  • Loading branch information
tlancina authored and mhartington committed Jun 18, 2015
1 parent d3c3e8c commit a293a23
Show file tree
Hide file tree
Showing 3 changed files with 150 additions and 35 deletions.
40 changes: 17 additions & 23 deletions js/utils/keyboard.js
Original file line number Diff line number Diff line change
Expand Up @@ -310,7 +310,7 @@ function keyboardFocusIn(e) {
if (!e.target ||
e.target.readOnly ||
!ionic.tap.isKeyboardElement(e.target) ||
!(scrollView = inputScrollView(e.target))) {
!(scrollView = ionic.DomUtil.getParentWithClass(e.target, SCROLL_CONTAINER_CSS))) {
keyboardActiveElement = null;
return;
}
Expand All @@ -319,12 +319,23 @@ function keyboardFocusIn(e) {

// if using JS scrolling, undo the effects of native overflow scroll so the
// scroll view is positioned correctly
document.body.scrollTop = 0;
scrollView.scrollTop = 0;
ionic.requestAnimationFrame(function(){
if (!scrollView.classList.contains("overflow-scroll")) {
document.body.scrollTop = 0;
scrollView.scrollTop = 0;
});
ionic.requestAnimationFrame(function(){
document.body.scrollTop = 0;
scrollView.scrollTop = 0;
});

// any showing part of the document that isn't within the scroll the user
// could touchmove and cause some ugly changes to the app, so disable
// any touchmove events while the keyboard is open using e.preventDefault()
if (window.navigator.msPointerEnabled) {
document.addEventListener("MSPointerMove", keyboardPreventDefault, false);
} else {
document.addEventListener('touchmove', keyboardPreventDefault, false);
}
}

if (!ionic.keyboard.isOpen || ionic.keyboard.isClosing) {
ionic.keyboard.isOpening = true;
Expand All @@ -336,14 +347,7 @@ function keyboardFocusIn(e) {
// keyboard
document.addEventListener('keydown', keyboardOnKeyDown, false);

// any showing part of the document that isn't within the scroll the user
// could touchmove and cause some ugly changes to the app, so disable
// any touchmove events while the keyboard is open using e.preventDefault()
if (window.navigator.msPointerEnabled) {
document.addEventListener("MSPointerMove", keyboardPreventDefault, false);
} else {
document.addEventListener('touchmove', keyboardPreventDefault, false);
}


// if we aren't using the plugin and the keyboard isn't open yet, wait for the
// window to resize so we can get an accurate estimate of the keyboard size,
Expand Down Expand Up @@ -725,16 +729,6 @@ function getViewportHeight() {
return windowHeight;
}

function inputScrollView(ele) {
while(ele) {
if (ele.classList.contains(SCROLL_CONTAINER_CSS)) {
return ele;
}
ele = ele.parentElement;
}
return null;
}

function keyboardHasPlugin() {
return !!(window.cordova && cordova.plugins && cordova.plugins.Keyboard);
}
Expand Down
138 changes: 128 additions & 10 deletions js/views/scrollViewNative.js
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,7 @@
// scroll animation loop w/ easing
// credit https://gist.github.com/dezinezync/5487119
var start = Date.now(),
duration = 1000, //milliseconds
duration = 250, //milliseconds
fromY = self.el.scrollTop,
fromX = self.el.scrollLeft;

Expand Down Expand Up @@ -239,6 +239,7 @@

} else {
// done
ionic.tap.removeClonedInputs(self.__container, self);
self.resize();
}
}
Expand Down Expand Up @@ -293,28 +294,144 @@

// Event Handler
var container = self.__container;
// save height when scroll view is shrunk so we don't need to reflow
var scrollViewOffsetHeight;

// should be unnecessary in native scrolling, but keep in case bugs show up
self.scrollChildIntoView = NOOP;
/**
* Shrink the scroll view when the keyboard is up if necessary and if the
* focused input is below the bottom of the shrunk scroll view, scroll it
* into view.
*/
self.scrollChildIntoView = function(e) {
//console.log("scrollChildIntoView at: " + Date.now());

// D
var scrollBottomOffsetToTop = container.getBoundingClientRect().bottom;
// D - A
scrollViewOffsetHeight = container.offsetHeight;
var alreadyShrunk = self.isShrunkForKeyboard;

var isModal = container.parentNode.classList.contains('modal');
// 680px is when the media query for 60% modal width kicks in
var isInsetModal = isModal && window.innerWidth >= 680;

/*
* _______
* |---A---| <- top of scroll view
* | |
* |---B---| <- keyboard
* | C | <- input
* |---D---| <- initial bottom of scroll view
* |___E___| <- bottom of viewport
*
* All commented calculations relative to the top of the viewport (ie E
* is the viewport height, not 0)
*/
if (!alreadyShrunk) {
// shrink scrollview so we can actually scroll if the input is hidden
// if it isn't shrink so we can scroll to inputs under the keyboard
// inset modals won't shrink on Android on their own when the keyboard appears
if ( ionic.Platform.isIOS() || ionic.Platform.isFullScreen || isInsetModal ) {
// if there are things below the scroll view account for them and
// subtract them from the keyboard height when resizing
// E - D E D
var scrollBottomOffsetToBottom = e.detail.viewportHeight - scrollBottomOffsetToTop;

// 0 or D - B if D > B E - B E - D
var keyboardOffset = Math.max(0, e.detail.keyboardHeight - scrollBottomOffsetToBottom);

ionic.requestAnimationFrame(function(){
// D - A or B - A if D > B D - A max(0, D - B)
scrollViewOffsetHeight = scrollViewOffsetHeight - keyboardOffset;
container.style.height = scrollViewOffsetHeight + "px";

//update scroll view
self.resize();
});
}

self.isShrunkForKeyboard = true;
}

/*
* _______
* |---A---| <- top of scroll view
* | * | <- where we want to scroll to
* |--B-D--| <- keyboard, bottom of scroll view
* | C | <- input
* | |
* |___E___| <- bottom of viewport
*
* All commented calculations relative to the top of the viewport (ie E
* is the viewport height, not 0)
*/
// if the element is positioned under the keyboard scroll it into view
if (e.detail.isElementUnderKeyboard) {

ionic.requestAnimationFrame(function(){
// update D if we shrunk
if (self.isShrunkForKeyboard && !alreadyShrunk) {
scrollBottomOffsetToTop = container.getBoundingClientRect().bottom;
}

// middle of the scrollview, this is where we want to scroll to
// (D - A) / 2
var scrollMidpointOffset = scrollViewOffsetHeight * 0.5;
//console.log("container.offsetHeight: " + scrollViewOffsetHeight);

// middle of the input we want to scroll into view
// C
var inputMidpoint = ((e.detail.elementBottom + e.detail.elementTop) / 2);

// distance from middle of input to the bottom of the scroll view
// C - D C D
var inputMidpointOffsetToScrollBottom = inputMidpoint - scrollBottomOffsetToTop;

//C - D + (D - A)/2 C - D (D - A)/ 2
var scrollTop = inputMidpointOffsetToScrollBottom + scrollMidpointOffset;

if ( scrollTop > 0) {
if (ionic.Platform.isIOS()) {
//just shrank scroll view, give it some breathing room before scrolling
setTimeout(function(){
ionic.tap.cloneFocusedInput(container, self);
self.scrollBy(0, scrollTop, true);
self.onScroll();
}, 32);
} else {
self.scrollBy(0, scrollTop, true);
self.onScroll();
}
}
});
}

// Only the first scrollView parent of the element that broadcasted this event
// (the active element that needs to be shown) should receive this event
e.stopPropagation();
};

self.resetScrollView = function() {
//return scrollview to original height once keyboard has hidden
if (self.isScrolledIntoView) {
self.isScrolledIntoView = false;
if (self.isShrunkForKeyboard) {
self.isShrunkForKeyboard = false;
container.style.height = "";
container.style.overflow = "";
self.resize();
ionic.scroll.isScrolling = false;
}
self.resize();
};

container.addEventListener('resetScrollView', self.resetScrollView);
container.addEventListener('scroll', self.onScroll);

//Broadcasted when keyboard is shown on some platforms.
//See js/utils/keyboard.js
container.addEventListener('scrollChildIntoView', self.scrollChildIntoView);
container.addEventListener('resetScrollView', self.resetScrollView);

// Listen on document because container may not have had the last
// keyboardActiveElement, for example after closing a modal with a focused
// input and returning to a previously resized scroll view in an ion-content.
// Since we can only resize scroll views that are currently visible, just resize
// the current scroll view when the keyboard is closed.
document.addEventListener('resetScrollView', self.resetScrollView);
},

__cleanup: function() {
Expand All @@ -336,6 +453,7 @@
delete self.options.el;

self.resize = self.scrollTo = self.onScroll = self.resetScrollView = NOOP;
self.scrollChildIntoView = NOOP;
container = null;
}
});
Expand Down
7 changes: 5 additions & 2 deletions test/unit/views/scrollViewNative.unit.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,16 @@ describe('Scroll View', function() {

it('Should bind to event listeners', function() {
spyOn(sc,'addEventListener');
spyOn(document,'addEventListener');
var sv = new ionic.views.ScrollNative({
el: sc
});

expect(document.addEventListener).toHaveBeenCalled();
expect(document.addEventListener.mostRecentCall.args[0]).toBe('resetScrollView');
expect(sc.addEventListener).toHaveBeenCalled();
expect(sc.addEventListener.callCount).toBe(4);
expect(sc.addEventListener.mostRecentCall.args[0]).toBe('resetScrollView');
expect(sc.addEventListener.callCount).toBe(2);
expect(sc.addEventListener.mostRecentCall.args[0]).toBe('scrollChildIntoView');
});

it('Should remove event listeners on cleanup', function() {
Expand Down

5 comments on commit a293a23

@wedgybo
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Has any of this been tested on the WKWebView? I get some funny behaviour which can be avoided by checking for the WKWebView in keyboardFocusIn and returning false if it's found.

If there's no testing done on WKWebView I'll raise a PR

@mhartington
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With WKWebView still being very unstable, we haven't been testing on it. Feel free to put together a PR for it though.

@wedgybo
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cool. Yea it's pretty wild with inputs. I've found that having ionic just let it do it's thing is better than the jumping about that currently happens. Will put it in now

@yiqinglan
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

native scroll for android does not work after upgraded to 1.0.1, the webview does not respond touch scroll event.
I enabled native scroll instead of Ionic js scrolling to improve scroll performance , it works well with last version,
.config(function($ionicConfigProvider) {
if (!ionic.Platform.isIOS()) {
$ionicConfigProvider.scrolling.jsScrolling(false);
}
})

not sure It's an issue , anyone met the issue ?

@mhartington
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@yiqinglan do you have an example for this? Scrolling works for me in 1.0.1. Please open an issue and provide a codepen example

Please sign in to comment.