Skip to content

Commit 3fd97c3

Browse files
committed
feature(cdk/testing) emulate selection in testbed environment
- emulate cursor move on send arrow keys - emulate selection on send shift + arrow key - respect cursor position and selection on send characters Prerequisite #19709 (will need select on tab into input with text)
1 parent 7e0aa85 commit 3fd97c3

File tree

7 files changed

+719
-85
lines changed

7 files changed

+719
-85
lines changed
Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
import {
10+
TextInputElement, getSelectionStart, getValueLength, getSelectionEnd,
11+
} from './text-input-element';
12+
13+
type ArrowKey = 'ArrowUp' | 'ArrowDown' | 'ArrowLeft' | 'ArrowRight';
14+
export function isArrowKey(key: string): key is ArrowKey {
15+
return key.startsWith('Arrow');
16+
}
17+
18+
/**
19+
* will move cursor and/or change selection of text value
20+
*/
21+
export function emulateArrowInTextInput(
22+
modifiers: { shift?: boolean }, key: ArrowKey, element: TextInputElement,
23+
) {
24+
if (modifiers.shift) {
25+
switch (key) {
26+
case 'ArrowUp': return emulateShiftArrowUp(element);
27+
case 'ArrowRight': return emulateShiftArrowRight(element);
28+
case 'ArrowDown': return emulateShiftArrowDown(element);
29+
case 'ArrowLeft': return emulateShiftArrowLeft(element);
30+
}
31+
} else {
32+
switch (key) {
33+
case 'ArrowUp': return emulateArrowUp(element);
34+
case 'ArrowRight': return emulateArrowRight(element);
35+
case 'ArrowDown': return emulateArrowDown(element);
36+
case 'ArrowLeft': return emulateArrowLeft(element);
37+
}
38+
}
39+
}
40+
41+
function emulateArrowUp(target: TextInputElement) {
42+
const lineLength = getLineLength(target);
43+
const selectionStart = getSelectionStart(target);
44+
45+
setCursor(target, Math.max(selectionStart - lineLength, 0));
46+
}
47+
48+
function setCursor(target: TextInputElement, position: number) {
49+
target.setSelectionRange(position, position, 'none');
50+
}
51+
52+
function getLineLength(target: TextInputElement, valueLength = getValueLength(target)) {
53+
return target instanceof HTMLTextAreaElement ? target.cols : valueLength;
54+
}
55+
56+
function emulateArrowRight(target: TextInputElement) {
57+
if (target.selectionStart !== target.selectionEnd) {
58+
setCursor(target, getSelectionEnd(target));
59+
return;
60+
}
61+
62+
const valueLength = getValueLength(target);
63+
const selectionEnd = target.selectionEnd;
64+
if (selectionEnd && selectionEnd < valueLength) {
65+
setCursor(target, selectionEnd + 1);
66+
} else {
67+
setCursor(target, valueLength);
68+
}
69+
}
70+
71+
function emulateArrowDown(target: TextInputElement) {
72+
const valueLength = getValueLength(target);
73+
const lineLength = getLineLength(target, valueLength);
74+
const selectionEnd = getSelectionEnd(target);
75+
setCursor(target, Math.min(selectionEnd + lineLength, valueLength));
76+
}
77+
78+
function emulateArrowLeft(target: TextInputElement) {
79+
if (target.selectionStart !== target.selectionEnd) {
80+
setCursor(target, getSelectionStart(target));
81+
target.selectionEnd = target.selectionStart;
82+
return;
83+
}
84+
85+
const selectionStart = getSelectionStart(target);
86+
if (selectionStart && selectionStart > 0) {
87+
setCursor(target, selectionStart - 1);
88+
return;
89+
}
90+
91+
const valueLength = getValueLength(target);
92+
if (valueLength > 0) {
93+
setCursor(target, valueLength - 1);
94+
return;
95+
}
96+
97+
setCursor(target, 0);
98+
}
99+
100+
function emulateShiftArrowUp(target: TextInputElement) {
101+
const lineLength = getLineLength(target);
102+
103+
if (isSelectionDirectionOf(target, 'forward')) {
104+
reduceSelectionAtSelectionEnd(target, lineLength);
105+
} else {
106+
extendSelectionAtSelectionStart(target, lineLength);
107+
}
108+
}
109+
110+
function isSelectionDirectionOf(target: TextInputElement, direction: 'forward' | 'backward') {
111+
return target.selectionDirection === direction && (target.selectionStart !== target.selectionEnd);
112+
}
113+
114+
function extendSelectionAtSelectionStart(target: TextInputElement, lengthToExtend: number) {
115+
const selectionStart = getSelectionStart(target);
116+
const newSelectionStart = Math.max(selectionStart - lengthToExtend, 0);
117+
target.selectionStart = newSelectionStart;
118+
if (target.selectionDirection !== 'backward') {
119+
target.selectionDirection = 'backward';
120+
}
121+
}
122+
123+
function reduceSelectionAtSelectionEnd(target: TextInputElement, lengthToReduce: number) {
124+
const selectionStart = getSelectionStart(target);
125+
const selectionEnd = getSelectionEnd(target);
126+
const newSelectionEnd = selectionEnd - lengthToReduce;
127+
128+
if (selectionStart < newSelectionEnd) {
129+
target.selectionEnd = newSelectionEnd;
130+
} else {
131+
target.selectionEnd = selectionStart;
132+
target.selectionDirection = 'none';
133+
// there's a different behavior in firefox, which would move selection end:
134+
// target.selectionEnd = selectionStart;
135+
// target.selectionStart = newSelectionEnd;
136+
// target.selectionDirection = 'backward';
137+
}
138+
}
139+
140+
function emulateShiftArrowRight(target: TextInputElement) {
141+
if (isSelectionDirectionOf(target, 'backward')) {
142+
reduceSelectionAtSelectionStart(target, 1);
143+
} else {
144+
extendSelectionAtSelectionEnd(target, 1);
145+
}
146+
}
147+
148+
function extendSelectionAtSelectionEnd(target: TextInputElement, lengthToExtend: number) {
149+
const valueLength = getValueLength(target);
150+
const selectionEnd = getSelectionEnd(target);
151+
target.selectionEnd = Math.min(selectionEnd + lengthToExtend, valueLength);
152+
if (target.selectionDirection !== 'forward') {
153+
target.selectionDirection = 'forward';
154+
}
155+
}
156+
157+
function reduceSelectionAtSelectionStart(target: TextInputElement, lengthToReduce: number) {
158+
const selectionStart = getSelectionStart(target);
159+
const selectionEnd = getSelectionEnd(target);
160+
const newSelectionStart = selectionStart + lengthToReduce;
161+
162+
if (newSelectionStart < selectionEnd) {
163+
target.selectionStart = newSelectionStart;
164+
} else {
165+
target.selectionStart = selectionEnd;
166+
target.selectionDirection = 'none';
167+
// there's a different behavior in firefox, which would move selection end:
168+
// target.selectionStart = selectionEnd;
169+
// target.selectionEnd = newSelectionStart;
170+
// target.selectionDirection = 'forward';
171+
}
172+
}
173+
174+
function emulateShiftArrowDown(target: TextInputElement) {
175+
const lineLength = getLineLength(target);
176+
177+
if (isSelectionDirectionOf(target, 'backward')) {
178+
reduceSelectionAtSelectionStart(target, lineLength);
179+
} else {
180+
extendSelectionAtSelectionEnd(target, lineLength);
181+
}
182+
}
183+
184+
function emulateShiftArrowLeft(target: TextInputElement) {
185+
if (isSelectionDirectionOf(target, 'forward')) {
186+
reduceSelectionAtSelectionEnd(target, 1);
187+
} else {
188+
extendSelectionAtSelectionStart(target, 1);
189+
}
190+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
import {dispatchFakeEvent} from './dispatch-events';
10+
import {getSelectionEnd, getSelectionStart, TextInputElement} from './text-input-element';
11+
12+
/**
13+
* emulate writing characters into text input fields.
14+
* @docs-private
15+
*/
16+
export function writeCharacter(element: TextInputElement, key: string) {
17+
if (!hasToRespectSelection(element)) {
18+
element.value += key;
19+
} else {
20+
const value = element.value;
21+
const selectionStart = getSelectionStart(element);
22+
const valueBeforeSelection = value.substr(0, selectionStart);
23+
const selectionEnd = getSelectionEnd(element);
24+
const valueAfterSelection = value.substr(selectionEnd);
25+
element.value = valueBeforeSelection + key + valueAfterSelection;
26+
27+
const cursor = selectionStart + key.length;
28+
element.setSelectionRange(cursor, cursor, 'none');
29+
}
30+
dispatchFakeEvent(element, 'input');
31+
}
32+
33+
function hasToRespectSelection(element: TextInputElement): boolean {
34+
return typeof element.value === 'string' && element.value.length > 0
35+
&& typeof element.selectionStart === 'number' && element.selectionStart < element.value.length
36+
;
37+
}

0 commit comments

Comments
 (0)