mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2025-08-19 03:32:21 +08:00
fix(keyboard): improve keyboard scroll assist
This commit is contained in:
@ -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;
|
||||||
|
}
|
||||||
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -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>
|
||||||
|
@ -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,25 +302,18 @@ export class TextInput {
|
|||||||
|
|
||||||
setFocus() {
|
setFocus() {
|
||||||
if (this.input) {
|
if (this.input) {
|
||||||
|
this.form.setAsFocused(this);
|
||||||
|
|
||||||
this.zone.run(() => {
|
// set focus on the actual input element
|
||||||
|
this.input.setFocus();
|
||||||
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;
|
|
||||||
});
|
|
||||||
|
|
||||||
|
// ensure the body hasn't scrolled down
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
|
|
||||||
|
@ -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
|
||||||
// --------------------------------------------------
|
// --------------------------------------------------
|
||||||
|
@ -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>
|
||||||
|
Reference in New Issue
Block a user