text input keyboard focus

This commit is contained in:
Adam Bradley
2015-08-27 14:03:00 -05:00
parent 57a5335282
commit 327c6de125
13 changed files with 289 additions and 73 deletions

View File

@ -20,7 +20,7 @@ export class ParallaxEffect {
elementRef: ElementRef elementRef: ElementRef
) { ) {
this.ele = elementRef.nativeElement; this.ele = elementRef.nativeElement;
this.scroller = this.ele.querySelector('.scroll-content'); this.scroller = this.ele.querySelector('scroll-content');
this.scroller.addEventListener('scroll', (e) => { this.scroller.addEventListener('scroll', (e) => {
//this.counter.innerHTML = e.target.scrollTop; //this.counter.innerHTML = e.target.scrollTop;
dom.raf(() => { dom.raf(() => {

View File

@ -47,7 +47,7 @@
ion-content { ion-content {
background: transparent !important; background: transparent !important;
} }
.scroll-content { scroll-content {
padding-top: 300px; padding-top: 300px;
} }
</style> </style>

View File

@ -10,17 +10,17 @@ ion-content {
display: block; display: block;
flex: 1; flex: 1;
background-color: $content-background-color; background-color: $content-background-color;
}
.scroll-content {
position: absolute; scroll-content {
top: 0; position: absolute;
right: 0; top: 0;
bottom: 0; right: 0;
left: 0; bottom: 0;
overflow-y: auto; left: 0;
overflow-x: hidden; display: block;
-webkit-overflow-scrolling: touch; overflow-y: scroll; // has to be scroll for momentum scrolling, not auto
will-change: scroll-position; overflow-x: hidden;
} -webkit-overflow-scrolling: touch;
will-change: scroll-position;
} }

View File

@ -4,7 +4,7 @@ import {Ion} from '../ion';
import {IonicConfig} from '../../config/config'; import {IonicConfig} from '../../config/config';
import {IonicComponent} from '../../config/annotations'; import {IonicComponent} from '../../config/annotations';
import {ScrollTo} from '../../animations/scroll-to'; import {ScrollTo} from '../../animations/scroll-to';
import {Platform} from '../../platform/platform'; import {hasFocusedTextInput} from '../../util/dom';
@Component({ @Component({
@ -14,11 +14,12 @@ import {Platform} from '../../platform/platform';
] ]
}) })
@View({ @View({
template: '<div class="scroll-content"><ng-content></ng-content></div>' template: '<scroll-content><ng-content></ng-content></scroll-content>'
}) })
export class Content extends Ion { export class Content extends Ion {
constructor(elementRef: ElementRef, config: IonicConfig) { constructor(elementRef: ElementRef, config: IonicConfig) {
super(elementRef, config); super(elementRef, config);
this.scrollPadding = 0;
} }
onIonInit() { onIonInit() {
@ -66,13 +67,13 @@ export class Content extends Ion {
let parentElement = scrollElement.parentElement; let parentElement = scrollElement.parentElement;
return { return {
height: parentElement.offsetHeight, contentHeight: parentElement.offsetHeight,
top: parentElement.offsetTop, contentTop: parentElement.offsetTop,
bottom: parentElement.offsetTop + parentElement.offsetHeight, contentBottom: parentElement.offsetTop + parentElement.offsetHeight,
width: parentElement.offsetWidth, contentWidth: parentElement.offsetWidth,
left: parentElement.offsetLeft, contentLeft: parentElement.offsetLeft,
right: parentElement.offsetLeft + parentElement.offsetWidth, contentRight: parentElement.offsetLeft + parentElement.offsetWidth,
scrollHeight: scrollElement.scrollHeight, scrollHeight: scrollElement.scrollHeight,
scrollTop: scrollElement.scrollTop, scrollTop: scrollElement.scrollTop,
@ -81,8 +82,31 @@ export class Content extends Ion {
scrollWidth: scrollElement.scrollWidth, scrollWidth: scrollElement.scrollWidth,
scrollLeft: scrollElement.scrollLeft, scrollLeft: scrollElement.scrollLeft,
scrollRight: scrollElement.scrollLeft + scrollElement.scrollWidth, scrollRight: scrollElement.scrollLeft + scrollElement.scrollWidth,
}
}
keyboardTop: (Platform.height() * 0.6) addKeyboardPadding(addPadding) {
if (addPadding > this.scrollPadding) {
this.scrollPadding = addPadding;
this.scrollElement.style.paddingBottom = addPadding + 'px';
}
}
pollFocus() {
if (hasFocusedTextInput()) {
this.isPollingFocus = true;
setTimeout(() => {
this.pollFocus();
}, 500);
} else {
this.isPollingFocus = false;
if (this.scrollPadding) {
this.scrollPadding = 0;
this.scrollElement.style.paddingBottom = '';
}
} }
} }

View File

@ -74,7 +74,7 @@ ion-refresher {
} }
} }
} }
.scroll-content.overscroll { scroll-content.overscroll {
overflow: visible; overflow: visible;
} }
/* /*

View File

@ -1,14 +1,14 @@
ion-scroll { ion-scroll {
position: relative; position: relative;
display: block; display: block;
&.scroll-x .scroll-content { &.scroll-x scroll-content {
overflow-x: auto; overflow-x: auto;
} }
&.scroll-y .scroll-content { &.scroll-y scroll-content {
overflow-y: auto; overflow-y: auto;
} }
.scroll-content { scroll-content {
position: absolute; position: absolute;
top: 0; top: 0;
right: 0; right: 0;

View File

@ -20,7 +20,7 @@ import {IonicComponent} from '../../config/annotations';
} }
}) })
@View({ @View({
template: '<div class="scroll-content"><ng-content></ng-content></div>' template: '<scroll-content><ng-content></ng-content></scroll-content>'
}) })
export class Scroll extends Ion { export class Scroll extends Ion {
constructor(elementRef: ElementRef, ionicConfig: IonicConfig) { constructor(elementRef: ElementRef, ionicConfig: IonicConfig) {

View File

@ -2,8 +2,129 @@ import {TextInput} from 'ionic/ionic';
export function run() { export function run() {
it('should not scroll', () => { it('should scroll, top and bottom below safe area, no room to scroll', () => {
let input = new TextInput();
let inputOffsetTop = 350;
let inputOffsetHeight = 35;
let scrollViewDimensions = {
contentTop: 100,
contentHeight: 700,
scrollTop: 30,
scrollHeight: 700
};
let keyboardHeight = 400;
let scrollData = TextInput.getScollData(inputOffsetTop, inputOffsetHeight, scrollViewDimensions, keyboardHeight);
expect(scrollData.scrollAmount).toBe(-178.5);
expect(scrollData.scrollTo).toBe(208.5);
expect(scrollData.scrollPadding).toBe(523.5);
});
it('should scroll, top and bottom below safe area, room to scroll', () => {
let inputOffsetTop = 350;
let inputOffsetHeight = 35;
let scrollViewDimensions = {
contentTop: 100,
contentHeight: 700,
scrollTop: 30,
scrollHeight: 1000
};
let keyboardHeight = 400;
let scrollData = TextInput.getScollData(inputOffsetTop, inputOffsetHeight, scrollViewDimensions, keyboardHeight);
expect(scrollData.scrollAmount).toBe(-178.5);
expect(scrollData.scrollTo).toBe(208.5);
expect(scrollData.scrollPadding).toBe(0);
});
it('should scroll, top above safe', () => {
// Input top within safe area, bottom below safe area, room to scroll
let inputOffsetTop = 100;
let inputOffsetHeight = 33;
let scrollViewDimensions = {
contentTop: 100,
contentHeight: 700,
scrollTop: 250,
scrollHeight: 700
};
let keyboardHeight = 400;
let scrollData = TextInput.getScollData(inputOffsetTop, inputOffsetHeight, scrollViewDimensions, keyboardHeight);
expect(scrollData.scrollAmount).toBe(150);
expect(scrollData.scrollTo).toBe(100);
expect(scrollData.scrollPadding).toBe(0);
});
it('should scroll, top in safe, bottom below safe, below more than top in, not enough padding', () => {
// Input top within safe area, bottom below safe area, room to scroll
let inputOffsetTop = 100;
let inputOffsetHeight = 320;
let scrollViewDimensions = {
contentTop: 100,
contentHeight: 700,
scrollTop: 20,
scrollHeight: 700
};
let keyboardHeight = 400;
let scrollData = TextInput.getScollData(inputOffsetTop, inputOffsetHeight, scrollViewDimensions, keyboardHeight);
expect(scrollData.scrollAmount).toBe(-80);
expect(scrollData.scrollTo).toBe(100);
expect(scrollData.scrollPadding).toBe(523.5);
});
it('should scroll, top in safe, bottom below safe, below more than top in, enough padding', () => {
// Input top within safe area, bottom below safe area, room to scroll
let inputOffsetTop = 20;
let inputOffsetHeight = 330;
let scrollViewDimensions = {
contentTop: 100,
scrollTop: 0,
};
let keyboardHeight = 400;
let scrollData = TextInput.getScollData(inputOffsetTop, inputOffsetHeight, scrollViewDimensions, keyboardHeight);
expect(scrollData.scrollAmount).toBe(-20);
expect(scrollData.scrollTo).toBe(20);
expect(scrollData.scrollPadding).toBe(0);
});
it('should scroll, top in safe, bottom below safe, below less than top in, enough padding', () => {
// Input top within safe area, bottom below safe area, room to scroll
let inputOffsetTop = 250;
let inputOffsetHeight = 80; // goes 30px below safe area
let scrollViewDimensions = {
contentTop: 100,
scrollTop: 0,
};
let keyboardHeight = 400;
let scrollData = TextInput.getScollData(inputOffsetTop, inputOffsetHeight, scrollViewDimensions, keyboardHeight);
expect(scrollData.scrollAmount).toBe(-153.5);
expect(scrollData.scrollTo).toBe(153.5);
expect(scrollData.scrollPadding).toBe(0);
});
it('should not scroll, top in safe, bottom in safe', () => {
// Input top within safe area, bottom within safe area
let inputOffsetTop = 100;
let inputOffsetHeight = 50;
let scrollViewDimensions = {
contentTop: 100,
scrollTop: 0,
keyboardTop: 400
};
let keyboardHeight = 400;
let scrollData = TextInput.getScollData(inputOffsetTop, inputOffsetHeight, scrollViewDimensions, keyboardHeight);
expect(scrollData.noScroll).toBe(true);
}); });
} }

View File

@ -9,6 +9,7 @@ import {IonicApp} from '../app/app';
import {Content} from '../content/content'; import {Content} from '../content/content';
import {ClickBlock} from '../../util/click-block'; import {ClickBlock} from '../../util/click-block';
import * as dom from '../../util/dom'; import * as dom from '../../util/dom';
import {Platform} from '../../platform/platform';
@ -161,16 +162,21 @@ export class TextInput extends Ion {
// find out if text input should be manually scrolled into view // find out if text input should be manually scrolled into view
let ele = this.elementRef.nativeElement; let ele = this.elementRef.nativeElement;
let scrollData = this.getScollData(ele.offsetTop, ele.offsetHeight, scrollView.getDimensions()); let keyboardHeight = this.config.setting('keyboardHeight');
let scrollData = TextInput.getScollData(ele.offsetTop, ele.offsetHeight, scrollView.getDimensions(), keyboardHeight);
if (scrollData.noScroll) { if (scrollData.noScroll) {
// the text input is in a safe position that doesn't require // the text input is in a safe position that doesn't require
// it to be scrolled into view, just set focus now // it to be scrolled into view, just set focus now
return this.setFocus(); return this.setFocus();
} }
// add padding to the bottom of the scroll view (if needed)
scrollView.addKeyboardPadding(scrollData.scrollPadding);
// manually scroll the text input to the top // manually scroll the text input to the top
// do not allow any clicks while it's scrolling // do not allow any clicks while it's scrolling
ClickBlock(true, SCROLL_INTO_VIEW_DURATION + 200); ClickBlock(true, SCROLL_INTO_VIEW_DURATION + 100);
// temporarily move the focus to the focus holder so the browser // temporarily move the focus to the focus holder so the browser
// doesn't freak out while it's trying to get the input in place // doesn't freak out while it's trying to get the input in place
@ -178,7 +184,7 @@ export class TextInput extends Ion {
this.tempFocusMove(); this.tempFocusMove();
// scroll the input into place // scroll the input into place
scrollView.scrollTo(0, scrollData.scrollTo, SCROLL_INTO_VIEW_DURATION, 8).then(() => { scrollView.scrollTo(0, scrollData.scrollTo, SCROLL_INTO_VIEW_DURATION, 6).then(() => {
// the scroll view is in the correct position now // the scroll view is in the correct position now
// give the native text input focus // give the native text input focus
this.setFocus(); this.setFocus();
@ -194,21 +200,22 @@ export class TextInput extends Ion {
} }
getScollData(inputOffsetTop, inputOffsetHeight, scrollViewDimensions) { static getScollData(inputOffsetTop, inputOffsetHeight, scrollViewDimensions, keyboardHeight) {
// compute input's Y values relative to the body // compute input's Y values relative to the body
let inputTop = (inputOffsetTop + scrollViewDimensions.top - scrollViewDimensions.scrollTop); let inputTop = (inputOffsetTop + scrollViewDimensions.contentTop - scrollViewDimensions.scrollTop);
let inputBottom = (inputTop + inputOffsetHeight); let inputBottom = (inputTop + inputOffsetHeight);
// compute the safe area which is the viewable content area when the soft keyboard is up // compute the safe area which is the viewable content area when the soft keyboard is up
let safeAreaTop = (scrollViewDimensions.top - 1); let safeAreaTop = scrollViewDimensions.contentTop;
let safeAreaBottom = scrollViewDimensions.keyboardTop; let safeAreaHeight = Platform.height() - keyboardHeight - safeAreaTop;
let safeAreaHeight = (safeAreaBottom - safeAreaTop); safeAreaHeight /= 2;
let safeAreaBottom = safeAreaTop + safeAreaHeight;
let inputTopWithinSafeArea = (inputTop >= safeAreaTop && inputTop <= safeAreaBottom); let inputTopWithinSafeArea = (inputTop >= safeAreaTop && inputTop <= safeAreaBottom);
let inputTopAboveSafeArea = (inputTop < safeAreaTop);
let inputTopBelowSafeArea = (inputTop > safeAreaBottom);
let inputBottomWithinSafeArea = (inputBottom >= safeAreaTop && inputBottom <= safeAreaBottom); let inputBottomWithinSafeArea = (inputBottom >= safeAreaTop && inputBottom <= safeAreaBottom);
let inputBottomBelowSafeArea = (inputBottom > safeAreaBottom); let inputBottomBelowSafeArea = (inputBottom > safeAreaBottom);
let inputFitsWithinSafeArea = (inputOffsetHeight <= safeAreaHeight);
let distanceInputBottomBelowSafeArea = (safeAreaBottom - inputBottom);
/* /*
Text Input Scroll To Scenarios Text Input Scroll To Scenarios
@ -231,34 +238,74 @@ export class TextInput extends Ion {
// looks like we'll have to do some auto-scrolling // looks like we'll have to do some auto-scrolling
let scrollData = { let scrollData = {
scrollAmount: 0, scrollAmount: 0,
scrollTo: inputOffsetTop, scrollTo: 0,
scrollPadding: 0, scrollPadding: 0,
}; };
if (inputTopWithinSafeArea && inputBottomBelowSafeArea) { if (inputTopBelowSafeArea || inputBottomBelowSafeArea) {
// Input top within safe area, bottom below safe area // Input top and bottom below safe area
let distanceInputTopIntoSafeArea = (safeAreaTop - inputTop); // auto scroll the input up so at least the top of it shows
let distanceInputBottomBelowSafeArea = (inputBottom - safeAreaBottom);
if (distanceInputBottomBelowSafeArea < distanceInputTopIntoSafeArea) { if (safeAreaHeight > inputOffsetHeight) {
// the input's top is farther into the safe area then the bottom is out of it // safe area height is taller than the input height, so we
// this means we can scroll it up a little bit and the top will still be // can bring it up the input just enough to show the input bottom
// within the safe area scrollData.scrollAmount = (safeAreaBottom - inputBottom);
scrollData.scrollAmount = distanceInputTopIntoSafeArea * -1;
} else { } else {
// the input's top is less below the safe area top than the // safe area height is smaller than the input height, so we can
// input's bottom is below the safe area bottom. So scroll the input // only scroll it up so the input top is at the top of the safe area
// to be at the top of the safe area, knowing that the bottom will go below // however the input bottom will be below the safe area
scrollData.scrollAmount = distanceInputTopIntoSafeArea * -1; scrollData.scrollAmount = (safeAreaTop - inputTop);
} }
} else if (inputTopAboveSafeArea) {
// Input top above safe area
// auto scroll the input down so at least the top of it shows
scrollData.scrollAmount = (safeAreaTop - inputTop);
} }
scrollData.scrollTo = (inputOffsetTop + scrollData.scrollAmount); // figure out where it should scroll to for the best position to the input
scrollData.scrollTo = (scrollViewDimensions.scrollTop - scrollData.scrollAmount);
if (scrollData.scrollAmount < 0) {
// when auto-scrolling up, there also needs to be enough
// content padding at the bottom of the scroll view
// manually add it if there isn't enough scrollable area
// figure out how many scrollable area is left to scroll up
let availablePadding = (scrollViewDimensions.scrollHeight - scrollViewDimensions.scrollTop) - scrollViewDimensions.contentHeight;
let paddingSpace = availablePadding + scrollData.scrollAmount;
if (paddingSpace < 0) {
// there's not enough scrollable area at the bottom, so manually add more
scrollData.scrollPadding = (scrollViewDimensions.contentHeight - safeAreaHeight);
}
}
// if (!window.safeAreaEle) {
// window.safeAreaEle = document.createElement('div');
// window.safeAreaEle.style.position = 'absolute';
// window.safeAreaEle.style.background = 'rgba(0, 128, 0, 0.3)';
// window.safeAreaEle.style.padding = '10px';
// window.safeAreaEle.style.textShadow = '2px 2px white';
// window.safeAreaEle.style.left = '0px';
// window.safeAreaEle.style.right = '0px';
// window.safeAreaEle.style.pointerEvents = 'none';
// document.body.appendChild(window.safeAreaEle);
// }
// window.safeAreaEle.style.top = safeAreaTop + 'px';
// window.safeAreaEle.style.height = safeAreaHeight + 'px';
// window.safeAreaEle.innerHTML = `
// <div>scrollTo: ${scrollData.scrollTo}</div>
// <div>scrollAmount: ${scrollData.scrollAmount}</div>
// <div>scrollPadding: ${scrollData.scrollPadding}</div>
// <div>scrollHeight: ${scrollViewDimensions.scrollHeight}</div>
// <div>scrollTop: ${scrollViewDimensions.scrollTop}</div>
// <div>contentHeight: ${scrollViewDimensions.contentHeight}</div>
// `;
// fallback for whatever reason
return scrollData; return scrollData;
} }
@ -268,12 +315,21 @@ export class TextInput extends Ion {
setFocus() { setFocus() {
this.zone.run(() => { this.zone.run(() => {
// set focus on the input element
this.input && this.input.setFocus(); this.input && this.input.setFocus();
// ensure the body hasn't scrolled down
document.body.scrollTop = 0;
IonInput.setAsLastInput(this); IonInput.setAsLastInput(this);
}); });
if (this.scrollAssist && this.scrollView) { if (this.scrollAssist && this.scrollView) {
setTimeout(() => { setTimeout(() => {
if (!this.scrollView.isPollingFocus) {
this.scrollView.pollFocus();
}
this.deregListeners(); this.deregListeners();
this.deregScroll = this.scrollView.addScrollEventListener(this.scrollMove); this.deregScroll = this.scrollView.addScrollEventListener(this.scrollMove);
}, 100); }, 100);
@ -306,4 +362,4 @@ export class TextInput extends Ion {
} }
const SCROLL_INTO_VIEW_DURATION = 500; const SCROLL_INTO_VIEW_DURATION = 400;

View File

@ -16,9 +16,6 @@ IonicConfig.modeConfig('ios', {
iconForward: 'ion-ios-arrow-forward', iconForward: 'ion-ios-arrow-forward',
iconMode: 'ios', iconMode: 'ios',
keyboardScrollAssist: true,
tapPolyfill: false,
navTitleAlign: 'center', navTitleAlign: 'center',
tabBarPlacement: 'bottom', tabBarPlacement: 'bottom',
viewTransition: 'ios', viewTransition: 'ios',
@ -40,9 +37,6 @@ IonicConfig.modeConfig('md', {
iconForward: '', iconForward: '',
iconMode: 'md', iconMode: 'md',
keyboardScrollAssist: true,
tapPolyfill: false,
navTitleAlign: 'left', navTitleAlign: 'left',
tabBarPlacement: 'top', tabBarPlacement: 'top',
viewTransition: 'md', viewTransition: 'md',

View File

@ -5,6 +5,7 @@ Platform.register({
name: 'core', name: 'core',
settings: { settings: {
mode: 'ios', mode: 'ios',
keyboardHeight: 290,
} }
}); });
Platform.setDefault('core'); Platform.setDefault('core');
@ -20,7 +21,6 @@ Platform.register({
isMatch(p) { isMatch(p) {
let smallest = Math.min(p.width(), p.height()); let smallest = Math.min(p.width(), p.height());
let largest = Math.max(p.width(), p.height()); let largest = Math.max(p.width(), p.height());
// http://www.mydevice.io/devices/
return (smallest > 390 && smallest < 520) && return (smallest > 390 && smallest < 520) &&
(largest > 620 && largest < 800); (largest > 620 && largest < 800);
} }
@ -32,7 +32,6 @@ Platform.register({
isMatch(p) { isMatch(p) {
let smallest = Math.min(p.width(), p.height()); let smallest = Math.min(p.width(), p.height());
let largest = Math.max(p.width(), p.height()); let largest = Math.max(p.width(), p.height());
// http://www.mydevice.io/devices/
return (smallest > 460 && smallest < 820) && return (smallest > 460 && smallest < 820) &&
(largest > 780 && largest < 1400); (largest > 780 && largest < 1400);
} }
@ -48,10 +47,11 @@ Platform.register({
], ],
settings: { settings: {
mode: 'md', mode: 'md',
keyboardHeight: 290,
keyboardScrollAssist: true,
}, },
isMatch(p) { isMatch(p) {
// "silk" is kindle fire return p.isPlatform('android', 'android|silk');
return p.isPlatform('android', 'android| silk');
}, },
versionParser(p) { versionParser(p) {
return p.matchUserAgentVersion(/Android (\d+).(\d+)?/); return p.matchUserAgentVersion(/Android (\d+).(\d+)?/);
@ -70,10 +70,12 @@ Platform.register({
settings: { settings: {
mode: 'ios', mode: 'ios',
tapPolyfill: function() { tapPolyfill: function() {
// this ensures it's actually a physical iOS device
// and not just an a spoofed user-agent string
return /iphone|ipad|ipod/i.test(Platform.navigatorPlatform()); return /iphone|ipad|ipod/i.test(Platform.navigatorPlatform());
}, },
keyboardScrollAssist: function() {
return /iphone|ipad|ipod/i.test(Platform.navigatorPlatform());
},
keyboardHeight: 290,
}, },
isMatch(p) { isMatch(p) {
return p.isPlatform('ios', 'iphone|ipad|ipod'); return p.isPlatform('ios', 'iphone|ipad|ipod');
@ -87,6 +89,9 @@ Platform.register({
Platform.register({ Platform.register({
name: 'ipad', name: 'ipad',
superset: 'tablet', superset: 'tablet',
settings: {
keyboardHeight: 500,
},
isMatch(p) { isMatch(p) {
return p.isPlatform('ipad'); return p.isPlatform('ipad');
} }

View File

@ -179,3 +179,18 @@ export function hasPointerMoved(threshold, startCoord, endCoord) {
export function hasFocus(ele) { export function hasFocus(ele) {
return !!(ele && (document.activeElement === ele.nativeElement || document.activeElement === ele)); return !!(ele && (document.activeElement === ele.nativeElement || document.activeElement === ele));
} }
export function isTextInput(ele) {
return !!ele &&
(ele.tagName == 'TEXTAREA' ||
ele.contentEditable === 'true' ||
(ele.tagName == 'INPUT' && !(/^(radio|checkbox|range|file|submit|reset|color|image|button)$/i).test(ele.type)));
}
export function hasFocusedTextInput() {
let ele = document.activeElement;
if (isTextInput(ele)) {
return (ele.parentElement.querySelector(':focus') === ele);
}
return false;
}

View File

@ -31,9 +31,10 @@
$content-padding: 10px !default; $content-padding: 10px !default;
[padding], .padding, .padding,
.padding > .scroll-content, [padding],
[padding] > .scroll-content { .padding > scroll-content,
[padding] > scroll-content {
padding: $content-padding; padding: $content-padding;
} }