fix(keyboard): improve keyboard scroll assist

This commit is contained in:
Adam Bradley
2015-11-13 16:53:25 -06:00
parent a0d0962a6c
commit d5ac78f7b0
8 changed files with 114 additions and 82 deletions

View File

@ -49,17 +49,16 @@ export class ScrollTo {
} }
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
let start;
let start = Date.now();
// start scroll loop // start scroll loop
self.isPlaying = true; self.isPlaying = true;
raf(step);
// decelerating to zero velocity // chill out for a frame first
function easeOutCubic(t) { raf(() => {
return (--t) * t * t + 1; start = Date.now();
} raf(step);
});
// scroll loop // scroll loop
function step() { function step() {
@ -104,3 +103,8 @@ export class ScrollTo {
} }
} }
// decelerating to zero velocity
function easeOutCubic(t) {
return (--t) * t * t + 1;
}

View File

@ -2,6 +2,13 @@ import {App} from 'ionic/ionic';
@App({ @App({
templateUrl: 'main.html' templateUrl: 'main.html',
config: {
scrollAssist: true
}
}) })
class E2EApp {} class E2EApp {
reload() {
window.location.reload();
}
}

View File

@ -1,5 +1,12 @@
<ion-toolbar><ion-title>Input Focus</ion-title></ion-toolbar> <ion-navbar>
<ion-title>Inset Focus</ion-title>
<ion-nav-items secondary>
<button (click)="reload()">
Reload
</button>
</ion-nav-items>
</ion-navbar>
<ion-content> <ion-content>

View File

@ -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 {Config} from '../../config/config';
import {Form} from '../../util/form'; import {Form} from '../../util/form';
@ -34,7 +34,6 @@ export class TextInput {
config: Config, config: Config,
renderer: Renderer, renderer: Renderer,
app: IonicApp, app: IonicApp,
zone: NgZone,
platform: Platform, platform: Platform,
@Optional() @Host() scrollView: Content @Optional() @Host() scrollView: Content
) { ) {
@ -49,7 +48,6 @@ export class TextInput {
this.app = app; this.app = app;
this.elementRef = elementRef; this.elementRef = elementRef;
this.zone = zone;
this.platform = platform; this.platform = platform;
this.scrollView = scrollView; this.scrollView = scrollView;
@ -80,7 +78,7 @@ export class TextInput {
self.deregListeners(); self.deregListeners();
if (self.hasFocus) { if (self.hasFocus) {
self.tempFocusMove();
} }
}; };
} }
@ -107,15 +105,13 @@ export class TextInput {
ev.preventDefault(); ev.preventDefault();
ev.stopPropagation(); ev.stopPropagation();
this.zone.runOutsideAngular(() => {
this.initFocus(); this.initFocus();
// temporarily prevent mouseup's from focusing // temporarily prevent mouseup's from focusing
this.lastTouch = Date.now(); this.lastTouch = Date.now();
});
} }
} else if (this.lastTouch + 500 < Date.now()) { } else if (this.lastTouch + 999 < Date.now()) {
ev.preventDefault(); ev.preventDefault();
ev.stopPropagation(); ev.stopPropagation();
@ -146,19 +142,20 @@ export class TextInput {
// 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
this.app.setEnabled(false, SCROLL_INTO_VIEW_DURATION); let scrollDuration = getScrollAssistDuration(scrollData.scrollAmount);
this.app.setTransitioning(true, SCROLL_INTO_VIEW_DURATION); this.app.setEnabled(false, scrollDuration);
this.app.setTransitioning(true, scrollDuration);
// 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
// at this point the native text input still does not have focus // 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 // 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 // the scroll view is in the correct position now
// give the native text input focus // give the native text input focus
this.setFocus(); this.input.relocate(false);
// all good, allow clicks again // all good, allow clicks again
this.app.setEnabled(true); this.app.setEnabled(true);
@ -220,6 +217,7 @@ export class TextInput {
scrollAmount: 0, scrollAmount: 0,
scrollTo: 0, scrollTo: 0,
scrollPadding: 0, scrollPadding: 0,
inputSafeY: 0
}; };
if (inputTopBelowSafeArea || inputBottomBelowSafeArea) { if (inputTopBelowSafeArea || inputBottomBelowSafeArea) {
@ -238,12 +236,15 @@ export class TextInput {
scrollData.scrollAmount = (safeAreaTop - inputTop); scrollData.scrollAmount = (safeAreaTop - inputTop);
} }
scrollData.inputSafeY = -(inputTop - safeAreaTop) + 4;
} else if (inputTopAboveSafeArea) { } else if (inputTopAboveSafeArea) {
// Input top above safe area // Input top above safe area
// auto scroll the input down so at least the top of it shows // auto scroll the input down so at least the top of it shows
scrollData.scrollAmount = (safeAreaTop - inputTop); scrollData.scrollAmount = (safeAreaTop - inputTop);
scrollData.inputSafeY = (safeAreaTop - inputTop) + 4;
} }
// figure out where it should scroll to for the best position to the input // figure out where it should scroll to for the best position to the input
@ -267,11 +268,12 @@ export class TextInput {
// if (!window.safeAreaEle) { // if (!window.safeAreaEle) {
// window.safeAreaEle = document.createElement('div'); // window.safeAreaEle = document.createElement('div');
// window.safeAreaEle.style.position = 'absolute'; // 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.padding = '10px';
// window.safeAreaEle.style.textShadow = '2px 2px white'; // window.safeAreaEle.style.textShadow = '1px 1px white';
// window.safeAreaEle.style.left = '0px'; // window.safeAreaEle.style.left = '0px';
// window.safeAreaEle.style.right = '0px'; // window.safeAreaEle.style.right = '0px';
// window.safeAreaEle.style.fontWeight = 'bold';
// window.safeAreaEle.style.pointerEvents = 'none'; // window.safeAreaEle.style.pointerEvents = 'none';
// document.body.appendChild(window.safeAreaEle); // document.body.appendChild(window.safeAreaEle);
// } // }
@ -281,6 +283,7 @@ export class TextInput {
// <div>scrollTo: ${scrollData.scrollTo}</div> // <div>scrollTo: ${scrollData.scrollTo}</div>
// <div>scrollAmount: ${scrollData.scrollAmount}</div> // <div>scrollAmount: ${scrollData.scrollAmount}</div>
// <div>scrollPadding: ${scrollData.scrollPadding}</div> // <div>scrollPadding: ${scrollData.scrollPadding}</div>
// <div>inputSafeY: ${scrollData.inputSafeY}</div>
// <div>scrollHeight: ${scrollViewDimensions.scrollHeight}</div> // <div>scrollHeight: ${scrollViewDimensions.scrollHeight}</div>
// <div>scrollTop: ${scrollViewDimensions.scrollTop}</div> // <div>scrollTop: ${scrollViewDimensions.scrollTop}</div>
// <div>contentHeight: ${scrollViewDimensions.contentHeight}</div> // <div>contentHeight: ${scrollViewDimensions.contentHeight}</div>
@ -299,9 +302,6 @@ export class TextInput {
setFocus() { setFocus() {
if (this.input) { if (this.input) {
this.zone.run(() => {
this.form.setAsFocused(this); this.form.setAsFocused(this);
// set focus on the actual input element // set focus on the actual input element
@ -309,15 +309,11 @@ export class TextInput {
// ensure the body hasn't scrolled down // ensure the body hasn't scrolled down
document.body.scrollTop = 0; document.body.scrollTop = 0;
});
} }
if (this.scrollAssist && this.scrollView) { if (this.scrollAssist && this.scrollView) {
this.zone.runOutsideAngular(() => {
this.deregListeners(); this.deregListeners();
this.deregScroll = this.scrollView.addScrollEventListener(this.scrollMove); this.deregScroll = this.scrollView.addScrollEventListener(this.scrollMove);
});
} }
} }
@ -325,10 +321,6 @@ export class TextInput {
this.deregScroll && this.deregScroll(); this.deregScroll && this.deregScroll();
} }
tempFocusMove() {
this.form.setFocusHolder(this.type);
}
get hasFocus() { get hasFocus() {
return !!this.input && this.input.hasFocus; return !!this.input && this.input.hasFocus;
} }
@ -387,6 +379,37 @@ export class TextInputElement {
this.getNativeElement().focus(); 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() { get hasFocus() {
return dom.hasFocus(this.getNativeElement()); 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));
}

View File

@ -1,6 +1,4 @@
import {Injectable, NgZone} from 'angular2/angular2'; import {Injectable} from 'angular2/angular2';
import {Config} from '../config/config';
/** /**
@ -17,16 +15,11 @@ import {Config} from '../config/config';
@Injectable() @Injectable()
export class Form { export class Form {
constructor(config: Config, zone: NgZone) { constructor() {
this._config = config;
this._zone = zone;
this._inputs = []; this._inputs = [];
this._focused = null; this._focused = null;
zone.runOutsideAngular(() => {
this.focusCtrl(document); this.focusCtrl(document);
});
} }
register(input) { register(input) {
@ -44,31 +37,15 @@ export class Form {
} }
focusCtrl(document) { focusCtrl(document) {
let scrollAssist = this._config.get('scrollAssist');
// raw DOM fun // raw DOM fun
let focusCtrl = document.createElement('focus-ctrl'); let focusCtrl = document.createElement('focus-ctrl');
focusCtrl.setAttribute('aria-hidden', true); 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 = document.createElement('button');
this._blur.tabIndex = -1; this._blur.tabIndex = -1;
focusCtrl.appendChild(this._blur); focusCtrl.appendChild(this._blur);
document.body.appendChild(focusCtrl); document.body.appendChild(focusCtrl);
if (scrollAssist) {
this._tmp.addEventListener('keydown', (ev) => {
ev.preventDefault();
ev.stopPropagation();
});
}
} }
focusOut() { focusOut() {
@ -76,14 +53,6 @@ export class Form {
this._blur.focus(); 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) { setAsFocused(input) {
this._focused = input; this._focused = input;
} }

View File

@ -74,7 +74,7 @@ export class Keyboard {
* focusOutline: false - Do not add the focus-outline * focusOutline: false - Do not add the focus-outline
*/ */
let self = this;
let isKeyInputEnabled = false; let isKeyInputEnabled = false;
function cssClass() { function cssClass() {
@ -107,7 +107,7 @@ export class Keyboard {
function enableKeyInput() { function enableKeyInput() {
cssClass(); cssClass();
this.zone.runOutsideAngular(() => { self.zone.runOutsideAngular(() => {
document.removeEventListener('mousedown', pointerDown); document.removeEventListener('mousedown', pointerDown);
document.removeEventListener('touchstart', pointerDown); document.removeEventListener('touchstart', pointerDown);

View File

@ -114,9 +114,21 @@ focus-ctrl {
width: 9px; width: 9px;
left: -9999px; left: -9999px;
z-index: 9999; 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 // Backdrop
// -------------------------------------------------- // --------------------------------------------------

View File

@ -20,6 +20,9 @@
} else { } else {
document.documentElement.classList.remove('snapshot'); document.documentElement.classList.remove('snapshot');
} }
if (location.href.indexOf('cordova=true') > -1) {
window.cordova = {};
}
</script> </script>
<style> <style>