feature(keyboard-controller): add keyboard-controller element

This commit is contained in:
Dan Bucholtz
2017-08-29 15:17:10 -05:00
parent 11385ea7f1
commit 275bf7fb33
5 changed files with 242 additions and 3 deletions

View File

@ -0,0 +1,173 @@
import { Component, Prop, Event, EventEmitter} from '@stencil/core';
import { KeyboardController } from './keyboard-interfaces';
import { Config } from '../..';
import { focusOutActiveElement, getDocument, getWindow, hasFocusedTextInput } from '../../utils/helpers';
import { KEY_TAB } from './keys';
let v2KeyboardWillShowHandler: () => void = null;
let v2KeyboardWillHideHandler: () => void = null;
let v2KeyboardDidShowHandler: () => void = null;
let v2KeyboardDidHideHandler: () => void = null;
let v1keyboardHide: () => void = null;
let v1keyboardShow: () => void = null;
let timeoutValue: number = null;
@Component({
tag: 'ion-keyboard-controller'
})
export class IonKeyboardController implements KeyboardController {
@Prop({context: 'config'}) config: Config;
@Prop({context: 'dom'}) domController: any;
@Event() keyboardWillShow: EventEmitter;
@Event() keyboardDidShow: EventEmitter;
@Event() keyboardWillHide: EventEmitter;
@Event() keyboardDidHide: EventEmitter;
componentDidLoad() {
componentDidLoadImpl(this);
}
isOpen(): boolean {
return hasFocusedTextInput();
}
onClose(callback: Function, pollingInterval: number = KEYBOARD_CLOSE_POLLING, maxPollingChecks: number = KEYBOARD_POLLING_CHECKS_MAX): Promise<any> {
return onCloseImpl(this, callback, pollingInterval, maxPollingChecks);
}
}
export function onCloseImpl(keyboardController: KeyboardController, callback: Function, pollingInterval: number, maxPollingChecks: number): Promise<any> {
let numChecks = 0;
const promise: Promise<any> = callback ? null : new Promise((resolve) => {
callback = resolve;
});
const checkKeyBoard = () => {
if (!keyboardController.isOpen() || numChecks > maxPollingChecks) {
setTimeout(() => {
callback();
}, 400);
} else {
setTimeout(checkKeyBoard, pollingInterval);
}
numChecks++;
};
setTimeout(checkKeyBoard, pollingInterval);
return promise;
}
export function componentDidLoadImpl(keyboardController: KeyboardController) {
focusOutline(getDocument(), keyboardController.config.get('focusOutline'), keyboardController);
if (keyboardController.config.getBoolean('keyboardResizes', false)) {
listenV2(getWindow(), keyboardController);
} else {
listenV1(getWindow(), keyboardController);
}
}
export function listenV2(win: Window, keyboardController: KeyboardController) {
v2KeyboardWillShowHandler = () => {
keyboardController.keyboardWillShow.emit();
};
win.addEventListener('keyboardWillShow', v2KeyboardWillShowHandler);
v2KeyboardWillHideHandler = () => {
keyboardController.keyboardWillHide.emit();
};
win.addEventListener('keyboardWillHide', v2KeyboardWillHideHandler);
v2KeyboardDidShowHandler = () => {
keyboardController.keyboardDidShow.emit();
};
win.addEventListener('keyboardDidShow', v2KeyboardDidShowHandler);
v2KeyboardDidHideHandler = () => {
keyboardController.keyboardDidHide.emit();
};
win.addEventListener('keyboardDidHide', v2KeyboardDidHideHandler);
}
export function listenV1(win: Window, keyboardController: KeyboardController) {
v1keyboardHide = () => {
blurActiveInput(true, keyboardController);
}
win.addEventListener('native.keyboardhide', v1keyboardHide);
v1keyboardShow = () => {
blurActiveInput(false, keyboardController);
}
win.addEventListener('native.keyboardshow', v1keyboardShow);
}
export function blurActiveInput(shouldBlur: boolean, keyboardController: KeyboardController) {
clearTimeout(timeoutValue);
if (shouldBlur) {
timeoutValue = setTimeout(() => {
if (keyboardController.isOpen()) {
focusOutActiveElement();
}
}, 80) as any as number;
}
}
export function focusOutline(doc: Document, value: boolean, keyboardController: KeyboardController) {
/* 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;
const cssClass = () => {
keyboardController.dom.write(() => {
doc.body.classList[isKeyInputEnabled ? 'add' : 'remove']('focus-outline');
});
};
if (value === true) {
isKeyInputEnabled = true;
return cssClass();
} else if (value === false) {
return;
}
const keyDownHandler = (event: KeyboardEvent) => {
if (!isKeyInputEnabled && event.keyCode === KEY_TAB) {
isKeyInputEnabled = true;
enableKeyInput();
}
};
const pointerDown = () => {
isKeyInputEnabled = false;
enableKeyInput();
};
const enableKeyInput = () => {
cssClass();
doc.removeEventListener('mousedown', pointerDown);
doc.removeEventListener('touchstart', pointerDown);
if (isKeyInputEnabled) {
doc.addEventListener('mousedown', pointerDown);
doc.addEventListener('touchstart', pointerDown);
}
};
doc.addEventListener('keydown', keyDownHandler);
}
const KEYBOARD_CLOSE_POLLING = 150;
const KEYBOARD_POLLING_CHECKS_MAX = 100;

View File

@ -0,0 +1,14 @@
import { EventEmitter } from '@stencil/core';
import { Config } from '../..';
export interface KeyboardController {
config?: Config;
dom?: any; // TODO, make dom controller
keyboardWillShow?: EventEmitter;
keyboardDidShow?: EventEmitter;
keyboardWillHide?: EventEmitter;
keyboardDidHide?: EventEmitter;
isOpen?(): boolean;
onClose?(callback: Function, pollingInterval: number, maxPollingchecks: number): Promise<any>;
}

View File

@ -0,0 +1,9 @@
export const KEY_LEFT = 37;
export const KEY_UP = 38;
export const KEY_RIGHT = 39;
export const KEY_DOWN = 40;
export const KEY_ENTER = 13;
export const KEY_ESCAPE = 27;
export const KEY_SPACE = 32;
export const KEY_TAB = 9;

View File

@ -30,7 +30,7 @@ import {
import { ViewControllerImpl } from './view-controller-impl';
import { assert, isDef, isNumber } from '../utils/helpers';
import { assert, focusOutActiveElement, isDef, isNumber } from '../utils/helpers';
import { buildIOSTransition } from './transitions/transition.ios';
import { buildMdTransition } from './transitions/transition.md';
@ -425,8 +425,8 @@ export function transitionFinish(nav: Nav, transition: Transition, delegate: Fra
// TODO - navChange on the deep linker used to be called here
if (opts.keyboardClose) {
// TODO - close the keyboard
if (opts.keyboardClose !== false) {
focusOutActiveElement();
}
}

View File

@ -168,7 +168,50 @@ export function isReady(element: Element): Promise<any> {
});
}
export function getOrAppendElement(tagName: string): Element {
const element = document.querySelector(tagName);
if (element) {
return element;
}
const tmp = document.createElement(tagName);
document.body.appendChild(tmp);
return tmp;
}
/** @hidden */
export function deepCopy(obj: any) {
return JSON.parse(JSON.stringify(obj));
}
export function getWindow() {
return window;
}
export function getDocument() {
return document;
}
export function getActiveElement(): HTMLElement {
return getDocument()['activeElement'] as HTMLElement;
}
export function focusOutActiveElement() {
const activeElement = getActiveElement();
activeElement && activeElement.blur && activeElement.blur();
}
export function isTextInput(ele: any) {
return !!ele &&
(ele.tagName === 'TEXTAREA'
|| ele.contentEditable === 'true'
|| (ele.tagName === 'INPUT' && !(NON_TEXT_INPUT_REGEX.test(ele.type))));
}
export const NON_TEXT_INPUT_REGEX = /^(radio|checkbox|range|file|submit|reset|color|image|button)$/i;
export function hasFocusedTextInput() {
const activeElement = getActiveElement();
if (isTextInput(activeElement)) {
return activeElement.parentElement.querySelector(':focus') === activeElement;
}
return false;
}