Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(97)

Side by Side Diff: chrome/browser/resources/chromeos/quick_unlock/pin_keyboard.js

Issue 2444443002: cros: Allow pin keyboard to keep focus without popping up the pin keyboard. (Closed)
Patch Set: Rebased. Created 4 years, 1 month ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
OLDNEW
1 // Copyright 2016 The Chromium Authors. All rights reserved. 1 // Copyright 2016 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be 2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file. 3 // found in the LICENSE file.
4 4
5 /** 5 /**
6 * @fileoverview 6 * @fileoverview
7 * 'pin-keyboard' is a keyboard that can be used to enter PINs or more generally 7 * 'pin-keyboard' is a keyboard that can be used to enter PINs or more generally
8 * numeric values. 8 * numeric values.
9 * 9 *
10 * Properties: 10 * Properties:
11 * value: The value of the PIN keyboard. Writing to this property will adjust 11 * value: The value of the PIN keyboard. Writing to this property will adjust
12 * the PIN keyboard's value. 12 * the PIN keyboard's value.
13 * 13 *
14 * Events: 14 * Events:
15 * pin-change: Fired when the PIN value has changed. The pin is available at 15 * pin-change: Fired when the PIN value has changed. The PIN is available at
16 * event.detail.pin. 16 * event.detail.pin.
17 * submit: Fired when the PIN is submitted. The pin is available at 17 * submit: Fired when the PIN is submitted. The PIN is available at
18 * event.detail.pin. 18 * event.detail.pin.
19 * 19 *
20 * Example: 20 * Example:
21 * <pin-keyboard on-pin-change="onPinChange" on-submit="onPinSubmit" 21 * <pin-keyboard on-pin-change="onPinChange" on-submit="onPinSubmit">
22 * value="{{pinValue}}">
23 * </pin-keyboard> 22 * </pin-keyboard>
24 */ 23 */
25 24
26 (function() { 25 (function() {
27 26
28 /** 27 /**
29 * Once auto backspace starts, the time between individual backspaces. 28 * Once auto backspace starts, the time between individual backspaces.
30 * @type {number} 29 * @type {number}
31 * @const 30 * @const
32 */ 31 */
33 var REPEAT_BACKSPACE_DELAY_MS = 150; 32 var REPEAT_BACKSPACE_DELAY_MS = 150;
34 33
35 /** 34 /**
36 * How long the backspace button must be held down before auto backspace 35 * How long the backspace button must be held down before auto backspace
37 * starts. 36 * starts.
38 * @type {number} 37 * @type {number}
39 * @const 38 * @const
40 */ 39 */
41 var INITIAL_BACKSPACE_DELAY_MS = 500; 40 var INITIAL_BACKSPACE_DELAY_MS = 500;
42 41
42 /**
43 * The key codes of the keys allowed to be used on the pin input, in addition to
44 * number keys. Currently we allow backspace(8), tab(9), left(37) and right(39).
45 * @type {Array<number>}
46 * @const
47 */
48 var PIN_INPUT_ALLOWED_NON_NUMBER_KEY_CODES = [8, 9, 37, 39];
49
43 Polymer({ 50 Polymer({
44 is: 'pin-keyboard', 51 is: 'pin-keyboard',
45 52
46 behaviors: [ 53 behaviors: [
47 I18nBehavior, 54 I18nBehavior,
48 ], 55 ],
49 56
50 properties: { 57 properties: {
51 /** 58 /**
52 * Whether or not the keyboard's input element should be numerical 59 * Whether or not the keyboard's input element should be numerical
53 * or password. 60 * or password.
61 * @private
54 */ 62 */
55 enablePassword: { 63 enablePassword: {
56 type: Boolean, 64 type: Boolean,
57 value: false 65 value: false
58 }, 66 },
59 67
60 /** 68 /**
61 * Whether or not the keyboard's input should be shown. 69 * The password element the pin keyboard is associated with. If this is not
70 * set, then a default input element is shown and used.
71 * @type {HTMLInputElement}
72 * @private
62 */ 73 */
63 hideInput: { 74 passwordElement: {
64 type: Boolean, 75 type: Object,
65 value: false 76 value: function() { return this.$$('#pin-input'); },
77 observer: 'onPasswordElementAttached_'
66 }, 78 },
67 79
68 /** The value stored in the keyboard's input element. */ 80 /**
81 * The value stored in the keyboard's input element.
82 * @private
83 */
69 value: { 84 value: {
70 type: String, 85 type: String,
71 notify: true, 86 notify: true,
72 value: '', 87 value: '',
73 observer: 'onPinValueChange_' 88 observer: 'onPinValueChange_'
74 }, 89 },
75 90
76 /** 91 /**
77 * The intervalID used for the backspace button set/clear interval. 92 * The intervalID used for the backspace button set/clear interval.
78 * @private 93 * @private
79 */ 94 */
80 repeatBackspaceIntervalId_: { 95 repeatBackspaceIntervalId_: {
81 type: Number, 96 type: Number,
82 value: 0 97 value: 0
83 }, 98 },
84 99
85 /** 100 /**
86 * The timeoutID used for the auto backspace. 101 * The timeoutID used for the auto backspace.
87 * @private 102 * @private
88 */ 103 */
89 startAutoBackspaceId_: { 104 startAutoBackspaceId_: {
90 type: Number, 105 type: Number,
91 value: 0 106 value: 0
92 } 107 }
93 }, 108 },
94 109
95 /** 110 /**
96 * @override 111 * Called when a password element is attached to the pin keyboard.
112 * @param {HTMLInputElement} inputElement The PIN keyboard's input element.
113 * @private
97 */ 114 */
98 attached: function() { 115 onPasswordElementAttached_: function(inputElement) {
99 // Remove the space/enter key binds from the polymer 116 if (inputElement != this.$$('#pin-input'))
100 // iron-a11y-keys-behavior. 117 this.$$('#pin-input').hidden = true;
101 var digitButtons = Polymer.dom(this.root).querySelectorAll('.digit-button'); 118 inputElement.addEventListener('input',
102 for (var i = 0; i < digitButtons.length; ++i) 119 this.handleInputChanged_.bind(this));
103 digitButtons[i].keyEventTarget = null;
104 }, 120 },
105 121
106 /** 122 /**
107 * Gets the container holding the password field. 123 * Called when the user uses the keyboard to enter a value into the input
108 * @type {!HTMLInputElement} 124 * element.
125 * @param {Event} event The event object.
126 * @private
109 */ 127 */
110 get inputElement() { 128 handleInputChanged_: function(event) {
111 return this.$$('#pin-input'); 129 this.value = event.target.value;
130 },
131
132 /**
133 * Gets the selection start of the input field.
134 * @type {number}
135 * @private
136 */
137 get selectionStart_() {
138 return this.passwordElement.selectionStart;
139 },
140
141 /**
142 * Gets the selection end of the input field.
143 * @type {number}
144 * @private
145 */
146 get selectionEnd_() {
147 return this.passwordElement.selectionEnd;
148 },
149
150 /**
151 * Sets the selection start of the input field.
152 * @param {number} start The new selection start of the input element.
153 * @private
154 */
155 set selectionStart_(start) {
156 this.passwordElement.selectionStart = start;
157 },
158
159 /**
160 * Sets the selection end of the input field.
161 * @param {number} end The new selection end of the input element.
162 * @private
163 */
164 set selectionEnd_(end) {
165 this.passwordElement.selectionEnd = end;
112 }, 166 },
113 167
114 /** Transfers focus to the input element. */ 168 /** Transfers focus to the input element. */
115 focus: function() { 169 focus: function() {
116 this.$$('#pin-input').focus(); 170 this.passwordElement.focus();
117 }, 171 },
118 172
119 /** 173 /**
120 * Called when a keypad number has been tapped. 174 * Called when a keypad number has been tapped.
121 * @param {!{target: !PaperButtonElement}} event 175 * @param {Event} event The event object.
122 * @private 176 * @private
123 */ 177 */
124 onNumberTap_: function(event, detail) { 178 onNumberTap_: function(event) {
125 var numberValue = event.target.getAttribute('value'); 179 var numberValue = event.target.getAttribute('value');
126 this.value += numberValue; 180
181 // Add the number where the caret is, then update the selection range of the
182 // input element.
183 this.value = this.value.substring(0, this.selectionStart_) + numberValue +
184 this.value.substring(this.selectionEnd_);
185 this.selectionStart_++;
186 this.selectionEnd_ = this.selectionStart_;
187
188 // If a number button is clicked, we do not want to switch focus to the
189 // button, therefore we transfer focus back to the input, but if a number
190 // button is tabbed into, it should keep focus, so users can use tab and
191 // spacebar/return to enter their PIN.
192 if (!event.target.receivedFocusFromKeyboard)
193 this.focus();
194 event.preventDefault();
127 }, 195 },
128 196
129 /** Fires a submit event with the current PIN value. */ 197 /** Fires a submit event with the current PIN value. */
130 firePinSubmitEvent_: function() { 198 firePinSubmitEvent_: function() {
131 this.fire('submit', { pin: this.value }); 199 this.fire('submit', { pin: this.value });
132 }, 200 },
133 201
134 /** 202 /**
135 * Fires an update event with the current PIN value. The event will only be 203 * Fires an update event with the current PIN value. The event will only be
136 * fired if the PIN value has actually changed. 204 * fired if the PIN value has actually changed.
137 * @param {string} value 205 * @param {string} value
138 * @param {string} previous 206 * @param {string} previous
139 */ 207 */
140 onPinValueChange_: function(value, previous) { 208 onPinValueChange_: function(value, previous) {
141 if (value != previous) 209 if (value != previous) {
210 // The selection caret gets placed at the end after altering the
211 // password element, so we store the previous location(s) and reapply
212 // them after the the new value is set.
xiyuan 2016/11/09 18:06:32 Why do we need to reapply the selection range? Wha
sammiequon 2016/11/10 21:06:01 The default behavior works fine if we are just wor
xiyuan 2016/11/10 21:48:35 Thanks for the clarification. I am still not convi
sammiequon 2016/11/10 22:36:56 I didn't test setting the value(forgot about that
213 var selectionStart = this.selectionStart_;
214 var selectionEnd = this.selectionEnd_;
215 this.passwordElement.value = this.value;
216 this.selectionStart_ = selectionStart;
217 this.selectionEnd_ = selectionEnd;
142 this.fire('pin-change', { pin: value }); 218 this.fire('pin-change', { pin: value });
219 }
143 }, 220 },
144 221
145 /** 222 /**
146 * Called when the user wants to erase the last character of the entered 223 * Called when the user wants to erase the last character of the entered
147 * PIN value. 224 * PIN value.
148 * @private 225 * @private
149 */ 226 */
150 onPinClear_: function() { 227 onPinClear_: function() {
151 this.value = this.value.substring(0, this.value.length - 1); 228 // If the input is shown, clear the text based on the caret location or
229 // selected region of the input element. If it is just a caret, remove the
230 // character in front of the caret.
231 if (this.selectionStart_ == this.selectionEnd_)
232 this.selectionStart_--;
233
234 this.value = this.value.substring(0, this.selectionStart_) +
235 this.value.substring(this.selectionEnd_);
236
237 // Move the caret or selected region to the correct new place.
238 this.selectionEnd_ = this.selectionStart_;
152 }, 239 },
153 240
154 /** 241 /**
155 * Called when the user presses or touches the backspace button. Starts a 242 * Called when the user presses or touches the backspace button. Starts a
156 * timer which starts an interval to repeatedly backspace the pin value until 243 * timer which starts an interval to repeatedly backspace the pin value until
157 * the interval is cleared. 244 * the interval is cleared.
158 * @param {Event} event The event object. 245 * @param {Event} event The event object.
159 * @private 246 * @private
160 */ 247 */
161 onBackspacePointerDown_: function(event) { 248 onBackspacePointerDown_: function(event) {
162 this.startAutoBackspaceId_ = setTimeout(function() { 249 this.startAutoBackspaceId_ = setTimeout(function() {
163 this.repeatBackspaceIntervalId_ = setInterval( 250 this.repeatBackspaceIntervalId_ = setInterval(
164 this.onPinClear_.bind(this), REPEAT_BACKSPACE_DELAY_MS); 251 this.onPinClear_.bind(this), REPEAT_BACKSPACE_DELAY_MS);
165 }.bind(this), INITIAL_BACKSPACE_DELAY_MS); 252 }.bind(this), INITIAL_BACKSPACE_DELAY_MS);
253
254 if (!event.target.receivedFocusFromKeyboard)
255 this.focus();
256 event.preventDefault();
166 }, 257 },
167 258
168 /** 259 /**
169 * Helper function which clears the timer / interval ids and resets them. 260 * Helper function which clears the timer / interval ids and resets them.
170 * @private 261 * @private
171 */ 262 */
172 clearAndReset_: function() { 263 clearAndReset_: function() {
173 clearInterval(this.repeatBackspaceIntervalId_); 264 clearInterval(this.repeatBackspaceIntervalId_);
174 this.repeatBackspaceIntervalId_ = 0; 265 this.repeatBackspaceIntervalId_ = 0;
175 clearTimeout(this.startAutoBackspaceId_); 266 clearTimeout(this.startAutoBackspaceId_);
176 this.startAutoBackspaceId_ = 0; 267 this.startAutoBackspaceId_ = 0;
177 }, 268 },
178 269
179 /** 270 /**
180 * Called when the user exits the backspace button. Stops the interval 271 * Called when the user exits the backspace button. Stops the interval
181 * callback. 272 * callback.
182 * @param {Event} event The event object. 273 * @param {Event} event The event object.
183 * @private 274 * @private
184 */ 275 */
185 onBackspacePointerOut_: function(event) { 276 onBackspacePointerOut_: function(event) {
186 this.clearAndReset_(); 277 this.clearAndReset_();
278
279 if (!event.target.receivedFocusFromKeyboard)
280 this.focus();
281 event.preventDefault();
187 }, 282 },
188 283
189 /** 284 /**
190 * Called when the user unpresses or untouches the backspace button. Stops the 285 * Called when the user unpresses or untouches the backspace button. Stops the
191 * interval callback and fires a backspace event if there is no interval 286 * interval callback and fires a backspace event if there is no interval
192 * running. 287 * running.
193 * @param {Event} event The event object. 288 * @param {Event} event The event object.
194 * @private 289 * @private
195 */ 290 */
196 onBackspacePointerUp_: function(event) { 291 onBackspacePointerUp_: function(event) {
197 // If an interval has started, do not fire event on pointer up. 292 // If an interval has started, do not fire event on pointer up.
198 if (!this.repeatBackspaceIntervalId_) 293 if (!this.repeatBackspaceIntervalId_)
199 this.onPinClear_(); 294 this.onPinClear_();
200 this.clearAndReset_(); 295 this.clearAndReset_();
296
297 if (!event.target.receivedFocusFromKeyboard)
298 this.focus();
299 event.preventDefault();
201 }, 300 },
202 301
203 /** Called when a key event is pressed while the input element has focus. */ 302 /**
303 * Helper function to check whether a given |event| should be processed by
304 * the numeric only input.
305 * @param {Event} event The event object.
306 * @private
307 */
308 isValidEventForInput_: function(event) {
309 // Valid if the key is a number, and shift is not pressed.
310 if ((event.keyCode >= 48 && event.keyCode <= 57) && !event.shiftKey)
311 return true;
312
313 // Valid if the key is one of the selected special keys defined in
314 // |PIN_INPUT_ALLOWED_NON_NUMBER_KEY_CODES|.
315 if (PIN_INPUT_ALLOWED_NON_NUMBER_KEY_CODES.indexOf(event.keyCode) > -1)
316 return true;
317
318 // Valid if a is pressed while the control key is pressed to allow users to
xiyuan 2016/11/09 18:06:32 nit: "a is pressed while the control key is presse
sammiequon 2016/11/10 21:06:01 Done.
319 // quickly select the entire PIN.
320 if (event.keyCode == 65 && event.ctrlKey)
321 return true;
322
323 // The rest of the keys are invalid.
324 return false;
325 },
326
327 /**
328 * Called when a key event is pressed while the input element has focus.
329 * @param {Event} event The event object.
330 * @private
331 */
204 onInputKeyDown_: function(event) { 332 onInputKeyDown_: function(event) {
205 // Up/down pressed, swallow the event to prevent the input value from 333 // Up/down pressed, swallow the event to prevent the input value from
206 // being incremented or decremented. 334 // being incremented or decremented.
207 if (event.keyCode == 38 || event.keyCode == 40) { 335 if (event.keyCode == 38 || event.keyCode == 40) {
208 event.preventDefault(); 336 event.preventDefault();
209 return; 337 return;
210 } 338 }
211 339
212 // Enter pressed. 340 // Enter pressed.
213 if (event.keyCode == 13) { 341 if (event.keyCode == 13) {
214 this.firePinSubmitEvent_(); 342 this.firePinSubmitEvent_();
215 event.preventDefault(); 343 event.preventDefault();
216 return; 344 return;
217 } 345 }
218 },
219 346
220 /** 347 // Do not pass events that are not numbers or special keys we care about. We
221 * Keypress does not handle backspace but handles the char codes nicely, so we 348 // use this instead of input type number because there are several issues
222 * have a seperate event to process the backspaces. 349 // with input type number, such as no selectionStart/selectionEnd and
223 * @param {Event} event Keydown Event object. 350 // entered non numbers causes the caret to jump to the left.
224 * @private 351 if (!this.isValidEventForInput_(event)) {
225 */
226 onKeyDown_: function(event) {
227 // Backspace pressed.
228 if (event.keyCode == 8) {
229 this.onPinClear_();
230 event.preventDefault(); 352 event.preventDefault();
231 return; 353 return;
232 } 354 }
233 }, 355 },
234 356
235 /** 357 /**
236 * Called when a key press event is fired while the number button is focused. 358 * Disables the backspace button if nothing is entered.
237 * Ideally we would want to pass focus back to the input element, but the we
238 * cannot or the virtual keyboard will keep poping up.
239 * @param {Event} event Keypress Event object.
240 * @private
241 */
242 onKeyPress_: function(event) {
243 // If the active element is the input element, the input element itself will
244 // handle the keypresses, so we do not handle them here.
245 if (this.shadowRoot.activeElement == this.inputElement)
246 return;
247
248 var code = event.keyCode;
249
250 // Enter pressed.
251 if (code == 13) {
252 this.firePinSubmitEvent_();
253 event.preventDefault();
254 return;
255 }
256
257 // Space pressed. We want the old polymer function of space activating the
258 // button with focus.
259 if (code == 32) {
260 // Check if target was a number button.
261 if (event.target.hasAttribute('value')) {
262 this.value += event.target.getAttribute('value');
263 return;
264 }
265 // Check if target was backspace button.
266 if (event.target.classList.contains('backspace-button')) {
267 this.onPinClear_();
268 return;
269 }
270 }
271
272 this.value += String.fromCharCode(code);
273 },
274
275 /**
276 * Disables the submit and backspace button if nothing is entered.
277 * @param {string} value 359 * @param {string} value
278 * @private 360 * @private
279 */ 361 */
280 hasInput_: function(value) { 362 hasInput_: function(value) {
281 return value.length > 0; 363 return value.length > 0;
282 }, 364 },
283 365
284 /** 366 /**
285 * Computes whether the input type for the pin input should be password or
286 * numerical.
287 * @param {boolean} enablePassword
288 * @private
289 */
290 getInputType_: function(enablePassword) {
291 return enablePassword ? 'password' : 'number';
292 },
293
294 /**
295 * Computes the value of the pin input placeholder. 367 * Computes the value of the pin input placeholder.
296 * @param {boolean} enablePassword 368 * @param {boolean} enablePassword
297 * @private 369 * @private
298 */ 370 */
299 getInputPlaceholder_: function(enablePassword) { 371 getInputPlaceholder_: function(enablePassword) {
300 return enablePassword ? this.i18n('pinKeyboardPlaceholderPinPassword') : 372 return enablePassword ? this.i18n('pinKeyboardPlaceholderPinPassword') :
301 this.i18n('pinKeyboardPlaceholderPin'); 373 this.i18n('pinKeyboardPlaceholderPin');
302 }, 374 },
303 375
304 /** 376 /**
305 * Computes the direction of the pin input. 377 * Computes the direction of the pin input.
306 * @param {string} password 378 * @param {string} password
307 * @private 379 * @private
308 */ 380 */
309 isInputRtl_: function(password) { 381 isInputRtl_: function(password) {
310 // +password will convert a string to a number or to NaN if that's not 382 // +password will convert a string to a number or to NaN if that's not
311 // possible. Number.isInteger will verify the value is not a NaN and that it 383 // possible. Number.isInteger will verify the value is not a NaN and that it
312 // does not contain decimals. 384 // does not contain decimals.
313 // This heuristic will fail for inputs like '1.0'. 385 // This heuristic will fail for inputs like '1.0'.
314 // 386 //
315 // Since we still support users entering their passwords through the PIN 387 // Since we still support users entering their passwords through the PIN
316 // keyboard, we swap the input box to rtl when we think it is a password 388 // keyboard, we swap the input box to rtl when we think it is a password
317 // (just numbers), if the document direction is rtl. 389 // (just numbers), if the document direction is rtl.
318 return (document.dir == 'rtl') && !Number.isInteger(+password); 390 return (document.dir == 'rtl') && !Number.isInteger(+password);
319 }, 391 },
320 }); 392 });
321 })(); 393 })();
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698