@flighthq/textinput 0.2.0 → 0.2.1-next.423.3350e53

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flighthq/textinput",
3
- "version": "0.2.0",
3
+ "version": "0.2.1-next.423.3350e53",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/flighthq/flight.git",
@@ -32,11 +32,11 @@
32
32
  "clean:dist": "tsx ../../scripts/clean-package-dist.ts"
33
33
  },
34
34
  "dependencies": {
35
- "@flighthq/node": "0.2.0",
36
- "@flighthq/signals": "0.2.0",
37
- "@flighthq/text": "0.2.0",
38
- "@flighthq/textlayout": "0.2.0",
39
- "@flighthq/types": "0.2.0"
35
+ "@flighthq/node": "0.2.1-next.423.3350e53",
36
+ "@flighthq/signals": "0.2.1-next.423.3350e53",
37
+ "@flighthq/text": "0.2.1-next.423.3350e53",
38
+ "@flighthq/textlayout": "0.2.1-next.423.3350e53",
39
+ "@flighthq/types": "0.2.1-next.423.3350e53"
40
40
  },
41
41
  "devDependencies": {
42
42
  "typescript": "^5.3.0"
@@ -839,6 +839,34 @@ describe('selectLineAtTextInputIndex', () => {
839
839
  expect(getTextInputSelectionBeginIndex(text)).toBe(6);
840
840
  expect(getTextInputSelectionEndIndex(text)).toBe(11);
841
841
  });
842
+
843
+ it('selects the correct line in multiline text', () => {
844
+ const text = createInput({ text: 'aaa\nbbb\nccc' });
845
+ selectLineAtTextInputIndex(text, 5);
846
+ expect(getTextInputSelectionBeginIndex(text)).toBe(4);
847
+ expect(getTextInputSelectionEndIndex(text)).toBe(7);
848
+ });
849
+
850
+ it('selects the first line when index is at a line boundary', () => {
851
+ const text = createInput({ text: 'hello\nworld' });
852
+ selectLineAtTextInputIndex(text, 5);
853
+ expect(getTextInputSelectionBeginIndex(text)).toBe(0);
854
+ expect(getTextInputSelectionEndIndex(text)).toBe(5);
855
+ });
856
+
857
+ it('selects empty range for empty text', () => {
858
+ const text = createInput({ text: '' });
859
+ selectLineAtTextInputIndex(text, 0);
860
+ expect(getTextInputSelectionBeginIndex(text)).toBe(0);
861
+ expect(getTextInputSelectionEndIndex(text)).toBe(0);
862
+ });
863
+
864
+ it('selects the last line', () => {
865
+ const text = createInput({ text: 'aaa\nbbb\nccc' });
866
+ selectLineAtTextInputIndex(text, 10);
867
+ expect(getTextInputSelectionBeginIndex(text)).toBe(8);
868
+ expect(getTextInputSelectionEndIndex(text)).toBe(11);
869
+ });
842
870
  });
843
871
 
844
872
  describe('selectWordAtTextInputIndex', () => {
@@ -848,6 +876,48 @@ describe('selectWordAtTextInputIndex', () => {
848
876
  expect(getTextInputSelectionBeginIndex(text)).toBe(0);
849
877
  expect(getTextInputSelectionEndIndex(text)).toBe(5);
850
878
  });
879
+
880
+ it('selects the preceding word when index is at a whitespace boundary', () => {
881
+ const text = createInput({ text: 'hello world' });
882
+ selectWordAtTextInputIndex(text, 5);
883
+ expect(getTextInputSelectionBeginIndex(text)).toBe(0);
884
+ expect(getTextInputSelectionEndIndex(text)).toBe(5);
885
+ });
886
+
887
+ it('selects non-word region when surrounded by non-word characters', () => {
888
+ const text = createInput({ text: ' hello' });
889
+ selectWordAtTextInputIndex(text, 1);
890
+ expect(getTextInputSelectionBeginIndex(text)).toBe(0);
891
+ expect(getTextInputSelectionEndIndex(text)).toBe(2);
892
+ });
893
+
894
+ it('selects the preceding word when index is at punctuation', () => {
895
+ const text = createInput({ text: 'hello, world' });
896
+ selectWordAtTextInputIndex(text, 5);
897
+ expect(getTextInputSelectionBeginIndex(text)).toBe(0);
898
+ expect(getTextInputSelectionEndIndex(text)).toBe(5);
899
+ });
900
+
901
+ it('selects word at start of text (index 0)', () => {
902
+ const text = createInput({ text: 'hello world' });
903
+ selectWordAtTextInputIndex(text, 0);
904
+ expect(getTextInputSelectionBeginIndex(text)).toBe(0);
905
+ expect(getTextInputSelectionEndIndex(text)).toBe(5);
906
+ });
907
+
908
+ it('selects word at end of text', () => {
909
+ const text = createInput({ text: 'hello world' });
910
+ selectWordAtTextInputIndex(text, 11);
911
+ expect(getTextInputSelectionBeginIndex(text)).toBe(6);
912
+ expect(getTextInputSelectionEndIndex(text)).toBe(11);
913
+ });
914
+
915
+ it('selects empty range for empty text', () => {
916
+ const text = createInput({ text: '' });
917
+ selectWordAtTextInputIndex(text, 0);
918
+ expect(getTextInputSelectionBeginIndex(text)).toBe(0);
919
+ expect(getTextInputSelectionEndIndex(text)).toBe(0);
920
+ });
851
921
  });
852
922
 
853
923
  describe('setTextInputSelection', () => {
@@ -1,6 +1,6 @@
1
1
  import { createSignal, emitSignal } from '@flighthq/signals';
2
- import { createRichText } from '@flighthq/text';
3
- import type { RichText, RichTextData } from '@flighthq/types';
2
+ import { createRichText, getRichTextRuntime } from '@flighthq/text';
3
+ import type { RichText, RichTextData, RichTextRuntime, TextLayoutResult } from '@flighthq/types';
4
4
  import { KeyCode } from '@flighthq/types';
5
5
 
6
6
  import { enableTextInput, getTextInputState } from './textInput';
@@ -27,6 +27,42 @@ function createInput(data: Partial<RichTextData> = {}): RichText {
27
27
  return text;
28
28
  }
29
29
 
30
+ // Creates a mock layout for "hello world" (11 chars) as a single line. Each character is 10px wide,
31
+ // starting at offsetX=2. This makes getTextInputCharacterIndexAtPoint predictable: x=7 lands on
32
+ // index 0, x=17 on index 1, etc. (midpoint rounding: x < offsetX + advance/2 → that index).
33
+ function createSingleLineLayout(): TextLayoutResult {
34
+ return {
35
+ groups: [
36
+ {
37
+ ascent: 10,
38
+ descent: 2,
39
+ endIndex: 11,
40
+ format: {},
41
+ height: 12,
42
+ leading: 0,
43
+ lineIndex: 0,
44
+ offsetX: 2,
45
+ offsetY: 2,
46
+ positions: [10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10],
47
+ startIndex: 0,
48
+ width: 110,
49
+ },
50
+ ],
51
+ lineAscents: [10],
52
+ lineDescents: [2],
53
+ lineHeights: [12],
54
+ lineLeadings: [0],
55
+ lineWidths: [110],
56
+ numLines: 1,
57
+ textHeight: 12,
58
+ textWidth: 110,
59
+ };
60
+ }
61
+
62
+ function setLayout(target: RichText, layout: TextLayoutResult): void {
63
+ (getRichTextRuntime(target) as RichTextRuntime).textLayout = layout;
64
+ }
65
+
30
66
  function isFocused(node: RichText): boolean {
31
67
  return getTextInputState(node)?.focused === true;
32
68
  }
@@ -129,12 +165,75 @@ describe('dispatchTextInputPointerDown', () => {
129
165
  expect(manager.focused).toBe(target);
130
166
  expect(isFocused(target)).toBe(true);
131
167
  });
168
+
169
+ it('selects a word on double-click (clickCount=2)', () => {
170
+ const manager = createTextInputManager();
171
+ const target = createInput({ text: 'hello world' });
172
+ setLayout(target, createSingleLineLayout());
173
+ // x=7 lands on index 0 (inside "hello"); clickCount=2 selects the word.
174
+ dispatchTextInputPointerDown(manager, target, 7, 5, false, 2);
175
+ expect(getTextInputSelectionBeginIndex(target)).toBe(0);
176
+ expect(getTextInputSelectionEndIndex(target)).toBe(5);
177
+ });
178
+
179
+ it('selects a line on triple-click (clickCount=3)', () => {
180
+ const manager = createTextInputManager();
181
+ const target = createInput({ text: 'hello world' });
182
+ setLayout(target, createSingleLineLayout());
183
+ // Any x on the single line; clickCount=3 selects the entire line.
184
+ dispatchTextInputPointerDown(manager, target, 7, 5, false, 3);
185
+ expect(getTextInputSelectionBeginIndex(target)).toBe(0);
186
+ expect(getTextInputSelectionEndIndex(target)).toBe(11);
187
+ });
132
188
  });
133
189
 
134
190
  describe('dispatchTextInputPointerMove', () => {
135
191
  it('does not throw when nothing is focused', () => {
136
192
  expect(() => dispatchTextInputPointerMove(createTextInputManager(), 0, 0)).not.toThrow();
137
193
  });
194
+
195
+ it('extends selection when dragging after pointer-down', () => {
196
+ const manager = createTextInputManager();
197
+ const target = createInput({ text: 'hello world' });
198
+ const layout = createSingleLineLayout();
199
+ setLayout(target, layout);
200
+ // Pointer-down at index 2 (x=2+2*10=22, midpoint at 27; use 23 to land on index 2).
201
+ dispatchTextInputPointerDown(manager, target, 23, 5);
202
+ expect(getTextInputSelectionBeginIndex(target)).toBe(2);
203
+ expect(getTextInputSelectionEndIndex(target)).toBe(2);
204
+ // Pointer-move to index 7 (x=2+7*10=72, midpoint at 77; use 73 to land on index 7).
205
+ dispatchTextInputPointerMove(manager, 73, 5);
206
+ expect(getTextInputSelectionBeginIndex(target)).toBe(2);
207
+ expect(getTextInputSelectionEndIndex(target)).toBe(7);
208
+ });
209
+
210
+ it('extends selection backward when pointer moves left', () => {
211
+ const manager = createTextInputManager();
212
+ const target = createInput({ text: 'hello world' });
213
+ const layout = createSingleLineLayout();
214
+ setLayout(target, layout);
215
+ // Pointer-down at index 7 (x=73).
216
+ dispatchTextInputPointerDown(manager, target, 73, 5);
217
+ // Pointer-move to index 2 (x=23).
218
+ dispatchTextInputPointerMove(manager, 23, 5);
219
+ expect(getTextInputSelectionBeginIndex(target)).toBe(2);
220
+ expect(getTextInputSelectionEndIndex(target)).toBe(7);
221
+ });
222
+
223
+ it('clamps selection at text boundaries', () => {
224
+ const manager = createTextInputManager();
225
+ const target = createInput({ text: 'hello world' });
226
+ const layout = createSingleLineLayout();
227
+ setLayout(target, layout);
228
+ // Pointer-down at index 5.
229
+ dispatchTextInputPointerDown(manager, target, 53, 5);
230
+ // Pointer-move far past the end of text.
231
+ dispatchTextInputPointerMove(manager, 999, 5);
232
+ expect(getTextInputSelectionEndIndex(target)).toBe(11);
233
+ // Pointer-move far before the start of text.
234
+ dispatchTextInputPointerMove(manager, -100, 5);
235
+ expect(getTextInputSelectionBeginIndex(target)).toBe(0);
236
+ });
138
237
  });
139
238
 
140
239
  describe('dispatchTextInputWheel', () => {