Skip to content

Commit e27452b

Browse files
committed
fix(focus): improve input focus control
Closes #5536
1 parent 4ba9eb0 commit e27452b

File tree

7 files changed

+105
-61
lines changed

7 files changed

+105
-61
lines changed

ionic/components/input/input-base.ts

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -340,19 +340,17 @@ export class InputBase {
340340
*/
341341
initFocus() {
342342
// begin the process of setting focus to the inner input element
343-
let scrollView = this._scrollView;
343+
var scrollView = this._scrollView;
344344

345345
if (scrollView) {
346346
// this input is inside of a scroll view
347-
348347
// find out if text input should be manually scrolled into view
349-
let ele = this._elementRef.nativeElement;
350-
let itemEle = closest(ele, 'ion-item');
351-
if (itemEle) {
352-
ele = itemEle;
353-
}
354348

355-
let scrollData = InputBase.getScrollData(ele.offsetTop, ele.offsetHeight, scrollView.getContentDimensions(), this._keyboardHeight, this._platform.height());
349+
// get container of this input, probably an ion-item a few nodes up
350+
var ele = this._elementRef.nativeElement;
351+
ele = closest(ele, 'ion-item,[ion-item]') || ele;
352+
353+
var scrollData = InputBase.getScrollData(ele.offsetTop, ele.offsetHeight, scrollView.getContentDimensions(), this._keyboardHeight, this._platform.height());
356354
if (scrollData.scrollAmount > -3 && scrollData.scrollAmount < 3) {
357355
// the text input is in a safe position that doesn't
358356
// require it to be scrolled into view, just set focus now
@@ -368,21 +366,22 @@ export class InputBase {
368366

369367
// manually scroll the text input to the top
370368
// do not allow any clicks while it's scrolling
371-
let scrollDuration = getScrollAssistDuration(scrollData.scrollAmount);
369+
var scrollDuration = getScrollAssistDuration(scrollData.scrollAmount);
372370
this._app.setEnabled(false, scrollDuration);
373371
this._nav && this._nav.setTransitioning(true, scrollDuration);
374372

375373
// temporarily move the focus to the focus holder so the browser
376374
// doesn't freak out while it's trying to get the input in place
377375
// at this point the native text input still does not have focus
378-
this._native.relocate(true, scrollData.inputSafeY);
376+
this._native.beginFocus(true, scrollData.inputSafeY);
379377

380378
// scroll the input into place
381379
scrollView.scrollTo(0, scrollData.scrollTo, scrollDuration).then(() => {
382380
// the scroll view is in the correct position now
383381
// give the native text input focus
384-
this._native.relocate(false, 0);
382+
this._native.beginFocus(false, 0);
385383

384+
// ensure this is the focused input
386385
this.setFocus();
387386

388387
// all good, allow clicks again
@@ -417,6 +416,7 @@ export class InputBase {
417416
this._form.setAsFocused(this);
418417

419418
// set focus on the actual input element
419+
console.debug(`input-base, setFocus ${this._native.element().value}`);
420420
this._native.setFocus();
421421

422422
// ensure the body hasn't scrolled down

ionic/components/input/input.scss

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,15 @@ input.text-input:-webkit-autofill {
9191
display: none;
9292
}
9393

94+
.input-has-focus {
95+
pointer-events: none;
96+
}
97+
98+
.input-has-focus input,
99+
.input-has-focus textarea {
100+
pointer-events: auto;
101+
}
102+
94103

95104
// Scroll Assist Input
96105
// --------------------------------------------------
@@ -131,7 +140,7 @@ input.text-input:-webkit-autofill {
131140
// --------------------------------------------------
132141

133142
.text-input.cloned-input {
134-
position: absolute;
143+
position: relative;
135144
top: 0;
136145

137146
pointer-events: none;

ionic/components/input/native-input.ts

Lines changed: 56 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import {Directive, Attribute, ElementRef, Renderer, Input, Output, EventEmitter, HostListener} from 'angular2/core';
22
import {NgControl} from 'angular2/common';
33

4+
import {Config} from '../../config/config';
45
import {CSS, hasFocus, raf} from '../../util/dom';
56

67

@@ -12,35 +13,30 @@ import {CSS, hasFocus, raf} from '../../util/dom';
1213
})
1314
export class NativeInput {
1415
private _relocated: boolean;
16+
private _clone: boolean;
1517

1618
@Output() focusChange: EventEmitter<boolean> = new EventEmitter();
1719
@Output() valueChange: EventEmitter<string> = new EventEmitter();
1820

1921
constructor(
2022
private _elementRef: ElementRef,
2123
private _renderer: Renderer,
24+
config: Config,
2225
public ngControl: NgControl
23-
) {}
26+
) {
27+
this._clone = config.getBoolean('inputCloning', false);
28+
}
2429

25-
/**
26-
* @private
27-
*/
2830
@HostListener('input', ['$event'])
2931
private _change(ev) {
3032
this.valueChange.emit(ev.target.value);
3133
}
3234

33-
/**
34-
* @private
35-
*/
3635
@HostListener('focus')
3736
private _focus() {
3837
this.focusChange.emit(true);
3938
}
4039

41-
/**
42-
* @private
43-
*/
4440
@HostListener('blur')
4541
private _blur() {
4642
this.focusChange.emit(false);
@@ -55,55 +51,69 @@ export class NativeInput {
5551
this._renderer.setElementAttribute(this._elementRef.nativeElement, 'disabled', val ? '' : null);
5652
}
5753

58-
/**
59-
* @private
60-
*/
6154
setFocus() {
62-
this.element().focus();
55+
// let's set focus to the element
56+
// but only if it does not already have focus
57+
if (document.activeElement !== this.element()) {
58+
this.element().focus();
59+
}
6360
}
6461

65-
/**
66-
* @private
67-
*/
68-
relocate(shouldRelocate: boolean, inputRelativeY: number) {
69-
console.debug('native input relocate', shouldRelocate, inputRelativeY);
70-
71-
if (this._relocated !== shouldRelocate) {
72-
73-
let focusedInputEle = this.element();
74-
if (shouldRelocate) {
75-
let clonedInputEle = cloneInput(focusedInputEle, 'cloned-focus');
76-
77-
focusedInputEle.parentNode.insertBefore(clonedInputEle, focusedInputEle);
78-
focusedInputEle.style[CSS.transform] = `translate3d(-9999px,${inputRelativeY}px,0)`;
79-
focusedInputEle.style.opacity = '0';
80-
62+
beginFocus(shouldFocus: boolean, inputRelativeY: number) {
63+
if (this._relocated !== shouldFocus) {
64+
var focusedInputEle = this.element();
65+
if (shouldFocus) {
66+
// we should focus into this element
67+
68+
if (this._clone) {
69+
// this platform needs the input to be cloned
70+
// this allows for the actual input to receive the focus from
71+
// the user's touch event, but before it receives focus, it
72+
// moves the actual input to a location that will not screw
73+
// up the app's layout, and does not allow the native browser
74+
// to attempt to scroll the input into place (messing up headers/footers)
75+
// the cloned input fills the area of where native input should be
76+
// while the native input fakes out the browser by relocating itself
77+
// before it receives the actual focus event
78+
var clonedInputEle = cloneInput(focusedInputEle, 'cloned-focus');
79+
focusedInputEle.parentNode.insertBefore(clonedInputEle, focusedInputEle);
80+
81+
// move the native input to a location safe to receive focus
82+
// according to the browser, the native input receives focus in an
83+
// area which doesn't require the browser to scroll the input into place
84+
focusedInputEle.style[CSS.transform] = `translate3d(-9999px,${inputRelativeY}px,0)`;
85+
focusedInputEle.style.opacity = '0';
86+
}
87+
88+
// let's now set focus to the actual native element
89+
// at this point it is safe to assume the browser will not attempt
90+
// to scroll the input into view itself (screwing up headers/footers)
8191
this.setFocus();
8292

83-
raf(() => {
93+
if (this._clone) {
8494
focusedInputEle.classList.add('cloned-active');
85-
});
95+
}
8696

8797
} else {
88-
focusedInputEle.classList.remove('cloned-active');
89-
focusedInputEle.style[CSS.transform] = '';
90-
focusedInputEle.style.opacity = '';
91-
92-
removeClone(focusedInputEle, 'cloned-focus');
98+
// should remove the focus
99+
if (this._clone) {
100+
// should remove the cloned node
101+
focusedInputEle.classList.remove('cloned-active');
102+
focusedInputEle.style[CSS.transform] = '';
103+
focusedInputEle.style.opacity = '';
104+
removeClone(focusedInputEle, 'cloned-focus');
105+
}
93106
}
94107

95-
this._relocated = shouldRelocate;
108+
this._relocated = shouldFocus;
96109
}
97110
}
98111

99-
/**
100-
* @private
101-
*/
102112
hideFocus(shouldHideFocus: boolean) {
103-
console.debug('native input hideFocus', shouldHideFocus);
104-
105113
let focusedInputEle = this.element();
106114

115+
console.debug(`native input hideFocus, shouldHideFocus: ${shouldHideFocus}, input value: ${focusedInputEle.value}`);
116+
107117
if (shouldHideFocus) {
108118
let clonedInputEle = cloneInput(focusedInputEle, 'cloned-move');
109119

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

127-
/**
128-
* @private
129-
*/
130137
element(): HTMLInputElement {
131138
return this._elementRef.nativeElement;
132139
}
@@ -141,6 +148,8 @@ function cloneInput(focusedInputEle, addCssClass) {
141148
clonedInputEle.removeAttribute('aria-labelledby');
142149
clonedInputEle.tabIndex = -1;
143150
clonedInputEle.style.width = (focusedInputEle.offsetWidth + 10) + 'px';
151+
clonedInputEle.style.height = focusedInputEle.offsetHeight + 'px';
152+
clonedInputEle.value = focusedInputEle.value;
144153
return clonedInputEle;
145154
}
146155

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

165174
@HostListener('focus')
166175
receivedFocus() {
176+
console.debug('native-input, next-input received focus');
167177
this.focused.emit(true);
168178
}
169179

ionic/components/input/test/input-focus/index.ts

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,31 @@ import {App} from 'ionic-angular';
44
@App({
55
templateUrl: 'main.html',
66
config: {
7-
scrollAssist: true
7+
//scrollAssist: true
88
}
99
})
1010
class E2EApp {
1111
reload() {
1212
window.location.reload();
1313
}
1414
}
15+
16+
document.addEventListener('click', function(ev) {
17+
console.log(`CLICK, ${ev.target.localName}.${ev.target.className}, time: ${Date.now()}`);
18+
});
19+
20+
document.addEventListener('touchstart', function(ev) {
21+
console.log(`TOUCH START, ${ev.target.localName}.${ev.target.className}, time: ${Date.now()}`);
22+
});
23+
24+
document.addEventListener('touchend', function(ev) {
25+
console.log(`TOUCH END, ${ev.target.localName}.${ev.target.className}, time: ${Date.now()}`);
26+
});
27+
28+
document.addEventListener('focusin', function(ev) {console.log(`CLICK, ${ev.target.localName}.${ev.target.className}, time: ${Date.now()}`);
29+
console.log(`FOCUS IN, ${ev.target.localName}.${ev.target.className}, time: ${Date.now()}`);
30+
});
31+
32+
document.addEventListener('focusout', function(ev) {console.log(`CLICK, ${ev.target.localName}.${ev.target.className}, time: ${Date.now()}`);
33+
console.log(`FOCUS OUT, ${ev.target.localName}.${ev.target.className}, time: ${Date.now()}`);
34+
});

ionic/components/input/test/input-focus/main.html

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,11 @@
128128
<ion-radio value="2"></ion-radio>
129129
</ion-item>
130130

131+
<ion-item>
132+
<ion-label>Bottom input:</ion-label>
133+
<ion-input value="bottom input"></ion-input>
134+
</ion-item>
135+
131136
</ion-list>
132137

133138
</ion-content>

ionic/platform/registry.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,6 @@ Platform.register({
7575
hoverCSS: false,
7676
keyboardHeight: 300,
7777
mode: 'md',
78-
scrollAssist: true,
7978
},
8079
isMatch(p: Platform): boolean {
8180
return p.isPlatformMatch('android', ['android', 'silk'], ['windows phone']);
@@ -98,6 +97,7 @@ Platform.register({
9897
autoFocusAssist: 'delay',
9998
clickBlock: true,
10099
hoverCSS: false,
100+
inputCloning: isIOSDevice,
101101
keyboardHeight: 300,
102102
mode: 'ios',
103103
scrollAssist: isIOSDevice,

ionic/util/keyboard.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ export class Keyboard {
8383
}
8484

8585
function checkKeyboard() {
86-
console.debug('keyboard isOpen', self.isOpen(), checks);
86+
console.debug('keyboard isOpen', self.isOpen());
8787
if (!self.isOpen() || checks > pollingChecksMax) {
8888
rafFrames(30, () => {
8989
self._zone.run(() => {

0 commit comments

Comments
 (0)