refactor(keyboard): Scroll to inputs hidden by keyboard

This commit is contained in:
Adam Bradley
2014-04-24 20:22:16 -05:00
parent f3a0ee7bd4
commit ae3318a081
12 changed files with 1904 additions and 153 deletions

View File

@@ -42,6 +42,7 @@ module.exports = {
'js/utils/activator.js',
'js/utils/utils.js',
'js/utils/keyboard.js',
'js/utils/viewport.js',
// Views
'js/views/view.js',

View File

@@ -73,7 +73,7 @@
// Make sure to trigger the event on the given target, or dispatch it from
// the window if we don't have an event target
data && data.target && data.target.dispatchEvent(event) || window.dispatchEvent(event);
data && data.target && data.target.dispatchEvent && data.target.dispatchEvent(event) || window.dispatchEvent(event);
},
/**

View File

@@ -1,84 +1,214 @@
(function(ionic) {
/*
IONIC KEYBOARD
---------------
*/
var keyboardViewportHeight = window.innerHeight;
var keyboardIsOpen;
var keyboardActiveElement;
var keyboardFocusOutTimer;
var keyboardFocusInTimer;
var KEYBOARD_OPEN_CSS = 'keyboard-open';
var SCROLL_CONTAINER_CSS = 'scroll';
ionic.keyboard = {
isOpen: false,
height: null
};
function keyboardInit() {
if( keyboardHasPlugin() ) {
window.addEventListener('native.showkeyboard', keyboardNativeShow);
}
document.body.addEventListener('ionic.focusin', keyboardBrowserFocusIn);
document.body.addEventListener('focusin', keyboardBrowserFocusIn);
document.body.addEventListener('focusout', keyboardFocusOut);
document.body.addEventListener('orientationchange', keyboardOrientationChange);
document.removeEventListener('touchstart', keyboardInit);
}
function keyboardNativeShow(e) {
ionic.keyboard.height = e.keyboardHeight;
}
function keyboardBrowserFocusIn(e) {
if( !e.target || !ionic.tap.isTextInput(e.target) || !keyboardIsWithinScroll(e.target) ) return;
document.addEventListener('keydown', keyboardOnKeyDown, false);
document.body.scrollTop = 0;
document.body.querySelector('.scroll-content').scrollTop = 0;
keyboardActiveElement = e.target;
keyboardSetShow(e);
}
function keyboardSetShow(e) {
clearTimeout(keyboardFocusInTimer);
clearTimeout(keyboardFocusOutTimer);
keyboardFocusInTimer = setTimeout(function(){
var keyboardHeight = keyboardGetHeight();
var elementBounds = keyboardActiveElement.getBoundingClientRect();
keyboardShow(e.target, elementBounds.top, elementBounds.bottom, keyboardViewportHeight, keyboardHeight);
}, 32);
}
function keyboardShow(element, elementTop, elementBottom, viewportHeight, keyboardHeight) {
var details = {
target: element,
elementTop: Math.round(elementTop),
elementBottom: Math.round(elementBottom),
keyboardHeight: keyboardHeight
};
if( keyboardIsOverWebView() ) {
// keyboard sits on top of the view, but doesn't adjust the view's height
// lower the content height by subtracting the keyboard height from the view height
details.contentHeight = viewportHeight - keyboardHeight;
} else {
// view's height was shrunk down and the keyboard takes up the space the view doesn't fill
// do not add extra padding at the bottom of the scroll view, native already did that
details.contentHeight = viewportHeight;
}
console.debug('keyboardShow', keyboardHeight, details.contentHeight);
// distance from top of input to the top of the keyboard
details.keyboardTopOffset = details.elementTop - details.contentHeight;
console.debug('keyboardTopOffset', details.elementTop, details.contentHeight, details.keyboardTopOffset);
// figure out if the element is under the keyboard
details.isElementUnderKeyboard = (details.elementBottom > details.contentHeight);
ionic.keyboard.isOpen = true;
// send event so the scroll view adjusts
keyboardActiveElement = element;
ionic.trigger('scrollChildIntoView', details, true);
ionic.requestAnimationFrame(function(){
document.body.classList.add(KEYBOARD_OPEN_CSS);
});
// 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()
document.addEventListener('touchmove', keyboardPreventDefault, false);
return details;
}
function keyboardFocusOut(e) {
clearTimeout(keyboardFocusInTimer);
clearTimeout(keyboardFocusOutTimer);
keyboardFocusOutTimer = setTimeout(keyboardHide, 350);
}
function keyboardHide() {
console.debug('keyboardHide');
ionic.keyboard.isOpen = false;
ionic.trigger('resetScrollView', {
target: keyboardActiveElement
}, true);
ionic.requestAnimationFrame(function(){
document.body.classList.remove(KEYBOARD_OPEN_CSS);
});
// the keyboard is gone now, remove the touchmove that disables native scroll
document.removeEventListener('touchmove', keyboardPreventDefault);
document.removeEventListener('keydown', keyboardOnKeyDown);
}
function keyboardUpdateViewportHeight() {
if( window.innerHeight > keyboardViewportHeight ) {
keyboardViewportHeight = window.innerHeight;
}
}
function keyboardOnKeyDown(e) {
if( ionic.scroll.isScrolling ) {
keyboardPreventDefault(e);
}
}
function keyboardPreventDefault(e) {
e.preventDefault();
}
function keyboardOrientationChange() {
keyboardViewportHeight = window.innerHeight;
setTimeout(function(){
keyboardViewportHeight = window.innerHeight;
}, 999);
}
function keyboardGetHeight() {
// check if we are already have a keyboard height from the plugin
if (ionic.keyboard.height ) {
return ionic.keyboard.height;
}
// fallback for when its the webview without the plugin
// or for just the standard web browser
if( ionic.Platform.isIOS() ) {
if( ionic.Platform.isWebView() ) {
return 260;
}
return 216;
} else if( ionic.Platform.isAndroid() ) {
if( ionic.Platform.isWebView() ) {
return 220;
}
if( ionic.Platform.version() <= 4.3) {
return 230;
}
}
// safe guess
return 275;
}
function keyboardIsWithinScroll(ele) {
while(ele) {
if(ele.classList.contains(SCROLL_CONTAINER_CSS)) {
return true;
}
ele = ele.parentElement;
}
return false;
}
function keyboardIsOverWebView() {
return ( ionic.Platform.isIOS() ) ||
( ionic.Platform.isAndroid() && !ionic.Platform.isWebView() );
}
function keyboardHasPlugin() {
return !!(window.cordova && cordova.plugins && cordova.plugins.Keyboard);
}
ionic.Platform.ready(function() {
var rememberedDeviceWidth = window.innerWidth;
var rememberedDeviceHeight = window.innerHeight;
var keyboardHeight;
var rememberedActiveEl;
var alreadyOpen = false;
keyboardUpdateViewportHeight();
window.addEventListener('focusin', onBrowserFocusIn);
if(ionic.Platform.isWebView() && window.cordova && cordova.plugins && cordova.plugins.Keyboard) {
window.addEventListener('native.showkeyboard', onNativeKeyboardShow);
window.addEventListener('native.hidekeyboard', onNativeKeyboardHide);
} else if (ionic.Platform.isAndroid()){
window.addEventListener('resize', onBrowserResize);
}
function onBrowserFocusIn(e) {
if (ionic.tap.containsOrIsTextInput(e.target) || e.srcElement.isContentEditable){
document.body.scrollTop = 0;
}
rememberedActiveEl = e.srcElement;
}
function onBrowserResize() {
if(rememberedDeviceWidth !== window.innerWidth) {
// If the width of the window changes, we have an orientation change
rememberedDeviceWidth = window.innerWidth;
rememberedDeviceHeight = window.innerHeight;
} else if(rememberedDeviceHeight !== window.innerHeight &&
window.innerHeight < rememberedDeviceHeight) {
// If the height changes, and it's less than before, we have a keyboard open
document.body.classList.add('keyboard-open');
keyboardHeight = rememberedDeviceHeight - window.innerHeight;
setTimeout(function() {
ionic.trigger('scrollChildIntoView', {
target: rememberedActiveEl,
}, true);
}, 100);
} else {
// Otherwise we have a keyboard close or a *really* weird resize
document.body.classList.remove('keyboard-open');
}
}
function onNativeKeyboardShow(e) {
if(rememberedActiveEl) {
// This event is caught by the nearest parent scrollView
// of the activeElement
if(cordova.plugins.Keyboard.isVisible) {
document.body.classList.add('keyboard-open');
ionic.trigger('scrollChildIntoView', {
keyboardHeight: e.keyboardHeight,
target: rememberedActiveEl,
firstKeyboardShow: !alreadyOpen
}, true);
if(!alreadyOpen) alreadyOpen = true;
}
}
}
function onNativeKeyboardHide() {
// wait to see if we're just switching inputs
setTimeout(function() {
if(!cordova.plugins.Keyboard.isVisible) {
document.body.classList.remove('keyboard-open');
alreadyOpen = false;
ionic.trigger('resetScrollView', {
target: rememberedActiveEl
}, true);
}
}, 100);
}
// Android sometimes reports bad innerHeight on window.load
// try it again in a lil bit to play it safe
setTimeout(keyboardUpdateViewportHeight, 999);
// only initialize the adjustments for the virtual keyboard
// if a touchstart event happens
document.addEventListener('touchstart', keyboardInit, false);
});
})(window.ionic);

View File

@@ -92,7 +92,8 @@ ionic.tap = {
isTextInput: function(ele) {
return !!ele &&
(ele.tagName == 'TEXTAREA' ||
(ele.tagName == 'INPUT' && !(/radio|checkbox|range|file|submit|reset/i).test(ele.type)));
ele.contentEditable === 'true' ||
(ele.tagName == 'INPUT' && !(/radio|checkbox|range|file|submit|reset/i).test(ele.type)) );
},
isLabelWithTextInput: function(ele) {
@@ -106,7 +107,7 @@ ionic.tap = {
return ionic.tap.isTextInput(ele) || ionic.tap.isLabelWithTextInput(ele);
},
cloneFocusedInput: function(container, instance) {
cloneFocusedInput: function(container, scrollIntance) {
if(ionic.tap.hasCheckedClone) return;
ionic.tap.hasCheckedClone = true;
@@ -119,6 +120,7 @@ ionic.tap = {
clonedInput.type = focusInput.type;
clonedInput.value = focusInput.value;
clonedInput.className = 'cloned-text-input';
clonedInput.readOnly = true;
focusInput.parentElement.insertBefore(clonedInput, focusInput);
focusInput.style.top = focusInput.offsetTop;
focusInput.classList.add('previous-input-focus');
@@ -129,7 +131,7 @@ ionic.tap = {
hasCheckedClone: false,
removeClonedInputs: function(container) {
removeClonedInputs: function(container, scrollIntance) {
ionic.tap.hasCheckedClone = false;
ionic.requestAnimationFrame(function(){
@@ -184,6 +186,11 @@ function triggerMouseEvent(type, ele, x, y) {
}
function tapClickGateKeeper(e) {
if(e.target.type == 'submit' && e.detail === 0) {
// do not prevent click if it came from an "Enter" or "Go" keypress submit
return;
}
// do not allow through any click events that were not created by ionic.tap
if( (ionic.scroll.isScrolling && ionic.tap.containsOrIsTextInput(e.target) ) ||
(!e.isIonicTap && !tapRequiresNativeClick(e.target)) ) {
@@ -241,6 +248,12 @@ function tapMouseDown(e) {
}
function tapMouseUp(e) {
if(tapEnabledTouchEvents) {
e.stopPropagation();
e.preventDefault();
return false;
}
if( tapIgnoreEvent(e) ) return;
if( !tapHasPointerMoved(e) ) {
@@ -272,6 +285,19 @@ function tapTouchStart(e) {
tapEventListener('touchmove');
ionic.activator.start(e);
if( ionic.Platform.isIOS() && ionic.tap.isLabelWithTextInput(e.target) ) {
// if the tapped element is a label, which has a child input
// then preventDefault so iOS doesn't ugly auto scroll to the input
// but do not prevent default on Android or else you cannot move the text caret
// and do not prevent default on Android or else no virtual keyboard shows up
var textInput = tapTargetElement( tapContainingElement(e.target) );
if( textInput !== tapActiveEle ) {
// don't preventDefault on an already focused input or else iOS's text caret isn't usable
e.preventDefault();
}
}
}
function tapTouchEnd(e) {
@@ -301,17 +327,11 @@ function tapTouchCancel(e) {
}
function tapEnableTouchEvents() {
if(!tapEnabledTouchEvents) {
tapEventListener('mouseup', false);
tapEnabledTouchEvents = true;
}
tapEnabledTouchEvents = true;
clearTimeout(tapMouseResetTimer);
tapMouseResetTimer = setTimeout(tapResetMouseEvent, 2500);
}
function tapResetMouseEvent() {
tapEventListener('mouseup', false);
tapEnabledTouchEvents = false;
tapMouseResetTimer = setTimeout(function(){
tapEnabledTouchEvents = false;
}, 2000);
}
function tapIgnoreEvent(e) {
@@ -327,25 +347,35 @@ function tapIgnoreEvent(e) {
function tapHandleFocus(ele) {
tapTouchFocusedInput = null;
var triggerFocusIn = false;
if(ele.tagName == 'SELECT') {
// trick to force Android options to show up
console.debug('tapHandleFocus', ele.tagName);
triggerMouseEvent('mousedown', ele, 0, 0);
tapActiveElement(ele);
ele.focus && ele.focus();
triggerFocusIn = true;
} else if(tapActiveElement() !== ele) {
if( (/input|textarea/i).test(ele.tagName) ) {
console.debug('tapHandleFocus', ele.tagName, ele.id);
tapActiveElement(ele);
ele.focus && ele.focus();
ele.value = ele.value;
if( tapEnabledTouchEvents ) {
tapTouchFocusedInput = ele;
}
} else {
tapFocusOutActive();
} else if(tapActiveElement() === ele) {
// already is the active element and has focus
triggerFocusIn = true;
} else if( (/input|textarea/i).test(ele.tagName) ) {
triggerFocusIn = true;
ele.focus && ele.focus();
ele.value = ele.value;
if( tapEnabledTouchEvents ) {
tapTouchFocusedInput = ele;
}
} else {
tapFocusOutActive();
}
if(triggerFocusIn) {
tapActiveElement(ele);
ionic.trigger('ionic.focusin', {
target: ele
}, true);
}
}
@@ -439,7 +469,5 @@ function tapTargetElement(ele) {
}
ionic.DomUtil.ready(function(){
ionic.tap.register(document.body);
ionic.tap.register(document);
});

57
js/utils/viewport.js Normal file
View File

@@ -0,0 +1,57 @@
var viewportTag;
var viewportProperties = {};
function viewportLoadTag() {
var x;
for(x=0; x<document.head.children.length; x++) {
if(document.head.children[x].name == 'viewport') {
viewportTag = document.head.children[x];
break;
}
}
if(viewportTag) {
var props = viewportTag.content.toLowerCase().replace(/\s+/g, '').split(',');
var keyValue;
for(x=0; x<props.length; x++) {
keyValue = props[x].split('=');
if(keyValue.length == 2) viewportProperties[ keyValue[0] ] = keyValue[1];
}
viewportInitWebView();
}
}
function viewportInitWebView() {
var hasViewportChange = false;
if( ionic.Platform.isWebView() ) {
if( viewportProperties.height != 'device-height' ) {
viewportProperties.height = 'device-height';
hasViewportChange = true;
}
} else if( viewportProperties.height ) {
delete viewportProperties.height;
hasViewportChange = true;
}
if(hasViewportChange) viewportUpdate();
}
function viewportUpdate(updates) {
if(!viewportTag) return;
ionic.Utils.extend(viewportProperties, updates);
var key, props = [];
for(key in viewportProperties) {
if(viewportProperties[key]) props.push(key + '=' + viewportProperties[key]);
}
viewportTag.content = props.join(',');
}
ionic.DomUtil.ready(function() {
viewportLoadTag();
});

View File

@@ -626,50 +626,31 @@ ionic.views.Scroll = ionic.views.View.inherit({
//Broadcasted when keyboard is shown on some platforms.
//See js/utils/keyboard.js
container.addEventListener('scrollChildIntoView', function(e) {
var keyboardHeight = e.detail.keyboardHeight || 0;
var deviceHeight = window.innerHeight;
var frameHeight;
if (ionic.Platform.isIOS() && ionic.Platform.version() >= 7.0 || (!ionic.Platform.isWebView() && ionic.Platform.isAndroid())){
frameHeight = deviceHeight;
}
else {
frameHeight = deviceHeight - keyboardHeight;
}
var element = e.target;
//getBoundingClientRect() will give us position relative to the viewport
var elementDeviceBottom = element.getBoundingClientRect().bottom;
if (e.detail.firstKeyboardShow){
//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
container.style.height = (container.clientHeight - keyboardHeight) + "px";
if( !self.isScrolledIntoView ) {
// 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
container.style.height = (container.clientHeight - e.detail.keyboardHeight) + "px";
container.style.overflow = "visible";
self.isScrolledIntoView = true;
//update scroll view
self.resize();
}
//If the element is positioned under the keyboard...
if (elementDeviceBottom > frameHeight) {
if( e.detail.isElementUnderKeyboard ) {
//Put element in middle of visible screen
//Wait for resize() to reset scroll position
ionic.scroll.isScrolling = true;
setTimeout(function(){
//distance from top of input to the top of the keyboard
var keyboardTopOffset = element.getBoundingClientRect().top - frameHeight;
//middle of the scrollview, where we want to scroll to
var scrollViewMidpointOffset = container.clientHeight * 0.5;
var scrollOffset = keyboardTopOffset + scrollViewMidpointOffset;
self.scrollBy(0, scrollOffset, true);
//please someone tell me there's a better way to do this
//wait until input is scrolled into view, then fix focus
setTimeout(function(){
element.value = element.value; //thanks @adambradley 1337h4x
}, 600);
}, 32);
var scrollTop = e.detail.keyboardTopOffset + scrollViewMidpointOffset;
console.debug('scrollChildIntoView', scrollTop);
ionic.tap.cloneFocusedInput(container, self);
self.scrollBy(0, scrollTop, true);
},
(ionic.Platform.isIOS() ? 80 : 350)
);
}
//Only the first scrollView parent of the element that broadcasted this event
@@ -679,9 +660,11 @@ ionic.views.Scroll = ionic.views.View.inherit({
container.addEventListener('resetScrollView', function(e) {
//return scrollview to original height once keyboard has hidden
self.isScrolledIntoView = false;
container.style.height = "";
container.style.overflow = "";
self.resize();
ionic.scroll.isScrolling = false;
});
@@ -737,7 +720,7 @@ ionic.views.Scroll = ionic.views.View.inherit({
// disabled being able to select text on an input
// hide the input which has focus, and show a cloned one that doesn't have focus
self.__isSelectable = false;
ionic.tap.cloneFocusedInput(self.__container);
ionic.tap.cloneFocusedInput(container, self);
}
}
@@ -751,13 +734,13 @@ ionic.views.Scroll = ionic.views.View.inherit({
self.__enableScrollY = true;
if( !self.__isDragging && !self.__isDecelerating && !self.__isAnimating ) {
ionic.tap.removeClonedInputs(self.__container);
ionic.tap.removeClonedInputs(container, self);
}
};
self.options.orgScrollingComplete = self.options.scrollingComplete;
self.options.scrollingComplete = function() {
ionic.tap.removeClonedInputs(self.__container);
ionic.tap.removeClonedInputs(container, self);
self.options.orgScrollingComplete();
};

View File

@@ -261,9 +261,9 @@ textarea {
input[disabled],
select[disabled],
textarea[disabled],
input[readonly],
select[readonly],
textarea[readonly] {
input[readonly]:not(.cloned-text-input),
textarea[readonly]:not(.cloned-text-input),
select[readonly] {
background-color: $input-bg-disabled;
cursor: not-allowed;
}

View File

@@ -2,7 +2,9 @@
<head>
<meta charset="utf-8">
<title>Inputs</title>
<meta name="viewport" content="initial-scale=1, maximum-scale=1, user-scalable=no, width=device-width">
<meta name="viewport" content="initial-scale=1, maximum-scale=1, user-scalable=no, width=device-width, height=device-height">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<link rel="stylesheet" href="../../dist/css/ionic.css">
<style>
input,
@@ -24,21 +26,46 @@
textarea.cloned-text-input {
background: red !important;
}
header .item {
background: lightblue !important;
}
body {
background: purple;
}
.pane {
background: black;
}
.scroll-content {
background: pink;
overflow: hidden !important;
}
.scroll {
background: lightgreen;
}
input,
textarea {
border: 1px solid blue !important;
}
</style>
</head>
<body ng-controller="AppCtrl">
<div id="logs" style="position:fixed; top:0; left:0; z-index:9999; background: #eee; font-size:8px; line-height:10px; display:none; "></div>
<div id="logs" style="position:fixed; top:0; left:0; z-index:9999; background: #eee; font-size:8px; line-height:10px; display:block; "></div>
<ion-view id="view">
<header>
<ion-header-bar>
<label class="item item-input">
<span class="input-label">Header</span>
<input type="text" value="header input">
</label>
</header>
</ion-header-bar>
<ion-content class="has-header padding">
<ion-content class="padding">
<form ng-submit="formSubmit()">
<div class="list">
<label class="item item-input">
@@ -128,10 +155,16 @@
<textarea id="textarea"></textarea>
</label>
<button class="button button-block button-energized" ng-click="openModal()">
<button type="button" class="button button-block button-energized" ng-click="openModal()">
Open Modal
</button>
<button type="submit" class="button button-block button-calm">
Submit!
</button>
</form>
<p>
<a href="clickTests.html">Click Tests</a> -
<a href="tapInputs.html">Tap Inputs</a> -
@@ -220,6 +253,10 @@
focusFirstInput: true
});
$scope.formSubmit = function(){
alert('SUBMIT!');
};
})
.controller('ModalCtrl', function($scope) {
@@ -233,6 +270,11 @@
var timeId;
var consoleDebug = console.debug;
function getTime() {
var d = new Date();
return d.getMilliseconds();
}
console.debug = function() {
index++;
var msg = [];
@@ -240,6 +282,7 @@
for (var i = 0, j = arguments.length; i < j; i++){
msg.push(arguments[i]);
}
//msg.push(getTime());
msg = msg.join(', ');

View File

@@ -0,0 +1,204 @@
/*
Physical Device Testing Scenarios
---------------------------------
- focusing inputs below the keyboard should scroll them into the middle of the view
- focusing inputs that are above the keyboard should not scroll, but still resize the scrollable content area
- focusing inputs should resize the scroll view so the user can scroll to inputs at the bottom of the page
- clicking the label of an input should focus that input
- focusing an input that is mostly offscreen should scroll into view using js scrolling, not the browser scrolling it into view
- focusing an input while another input already has focus should not (visibly) close and re-open the keyboard
- focusing an input that is above the keyboard while another input already has focus should not do anything
- focusing an input that is below the keyboard while another input already has focus should scroll it into view
- the header should not move when an input is focused
- entering an input on a popup or modal should resize and un-resize that scrollview
- opening a popup or a modal while the keyboard is up should un-resize the scrollview before opening the modal or popup
- changing the orientation of the device should not break any of the above^
- quickly tap different text inputs then end up tapping an element that isn't a text input, the scroll resize should go away
- focusing inputs at the bottom of the page should scroll into view normally (broken on iOS 7.0 w/o height meta tag)
- on iOS in safari, shrinking the view should account for the button-bar at the bottom (currently not working)
Tentative:
- height=device-height not needed on iOS 6.1
- height=device-height needed on iOS 7.0 Cordova
** without it, fires 4 resize events when the keyboard comes up, and the scroll view resizes incorrectly, with it, does not fire resize events? **
- height=device-height not needed on iOS 7.1
Tested On
-----------------------
- iOS 7.1 Safari
- iOS 7.1 Cordova
- iOS 7.0 Safari
- iOS 7.0 Cordova
- iOS 6.1 Safari
- iOS 6.1 Cordova
- Android 4.4 Browser
- Android 4.4 Cordova
- Android 4.2 Browser
- Android 4.2 Cordova
Notes:
---------------------------------
iOS 7 keyboard is 216px tall without the accessory bar
iOS 7 keyboard is 260px tall with the accessory bar
*/
describe('Ionic Keyboard', function() {
var window;
beforeEach(inject(function($window) {
window = $window;
window._setTimeout = window.setTimeout;
window.setTimeout = function(){};
_activeElement = null; // the element which has focus
window.cordova = undefined;
window.device = undefined;
ionic.Platform.ua = '';
ionic.Platform.platforms = null;
ionic.Platform.setPlatform('');
ionic.Platform.setVersion('');
ionic.keyboard.isOpen = false;
}));
afterEach(function(){
window.setTimeout = window._setTimeout;
});
it('Should keyboardShow', function(){
var element = document.createElement('textarea');
var elementTop = 100;
var elementBottom = 200;
var keyboardHeight = 200;
var deviceHeight = 500;
var details = keyboardShow(element, elementTop, elementBottom, deviceHeight, keyboardHeight);
expect( details.keyboardHeight ).toEqual(200);
});
it('Should keyboardIsOverWebView()=false if Android and not isWebView', function(){
// Android browser places the keyboard on top of the content and doesn't resize the window
ionic.Platform.setPlatform('Android');
expect( ionic.Platform.isAndroid() ).toEqual(true);
expect( ionic.Platform.isWebView() ).toEqual(false);
expect( ionic.Platform.isIOS() ).toEqual(false);
expect( keyboardIsOverWebView() ).toEqual(true);
});
it('Should keyboardIsOverWebView()=false if Android and isWebView', function(){
// Android webview gets shrunk by cordova and the keyboard fills the gap
ionic.Platform.setPlatform('Android');
window.cordova = {};
expect( ionic.Platform.isAndroid() ).toEqual(true);
expect( ionic.Platform.isWebView() ).toEqual(true);
expect( keyboardIsOverWebView() ).toEqual(false);
});
it('Should keyboardIsOverWebView()=true if iOS 7.0 or greater', function(){
ionic.Platform.setPlatform('iOS');
ionic.Platform.setVersion('7.0');
expect( ionic.Platform.isAndroid() ).toEqual(false);
expect( ionic.Platform.isIOS() ).toEqual(true);
expect( keyboardIsOverWebView() ).toEqual(true);
});
it('Should keyboardIsOverWebView()=true if less than iOS 7.0', function(){
ionic.Platform.setPlatform('iOS');
ionic.Platform.setVersion('6.0');
expect( ionic.Platform.isAndroid() ).toEqual(false);
expect( ionic.Platform.isIOS() ).toEqual(true);
expect( keyboardIsOverWebView() ).toEqual(true);
});
it('Should keyboardHasPlugin', function() {
expect( keyboardHasPlugin() ).toEqual(false);
window.cordova = {};
expect( keyboardHasPlugin() ).toEqual(false);
window.cordova.plugins = {};
expect( keyboardHasPlugin() ).toEqual(false);
window.cordova.plugins.Keyboard = {};
expect( keyboardHasPlugin() ).toEqual(true);
});
it('keyboardGetHeight() should = DEFAULT_KEYBOARD_HEIGHT if no plugin or resized view', function(){
expect( keyboardGetHeight() ).toEqual(275);
});
it('keyboardUpdateViewportHeight() should update when window.innerHeight > keyboardViewportHeight', function(){
window.innerHeight = 460;
keyboardViewportHeight = 320;
keyboardUpdateViewportHeight();
expect( keyboardViewportHeight ).toEqual(460);
});
it('keyboardUpdateViewportHeight() should not update when window.innerHeight < keyboardViewportHeight', function(){
window.innerHeight = 100;
keyboardViewportHeight = 320;
keyboardUpdateViewportHeight();
expect( keyboardViewportHeight ).toEqual(320);
});
it('Should scroll input into view if it is under the keyboard', function(){
var element = document.createElement('textarea');
var elementTop = 300;
var elementBottom = 400;
var keyboardHeight = 200;
var deviceHeight = 260;
var details = keyboardShow(element, elementTop, elementBottom, deviceHeight, keyboardHeight);
expect( details.isElementUnderKeyboard ).toEqual(true);
});
it('Should not scroll input into view if it is not under the keyboard', function(){
var element = document.createElement('textarea');
var elementTop = 100;
var elementBottom = 200;
var keyboardHeight = 200;
var deviceHeight = 500;
var details = keyboardShow(element, elementTop, elementBottom, deviceHeight, keyboardHeight);
expect( details.isElementUnderKeyboard ).toEqual(false);
});
it('Should not subtract the keyboard height from the contentHeight if not keyboardIsOverWebView()', function(){
var element = document.createElement('textarea');
var elementTop = 300;
var elementBottom = 400;
var keyboardHeight = 200;
var deviceHeight = 260;
var details = keyboardShow(element, elementTop, elementBottom, deviceHeight, keyboardHeight);
expect( details.contentHeight ).toEqual(260);
});
it('Should subtract the keyboard height from the contentHeight if keyboardIsOverWebView()', function(){
ionic.Platform.setPlatform('iOS');
ionic.Platform.setVersion('7.1');
var element = document.createElement('textarea');
var elementTop = 300;
var elementBottom = 400;
var keyboardHeight = 200;
var deviceHeight = 568;
var details = keyboardShow(element, elementTop, elementBottom, deviceHeight, keyboardHeight);
expect( details.contentHeight ).toEqual(368);
});
});

1148
test/unit/utils/tap.unit.js Normal file
View File

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,125 @@
/*
iOS 7.1 Cordova with AND without viewport height DOES resize, DOES NOT fire resize event
iOS 7.1 Safari with AND without viewport height DOES NOT resize
iOS 7.0 Cordova with viewport height DOES resize, DOES fire resize event
iOS 7.0 Cordova without viewport height DOES resize, DOES NOT fire resize event
iOS 7.0 Safari with AND without viewport height DOES NOT resize
iOS 6.1 Cordova with AND without viewport height DOES NOT resize
iOS 6.1 Safari without viewport height DOES NOT resize
NOTES:
-iOS 7.1 Safari with viewport height screws up ionic layout
-iOS 7.0 Safari with viewport height, the scroll view does not resize properly on keyboardhide
-iOS 7.0 Cordova without viewport height, scroll view does not resize properly switching inputs at bottom of page
-iOS 6.1 Cordova and Safari don't work well with viewport height
RECOMMENDATIONS:
-iOS 7.1 Cordova no viewport height, keyboard is not over webview
-iOS 7.1 Safari no viewport height, keyboard is over webview
-iOS 7.0 Cordova yes viewport height, keyboard is not over webview
-iOS 7.0 Safari no viewport height, keyboard is over webview
-iOS 6.1 Cordova no viewport height, keyboard is over webview
-iOS 6.1 Safari no viewport height, keyboard is over webview
*/
describe('Ionic Viewport', function() {
var window, vportTag;
beforeEach(inject(function($window) {
window = $window;
window._setTimeout = window.setTimeout;
window.setTimeout = function(){};
_activeElement = null; // the element which has focus
window.cordova = undefined;
window.device = undefined;
ionic.Platform.ua = '';
ionic.Platform.platforms = null;
ionic.Platform.setPlatform('');
ionic.Platform.setVersion('');
ionic.keyboard.isOpen = false;
viewportProperties = {};
vportTag = document.createElement('meta');
vportTag.setAttribute('name', 'viewport');
document.head.appendChild(vportTag);
}));
afterEach(function(){
window.setTimeout = window._setTimeout;
if(vportTag) vportTag.parentNode.removeChild(vportTag);
});
it('Should have height=device-height for iOS 7+ on webview', function(){
ionic.Platform.setPlatform('iOS');
ionic.Platform.setVersion('7.0');
expect( ionic.Platform.isAndroid() ).toEqual(false);
expect( ionic.Platform.isIOS() ).toEqual(true);
//so isWebView() is true
window.cordova = {};
viewportLoadTag();
expect( viewportProperties.height ).toEqual('device-height');
});
it('Should not have height=device-height for iOS 7+ on browser', function(){
ionic.Platform.setPlatform('iOS');
ionic.Platform.setVersion('7.0');
expect( ionic.Platform.isAndroid() ).toEqual(false);
expect( ionic.Platform.isIOS() ).toEqual(true);
viewportLoadTag();
expect( viewportProperties.height ).not.toEqual('device-height');
});
it('Should have height=device-height for Android on webview', function(){
ionic.Platform.setPlatform('Android');
expect( ionic.Platform.isAndroid() ).toEqual(true);
expect( ionic.Platform.isIOS() ).toEqual(false);
//so isWebView() is true
window.cordova = {};
viewportLoadTag();
expect( viewportProperties.height ).toEqual('device-height');
});
it('Should not have height=device-height for Android on browser', function(){
ionic.Platform.setPlatform('Android');
expect( ionic.Platform.isAndroid() ).toEqual(true);
expect( ionic.Platform.isIOS() ).toEqual(false);
viewportLoadTag();
expect( viewportProperties.height ).not.toEqual('device-height');
});
it('Should not re-add height=device-height for webview if its already there', function(){
ionic.Platform.setPlatform('ios');
window.cordova = {};
var originalViewport = ' initial-scale=1, maximum-scale=1, user-scalable=no, width=device-width, height=device-height ';
vportTag.setAttribute('content', originalViewport);
viewportLoadTag();
// if it was changed the spaces would have been removed
expect( vportTag.content ).toEqual(originalViewport);
});
it('Should not update the viewport if its not a webview and height=device-height wasnt already in', function(){
ionic.Platform.setPlatform('ios');
var originalViewport = ' initial-scale=1, maximum-scale=1, user-scalable=no, width=device-width ';
vportTag.setAttribute('content', originalViewport);
viewportLoadTag();
// if it was changed the spaces would have been removed
expect( vportTag.content ).toEqual(originalViewport);
});
});

View File

@@ -34,4 +34,36 @@ describe('Scroll View', function() {
expect(sc.children[1].classList.contains('scroll-bar')).toBe(true);
expect(sc.children[2].classList.contains('scroll-bar')).toBe(true);
});
it('Should resize when the keyboard is showing', function() {
var element = document.createElement('textarea');
s.appendChild(element);
document.body.appendChild(sc);
var sv = new ionic.views.Scroll({
el: sc,
});
var scHeight = 500;
sc.style.height = scHeight + "px";
sc.style.display = "block";
var keyboardHeight = 200;
details = {
contentHeight: 260,
elementBottom: 400,
elementTop: 300,
isElementUnderKeyboard: true,
keyboardHeight: keyboardHeight,
keyboardTopOffset: 40,
target: element,
}
expect( sv.isScrolledIntoView ).toBeFalsy();
ionic.trigger('scrollChildIntoView', details, true);
expect( sv.isScrolledIntoView ).toEqual(true);
expect( sc.style.height ).toEqual(scHeight - keyboardHeight + "px");
expect( sc.clientHeight ).toEqual(scHeight - keyboardHeight);
});
});