@headless-tree/core 1.4.0 → 1.5.0

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.
@@ -3,147 +3,136 @@ import { TestTree } from "../../test-utils/test-tree";
3
3
  import { checkboxesFeature } from "./feature";
4
4
  import { CheckedState } from "./types";
5
5
 
6
- const factory = TestTree.default({})
7
- .withFeatures(checkboxesFeature)
8
- .suits.sync().tree;
6
+ const factory = TestTree.default({
7
+ propagateCheckedState: true,
8
+ canCheckFolders: false,
9
+ }).withFeatures(checkboxesFeature);
9
10
 
10
11
  describe("core-feature/checkboxes", () => {
11
- it("should initialize with no checked items", async () => {
12
- const tree = await factory.createTestCaseTree();
13
- expect(tree.instance.getState().checkedItems).toEqual([]);
14
- });
15
-
16
- it("should check items", async () => {
17
- const tree = await factory.createTestCaseTree();
18
- tree.item("x111").setChecked();
19
- tree.item("x112").setChecked();
20
- expect(tree.instance.getState().checkedItems).toEqual(["x111", "x112"]);
21
- });
22
-
23
- it("should uncheck an item", async () => {
24
- const tree = await factory
25
- .with({ state: { checkedItems: ["x111"] } })
26
- .createTestCaseTree();
27
- tree.item("x111").setUnchecked();
28
- expect(tree.instance.getState().checkedItems).not.toContain("x111");
29
- });
30
-
31
- it("should toggle checked state", async () => {
32
- const tree = await factory.createTestCaseTree();
33
- const item = tree.item("x111");
12
+ factory.forSuits((tree) => {
13
+ it("should initialize with no checked items", async () => {
14
+ expect(tree.instance.getState().checkedItems).toEqual([]);
15
+ });
34
16
 
35
- item.toggleCheckedState();
36
- expect(tree.instance.getState().checkedItems).toContain("x111");
17
+ it("should check items", async () => {
18
+ await tree.item("x111").setChecked();
19
+ await tree.item("x112").setChecked();
20
+ expect(tree.instance.getState().checkedItems).toEqual(["x111", "x112"]);
21
+ });
37
22
 
38
- item.toggleCheckedState();
39
- expect(tree.instance.getState().checkedItems).not.toContain("x111");
40
- });
23
+ it("should uncheck an item", async () => {
24
+ await tree.item("x111").setChecked();
25
+ await tree.item("x111").setUnchecked();
26
+ expect(tree.instance.getState().checkedItems).not.toContain("x111");
27
+ });
41
28
 
42
- describe("props", () => {
43
29
  it("should toggle checked state", async () => {
44
- const tree = await factory.createTestCaseTree();
45
30
  const item = tree.item("x111");
46
-
47
- item.getCheckboxProps().onChange();
31
+ await item.toggleCheckedState();
48
32
  expect(tree.instance.getState().checkedItems).toContain("x111");
49
-
50
- item.getCheckboxProps().onChange();
33
+ await item.toggleCheckedState();
51
34
  expect(tree.instance.getState().checkedItems).not.toContain("x111");
52
35
  });
53
36
 
54
- it("should return checked state in props", async () => {
55
- const tree = await factory.createTestCaseTree();
56
- tree.item("x111").setChecked();
57
- expect(tree.item("x111").getCheckboxProps().checked).toBe(true);
58
- expect(tree.item("x112").getCheckboxProps().checked).toBe(false);
37
+ describe("props", () => {
38
+ it("should toggle checked state", async () => {
39
+ const item = tree.item("x111");
40
+ item.getCheckboxProps().onChange();
41
+ expect(tree.instance.getState().checkedItems).toContain("x111");
42
+ item.getCheckboxProps().onChange();
43
+ expect(tree.instance.getState().checkedItems).not.toContain("x111");
44
+ });
45
+
46
+ it("should return checked state in props", async () => {
47
+ tree.item("x111").setChecked();
48
+ expect(tree.item("x111").getCheckboxProps().checked).toBe(true);
49
+ expect(tree.item("x112").getCheckboxProps().checked).toBe(false);
50
+ });
51
+
52
+ it("should create indeterminate state", async () => {
53
+ await tree.item("x111").setChecked();
54
+ const refObject = { indeterminate: undefined };
55
+ tree.item("x11").getCheckboxProps().ref(refObject);
56
+ expect(refObject.indeterminate).toBe(true);
57
+ });
58
+
59
+ it("should not create indeterminate state", async () => {
60
+ const refObject = { indeterminate: undefined };
61
+ tree.item("x11").getCheckboxProps().ref(refObject);
62
+ expect(refObject.indeterminate).toBe(false);
63
+ });
59
64
  });
60
65
 
61
- it("should create indeterminate state", async () => {
62
- const tree = await factory.createTestCaseTree();
63
- tree.item("x111").setChecked();
64
- const refObject = { indeterminate: undefined };
65
- tree.item("x11").getCheckboxProps().ref(refObject);
66
- expect(refObject.indeterminate).toBe(true);
66
+ it("should handle folder checking", async () => {
67
+ const testTree = await tree
68
+ .with({ canCheckFolders: true, propagateCheckedState: false })
69
+ .createTestCaseTree();
70
+ testTree.item("x11").setChecked();
71
+ expect(testTree.instance.getState().checkedItems).toContain("x11");
67
72
  });
68
73
 
69
- it("should not create indeterminate state", async () => {
70
- const tree = await factory.createTestCaseTree();
71
- const refObject = { indeterminate: undefined };
72
- tree.item("x11").getCheckboxProps().ref(refObject);
73
- expect(refObject.indeterminate).toBe(false);
74
+ it("should not check folders if disabled", async () => {
75
+ const testTree = await tree
76
+ .with({ canCheckFolders: false, propagateCheckedState: false })
77
+ .createTestCaseTree();
78
+ testTree.item("x11").setChecked();
79
+ expect(testTree.instance.getState().checkedItems.length).toBe(0);
74
80
  });
75
- });
76
-
77
- it("should handle folder checking", async () => {
78
- const tree = await factory
79
- .with({ canCheckFolders: true, propagateCheckedState: false })
80
- .createTestCaseTree();
81
-
82
- tree.item("x11").setChecked();
83
- expect(tree.instance.getState().checkedItems).toContain("x11");
84
- });
85
81
 
86
- it("should not check folders if disabled", async () => {
87
- const tree = await factory
88
- .with({ canCheckFolders: false, propagateCheckedState: false })
89
- .createTestCaseTree();
90
-
91
- tree.item("x11").setChecked();
92
- expect(tree.instance.getState().checkedItems.length).toBe(0);
93
- });
94
-
95
- it("should propagate checked state", async () => {
96
- const tree = await factory
97
- .with({ propagateCheckedState: true })
98
- .createTestCaseTree();
99
-
100
- tree.item("x11").setChecked();
101
- expect(tree.instance.getState().checkedItems).toEqual(
102
- expect.arrayContaining(["x111", "x112", "x113", "x114"]),
103
- );
104
- });
105
-
106
- it("should turn folder indeterminate", async () => {
107
- const tree = await factory
108
- .with({ propagateCheckedState: true })
109
- .createTestCaseTree();
110
-
111
- tree.item("x111").setChecked();
112
- expect(tree.item("x11").getCheckedState()).toBe(CheckedState.Indeterminate);
113
- });
114
-
115
- it("should turn folder checked if all children are checked", async () => {
116
- const tree = await factory
117
- .with({
118
- isItemFolder: (item) => item.getItemData().length < 4,
119
- propagateCheckedState: true,
120
- canCheckFolders: false,
121
- })
122
- .createTestCaseTree();
123
-
124
- tree.item("x11").setChecked();
125
- tree.item("x12").setChecked();
126
- tree.item("x13").setChecked();
127
- expect(tree.item("x1").getCheckedState()).toBe(CheckedState.Indeterminate);
128
- tree.do.selectItem("x14");
129
- tree.item("x141").setChecked();
130
- tree.item("x142").setChecked();
131
- tree.item("x143").setChecked();
132
- expect(tree.item("x1").getCheckedState()).toBe(CheckedState.Indeterminate);
133
- tree.item("x144").setChecked();
134
- expect(tree.item("x1").getCheckedState()).toBe(CheckedState.Checked);
135
- });
136
-
137
- it("should return correct checked state for items", async () => {
138
- const tree = await factory.createTestCaseTree();
139
- const item = tree.instance.getItemInstance("x111");
82
+ it("should propagate checked state", async () => {
83
+ const testTree = await tree
84
+ .with({ propagateCheckedState: true })
85
+ .createTestCaseTree();
86
+ await testTree.item("x11").setChecked();
87
+ expect(testTree.instance.getState().checkedItems).toEqual(
88
+ expect.arrayContaining(["x111", "x112", "x113", "x114"]),
89
+ );
90
+ });
140
91
 
141
- expect(item.getCheckedState()).toBe(CheckedState.Unchecked);
92
+ it("should turn folder indeterminate", async () => {
93
+ const testTree = await tree
94
+ .with({ propagateCheckedState: true })
95
+ .createTestCaseTree();
96
+ testTree.item("x111").setChecked();
97
+ expect(testTree.item("x11").getCheckedState()).toBe(
98
+ CheckedState.Indeterminate,
99
+ );
100
+ });
142
101
 
143
- item.setChecked();
144
- expect(item.getCheckedState()).toBe(CheckedState.Checked);
102
+ it("should turn folder checked if all children are checked", async () => {
103
+ const testTree = await tree
104
+ .with({
105
+ isItemFolder: (item: any) => item.getItemData().length < 4,
106
+ propagateCheckedState: true,
107
+ canCheckFolders: false,
108
+ })
109
+ .createTestCaseTree();
110
+ testTree.do.selectItem("x14"); // all leafs must be loaded initially, checkpropagation check only respects visibly loaded items
111
+ // TODO ^ might be a restriction we want to avoid
112
+ await testTree.resolveAsyncVisibleItems();
113
+ await testTree.runWhileResolvingItems(testTree.item("x11").setChecked);
114
+ await testTree.runWhileResolvingItems(testTree.item("x12").setChecked);
115
+ await testTree.runWhileResolvingItems(testTree.item("x13").setChecked);
116
+ expect(testTree.item("x1").getCheckedState()).toBe(
117
+ CheckedState.Indeterminate,
118
+ );
119
+ await testTree.runWhileResolvingItems(testTree.item("x141").setChecked);
120
+ await testTree.runWhileResolvingItems(testTree.item("x142").setChecked);
121
+ await testTree.runWhileResolvingItems(testTree.item("x143").setChecked);
122
+ expect(testTree.item("x1").getCheckedState()).toBe(
123
+ CheckedState.Indeterminate,
124
+ );
125
+ await testTree.runWhileResolvingItems(testTree.item("x144").setChecked);
126
+ expect(testTree.item("x1").getCheckedState()).toBe(CheckedState.Checked);
127
+ });
145
128
 
146
- item.setUnchecked();
147
- expect(item.getCheckedState()).toBe(CheckedState.Unchecked);
129
+ it("should return correct checked state for items", async () => {
130
+ const item = tree.instance.getItemInstance("x111");
131
+ expect(item.getCheckedState()).toBe(CheckedState.Unchecked);
132
+ item.setChecked();
133
+ expect(item.getCheckedState()).toBe(CheckedState.Checked);
134
+ item.setUnchecked();
135
+ expect(item.getCheckedState()).toBe(CheckedState.Unchecked);
136
+ });
148
137
  });
149
138
  });
@@ -1,7 +1,6 @@
1
- import { FeatureImplementation, TreeInstance } from "../../types/core";
1
+ import { type FeatureImplementation, TreeInstance } from "../../types/core";
2
2
  import { makeStateUpdater } from "../../utils";
3
3
  import { CheckedState } from "./types";
4
- import { throwError } from "../../utilities/errors";
5
4
 
6
5
  const getAllLoadedDescendants = <T>(
7
6
  tree: TreeInstance<T>,
@@ -12,12 +11,50 @@ const getAllLoadedDescendants = <T>(
12
11
  return [itemId];
13
12
  }
14
13
  const descendants = tree
15
- .retrieveChildrenIds(itemId)
14
+ .retrieveChildrenIds(itemId, true)
16
15
  .map((child) => getAllLoadedDescendants(tree, child, includeFolders))
17
16
  .flat();
18
17
  return includeFolders ? [itemId, ...descendants] : descendants;
19
18
  };
20
19
 
20
+ const getAllDescendants = async <T>(
21
+ tree: TreeInstance<T>,
22
+ itemId: string,
23
+ includeFolders = false,
24
+ ): Promise<string[]> => {
25
+ await tree.loadItemData(itemId);
26
+ if (!tree.getConfig().isItemFolder(tree.getItemInstance(itemId))) {
27
+ return [itemId];
28
+ }
29
+ const childrenIds = await tree.loadChildrenIds(itemId);
30
+ const descendants = (
31
+ await Promise.all(
32
+ childrenIds.map((child) =>
33
+ getAllDescendants(tree, child, includeFolders),
34
+ ),
35
+ )
36
+ ).flat();
37
+ return includeFolders ? [itemId, ...descendants] : descendants;
38
+ };
39
+
40
+ const withLoadingState = async <T>(
41
+ tree: TreeInstance<T>,
42
+ itemId: string,
43
+ callback: () => Promise<void>,
44
+ ) => {
45
+ tree.applySubStateUpdate("loadingCheckPropagationItems", (items) => [
46
+ ...items,
47
+ itemId,
48
+ ]);
49
+ try {
50
+ await callback();
51
+ } finally {
52
+ tree.applySubStateUpdate("loadingCheckPropagationItems", (items) =>
53
+ items.filter((id) => id !== itemId),
54
+ );
55
+ }
56
+ };
57
+
21
58
  export const checkboxesFeature: FeatureImplementation = {
22
59
  key: "checkboxes",
23
60
 
@@ -25,22 +62,20 @@ export const checkboxesFeature: FeatureImplementation = {
25
62
 
26
63
  getInitialState: (initialState) => ({
27
64
  checkedItems: [],
65
+ loadingCheckPropagationItems: [],
28
66
  ...initialState,
29
67
  }),
30
68
 
31
69
  getDefaultConfig: (defaultConfig, tree) => {
32
- const hasAsyncLoader = defaultConfig.features?.some(
33
- (f) => f.key === "async-data-loader",
34
- );
35
- if (hasAsyncLoader && defaultConfig.propagateCheckedState) {
36
- throwError(`propagateCheckedState not supported with async trees`);
37
- }
38
- const propagateCheckedState =
39
- defaultConfig.propagateCheckedState ?? !hasAsyncLoader;
70
+ const propagateCheckedState = defaultConfig.propagateCheckedState ?? true;
40
71
  const canCheckFolders =
41
72
  defaultConfig.canCheckFolders ?? !propagateCheckedState;
42
73
  return {
43
74
  setCheckedItems: makeStateUpdater("checkedItems", tree),
75
+ setLoadingCheckPropagationItems: makeStateUpdater(
76
+ "loadingCheckPropagationItems",
77
+ tree,
78
+ ),
44
79
  propagateCheckedState,
45
80
  canCheckFolders,
46
81
  ...defaultConfig,
@@ -49,6 +84,7 @@ export const checkboxesFeature: FeatureImplementation = {
49
84
 
50
85
  stateHandlerNames: {
51
86
  checkedItems: "setCheckedItems",
87
+ loadingCheckPropagationItems: "setLoadingCheckPropagationItems",
52
88
  },
53
89
 
54
90
  treeInstance: {
@@ -71,11 +107,11 @@ export const checkboxesFeature: FeatureImplementation = {
71
107
  };
72
108
  },
73
109
 
74
- toggleCheckedState: ({ item }) => {
110
+ toggleCheckedState: async ({ item }) => {
75
111
  if (item.getCheckedState() === CheckedState.Checked) {
76
- item.setUnchecked();
112
+ await item.setUnchecked();
77
113
  } else {
78
- item.setChecked();
114
+ await item.setChecked();
79
115
  }
80
116
  },
81
117
 
@@ -90,6 +126,7 @@ export const checkboxesFeature: FeatureImplementation = {
90
126
 
91
127
  if (item.isFolder() && propagateCheckedState) {
92
128
  const descendants = getAllLoadedDescendants(tree, itemId);
129
+ if (descendants.length === 0) return CheckedState.Unchecked;
93
130
  if (descendants.every((d) => checkedItems.includes(d))) {
94
131
  return CheckedState.Checked;
95
132
  }
@@ -101,34 +138,46 @@ export const checkboxesFeature: FeatureImplementation = {
101
138
  return CheckedState.Unchecked;
102
139
  },
103
140
 
104
- setChecked: ({ item, tree, itemId }) => {
105
- const { propagateCheckedState, canCheckFolders } = tree.getConfig();
106
- if (item.isFolder() && propagateCheckedState) {
107
- tree.applySubStateUpdate("checkedItems", (items) => [
108
- ...items,
109
- ...getAllLoadedDescendants(tree, itemId, canCheckFolders),
110
- ]);
111
- } else if (!item.isFolder() || canCheckFolders) {
112
- tree.applySubStateUpdate("checkedItems", (items) => [...items, itemId]);
113
- }
141
+ setChecked: async ({ item, tree, itemId }) => {
142
+ await withLoadingState(tree, itemId, async () => {
143
+ const { propagateCheckedState, canCheckFolders } = tree.getConfig();
144
+ if (item.isFolder() && propagateCheckedState) {
145
+ const descendants = await getAllDescendants(
146
+ tree,
147
+ itemId,
148
+ canCheckFolders,
149
+ );
150
+ tree.applySubStateUpdate("checkedItems", (items) => [
151
+ ...items,
152
+ ...descendants,
153
+ ]);
154
+ } else if (!item.isFolder() || canCheckFolders) {
155
+ tree.applySubStateUpdate("checkedItems", (items) => [
156
+ ...items,
157
+ itemId,
158
+ ]);
159
+ }
160
+ });
114
161
  },
115
162
 
116
- setUnchecked: ({ item, tree, itemId }) => {
117
- const { propagateCheckedState, canCheckFolders } = tree.getConfig();
118
- if (item.isFolder() && propagateCheckedState) {
119
- const descendants = getAllLoadedDescendants(
120
- tree,
121
- itemId,
122
- canCheckFolders,
123
- );
124
- tree.applySubStateUpdate("checkedItems", (items) =>
125
- items.filter((id) => !descendants.includes(id) && id !== itemId),
126
- );
127
- } else {
128
- tree.applySubStateUpdate("checkedItems", (items) =>
129
- items.filter((id) => id !== itemId),
130
- );
131
- }
163
+ setUnchecked: async ({ item, tree, itemId }) => {
164
+ await withLoadingState(tree, itemId, async () => {
165
+ const { propagateCheckedState, canCheckFolders } = tree.getConfig();
166
+ if (item.isFolder() && propagateCheckedState) {
167
+ const descendants = await getAllDescendants(
168
+ tree,
169
+ itemId,
170
+ canCheckFolders,
171
+ );
172
+ tree.applySubStateUpdate("checkedItems", (items) =>
173
+ items.filter((id) => !descendants.includes(id) && id !== itemId),
174
+ );
175
+ } else {
176
+ tree.applySubStateUpdate("checkedItems", (items) =>
177
+ items.filter((id) => id !== itemId),
178
+ );
179
+ }
180
+ });
132
181
  },
133
182
  },
134
183
  };
@@ -9,9 +9,11 @@ export enum CheckedState {
9
9
  export type CheckboxesFeatureDef<T> = {
10
10
  state: {
11
11
  checkedItems: string[];
12
+ loadingCheckPropagationItems: string[];
12
13
  };
13
14
  config: {
14
15
  setCheckedItems?: SetStateFn<string[]>;
16
+ setLoadingCheckPropagationItems?: SetStateFn<string[]>;
15
17
  canCheckFolders?: boolean;
16
18
  propagateCheckedState?: boolean;
17
19
  };
@@ -19,11 +21,22 @@ export type CheckboxesFeatureDef<T> = {
19
21
  setCheckedItems: (checkedItems: string[]) => void;
20
22
  };
21
23
  itemInstance: {
22
- setChecked: () => void;
23
- setUnchecked: () => void;
24
- toggleCheckedState: () => void;
24
+ /** Will recursively load descendants if propagateCheckedState=true and async data loader is used. If not,
25
+ * this will return immediately. */
26
+ setChecked: () => Promise<void>;
27
+
28
+ /** Will recursively load descendants if propagateCheckedState=true and async data loader is used. If not,
29
+ * this will return immediately. */
30
+ setUnchecked: () => Promise<void>;
31
+
32
+ /** Will recursively load descendants if propagateCheckedState=true and async data loader is used. If not,
33
+ * this will return immediately. */
34
+ toggleCheckedState: () => Promise<void>;
35
+
25
36
  getCheckedState: () => CheckedState;
26
37
  getCheckboxProps: () => Record<string, any>;
38
+
39
+ isLoadingCheckPropagation: () => boolean;
27
40
  };
28
41
  hotkeys: never;
29
42
  };
@@ -332,6 +332,13 @@ export const dragAndDropFeature: FeatureImplementation = {
332
332
  return target ? target.item.getId() === item.getId() : false;
333
333
  },
334
334
 
335
+ isUnorderedDragTarget: ({ tree, item }) => {
336
+ const target = tree.getDragTarget();
337
+ return target
338
+ ? !isOrderedDragTarget(target) && target.item.getId() === item.getId()
339
+ : false;
340
+ },
341
+
335
342
  isDragTargetAbove: ({ tree, item }) => {
336
343
  const target = tree.getDragTarget();
337
344
 
@@ -108,7 +108,13 @@ export type DragAndDropFeatureDef<T> = {
108
108
  ) => Record<string, any>;
109
109
  };
110
110
  itemInstance: {
111
+ /** Checks if the user is dragging in a way which makes this the new parent of the dragged items, either by dragging on top of
112
+ * this item, or by dragging inbetween children of this item. See @{isUnorderedDragTarget} if the latter is undesirable. */
111
113
  isDragTarget: () => boolean;
114
+
115
+ /** As opposed to @{isDragTarget}, this will not be true if the target is inbetween children of this item. This returns only true
116
+ * if the user is dragging directly on top of this item. */
117
+ isUnorderedDragTarget: () => boolean;
112
118
  isDragTargetAbove: () => boolean;
113
119
  isDragTargetBelow: () => boolean;
114
120
  isDraggingOver: () => boolean;
@@ -13,6 +13,7 @@ const specialKeys: Record<string, RegExp> = {
13
13
  minus: /^(NumpadSubtract|Minus)$/,
14
14
  control: /^(ControlLeft|ControlRight)$/,
15
15
  shift: /^(ShiftLeft|ShiftRight)$/,
16
+ metaorcontrol: /^(MetaLeft|MetaRight|ControlLeft|ControlRight)$/,
16
17
  };
17
18
 
18
19
  const testHotkeyMatch = (
@@ -10,6 +10,11 @@ import {
10
10
  } from "../../types/core";
11
11
  import { ItemMeta } from "../tree/types";
12
12
 
13
+ export interface TreeDataRef {
14
+ isMounted?: boolean;
15
+ waitingForMount?: (() => void)[];
16
+ }
17
+
13
18
  export type InstanceTypeMap = {
14
19
  itemInstance: ItemInstance<any>;
15
20
  treeInstance: TreeInstance<any>;
@@ -49,6 +54,10 @@ export type MainFeatureDef<T = any> = {
49
54
  /* @internal */
50
55
  getHotkeyPresets: () => HotkeysConfig<T>;
51
56
  rebuildTree: () => void;
57
+ /** @deprecated Experimental feature, might get removed or changed in the future. */
58
+ scheduleRebuildTree: () => void;
59
+ /** @internal */
60
+ setMounted: (isMounted: boolean) => void;
52
61
  };
53
62
  itemInstance: {
54
63
  registerElement: (element: HTMLElement | null) => void;
@@ -18,7 +18,13 @@ export type SyncDataLoaderFeatureDef<T> = {
18
18
  };
19
19
  treeInstance: {
20
20
  retrieveItemData: (itemId: string) => T;
21
- retrieveChildrenIds: (itemId: string) => string[];
21
+
22
+ /** Retrieve children Ids. If an async data loader is used, skipFetch is set to true, and children have not been retrieved
23
+ * yet for this item, this will initiate fetching the children, and return an empty array. Once the children have loaded,
24
+ * a rerender will be triggered.
25
+ * @param skipFetch - Defaults to false.
26
+ */
27
+ retrieveChildrenIds: (itemId: string, skipFetch?: boolean) => string[];
22
28
  };
23
29
  itemInstance: {
24
30
  isLoading: () => boolean;
@@ -10,6 +10,7 @@ import { SyncDataLoaderFeatureDef } from "./features/sync-data-loader/types";
10
10
  import { TreeFeatureDef } from "./features/tree/types";
11
11
  import { PropMemoizationFeatureDef } from "./features/prop-memoization/types";
12
12
  import { KeyboardDragAndDropFeatureDef } from "./features/keyboard-drag-and-drop/types";
13
+ import type { CheckboxesFeatureDef } from "./features/checkboxes/types";
13
14
 
14
15
  export * from ".";
15
16
 
@@ -167,3 +168,15 @@ export type TreeFeatureTreeInstance<T> = TreeFeatureDef<T>["treeInstance"];
167
168
  /** @interface */
168
169
  export type TreeFeatureItemInstance<T> = TreeFeatureDef<T>["itemInstance"];
169
170
  export type TreeFeatureHotkeys<T> = TreeFeatureDef<T>["hotkeys"];
171
+
172
+ /** @interface */
173
+ export type CheckboxesFeatureConfig<T> = CheckboxesFeatureDef<T>["config"];
174
+ /** @interface */
175
+ export type CheckboxesFeatureState<T> = CheckboxesFeatureDef<T>["state"];
176
+ /** @interface */
177
+ export type CheckboxesFeatureTreeInstance<T> =
178
+ CheckboxesFeatureDef<T>["treeInstance"];
179
+ /** @interface */
180
+ export type CheckboxesFeatureItemInstance<T> =
181
+ CheckboxesFeatureDef<T>["itemInstance"];
182
+ export type CheckboxesFeatureHotkeys<T> = CheckboxesFeatureDef<T>["hotkeys"];
@@ -135,4 +135,10 @@ export class TestTreeDo<T> {
135
135
  "function called with inconsistent parameters",
136
136
  ).toBeOneOf([0, 1]);
137
137
  }
138
+
139
+ async awaitNextTick() {
140
+ await new Promise((r) => {
141
+ setTimeout(r);
142
+ });
143
+ }
138
144
  }
@@ -78,6 +78,7 @@ export class TestTree<T = string> {
78
78
  get instance() {
79
79
  if (!this.treeInstance) {
80
80
  this.treeInstance = createTree(this.config);
81
+ this.treeInstance.setMounted(true);
81
82
  this.treeInstance.rebuildTree();
82
83
  }
83
84
  return this.treeInstance;
@@ -87,10 +88,9 @@ export class TestTree<T = string> {
87
88
 
88
89
  static async resolveAsyncLoaders() {
89
90
  do {
91
+ await vi.advanceTimersToNextTimerAsync();
90
92
  TestTree.asyncLoaderResolvers.shift()?.();
91
- await new Promise<void>((r) => {
92
- setTimeout(r);
93
- });
93
+ await vi.advanceTimersToNextTimerAsync();
94
94
  } while (TestTree.asyncLoaderResolvers.length);
95
95
  }
96
96
 
@@ -101,6 +101,17 @@ export class TestTree<T = string> {
101
101
  await TestTree.resolveAsyncLoaders();
102
102
  }
103
103
 
104
+ async runWhileResolvingItems(cb: () => Promise<void>) {
105
+ const interval = setInterval(() => {
106
+ TestTree.resolveAsyncLoaders();
107
+ }, 5);
108
+ try {
109
+ await cb();
110
+ } finally {
111
+ clearInterval(interval);
112
+ }
113
+ }
114
+
104
115
  static default(config: Partial<TreeConfig<string>>) {
105
116
  return new TestTree({
106
117
  rootItemId: "x",
package/src/types/core.ts CHANGED
@@ -93,11 +93,11 @@ type MayReturnNull<T extends (...x: any[]) => any> = (
93
93
  ...args: Parameters<T>
94
94
  ) => ReturnType<T> | null;
95
95
 
96
- export type ItemInstanceOpts<Key extends keyof ItemInstance<any>> = {
97
- item: ItemInstance<any>;
98
- tree: TreeInstance<any>;
96
+ export type ItemInstanceOpts<T, Key extends keyof ItemInstance<any>> = {
97
+ item: ItemInstance<T>;
98
+ tree: TreeInstance<T>;
99
99
  itemId: string;
100
- prev?: MayReturnNull<ItemInstance<any>[Key]>;
100
+ prev?: MayReturnNull<ItemInstance<T>[Key]>;
101
101
  };
102
102
 
103
103
  export type TreeInstanceOpts<Key extends keyof TreeInstance<any>> = {
@@ -131,7 +131,7 @@ export type FeatureImplementation<T = any> = {
131
131
 
132
132
  itemInstance?: {
133
133
  [key in keyof ItemInstance<T>]?: (
134
- opts: ItemInstanceOpts<key>,
134
+ opts: ItemInstanceOpts<T, key>,
135
135
  ...args: Parameters<ItemInstance<T>[key]>
136
136
  ) => void;
137
137
  };