fix(alert): fix alert input focusing and keyboard

Related #8185
This commit is contained in:
Adam Bradley
2016-11-21 13:16:37 -06:00
parent d6f959b3e2
commit 29a56c4536
2 changed files with 43 additions and 19 deletions

View File

@ -1,11 +1,14 @@
import { Component, ElementRef, HostListener, Renderer, ViewEncapsulation } from '@angular/core'; import { Component, ElementRef, HostListener, Renderer, ViewEncapsulation } from '@angular/core';
import { Config } from '../../config/config'; 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 { isPresent, assert } from '../../util/util';
import { Key } from '../../util/key'; import { Key } from '../../util/key';
import { NavParams } from '../../navigation/nav-params'; import { NavParams } from '../../navigation/nav-params';
import { Platform } from '../../platform/platform';
import { ViewController } from '../../navigation/view-controller'; import { ViewController } from '../../navigation/view-controller';
import { GestureController, BlockerDelegate, BLOCK_ALL } from '../../gestures/gesture-controller';
/** /**
* @private * @private
@ -94,18 +97,19 @@ export class AlertCmp {
public _config: Config, public _config: Config,
gestureCtrl: GestureController, gestureCtrl: GestureController,
params: NavParams, params: NavParams,
renderer: Renderer private _renderer: Renderer,
private _platform: Platform
) { ) {
// gesture blocker is used to disable gestures dynamically // gesture blocker is used to disable gestures dynamically
this.gestureBlocker = gestureCtrl.createBlocker(BLOCK_ALL); this.gestureBlocker = gestureCtrl.createBlocker(BLOCK_ALL);
this.d = params.data; this.d = params.data;
this.mode = _config.get('mode'); 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) { if (this.d.cssClass) {
this.d.cssClass.split(' ').forEach(cssClass => { this.d.cssClass.split(' ').forEach(cssClass => {
// Make sure the class isn't whitespace, otherwise it throws exceptions // 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() { ionViewDidLoad() {
// normalize the data // normalize the data
let data = this.d; const data = this.d;
data.buttons = data.buttons.map(button => { data.buttons = data.buttons.map(button => {
if (typeof button === 'string') { if (typeof button === 'string') {
@ -149,7 +153,7 @@ export class AlertCmp {
label: input.label, label: input.label,
checked: !!input.checked, checked: !!input.checked,
disabled: !!input.disabled, disabled: !!input.disabled,
id: 'alert-input-' + this.id + '-' + index, id: `alert-input-${this.id}-${index}`,
handler: isPresent(input.handler) ? input.handler : null, handler: isPresent(input.handler) ? input.handler : null,
}; };
}); });
@ -157,7 +161,7 @@ export class AlertCmp {
// An alert can be created with several different inputs. Radios, // An alert can be created with several different inputs. Radios,
// checkboxes and inputs are all accepted, but they cannot be mixed. // checkboxes and inputs are all accepted, but they cannot be mixed.
let inputTypes: any[] = []; const inputTypes: string[] = [];
data.inputs.forEach(input => { data.inputs.forEach(input => {
if (inputTypes.indexOf(input.type) < 0) { if (inputTypes.indexOf(input.type) < 0) {
inputTypes.push(input.type); inputTypes.push(input.type);
@ -165,15 +169,24 @@ export class AlertCmp {
}); });
if (inputTypes.length > 1 && (inputTypes.indexOf('checkbox') > -1 || inputTypes.indexOf('radio') > -1)) { 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; 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) { if (checkedInput) {
this.activeId = checkedInput.id; 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() { ionViewWillEnter() {
@ -185,12 +198,14 @@ export class AlertCmp {
} }
ionViewDidEnter() { ionViewDidEnter() {
let activeElement: any = document.activeElement; // focus out of the active element
if (document.activeElement) { focusOutActiveElement();
activeElement.blur();
}
let focusableEle = this._elementRef.nativeElement.querySelector('input,button'); // 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) { if (focusableEle) {
focusableEle.focus(); focusableEle.focus();
} }
@ -206,13 +221,13 @@ export class AlertCmp {
// this can happen when the button has focus and used the enter // this can happen when the button has focus and used the enter
// key to click the button. However, both the click handler and // key to click the button. However, both the click handler and
// this keyup event will fire, so only allow one of them to go. // 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]; let button = this.d.buttons[this.d.buttons.length - 1];
this.btnClick(button); this.btnClick(button);
} }
} else if (ev.keyCode === Key.ESCAPE) { } else if (ev.keyCode === Key.ESCAPE) {
console.debug('alert, escape button'); console.debug(`alert, escape button`);
this.bdClick(); this.bdClick();
} }
} }
@ -241,6 +256,8 @@ export class AlertCmp {
setTimeout(() => { setTimeout(() => {
this.dismiss(button.role); this.dismiss(button.role);
}, dismissDelay || this._config.get('pageTransitionDelay')); }, dismissDelay || this._config.get('pageTransitionDelay'));
focusOutActiveElement();
} }
} }
@ -280,14 +297,15 @@ export class AlertCmp {
} }
dismiss(role: any): Promise<any> { dismiss(role: any): Promise<any> {
focusOutActiveElement();
return this._viewCtrl.dismiss(this.getValues(), role); return this._viewCtrl.dismiss(this.getValues(), role);
} }
getValues() { getValues(): any {
if (this.inputType === 'radio') { if (this.inputType === 'radio') {
// this is an alert with radio buttons (single value select) // this is an alert with radio buttons (single value select)
// return the one value which is checked, otherwise undefined // 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; return checkedInput ? checkedInput.value : undefined;
} }
@ -299,7 +317,7 @@ export class AlertCmp {
// this is an alert with text inputs // this is an alert with text inputs
// return an object of all the values with the input name as the key // 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 => { this.d.inputs.forEach(i => {
values[i.name] = i.value; values[i.name] = i.value;
}); });

View File

@ -24,6 +24,12 @@ ion-alert {
justify-content: center; justify-content: center;
} }
ion-alert.alert-top {
align-items: flex-start;
padding-top: 50px;
}
ion-alert input { ion-alert input {
width: 100%; width: 100%;
} }