diff --git a/ionic/components/app/test/profile/index.ts b/ionic/components/app/test/profile/index.ts index 89c84b2803..8403a4c139 100644 --- a/ionic/components/app/test/profile/index.ts +++ b/ionic/components/app/test/profile/index.ts @@ -20,7 +20,7 @@ export class ParallaxEffect { elementRef: ElementRef ) { this.ele = elementRef.nativeElement; - this.scroller = this.ele.querySelector('.scroll-content'); + this.scroller = this.ele.querySelector('scroll-content'); this.scroller.addEventListener('scroll', (e) => { //this.counter.innerHTML = e.target.scrollTop; dom.raf(() => { diff --git a/ionic/components/app/test/profile/main.html b/ionic/components/app/test/profile/main.html index 182a771708..d72ffcf178 100644 --- a/ionic/components/app/test/profile/main.html +++ b/ionic/components/app/test/profile/main.html @@ -47,7 +47,7 @@ ion-content { background: transparent !important; } -.scroll-content { +scroll-content { padding-top: 300px; } diff --git a/ionic/components/content/content.scss b/ionic/components/content/content.scss index cec8ae1b55..e0664acf13 100644 --- a/ionic/components/content/content.scss +++ b/ionic/components/content/content.scss @@ -10,17 +10,17 @@ ion-content { display: block; flex: 1; background-color: $content-background-color; - - .scroll-content { - position: absolute; - top: 0; - right: 0; - bottom: 0; - left: 0; - overflow-y: auto; - overflow-x: hidden; - -webkit-overflow-scrolling: touch; - will-change: scroll-position; - } - +} + +scroll-content { + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + display: block; + overflow-y: scroll; // has to be scroll for momentum scrolling, not auto + overflow-x: hidden; + -webkit-overflow-scrolling: touch; + will-change: scroll-position; } diff --git a/ionic/components/content/content.ts b/ionic/components/content/content.ts index 69c3000d31..2f65d20cc8 100644 --- a/ionic/components/content/content.ts +++ b/ionic/components/content/content.ts @@ -4,7 +4,7 @@ import {Ion} from '../ion'; import {IonicConfig} from '../../config/config'; import {IonicComponent} from '../../config/annotations'; import {ScrollTo} from '../../animations/scroll-to'; -import {Platform} from '../../platform/platform'; +import {hasFocusedTextInput} from '../../util/dom'; @Component({ @@ -14,11 +14,12 @@ import {Platform} from '../../platform/platform'; ] }) @View({ - template: '
' + template: '' }) export class Content extends Ion { constructor(elementRef: ElementRef, config: IonicConfig) { super(elementRef, config); + this.scrollPadding = 0; } onIonInit() { @@ -66,13 +67,13 @@ export class Content extends Ion { let parentElement = scrollElement.parentElement; return { - height: parentElement.offsetHeight, - top: parentElement.offsetTop, - bottom: parentElement.offsetTop + parentElement.offsetHeight, + contentHeight: parentElement.offsetHeight, + contentTop: parentElement.offsetTop, + contentBottom: parentElement.offsetTop + parentElement.offsetHeight, - width: parentElement.offsetWidth, - left: parentElement.offsetLeft, - right: parentElement.offsetLeft + parentElement.offsetWidth, + contentWidth: parentElement.offsetWidth, + contentLeft: parentElement.offsetLeft, + contentRight: parentElement.offsetLeft + parentElement.offsetWidth, scrollHeight: scrollElement.scrollHeight, scrollTop: scrollElement.scrollTop, @@ -81,8 +82,31 @@ export class Content extends Ion { scrollWidth: scrollElement.scrollWidth, scrollLeft: scrollElement.scrollLeft, 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 = ''; + } } } diff --git a/ionic/components/scroll/pull-to-refresh.scss b/ionic/components/scroll/pull-to-refresh.scss index 485888130b..1ee2dcf1ff 100644 --- a/ionic/components/scroll/pull-to-refresh.scss +++ b/ionic/components/scroll/pull-to-refresh.scss @@ -74,7 +74,7 @@ ion-refresher { } } } -.scroll-content.overscroll { +scroll-content.overscroll { overflow: visible; } /* diff --git a/ionic/components/scroll/scroll.scss b/ionic/components/scroll/scroll.scss index 88eec07982..ca639122fe 100644 --- a/ionic/components/scroll/scroll.scss +++ b/ionic/components/scroll/scroll.scss @@ -1,14 +1,14 @@ ion-scroll { position: relative; display: block; - &.scroll-x .scroll-content { + &.scroll-x scroll-content { overflow-x: auto; } - &.scroll-y .scroll-content { + &.scroll-y scroll-content { overflow-y: auto; } - .scroll-content { + scroll-content { position: absolute; top: 0; right: 0; diff --git a/ionic/components/scroll/scroll.ts b/ionic/components/scroll/scroll.ts index 673177f223..ece72e1b8d 100644 --- a/ionic/components/scroll/scroll.ts +++ b/ionic/components/scroll/scroll.ts @@ -20,7 +20,7 @@ import {IonicComponent} from '../../config/annotations'; } }) @View({ - template: '
' + template: '' }) export class Scroll extends Ion { constructor(elementRef: ElementRef, ionicConfig: IonicConfig) { diff --git a/ionic/components/text-input/test/text-input.spec.ts b/ionic/components/text-input/test/text-input.spec.ts index 0f8aa517fd..9df39f6f5c 100644 --- a/ionic/components/text-input/test/text-input.spec.ts +++ b/ionic/components/text-input/test/text-input.spec.ts @@ -2,8 +2,129 @@ import {TextInput} from 'ionic/ionic'; export function run() { - it('should not scroll', () => { - let input = new TextInput(); + it('should scroll, top and bottom below safe area, no room to scroll', () => { + + 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); }); } diff --git a/ionic/components/text-input/text-input.ts b/ionic/components/text-input/text-input.ts index 8dab08ba54..eba0909075 100644 --- a/ionic/components/text-input/text-input.ts +++ b/ionic/components/text-input/text-input.ts @@ -9,6 +9,7 @@ import {IonicApp} from '../app/app'; import {Content} from '../content/content'; import {ClickBlock} from '../../util/click-block'; 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 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) { // the text input is in a safe position that doesn't require // it to be scrolled into view, just set focus now 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 // 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 // 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(); // 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 // give the native text input focus 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 - let inputTop = (inputOffsetTop + scrollViewDimensions.top - scrollViewDimensions.scrollTop); + let inputTop = (inputOffsetTop + scrollViewDimensions.contentTop - scrollViewDimensions.scrollTop); let inputBottom = (inputTop + inputOffsetHeight); // compute the safe area which is the viewable content area when the soft keyboard is up - let safeAreaTop = (scrollViewDimensions.top - 1); - let safeAreaBottom = scrollViewDimensions.keyboardTop; - let safeAreaHeight = (safeAreaBottom - safeAreaTop); + let safeAreaTop = scrollViewDimensions.contentTop; + let safeAreaHeight = Platform.height() - keyboardHeight - safeAreaTop; + safeAreaHeight /= 2; + let safeAreaBottom = safeAreaTop + safeAreaHeight; let inputTopWithinSafeArea = (inputTop >= safeAreaTop && inputTop <= safeAreaBottom); + let inputTopAboveSafeArea = (inputTop < safeAreaTop); + let inputTopBelowSafeArea = (inputTop > safeAreaBottom); let inputBottomWithinSafeArea = (inputBottom >= safeAreaTop && inputBottom <= safeAreaBottom); let inputBottomBelowSafeArea = (inputBottom > safeAreaBottom); - let inputFitsWithinSafeArea = (inputOffsetHeight <= safeAreaHeight); - let distanceInputBottomBelowSafeArea = (safeAreaBottom - inputBottom); /* Text Input Scroll To Scenarios @@ -231,34 +238,74 @@ export class TextInput extends Ion { // looks like we'll have to do some auto-scrolling let scrollData = { scrollAmount: 0, - scrollTo: inputOffsetTop, + scrollTo: 0, scrollPadding: 0, }; - if (inputTopWithinSafeArea && inputBottomBelowSafeArea) { - // Input top within safe area, bottom below safe area - let distanceInputTopIntoSafeArea = (safeAreaTop - inputTop); - let distanceInputBottomBelowSafeArea = (inputBottom - safeAreaBottom); + if (inputTopBelowSafeArea || inputBottomBelowSafeArea) { + // Input top and bottom below safe area + // auto scroll the input up so at least the top of it shows - if (distanceInputBottomBelowSafeArea < distanceInputTopIntoSafeArea) { - // the input's top is farther into the safe area then the bottom is out of it - // this means we can scroll it up a little bit and the top will still be - // within the safe area - scrollData.scrollAmount = distanceInputTopIntoSafeArea * -1; + if (safeAreaHeight > inputOffsetHeight) { + // safe area height is taller than the input height, so we + // can bring it up the input just enough to show the input bottom + scrollData.scrollAmount = (safeAreaBottom - inputBottom); } else { - // the input's top is less below the safe area top than the - // input's bottom is below the safe area bottom. So scroll the input - // to be at the top of the safe area, knowing that the bottom will go below - scrollData.scrollAmount = distanceInputTopIntoSafeArea * -1; + // safe area height is smaller than the input height, so we can + // only scroll it up so the input top is at the top of the safe area + // however the input bottom will be below the safe area + 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 = ` + //
scrollTo: ${scrollData.scrollTo}
+ //
scrollAmount: ${scrollData.scrollAmount}
+ //
scrollPadding: ${scrollData.scrollPadding}
+ //
scrollHeight: ${scrollViewDimensions.scrollHeight}
+ //
scrollTop: ${scrollViewDimensions.scrollTop}
+ //
contentHeight: ${scrollViewDimensions.contentHeight}
+ // `; - // fallback for whatever reason return scrollData; } @@ -268,12 +315,21 @@ export class TextInput extends Ion { setFocus() { this.zone.run(() => { + // set focus on the input element this.input && this.input.setFocus(); + + // ensure the body hasn't scrolled down + document.body.scrollTop = 0; + IonInput.setAsLastInput(this); }); if (this.scrollAssist && this.scrollView) { setTimeout(() => { + if (!this.scrollView.isPollingFocus) { + this.scrollView.pollFocus(); + } + this.deregListeners(); this.deregScroll = this.scrollView.addScrollEventListener(this.scrollMove); }, 100); @@ -306,4 +362,4 @@ export class TextInput extends Ion { } -const SCROLL_INTO_VIEW_DURATION = 500; +const SCROLL_INTO_VIEW_DURATION = 400; diff --git a/ionic/config/modes.ts b/ionic/config/modes.ts index b2401b46c7..de45fd08b9 100644 --- a/ionic/config/modes.ts +++ b/ionic/config/modes.ts @@ -16,9 +16,6 @@ IonicConfig.modeConfig('ios', { iconForward: 'ion-ios-arrow-forward', iconMode: 'ios', - keyboardScrollAssist: true, - tapPolyfill: false, - navTitleAlign: 'center', tabBarPlacement: 'bottom', viewTransition: 'ios', @@ -40,9 +37,6 @@ IonicConfig.modeConfig('md', { iconForward: '', iconMode: 'md', - keyboardScrollAssist: true, - tapPolyfill: false, - navTitleAlign: 'left', tabBarPlacement: 'top', viewTransition: 'md', diff --git a/ionic/platform/registry.ts b/ionic/platform/registry.ts index e67fdc2ca7..f6efc7f3d1 100644 --- a/ionic/platform/registry.ts +++ b/ionic/platform/registry.ts @@ -5,6 +5,7 @@ Platform.register({ name: 'core', settings: { mode: 'ios', + keyboardHeight: 290, } }); Platform.setDefault('core'); @@ -20,7 +21,6 @@ Platform.register({ isMatch(p) { let smallest = Math.min(p.width(), p.height()); let largest = Math.max(p.width(), p.height()); - // http://www.mydevice.io/devices/ return (smallest > 390 && smallest < 520) && (largest > 620 && largest < 800); } @@ -32,7 +32,6 @@ Platform.register({ isMatch(p) { let smallest = Math.min(p.width(), p.height()); let largest = Math.max(p.width(), p.height()); - // http://www.mydevice.io/devices/ return (smallest > 460 && smallest < 820) && (largest > 780 && largest < 1400); } @@ -48,10 +47,11 @@ Platform.register({ ], settings: { mode: 'md', + keyboardHeight: 290, + keyboardScrollAssist: true, }, isMatch(p) { - // "silk" is kindle fire - return p.isPlatform('android', 'android| silk'); + return p.isPlatform('android', 'android|silk'); }, versionParser(p) { return p.matchUserAgentVersion(/Android (\d+).(\d+)?/); @@ -70,10 +70,12 @@ Platform.register({ settings: { mode: 'ios', 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()); }, + keyboardScrollAssist: function() { + return /iphone|ipad|ipod/i.test(Platform.navigatorPlatform()); + }, + keyboardHeight: 290, }, isMatch(p) { return p.isPlatform('ios', 'iphone|ipad|ipod'); @@ -87,6 +89,9 @@ Platform.register({ Platform.register({ name: 'ipad', superset: 'tablet', + settings: { + keyboardHeight: 500, + }, isMatch(p) { return p.isPlatform('ipad'); } diff --git a/ionic/util/dom.ts b/ionic/util/dom.ts index 149944664e..57448005a5 100644 --- a/ionic/util/dom.ts +++ b/ionic/util/dom.ts @@ -179,3 +179,18 @@ export function hasPointerMoved(threshold, startCoord, endCoord) { export function hasFocus(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; +} diff --git a/ionic/util/util.scss b/ionic/util/util.scss index 4059db92dc..9d86775a70 100755 --- a/ionic/util/util.scss +++ b/ionic/util/util.scss @@ -31,9 +31,10 @@ $content-padding: 10px !default; -[padding], .padding, -.padding > .scroll-content, -[padding] > .scroll-content { +.padding, +[padding], +.padding > scroll-content, +[padding] > scroll-content { padding: $content-padding; }