From 29a56c45364cdd277abbbf685ed25371396a5164 Mon Sep 17 00:00:00 2001 From: Adam Bradley Date: Mon, 21 Nov 2016 13:16:37 -0600 Subject: [PATCH] fix(alert): fix alert input focusing and keyboard Related #8185 --- src/components/alert/alert-component.ts | 58 ++++++++++++++++--------- src/components/alert/alert.scss | 6 +++ 2 files changed, 44 insertions(+), 20 deletions(-) diff --git a/src/components/alert/alert-component.ts b/src/components/alert/alert-component.ts index a10ae732f7f..48713baffb6 100644 --- a/src/components/alert/alert-component.ts +++ b/src/components/alert/alert-component.ts @@ -1,11 +1,14 @@ import { Component, ElementRef, HostListener, Renderer, ViewEncapsulation } from '@angular/core'; import { Config } from '../../config/config'; +import { focusOutActiveElement, NON_TEXT_INPUT_REGEX } from '../../util/dom'; +import { GestureController, BlockerDelegate, BLOCK_ALL } from '../../gestures/gesture-controller'; import { isPresent, assert } from '../../util/util'; import { Key } from '../../util/key'; import { NavParams } from '../../navigation/nav-params'; +import { Platform } from '../../platform/platform'; import { ViewController } from '../../navigation/view-controller'; -import { GestureController, BlockerDelegate, BLOCK_ALL } from '../../gestures/gesture-controller'; + /** * @private @@ -94,18 +97,19 @@ export class AlertCmp { public _config: Config, gestureCtrl: GestureController, params: NavParams, - renderer: Renderer + private _renderer: Renderer, + private _platform: Platform ) { // gesture blocker is used to disable gestures dynamically this.gestureBlocker = gestureCtrl.createBlocker(BLOCK_ALL); this.d = params.data; this.mode = _config.get('mode'); - renderer.setElementClass(_elementRef.nativeElement, `alert-${this.mode}`, true); + _renderer.setElementClass(_elementRef.nativeElement, `alert-${this.mode}`, true); if (this.d.cssClass) { this.d.cssClass.split(' ').forEach(cssClass => { // Make sure the class isn't whitespace, otherwise it throws exceptions - if (cssClass.trim() !== '') renderer.setElementClass(_elementRef.nativeElement, cssClass, true); + if (cssClass.trim() !== '') _renderer.setElementClass(_elementRef.nativeElement, cssClass, true); }); } @@ -131,7 +135,7 @@ export class AlertCmp { ionViewDidLoad() { // normalize the data - let data = this.d; + const data = this.d; data.buttons = data.buttons.map(button => { if (typeof button === 'string') { @@ -149,7 +153,7 @@ export class AlertCmp { label: input.label, checked: !!input.checked, disabled: !!input.disabled, - id: 'alert-input-' + this.id + '-' + index, + id: `alert-input-${this.id}-${index}`, handler: isPresent(input.handler) ? input.handler : null, }; }); @@ -157,7 +161,7 @@ export class AlertCmp { // An alert can be created with several different inputs. Radios, // checkboxes and inputs are all accepted, but they cannot be mixed. - let inputTypes: any[] = []; + const inputTypes: string[] = []; data.inputs.forEach(input => { if (inputTypes.indexOf(input.type) < 0) { inputTypes.push(input.type); @@ -165,15 +169,24 @@ export class AlertCmp { }); if (inputTypes.length > 1 && (inputTypes.indexOf('checkbox') > -1 || inputTypes.indexOf('radio') > -1)) { - console.warn('Alert cannot mix input types: ' + (inputTypes.join('/')) + '. Please see alert docs for more info.'); + console.warn(`Alert cannot mix input types: ${(inputTypes.join('/'))}. Please see alert docs for more info.`); } this.inputType = inputTypes.length ? inputTypes[0] : null; - let checkedInput = this.d.inputs.find(input => input.checked); + const checkedInput = this.d.inputs.find(input => input.checked); if (checkedInput) { this.activeId = checkedInput.id; } + + const hasTextInput = (this.d.inputs.length && this.d.inputs.some(i => !(NON_TEXT_INPUT_REGEX.test(i.type)))); + if (hasTextInput && this._platform.is('mobile')) { + // this alert has a text input and it's on a mobile device so we should align + // the alert up high because we need to leave space for the virtual keboard + // this also helps prevent the layout getting all messed up from + // the browser trying to scroll the input into a safe area + this._renderer.setElementClass(this._elementRef.nativeElement, 'alert-top', true); + } } ionViewWillEnter() { @@ -185,12 +198,14 @@ export class AlertCmp { } ionViewDidEnter() { - let activeElement: any = document.activeElement; - if (document.activeElement) { - activeElement.blur(); - } - - let focusableEle = this._elementRef.nativeElement.querySelector('input,button'); + // focus out of the active element + focusOutActiveElement(); + + // set focus on the first input or button in the alert + // note that this does not always work and bring up the keyboard on + // devices since the focus command must come from the user's touch event + // and ionViewDidEnter is not in the same callstack as the touch event :( + const focusableEle = this._elementRef.nativeElement.querySelector('input,button'); if (focusableEle) { focusableEle.focus(); } @@ -206,13 +221,13 @@ export class AlertCmp { // this can happen when the button has focus and used the enter // key to click the button. However, both the click handler and // this keyup event will fire, so only allow one of them to go. - console.debug('alert, enter button'); + console.debug(`alert, enter button`); let button = this.d.buttons[this.d.buttons.length - 1]; this.btnClick(button); } } else if (ev.keyCode === Key.ESCAPE) { - console.debug('alert, escape button'); + console.debug(`alert, escape button`); this.bdClick(); } } @@ -241,6 +256,8 @@ export class AlertCmp { setTimeout(() => { this.dismiss(button.role); }, dismissDelay || this._config.get('pageTransitionDelay')); + + focusOutActiveElement(); } } @@ -280,14 +297,15 @@ export class AlertCmp { } dismiss(role: any): Promise { + focusOutActiveElement(); return this._viewCtrl.dismiss(this.getValues(), role); } - getValues() { + getValues(): any { if (this.inputType === 'radio') { // this is an alert with radio buttons (single value select) // return the one value which is checked, otherwise undefined - let checkedInput = this.d.inputs.find(i => i.checked); + const checkedInput = this.d.inputs.find(i => i.checked); return checkedInput ? checkedInput.value : undefined; } @@ -299,7 +317,7 @@ export class AlertCmp { // this is an alert with text inputs // return an object of all the values with the input name as the key - let values: {[k: string]: string} = {}; + const values: {[k: string]: string} = {}; this.d.inputs.forEach(i => { values[i.name] = i.value; }); diff --git a/src/components/alert/alert.scss b/src/components/alert/alert.scss index b77584fc713..146668c2873 100644 --- a/src/components/alert/alert.scss +++ b/src/components/alert/alert.scss @@ -24,6 +24,12 @@ ion-alert { justify-content: center; } +ion-alert.alert-top { + align-items: flex-start; + + padding-top: 50px; +} + ion-alert input { width: 100%; }