@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.
Files changed (114) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/lib/cjs/core/build-proxified-instance.d.ts +2 -0
  3. package/lib/cjs/core/build-proxified-instance.js +58 -0
  4. package/lib/cjs/core/build-static-instance.d.ts +2 -0
  5. package/lib/cjs/core/build-static-instance.js +27 -0
  6. package/lib/cjs/core/create-tree.js +55 -36
  7. package/lib/cjs/features/async-data-loader/feature.js +37 -23
  8. package/lib/cjs/features/async-data-loader/types.d.ts +2 -1
  9. package/lib/cjs/features/drag-and-drop/feature.js +64 -32
  10. package/lib/cjs/features/drag-and-drop/types.d.ts +13 -4
  11. package/lib/cjs/features/drag-and-drop/utils.d.ts +1 -2
  12. package/lib/cjs/features/drag-and-drop/utils.js +140 -37
  13. package/lib/cjs/features/expand-all/feature.js +12 -6
  14. package/lib/cjs/features/main/types.d.ts +8 -2
  15. package/lib/cjs/features/renaming/feature.js +33 -18
  16. package/lib/cjs/features/renaming/types.d.ts +1 -1
  17. package/lib/cjs/features/search/feature.js +38 -24
  18. package/lib/cjs/features/search/types.d.ts +0 -1
  19. package/lib/cjs/features/selection/feature.js +23 -14
  20. package/lib/cjs/features/sync-data-loader/feature.js +7 -2
  21. package/lib/cjs/features/tree/feature.d.ts +2 -1
  22. package/lib/cjs/features/tree/feature.js +85 -63
  23. package/lib/cjs/features/tree/types.d.ts +5 -3
  24. package/lib/cjs/index.d.ts +3 -1
  25. package/lib/cjs/index.js +2 -1
  26. package/lib/cjs/test-utils/test-tree-do.d.ts +23 -0
  27. package/lib/cjs/test-utils/test-tree-do.js +99 -0
  28. package/lib/cjs/test-utils/test-tree-expect.d.ts +15 -0
  29. package/lib/cjs/test-utils/test-tree-expect.js +62 -0
  30. package/lib/cjs/test-utils/test-tree.d.ts +47 -0
  31. package/lib/cjs/test-utils/test-tree.js +195 -0
  32. package/lib/cjs/types/core.d.ts +31 -15
  33. package/lib/cjs/utilities/errors.d.ts +1 -0
  34. package/lib/cjs/utilities/errors.js +5 -0
  35. package/lib/cjs/utilities/insert-items-at-target.js +10 -3
  36. package/lib/cjs/utilities/remove-items-from-parents.js +14 -8
  37. package/lib/cjs/utils.d.ts +3 -3
  38. package/lib/cjs/utils.js +6 -6
  39. package/lib/esm/core/build-proxified-instance.d.ts +2 -0
  40. package/lib/esm/core/build-proxified-instance.js +54 -0
  41. package/lib/esm/core/build-static-instance.d.ts +2 -0
  42. package/lib/esm/core/build-static-instance.js +23 -0
  43. package/lib/esm/core/create-tree.js +55 -36
  44. package/lib/esm/features/async-data-loader/feature.js +37 -23
  45. package/lib/esm/features/async-data-loader/types.d.ts +2 -1
  46. package/lib/esm/features/drag-and-drop/feature.js +64 -32
  47. package/lib/esm/features/drag-and-drop/types.d.ts +13 -4
  48. package/lib/esm/features/drag-and-drop/utils.d.ts +1 -2
  49. package/lib/esm/features/drag-and-drop/utils.js +138 -34
  50. package/lib/esm/features/expand-all/feature.js +12 -6
  51. package/lib/esm/features/main/types.d.ts +8 -2
  52. package/lib/esm/features/renaming/feature.js +33 -18
  53. package/lib/esm/features/renaming/types.d.ts +1 -1
  54. package/lib/esm/features/search/feature.js +38 -24
  55. package/lib/esm/features/search/types.d.ts +0 -1
  56. package/lib/esm/features/selection/feature.js +23 -14
  57. package/lib/esm/features/sync-data-loader/feature.js +7 -2
  58. package/lib/esm/features/tree/feature.d.ts +2 -1
  59. package/lib/esm/features/tree/feature.js +86 -64
  60. package/lib/esm/features/tree/types.d.ts +5 -3
  61. package/lib/esm/index.d.ts +3 -1
  62. package/lib/esm/index.js +2 -1
  63. package/lib/esm/test-utils/test-tree-do.d.ts +23 -0
  64. package/lib/esm/test-utils/test-tree-do.js +95 -0
  65. package/lib/esm/test-utils/test-tree-expect.d.ts +15 -0
  66. package/lib/esm/test-utils/test-tree-expect.js +58 -0
  67. package/lib/esm/test-utils/test-tree.d.ts +47 -0
  68. package/lib/esm/test-utils/test-tree.js +191 -0
  69. package/lib/esm/types/core.d.ts +31 -15
  70. package/lib/esm/utilities/errors.d.ts +1 -0
  71. package/lib/esm/utilities/errors.js +1 -0
  72. package/lib/esm/utilities/insert-items-at-target.js +10 -3
  73. package/lib/esm/utilities/remove-items-from-parents.js +14 -8
  74. package/lib/esm/utils.d.ts +3 -3
  75. package/lib/esm/utils.js +3 -3
  76. package/package.json +7 -3
  77. package/src/core/build-proxified-instance.ts +115 -0
  78. package/src/core/build-static-instance.ts +28 -0
  79. package/src/core/create-tree.ts +60 -62
  80. package/src/features/async-data-loader/async-data-loader.spec.ts +143 -0
  81. package/src/features/async-data-loader/feature.ts +33 -31
  82. package/src/features/async-data-loader/types.ts +3 -1
  83. package/src/features/drag-and-drop/drag-and-drop.spec.ts +716 -0
  84. package/src/features/drag-and-drop/feature.ts +109 -85
  85. package/src/features/drag-and-drop/types.ts +21 -7
  86. package/src/features/drag-and-drop/utils.ts +196 -55
  87. package/src/features/expand-all/expand-all.spec.ts +52 -0
  88. package/src/features/expand-all/feature.ts +8 -12
  89. package/src/features/hotkeys-core/feature.ts +1 -1
  90. package/src/features/main/types.ts +14 -1
  91. package/src/features/renaming/feature.ts +30 -29
  92. package/src/features/renaming/renaming.spec.ts +125 -0
  93. package/src/features/renaming/types.ts +1 -1
  94. package/src/features/search/feature.ts +34 -38
  95. package/src/features/search/search.spec.ts +115 -0
  96. package/src/features/search/types.ts +0 -1
  97. package/src/features/selection/feature.ts +29 -30
  98. package/src/features/selection/selection.spec.ts +220 -0
  99. package/src/features/sync-data-loader/feature.ts +8 -11
  100. package/src/features/tree/feature.ts +82 -87
  101. package/src/features/tree/tree.spec.ts +515 -0
  102. package/src/features/tree/types.ts +5 -3
  103. package/src/index.ts +4 -1
  104. package/src/test-utils/test-tree-do.ts +136 -0
  105. package/src/test-utils/test-tree-expect.ts +86 -0
  106. package/src/test-utils/test-tree.ts +217 -0
  107. package/src/types/core.ts +92 -33
  108. package/src/utilities/errors.ts +2 -0
  109. package/src/utilities/insert-items-at-target.ts +10 -3
  110. package/src/utilities/remove-items-from-parents.ts +15 -10
  111. package/src/utils.spec.ts +89 -0
  112. package/src/utils.ts +6 -6
  113. package/tsconfig.json +1 -0
  114. package/vitest.config.ts +6 -0
@@ -25,23 +25,19 @@ export const selectionFeature: FeatureImplementation<
25
25
  selectedItems: "setSelectedItems",
26
26
  },
27
27
 
28
- createTreeInstance: (prev, instance) => ({
29
- ...prev,
30
-
31
- setSelectedItems: (selectedItems) => {
32
- instance.applySubStateUpdate("selectedItems", selectedItems);
28
+ treeInstance: {
29
+ setSelectedItems: ({ tree }, selectedItems) => {
30
+ tree.applySubStateUpdate("selectedItems", selectedItems);
33
31
  },
34
32
 
35
33
  // TODO memo
36
- getSelectedItems: () => {
37
- return instance.getState().selectedItems.map(instance.getItemInstance);
34
+ getSelectedItems: ({ tree }) => {
35
+ return tree.getState().selectedItems.map(tree.getItemInstance);
38
36
  },
39
- }),
40
-
41
- createItemInstance: (prev, item, tree) => ({
42
- ...prev,
37
+ },
43
38
 
44
- select: () => {
39
+ itemInstance: {
40
+ select: ({ tree, item }) => {
45
41
  const { selectedItems } = tree.getState();
46
42
  tree.setSelectedItems(
47
43
  selectedItems.includes(item.getItemMeta().itemId)
@@ -50,19 +46,19 @@ export const selectionFeature: FeatureImplementation<
50
46
  );
51
47
  },
52
48
 
53
- deselect: () => {
49
+ deselect: ({ tree, item }) => {
54
50
  const { selectedItems } = tree.getState();
55
51
  tree.setSelectedItems(
56
52
  selectedItems.filter((id) => id !== item.getItemMeta().itemId),
57
53
  );
58
54
  },
59
55
 
60
- isSelected: () => {
56
+ isSelected: ({ tree, item }) => {
61
57
  const { selectedItems } = tree.getState();
62
58
  return selectedItems.includes(item.getItemMeta().itemId);
63
59
  },
64
60
 
65
- selectUpTo: (ctrl: boolean) => {
61
+ selectUpTo: ({ tree, item }, ctrl: boolean) => {
66
62
  const indexA = item.getItemMeta().index;
67
63
  // TODO dont use focused item as anchor, but last primary-clicked item
68
64
  const indexB = tree.getFocusedItem().getItemMeta().index;
@@ -84,7 +80,7 @@ export const selectionFeature: FeatureImplementation<
84
80
  tree.setSelectedItems(uniqueSelectedItems);
85
81
  },
86
82
 
87
- toggleSelect: () => {
83
+ toggleSelect: ({ item }) => {
88
84
  if (item.isSelected()) {
89
85
  item.deselect();
90
86
  } else {
@@ -92,22 +88,25 @@ export const selectionFeature: FeatureImplementation<
92
88
  }
93
89
  },
94
90
 
95
- getProps: () => ({
96
- ...prev.getProps(),
91
+ getProps: ({ tree, item, prev }) => ({
92
+ ...prev?.(),
97
93
  "aria-selected": item.isSelected() ? "true" : "false",
98
- onClick: item.getMemoizedProp("selection/onClick", () => (e) => {
99
- if (e.shiftKey) {
100
- item.selectUpTo(e.ctrlKey || e.metaKey);
101
- } else if (e.ctrlKey || e.metaKey) {
102
- item.toggleSelect();
103
- } else {
104
- tree.setSelectedItems([item.getItemMeta().itemId]);
105
- }
106
-
107
- prev.getProps().onClick?.(e);
108
- }),
94
+ onClick: item.getMemoizedProp(
95
+ "selection/onClick",
96
+ () => (e: MouseEvent) => {
97
+ if (e.shiftKey) {
98
+ item.selectUpTo(e.ctrlKey || e.metaKey);
99
+ } else if (e.ctrlKey || e.metaKey) {
100
+ item.toggleSelect();
101
+ } else {
102
+ tree.setSelectedItems([item.getItemMeta().itemId]);
103
+ }
104
+
105
+ prev?.()?.onClick?.(e);
106
+ },
107
+ ),
109
108
  }),
110
- }),
109
+ },
111
110
 
112
111
  hotkeys: {
113
112
  // setSelectedItem: {
@@ -0,0 +1,220 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { TestTree } from "../../test-utils/test-tree";
3
+ import { selectionFeature } from "./feature";
4
+
5
+ const factory = TestTree.default({}).withFeatures(selectionFeature);
6
+
7
+ // it("test", async () => {
8
+ // const tree = await factory.suits.async().tree.createDebugTree();
9
+ // const setSelectedItems = tree.mockedHandler("setSelectedItems");
10
+ // const setFocusedItem = tree.mockedHandler("setFocusedItem");
11
+ //
12
+ // tree.do.selectItem("x111");
13
+ // tree.do.ctrlSelectItem("x112");
14
+ // tree.do.ctrlSelectItem("x113");
15
+ //
16
+ // expect(setSelectedItems).toHaveBeenCalledWith(["x111", "x112", "x113"]);
17
+ // expect(setFocusedItem).toHaveBeenCalledWith("x113");
18
+ // });
19
+
20
+ describe("core-feature/selections", () => {
21
+ factory.forSuits((tree) => {
22
+ it("sets aria-selected to false", () => {
23
+ expect(
24
+ tree.instance.getItemInstance("x111").getProps()["aria-selected"],
25
+ ).toBe("false");
26
+ });
27
+
28
+ it("sets aria-selected to true", () => {
29
+ tree.do.selectItem("x111");
30
+ expect(
31
+ tree.instance.getItemInstance("x111").getProps()["aria-selected"],
32
+ ).toBe("true");
33
+ });
34
+
35
+ it("resets old selection after new select", () => {
36
+ tree.do.selectItem("x111");
37
+ tree.do.selectItem("x112");
38
+ expect(
39
+ tree.instance.getItemInstance("x111").getProps()["aria-selected"],
40
+ ).toBe("false");
41
+ expect(
42
+ tree.instance.getItemInstance("x112").getProps()["aria-selected"],
43
+ ).toBe("true");
44
+ expect(tree.instance.getItemInstance("x111").isSelected()).toBe(false);
45
+ expect(tree.instance.getItemInstance("x112").isSelected()).toBe(true);
46
+ });
47
+
48
+ describe("select and control select", () => {
49
+ it("should make isSelected true", () => {
50
+ tree.do.selectItem("x111");
51
+ tree.do.ctrlSelectItem("x112");
52
+ tree.do.ctrlSelectItem("x113");
53
+
54
+ expect(tree.instance.getItemInstance("x111").isSelected()).toBe(true);
55
+ expect(tree.instance.getItemInstance("x112").isSelected()).toBe(true);
56
+ expect(tree.instance.getItemInstance("x113").isSelected()).toBe(true);
57
+ });
58
+
59
+ it("should call individual state setters", () => {
60
+ const setSelectedItems = tree.mockedHandler("setSelectedItems");
61
+ const setFocusedItem = tree.mockedHandler("setFocusedItem");
62
+
63
+ tree.do.selectItem("x111");
64
+ tree.do.ctrlSelectItem("x112");
65
+ tree.do.ctrlSelectItem("x113");
66
+
67
+ expect(setSelectedItems).toHaveBeenCalledWith(["x111", "x112", "x113"]);
68
+ expect(setFocusedItem).toHaveBeenCalledWith("x113");
69
+ });
70
+
71
+ it("should call joint state setter", () => {
72
+ const setState = tree.mockedHandler("setState");
73
+
74
+ tree.do.selectItem("x111");
75
+ tree.do.ctrlSelectItem("x112");
76
+ tree.do.ctrlSelectItem("x113");
77
+
78
+ expect(setState).toHaveBeenCalledWith(
79
+ expect.objectContaining({
80
+ selectedItems: ["x111", "x112", "x113"],
81
+ focusedItem: "x113",
82
+ }),
83
+ );
84
+ });
85
+ });
86
+
87
+ describe("shift select", () => {
88
+ it("should make isSelected true", () => {
89
+ tree.do.selectItem("x111");
90
+ tree.do.shiftSelectItem("x113");
91
+ tree.do.ctrlSelectItem("x2");
92
+
93
+ expect(tree.instance.getItemInstance("x111").isSelected()).toBe(true);
94
+ expect(tree.instance.getItemInstance("x112").isSelected()).toBe(true);
95
+ expect(tree.instance.getItemInstance("x113").isSelected()).toBe(true);
96
+ expect(tree.instance.getItemInstance("x2").isSelected()).toBe(true);
97
+ });
98
+
99
+ it("should call individual state setters", () => {
100
+ const setSelectedItems = tree.mockedHandler("setSelectedItems");
101
+ const setFocusedItem = tree.mockedHandler("setFocusedItem");
102
+
103
+ tree.do.selectItem("x111");
104
+ tree.do.shiftSelectItem("x113");
105
+ expect(setFocusedItem).toHaveBeenCalledWith("x113");
106
+
107
+ tree.do.ctrlSelectItem("x2");
108
+
109
+ expect(setSelectedItems).toHaveBeenCalledWith([
110
+ "x111",
111
+ "x112",
112
+ "x113",
113
+ "x2",
114
+ ]);
115
+
116
+ expect(setFocusedItem).toHaveBeenCalledWith("x2");
117
+ });
118
+
119
+ it("should call joint state setter", () => {
120
+ const setState = tree.mockedHandler("setState");
121
+
122
+ tree.do.selectItem("x111");
123
+ tree.do.shiftSelectItem("x113");
124
+ expect(setState).toHaveBeenCalledWith(
125
+ expect.objectContaining({
126
+ selectedItems: ["x111", "x112", "x113"],
127
+ focusedItem: "x113",
128
+ }),
129
+ );
130
+
131
+ tree.do.ctrlSelectItem("x2");
132
+
133
+ expect(setState).toHaveBeenCalledWith(
134
+ expect.objectContaining({
135
+ selectedItems: ["x111", "x112", "x113", "x2"],
136
+ focusedItem: "x2",
137
+ }),
138
+ );
139
+ });
140
+ });
141
+
142
+ describe("programmatic select", () => {
143
+ describe("item instance actions", () => {
144
+ it("should handle select", () => {
145
+ const setSelectedItems = tree.mockedHandler("setSelectedItems");
146
+ tree.instance.getItemInstance("x111").select();
147
+ expect(setSelectedItems).toHaveBeenCalledWith(["x111"]);
148
+ expect(tree.instance.getItemInstance("x111").isSelected()).toBe(true);
149
+ });
150
+
151
+ it("should handle deselect", () => {
152
+ const setSelectedItems = tree.mockedHandler("setSelectedItems");
153
+ tree.instance.getItemInstance("x111").select();
154
+ tree.instance.getItemInstance("x111").deselect();
155
+ expect(setSelectedItems).toHaveBeenCalledWith([]);
156
+ expect(tree.instance.getItemInstance("x111").isSelected()).toBe(
157
+ false,
158
+ );
159
+ });
160
+
161
+ it("should handle toggle select on", () => {
162
+ const setSelectedItems = tree.mockedHandler("setSelectedItems");
163
+ tree.instance.getItemInstance("x111").toggleSelect();
164
+ expect(setSelectedItems).toHaveBeenCalledWith(["x111"]);
165
+ expect(tree.instance.getItemInstance("x111").isSelected()).toBe(true);
166
+ });
167
+
168
+ it("should handle toggle select off", () => {
169
+ const setSelectedItems = tree.mockedHandler("setSelectedItems");
170
+ tree.instance.getItemInstance("x111").select();
171
+ tree.instance.getItemInstance("x111").toggleSelect();
172
+ expect(setSelectedItems).toHaveBeenCalledWith([]);
173
+ expect(tree.instance.getItemInstance("x111").isSelected()).toBe(
174
+ false,
175
+ );
176
+ });
177
+
178
+ it("should handle selectUpTo without ctrl", () => {
179
+ const setSelectedItems = tree.mockedHandler("setSelectedItems");
180
+ tree.instance.getItemInstance("x111").toggleSelect();
181
+ tree.instance.getItemInstance("x112").toggleSelect();
182
+ tree.instance.getItemInstance("x112").setFocused();
183
+ tree.instance.getItemInstance("x114").selectUpTo(false);
184
+ expect(setSelectedItems).toHaveBeenCalledWith([
185
+ "x112",
186
+ "x113",
187
+ "x114",
188
+ ]);
189
+ expect(tree.instance.getItemInstance("x112").isSelected()).toBe(true);
190
+ });
191
+
192
+ it("should handle selectUpTo with ctrl", () => {
193
+ const setSelectedItems = tree.mockedHandler("setSelectedItems");
194
+ tree.instance.getItemInstance("x111").toggleSelect();
195
+ tree.instance.getItemInstance("x112").toggleSelect();
196
+ tree.instance.getItemInstance("x112").setFocused();
197
+ tree.instance.getItemInstance("x114").selectUpTo(true);
198
+ expect(setSelectedItems).toHaveBeenCalledWith([
199
+ "x111",
200
+ "x112",
201
+ "x113",
202
+ "x114",
203
+ ]);
204
+ expect(tree.instance.getItemInstance("x112").isSelected()).toBe(true);
205
+ });
206
+ });
207
+
208
+ it("should handle getters", () => {
209
+ const setSelectedItems = tree.mockedHandler("setSelectedItems");
210
+ tree.instance.setSelectedItems(["x111", "x112"]);
211
+ expect(setSelectedItems).toHaveBeenCalledWith(["x111", "x112"]);
212
+ expect(tree.instance.getItemInstance("x111").isSelected()).toBe(true);
213
+ expect(tree.instance.getItemInstance("x112").isSelected()).toBe(true);
214
+ expect(tree.instance.getSelectedItems()[0].getId()).toEqual("x111");
215
+ expect(tree.instance.getSelectedItems()[1].getId()).toEqual("x112");
216
+ expect(tree.instance.getSelectedItems().length).toBe(2);
217
+ });
218
+ });
219
+ });
220
+ });
@@ -24,18 +24,15 @@ export const syncDataLoaderFeature: FeatureImplementation<
24
24
  loadingItems: "setLoadingItems",
25
25
  },
26
26
 
27
- createTreeInstance: (prev, instance) => ({
28
- ...prev,
27
+ treeInstance: {
28
+ retrieveItemData: ({ tree }, itemId) =>
29
+ tree.getConfig().dataLoader!.getItem(itemId),
29
30
 
30
- retrieveItemData: (itemId) =>
31
- instance.getConfig().dataLoader!.getItem(itemId),
32
-
33
- retrieveChildrenIds: (itemId) =>
34
- instance.getConfig().dataLoader!.getChildren(itemId),
35
- }),
31
+ retrieveChildrenIds: ({ tree }, itemId) =>
32
+ tree.getConfig().dataLoader!.getChildren(itemId),
33
+ },
36
34
 
37
- createItemInstance: (prev) => ({
38
- ...prev,
35
+ itemInstance: {
39
36
  isLoading: () => false,
40
- }),
37
+ },
41
38
  };
@@ -1,9 +1,10 @@
1
1
  import { FeatureImplementation, ItemInstance } from "../../types/core";
2
2
  import { ItemMeta, TreeFeatureDef, TreeItemDataRef } from "./types";
3
- import { makeStateUpdater, memo, poll } from "../../utils";
3
+ import { makeStateUpdater, poll } from "../../utils";
4
4
  import { MainFeatureDef } from "../main/types";
5
5
  import { HotkeysCoreFeatureDef } from "../hotkeys-core/types";
6
6
  import { SyncDataLoaderFeatureDef } from "../sync-data-loader/types";
7
+ import { SearchFeatureDef } from "../search/types";
7
8
 
8
9
  export const treeFeature: FeatureImplementation<
9
10
  any,
@@ -12,6 +13,7 @@ export const treeFeature: FeatureImplementation<
12
13
  | TreeFeatureDef<any>
13
14
  | HotkeysCoreFeatureDef<any>
14
15
  | SyncDataLoaderFeatureDef<any>
16
+ | SearchFeatureDef<any>
15
17
  > = {
16
18
  key: "tree",
17
19
 
@@ -32,24 +34,15 @@ export const treeFeature: FeatureImplementation<
32
34
  focusedItem: "setFocusedItem",
33
35
  },
34
36
 
35
- createTreeInstance: (prev, instance) => ({
36
- ...prev,
37
+ treeInstance: {
38
+ isItemExpanded: ({ tree }, itemId) =>
39
+ tree.getState().expandedItems.includes(itemId),
37
40
 
38
- retrieveItemData: () => {
39
- throw new Error("No data-loader registered");
40
- },
41
-
42
- retrieveChildrenIds: () => {
43
- throw new Error("No data-loader registered");
44
- },
45
-
46
- isItemExpanded: (itemId) =>
47
- instance.getState().expandedItems.includes(itemId),
48
-
49
- getItemsMeta: () => {
50
- const { rootItemId } = instance.getConfig();
51
- const { expandedItems } = instance.getState();
41
+ getItemsMeta: ({ tree }) => {
42
+ const { rootItemId } = tree.getConfig();
43
+ const { expandedItems } = tree.getState();
52
44
  const flatItems: ItemMeta[] = [];
45
+ const expandedItemsSet = new Set(expandedItems);
53
46
 
54
47
  const recursiveAdd = (
55
48
  itemId: string,
@@ -67,8 +60,9 @@ export const treeFeature: FeatureImplementation<
67
60
  posInSet,
68
61
  });
69
62
 
70
- if (expandedItems.includes(itemId)) {
71
- const children = instance.retrieveChildrenIds(itemId) ?? [];
63
+ if (expandedItemsSet.has(itemId)) {
64
+ // TODO THIS MADE A HUGE DIFFERENCE!
65
+ const children = tree.retrieveChildrenIds(itemId) ?? [];
72
66
  let i = 0;
73
67
  for (const childId of children) {
74
68
  recursiveAdd(childId, itemId, level + 1, children.length, i++);
@@ -76,7 +70,7 @@ export const treeFeature: FeatureImplementation<
76
70
  }
77
71
  };
78
72
 
79
- const children = instance.retrieveChildrenIds(rootItemId);
73
+ const children = tree.retrieveChildrenIds(rootItemId);
80
74
  let i = 0;
81
75
  for (const itemId of children) {
82
76
  recursiveAdd(itemId, rootItemId, 0, children.length, i++);
@@ -85,62 +79,62 @@ export const treeFeature: FeatureImplementation<
85
79
  return flatItems;
86
80
  },
87
81
 
88
- expandItem: (itemId) => {
89
- if (!instance.getItemInstance(itemId).isFolder()) {
82
+ expandItem: ({ tree }, itemId) => {
83
+ if (!tree.getItemInstance(itemId).isFolder()) {
90
84
  return;
91
85
  }
92
86
 
93
- if (instance.getState().loadingItems?.includes(itemId)) {
87
+ if (tree.getState().loadingItems?.includes(itemId)) {
94
88
  return;
95
89
  }
96
90
 
97
- instance.applySubStateUpdate("expandedItems", (expandedItems) => [
91
+ tree.applySubStateUpdate("expandedItems", (expandedItems) => [
98
92
  ...expandedItems,
99
93
  itemId,
100
94
  ]);
101
- instance.rebuildTree();
95
+ tree.rebuildTree();
102
96
  },
103
97
 
104
- collapseItem: (itemId) => {
105
- if (!instance.getItemInstance(itemId).isFolder()) {
98
+ collapseItem: ({ tree }, itemId) => {
99
+ if (!tree.getItemInstance(itemId).isFolder()) {
106
100
  return;
107
101
  }
108
102
 
109
- instance.applySubStateUpdate("expandedItems", (expandedItems) =>
103
+ tree.applySubStateUpdate("expandedItems", (expandedItems) =>
110
104
  expandedItems.filter((id) => id !== itemId),
111
105
  );
112
- instance.rebuildTree();
106
+ tree.rebuildTree();
113
107
  },
114
108
 
115
109
  // TODO memo
116
- getFocusedItem: () => {
110
+ getFocusedItem: ({ tree }) => {
117
111
  return (
118
- instance.getItemInstance(instance.getState().focusedItem ?? "") ??
119
- instance.getItems()[0]
112
+ tree.getItemInstance(tree.getState().focusedItem ?? "") ??
113
+ tree.getItems()[0]
120
114
  );
121
115
  },
122
116
 
123
- focusItem: (itemId) => {
124
- instance.applySubStateUpdate("focusedItem", itemId);
117
+ focusItem: ({ tree }, itemId) => {
118
+ tree.applySubStateUpdate("focusedItem", itemId);
125
119
  },
126
120
 
127
- focusNextItem: () => {
128
- const { index } = instance.getFocusedItem().getItemMeta();
129
- const nextIndex = Math.min(index + 1, instance.getItems().length - 1);
130
- instance.focusItem(instance.getItems()[nextIndex].getId());
121
+ focusNextItem: ({ tree }) => {
122
+ const { index } = tree.getFocusedItem().getItemMeta();
123
+ const nextIndex = Math.min(index + 1, tree.getItems().length - 1);
124
+ tree.focusItem(tree.getItems()[nextIndex].getId());
131
125
  },
132
126
 
133
- focusPreviousItem: () => {
134
- const { index } = instance.getFocusedItem().getItemMeta();
127
+ focusPreviousItem: ({ tree }) => {
128
+ const { index } = tree.getFocusedItem().getItemMeta();
135
129
  const nextIndex = Math.max(index - 1, 0);
136
- instance.focusItem(instance.getItems()[nextIndex].getId());
130
+ tree.focusItem(tree.getItems()[nextIndex].getId());
137
131
  },
138
132
 
139
- updateDomFocus: () => {
133
+ updateDomFocus: ({ tree }) => {
140
134
  // Required because if the state is managed outside in react, the state only updated during next render
141
135
  setTimeout(async () => {
142
- const focusedItem = instance.getFocusedItem();
143
- instance.getConfig().scrollToItem?.(focusedItem);
136
+ const focusedItem = tree.getFocusedItem();
137
+ tree.getConfig().scrollToItem?.(focusedItem);
144
138
  await poll(() => focusedItem.getElement() !== null, 20);
145
139
  const focusedElement = focusedItem.getElement();
146
140
  if (!focusedElement) return;
@@ -148,29 +142,28 @@ export const treeFeature: FeatureImplementation<
148
142
  });
149
143
  },
150
144
 
151
- getContainerProps: () => ({
152
- ...prev.getContainerProps?.(),
145
+ getContainerProps: ({ prev }) => ({
146
+ ...prev?.(),
153
147
  role: "tree",
154
148
  ariaLabel: "",
155
149
  ariaActivedescendant: "",
156
150
  }),
157
- }),
158
151
 
159
- createItemInstance: (prev, item, tree) => ({
160
- ...prev,
161
- isLoading: () => {
162
- throw new Error("No data-loader registered");
163
- },
164
- scrollTo: async (scrollIntoViewArg) => {
152
+ // relevant for hotkeys of this feature
153
+ isSearchOpen: () => false,
154
+ },
155
+
156
+ itemInstance: {
157
+ scrollTo: async ({ tree, item }, scrollIntoViewArg) => {
165
158
  tree.getConfig().scrollToItem?.(item as any);
166
159
  await poll(() => item.getElement() !== null, 20);
167
- item.getElement()!.scrollIntoView(scrollIntoViewArg);
160
+ item.getElement()?.scrollIntoView(scrollIntoViewArg);
168
161
  },
169
- getId: () => item.getItemMeta().itemId,
170
- getProps: () => {
162
+ getId: ({ item }) => item.getItemMeta().itemId,
163
+ getProps: ({ item, prev }) => {
171
164
  const itemMeta = item.getItemMeta();
172
165
  return {
173
- ...prev.getProps?.(),
166
+ ...prev?.(),
174
167
  role: "treeitem",
175
168
  "aria-setsize": itemMeta.setSize,
176
169
  "aria-posinset": itemMeta.posInSet,
@@ -178,7 +171,7 @@ export const treeFeature: FeatureImplementation<
178
171
  "aria-label": item.getItemName(),
179
172
  "aria-level": itemMeta.level,
180
173
  tabIndex: item.isFocused() ? 0 : -1,
181
- onClick: item.getMemoizedProp("tree/onClick", () => (e) => {
174
+ onClick: item.getMemoizedProp("tree/onClick", () => (e: MouseEvent) => {
182
175
  item.setFocused();
183
176
  item.primaryAction();
184
177
 
@@ -198,46 +191,48 @@ export const treeFeature: FeatureImplementation<
198
191
  }),
199
192
  };
200
193
  },
201
- expand: () => tree.expandItem(item.getItemMeta().itemId),
202
- collapse: () => tree.collapseItem(item.getItemMeta().itemId),
203
- getItemData: () => tree.retrieveItemData(item.getItemMeta().itemId),
204
- isExpanded: () =>
194
+ expand: ({ tree, item }) => tree.expandItem(item.getItemMeta().itemId),
195
+ collapse: ({ tree, item }) => tree.collapseItem(item.getItemMeta().itemId),
196
+ getItemData: ({ tree, item }) =>
197
+ tree.retrieveItemData(item.getItemMeta().itemId),
198
+ equals: ({ item }, other) => item.getId() === other?.getId(),
199
+ isExpanded: ({ tree, item }) =>
205
200
  tree.getState().expandedItems.includes(item.getItemMeta().itemId),
206
- isFocused: () =>
201
+ isDescendentOf: ({ item }, parentId) => {
202
+ const parent = item.getParent();
203
+ return Boolean(
204
+ parent?.getId() === parentId || parent?.isDescendentOf(parentId),
205
+ );
206
+ },
207
+ isFocused: ({ tree, item }) =>
207
208
  tree.getState().focusedItem === item.getItemMeta().itemId ||
208
209
  (tree.getState().focusedItem === null && item.getItemMeta().index === 0),
209
- isFolder: () =>
210
+ isFolder: ({ tree, item }) =>
210
211
  item.getItemMeta().level === -1 ||
211
212
  tree.getConfig().isItemFolder(item as ItemInstance<any>),
212
- getItemName: () => {
213
+ getItemName: ({ tree, item }) => {
213
214
  const config = tree.getConfig();
214
215
  return config.getItemName(item as ItemInstance<any>);
215
216
  },
216
- setFocused: () => tree.focusItem(item.getItemMeta().itemId),
217
- primaryAction: () =>
217
+ setFocused: ({ tree, item }) => tree.focusItem(item.getItemMeta().itemId),
218
+ primaryAction: ({ tree, item }) =>
218
219
  tree.getConfig().onPrimaryAction?.(item as ItemInstance<any>),
219
- getParent: memo(
220
- (itemMeta) => {
221
- for (let i = itemMeta.index - 1; i >= 0; i--) {
222
- const potentialParent = tree.getItems()[i];
223
- if (potentialParent.getItemMeta().level < itemMeta.level) {
224
- return potentialParent;
225
- }
226
- }
227
- return tree.getItemInstance(tree.getConfig().rootItemId);
228
- },
229
- () => [item.getItemMeta()],
230
- ),
220
+ getParent: ({ tree, item }) =>
221
+ item.getItemMeta().parentId
222
+ ? tree.getItemInstance(item.getItemMeta().parentId)
223
+ : undefined,
231
224
  // TODO remove
232
- getIndexInParent: () => item.getItemMeta().posInSet,
233
- getChildren: () =>
225
+ getIndexInParent: ({ item }) => item.getItemMeta().posInSet,
226
+ getChildren: ({ tree, item }) =>
234
227
  tree
235
228
  .retrieveChildrenIds(item.getItemMeta().itemId)
236
229
  .map((id) => tree.getItemInstance(id)),
237
- getTree: () => tree as any,
238
- getItemAbove: () => tree.getItems()[item.getItemMeta().index - 1],
239
- getItemBelow: () => tree.getItems()[item.getItemMeta().index + 1],
240
- getMemoizedProp: (name, create, deps) => {
230
+ getTree: ({ tree }) => tree as any,
231
+ getItemAbove: ({ tree, item }) =>
232
+ tree.getItems()[item.getItemMeta().index - 1],
233
+ getItemBelow: ({ tree, item }) =>
234
+ tree.getItems()[item.getItemMeta().index + 1],
235
+ getMemoizedProp: ({ item }, name, create, deps) => {
241
236
  const data = item.getDataRef<TreeItemDataRef>();
242
237
  const memoizedValue = data.current.memoizedValues?.[name];
243
238
  if (
@@ -254,7 +249,7 @@ export const treeFeature: FeatureImplementation<
254
249
  data.current.memoizedValues[name] = value;
255
250
  return value;
256
251
  },
257
- }),
252
+ },
258
253
 
259
254
  hotkeys: {
260
255
  focusNextItem: {
@@ -262,7 +257,7 @@ export const treeFeature: FeatureImplementation<
262
257
  canRepeat: true,
263
258
  preventDefault: true,
264
259
  isEnabled: (tree) =>
265
- !(tree.isSearchOpen?.() ?? false) && !tree.getState().dnd,
260
+ !(tree.isSearchOpen?.() ?? false) && !tree.getState().dnd, // TODO what happens when the feature doesnt exist? proxy method still claims to exist
266
261
  handler: (e, tree) => {
267
262
  tree.focusNextItem();
268
263
  tree.updateDomFocus();