From 275bf7fb33a6cb6e6040f5fad4626da303e7727b Mon Sep 17 00:00:00 2001 From: Dan Bucholtz Date: Tue, 29 Aug 2017 15:17:10 -0500 Subject: [PATCH] feature(keyboard-controller): add keyboard-controller element --- .../keyboard-controller.tsx | 173 ++++++++++++++++++ .../keyboard-interfaces.d.ts | 14 ++ .../components/keyboard-controller/keys.ts | 9 + .../navigation/nav-controller-functions.ts | 6 +- packages/core/src/utils/helpers.ts | 43 +++++ 5 files changed, 242 insertions(+), 3 deletions(-) create mode 100644 packages/core/src/components/keyboard-controller/keyboard-controller.tsx create mode 100644 packages/core/src/components/keyboard-controller/keyboard-interfaces.d.ts create mode 100644 packages/core/src/components/keyboard-controller/keys.ts diff --git a/packages/core/src/components/keyboard-controller/keyboard-controller.tsx b/packages/core/src/components/keyboard-controller/keyboard-controller.tsx new file mode 100644 index 0000000000..a88ec15ade --- /dev/null +++ b/packages/core/src/components/keyboard-controller/keyboard-controller.tsx @@ -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 { + + return onCloseImpl(this, callback, pollingInterval, maxPollingChecks); + } +} + +export function onCloseImpl(keyboardController: KeyboardController, callback: Function, pollingInterval: number, maxPollingChecks: number): Promise { + let numChecks = 0; + + const promise: Promise = 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; diff --git a/packages/core/src/components/keyboard-controller/keyboard-interfaces.d.ts b/packages/core/src/components/keyboard-controller/keyboard-interfaces.d.ts new file mode 100644 index 0000000000..a30d798c86 --- /dev/null +++ b/packages/core/src/components/keyboard-controller/keyboard-interfaces.d.ts @@ -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; +} \ No newline at end of file diff --git a/packages/core/src/components/keyboard-controller/keys.ts b/packages/core/src/components/keyboard-controller/keys.ts new file mode 100644 index 0000000000..eb797bac0e --- /dev/null +++ b/packages/core/src/components/keyboard-controller/keys.ts @@ -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; diff --git a/packages/core/src/navigation/nav-controller-functions.ts b/packages/core/src/navigation/nav-controller-functions.ts index 1f75757e79..f44f501208 100644 --- a/packages/core/src/navigation/nav-controller-functions.ts +++ b/packages/core/src/navigation/nav-controller-functions.ts @@ -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(); } } diff --git a/packages/core/src/utils/helpers.ts b/packages/core/src/utils/helpers.ts index ef3fce1add..24f0c1b326 100644 --- a/packages/core/src/utils/helpers.ts +++ b/packages/core/src/utils/helpers.ts @@ -168,7 +168,50 @@ export function isReady(element: Element): Promise { }); } +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; } \ No newline at end of file