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 {IonicKeyboard} from '../../util/keyboard';
import {ViewController} from '../nav/view-controller';
import {Tab} from '../tabs/tab';
import {Animation} from '../../animations/animation';
import {ScrollTo} from '../../animations/scroll-to';
@ -25,8 +25,10 @@ import {ScrollTo} from '../../animations/scroll-to';
*/
@Component({
selector: 'ion-content',
inputs: ['parallax'],
template: '<scroll-content><ng-content></ng-content></scroll-content>'
template:
'<scroll-content>' +
'<ng-content></ng-content>' +
'</scroll-content>'
})
export class Content extends Ion {
/**
@ -38,7 +40,7 @@ export class Content extends Ion {
this.scrollPadding = 0;
this.keyboard = keyboard;
if(viewCtrl) {
if (viewCtrl) {
viewCtrl.setContent(this);
}
}
@ -58,7 +60,7 @@ export class Content extends Ion {
* @returns {Function} A function that removes the scroll handler.
*/
addScrollEventListener(handler) {
if(!this.scrollElement) { return; }
if (!this.scrollElement) { return; }
// ensure we're not creating duplicates
this.scrollElement.removeEventListener('scroll', handler);
@ -76,7 +78,7 @@ export class Content extends Ion {
* @returns {Function} A function that removes the touchmove handler.
*/
addTouchMoveListener(handler) {
if(!this.scrollElement) { return; }
if (!this.scrollElement) { return; }
// ensure we're not creating duplicates
this.scrollElement.removeEventListener('touchmove', handler);
@ -161,17 +163,29 @@ export class Content extends Ion {
* Adds padding to the bottom of the scroll element when the keyboard is open
* so content below the keyboard can be scrolled into view.
*/
addKeyboardPadding(addPadding) {
if (addPadding > this.scrollPadding) {
this.scrollPadding = addPadding;
this.scrollElement.style.paddingBottom = addPadding + 'px';
addScrollPadding(newScrollPadding) {
if (newScrollPadding > this.scrollPadding) {
console.debug('addScrollPadding', newScrollPadding);
this.scrollPadding = newScrollPadding;
this.scrollElement.style.paddingBottom = newScrollPadding + 'px';
if (!this.keyboardPromise) {
console.debug('add scroll keyboard close callback', newScrollPadding);
this.keyboardPromise = this.keyboard.onClose(() => {
console.debug('scroll keyboard closed', newScrollPadding);
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;
if (this.scrollElement) this.scrollElement.style.paddingBottom = '';
this.keyboardPromise = null;
}
});

View File

@ -25,9 +25,13 @@ export class Label {
* TODO
* @param {IonicConfig} config
*/
constructor(config: IonicConfig, @Optional() textInput: TextInput) {
this.scrollAssist = config.get('keyboardScrollAssist');
textInput && textInput.registerLabel(this);
constructor(config: IonicConfig, @Optional() container: TextInput) {
this.scrollAssist = config.get('scrollAssist');
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)) {
ev.preventDefault();
ev.stopPropagation();
this.container.focus();
this.container.initFocus();
}
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
// --------------------------------------------------
$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;
@ -37,18 +34,6 @@ ion-input.item {
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] {
flex: 1;
background-color: $text-input-background-color;
@ -59,6 +44,13 @@ ion-input.has-focus [text-input] {
pointer-events: auto;
}
ion-input input[scroll-assist] {
display: inline-block;
width: 1px;
height: 1px;
pointer-events: none;
}
ion-input textarea {
padding-top: 9px;
}
@ -80,4 +72,3 @@ input,
textarea {
@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 {IonicForm} from '../../util/form';
@ -12,29 +12,20 @@ import {IonicPlatform} from '../../platform/platform';
/**
* TODO
*/
@Directive({
@Component({
selector: 'ion-input',
host: {
'(focus)': 'receivedFocus(true)',
'(blur)': 'receivedFocus(false)',
'(touchstart)': 'pointerStart($event)',
'(touchend)': 'pointerEnd($event)',
'(mouseup)': 'pointerEnd($event)',
'[class.has-focus]': 'hasFocus',
'[class.has-value]': 'hasValue'
}
'(mouseup)': 'pointerEnd($event)'
},
template:
'<ng-content></ng-content>' +
'<input [type]="type" aria-hidden="true" scroll-assist *ng-if="scrollAssist">',
directives: [NgIf, forwardRef(() => InputScrollAssist)]
})
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(
form: IonicForm,
elementRef: ElementRef,
@ -46,40 +37,44 @@ export class TextInput {
@Optional() @Host() scrollView: Content
) {
renderer.setElementClass(elementRef, 'item', true);
this.renderer = renderer;
this.form = form;
form.register(this);
this.type = 'text';
this.lastTouch = 0;
this.app = app;
this.elementRef = elementRef;
this.zone = zone;
this.platform = platform;
this.scrollView = scrollView;
this.scrollAssist = config.get('keyboardScrollAssist');
this.scrollAssist = config.get('scrollAssist');
this.keyboardHeight = config.get('keyboardHeight');
}
registerInput(textInputElement) {
this.input = textInputElement;
this.type = textInputElement.type;
this.type = textInputElement.type || 'text';
}
registerLabel(label) {
this.label = label;
}
/**
* TODO
*/
onInit() {
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);
}
let self = this;
self.scrollMove = (ev) => {
console.debug('content scrollMove');
self.deregListeners();
if (self.hasFocus) {
@ -88,10 +83,6 @@ export class TextInput {
};
}
/**
* TODO
* @param {Event} ev TODO
*/
pointerStart(ev) {
if (this.scrollAssist && this.app.isEnabled()) {
// remember where the touchstart/mousedown started
@ -99,10 +90,6 @@ export class TextInput {
}
}
/**
* TODO
* @param {Event} ev TODO
*/
pointerEnd(ev) {
if (!this.app.isEnabled()) {
ev.preventDefault();
@ -122,29 +109,21 @@ export class TextInput {
this.initFocus();
// temporarily prevent mouseup's from focusing
this.preventMouse = true;
clearTimeout(this.mouseTimer);
this.mouseTimer = setTimeout(() => {
this.preventMouse = false;
}, 500);
this.lastTouch = Date.now();
});
}
} else if (!this.preventMouse) {
} else if (this.lastTouch + 500 < Date.now()) {
ev.preventDefault();
ev.stopPropagation();
this.zone.runOutsideAngular(() => {
this.setFocus();
});
this.setFocus();
}
}
/**
* TODO
* @returns {TODO} TODO
*/
initFocus() {
// begin the process of setting focus to the inner input element
let scrollView = this.scrollView;
if (scrollView && this.scrollAssist) {
@ -161,7 +140,7 @@ export class TextInput {
}
// 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
// do not allow any clicks while it's scrolling
@ -308,33 +287,42 @@ export class TextInput {
return scrollData;
}
/**
* TODO
*/
deregListeners() {
this.deregScroll && this.deregScroll();
focusChange(hasFocus) {
this.renderer.setElementClass(this.elementRef, 'has-focus', hasFocus);
}
/**
* TODO
*/
setFocus() {
this.zone.run(() => {
// set focus on the input element
this.input && this.input.initFocus();
hasValue(inputValue) {
this.renderer.setElementClass(this.elementRef, 'has-value', inputValue && inputValue !== '');
}
// ensure the body hasn't scrolled down
document.body.scrollTop = 0;
});
setFocus() {
if (this.input) {
this.zone.run(() => {
this.form.setAsFocused(this);
// set focus on the actual input element
this.input.setFocus();
// ensure the body hasn't scrolled down
document.body.scrollTop = 0;
});
}
if (this.scrollAssist && this.scrollView) {
setTimeout(() => {
this.zone.runOutsideAngular(() => {
this.deregListeners();
this.deregScroll = this.scrollView.addScrollEventListener(this.scrollMove);
}, 100);
});
}
}
deregListeners() {
this.deregScroll && this.deregScroll();
}
tempFocusMove() {
this.form.setFocusHolder(this.type);
}
@ -343,33 +331,6 @@ export class TextInput {
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() {
this.deregListeners();
this.form.deregister(this);
@ -377,73 +338,80 @@ export class TextInput {
}
/**
* TODO
*/
@Directive({
selector: 'textarea,input[type=text],input[type=password],input[type=number],input[type=search],input[type=email],input[type=url],input[type=tel]',
inputs: [
'tabIndex'
],
inputs: ['value'],
host: {
'[tabIndex]': 'tabIndex'
'(focus)': 'wrapper.focusChange(true)',
'(blur)': 'wrapper.focusChange(false)',
'(keyup)': 'onKeyup($event)'
}
})
export class TextInputElement {
constructor(
form: IonicForm,
@Attribute('type') type: string,
elementRef: ElementRef,
renderer: Renderer,
@Optional() textInputWrapper: TextInput
@Optional() wrapper: TextInput
) {
this.form = form;
this.type = type;
this.elementRef = elementRef;
this.tabIndex = 0;
this.wrapper = wrapper;
this.renderer = renderer;
renderer.setElementAttribute(this.elementRef, 'text-input', '');
if (textInputWrapper) {
if (wrapper) {
// it's within ionic's ion-input, let ion-input handle what's up
textInputWrapper.registerInput(this);
} else {
// not within ion-input
form.register(this);
wrapper.registerInput(this);
}
}
onKeyup(ev) {
this.wrapper.hasValue(ev.target.value);
}
onInit() {
this.wrapper.hasValue(this.value);
}
labelledBy(val) {
this.renderer.setElementAttribute(this.elementRef, 'aria-labelledby', val);
val && this.renderer.setElementAttribute(this.elementRef, 'aria-labelledby', val);
}
initFocus() {
this.elementRef.nativeElement.focus();
setFocus() {
this.getNativeElement().focus();
}
/**
* Whether the input has focus or not.
* @returns {boolean} true if the input has focus, otherwise false.
*/
get hasFocus() {
return dom.hasFocus(this.elementRef.nativeElement);
return dom.hasFocus(this.getNativeElement());
}
/**
* Whether the input has a value.
* @returns {boolean} true if the input has a value, otherwise false.
*/
get hasValue() {
return (this.elementRef.nativeElement.value !== '');
getNativeElement() {
return this.elementRef.nativeElement;
}
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: {
mode: 'md',
keyboardHeight: 290,
keyboardScrollAssist: true,
scrollAssist: true,
hoverCSS: false,
},
isMatch(p) {
@ -71,7 +71,7 @@ IonicPlatform.register({
],
settings: {
mode: 'ios',
keyboardScrollAssist: function(p) {
scrollAssist: function(p) {
return /iphone|ipad|ipod/i.test(p.navigatorPlatform());
},
keyboardHeight: 290,

View File

@ -1,14 +1,11 @@
import {Injectable, NgZone} from 'angular2/angular2';
import {IonicConfig} from '../config/config';
import {raf} from './dom';
/**
* 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
* ```html
* <ion-input>
@ -25,150 +22,18 @@ export class IonicForm {
this._zone = zone;
this._inputs = [];
this._ids = -1;
this._focused = null;
zone.runOutsideAngular(() => {
this.initHolders(document, this._config.get('keyboardScrollAssist'));
if (this._config.get('keyboardInputListener') !== false) {
this.initKeyInput(document);
}
this.focusCtrl(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) {
console.debug('register input', input);
input.inputId = ++this._ids;
input.tabIndex = NORMAL_FOCUS_TAB_INDEX;
this._inputs.push(input);
}
deregister(input) {
console.debug('deregister input', input);
let index = this._inputs.indexOf(input);
if (index > -1) {
this._inputs.splice(index, 1);
@ -178,72 +43,72 @@ export class IonicForm {
}
}
resetInputs() {
this._focused = null;
focusCtrl(document) {
let scrollAssist = this._config.get('scrollAssist');
for (let i = 0, ii = this._inputs.length; i < ii; i++) {
if (!this._focused && this._inputs[i].hasFocus) {
this._focused = this._inputs[i];
this._focused.tabIndex = ACTIVE_FOCUS_TAB_INDEX;
// raw DOM fun
let focusCtrl = document.createElement('focus-ctrl');
focusCtrl.setAttribute('aria-hidden', true);
} else {
this._inputs[i].tabIndex = NORMAL_FOCUS_TAB_INDEX;
}
if (scrollAssist) {
this._tmp = document.createElement('input');
this._tmp.tabIndex = -1;
focusCtrl.appendChild(this._tmp);
}
if (this._temp) {
this._temp.tabIndex = NO_FOCUS_TAB_INDEX;
this._blur = document.createElement('button');
this._blur.tabIndex = -1;
focusCtrl.appendChild(this._blur);
if (this._focused) {
// there's a focused input
this._prev.tabIndex = PREV_TAB_INDEX;
this._next.tabIndex = NEXT_TAB_INDEX;
document.body.appendChild(focusCtrl);
} else {
this._prev.tabIndex = this._next.tabIndex = NO_FOCUS_TAB_INDEX;
}
if (scrollAssist) {
this._tmp.addEventListener('keydown', (ev) => {
ev.preventDefault();
ev.stopPropagation();
});
}
}
/**
* Focuses the previous input element, if it exists.
*/
focusPrevious() {
console.debug('focusPrevious');
this.focusMove(-1);
focusOut() {
console.debug('focusOut');
this._blur.focus();
}
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.
*/
focusNext() {
focusNext(currentInput) {
console.debug('focusNext');
this.focusMove(1);
}
/**
* @param {Number} inc TODO
*/
focusMove(inc) {
let input = this._focused;
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();
}
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);
if (index > 0) {
let previousInput = this._inputs[index - 1];
if (previousInput) {
previousInput.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 * as dom from './dom';
@ -7,8 +8,13 @@ import * as dom from './dom';
@Injectable()
export class IonicKeyboard {
constructor(form: IonicForm) {
constructor(config: IonicConfig, form: IonicForm, zone: NgZone) {
this.form = form;
this.zone = zone;
zone.runOutsideAngular(() => {
this.focusOutline(config.get('focusOutline'), document);
});
}
isOpen() {
@ -25,16 +31,23 @@ export class IonicKeyboard {
promise = new Promise(resolve => { callback = resolve; });
}
function checkKeyboard() {
if (!self.isOpen()) {
callback();
self.zone.runOutsideAngular(() => {
} else {
setTimeout(checkKeyboard, 500);
function checkKeyboard() {
if (!self.isOpen()) {
self.zone.run(() => {
console.debug('keyboard closed');
callback();
});
} else {
setTimeout(checkKeyboard, KEYBOARD_CLOSE_POLLING);
}
}
}
setTimeout(checkKeyboard, 100);
setTimeout(checkKeyboard, KEYBOARD_CLOSE_POLLING);
});
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
// --------------------------------------------------
// 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.
$focus-outline-border-color: #51a7e8 !default;
$focus-outline-box-shadow: 0px 0px 8px 0px $focus-outline-border-color !default;
:focus,
:active {
outline: none;
}
.key-input {
.focus-outline {
:focus {
outline-offset: -1px;
@ -46,8 +46,20 @@
button:focus,
[button]:focus {
outline-offset: -2px;
outline: 2px solid red;
border-color: $focus-outline-border-color;
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;
}
}