From d5ac78f7b095ffbf6cf641b83ca719b763ff177a Mon Sep 17 00:00:00 2001 From: Adam Bradley Date: Fri, 13 Nov 2015 16:53:25 -0600 Subject: [PATCH] fix(keyboard): improve keyboard scroll assist --- ionic/animations/scroll-to.ts | 18 ++-- .../text-input/test/input-focus/index.ts | 11 +- .../text-input/test/input-focus/main.html | 9 +- ionic/components/text-input/text-input.ts | 102 +++++++++++------- ionic/util/form.ts | 37 +------ ionic/util/keyboard.ts | 4 +- ionic/util/util.scss | 12 +++ scripts/e2e/e2e.template.html | 3 + 8 files changed, 114 insertions(+), 82 deletions(-) diff --git a/ionic/animations/scroll-to.ts b/ionic/animations/scroll-to.ts index 1652344d73..7130bab44d 100644 --- a/ionic/animations/scroll-to.ts +++ b/ionic/animations/scroll-to.ts @@ -49,17 +49,16 @@ export class ScrollTo { } return new Promise((resolve, reject) => { - - let start = Date.now(); + let start; // start scroll loop self.isPlaying = true; - raf(step); - // decelerating to zero velocity - function easeOutCubic(t) { - return (--t) * t * t + 1; - } + // chill out for a frame first + raf(() => { + start = Date.now(); + raf(step); + }); // scroll loop function step() { @@ -104,3 +103,8 @@ export class ScrollTo { } } + +// decelerating to zero velocity +function easeOutCubic(t) { + return (--t) * t * t + 1; +} diff --git a/ionic/components/text-input/test/input-focus/index.ts b/ionic/components/text-input/test/input-focus/index.ts index 43aed36502..b8a6ae0b86 100644 --- a/ionic/components/text-input/test/input-focus/index.ts +++ b/ionic/components/text-input/test/input-focus/index.ts @@ -2,6 +2,13 @@ import {App} from 'ionic/ionic'; @App({ - templateUrl: 'main.html' + templateUrl: 'main.html', + config: { + scrollAssist: true + } }) -class E2EApp {} +class E2EApp { + reload() { + window.location.reload(); + } +} diff --git a/ionic/components/text-input/test/input-focus/main.html b/ionic/components/text-input/test/input-focus/main.html index 8ab18e4e1b..668ebbfb19 100644 --- a/ionic/components/text-input/test/input-focus/main.html +++ b/ionic/components/text-input/test/input-focus/main.html @@ -1,5 +1,12 @@ -Input Focus + + Inset Focus + + + + diff --git a/ionic/components/text-input/text-input.ts b/ionic/components/text-input/text-input.ts index f5e5f032ea..ec17c5626e 100644 --- a/ionic/components/text-input/text-input.ts +++ b/ionic/components/text-input/text-input.ts @@ -1,4 +1,4 @@ -import {Component, Directive, NgIf, forwardRef, Host, Optional, ElementRef, Renderer, Attribute, Query, QueryList, NgZone} from 'angular2/angular2'; +import {Component, Directive, NgIf, forwardRef, Host, Optional, ElementRef, Renderer, Attribute} from 'angular2/angular2'; import {Config} from '../../config/config'; import {Form} from '../../util/form'; @@ -34,7 +34,6 @@ export class TextInput { config: Config, renderer: Renderer, app: IonicApp, - zone: NgZone, platform: Platform, @Optional() @Host() scrollView: Content ) { @@ -49,7 +48,6 @@ export class TextInput { this.app = app; this.elementRef = elementRef; - this.zone = zone; this.platform = platform; this.scrollView = scrollView; @@ -80,7 +78,7 @@ export class TextInput { self.deregListeners(); if (self.hasFocus) { - self.tempFocusMove(); + } }; } @@ -107,15 +105,13 @@ export class TextInput { ev.preventDefault(); ev.stopPropagation(); - this.zone.runOutsideAngular(() => { - this.initFocus(); + this.initFocus(); - // temporarily prevent mouseup's from focusing - this.lastTouch = Date.now(); - }); + // temporarily prevent mouseup's from focusing + this.lastTouch = Date.now(); } - } else if (this.lastTouch + 500 < Date.now()) { + } else if (this.lastTouch + 999 < Date.now()) { ev.preventDefault(); ev.stopPropagation(); @@ -146,19 +142,20 @@ export class TextInput { // manually scroll the text input to the top // do not allow any clicks while it's scrolling - this.app.setEnabled(false, SCROLL_INTO_VIEW_DURATION); - this.app.setTransitioning(true, SCROLL_INTO_VIEW_DURATION); + let scrollDuration = getScrollAssistDuration(scrollData.scrollAmount); + this.app.setEnabled(false, scrollDuration); + this.app.setTransitioning(true, scrollDuration); // 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 // at this point the native text input still does not have focus - this.tempFocusMove(); + this.input.relocate(true, scrollData.inputSafeY); // scroll the input into place - scrollView.scrollTo(0, scrollData.scrollTo, SCROLL_INTO_VIEW_DURATION, 6).then(() => { + scrollView.scrollTo(0, scrollData.scrollTo, scrollDuration).then(() => { // the scroll view is in the correct position now // give the native text input focus - this.setFocus(); + this.input.relocate(false); // all good, allow clicks again this.app.setEnabled(true); @@ -220,6 +217,7 @@ export class TextInput { scrollAmount: 0, scrollTo: 0, scrollPadding: 0, + inputSafeY: 0 }; if (inputTopBelowSafeArea || inputBottomBelowSafeArea) { @@ -238,12 +236,15 @@ export class TextInput { scrollData.scrollAmount = (safeAreaTop - inputTop); } + scrollData.inputSafeY = -(inputTop - safeAreaTop) + 4; } 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.inputSafeY = (safeAreaTop - inputTop) + 4; + } // figure out where it should scroll to for the best position to the input @@ -267,11 +268,12 @@ export class TextInput { // 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.background = 'rgba(0, 128, 0, 0.7)'; // window.safeAreaEle.style.padding = '10px'; - // window.safeAreaEle.style.textShadow = '2px 2px white'; + // window.safeAreaEle.style.textShadow = '1px 1px white'; // window.safeAreaEle.style.left = '0px'; // window.safeAreaEle.style.right = '0px'; + // window.safeAreaEle.style.fontWeight = 'bold'; // window.safeAreaEle.style.pointerEvents = 'none'; // document.body.appendChild(window.safeAreaEle); // } @@ -281,6 +283,7 @@ export class TextInput { //
scrollTo: ${scrollData.scrollTo}
//
scrollAmount: ${scrollData.scrollAmount}
//
scrollPadding: ${scrollData.scrollPadding}
+ //
inputSafeY: ${scrollData.inputSafeY}
//
scrollHeight: ${scrollViewDimensions.scrollHeight}
//
scrollTop: ${scrollViewDimensions.scrollTop}
//
contentHeight: ${scrollViewDimensions.contentHeight}
@@ -299,25 +302,18 @@ export class TextInput { setFocus() { if (this.input) { + this.form.setAsFocused(this); - this.zone.run(() => { - - this.form.setAsFocused(this); - - // set focus on the actual input element - this.input.setFocus(); - - // ensure the body hasn't scrolled down - document.body.scrollTop = 0; - }); + // set focus on the actual input element + this.input.setFocus(); + // ensure the body hasn't scrolled down + document.body.scrollTop = 0; } if (this.scrollAssist && this.scrollView) { - this.zone.runOutsideAngular(() => { - this.deregListeners(); - this.deregScroll = this.scrollView.addScrollEventListener(this.scrollMove); - }); + this.deregListeners(); + this.deregScroll = this.scrollView.addScrollEventListener(this.scrollMove); } } @@ -325,10 +321,6 @@ export class TextInput { this.deregScroll && this.deregScroll(); } - tempFocusMove() { - this.form.setFocusHolder(this.type); - } - get hasFocus() { return !!this.input && this.input.hasFocus; } @@ -387,6 +379,37 @@ export class TextInputElement { this.getNativeElement().focus(); } + relocate(shouldRelocate, inputRelativeY) { + this.clone(shouldRelocate, inputRelativeY); + + if (shouldRelocate) { + this.wrapper.setFocus(); + } + } + + clone(shouldClone, inputRelativeY) { + let focusedInputEle = this.getNativeElement(); + + if (shouldRelocate) { + let clonedInputEle = focusedInputEle.cloneNode(true); + clonedInputEle.classList.add('cloned-input'); + clonedInputEle.setAttribute('aria-hidden', true); + clonedInputEle.tabIndex = -1; + + focusedInputEle.classList.add('hide-focused-input'); + focusedInputEle.style[dom.CSS.transform] = `translate3d(-9999px,${inputRelativeY}px,0)`; + focusedInputEle.parentNode.insertBefore(clonedInputEle, focusedInputEle); + + } else { + focusedInputEle.classList.remove('hide-focused-input'); + focusedInputEle.style[dom.CSS.transform] = ''; + let clonedInputEle = focusedInputEle.parentNode.querySelector('.cloned-input'); + if (clonedInputEle) { + clonedInputEle.parentNode.removeChild(clonedInputEle); + } + } + } + get hasFocus() { return dom.hasFocus(this.getNativeElement()); } @@ -417,4 +440,11 @@ class InputScrollAssist { } -const SCROLL_INTO_VIEW_DURATION = 400; +const SCROLL_ASSIST_SPEED = 0.5; + +function getScrollAssistDuration(distanceToScroll) { + //return 3000; + distanceToScroll = Math.abs(distanceToScroll); + let duration = distanceToScroll / SCROLL_ASSIST_SPEED; + return Math.min(380, Math.max(80, duration)); +} diff --git a/ionic/util/form.ts b/ionic/util/form.ts index e4eefbaf96..7c87a8c727 100644 --- a/ionic/util/form.ts +++ b/ionic/util/form.ts @@ -1,6 +1,4 @@ -import {Injectable, NgZone} from 'angular2/angular2'; - -import {Config} from '../config/config'; +import {Injectable} from 'angular2/angular2'; /** @@ -17,16 +15,11 @@ import {Config} from '../config/config'; @Injectable() export class Form { - constructor(config: Config, zone: NgZone) { - this._config = config; - this._zone = zone; - + constructor() { this._inputs = []; this._focused = null; - zone.runOutsideAngular(() => { - this.focusCtrl(document); - }); + this.focusCtrl(document); } register(input) { @@ -44,31 +37,15 @@ export class Form { } focusCtrl(document) { - let scrollAssist = this._config.get('scrollAssist'); - // raw DOM fun let focusCtrl = document.createElement('focus-ctrl'); focusCtrl.setAttribute('aria-hidden', true); - if (scrollAssist) { - this._tmp = document.createElement('input'); - this._tmp.tabIndex = -1; - focusCtrl.appendChild(this._tmp); - } - this._blur = document.createElement('button'); this._blur.tabIndex = -1; focusCtrl.appendChild(this._blur); document.body.appendChild(focusCtrl); - - if (scrollAssist) { - this._tmp.addEventListener('keydown', (ev) => { - ev.preventDefault(); - ev.stopPropagation(); - }); - } - } focusOut() { @@ -76,14 +53,6 @@ export class Form { this._blur.focus(); } - setFocusHolder(type) { - if (this._tmp && this._config.get('scrollAssist')) { - this._tmp.type = type; - console.debug('setFocusHolder', this._tmp.type); - this._tmp.focus(); - } - } - setAsFocused(input) { this._focused = input; } diff --git a/ionic/util/keyboard.ts b/ionic/util/keyboard.ts index 1cbef12c85..5d6b854b85 100644 --- a/ionic/util/keyboard.ts +++ b/ionic/util/keyboard.ts @@ -74,7 +74,7 @@ export class Keyboard { * focusOutline: false - Do not add the focus-outline */ - + let self = this; let isKeyInputEnabled = false; function cssClass() { @@ -107,7 +107,7 @@ export class Keyboard { function enableKeyInput() { cssClass(); - this.zone.runOutsideAngular(() => { + self.zone.runOutsideAngular(() => { document.removeEventListener('mousedown', pointerDown); document.removeEventListener('touchstart', pointerDown); diff --git a/ionic/util/util.scss b/ionic/util/util.scss index 3fcad7487d..6f94eb447d 100755 --- a/ionic/util/util.scss +++ b/ionic/util/util.scss @@ -114,9 +114,21 @@ focus-ctrl { width: 9px; left: -9999px; z-index: 9999; + pointer-events: none; } } +.hide-focused-input { + flex: 0 0 8px !important; + margin: 0 !important; + transform: translate3d(-9999px,0,0); + pointer-events: none; +} + +.cloned-input { + pointer-events: none; +} + // Backdrop // -------------------------------------------------- diff --git a/scripts/e2e/e2e.template.html b/scripts/e2e/e2e.template.html index 3a68b06727..01cd01b8ab 100644 --- a/scripts/e2e/e2e.template.html +++ b/scripts/e2e/e2e.template.html @@ -20,6 +20,9 @@ } else { document.documentElement.classList.remove('snapshot'); } + if (location.href.indexOf('cordova=true') > -1) { + window.cordova = {}; + }