Skip to content

Commit

Permalink
fix(focus): improve input focus control
Browse files Browse the repository at this point in the history
Closes #5536
  • Loading branch information
adamdbradley committed Apr 14, 2016
1 parent 4ba9eb0 commit e27452b
Show file tree
Hide file tree
Showing 7 changed files with 105 additions and 61 deletions.
22 changes: 11 additions & 11 deletions ionic/components/input/input-base.ts
Expand Up @@ -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;

This comment has been minimized.

Copy link
@trakhimenok

trakhimenok Apr 14, 2016

@adamdbradley can I ask why are you switching back to var from let? I've seen before you were replacing var's with let's and now it is opposite.

Just trying to learn from "big" guys.

This comment has been minimized.

Copy link
@adamdbradley

adamdbradley Apr 14, 2016

Author Contributor

That particular one could have stayed "let" because it really wouldn't matter. But I changed the "lets" that were inside of the "if" blocks just because I didn't like how the transpiled code was making nested functions inside of this function, just because I used let. But really it doesn't matter much, I was only influenced by how typescript transpiled it to es5.


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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down
11 changes: 10 additions & 1 deletion ionic/components/input/input.scss
Expand Up @@ -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
// --------------------------------------------------
Expand Down Expand Up @@ -131,7 +140,7 @@ input.text-input:-webkit-autofill {
// --------------------------------------------------

.text-input.cloned-input {
position: absolute;
position: relative;
top: 0;

pointer-events: none;
Expand Down
102 changes: 56 additions & 46 deletions 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';


Expand All @@ -12,35 +13,30 @@ 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();

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);
Expand All @@ -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');

Expand All @@ -124,9 +134,6 @@ export class NativeInput {
return this.element().value;
}

/**
* @private
*/
element(): HTMLInputElement {
return this._elementRef.nativeElement;
}
Expand All @@ -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;
}

Expand All @@ -164,6 +173,7 @@ export class NextInput {

@HostListener('focus')
receivedFocus() {
console.debug('native-input, next-input received focus');
this.focused.emit(true);
}

Expand Down
22 changes: 21 additions & 1 deletion ionic/components/input/test/input-focus/index.ts
Expand Up @@ -4,11 +4,31 @@ import {App} from 'ionic-angular';
@App({
templateUrl: 'main.html',
config: {
scrollAssist: true
//scrollAssist: true
}
})
class E2EApp {
reload() {
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()}`);
});
5 changes: 5 additions & 0 deletions ionic/components/input/test/input-focus/main.html
Expand Up @@ -128,6 +128,11 @@
<ion-radio value="2"></ion-radio>
</ion-item>

<ion-item>
<ion-label>Bottom input:</ion-label>
<ion-input value="bottom input"></ion-input>
</ion-item>

</ion-list>

</ion-content>
2 changes: 1 addition & 1 deletion ionic/platform/registry.ts
Expand Up @@ -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']);
Expand All @@ -98,6 +97,7 @@ Platform.register({
autoFocusAssist: 'delay',
clickBlock: true,
hoverCSS: false,
inputCloning: isIOSDevice,
keyboardHeight: 300,
mode: 'ios',
scrollAssist: isIOSDevice,
Expand Down
2 changes: 1 addition & 1 deletion ionic/util/keyboard.ts
Expand Up @@ -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(() => {
Expand Down

0 comments on commit e27452b

Please sign in to comment.