mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2025-08-18 11:17:19 +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) => {
|
||||
|
||||
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;
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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 {
|
||||
// <div>scrollTo: ${scrollData.scrollTo}</div>
|
||||
// <div>scrollAmount: ${scrollData.scrollAmount}</div>
|
||||
// <div>scrollPadding: ${scrollData.scrollPadding}</div>
|
||||
// <div>inputSafeY: ${scrollData.inputSafeY}</div>
|
||||
// <div>scrollHeight: ${scrollViewDimensions.scrollHeight}</div>
|
||||
// <div>scrollTop: ${scrollViewDimensions.scrollTop}</div>
|
||||
// <div>contentHeight: ${scrollViewDimensions.contentHeight}</div>
|
||||
@ -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));
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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
|
||||
// --------------------------------------------------
|
||||
|
@ -20,6 +20,9 @@
|
||||
} else {
|
||||
document.documentElement.classList.remove('snapshot');
|
||||
}
|
||||
if (location.href.indexOf('cordova=true') > -1) {
|
||||
window.cordova = {};
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
|
Reference in New Issue
Block a user