@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
@@ -0,0 +1,52 @@
1
+ import { describe, it } from "vitest";
2
+ import { TestTree } from "../../test-utils/test-tree";
3
+ import { expandAllFeature } from "./feature";
4
+
5
+ const factory = TestTree.default({}).withFeatures(expandAllFeature);
6
+
7
+ describe("core-feature/expand-all", () => {
8
+ factory.forSuits((tree) => {
9
+ describe("tree instance calls", () => {
10
+ it("expands all", async () => {
11
+ const expandPromise = tree.instance.expandAll();
12
+ await tree.resolveAsyncVisibleItems();
13
+ await expandPromise;
14
+ tree.expect.foldersExpanded("x12", "x13", "x14", "x4", "x41", "x44");
15
+ });
16
+
17
+ it("collapses all", () => {
18
+ tree.instance.collapseAll();
19
+ tree.expect.foldersCollapsed("x1", "x2", "x3", "x4");
20
+ });
21
+
22
+ it("cancels expanding all", async () => {
23
+ const token = { current: true };
24
+ const expandPromise = tree.instance.expandAll(token);
25
+ token.current = false;
26
+ await tree.resolveAsyncVisibleItems();
27
+ await expandPromise;
28
+ tree.expect.foldersCollapsed("x2", "x3", "x4");
29
+ });
30
+ });
31
+
32
+ describe("item instance calls", () => {
33
+ it("expands all", async () => {
34
+ const expandPromise = Promise.all([
35
+ // not sure why all are needed...
36
+ tree.instance.getItemInstance("x1").expandAll(),
37
+ tree.instance.getItemInstance("x2").expandAll(),
38
+ tree.instance.getItemInstance("x3").expandAll(),
39
+ tree.instance.getItemInstance("x4").expandAll(),
40
+ ]);
41
+ await tree.resolveAsyncVisibleItems();
42
+ await expandPromise;
43
+ tree.expect.foldersExpanded("x2", "x21", "x24");
44
+ });
45
+
46
+ it("collapses all", () => {
47
+ tree.instance.collapseAll();
48
+ tree.expect.foldersCollapsed("x1", "x2", "x3", "x4");
49
+ });
50
+ });
51
+ });
52
+ });
@@ -15,25 +15,21 @@ export const expandAllFeature: FeatureImplementation<
15
15
  > = {
16
16
  key: "expand-all",
17
17
 
18
- createTreeInstance: (prev, tree) => ({
19
- ...prev,
20
-
21
- expandAll: async (cancelToken) => {
18
+ treeInstance: {
19
+ expandAll: async ({ tree }, cancelToken) => {
22
20
  await Promise.all(
23
21
  tree.getItems().map((item) => item.expandAll(cancelToken)),
24
22
  );
25
23
  },
26
24
 
27
- collapseAll: () => {
25
+ collapseAll: ({ tree }) => {
28
26
  tree.applySubStateUpdate("expandedItems", []);
29
27
  tree.rebuildTree();
30
28
  },
31
- }),
32
-
33
- createItemInstance: (prev, item, tree) => ({
34
- ...prev,
29
+ },
35
30
 
36
- expandAll: async (cancelToken) => {
31
+ itemInstance: {
32
+ expandAll: async ({ tree, item }, cancelToken) => {
37
33
  if (cancelToken?.current) {
38
34
  return;
39
35
  }
@@ -53,11 +49,11 @@ export const expandAllFeature: FeatureImplementation<
53
49
  );
54
50
  },
55
51
 
56
- collapseAll: () => {
52
+ collapseAll: ({ item }) => {
57
53
  for (const child of item.getChildren()) {
58
54
  child?.collapseAll();
59
55
  }
60
56
  item.collapse();
61
57
  },
62
- }),
58
+ },
63
59
  };
@@ -39,7 +39,7 @@ const findHotkeyMatch = (
39
39
  ) => {
40
40
  return Object.entries({ ...config1, ...config2 }).find(([, hotkey]) =>
41
41
  testHotkeyMatch(pressedKeys, tree, hotkey),
42
- )?.[0];
42
+ )?.[0] as keyof HotkeysConfig<any> | undefined;
43
43
  };
44
44
 
45
45
  export const hotkeysCoreFeature: FeatureImplementation<
@@ -4,18 +4,31 @@ import {
4
4
  ItemInstance,
5
5
  SetStateFn,
6
6
  TreeConfig,
7
+ TreeInstance,
7
8
  TreeState,
8
9
  Updater,
9
10
  } from "../../types/core";
10
11
  import { ItemMeta } from "../tree/types";
11
12
 
13
+ export type InstanceTypeMap = {
14
+ itemInstance: ItemInstance<any>;
15
+ treeInstance: TreeInstance<any>;
16
+ };
17
+
18
+ export type InstanceBuilder = <T extends keyof InstanceTypeMap>(
19
+ features: FeatureImplementation[],
20
+ instanceType: T,
21
+ buildOpts: (self: any) => any,
22
+ ) => [instance: InstanceTypeMap[T], finalize: () => void];
23
+
12
24
  export type MainFeatureDef<T = any> = {
13
25
  state: {};
14
26
  config: {
15
27
  features?: FeatureImplementation<any>[];
16
28
  initialState?: Partial<TreeState<T>>;
17
29
  state?: Partial<TreeState<T>>;
18
- setState?: SetStateFn<TreeState<T>>;
30
+ setState?: SetStateFn<Partial<TreeState<T>>>;
31
+ instanceBuilder?: InstanceBuilder;
19
32
  };
20
33
  treeInstance: {
21
34
  /** @internal */
@@ -23,58 +23,59 @@ export const renamingFeature: FeatureImplementation<
23
23
  renamingValue: "setRenamingValue",
24
24
  },
25
25
 
26
- createTreeInstance: (prev, instance) => ({
27
- ...prev,
28
-
29
- startRenamingItem: (itemId) => {
30
- const item = instance.getItemInstance(itemId);
26
+ treeInstance: {
27
+ startRenamingItem: ({ tree }, itemId) => {
28
+ const item = tree.getItemInstance(itemId);
31
29
 
32
30
  if (!item.canRename()) {
33
31
  return;
34
32
  }
35
33
 
36
- instance.applySubStateUpdate("renamingItem", itemId);
37
- instance.applySubStateUpdate("renamingValue", item.getItemName());
34
+ tree.applySubStateUpdate("renamingItem", itemId);
35
+ tree.applySubStateUpdate("renamingValue", item.getItemName());
38
36
  },
39
37
 
40
- getRenamingItem: () => {
41
- const itemId = instance.getState().renamingItem;
42
- return itemId ? instance.getItemInstance(itemId) : null;
38
+ getRenamingItem: ({ tree }) => {
39
+ const itemId = tree.getState().renamingItem;
40
+ return itemId ? tree.getItemInstance(itemId) : null;
43
41
  },
44
42
 
45
- getRenamingValue: () => instance.getState().renamingValue || "",
43
+ getRenamingValue: ({ tree }) => tree.getState().renamingValue || "",
46
44
 
47
- abortRenaming: () => {
48
- instance.applySubStateUpdate("renamingItem", null);
45
+ abortRenaming: ({ tree }) => {
46
+ tree.applySubStateUpdate("renamingItem", null);
47
+ tree.updateDomFocus();
49
48
  },
50
49
 
51
- completeRenaming: () => {
52
- const config = instance.getConfig();
53
- const item = instance.getRenamingItem();
50
+ completeRenaming: ({ tree }) => {
51
+ const config = tree.getConfig();
52
+ const item = tree.getRenamingItem();
54
53
  if (item) {
55
- config.onRename?.(item, instance.getState().renamingValue || "");
54
+ config.onRename?.(item, tree.getState().renamingValue || "");
56
55
  }
57
- instance.applySubStateUpdate("renamingItem", null);
56
+ tree.applySubStateUpdate("renamingItem", null);
57
+ tree.updateDomFocus();
58
58
  },
59
59
 
60
- isRenamingItem: () => !!instance.getState().renamingItem,
61
- }),
60
+ isRenamingItem: ({ tree }) => !!tree.getState().renamingItem,
61
+ },
62
62
 
63
- createItemInstance: (prev, instance, tree) => ({
64
- ...prev,
65
- getRenameInputProps: () => ({
63
+ itemInstance: {
64
+ getRenameInputProps: ({ tree }) => ({
66
65
  onBlur: () => tree.abortRenaming(),
67
66
  value: tree.getRenamingValue(),
68
- onChange: (e) => {
69
- tree.applySubStateUpdate("renamingValue", e.target.value);
67
+ onChange: (e: any) => {
68
+ // TODO custom type with e.target.value
69
+ tree.applySubStateUpdate("renamingValue", e.target?.value);
70
70
  },
71
71
  }),
72
72
 
73
- canRename: () =>
74
- tree.getConfig().canRename?.(instance as ItemInstance<any>) ?? true,
73
+ canRename: ({ tree, item }) =>
74
+ tree.getConfig().canRename?.(item as ItemInstance<any>) ?? true,
75
75
 
76
- isRenaming: () => instance.getId() === tree.getState().renamingItem,
77
- }),
76
+ isRenaming: ({ tree, item }) =>
77
+ item.getId() === tree.getState().renamingItem,
78
+ },
78
79
 
79
80
  hotkeys: {
80
81
  renameItem: {
@@ -0,0 +1,125 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { TestTree } from "../../test-utils/test-tree";
3
+ import { renamingFeature } from "./feature";
4
+ import { selectionFeature } from "../selection/feature";
5
+
6
+ const factory = TestTree.default({}).withFeatures(
7
+ renamingFeature,
8
+ selectionFeature,
9
+ );
10
+
11
+ describe("core-feature/renaming", () => {
12
+ factory.forSuits((tree) => {
13
+ it("starts and aborts renaming", () => {
14
+ tree.instance.startRenamingItem("x1");
15
+ expect(tree.instance.isRenamingItem()).toBe(true);
16
+ expect(tree.instance.getRenamingValue()).toBe("x1");
17
+ tree.instance.abortRenaming();
18
+ expect(tree.instance.isRenamingItem()).toBe(false);
19
+ });
20
+
21
+ it("stops renaming by blurring", () => {
22
+ tree.instance.startRenamingItem("x1");
23
+ tree.instance.getRenamingItem()!.getRenameInputProps().onBlur();
24
+ expect(tree.instance.isRenamingItem()).toBe(false);
25
+ });
26
+
27
+ it("completes renaming programmatically", () => {
28
+ const onRename = tree.mockedHandler("onRename");
29
+
30
+ tree.instance.startRenamingItem("x1");
31
+ expect(tree.instance.getRenamingItem()!.getRenameInputProps().value).toBe(
32
+ "x1",
33
+ );
34
+ tree.instance
35
+ .getRenamingItem()!
36
+ .getRenameInputProps()
37
+ .onChange({
38
+ target: { value: "renamed" },
39
+ });
40
+ expect(tree.instance.getRenamingItem()!.getRenameInputProps().value).toBe(
41
+ "renamed",
42
+ );
43
+ tree.instance.completeRenaming();
44
+ expect(onRename).toHaveBeenCalledWith(
45
+ tree.instance.getItemInstance("x1"),
46
+ "renamed",
47
+ );
48
+ expect(tree.instance.isRenamingItem()).toBe(false);
49
+ });
50
+
51
+ it("invokes state setters when aborting", () => {
52
+ const setRenamingItem = tree.mockedHandler("setRenamingItem");
53
+ const setRenamingValue = tree.mockedHandler("setRenamingValue");
54
+ tree.instance.startRenamingItem("x1");
55
+ expect(setRenamingItem).toHaveBeenCalledWith("x1");
56
+ expect(setRenamingValue).toHaveBeenCalledWith("x1");
57
+ tree.instance.abortRenaming();
58
+ expect(setRenamingItem).toHaveBeenCalledWith(null);
59
+ });
60
+
61
+ it("invokes state setters when completing", () => {
62
+ const setRenamingItem = tree.mockedHandler("setRenamingItem");
63
+ const setRenamingValue = tree.mockedHandler("setRenamingValue");
64
+ tree.instance.startRenamingItem("x1");
65
+ tree.instance
66
+ .getRenamingItem()!
67
+ .getRenameInputProps()
68
+ .onChange({
69
+ target: { value: "renamed" },
70
+ });
71
+ expect(setRenamingItem).toHaveBeenCalledWith("x1");
72
+ expect(setRenamingValue).toHaveBeenCalledWith("renamed");
73
+ tree.instance.completeRenaming();
74
+ expect(setRenamingItem).toHaveBeenCalledWith(null);
75
+ });
76
+
77
+ it("changes renaming input content with input props", () => {
78
+ const setRenamingValue = tree.mockedHandler("setRenamingValue");
79
+ tree.instance.startRenamingItem("x1");
80
+ tree.instance
81
+ .getRenamingItem()!
82
+ .getRenameInputProps()
83
+ .onChange({ target: { value: "New Name" } });
84
+ expect(setRenamingValue).toHaveBeenCalledWith("New Name");
85
+ });
86
+
87
+ it("aborts renaming with input props", () => {
88
+ const setRenamingItem = tree.mockedHandler("setRenamingItem");
89
+ tree.instance.startRenamingItem("x1");
90
+ tree.instance.getRenamingItem()!.getRenameInputProps().onBlur();
91
+ expect(setRenamingItem).toHaveBeenCalledWith(null);
92
+ });
93
+
94
+ describe("hotkeys", () => {
95
+ it("starts renaming", () => {
96
+ const setRenamingItem = tree.mockedHandler("setRenamingItem");
97
+ tree.do.hotkey("renameItem");
98
+ expect(setRenamingItem).toHaveBeenCalledWith("x1");
99
+ });
100
+
101
+ it("aborts renaming with Escape key", () => {
102
+ const setRenamingItem = tree.mockedHandler("setRenamingItem");
103
+ tree.instance.startRenamingItem("x1");
104
+ tree.do.hotkey("abortRenaming");
105
+ expect(setRenamingItem).toHaveBeenCalledWith(null);
106
+ });
107
+
108
+ it("completes renaming with Enter key", () => {
109
+ const onRename = tree.mockedHandler("onRename");
110
+ tree.instance.startRenamingItem("x1");
111
+ tree.instance
112
+ .getRenamingItem()!
113
+ .getRenameInputProps()
114
+ .onChange({
115
+ target: { value: "renamed" },
116
+ });
117
+ tree.do.hotkey("completeRenaming");
118
+ expect(onRename).toHaveBeenCalledWith(
119
+ tree.instance.getItemInstance("x1"),
120
+ "renamed",
121
+ );
122
+ });
123
+ });
124
+ });
125
+ });
@@ -6,7 +6,7 @@ export type RenamingFeatureDef<T> = {
6
6
  renamingValue?: string;
7
7
  };
8
8
  config: {
9
- setRenamingItem?: SetStateFn<string | null>;
9
+ setRenamingItem?: SetStateFn<string | null | undefined>;
10
10
  setRenamingValue?: SetStateFn<string | undefined>;
11
11
  canRename?: (item: ItemInstance<T>) => boolean;
12
12
  onRename?: (item: ItemInstance<T>, value: string) => void;
@@ -28,65 +28,61 @@ export const searchFeature: FeatureImplementation<
28
28
  search: "setSearch",
29
29
  },
30
30
 
31
- createTreeInstance: (prev, instance) => ({
32
- ...prev,
33
-
34
- setSearch: (search) => {
35
- instance.applySubStateUpdate("search", search);
36
- instance
31
+ treeInstance: {
32
+ setSearch: ({ tree }, search) => {
33
+ tree.applySubStateUpdate("search", search);
34
+ tree
37
35
  .getItems()
38
36
  .find((item) =>
39
- instance
40
- .getConfig()
41
- .isSearchMatchingItem?.(instance.getSearchValue(), item),
37
+ tree.getConfig().isSearchMatchingItem?.(tree.getSearchValue(), item),
42
38
  )
43
39
  ?.setFocused();
44
40
  },
45
- openSearch: (initialValue = "") => {
46
- instance.setSearch(initialValue);
41
+ openSearch: ({ tree }, initialValue = "") => {
42
+ tree.setSearch(initialValue);
43
+ tree.getConfig().onOpenSearch?.();
47
44
  setTimeout(() => {
48
- instance
49
- .getDataRef<SearchFeatureDataRef>()
50
- .current.searchInput?.focus();
45
+ tree.getDataRef<SearchFeatureDataRef>().current.searchInput?.focus();
51
46
  });
52
47
  },
53
- closeSearch: () => {
54
- instance.setSearch(null);
55
- instance.updateDomFocus();
48
+ closeSearch: ({ tree }) => {
49
+ tree.setSearch(null);
50
+ tree.getConfig().onCloseSearch?.();
51
+ tree.updateDomFocus();
56
52
  },
57
- isSearchOpen: () => instance.getState().search !== null,
58
- getSearchValue: () => instance.getState().search || "",
59
- registerSearchInputElement: (element) => {
60
- const dataRef = instance.getDataRef<SearchFeatureDataRef>();
53
+ isSearchOpen: ({ tree }) => tree.getState().search !== null,
54
+ getSearchValue: ({ tree }) => tree.getState().search || "",
55
+ registerSearchInputElement: ({ tree }, element) => {
56
+ const dataRef = tree.getDataRef<SearchFeatureDataRef>();
61
57
  dataRef.current.searchInput = element;
62
58
  if (element && dataRef.current.keydownHandler) {
63
59
  element.addEventListener("keydown", dataRef.current.keydownHandler);
64
60
  }
65
61
  },
66
- getSearchInputElement: () =>
67
- instance.getDataRef<SearchFeatureDataRef>().current.searchInput ?? null,
62
+ getSearchInputElement: ({ tree }) =>
63
+ tree.getDataRef<SearchFeatureDataRef>().current.searchInput ?? null,
68
64
 
69
- getSearchInputElementProps: () => ({
70
- value: instance.getSearchValue(),
71
- onChange: (e: any) => instance.setSearch(e.target.value),
72
- onBlur: () => instance.closeSearch(),
65
+ getSearchInputElementProps: ({ tree }) => ({
66
+ value: tree.getSearchValue(),
67
+ onChange: (e: any) => tree.setSearch(e.target.value),
68
+ onBlur: () => tree.closeSearch(),
73
69
  }),
74
70
 
75
71
  getSearchMatchingItems: memo(
76
- (search, items) =>
77
- items.filter(
78
- (item) =>
79
- search && instance.getConfig().isSearchMatchingItem?.(search, item),
80
- ),
81
- () => [instance.getSearchValue(), instance.getItems()],
72
+ ({ tree }) => [
73
+ tree.getSearchValue(),
74
+ tree.getItems(),
75
+ tree.getConfig().isSearchMatchingItem,
76
+ ],
77
+ (search, items, isSearchMatchingItem) =>
78
+ items.filter((item) => search && isSearchMatchingItem?.(search, item)),
82
79
  ),
83
- }),
80
+ },
84
81
 
85
- createItemInstance: (prev, item, tree) => ({
86
- ...prev,
87
- isMatchingSearch: () =>
82
+ itemInstance: {
83
+ isMatchingSearch: ({ tree, item }) =>
88
84
  tree.getSearchMatchingItems().some((i) => i.getId() === item.getId()),
89
- }),
85
+ },
90
86
 
91
87
  hotkeys: {
92
88
  openSearch: {
@@ -0,0 +1,115 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { TestTree } from "../../test-utils/test-tree";
3
+ import { searchFeature } from "./feature";
4
+ import { selectionFeature } from "../selection/feature";
5
+
6
+ const factory = TestTree.default({}).withFeatures(
7
+ searchFeature,
8
+ selectionFeature,
9
+ );
10
+
11
+ describe("core-feature/search", () => {
12
+ factory.forSuits((tree) => {
13
+ it("opens and closes search", () => {
14
+ tree.instance.openSearch();
15
+ expect(tree.instance.isSearchOpen()).toBe(true);
16
+ expect(tree.instance.getSearchValue()).toBe("");
17
+ tree.instance.closeSearch();
18
+ expect(tree.instance.isSearchOpen()).toBe(false);
19
+ });
20
+
21
+ it("opens and closes search with initial value", () => {
22
+ tree.instance.openSearch("test");
23
+ expect(tree.instance.isSearchOpen()).toBe(true);
24
+ expect(tree.instance.getSearchValue()).toBe("test");
25
+ tree.instance.closeSearch();
26
+ expect(tree.instance.isSearchOpen()).toBe(false);
27
+ });
28
+
29
+ it("invokes open and close handlers", () => {
30
+ const onOpenSearch = tree.mockedHandler("onOpenSearch");
31
+ const onCloseSearch = tree.mockedHandler("onCloseSearch");
32
+ tree.instance.openSearch("test");
33
+ expect(onOpenSearch).toHaveBeenCalled();
34
+ tree.instance.closeSearch();
35
+ expect(onCloseSearch).toHaveBeenCalled();
36
+ });
37
+
38
+ it("invokes state setter", () => {
39
+ const setSearch = tree.mockedHandler("setSearch");
40
+ tree.instance.openSearch("test");
41
+ expect(setSearch).toHaveBeenCalledWith("test");
42
+ tree.instance.closeSearch();
43
+ expect(setSearch).toHaveBeenCalledWith(null);
44
+ });
45
+
46
+ it("can search and return matches", () => {
47
+ tree.instance.setSearch("12");
48
+ expect(tree.instance.getSearchMatchingItems().length).toBe(2);
49
+ expect(tree.instance.getSearchMatchingItems()[0].getId()).toBe("x112");
50
+ expect(tree.instance.getSearchMatchingItems()[1].getId()).toBe("x12");
51
+ });
52
+
53
+ it("uses isSearchMatchingItem handler", () => {
54
+ const isSearchMatchingItem = tree.mockedHandler("isSearchMatchingItem");
55
+ isSearchMatchingItem.mockImplementation(
56
+ (_, item) => item.getId() === "x3",
57
+ );
58
+ tree.instance.setSearch("xyz");
59
+ expect(tree.instance.getSearchMatchingItems().length).toBe(1);
60
+ expect(tree.instance.getSearchMatchingItems()[0].getId()).toBe("x3");
61
+ });
62
+
63
+ it("changes search input contentwith input props", () => {
64
+ const setSearch = tree.mockedHandler("setSearch");
65
+ tree.instance
66
+ .getSearchInputElementProps()
67
+ .onChange({ target: { value: "test" } });
68
+ expect(setSearch).toHaveBeenCalledWith("test");
69
+ });
70
+
71
+ it("closes search input with input props", () => {
72
+ const onCloseSearch = tree.mockedHandler("onCloseSearch");
73
+ tree.instance.openSearch();
74
+ tree.instance.getSearchInputElementProps().onBlur();
75
+ expect(onCloseSearch).toHaveBeenCalled();
76
+ });
77
+
78
+ describe("hotkeys", () => {
79
+ it("opens search", () => {
80
+ const setSearch = tree.mockedHandler("setSearch");
81
+ tree.do.hotkey("openSearch");
82
+ expect(setSearch).toHaveBeenCalledWith("");
83
+ });
84
+
85
+ it("selects focused item when search is submitted", () => {
86
+ const setSelectedItems = tree.mockedHandler("setSelectedItems");
87
+ const onCloseSearch = tree.mockedHandler("onCloseSearch");
88
+ tree.instance.focusItem("x1");
89
+ tree.do.hotkey("openSearch");
90
+ tree.do.hotkey("submitSearch");
91
+ expect(setSelectedItems).toHaveBeenCalledWith(["x1"]);
92
+ expect(onCloseSearch).toHaveBeenCalled();
93
+ });
94
+
95
+ it("scrolls through searched items", () => {
96
+ const setFocusedItem = tree.mockedHandler("setFocusedItem");
97
+ tree.instance.setSearch("12");
98
+ tree.do.hotkey("nextSearchItem");
99
+ expect(setFocusedItem).toHaveBeenCalledWith("x112");
100
+
101
+ tree.do.hotkey("nextSearchItem");
102
+ expect(setFocusedItem).toHaveBeenCalledWith("x12");
103
+
104
+ tree.do.hotkey("nextSearchItem");
105
+ expect(setFocusedItem).toHaveBeenCalledTimes(2);
106
+
107
+ tree.do.hotkey("previousSearchItem");
108
+ tree.do.hotkey("previousSearchItem");
109
+ tree.do.hotkey("previousSearchItem");
110
+ expect(setFocusedItem).toHaveBeenCalledTimes(3);
111
+ expect(setFocusedItem).nthCalledWith(3, "x112");
112
+ });
113
+ });
114
+ });
115
+ });
@@ -14,7 +14,6 @@ export type SearchFeatureDef<T> = {
14
14
  setSearch?: SetStateFn<string | null>;
15
15
  onOpenSearch?: () => void;
16
16
  onCloseSearch?: () => void;
17
- onSearchMatchesItems?: (search: string, items: ItemInstance<T>[]) => void;
18
17
  isSearchMatchingItem?: (search: string, item: ItemInstance<T>) => boolean;
19
18
  };
20
19
  treeInstance: {