From e27452b789232c0a3222a03e75de1a9af19d3411 Mon Sep 17 00:00:00 2001 From: Adam Bradley Date: Thu, 14 Apr 2016 09:31:09 -0500 Subject: [PATCH] fix(focus): improve input focus control Closes #5536 --- ionic/components/input/input-base.ts | 22 ++-- ionic/components/input/input.scss | 11 +- ionic/components/input/native-input.ts | 102 ++++++++++-------- .../input/test/input-focus/index.ts | 22 +++- .../input/test/input-focus/main.html | 5 + ionic/platform/registry.ts | 2 +- ionic/util/keyboard.ts | 2 +- 7 files changed, 105 insertions(+), 61 deletions(-) diff --git a/ionic/components/input/input-base.ts b/ionic/components/input/input-base.ts index 204e2c9547d..96a94dda7cb 100644 --- a/ionic/components/input/input-base.ts +++ b/ionic/components/input/input-base.ts @@ -340,19 +340,17 @@ export class InputBase { */ initFocus() { // begin the process of setting focus to the inner input element - let scrollView = this._scrollView; + var scrollView = this._scrollView; if (scrollView) { // this input is inside of a scroll view - // find out if text input should be manually scrolled into view - let ele = this._elementRef.nativeElement; - let itemEle = closest(ele, 'ion-item'); - if (itemEle) { - ele = itemEle; - } - let scrollData = InputBase.getScrollData(ele.offsetTop, ele.offsetHeight, scrollView.getContentDimensions(), this._keyboardHeight, this._platform.height()); + // get container of this input, probably an ion-item a few nodes up + var ele = this._elementRef.nativeElement; + ele = closest(ele, 'ion-item,[ion-item]') || ele; + + var scrollData = InputBase.getScrollData(ele.offsetTop, ele.offsetHeight, scrollView.getContentDimensions(), this._keyboardHeight, this._platform.height()); if (scrollData.scrollAmount > -3 && scrollData.scrollAmount < 3) { // the text input is in a safe position that doesn't // require it to be scrolled into view, just set focus now @@ -368,21 +366,22 @@ export class InputBase { // manually scroll the text input to the top // do not allow any clicks while it's scrolling - let scrollDuration = getScrollAssistDuration(scrollData.scrollAmount); + var scrollDuration = getScrollAssistDuration(scrollData.scrollAmount); this._app.setEnabled(false, scrollDuration); this._nav && this._nav.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._native.relocate(true, scrollData.inputSafeY); + this._native.beginFocus(true, scrollData.inputSafeY); // scroll the input into place scrollView.scrollTo(0, scrollData.scrollTo, scrollDuration).then(() => { // the scroll view is in the correct position now // give the native text input focus - this._native.relocate(false, 0); + this._native.beginFocus(false, 0); + // ensure this is the focused input this.setFocus(); // all good, allow clicks again @@ -417,6 +416,7 @@ export class InputBase { this._form.setAsFocused(this); // set focus on the actual input element + console.debug(`input-base, setFocus ${this._native.element().value}`); this._native.setFocus(); // ensure the body hasn't scrolled down diff --git a/ionic/components/input/input.scss b/ionic/components/input/input.scss index 7338b5a4bb8..e42c2de2bf5 100644 --- a/ionic/components/input/input.scss +++ b/ionic/components/input/input.scss @@ -91,6 +91,15 @@ input.text-input:-webkit-autofill { display: none; } +.input-has-focus { + pointer-events: none; +} + +.input-has-focus input, +.input-has-focus textarea { + pointer-events: auto; +} + // Scroll Assist Input // -------------------------------------------------- @@ -131,7 +140,7 @@ input.text-input:-webkit-autofill { // -------------------------------------------------- .text-input.cloned-input { - position: absolute; + position: relative; top: 0; pointer-events: none; diff --git a/ionic/components/input/native-input.ts b/ionic/components/input/native-input.ts index 8f7ab414d2b..8c87d4e439c 100644 --- a/ionic/components/input/native-input.ts +++ b/ionic/components/input/native-input.ts @@ -1,6 +1,7 @@ import {Directive, Attribute, ElementRef, Renderer, Input, Output, EventEmitter, HostListener} from 'angular2/core'; import {NgControl} from 'angular2/common'; +import {Config} from '../../config/config'; import {CSS, hasFocus, raf} from '../../util/dom'; @@ -12,6 +13,7 @@ import {CSS, hasFocus, raf} from '../../util/dom'; }) export class NativeInput { private _relocated: boolean; + private _clone: boolean; @Output() focusChange: EventEmitter = new EventEmitter(); @Output() valueChange: EventEmitter = new EventEmitter(); @@ -19,28 +21,22 @@ export class NativeInput { constructor( private _elementRef: ElementRef, private _renderer: Renderer, + config: Config, public ngControl: NgControl - ) {} + ) { + this._clone = config.getBoolean('inputCloning', false); + } - /** - * @private - */ @HostListener('input', ['$event']) private _change(ev) { this.valueChange.emit(ev.target.value); } - /** - * @private - */ @HostListener('focus') private _focus() { this.focusChange.emit(true); } - /** - * @private - */ @HostListener('blur') private _blur() { this.focusChange.emit(false); @@ -55,55 +51,69 @@ export class NativeInput { this._renderer.setElementAttribute(this._elementRef.nativeElement, 'disabled', val ? '' : null); } - /** - * @private - */ setFocus() { - this.element().focus(); + // let's set focus to the element + // but only if it does not already have focus + if (document.activeElement !== this.element()) { + this.element().focus(); + } } - /** - * @private - */ - relocate(shouldRelocate: boolean, inputRelativeY: number) { - console.debug('native input relocate', shouldRelocate, inputRelativeY); - - if (this._relocated !== shouldRelocate) { - - let focusedInputEle = this.element(); - if (shouldRelocate) { - let clonedInputEle = cloneInput(focusedInputEle, 'cloned-focus'); - - focusedInputEle.parentNode.insertBefore(clonedInputEle, focusedInputEle); - focusedInputEle.style[CSS.transform] = `translate3d(-9999px,${inputRelativeY}px,0)`; - focusedInputEle.style.opacity = '0'; - + beginFocus(shouldFocus: boolean, inputRelativeY: number) { + if (this._relocated !== shouldFocus) { + var focusedInputEle = this.element(); + if (shouldFocus) { + // we should focus into this element + + if (this._clone) { + // this platform needs the input to be cloned + // this allows for the actual input to receive the focus from + // the user's touch event, but before it receives focus, it + // moves the actual input to a location that will not screw + // up the app's layout, and does not allow the native browser + // to attempt to scroll the input into place (messing up headers/footers) + // the cloned input fills the area of where native input should be + // while the native input fakes out the browser by relocating itself + // before it receives the actual focus event + var clonedInputEle = cloneInput(focusedInputEle, 'cloned-focus'); + focusedInputEle.parentNode.insertBefore(clonedInputEle, focusedInputEle); + + // move the native input to a location safe to receive focus + // according to the browser, the native input receives focus in an + // area which doesn't require the browser to scroll the input into place + focusedInputEle.style[CSS.transform] = `translate3d(-9999px,${inputRelativeY}px,0)`; + focusedInputEle.style.opacity = '0'; + } + + // let's now set focus to the actual native element + // at this point it is safe to assume the browser will not attempt + // to scroll the input into view itself (screwing up headers/footers) this.setFocus(); - raf(() => { + if (this._clone) { focusedInputEle.classList.add('cloned-active'); - }); + } } else { - focusedInputEle.classList.remove('cloned-active'); - focusedInputEle.style[CSS.transform] = ''; - focusedInputEle.style.opacity = ''; - - removeClone(focusedInputEle, 'cloned-focus'); + // should remove the focus + if (this._clone) { + // should remove the cloned node + focusedInputEle.classList.remove('cloned-active'); + focusedInputEle.style[CSS.transform] = ''; + focusedInputEle.style.opacity = ''; + removeClone(focusedInputEle, 'cloned-focus'); + } } - this._relocated = shouldRelocate; + this._relocated = shouldFocus; } } - /** - * @private - */ hideFocus(shouldHideFocus: boolean) { - console.debug('native input hideFocus', shouldHideFocus); - let focusedInputEle = this.element(); + console.debug(`native input hideFocus, shouldHideFocus: ${shouldHideFocus}, input value: ${focusedInputEle.value}`); + if (shouldHideFocus) { let clonedInputEle = cloneInput(focusedInputEle, 'cloned-move'); @@ -124,9 +134,6 @@ export class NativeInput { return this.element().value; } - /** - * @private - */ element(): HTMLInputElement { return this._elementRef.nativeElement; } @@ -141,6 +148,8 @@ function cloneInput(focusedInputEle, addCssClass) { clonedInputEle.removeAttribute('aria-labelledby'); clonedInputEle.tabIndex = -1; clonedInputEle.style.width = (focusedInputEle.offsetWidth + 10) + 'px'; + clonedInputEle.style.height = focusedInputEle.offsetHeight + 'px'; + clonedInputEle.value = focusedInputEle.value; return clonedInputEle; } @@ -164,6 +173,7 @@ export class NextInput { @HostListener('focus') receivedFocus() { + console.debug('native-input, next-input received focus'); this.focused.emit(true); } diff --git a/ionic/components/input/test/input-focus/index.ts b/ionic/components/input/test/input-focus/index.ts index 552fec2f9c8..61c3566c0a0 100644 --- a/ionic/components/input/test/input-focus/index.ts +++ b/ionic/components/input/test/input-focus/index.ts @@ -4,7 +4,7 @@ import {App} from 'ionic-angular'; @App({ templateUrl: 'main.html', config: { - scrollAssist: true + //scrollAssist: true } }) class E2EApp { @@ -12,3 +12,23 @@ class E2EApp { window.location.reload(); } } + +document.addEventListener('click', function(ev) { + console.log(`CLICK, ${ev.target.localName}.${ev.target.className}, time: ${Date.now()}`); +}); + +document.addEventListener('touchstart', function(ev) { + console.log(`TOUCH START, ${ev.target.localName}.${ev.target.className}, time: ${Date.now()}`); +}); + +document.addEventListener('touchend', function(ev) { + console.log(`TOUCH END, ${ev.target.localName}.${ev.target.className}, time: ${Date.now()}`); +}); + +document.addEventListener('focusin', function(ev) {console.log(`CLICK, ${ev.target.localName}.${ev.target.className}, time: ${Date.now()}`); + console.log(`FOCUS IN, ${ev.target.localName}.${ev.target.className}, time: ${Date.now()}`); +}); + +document.addEventListener('focusout', function(ev) {console.log(`CLICK, ${ev.target.localName}.${ev.target.className}, time: ${Date.now()}`); + console.log(`FOCUS OUT, ${ev.target.localName}.${ev.target.className}, time: ${Date.now()}`); +}); diff --git a/ionic/components/input/test/input-focus/main.html b/ionic/components/input/test/input-focus/main.html index 18725bb961c..684526ac6aa 100644 --- a/ionic/components/input/test/input-focus/main.html +++ b/ionic/components/input/test/input-focus/main.html @@ -128,6 +128,11 @@ + + Bottom input: + + + diff --git a/ionic/platform/registry.ts b/ionic/platform/registry.ts index 72114d548a9..7347ba27f34 100644 --- a/ionic/platform/registry.ts +++ b/ionic/platform/registry.ts @@ -75,7 +75,6 @@ Platform.register({ hoverCSS: false, keyboardHeight: 300, mode: 'md', - scrollAssist: true, }, isMatch(p: Platform): boolean { return p.isPlatformMatch('android', ['android', 'silk'], ['windows phone']); @@ -98,6 +97,7 @@ Platform.register({ autoFocusAssist: 'delay', clickBlock: true, hoverCSS: false, + inputCloning: isIOSDevice, keyboardHeight: 300, mode: 'ios', scrollAssist: isIOSDevice, diff --git a/ionic/util/keyboard.ts b/ionic/util/keyboard.ts index 61009130a1d..aa6af126bc6 100644 --- a/ionic/util/keyboard.ts +++ b/ionic/util/keyboard.ts @@ -83,7 +83,7 @@ export class Keyboard { } function checkKeyboard() { - console.debug('keyboard isOpen', self.isOpen(), checks); + console.debug('keyboard isOpen', self.isOpen()); if (!self.isOpen() || checks > pollingChecksMax) { rafFrames(30, () => { self._zone.run(() => {