* @fileoverview
* 'pin-keyboard' is a keyboard that can be used to enter PINs or more generally
* numeric values.
*
* Properties:
* value: The value of the PIN keyboard. Writing to this property will adjust
* the PIN keyboard's value.
*
* Events:
* pin-change: Fired when the PIN value has changed. The PIN is available at
* event.detail.pin.
* submit: Fired when the PIN is submitted. The PIN is available at
* event.detail.pin.
*
* Example:
* <pin-keyboard on-pin-change="onPinChange" on-submit="onPinSubmit">
* </pin-keyboard>
*/
import 'chrome://resources/ash/common/cr_elements/cros_color_overrides.css.js';
import 'chrome://resources/ash/common/cr_elements/cr_button/cr_button.js';
import 'chrome://resources/ash/common/cr_elements/cr_icon_button/cr_icon_button.js';
import 'chrome://resources/ash/common/cr_elements/cr_input/cr_input.js';
import 'chrome://resources/ash/common/cr_elements/icons.html.js';
import 'chrome://resources/ash/common/cr_elements/cr_shared_vars.css.js';
import 'chrome://resources/polymer/v3_0/iron-icon/iron-icon.js';
import './pin_keyboard_icons.html.js';
import {CrButtonElement} from 'chrome://resources/ash/common/cr_elements/cr_button/cr_button.js';
import {CrInputElement} from 'chrome://resources/ash/common/cr_elements/cr_input/cr_input.js';
import {I18nMixin} from 'chrome://resources/ash/common/cr_elements/i18n_mixin.js';
import {WebUiListenerMixin} from 'chrome://resources/ash/common/cr_elements/web_ui_listener_mixin.js';
import {assert, assertInstanceof} from 'chrome://resources/js/assert.js';
import {loadTimeData} from 'chrome://resources/js/load_time_data.js';
import {PolymerElement} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';
import {getTemplate} from './pin_keyboard.html.js';
* Once auto backspace starts, the time between individual backspaces.
* @type {number}
* @const
*/
const REPEAT_BACKSPACE_DELAY_MS = 150;
* How long the backspace button must be held down before auto backspace
* starts.
* @type {number}
* @const
*/
const INITIAL_BACKSPACE_DELAY_MS = 500;
* The key codes of the keys allowed to be used on the pin input, in addition to
* number keys. We allow some editing keys. We also allow system keys, otherwise
* preventDefault() will prevent the user from changing screen brightness,
* taking screenshots, etc. https://crbug.com/1002863
* @type {!Set<number>}
* @const
*/
const PIN_INPUT_ALLOWED_NON_NUMBER_KEY_CODES = new Set([
8,
9,
27,
37,
39,
183,
182,
216,
217,
179,
173,
174,
175,
154,
]);
function receivedEventFromKeyboard(event: Event): boolean {
if (!(event instanceof CustomEvent)) {
return false;
}
if (!('sourceEvent' in event.detail)) {
return false;
}
return event.detail.sourceEvent.detail === 0;
}
const PinKeyboardElementBase = WebUiListenerMixin(I18nMixin(PolymerElement));
export interface PinKeyboardElement {
$: {
pinInput: CrInputElement,
};
}
export class PinKeyboardElement extends PinKeyboardElementBase {
static get is(): string {
return 'pin-keyboard' as const;
}
static get template(): HTMLTemplateElement {
return getTemplate();
}
static get properties(): object {
return {
* Whether or not the keyboard's input element should be numerical
* or password.
*/
enablePassword: {
type: Boolean,
value: false,
},
allowNonDigit: {
type: Boolean,
value: false,
},
hasError: {
type: Boolean,
value: false,
},
disabled: {
type: Boolean,
value: false,
},
* The password input element the pin keyboard is associated with. If this
* is not set, then a default input element is shown and used. If set,
* this must be an HTMLInputElement of a |type| to which the
* |selectionStart| and |selectionEnd| attributes apply, for example
* "password" but not "date".
*/
passwordElement: {
type: Object,
value: null,
},
* The intervalID used for the backspace button set/clear interval.
*/
repeatBackspaceIntervalId_: {
type: Number,
value: 0,
},
* The timeoutID used for the auto backspace.
*/
startAutoBackspaceId_: {
type: Number,
value: 0,
},
* The value stored in the keyboard's input element.
*/
value: {
type: String,
notify: true,
value: '',
observer: 'onPinValueChange_',
},
focused_: {
type: Boolean,
value: false,
},
* Enables pin placeholder.
*/
enablePlaceholder: {
type: Boolean,
value: false,
},
* Enables the visibility icon for showing/hiding the PIN.
*/
enableVisibilityIcon: {
type: Boolean,
value: false,
},
* Controls the visibility icon logic.
*/
isPinVisible_: {
type: Boolean,
value: false,
},
* The aria label to be used for the input element.
*/
ariaLabel: {
type: String,
},
};
}
enablePassword: boolean;
allowNonDigit: boolean;
hasError: boolean;
disabled: boolean;
passwordElement: HTMLElement|undefined;
value: string;
enablePlaceholder: boolean;
enableVisibilityIcon: boolean;
private repeatBackspaceIntervalId_: number;
private startAutoBackspaceId_: number;
private focused_: boolean;
private isPinVisible_: boolean;
override ready(): void {
super.ready();
this.addWebUiListener('blur', this.onBlur_.bind(this));
this.addWebUiListener('focus', this.onFocus_.bind(this));
}
* Gets the selection start of the input field.
*/
private get selectionStart_(): number {
const selectionStart = this.passwordElement_().selectionStart;
assert(selectionStart !== null);
return selectionStart;
}
* Gets the selection end of the input field.
*/
private get selectionEnd_(): number {
const selectionEnd = this.passwordElement_().selectionEnd;
assert(selectionEnd !== null);
return selectionEnd;
}
* Sets the selection start of the input field.
*/
private set selectionStart_(start: number) {
this.passwordElement_().selectionStart = start;
}
* Sets the selection end of the input field.
*/
private set selectionEnd_(end: number) {
this.passwordElement_().selectionEnd = end;
}
* Transfers blur to the input element.
*/
override blur(): void {
this.passwordElement_().blur();
}
* Schedules a call to focusInputSynchronously().
*/
focusInput(selectionStart?: number, selectionEnd?: number): void {
setTimeout(
() => this.focusInputSynchronously(selectionStart, selectionEnd), 0);
}
* Transfers focus to the input element. This should not bring up the virtual
* keyboard, if it is enabled. After focus, moves the caret to the correct
* location if specified.
*/
focusInputSynchronously(selectionStart?: number, selectionEnd?: number):
void {
this.passwordElement_().focus();
if (selectionStart !== undefined) {
this.selectionStart_ = selectionStart;
}
if (selectionEnd !== undefined) {
this.selectionEnd_ = selectionEnd;
}
}
resetPinVisibility(): void {
this.isPinVisible_ = false;
}
* Transfers focus to the input. Called when a non button element on the
* PIN button area is clicked to prevent focus from leaving the input.
*/
private onRootClick_(): void {
this.focusInput(this.selectionStart_, this.selectionEnd_);
}
private onFocus_(): void {
this.focused_ = true;
}
private onBlur_(): void {
this.focused_ = false;
}
* Called when a keypad number has been clicked.
*/
private onNumberClick_(event: Event): void {
const button = event.target;
assertInstanceof(button, CrButtonElement);
const numberValue = button.getAttribute('value');
assert(numberValue !== null);
const selectionStart = this.selectionStart_;
const selectionEnd = this.selectionEnd_;
const beforeStart = this.value.substring(0, selectionStart);
const afterEnd = this.value.substring(selectionEnd);
this.value = beforeStart + numberValue + afterEnd;
if (!receivedEventFromKeyboard(event) && selectionStart !== null) {
this.focusInputSynchronously(selectionStart + 1, selectionStart + 1);
}
event.stopImmediatePropagation();
}
private firePinSubmitEvent_(): void {
this.dispatchEvent(new CustomEvent('submit', {detail: {pin: this.value}}));
}
* Fires an update event with the current PIN value. The event will only be
* fired if the PIN value has actually changed.
*/
private onPinValueChange_(value: string): void {
if (this.passwordElement) {
assertInstanceof(this.passwordElement, HTMLInputElement);
this.passwordElement.value = value;
}
this.dispatchEvent(new CustomEvent('pin-change', {detail: {pin: value}}));
}
* Called when the user wants to erase the last character of the entered
* PIN value.
*/
private onPinClear_(): void {
let selectionStart = this.selectionStart_;
const selectionEnd = this.selectionEnd_;
if (selectionStart === selectionEnd && selectionStart) {
selectionStart--;
}
this.value = this.value.substring(0, selectionStart) +
this.value.substring(selectionEnd);
this.selectionStart_ = selectionStart;
this.selectionEnd_ = selectionStart;
}
* Called when user taps the backspace the button. Only does something when
* the tap comes from the keyboard. onBackspacePointerDown_ and
* onBackspacePointerUp_ will handle the events if they come from mouse or
* touch. Note: This does not support repeatedly backspacing by holding down
* the space or enter key like touch or mouse does.
*/
private onBackspaceClick_(event: Event): void {
if (!receivedEventFromKeyboard(event)) {
return;
}
this.onPinClear_();
this.clearAndReset_();
event.stopImmediatePropagation();
}
* Called when the user presses or touches the backspace button. Starts a
* timer which starts an interval to repeatedly backspace the pin value until
* the interval is cleared.
*/
private onBackspacePointerDown_(event: Event): void {
this.startAutoBackspaceId_ = setTimeout(() => {
this.repeatBackspaceIntervalId_ =
setInterval(this.onPinClear_.bind(this), REPEAT_BACKSPACE_DELAY_MS);
}, INITIAL_BACKSPACE_DELAY_MS);
if (!receivedEventFromKeyboard(event)) {
this.focusInput(this.selectionStart_, this.selectionEnd_);
}
event.stopImmediatePropagation();
}
* Helper function which clears the timer / interval ids and resets them.
* @private
*/
private clearAndReset_(): void {
clearInterval(this.repeatBackspaceIntervalId_);
this.repeatBackspaceIntervalId_ = 0;
clearTimeout(this.startAutoBackspaceId_);
this.startAutoBackspaceId_ = 0;
}
* Called when the user unpresses or untouches the backspace button. Stops the
* interval callback and fires a backspace event if there is no interval
* running.
*/
private onBackspacePointerUp_(event: Event): void {
if (!this.repeatBackspaceIntervalId_) {
this.onPinClear_();
}
this.clearAndReset_();
this.blur();
if (!receivedEventFromKeyboard(event)) {
this.focusInput(this.selectionStart_, this.selectionEnd_);
}
event.stopImmediatePropagation();
}
* Helper function to check whether a given |event| should be processed by
* the input.
*/
private isValidEventForInput_(event: KeyboardEvent): boolean {
if (this.allowNonDigit) {
return true;
}
if ((event.keyCode >= 48 && event.keyCode <= 57) && !event.shiftKey) {
return true;
}
if ((event.keyCode >= 96 && event.keyCode <= 105) && !event.shiftKey) {
return true;
}
if (PIN_INPUT_ALLOWED_NON_NUMBER_KEY_CODES.has(event.keyCode)) {
return true;
}
if (event.keyCode === 65 && event.ctrlKey) {
return true;
}
if (event.ctrlKey && [48, 187, 189].includes(event.keyCode)) {
return true;
}
if (event.keyCode === 168 && event.ctrlKey && event.shiftKey) {
return true;
}
if (event.ctrlKey && event.altKey && event.key === 'z') {
return true;
}
return false;
}
* Called when a key event is pressed while the input element has focus.
*/
private onInputKeyDown_(event: KeyboardEvent): void {
assertInstanceof(event, KeyboardEvent);
if (event.keyCode === 38 || event.keyCode === 40 ||
event.code === 'ArrowUp' || event.code === 'ArrowDown') {
event.preventDefault();
return;
}
if (event.keyCode === 13 || event.code === 'Enter') {
this.firePinSubmitEvent_();
event.preventDefault();
return;
}
if (!this.isValidEventForInput_(event)) {
event.preventDefault();
return;
}
}
* Indicates if something is entered.
*/
private hasInput_(value: string): boolean {
return value.length > 0;
}
* Determines if the pin input should be contrasted.
*/
private hasInputOrFocus_(value: string, focused: boolean): boolean {
return this.hasInput_(value) || focused;
}
* Computes the value of the pin input placeholder.
*/
private getInputPlaceholder_(
enablePassword: boolean, enablePlaceholder: boolean): string {
if (!enablePlaceholder) {
return '';
}
return enablePassword ? this.i18n('pinKeyboardPlaceholderPinPassword') :
this.i18n('pinKeyboardPlaceholderPin');
}
private getShowHideButtonLabel(isVisible: boolean): string {
return isVisible ? loadTimeData.getString('hidePin') :
loadTimeData.getString('showPin');
}
private getShowHideButtonIcon(isVisible: boolean): string {
return isVisible ? 'pin-keyboard:visibility-off' :
'pin-keyboard:visibility';
}
private onPinShowHideButtonClick() {
this.isPinVisible_ = !this.isPinVisible_;
}
private getPinInputType(isVisible: boolean): string {
return isVisible ? 'text' : 'password';
}
* Computes the direction of the pin input.
*/
private isInputRtl_(password: string): boolean {
return (document.dir === 'rtl') && !Number.isInteger(+password);
}
private onBackspaceContextMenu_(e: Event): void {
assertInstanceof(e, MouseEvent);
if (e.which) {
return;
}
e.preventDefault();
e.stopPropagation();
}
* Returns the native input element of |pinInput|.
*/
private passwordElement_(): HTMLInputElement {
if (this.passwordElement) {
assertInstanceof(this.passwordElement, HTMLInputElement);
return this.passwordElement;
} else {
assertInstanceof(this.$.pinInput, CrInputElement);
return this.$.pinInput.inputElement;
}
}
}
customElements.define(PinKeyboardElement.is, PinKeyboardElement);