|
7 | 7 | */
|
8 | 8 |
|
9 | 9 | import {ModifierKeys} from '@angular/cdk/testing';
|
| 10 | +import {PERIOD} from '@angular/cdk/keycodes'; |
10 | 11 | import {dispatchFakeEvent, dispatchKeyboardEvent} from './dispatch-events';
|
11 | 12 | import {triggerFocus} from './element-focus';
|
12 | 13 |
|
| 14 | +/** Input types for which the value can be entered incrementally. */ |
| 15 | +const incrementalInputTypes = |
| 16 | + new Set(['text', 'email', 'hidden', 'password', 'search', 'tel', 'url']); |
| 17 | + |
13 | 18 | /**
|
14 | 19 | * Checks whether the given Element is a text input element.
|
15 | 20 | * @docs-private
|
16 | 21 | */
|
17 | 22 | export function isTextInput(element: Element): element is HTMLInputElement | HTMLTextAreaElement {
|
18 | 23 | const nodeName = element.nodeName.toLowerCase();
|
19 |
| - return nodeName === 'input' || nodeName === 'textarea' ; |
| 24 | + return nodeName === 'input' || nodeName === 'textarea'; |
20 | 25 | }
|
21 | 26 |
|
22 | 27 | /**
|
@@ -51,21 +56,47 @@ export function typeInElement(element: HTMLElement, ...modifiersAndKeys: any) {
|
51 | 56 | modifiers = {};
|
52 | 57 | rest = modifiersAndKeys;
|
53 | 58 | }
|
| 59 | + const isInput = isTextInput(element); |
| 60 | + const inputType = element.getAttribute('type') || 'text'; |
54 | 61 | const keys: {keyCode?: number, key?: string}[] = rest
|
55 | 62 | .map(k => typeof k === 'string' ?
|
56 | 63 | k.split('').map(c => ({keyCode: c.toUpperCase().charCodeAt(0), key: c})) : [k])
|
57 | 64 | .reduce((arr, k) => arr.concat(k), []);
|
58 | 65 |
|
| 66 | + // We simulate the user typing in a value by incrementally assigning the value below. The problem |
| 67 | + // is that for some input types, the browser won't allow for an invalid value to be set via the |
| 68 | + // `value` property which will always be the case when going character-by-character. If we detect |
| 69 | + // such an input, we have to set the value all at once or listeners to the `input` event (e.g. |
| 70 | + // the `ReactiveFormsModule` uses such an approach) won't receive the correct value. |
| 71 | + const enterValueIncrementally = inputType === 'number' && keys.length > 0 ? |
| 72 | + // The value can be set character by character in number inputs if it doesn't have any decimals. |
| 73 | + keys.every(key => key.key !== '.' && key.keyCode !== PERIOD) : |
| 74 | + incrementalInputTypes.has(inputType); |
| 75 | + |
59 | 76 | triggerFocus(element);
|
| 77 | + |
| 78 | + // When we aren't entering the value incrementally, assign it all at once ahead |
| 79 | + // of time so that any listeners to the key events below will have access to it. |
| 80 | + if (!enterValueIncrementally) { |
| 81 | + (element as HTMLInputElement).value = keys.reduce((value, key) => value + (key.key || ''), ''); |
| 82 | + } |
| 83 | + |
60 | 84 | for (const key of keys) {
|
61 | 85 | dispatchKeyboardEvent(element, 'keydown', key.keyCode, key.key, modifiers);
|
62 | 86 | dispatchKeyboardEvent(element, 'keypress', key.keyCode, key.key, modifiers);
|
63 |
| - if (isTextInput(element) && key.key && key.key.length === 1) { |
64 |
| - element.value += key.key; |
65 |
| - dispatchFakeEvent(element, 'input'); |
| 87 | + if (isInput && key.key && key.key.length === 1) { |
| 88 | + if (enterValueIncrementally) { |
| 89 | + (element as HTMLInputElement | HTMLTextAreaElement).value += key.key; |
| 90 | + dispatchFakeEvent(element, 'input'); |
| 91 | + } |
66 | 92 | }
|
67 | 93 | dispatchKeyboardEvent(element, 'keyup', key.keyCode, key.key, modifiers);
|
68 | 94 | }
|
| 95 | + |
| 96 | + // Since we weren't dispatching `input` events while sending the keys, we have to do it now. |
| 97 | + if (!enterValueIncrementally) { |
| 98 | + dispatchFakeEvent(element, 'input'); |
| 99 | + } |
69 | 100 | }
|
70 | 101 |
|
71 | 102 | /**
|
|
0 commit comments