fix(focus): improve input focus control

Closes #5536
This commit is contained in:
Adam Bradley
2016-04-14 09:31:09 -05:00
parent 4ba9eb0807
commit e27452b789
7 changed files with 114 additions and 70 deletions

View File

@ -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<boolean> = new EventEmitter();
@Output() valueChange: EventEmitter<string> = 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();
}
/**
* @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';
this.setFocus();
raf(() => {
focusedInputEle.classList.add('cloned-active');
});
} else {
focusedInputEle.classList.remove('cloned-active');
focusedInputEle.style[CSS.transform] = '';
focusedInputEle.style.opacity = '';
removeClone(focusedInputEle, 'cloned-focus');
}
this._relocated = shouldRelocate;
// 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
*/
hideFocus(shouldHideFocus: boolean) {
console.debug('native input hideFocus', shouldHideFocus);
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();
if (this._clone) {
focusedInputEle.classList.add('cloned-active');
}
} else {
// 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 = shouldFocus;
}
}
hideFocus(shouldHideFocus: boolean) {
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);
}