fix(input): keyboard focus/scrolling/tabbing

This commit is contained in:
Adam Bradley
2015-10-10 16:28:36 -05:00
parent 5f283cc045
commit 039ecac2ae
10 changed files with 479 additions and 355 deletions

View File

@ -4,7 +4,7 @@ import {Ion} from '../ion';
import {IonicConfig} from '../../config/config'; import {IonicConfig} from '../../config/config';
import {IonicKeyboard} from '../../util/keyboard'; import {IonicKeyboard} from '../../util/keyboard';
import {ViewController} from '../nav/view-controller'; import {ViewController} from '../nav/view-controller';
import {Tab} from '../tabs/tab'; import {Animation} from '../../animations/animation';
import {ScrollTo} from '../../animations/scroll-to'; import {ScrollTo} from '../../animations/scroll-to';
@ -25,8 +25,10 @@ import {ScrollTo} from '../../animations/scroll-to';
*/ */
@Component({ @Component({
selector: 'ion-content', selector: 'ion-content',
inputs: ['parallax'], template:
template: '<scroll-content><ng-content></ng-content></scroll-content>' '<scroll-content>' +
'<ng-content></ng-content>' +
'</scroll-content>'
}) })
export class Content extends Ion { export class Content extends Ion {
/** /**
@ -161,17 +163,29 @@ export class Content extends Ion {
* Adds padding to the bottom of the scroll element when the keyboard is open * Adds padding to the bottom of the scroll element when the keyboard is open
* so content below the keyboard can be scrolled into view. * so content below the keyboard can be scrolled into view.
*/ */
addKeyboardPadding(addPadding) { addScrollPadding(newScrollPadding) {
if (addPadding > this.scrollPadding) { if (newScrollPadding > this.scrollPadding) {
this.scrollPadding = addPadding; console.debug('addScrollPadding', newScrollPadding);
this.scrollElement.style.paddingBottom = addPadding + 'px';
this.scrollPadding = newScrollPadding;
this.scrollElement.style.paddingBottom = newScrollPadding + 'px';
if (!this.keyboardPromise) { if (!this.keyboardPromise) {
console.debug('add scroll keyboard close callback', newScrollPadding);
this.keyboardPromise = this.keyboard.onClose(() => { this.keyboardPromise = this.keyboard.onClose(() => {
console.debug('scroll keyboard closed', newScrollPadding);
if (this) { if (this) {
if (this.scrollPadding && this.scrollElement) {
let close = new Animation(this.scrollElement);
close
.duration(150)
.fromTo('paddingBottom', this.scrollPadding + 'px', '0px')
.play();
}
this.scrollPadding = 0; this.scrollPadding = 0;
if (this.scrollElement) this.scrollElement.style.paddingBottom = '';
this.keyboardPromise = null; this.keyboardPromise = null;
} }
}); });

View File

@ -25,9 +25,13 @@ export class Label {
* TODO * TODO
* @param {IonicConfig} config * @param {IonicConfig} config
*/ */
constructor(config: IonicConfig, @Optional() textInput: TextInput) { constructor(config: IonicConfig, @Optional() container: TextInput) {
this.scrollAssist = config.get('keyboardScrollAssist'); this.scrollAssist = config.get('scrollAssist');
textInput && textInput.registerLabel(this); if (!this.id) {
this.id = 'lbl-' + (++labelIds);
}
this.container = container;
container && container.registerLabel(this);
} }
/** /**
@ -55,7 +59,7 @@ export class Label {
if (!hasPointerMoved(20, this.startCoord, endCoord)) { if (!hasPointerMoved(20, this.startCoord, endCoord)) {
ev.preventDefault(); ev.preventDefault();
ev.stopPropagation(); ev.stopPropagation();
this.container.focus(); this.container.initFocus();
} }
this.startCoord = null; this.startCoord = null;
@ -63,3 +67,5 @@ export class Label {
} }
} }
let labelIds = -1;

View File

@ -0,0 +1,7 @@
import {App} from 'ionic/ionic';
@App({
templateUrl: 'main.html'
})
class E2EApp {}

View File

@ -0,0 +1,188 @@
<ion-toolbar><ion-title>Input Focus</ion-title></ion-toolbar>
<ion-content>
<p>Paragraph text with a <a href="#">link</a>.</p>
<ion-list>
<ion-input>
<ion-label>Text 1:</ion-label>
<input type="text">
</ion-input>
<ion-item>
Item with button right
<button item-right>Button 1</button>
</ion-item>
<ion-input>
<ion-label id="my-label1">Text 2:</ion-label>
<input value="value" type="text">
</ion-input>
<button ion-item>
Button Item
</button>
<ion-input>
<ion-label>Text 3:</ion-label>
<input type="text">
<button clear item-right>
<icon power></icon>
</button>
</ion-input>
<ion-input>
<ion-label>Comments:</ion-label>
<textarea>Comment value</textarea>
</ion-input>
<ion-input>
<icon globe item-left></icon>
<ion-label>Website:</ion-label>
<input value="http://ionic.io/" type="url">
</ion-input>
<ion-input>
<icon mail item-left></icon>
<ion-label>Email:</ion-label>
<input value="email6@email.com" type="email">
</ion-input>
<ion-input>
<icon create item-left></icon>
<ion-label>Feedback:</ion-label>
<textarea placeholder="Placeholder Text"></textarea>
</ion-input>
<ion-input>
<ion-label>More Info:</ion-label>
<input placeholder="Placeholder Text" type="text">
<icon flag item-right></icon>
</ion-input>
<ion-input>
<ion-label>Score:</ion-label>
<input value="10" type="number">
<button outline item-right>Update</button>
</ion-input>
<ion-input>
<ion-label>First Name:</ion-label>
<input value="Lightning" type="text">
</ion-input>
<ion-input>
<ion-label>Last Name:</ion-label>
<input value="McQueen" type="text">
</ion-input>
<ion-input>
<ion-label>Message:</ion-label>
<textarea>KA-CHOW!</textarea>
</ion-input>
<ion-item>
Item
</ion-item>
<ion-item>
Item
</ion-item>
<!--
<ion-item>
Item
</ion-item>
<ion-item>
Item
</ion-item>
<ion-item>
Item
</ion-item>
<ion-item>
Item
</ion-item>
<ion-item>
Item
</ion-item>
<ion-item>
Item
</ion-item>
<ion-item>
Item
</ion-item>
<ion-item>
Item
</ion-item>
<ion-item>
Item
</ion-item>
<ion-item>
Item
</ion-item>
<ion-item>
Item
</ion-item>
<ion-item>
Item
</ion-item>
<ion-item>
Item
</ion-item>
<ion-item>
Item
</ion-item>
<ion-item>
Item
</ion-item>
<ion-item>
Item
</ion-item>
<ion-item>
Item
</ion-item>
<ion-item>
Item
</ion-item>
<ion-item>
Item
</ion-item>
<ion-item>
Item
</ion-item>
<ion-item>
Item
</ion-item>
<ion-item>
Item
</ion-item> -->
</ion-list>
</ion-content>

View File

@ -2,9 +2,6 @@
// Text Input // Text Input
// -------------------------------------------------- // --------------------------------------------------
$input-focus-border-color: #51a7e8 !default;
$input-focus-box-shadow: inset 0px 0px 8px 0px $input-focus-border-color !default;
$text-input-background-color: $list-background-color !default; $text-input-background-color: $list-background-color !default;
@ -37,18 +34,6 @@ ion-input.item {
align-items: flex-start; align-items: flex-start;
} }
.key-input ion-input {
&.has-focus {
border-color: $input-focus-border-color;
box-shadow: $input-focus-box-shadow;
}
:focus {
outline: none;
}
}
ion-input [text-input] { ion-input [text-input] {
flex: 1; flex: 1;
background-color: $text-input-background-color; background-color: $text-input-background-color;
@ -59,6 +44,13 @@ ion-input.has-focus [text-input] {
pointer-events: auto; pointer-events: auto;
} }
ion-input input[scroll-assist] {
display: inline-block;
width: 1px;
height: 1px;
pointer-events: none;
}
ion-input textarea { ion-input textarea {
padding-top: 9px; padding-top: 9px;
} }
@ -80,4 +72,3 @@ input,
textarea { textarea {
@include placeholder(); @include placeholder();
} }

View File

@ -1,4 +1,4 @@
import {Directive, Host, Optional, ElementRef, Renderer, Attribute, Query, QueryList, NgZone} from 'angular2/angular2'; import {Component, Directive, NgIf, forwardRef, Host, Optional, ElementRef, Renderer, Attribute, Query, QueryList, NgZone} from 'angular2/angular2';
import {IonicConfig} from '../../config/config'; import {IonicConfig} from '../../config/config';
import {IonicForm} from '../../util/form'; import {IonicForm} from '../../util/form';
@ -12,29 +12,20 @@ import {IonicPlatform} from '../../platform/platform';
/** /**
* TODO * TODO
*/ */
@Directive({ @Component({
selector: 'ion-input', selector: 'ion-input',
host: { host: {
'(focus)': 'receivedFocus(true)',
'(blur)': 'receivedFocus(false)',
'(touchstart)': 'pointerStart($event)', '(touchstart)': 'pointerStart($event)',
'(touchend)': 'pointerEnd($event)', '(touchend)': 'pointerEnd($event)',
'(mouseup)': 'pointerEnd($event)', '(mouseup)': 'pointerEnd($event)'
'[class.has-focus]': 'hasFocus', },
'[class.has-value]': 'hasValue' template:
} '<ng-content></ng-content>' +
'<input [type]="type" aria-hidden="true" scroll-assist *ng-if="scrollAssist">',
directives: [NgIf, forwardRef(() => InputScrollAssist)]
}) })
export class TextInput { export class TextInput {
/**
* TODO
* @param {ElementRef} elementRef TODO
* @param {IonicConfig} config TODO
* @param {IonicApp} app TODO
* @param {NgZone} ngZone TODO
* @param {Content=} scrollView The parent scroll view.
* @param {QueryList<TextInputElement>} inputQry TODO
* @param {QueryList<Label>} labelQry TODO
*/
constructor( constructor(
form: IonicForm, form: IonicForm,
elementRef: ElementRef, elementRef: ElementRef,
@ -46,40 +37,44 @@ export class TextInput {
@Optional() @Host() scrollView: Content @Optional() @Host() scrollView: Content
) { ) {
renderer.setElementClass(elementRef, 'item', true); renderer.setElementClass(elementRef, 'item', true);
this.renderer = renderer;
this.form = form; this.form = form;
form.register(this); form.register(this);
this.type = 'text';
this.lastTouch = 0;
this.app = app; this.app = app;
this.elementRef = elementRef; this.elementRef = elementRef;
this.zone = zone; this.zone = zone;
this.platform = platform; this.platform = platform;
this.scrollView = scrollView; this.scrollView = scrollView;
this.scrollAssist = config.get('keyboardScrollAssist'); this.scrollAssist = config.get('scrollAssist');
this.keyboardHeight = config.get('keyboardHeight'); this.keyboardHeight = config.get('keyboardHeight');
} }
registerInput(textInputElement) { registerInput(textInputElement) {
this.input = textInputElement; this.input = textInputElement;
this.type = textInputElement.type; this.type = textInputElement.type || 'text';
} }
registerLabel(label) { registerLabel(label) {
this.label = label; this.label = label;
} }
/**
* TODO
*/
onInit() { onInit() {
if (this.input && this.label) { if (this.input && this.label) {
this.label.id = (this.label.id || 'label-' + this.inputId) // if there is an input and an label
// then give the label an ID
// and tell the input the ID of who it's labelled by
this.input.labelledBy(this.label.id); this.input.labelledBy(this.label.id);
} }
let self = this; let self = this;
self.scrollMove = (ev) => { self.scrollMove = (ev) => {
console.debug('content scrollMove');
self.deregListeners(); self.deregListeners();
if (self.hasFocus) { if (self.hasFocus) {
@ -88,10 +83,6 @@ export class TextInput {
}; };
} }
/**
* TODO
* @param {Event} ev TODO
*/
pointerStart(ev) { pointerStart(ev) {
if (this.scrollAssist && this.app.isEnabled()) { if (this.scrollAssist && this.app.isEnabled()) {
// remember where the touchstart/mousedown started // remember where the touchstart/mousedown started
@ -99,10 +90,6 @@ export class TextInput {
} }
} }
/**
* TODO
* @param {Event} ev TODO
*/
pointerEnd(ev) { pointerEnd(ev) {
if (!this.app.isEnabled()) { if (!this.app.isEnabled()) {
ev.preventDefault(); ev.preventDefault();
@ -122,29 +109,21 @@ export class TextInput {
this.initFocus(); this.initFocus();
// temporarily prevent mouseup's from focusing // temporarily prevent mouseup's from focusing
this.preventMouse = true; this.lastTouch = Date.now();
clearTimeout(this.mouseTimer);
this.mouseTimer = setTimeout(() => {
this.preventMouse = false;
}, 500);
}); });
} }
} else if (!this.preventMouse) { } else if (this.lastTouch + 500 < Date.now()) {
ev.preventDefault(); ev.preventDefault();
ev.stopPropagation(); ev.stopPropagation();
this.zone.runOutsideAngular(() => {
this.setFocus(); this.setFocus();
});
} }
} }
/**
* TODO
* @returns {TODO} TODO
*/
initFocus() { initFocus() {
// begin the process of setting focus to the inner input element
let scrollView = this.scrollView; let scrollView = this.scrollView;
if (scrollView && this.scrollAssist) { if (scrollView && this.scrollAssist) {
@ -161,7 +140,7 @@ export class TextInput {
} }
// add padding to the bottom of the scroll view (if needed) // add padding to the bottom of the scroll view (if needed)
scrollView.addKeyboardPadding(scrollData.scrollPadding); scrollView.addScrollPadding(scrollData.scrollPadding);
// manually scroll the text input to the top // manually scroll the text input to the top
// do not allow any clicks while it's scrolling // do not allow any clicks while it's scrolling
@ -308,33 +287,42 @@ export class TextInput {
return scrollData; return scrollData;
} }
/** focusChange(hasFocus) {
* TODO this.renderer.setElementClass(this.elementRef, 'has-focus', hasFocus);
*/ }
deregListeners() {
this.deregScroll && this.deregScroll(); hasValue(inputValue) {
this.renderer.setElementClass(this.elementRef, 'has-value', inputValue && inputValue !== '');
} }
/**
* TODO
*/
setFocus() { setFocus() {
if (this.input) {
this.zone.run(() => { this.zone.run(() => {
// set focus on the input element
this.input && this.input.initFocus(); this.form.setAsFocused(this);
// set focus on the actual input element
this.input.setFocus();
// ensure the body hasn't scrolled down // ensure the body hasn't scrolled down
document.body.scrollTop = 0; document.body.scrollTop = 0;
}); });
}
if (this.scrollAssist && this.scrollView) { if (this.scrollAssist && this.scrollView) {
setTimeout(() => { this.zone.runOutsideAngular(() => {
this.deregListeners(); this.deregListeners();
this.deregScroll = this.scrollView.addScrollEventListener(this.scrollMove); this.deregScroll = this.scrollView.addScrollEventListener(this.scrollMove);
}, 100); });
} }
} }
deregListeners() {
this.deregScroll && this.deregScroll();
}
tempFocusMove() { tempFocusMove() {
this.form.setFocusHolder(this.type); this.form.setFocusHolder(this.type);
} }
@ -343,33 +331,6 @@ export class TextInput {
return !!this.input && this.input.hasFocus; return !!this.input && this.input.hasFocus;
} }
get hasValue() {
return !!this.input && this.input.hasValue;
}
get tabIndex() {
return this.input && this.input.tabIndex;
}
set tabIndex(val) {
if (this.input) {
this.input.tabIndex = val;
}
}
/**
* TODO
* @param {boolean} receivedFocus TODO
*/
receivedFocus(receivedFocus) {
if (receivedFocus && !this.hasFocus) {
this.initFocus();
} else {
this.deregListeners();
}
}
onDestroy() { onDestroy() {
this.deregListeners(); this.deregListeners();
this.form.deregister(this); this.form.deregister(this);
@ -377,73 +338,80 @@ export class TextInput {
} }
/**
* TODO
*/
@Directive({ @Directive({
selector: 'textarea,input[type=text],input[type=password],input[type=number],input[type=search],input[type=email],input[type=url],input[type=tel]', selector: 'textarea,input[type=text],input[type=password],input[type=number],input[type=search],input[type=email],input[type=url],input[type=tel]',
inputs: [ inputs: ['value'],
'tabIndex'
],
host: { host: {
'[tabIndex]': 'tabIndex' '(focus)': 'wrapper.focusChange(true)',
'(blur)': 'wrapper.focusChange(false)',
'(keyup)': 'onKeyup($event)'
} }
}) })
export class TextInputElement { export class TextInputElement {
constructor( constructor(
form: IonicForm,
@Attribute('type') type: string, @Attribute('type') type: string,
elementRef: ElementRef, elementRef: ElementRef,
renderer: Renderer, renderer: Renderer,
@Optional() textInputWrapper: TextInput @Optional() wrapper: TextInput
) { ) {
this.form = form;
this.type = type; this.type = type;
this.elementRef = elementRef; this.elementRef = elementRef;
this.tabIndex = 0; this.wrapper = wrapper;
this.renderer = renderer; this.renderer = renderer;
renderer.setElementAttribute(this.elementRef, 'text-input', ''); renderer.setElementAttribute(this.elementRef, 'text-input', '');
if (textInputWrapper) { if (wrapper) {
// it's within ionic's ion-input, let ion-input handle what's up // it's within ionic's ion-input, let ion-input handle what's up
textInputWrapper.registerInput(this); wrapper.registerInput(this);
} else {
// not within ion-input
form.register(this);
} }
} }
onKeyup(ev) {
this.wrapper.hasValue(ev.target.value);
}
onInit() {
this.wrapper.hasValue(this.value);
}
labelledBy(val) { labelledBy(val) {
this.renderer.setElementAttribute(this.elementRef, 'aria-labelledby', val); val && this.renderer.setElementAttribute(this.elementRef, 'aria-labelledby', val);
} }
initFocus() { setFocus() {
this.elementRef.nativeElement.focus(); this.getNativeElement().focus();
} }
/**
* Whether the input has focus or not.
* @returns {boolean} true if the input has focus, otherwise false.
*/
get hasFocus() { get hasFocus() {
return dom.hasFocus(this.elementRef.nativeElement); return dom.hasFocus(this.getNativeElement());
} }
/** getNativeElement() {
* Whether the input has a value. return this.elementRef.nativeElement;
* @returns {boolean} true if the input has a value, otherwise false.
*/
get hasValue() {
return (this.elementRef.nativeElement.value !== '');
} }
onDestroy() {
this.form.deregister(this);
} }
@Directive({
selector: '[scroll-assist]',
host: {
'(focus)': 'receivedFocus($event)'
}
})
class InputScrollAssist {
constructor(form: IonicForm, textInput: TextInput) {
this.form = form;
this.textInput = textInput;
}
receivedFocus(ev) {
this.form.focusNext(this.textInput);
}
} }

View File

@ -49,7 +49,7 @@ IonicPlatform.register({
settings: { settings: {
mode: 'md', mode: 'md',
keyboardHeight: 290, keyboardHeight: 290,
keyboardScrollAssist: true, scrollAssist: true,
hoverCSS: false, hoverCSS: false,
}, },
isMatch(p) { isMatch(p) {
@ -71,7 +71,7 @@ IonicPlatform.register({
], ],
settings: { settings: {
mode: 'ios', mode: 'ios',
keyboardScrollAssist: function(p) { scrollAssist: function(p) {
return /iphone|ipad|ipod/i.test(p.navigatorPlatform()); return /iphone|ipad|ipod/i.test(p.navigatorPlatform());
}, },
keyboardHeight: 290, keyboardHeight: 290,

View File

@ -1,14 +1,11 @@
import {Injectable, NgZone} from 'angular2/angular2'; import {Injectable, NgZone} from 'angular2/angular2';
import {IonicConfig} from '../config/config'; import {IonicConfig} from '../config/config';
import {raf} from './dom';
/** /**
* The Input component is used to focus text input elements. * The Input component is used to focus text input elements.
* *
* The `focusNext()` and `focusPrevious()` methods make it easy to focus input elements across all devices.
*
* @usage * @usage
* ```html * ```html
* <ion-input> * <ion-input>
@ -25,150 +22,18 @@ export class IonicForm {
this._zone = zone; this._zone = zone;
this._inputs = []; this._inputs = [];
this._ids = -1;
this._focused = null; this._focused = null;
zone.runOutsideAngular(() => { zone.runOutsideAngular(() => {
this.initHolders(document, this._config.get('keyboardScrollAssist')); this.focusCtrl(document);
if (this._config.get('keyboardInputListener') !== false) {
this.initKeyInput(document);
}
}); });
} }
initKeyInput(document) {
/* Focus Outline
* --------------------------------------------------
* When a keydown event happens, from a tab key, then the
* 'key-input' class is added to the body element so focusable
* elements have an outline. On a mousedown or touchstart
* event then the 'key-input' class is removed.
*/
let isKeyInputEnabled = false;
function keyDown(ev) {
if (!isKeyInputEnabled && ev.keyCode == 9) {
isKeyInputEnabled = true;
raf(enableKeyInput);
}
}
function pointerDown() {
isKeyInputEnabled = false;
raf(enableKeyInput);
}
function enableKeyInput() {
document.body.classList[isKeyInputEnabled ? 'add' : 'remove']('key-input');
document.removeEventListener('mousedown', pointerDown);
document.removeEventListener('touchstart', pointerDown);
if (isKeyInputEnabled) {
document.addEventListener('mousedown', pointerDown);
document.addEventListener('touchstart', pointerDown);
}
}
document.addEventListener('keydown', keyDown);
}
initHolders(document, scrollAssist) {
// raw DOM fun
this._ctrl = document.createElement('focus-ctrl');
this._ctrl.setAttribute('aria-hidden', true);
if (scrollAssist) {
this._prev = document.createElement('input');
this._prev.tabIndex = NO_FOCUS_TAB_INDEX;
this._ctrl.appendChild(this._prev);
this._next = document.createElement('input');
this._next.tabIndex = NO_FOCUS_TAB_INDEX;
this._ctrl.appendChild(this._next);
this._temp = document.createElement('input');
this._temp.tabIndex = NO_FOCUS_TAB_INDEX;
this._ctrl.appendChild(this._temp);
}
this._blur = document.createElement('button');
this._blur.tabIndex = NO_FOCUS_TAB_INDEX;
this._ctrl.appendChild(this._blur);
document.body.appendChild(this._ctrl);
function preventDefault(ev) {
ev.preventDefault();
ev.stopPropagation();
}
if (scrollAssist) {
this._prev.addEventListener('keydown', preventDefault);
this._next.addEventListener('keydown', preventDefault);
this._temp.addEventListener('keydown', preventDefault);
this._prev.addEventListener('focus', () => {
this.focusPrevious();
});
this._next.addEventListener('focus', () => {
this.focusNext();
});
let self = this;
let resetTimer;
function queueReset() {
clearTimeout(resetTimer);
resetTimer = setTimeout(function() {
self._zone.run(() => {
self.resetInputs();
});
}, 100);
}
document.addEventListener('focusin', queueReset);
document.addEventListener('focusout', queueReset);
}
}
focusOut() {
console.debug('focusOut')
this._blur.tabIndex = NORMAL_FOCUS_TAB_INDEX;
this._blur.focus();
this._blur.tabIndex = NO_FOCUS_TAB_INDEX;
}
setFocusHolder(type) {
if (this._temp) {
this._temp.tabIndex = TEMP_TAB_INDEX;
this._temp.type = type || 'text';
console.debug('setFocusHolder', this._temp.type);
this._temp.focus();
}
}
/**
* @param {TODO} input TODO
*/
register(input) { register(input) {
console.debug('register input', input);
input.inputId = ++this._ids;
input.tabIndex = NORMAL_FOCUS_TAB_INDEX;
this._inputs.push(input); this._inputs.push(input);
} }
deregister(input) { deregister(input) {
console.debug('deregister input', input);
let index = this._inputs.indexOf(input); let index = this._inputs.indexOf(input);
if (index > -1) { if (index > -1) {
this._inputs.splice(index, 1); this._inputs.splice(index, 1);
@ -178,72 +43,72 @@ export class IonicForm {
} }
} }
resetInputs() { focusCtrl(document) {
this._focused = null; let scrollAssist = this._config.get('scrollAssist');
for (let i = 0, ii = this._inputs.length; i < ii; i++) { // raw DOM fun
if (!this._focused && this._inputs[i].hasFocus) { let focusCtrl = document.createElement('focus-ctrl');
this._focused = this._inputs[i]; focusCtrl.setAttribute('aria-hidden', true);
this._focused.tabIndex = ACTIVE_FOCUS_TAB_INDEX;
} else { if (scrollAssist) {
this._inputs[i].tabIndex = NORMAL_FOCUS_TAB_INDEX; this._tmp = document.createElement('input');
} this._tmp.tabIndex = -1;
focusCtrl.appendChild(this._tmp);
} }
if (this._temp) { this._blur = document.createElement('button');
this._temp.tabIndex = NO_FOCUS_TAB_INDEX; this._blur.tabIndex = -1;
focusCtrl.appendChild(this._blur);
if (this._focused) { document.body.appendChild(focusCtrl);
// there's a focused input
this._prev.tabIndex = PREV_TAB_INDEX;
this._next.tabIndex = NEXT_TAB_INDEX;
} else { if (scrollAssist) {
this._prev.tabIndex = this._next.tabIndex = NO_FOCUS_TAB_INDEX; this._tmp.addEventListener('keydown', (ev) => {
} ev.preventDefault();
ev.stopPropagation();
});
} }
} }
/** focusOut() {
* Focuses the previous input element, if it exists. console.debug('focusOut');
*/ this._blur.focus();
focusPrevious() { }
console.debug('focusPrevious');
this.focusMove(-1); 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;
} }
/** /**
* Focuses the next input element, if it exists. * Focuses the next input element, if it exists.
*/ */
focusNext() { focusNext(currentInput) {
console.debug('focusNext'); console.debug('focusNext');
this.focusMove(1);
let index = this._inputs.indexOf(currentInput);
if (index > -1 && (index + 1) < this._inputs.length) {
let nextInput = this._inputs[index + 1];
if (nextInput !== this._focused) {
return nextInput.initFocus();
}
} }
/** index = this._inputs.indexOf(this._focused);
* @param {Number} inc TODO if (index > 0) {
*/ let previousInput = this._inputs[index - 1];
focusMove(inc) { if (previousInput) {
let input = this._focused; previousInput.initFocus();
if (input) {
let index = this._inputs.indexOf(input);
if (index > -1 && (index + inc) < this._inputs.length) {
let siblingInput = this._inputs[index + inc];
if (siblingInput) {
return siblingInput.initFocus();
} }
} }
this._focused.initFocus();
}
} }
} }
const NO_FOCUS_TAB_INDEX = -1;
const NORMAL_FOCUS_TAB_INDEX = 0;
const PREV_TAB_INDEX = 999;
const ACTIVE_FOCUS_TAB_INDEX = 1000;
const NEXT_TAB_INDEX = 1001;
const TEMP_TAB_INDEX = 2000;

View File

@ -1,5 +1,6 @@
import {Injectable} from 'angular2/angular2'; import {Injectable, NgZone} from 'angular2/angular2';
import {IonicConfig} from '../config/config';
import {IonicForm} from './form'; import {IonicForm} from './form';
import * as dom from './dom'; import * as dom from './dom';
@ -7,8 +8,13 @@ import * as dom from './dom';
@Injectable() @Injectable()
export class IonicKeyboard { export class IonicKeyboard {
constructor(form: IonicForm) { constructor(config: IonicConfig, form: IonicForm, zone: NgZone) {
this.form = form; this.form = form;
this.zone = zone;
zone.runOutsideAngular(() => {
this.focusOutline(config.get('focusOutline'), document);
});
} }
isOpen() { isOpen() {
@ -25,16 +31,23 @@ export class IonicKeyboard {
promise = new Promise(resolve => { callback = resolve; }); promise = new Promise(resolve => { callback = resolve; });
} }
self.zone.runOutsideAngular(() => {
function checkKeyboard() { function checkKeyboard() {
if (!self.isOpen()) { if (!self.isOpen()) {
self.zone.run(() => {
console.debug('keyboard closed');
callback(); callback();
});
} else { } else {
setTimeout(checkKeyboard, 500); setTimeout(checkKeyboard, KEYBOARD_CLOSE_POLLING);
} }
} }
setTimeout(checkKeyboard, 100); setTimeout(checkKeyboard, KEYBOARD_CLOSE_POLLING);
});
return promise; return promise;
} }
@ -48,4 +61,64 @@ export class IonicKeyboard {
}); });
} }
focusOutline(setting, document) {
/* Focus Outline
* --------------------------------------------------
* By default, when a keydown event happens from a tab key, then
* the 'focus-outline' css class is added to the body element
* so focusable elements have an outline. On a mousedown or
* touchstart event, then the 'focus-outline' css class is removed.
*
* Config default overrides:
* focusOutline: true - Always add the focus-outline
* focusOutline: false - Do not add the focus-outline
*/
let isKeyInputEnabled = false;
function cssClass() {
dom.raf(() => {
document.body.classList[isKeyInputEnabled ? 'add' : 'remove']('focus-outline');
});
} }
if (setting === true) {
isKeyInputEnabled = true;
return cssClass();
} else if (setting === false) {
return;
}
// default is to add the focus-outline when the tab key is used
function keyDown(ev) {
if (!isKeyInputEnabled && ev.keyCode == 9) {
isKeyInputEnabled = true;
enableKeyInput();
}
}
function pointerDown() {
isKeyInputEnabled = false;
enableKeyInput();
}
function enableKeyInput() {
cssClass();
document.removeEventListener('mousedown', pointerDown);
document.removeEventListener('touchstart', pointerDown);
if (isKeyInputEnabled) {
document.addEventListener('mousedown', pointerDown);
document.addEventListener('touchstart', pointerDown);
}
}
document.addEventListener('keydown', keyDown);
}
}
const KEYBOARD_CLOSE_POLLING = 150;

View File

@ -27,17 +27,17 @@
// Focus Outline // Focus Outline
// -------------------------------------------------- // --------------------------------------------------
// When a keydown event happens, from a tab key, then the
// 'key-input' class is added to the body element so focusable $focus-outline-border-color: #51a7e8 !default;
// elements have an outline. On a mousedown or touchstart $focus-outline-box-shadow: 0px 0px 8px 0px $focus-outline-border-color !default;
// event then the 'key-input' class is removed.
:focus, :focus,
:active { :active {
outline: none; outline: none;
} }
.key-input { .focus-outline {
:focus { :focus {
outline-offset: -1px; outline-offset: -1px;
@ -46,8 +46,20 @@
button:focus, button:focus,
[button]:focus { [button]:focus {
outline-offset: -2px; border-color: $focus-outline-border-color;
outline: 2px solid red; box-shadow: $focus-outline-box-shadow;
outline: thin solid $focus-outline-border-color;
}
ion-input.has-focus,
button[ion-item]:focus,
a[ion-item]:focus {
border-color: $focus-outline-border-color;
box-shadow: inset $focus-outline-box-shadow !important;
}
ion-input :focus {
outline: none;
} }
} }