@headless-tree/core 0.0.9 → 0.0.11
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/CHANGELOG.md +12 -0
- package/lib/cjs/core/build-proxified-instance.d.ts +2 -0
- package/lib/cjs/core/build-proxified-instance.js +58 -0
- package/lib/cjs/core/build-static-instance.d.ts +2 -0
- package/lib/cjs/core/build-static-instance.js +27 -0
- package/lib/cjs/core/create-tree.js +55 -36
- package/lib/cjs/features/async-data-loader/feature.js +37 -23
- package/lib/cjs/features/async-data-loader/types.d.ts +2 -1
- package/lib/cjs/features/drag-and-drop/feature.js +64 -32
- package/lib/cjs/features/drag-and-drop/types.d.ts +13 -4
- package/lib/cjs/features/drag-and-drop/utils.d.ts +1 -2
- package/lib/cjs/features/drag-and-drop/utils.js +140 -37
- package/lib/cjs/features/expand-all/feature.js +12 -6
- package/lib/cjs/features/main/types.d.ts +8 -2
- package/lib/cjs/features/renaming/feature.js +33 -18
- package/lib/cjs/features/renaming/types.d.ts +1 -1
- package/lib/cjs/features/search/feature.js +38 -24
- package/lib/cjs/features/search/types.d.ts +0 -1
- package/lib/cjs/features/selection/feature.js +23 -14
- package/lib/cjs/features/sync-data-loader/feature.js +7 -2
- package/lib/cjs/features/tree/feature.d.ts +2 -1
- package/lib/cjs/features/tree/feature.js +85 -63
- package/lib/cjs/features/tree/types.d.ts +5 -3
- package/lib/cjs/index.d.ts +3 -1
- package/lib/cjs/index.js +2 -1
- package/lib/cjs/test-utils/test-tree-do.d.ts +23 -0
- package/lib/cjs/test-utils/test-tree-do.js +99 -0
- package/lib/cjs/test-utils/test-tree-expect.d.ts +15 -0
- package/lib/cjs/test-utils/test-tree-expect.js +62 -0
- package/lib/cjs/test-utils/test-tree.d.ts +47 -0
- package/lib/cjs/test-utils/test-tree.js +195 -0
- package/lib/cjs/types/core.d.ts +31 -15
- package/lib/cjs/utilities/errors.d.ts +1 -0
- package/lib/cjs/utilities/errors.js +5 -0
- package/lib/cjs/utilities/insert-items-at-target.js +10 -3
- package/lib/cjs/utilities/remove-items-from-parents.js +14 -8
- package/lib/cjs/utils.d.ts +3 -3
- package/lib/cjs/utils.js +6 -6
- package/lib/esm/core/build-proxified-instance.d.ts +2 -0
- package/lib/esm/core/build-proxified-instance.js +54 -0
- package/lib/esm/core/build-static-instance.d.ts +2 -0
- package/lib/esm/core/build-static-instance.js +23 -0
- package/lib/esm/core/create-tree.js +55 -36
- package/lib/esm/features/async-data-loader/feature.js +37 -23
- package/lib/esm/features/async-data-loader/types.d.ts +2 -1
- package/lib/esm/features/drag-and-drop/feature.js +64 -32
- package/lib/esm/features/drag-and-drop/types.d.ts +13 -4
- package/lib/esm/features/drag-and-drop/utils.d.ts +1 -2
- package/lib/esm/features/drag-and-drop/utils.js +138 -34
- package/lib/esm/features/expand-all/feature.js +12 -6
- package/lib/esm/features/main/types.d.ts +8 -2
- package/lib/esm/features/renaming/feature.js +33 -18
- package/lib/esm/features/renaming/types.d.ts +1 -1
- package/lib/esm/features/search/feature.js +38 -24
- package/lib/esm/features/search/types.d.ts +0 -1
- package/lib/esm/features/selection/feature.js +23 -14
- package/lib/esm/features/sync-data-loader/feature.js +7 -2
- package/lib/esm/features/tree/feature.d.ts +2 -1
- package/lib/esm/features/tree/feature.js +86 -64
- package/lib/esm/features/tree/types.d.ts +5 -3
- package/lib/esm/index.d.ts +3 -1
- package/lib/esm/index.js +2 -1
- package/lib/esm/test-utils/test-tree-do.d.ts +23 -0
- package/lib/esm/test-utils/test-tree-do.js +95 -0
- package/lib/esm/test-utils/test-tree-expect.d.ts +15 -0
- package/lib/esm/test-utils/test-tree-expect.js +58 -0
- package/lib/esm/test-utils/test-tree.d.ts +47 -0
- package/lib/esm/test-utils/test-tree.js +191 -0
- package/lib/esm/types/core.d.ts +31 -15
- package/lib/esm/utilities/errors.d.ts +1 -0
- package/lib/esm/utilities/errors.js +1 -0
- package/lib/esm/utilities/insert-items-at-target.js +10 -3
- package/lib/esm/utilities/remove-items-from-parents.js +14 -8
- package/lib/esm/utils.d.ts +3 -3
- package/lib/esm/utils.js +3 -3
- package/package.json +7 -3
- package/src/core/build-proxified-instance.ts +115 -0
- package/src/core/build-static-instance.ts +28 -0
- package/src/core/create-tree.ts +60 -62
- package/src/features/async-data-loader/async-data-loader.spec.ts +143 -0
- package/src/features/async-data-loader/feature.ts +33 -31
- package/src/features/async-data-loader/types.ts +3 -1
- package/src/features/drag-and-drop/drag-and-drop.spec.ts +716 -0
- package/src/features/drag-and-drop/feature.ts +109 -85
- package/src/features/drag-and-drop/types.ts +21 -7
- package/src/features/drag-and-drop/utils.ts +196 -55
- package/src/features/expand-all/expand-all.spec.ts +52 -0
- package/src/features/expand-all/feature.ts +8 -12
- package/src/features/hotkeys-core/feature.ts +1 -1
- package/src/features/main/types.ts +14 -1
- package/src/features/renaming/feature.ts +30 -29
- package/src/features/renaming/renaming.spec.ts +125 -0
- package/src/features/renaming/types.ts +1 -1
- package/src/features/search/feature.ts +34 -38
- package/src/features/search/search.spec.ts +115 -0
- package/src/features/search/types.ts +0 -1
- package/src/features/selection/feature.ts +29 -30
- package/src/features/selection/selection.spec.ts +220 -0
- package/src/features/sync-data-loader/feature.ts +8 -11
- package/src/features/tree/feature.ts +82 -87
- package/src/features/tree/tree.spec.ts +515 -0
- package/src/features/tree/types.ts +5 -3
- package/src/index.ts +4 -1
- package/src/test-utils/test-tree-do.ts +136 -0
- package/src/test-utils/test-tree-expect.ts +86 -0
- package/src/test-utils/test-tree.ts +217 -0
- package/src/types/core.ts +92 -33
- package/src/utilities/errors.ts +2 -0
- package/src/utilities/insert-items-at-target.ts +10 -3
- package/src/utilities/remove-items-from-parents.ts +15 -10
- package/src/utils.spec.ts +89 -0
- package/src/utils.ts +6 -6
- package/tsconfig.json +1 -0
- package/vitest.config.ts +6 -0
|
@@ -0,0 +1,515 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { TestTree } from "../../test-utils/test-tree";
|
|
3
|
+
|
|
4
|
+
const factory = TestTree.default({});
|
|
5
|
+
|
|
6
|
+
describe("core-feature/selections", () => {
|
|
7
|
+
factory.forSuits((tree) => {
|
|
8
|
+
describe("expanded items", () => {
|
|
9
|
+
it("can expand via tree instance", () => {
|
|
10
|
+
const setExpandedItems = tree.mockedHandler("setExpandedItems");
|
|
11
|
+
tree.instance.expandItem("x2");
|
|
12
|
+
expect(setExpandedItems).toHaveBeenCalledWith(["x1", "x11", "x2"]);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it("can expand via item", () => {
|
|
16
|
+
const setExpandedItems = tree.mockedHandler("setExpandedItems");
|
|
17
|
+
tree.instance.getItemInstance("x2").expand();
|
|
18
|
+
expect(setExpandedItems).toHaveBeenCalledWith(["x1", "x11", "x2"]);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("returns correct isItemExpanded value", () => {
|
|
22
|
+
expect(tree.instance.isItemExpanded("x2")).toBe(false);
|
|
23
|
+
tree.instance.expandItem("x2");
|
|
24
|
+
expect(tree.instance.isItemExpanded("x2")).toBe(true);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("calls setState", () => {
|
|
28
|
+
const setState = tree.mockedHandler("setState");
|
|
29
|
+
tree.instance.expandItem("x2");
|
|
30
|
+
expect(setState).toBeCalledWith(
|
|
31
|
+
expect.objectContaining({ expandedItems: ["x1", "x11", "x2"] }),
|
|
32
|
+
);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("expands on click", () => {
|
|
36
|
+
const setExpandedItems = tree.mockedHandler("setExpandedItems");
|
|
37
|
+
tree.do.selectItem("x2");
|
|
38
|
+
expect(setExpandedItems).toHaveBeenCalledWith(["x1", "x11", "x2"]);
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
describe("collapsed items", () => {
|
|
43
|
+
it("can collapse via tree instance", () => {
|
|
44
|
+
const setExpandedItems = tree.mockedHandler("setExpandedItems");
|
|
45
|
+
tree.instance.collapseItem("x1");
|
|
46
|
+
expect(setExpandedItems).toHaveBeenCalledWith(["x11"]);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("can collapse via item", () => {
|
|
50
|
+
const setExpandedItems = tree.mockedHandler("setExpandedItems");
|
|
51
|
+
tree.instance.getItemInstance("x1").collapse();
|
|
52
|
+
expect(setExpandedItems).toHaveBeenCalledWith(["x11"]);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("returns correct isItemExpanded value", () => {
|
|
56
|
+
expect(tree.instance.isItemExpanded("x1")).toBe(true);
|
|
57
|
+
tree.instance.collapseItem("x1");
|
|
58
|
+
expect(tree.instance.isItemExpanded("x1")).toBe(false);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("calls setState", () => {
|
|
62
|
+
const setState = tree.mockedHandler("setState");
|
|
63
|
+
tree.instance.collapseItem("x1");
|
|
64
|
+
expect(setState).toBeCalledWith(
|
|
65
|
+
expect.objectContaining({ expandedItems: ["x11"] }),
|
|
66
|
+
);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("collapses on click", () => {
|
|
70
|
+
const setExpandedItems = tree.mockedHandler("setExpandedItems");
|
|
71
|
+
tree.do.selectItem("x1");
|
|
72
|
+
expect(setExpandedItems).toHaveBeenCalledWith(["x11"]);
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
describe("focused item", () => {
|
|
77
|
+
it("focuses item on tree instance", () => {
|
|
78
|
+
const setFocusedItem = tree.mockedHandler("setFocusedItem");
|
|
79
|
+
tree.instance.focusItem("x11");
|
|
80
|
+
expect(setFocusedItem).toHaveBeenCalledWith("x11");
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("focuses item on item instance", () => {
|
|
84
|
+
const setFocusedItem = tree.mockedHandler("setFocusedItem");
|
|
85
|
+
tree.instance.getItemInstance("x11").setFocused();
|
|
86
|
+
expect(setFocusedItem).toHaveBeenCalledWith("x11");
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("returns correct initial focused item", () => {
|
|
90
|
+
expect(tree.instance.getFocusedItem().getId()).toBe("x1");
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("returns correct focused item", () => {
|
|
94
|
+
tree.instance.focusItem("x11");
|
|
95
|
+
expect(tree.instance.getFocusedItem().getId()).toBe("x11");
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it("calls setState", () => {
|
|
99
|
+
const setState = tree.mockedHandler("setState");
|
|
100
|
+
tree.instance.focusItem("x1");
|
|
101
|
+
expect(setState).toBeCalledWith(
|
|
102
|
+
expect.objectContaining({ focusedItem: "x1" }),
|
|
103
|
+
);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("updates dom focus", async () => {
|
|
107
|
+
const scrollToItem = tree.mockedHandler("scrollToItem");
|
|
108
|
+
const element = { focus: vi.fn() };
|
|
109
|
+
tree.instance.getItemInstance("x2").registerElement(element as any);
|
|
110
|
+
tree.instance.focusItem("x2");
|
|
111
|
+
tree.instance.updateDomFocus();
|
|
112
|
+
vi.runAllTimers();
|
|
113
|
+
await vi.waitFor(() =>
|
|
114
|
+
expect(scrollToItem).toBeCalledWith(tree.instance.getFocusedItem()),
|
|
115
|
+
);
|
|
116
|
+
expect(element.focus).toBeCalled();
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
describe("move focus", () => {
|
|
120
|
+
it("focuses next item", () => {
|
|
121
|
+
tree.instance.focusItem("x2");
|
|
122
|
+
const setFocusedItem = tree.mockedHandler("setFocusedItem");
|
|
123
|
+
tree.instance.focusNextItem();
|
|
124
|
+
expect(setFocusedItem).toHaveBeenCalledWith("x3");
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it("focuses previous item", () => {
|
|
128
|
+
tree.instance.focusItem("x2");
|
|
129
|
+
const setFocusedItem = tree.mockedHandler("setFocusedItem");
|
|
130
|
+
tree.instance.focusPreviousItem();
|
|
131
|
+
expect(setFocusedItem).toHaveBeenCalledWith("x14");
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it("remains at last item when focus is at bottom", () => {
|
|
135
|
+
tree.instance.focusItem("x4");
|
|
136
|
+
const setFocusedItem = tree.mockedHandler("setFocusedItem");
|
|
137
|
+
tree.instance.focusNextItem();
|
|
138
|
+
expect(setFocusedItem).toHaveBeenCalledWith("x4");
|
|
139
|
+
expect(setFocusedItem).toHaveBeenCalledTimes(1);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it("remains at first item when focus is at top", () => {
|
|
143
|
+
tree.instance.focusItem("x1");
|
|
144
|
+
const setFocusedItem = tree.mockedHandler("setFocusedItem");
|
|
145
|
+
tree.instance.focusPreviousItem();
|
|
146
|
+
expect(setFocusedItem).toHaveBeenCalledWith("x1");
|
|
147
|
+
expect(setFocusedItem).toHaveBeenCalledTimes(1);
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
describe("items meta", () => {
|
|
153
|
+
it("root item", () => {
|
|
154
|
+
expect(tree.instance.getItemInstance("x").getItemMeta()).toEqual({
|
|
155
|
+
index: -1,
|
|
156
|
+
itemId: "x",
|
|
157
|
+
level: -1,
|
|
158
|
+
parentId: null,
|
|
159
|
+
posInSet: 0,
|
|
160
|
+
setSize: 1,
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it("expanded container", () => {
|
|
165
|
+
expect(tree.instance.getItemInstance("x11").getItemMeta()).toEqual({
|
|
166
|
+
index: 1,
|
|
167
|
+
itemId: "x11",
|
|
168
|
+
level: 1,
|
|
169
|
+
parentId: "x1",
|
|
170
|
+
posInSet: 0,
|
|
171
|
+
setSize: 4,
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it("top leaf", () => {
|
|
176
|
+
expect(tree.instance.getItemInstance("x111").getItemMeta()).toEqual({
|
|
177
|
+
index: 2,
|
|
178
|
+
itemId: "x111",
|
|
179
|
+
level: 2,
|
|
180
|
+
parentId: "x11",
|
|
181
|
+
posInSet: 0,
|
|
182
|
+
setSize: 4,
|
|
183
|
+
});
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it("middle leaf", () => {
|
|
187
|
+
expect(tree.instance.getItemInstance("x112").getItemMeta()).toEqual({
|
|
188
|
+
index: 3,
|
|
189
|
+
itemId: "x112",
|
|
190
|
+
level: 2,
|
|
191
|
+
parentId: "x11",
|
|
192
|
+
posInSet: 1,
|
|
193
|
+
setSize: 4,
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it("bottom leaf", () => {
|
|
198
|
+
expect(tree.instance.getItemInstance("x114").getItemMeta()).toEqual({
|
|
199
|
+
index: 5,
|
|
200
|
+
itemId: "x114",
|
|
201
|
+
level: 2,
|
|
202
|
+
parentId: "x11",
|
|
203
|
+
posInSet: 3,
|
|
204
|
+
setSize: 4,
|
|
205
|
+
});
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it("item after expanded contents", () => {
|
|
209
|
+
expect(tree.instance.getItemInstance("x2").getItemMeta()).toEqual({
|
|
210
|
+
index: 9,
|
|
211
|
+
itemId: "x2",
|
|
212
|
+
level: 0,
|
|
213
|
+
parentId: "x",
|
|
214
|
+
posInSet: 1,
|
|
215
|
+
setSize: 4,
|
|
216
|
+
});
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it("last item", () => {
|
|
220
|
+
expect(tree.instance.getItemInstance("x2").getItemMeta()).toEqual({
|
|
221
|
+
index: 9,
|
|
222
|
+
itemId: "x2",
|
|
223
|
+
level: 0,
|
|
224
|
+
parentId: "x",
|
|
225
|
+
posInSet: 1,
|
|
226
|
+
setSize: 4,
|
|
227
|
+
});
|
|
228
|
+
});
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
describe("props generation", () => {
|
|
232
|
+
it("generates container props", () => {
|
|
233
|
+
expect(tree.instance.getContainerProps()).toEqual(
|
|
234
|
+
expect.objectContaining({
|
|
235
|
+
role: "tree",
|
|
236
|
+
}),
|
|
237
|
+
);
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
it("generates item props for random item", () => {
|
|
241
|
+
expect(tree.instance.getItemInstance("x2").getProps()).toEqual({
|
|
242
|
+
"aria-label": "x2",
|
|
243
|
+
"aria-level": 0,
|
|
244
|
+
"aria-posinset": 1,
|
|
245
|
+
"aria-selected": "false",
|
|
246
|
+
"aria-setsize": 4,
|
|
247
|
+
onClick: expect.any(Function),
|
|
248
|
+
role: "treeitem",
|
|
249
|
+
tabIndex: -1,
|
|
250
|
+
});
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
it("generates item props for focused", () => {
|
|
254
|
+
expect(tree.instance.getItemInstance("x1").getProps()).toEqual({
|
|
255
|
+
"aria-label": "x1",
|
|
256
|
+
"aria-level": 0,
|
|
257
|
+
"aria-posinset": 0,
|
|
258
|
+
"aria-selected": "false",
|
|
259
|
+
"aria-setsize": 4,
|
|
260
|
+
onClick: expect.any(Function),
|
|
261
|
+
role: "treeitem",
|
|
262
|
+
tabIndex: 0,
|
|
263
|
+
});
|
|
264
|
+
});
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
describe("util functions", () => {
|
|
268
|
+
it("returns correctly for getId()", () => {
|
|
269
|
+
expect(tree.instance.getItemInstance("x2").getId()).toBe("x2");
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
it("returns correctly for getItemName()", () => {
|
|
273
|
+
expect(tree.instance.getItemInstance("x2").getItemName()).toBe("x2");
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
it("returns correctly for getItemData()", () => {
|
|
277
|
+
expect(tree.instance.getItemInstance("x2").getItemData()).toBe("x2");
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
it("returns correctly for true cases of isExpanded()", () => {
|
|
281
|
+
expect(tree.instance.getItemInstance("x1").isExpanded()).toBe(true);
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
it("returns correctly for false cases of isExpanded()", () => {
|
|
285
|
+
expect(tree.instance.getItemInstance("x12").isExpanded()).toBe(false);
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
it("returns correctly for direct descendants of isDescendentOf()", () => {
|
|
289
|
+
expect(
|
|
290
|
+
tree.instance.getItemInstance("x111").isDescendentOf("x11"),
|
|
291
|
+
).toBe(true);
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
it("returns correctly for indirect descendants of isDescendentOf()", () => {
|
|
295
|
+
expect(tree.instance.getItemInstance("x111").isDescendentOf("x")).toBe(
|
|
296
|
+
true,
|
|
297
|
+
);
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
it("returns correctly for non-descendants of isDescendentOf()", () => {
|
|
301
|
+
expect(tree.instance.getItemInstance("x12").isDescendentOf("x2")).toBe(
|
|
302
|
+
false,
|
|
303
|
+
);
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
it("returns correctly for true cases of isFocused()", () => {
|
|
307
|
+
expect(tree.instance.getItemInstance("x1").isFocused()).toBe(true);
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
it("returns correctly for false cases of isFocused()", () => {
|
|
311
|
+
expect(tree.instance.getItemInstance("x2").isFocused()).toBe(false);
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
it("returns correctly for true cases of isFolder()", () => {
|
|
315
|
+
expect(tree.instance.getItemInstance("x1").isFolder()).toBe(true);
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
it("returns correctly for false cases of isFolder()", () => {
|
|
319
|
+
expect(tree.instance.getItemInstance("x111").isFolder()).toBe(false);
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
it("returns correctly for getParent()", () => {
|
|
323
|
+
expect(tree.instance.getItemInstance("x111").getParent().getId()).toBe(
|
|
324
|
+
"x11",
|
|
325
|
+
);
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
it("returns correctly for getChildren()", () => {
|
|
329
|
+
expect(
|
|
330
|
+
tree.instance
|
|
331
|
+
.getItemInstance("x1")
|
|
332
|
+
.getChildren()
|
|
333
|
+
.map((i) => i.getId()),
|
|
334
|
+
).toEqual(["x11", "x12", "x13", "x14"]);
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
it("returns correctly for getIndexInParent()", () => {
|
|
338
|
+
expect(tree.instance.getItemInstance("x113").getIndexInParent()).toBe(
|
|
339
|
+
2,
|
|
340
|
+
);
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
it("returns correctly for getTree()", () => {
|
|
344
|
+
expect(tree.instance.getItemInstance("x111").getTree()).toBe(
|
|
345
|
+
tree.instance,
|
|
346
|
+
);
|
|
347
|
+
});
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
describe("primary action", () => {
|
|
351
|
+
it("calls primary action", () => {
|
|
352
|
+
const onPrimaryAction = tree.mockedHandler("onPrimaryAction");
|
|
353
|
+
tree.instance.getItemInstance("x2").primaryAction();
|
|
354
|
+
expect(onPrimaryAction).toHaveBeenCalledWith(
|
|
355
|
+
tree.instance.getItemInstance("x2"),
|
|
356
|
+
);
|
|
357
|
+
});
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
describe("item above/below", () => {
|
|
361
|
+
it("returns correctly for getItemAbove()", () => {
|
|
362
|
+
expect(
|
|
363
|
+
tree.instance.getItemInstance("x12").getItemAbove()?.getId(),
|
|
364
|
+
).toBe("x114");
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
it("returns correctly for getItemAbove() at top", () => {
|
|
368
|
+
expect(tree.instance.getItemInstance("x1").getItemAbove()).toBe(
|
|
369
|
+
undefined,
|
|
370
|
+
);
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
it("returns correctly for getItemBelow()", () => {
|
|
374
|
+
expect(
|
|
375
|
+
tree.instance.getItemInstance("x14").getItemBelow()?.getId(),
|
|
376
|
+
).toBe("x2");
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
it("returns correctly for getItemBelow() at bottom", () => {
|
|
380
|
+
expect(tree.instance.getItemInstance("x4").getItemBelow()).toBe(
|
|
381
|
+
undefined,
|
|
382
|
+
);
|
|
383
|
+
});
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
describe("memoized props", () => {
|
|
387
|
+
it("runs memoized props correctly", () => {
|
|
388
|
+
const fn = vi.fn().mockReturnValue("result");
|
|
389
|
+
const memoizedProp = tree.instance
|
|
390
|
+
.getItemInstance("x1")
|
|
391
|
+
.getMemoizedProp("fn", () => fn);
|
|
392
|
+
expect(memoizedProp(1, 2, 3)).toBe("result");
|
|
393
|
+
expect(fn).toBeCalledWith(1, 2, 3);
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
it("runs just once", () => {
|
|
397
|
+
const fn = vi.fn().mockImplementation(() => () => "result");
|
|
398
|
+
tree.instance.getItemInstance("x1").getMemoizedProp("fn", fn)(1);
|
|
399
|
+
tree.instance.getItemInstance("x1").getMemoizedProp("fn", fn)(1);
|
|
400
|
+
tree.instance.getItemInstance("x1").getMemoizedProp("fn", fn)(1);
|
|
401
|
+
expect(fn).toBeCalledTimes(1);
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
it("reruns if dependencies changed", () => {
|
|
405
|
+
const fn = vi.fn().mockImplementation(() => () => "result");
|
|
406
|
+
tree.instance.getItemInstance("x1").getMemoizedProp("fn", fn, [1])(1);
|
|
407
|
+
tree.instance.getItemInstance("x1").getMemoizedProp("fn", fn, [1])(1);
|
|
408
|
+
tree.instance.getItemInstance("x1").getMemoizedProp("fn", fn, [2])(1);
|
|
409
|
+
expect(fn).toBeCalledTimes(2);
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
it("seperates functions by name", () => {
|
|
413
|
+
const fn1 = vi.fn().mockImplementation(() => () => "result");
|
|
414
|
+
const fn2 = vi.fn().mockImplementation(() => () => "result");
|
|
415
|
+
tree.instance.getItemInstance("x1").getMemoizedProp("fn", fn1, [1])(1);
|
|
416
|
+
tree.instance.getItemInstance("x1").getMemoizedProp("fn", fn1, [1])(1);
|
|
417
|
+
tree.instance.getItemInstance("x1").getMemoizedProp("fn2", fn2, [1])(1);
|
|
418
|
+
expect(fn1).toBeCalledTimes(1);
|
|
419
|
+
expect(fn2).toBeCalledTimes(1);
|
|
420
|
+
});
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
describe("hotkeys", () => {
|
|
424
|
+
it("focuses next item", () => {
|
|
425
|
+
const setFocusedItem = tree.mockedHandler("setFocusedItem");
|
|
426
|
+
tree.do.selectItem("x112");
|
|
427
|
+
tree.do.hotkey("focusNextItem");
|
|
428
|
+
expect(setFocusedItem).toHaveBeenCalledWith("x113");
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
it("focuses previous item", () => {
|
|
432
|
+
const setFocusedItem = tree.mockedHandler("setFocusedItem");
|
|
433
|
+
tree.do.selectItem("x112");
|
|
434
|
+
tree.do.hotkey("focusPreviousItem");
|
|
435
|
+
expect(setFocusedItem).toHaveBeenCalledWith("x111");
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
it("runs expandOrDown for expandable folder", () => {
|
|
439
|
+
const setExpandedItems = tree.mockedHandler("setExpandedItems");
|
|
440
|
+
const setFocusedItem = tree.mockedHandler("setFocusedItem");
|
|
441
|
+
tree.do.ctrlSelectItem("x2");
|
|
442
|
+
setFocusedItem.mockClear();
|
|
443
|
+
tree.do.hotkey("expandOrDown");
|
|
444
|
+
expect(setExpandedItems).toHaveBeenCalledWith(["x1", "x11", "x2"]);
|
|
445
|
+
expect(setFocusedItem).not.toBeCalled();
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
it("runs expandOrDown for expanded folder", () => {
|
|
449
|
+
const setExpandedItems = tree.mockedHandler("setExpandedItems");
|
|
450
|
+
const setFocusedItem = tree.mockedHandler("setFocusedItem");
|
|
451
|
+
tree.do.ctrlSelectItem("x1");
|
|
452
|
+
setFocusedItem.mockClear();
|
|
453
|
+
tree.do.hotkey("expandOrDown");
|
|
454
|
+
expect(setExpandedItems).not.toBeCalled();
|
|
455
|
+
expect(setFocusedItem).toHaveBeenCalledWith("x11");
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
it("runs expandOrDown for non-folder item", () => {
|
|
459
|
+
const setExpandedItems = tree.mockedHandler("setExpandedItems");
|
|
460
|
+
const setFocusedItem = tree.mockedHandler("setFocusedItem");
|
|
461
|
+
tree.do.selectItem("x111");
|
|
462
|
+
setFocusedItem.mockClear();
|
|
463
|
+
tree.do.hotkey("expandOrDown");
|
|
464
|
+
expect(setExpandedItems).not.toBeCalled();
|
|
465
|
+
expect(setFocusedItem).toHaveBeenCalledWith("x112");
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
it("runs collapseOrUp for expanded folder", () => {
|
|
469
|
+
const setExpandedItems = tree.mockedHandler("setExpandedItems");
|
|
470
|
+
const setFocusedItem = tree.mockedHandler("setFocusedItem");
|
|
471
|
+
tree.do.ctrlSelectItem("x1");
|
|
472
|
+
setFocusedItem.mockClear();
|
|
473
|
+
tree.do.hotkey("collapseOrUp");
|
|
474
|
+
expect(setExpandedItems).toHaveBeenCalledWith(["x11"]);
|
|
475
|
+
expect(setFocusedItem).not.toBeCalled();
|
|
476
|
+
});
|
|
477
|
+
|
|
478
|
+
it("runs collapseOrUp for collapsed folder", () => {
|
|
479
|
+
const setExpandedItems = tree.mockedHandler("setExpandedItems");
|
|
480
|
+
const setFocusedItem = tree.mockedHandler("setFocusedItem");
|
|
481
|
+
tree.do.ctrlSelectItem("x2");
|
|
482
|
+
setFocusedItem.mockClear();
|
|
483
|
+
tree.do.hotkey("collapseOrUp");
|
|
484
|
+
expect(setExpandedItems).toBeCalledWith(["x1", "x11"]);
|
|
485
|
+
expect(setFocusedItem).not.toBeCalled();
|
|
486
|
+
});
|
|
487
|
+
|
|
488
|
+
it("runs collapseOrUp for non-folder item", () => {
|
|
489
|
+
const setExpandedItems = tree.mockedHandler("setExpandedItems");
|
|
490
|
+
const setFocusedItem = tree.mockedHandler("setFocusedItem");
|
|
491
|
+
tree.do.ctrlSelectItem("x112");
|
|
492
|
+
setFocusedItem.mockClear();
|
|
493
|
+
tree.do.hotkey("collapseOrUp");
|
|
494
|
+
expect(setExpandedItems).not.toBeCalled();
|
|
495
|
+
expect(setFocusedItem).toHaveBeenCalledWith("x11");
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
it("runs focusFirstItem", () => {
|
|
499
|
+
const setFocusedItem = tree.mockedHandler("setFocusedItem");
|
|
500
|
+
tree.do.selectItem("x2");
|
|
501
|
+
setFocusedItem.mockClear();
|
|
502
|
+
tree.do.hotkey("focusFirstItem");
|
|
503
|
+
expect(setFocusedItem).toHaveBeenCalledWith("x1");
|
|
504
|
+
});
|
|
505
|
+
|
|
506
|
+
it("runs focusLastItem", () => {
|
|
507
|
+
const setFocusedItem = tree.mockedHandler("setFocusedItem");
|
|
508
|
+
tree.do.selectItem("x2");
|
|
509
|
+
setFocusedItem.mockClear();
|
|
510
|
+
tree.do.hotkey("focusLastItem");
|
|
511
|
+
expect(setFocusedItem).toHaveBeenCalledWith("x4");
|
|
512
|
+
});
|
|
513
|
+
});
|
|
514
|
+
});
|
|
515
|
+
});
|
|
@@ -50,19 +50,21 @@ export type TreeFeatureDef<T> = {
|
|
|
50
50
|
getProps: () => Record<string, any>;
|
|
51
51
|
getItemName: () => string;
|
|
52
52
|
getItemData: () => T;
|
|
53
|
+
equals: (other?: ItemInstance<any> | null) => boolean;
|
|
53
54
|
expand: () => void;
|
|
54
55
|
collapse: () => void;
|
|
55
56
|
isExpanded: () => boolean;
|
|
57
|
+
isDescendentOf: (parentId: string) => boolean;
|
|
56
58
|
isFocused: () => boolean;
|
|
57
59
|
isFolder: () => boolean;
|
|
58
60
|
setFocused: () => void;
|
|
59
|
-
getParent: () => ItemInstance<T
|
|
61
|
+
getParent: () => ItemInstance<T> | undefined;
|
|
60
62
|
getChildren: () => ItemInstance<T>[];
|
|
61
63
|
getIndexInParent: () => number;
|
|
62
64
|
primaryAction: () => void;
|
|
63
65
|
getTree: () => TreeInstance<T>;
|
|
64
|
-
getItemAbove: () => ItemInstance<T> |
|
|
65
|
-
getItemBelow: () => ItemInstance<T> |
|
|
66
|
+
getItemAbove: () => ItemInstance<T> | undefined;
|
|
67
|
+
getItemBelow: () => ItemInstance<T> | undefined;
|
|
66
68
|
getMemoizedProp: <X>(name: string, create: () => X, deps?: any[]) => X;
|
|
67
69
|
scrollTo: (
|
|
68
70
|
scrollIntoViewArg?: boolean | ScrollIntoViewOptions,
|
package/src/index.ts
CHANGED
|
@@ -2,7 +2,7 @@ export * from "./types/core";
|
|
|
2
2
|
export * from "./core/create-tree";
|
|
3
3
|
|
|
4
4
|
export * from "./features/tree/types";
|
|
5
|
-
export
|
|
5
|
+
export { MainFeatureDef, InstanceBuilder } from "./features/main/types";
|
|
6
6
|
export * from "./features/drag-and-drop/types";
|
|
7
7
|
export * from "./features/selection/types";
|
|
8
8
|
export * from "./features/async-data-loader/types";
|
|
@@ -24,3 +24,6 @@ export * from "./features/expand-all/feature";
|
|
|
24
24
|
export * from "./utilities/create-on-drop-handler";
|
|
25
25
|
export * from "./utilities/insert-items-at-target";
|
|
26
26
|
export * from "./utilities/remove-items-from-parents";
|
|
27
|
+
|
|
28
|
+
export * from "./core/build-proxified-instance";
|
|
29
|
+
export * from "./core/build-static-instance";
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/* eslint-disable import/no-extraneous-dependencies */
|
|
2
|
+
import { DragEvent } from "react";
|
|
3
|
+
import { Mock, expect, vi } from "vitest";
|
|
4
|
+
import { TestTree } from "./test-tree";
|
|
5
|
+
import { HotkeyName } from "../types/core";
|
|
6
|
+
import { HotkeyConfig } from "../features/hotkeys-core/types";
|
|
7
|
+
|
|
8
|
+
export class TestTreeDo<T> {
|
|
9
|
+
protected itemInstance(itemId: string) {
|
|
10
|
+
return this.tree.instance.getItemInstance(itemId);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
protected itemProps(itemId: string) {
|
|
14
|
+
return this.itemInstance(itemId).getProps();
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
constructor(protected tree: TestTree<T>) {}
|
|
18
|
+
|
|
19
|
+
selectItem(id: string) {
|
|
20
|
+
this.itemProps(id).onClick({});
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
shiftSelectItem(id: string) {
|
|
24
|
+
this.itemProps(id).onClick({ shiftKey: true });
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
ctrlSelectItem(id: string) {
|
|
28
|
+
this.itemProps(id).onClick({ ctrlKey: true });
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
ctrlShiftSelectItem(id: string) {
|
|
32
|
+
this.itemProps(id).onClick({ shiftKey: true, ctrlKey: true });
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
selectMultiple(...ids: string[]) {
|
|
36
|
+
ids.forEach((id) => this.ctrlSelectItem(id));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
hotkey(hotkey: HotkeyName, e: Partial<KeyboardEvent> = {}) {
|
|
40
|
+
const hotkeyConfig: HotkeyConfig<any> = {
|
|
41
|
+
...this.tree.instance.getHotkeyPresets()[hotkey],
|
|
42
|
+
...this.tree.instance.getConfig().hotkeys?.[hotkey],
|
|
43
|
+
};
|
|
44
|
+
if (
|
|
45
|
+
hotkeyConfig.isEnabled &&
|
|
46
|
+
!hotkeyConfig.isEnabled?.(this.tree.instance)
|
|
47
|
+
) {
|
|
48
|
+
throw new Error(`Hotkey "${hotkey}" is disabled`);
|
|
49
|
+
}
|
|
50
|
+
if (!hotkeyConfig.handler) {
|
|
51
|
+
throw new Error(`Hotkey "${hotkey}" has no handler`);
|
|
52
|
+
}
|
|
53
|
+
hotkeyConfig.handler(
|
|
54
|
+
{
|
|
55
|
+
...e,
|
|
56
|
+
stopPropagation: () => {},
|
|
57
|
+
preventDefault: () => {},
|
|
58
|
+
} as any,
|
|
59
|
+
this.tree.instance,
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
startDrag(itemId: string, event?: DragEvent) {
|
|
64
|
+
if (!this.itemProps(itemId).draggable) {
|
|
65
|
+
throw new Error(
|
|
66
|
+
`Can't drag item ${itemId}, has attribute draggable=false`,
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const e = event ?? TestTree.dragEvent();
|
|
71
|
+
this.itemProps(itemId).onDragStart(e);
|
|
72
|
+
return e;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
dragOver(itemId: string, event?: DragEvent) {
|
|
76
|
+
const e = event ?? TestTree.dragEvent();
|
|
77
|
+
(e.preventDefault as Mock).mockClear();
|
|
78
|
+
this.itemProps(itemId).onDragOver(e);
|
|
79
|
+
this.itemProps(itemId).onDragOver(e);
|
|
80
|
+
this.itemProps(itemId).onDragOver(e);
|
|
81
|
+
expect(e.preventDefault).toBeCalledTimes(3);
|
|
82
|
+
|
|
83
|
+
this.consistentCalls(e.preventDefault);
|
|
84
|
+
this.consistentCalls(e.stopPropagation);
|
|
85
|
+
return e;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
dragOverNotAllowed(itemId: string, event?: DragEvent) {
|
|
89
|
+
const e = event ?? TestTree.dragEvent();
|
|
90
|
+
(e.preventDefault as Mock).mockClear();
|
|
91
|
+
this.itemProps(itemId).onDragOver(e);
|
|
92
|
+
this.itemProps(itemId).onDragOver(e);
|
|
93
|
+
this.itemProps(itemId).onDragOver(e);
|
|
94
|
+
expect(e.preventDefault).toBeCalledTimes(0);
|
|
95
|
+
|
|
96
|
+
this.consistentCalls(e.preventDefault);
|
|
97
|
+
this.consistentCalls(e.stopPropagation);
|
|
98
|
+
return e;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
dragLeave(itemId: string) {
|
|
102
|
+
this.itemProps(itemId).onDragLeave({});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
dragEnd(itemId: string, event?: DragEvent) {
|
|
106
|
+
const e = event ?? TestTree.dragEvent();
|
|
107
|
+
this.itemProps(itemId).onDragEnd(e);
|
|
108
|
+
return e;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
drop(itemId: string, event?: DragEvent) {
|
|
112
|
+
const e = event ?? TestTree.dragEvent();
|
|
113
|
+
this.itemProps(itemId).onDrop(e);
|
|
114
|
+
return e;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
dragOverAndDrop(itemId: string, event?: DragEvent) {
|
|
118
|
+
const e = event ?? TestTree.dragEvent();
|
|
119
|
+
this.dragOver(itemId, e);
|
|
120
|
+
return this.drop(itemId, e);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
private consistentCalls(fn: any) {
|
|
124
|
+
if (!vi.isMockFunction(fn)) {
|
|
125
|
+
throw new Error("fn is not a mock");
|
|
126
|
+
}
|
|
127
|
+
expect(
|
|
128
|
+
fn.mock.calls.length,
|
|
129
|
+
"function called inconsistent times",
|
|
130
|
+
).toBeOneOf([0, 3]);
|
|
131
|
+
expect(
|
|
132
|
+
new Set(fn.mock.calls.map((call) => call.join("__"))).size,
|
|
133
|
+
"function called with inconsistent parameters",
|
|
134
|
+
).toBeOneOf([0, 1]);
|
|
135
|
+
}
|
|
136
|
+
}
|