Skip to content

Commit 9327ac7

Browse files
committed
fix(android): when keyboard comes up, ensure input is in view
This requires us to set fullscreen="false" in our cordova apps. Uses the resize event to determine when the keyboard has been shown, then broadcasts an event from the activeElement: 'scrollChildIntoView', which is caught by the nearest parent scrollView. The scrollView will then see if that element is within the new device's height (since the keyboard resizes the screen), and if not scroll it into view. Additionally, when the keyboard resizes the screen we add a `.hide-footer` class to the body, which will hide tabbars and footer bars while the keyboard is opened. For now, this is android only. Closes #314.
1 parent 63b0a31 commit 9327ac7

File tree

9 files changed

+229
-19
lines changed

9 files changed

+229
-19
lines changed

Diff for: config/build.js

+8-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,14 @@ module.exports = {
66
'js/ionic.js',
77

88
// Utils
9-
'js/utils/**/*.js',
9+
'js/utils/animate.js',
10+
'js/utils/dom.js',
11+
'js/utils/events.js',
12+
'js/utils/gestures.js',
13+
'js/utils/platform.js',
14+
'js/utils/poly.js',
15+
'js/utils/utils.js',
16+
'js/utils/keyboard.js',
1017

1118
// Views
1219
'js/views/view.js',

Diff for: js/ext/angular/src/controller/ionicScrollController.js

+9-2
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,7 @@
33

44
angular.module('ionic.ui.scroll')
55

6-
.controller('$ionicScroll', ['$scope', 'scrollViewOptions', '$timeout', '$ionicScrollDelegate',
7-
function($scope, scrollViewOptions, $timeout, $ionicScrollDelegate) {
6+
.controller('$ionicScroll', ['$scope', 'scrollViewOptions', '$timeout', '$ionicScrollDelegate', '$window', function($scope, scrollViewOptions, $timeout, $ionicScrollDelegate, $window) {
87

98
scrollViewOptions.bouncing = angular.isDefined(scrollViewOptions.bouncing) ?
109
scrollViewOptions.bouncing :
@@ -24,6 +23,14 @@ angular.module('ionic.ui.scroll')
2423
//Register delegate for event handling
2524
$ionicScrollDelegate.register($scope, $element, scrollView);
2625

26+
$window.addEventListener('resize', resize);
27+
$scope.$on('$destroy', function() {
28+
$window.removeEventListener('resize', resize);
29+
});
30+
function resize() {
31+
scrollView.resize();
32+
}
33+
2734
$timeout(function() {
2835
scrollView.run();
2936

Diff for: js/ext/angular/test/controller/ionicScrollController.unit.js

+23-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,10 @@ describe('$ionicScroll Controller', function() {
66
function setup(options) {
77
options = options || {};
88

9-
options.el = options.el || document.createElement('div');
9+
options.el = options.el ||
10+
//scrollView requires an outer container element and a child
11+
//content element
12+
angular.element('<div><div></div></div>')[0];
1013

1114
inject(function($controller, $rootScope, $timeout) {
1215
scope = $rootScope.$new();
@@ -41,6 +44,25 @@ describe('$ionicScroll Controller', function() {
4144
expect(ctrl.scrollView.run).toHaveBeenCalled();
4245
});
4346

47+
it('should resize the scrollview on window resize', function() {
48+
setup();
49+
timeout.flush();
50+
spyOn(ctrl.scrollView, 'resize');
51+
ionic.trigger('resize', { target: window });
52+
expect(ctrl.scrollView.resize).toHaveBeenCalled();
53+
});
54+
55+
it('should unbind window event listener on scope destroy', function() {
56+
spyOn(window, 'removeEventListener');
57+
spyOn(window, 'addEventListener');
58+
setup();
59+
expect(window.addEventListener).toHaveBeenCalled();
60+
expect(window.addEventListener.mostRecentCall.args[0]).toBe('resize');
61+
scope.$destroy();
62+
expect(window.removeEventListener).toHaveBeenCalled();
63+
expect(window.removeEventListener.mostRecentCall.args[0]).toBe('resize');
64+
});
65+
4466
it('should register with $ionicScrollDelegate', inject(function($ionicScrollDelegate) {
4567
spyOn($ionicScrollDelegate, 'register');
4668
setup();

Diff for: js/ext/angular/test/list.html

+1
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,7 @@ <h2>Nic Cage</h2>
157157
<i class="icon {{ item.icon }}"></i>
158158
{{ item.text }}
159159
</a>
160+
<input type="text" placeholder="text input">
160161
<div class="item">
161162
<slide-box show-pager="false">
162163
<slide>

Diff for: js/utils/events.js

+10-6
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
*
44
* Author: Max Lynch <[email protected]>
55
*
6-
* Framework events handles various mobile browser events, and
6+
* Framework events handles various mobile browser events, and
77
* detects special events like tap/swipe/etc. and emits them
88
* as custom events that can be used in an app.
99
*
@@ -48,14 +48,18 @@
4848
VIRTUALIZED_EVENTS: ['tap', 'swipe', 'swiperight', 'swipeleft', 'drag', 'hold', 'release'],
4949

5050
// Trigger a new event
51-
trigger: function(eventType, data) {
52-
var event = new CustomEvent(eventType, { detail: data });
51+
trigger: function(eventType, data, bubbles, cancelable) {
52+
var event = new CustomEvent(eventType, {
53+
detail: data,
54+
bubbles: !!bubbles,
55+
cancelable: !!cancelable
56+
});
5357

5458
// Make sure to trigger the event on the given target, or dispatch it from
5559
// the window if we don't have an event target
5660
data && data.target && data.target.dispatchEvent(event) || window.dispatchEvent(event);
5761
},
58-
62+
5963
// Bind an event
6064
on: function(type, callback, element) {
6165
var e = element || window;
@@ -92,8 +96,8 @@
9296
handlePopState: function(event) {
9397
},
9498
};
95-
96-
99+
100+
97101
// Map some convenient top-level functions for event handling
98102
ionic.on = function() { ionic.EventController.on.apply(ionic.EventController, arguments); };
99103
ionic.off = function() { ionic.EventController.off.apply(ionic.EventController, arguments); };

Diff for: js/utils/keyboard.js

+52
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
(function(ionic) {
2+
3+
ionic.Platform.ready(function() {
4+
if (ionic.Platform.is('android')) {
5+
androidKeyboardFix();
6+
}
7+
});
8+
9+
function androidKeyboardFix() {
10+
var rememberedDeviceWidth = window.innerWidth;
11+
var rememberedDeviceHeight = window.innerHeight;
12+
var keyboardHeight;
13+
14+
window.addEventListener('resize', resize);
15+
16+
function resize() {
17+
18+
//If the width of the window changes, we have an orientation change
19+
if (rememberedDeviceWidth !== window.innerWidth) {
20+
rememberedDeviceWidth = window.innerWidth;
21+
rememberedDeviceHeight = window.innerHeight;
22+
console.info('orientation change. deviceWidth =', rememberedDeviceWidth,
23+
', deviceHeight =', rememberedDeviceHeight);
24+
25+
//If the height changes, and it's less than before, we have a keyboard open
26+
} else if (rememberedDeviceHeight !== window.innerHeight &&
27+
window.innerHeight < rememberedDeviceHeight) {
28+
document.body.classList.add('hide-footer');
29+
//Wait for next frame so document.activeElement is set
30+
window.rAF(handleKeyboardChange);
31+
} else {
32+
//Otherwise we have a keyboard close or a *really* weird resize
33+
document.body.classList.remove('hide-footer');
34+
}
35+
36+
function handleKeyboardChange() {
37+
//keyboard opens
38+
keyboardHeight = rememberedDeviceHeight - window.innerHeight;
39+
var activeEl = document.activeElement;
40+
if (activeEl) {
41+
//This event is caught by the nearest parent scrollView
42+
//of the activeElement
43+
ionic.trigger('scrollChildIntoView', {
44+
target: activeEl
45+
}, true);
46+
}
47+
48+
}
49+
}
50+
}
51+
52+
})(window.ionic);

Diff for: js/views/scrollView.js

+22
Original file line numberDiff line numberDiff line change
@@ -593,6 +593,28 @@ ionic.views.Scroll = ionic.views.View.inherit({
593593
// Event Handler
594594
var container = this.__container;
595595

596+
//Broadcasted when keyboard is shown on some platforms.
597+
//See js/utils/keyboard.js
598+
container.addEventListener('scrollChildIntoView', function(e) {
599+
var deviceHeight = window.innerHeight;
600+
var element = e.target;
601+
var elementHeight = e.target.offsetHeight;
602+
603+
//getBoundingClientRect() will actually give us position relative to the viewport
604+
var elementDeviceTop = element.getBoundingClientRect().top;
605+
var elementScrollTop = ionic.DomUtil.getPositionInParent(element, container).top;
606+
607+
//If the element is positioned under the keyboard...
608+
if (elementDeviceTop + elementHeight > deviceHeight) {
609+
//Put element in middle of visible screen
610+
self.scrollTo(0, elementScrollTop + elementHeight - (deviceHeight * 0.5), true);
611+
}
612+
613+
//Only the first scrollView parent of the element that broadcasted this event
614+
//(the active element that needs to be shown) should receive this event
615+
e.stopPropagation();
616+
});
617+
596618
if ('ontouchstart' in window) {
597619

598620
container.addEventListener("touchstart", function(e) {

Diff for: scss/_util.scss

+20-9
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@
44
* --------------------------------------------------
55
*/
66

7-
.hidden,
8-
.hide {
9-
display: none;
7+
.hidden,
8+
.hide {
9+
display: none;
1010
}
1111
.show {
1212
display: block;
@@ -15,6 +15,17 @@
1515
visibility: hidden;
1616
}
1717

18+
.hide-footer {
19+
.bar-footer,
20+
.tabs {
21+
display: none;
22+
}
23+
.has-footer,
24+
.has-tabs {
25+
bottom: 0;
26+
}
27+
}
28+
1829
.inline {
1930
display: inline-block;
2031
}
@@ -30,10 +41,10 @@
3041
.block {
3142
display: block;
3243
clear: both;
33-
&:after {
34-
display: block;
35-
visibility: hidden;
36-
clear: both;
44+
&:after {
45+
display: block;
46+
visibility: hidden;
47+
clear: both;
3748
height: 0;
3849
content: ".";
3950
}
@@ -101,8 +112,8 @@
101112
/**
102113
* Utility Colors
103114
* --------------------------------------------------
104-
* Utility colors are added to help set a naming convention. You'll
105-
* notice we purposely do not use words like "red" or "blue", but
115+
* Utility colors are added to help set a naming convention. You'll
116+
* notice we purposely do not use words like "red" or "blue", but
106117
* instead have colors which represent an emotion or generic theme.
107118
*/
108119

Diff for: test/inputs.html

+84
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
<html ng-app="navTest">
2+
<head>
3+
<meta charset="utf-8">
4+
<title>List</title>
5+
6+
<!-- Sets initial viewport load and disables zooming -->
7+
<meta name="viewport" content="initial-scale=1, maximum-scale=1, user-scalable=no">
8+
<link rel="stylesheet" href="lib/css/ionic.css">
9+
<script src="lib/js/ionic.bundle.js"></script>
10+
</head>
11+
<body>
12+
13+
<pane>
14+
15+
<header class="bar bar-header bar-positive">
16+
<div class="buttons">
17+
<button ng-click="toggleDelete()" class="button button-clear">{{ editBtnText }}</button>
18+
</div>
19+
<h1 class="title">List Tests</h1>
20+
<div class="buttons">
21+
<button ng-click="toggleReorder()" class="button button-clear">{{ reorderBtnText }}</button>
22+
</div>
23+
</header>
24+
25+
<footer class="bar bar-footer bar-positive">
26+
<h1 class="title">Footer time!</h1>
27+
</footer>
28+
29+
<content has-header="true" has-footer="true">
30+
<input type="text" placeholder="text me!">
31+
<p style="margin: 10px;">...</p>
32+
<p style="margin: 10px;">...</p>
33+
<p style="margin: 10px;">...</p>
34+
<p style="margin: 10px;">...</p>
35+
<p style="margin: 10px;">...</p>
36+
<input type="text" placeholder="text me!">
37+
<p style="margin: 10px;">...</p>
38+
<p style="margin: 10px;">...</p>
39+
<p style="margin: 10px;">...</p>
40+
<p style="margin: 10px;">...</p>
41+
<p style="margin: 10px;">...</p>
42+
<input type="text" placeholder="text me!">
43+
<p style="margin: 10px;">...</p>
44+
<p style="margin: 10px;">...</p>
45+
<p style="margin: 10px;">...</p>
46+
<p style="margin: 10px;">...</p>
47+
<p style="margin: 10px;">...</p>
48+
<input type="text" placeholder="text me!">
49+
<p style="margin: 10px;">...</p>
50+
<p style="margin: 10px;">...</p>
51+
<p style="margin: 10px;">...</p>
52+
<p style="margin: 10px;">...</p>
53+
<p style="margin: 10px;">...</p>
54+
<input type="text" placeholder="text me!">
55+
<p style="margin: 10px;">...</p>
56+
<p style="margin: 10px;">...</p>
57+
<p style="margin: 10px;">...</p>
58+
<p style="margin: 10px;">...</p>
59+
<p style="margin: 10px;">...</p>
60+
<input type="text" placeholder="text me!">
61+
<p style="margin: 10px;">...</p>
62+
<p style="margin: 10px;">...</p>
63+
<p style="margin: 10px;">...</p>
64+
<p style="margin: 10px;">...</p>
65+
<p style="margin: 10px;">...</p>
66+
<input type="text" placeholder="text me!">
67+
<p style="margin: 10px;">...</p>
68+
<p style="margin: 10px;">...</p>
69+
<p style="margin: 10px;">...</p>
70+
<p style="margin: 10px;">...</p>
71+
<p style="margin: 10px;">...</p>
72+
<input type="text" placeholder="text me!">
73+
<p style="margin: 10px;">...</p>
74+
<p style="margin: 10px;">...</p>
75+
<p style="margin: 10px;">...</p>
76+
<p style="margin: 10px;">...</p>
77+
<p style="margin: 10px;">...</p>
78+
<input type="text" placeholder="text me!">
79+
</content>
80+
81+
</pane>
82+
</body>
83+
</html>
84+

0 commit comments

Comments
 (0)