@headless-tree/core 0.0.10 → 0.0.12

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 (148) 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 +26 -0
  6. package/lib/cjs/core/create-tree.js +62 -40
  7. package/lib/cjs/features/async-data-loader/feature.d.ts +1 -4
  8. package/lib/cjs/features/async-data-loader/feature.js +35 -23
  9. package/lib/cjs/features/async-data-loader/types.d.ts +4 -6
  10. package/lib/cjs/features/drag-and-drop/feature.d.ts +2 -3
  11. package/lib/cjs/features/drag-and-drop/feature.js +79 -44
  12. package/lib/cjs/features/drag-and-drop/types.d.ts +15 -6
  13. package/lib/cjs/features/drag-and-drop/utils.d.ts +2 -3
  14. package/lib/cjs/features/drag-and-drop/utils.js +140 -37
  15. package/lib/cjs/features/expand-all/feature.d.ts +1 -5
  16. package/lib/cjs/features/expand-all/feature.js +12 -6
  17. package/lib/cjs/features/hotkeys-core/feature.d.ts +1 -3
  18. package/lib/cjs/features/main/types.d.ts +8 -2
  19. package/lib/cjs/features/prop-memoization/feature.d.ts +2 -0
  20. package/lib/cjs/features/prop-memoization/feature.js +48 -0
  21. package/lib/cjs/features/prop-memoization/types.d.ts +10 -0
  22. package/lib/cjs/features/prop-memoization/types.js +2 -0
  23. package/lib/cjs/features/renaming/feature.d.ts +1 -4
  24. package/lib/cjs/features/renaming/feature.js +36 -22
  25. package/lib/cjs/features/renaming/types.d.ts +2 -2
  26. package/lib/cjs/features/search/feature.d.ts +1 -4
  27. package/lib/cjs/features/search/feature.js +38 -24
  28. package/lib/cjs/features/search/types.d.ts +0 -1
  29. package/lib/cjs/features/selection/feature.d.ts +1 -4
  30. package/lib/cjs/features/selection/feature.js +54 -35
  31. package/lib/cjs/features/selection/types.d.ts +1 -1
  32. package/lib/cjs/features/sync-data-loader/feature.d.ts +1 -3
  33. package/lib/cjs/features/sync-data-loader/feature.js +7 -2
  34. package/lib/cjs/features/tree/feature.d.ts +1 -5
  35. package/lib/cjs/features/tree/feature.js +97 -92
  36. package/lib/cjs/features/tree/types.d.ts +5 -8
  37. package/lib/cjs/index.d.ts +5 -1
  38. package/lib/cjs/index.js +4 -1
  39. package/lib/cjs/mddocs-entry.d.ts +10 -0
  40. package/lib/cjs/test-utils/test-tree-do.d.ts +23 -0
  41. package/lib/cjs/test-utils/test-tree-do.js +99 -0
  42. package/lib/cjs/test-utils/test-tree-expect.d.ts +15 -0
  43. package/lib/cjs/test-utils/test-tree-expect.js +62 -0
  44. package/lib/cjs/test-utils/test-tree.d.ts +47 -0
  45. package/lib/cjs/test-utils/test-tree.js +203 -0
  46. package/lib/cjs/types/core.d.ts +39 -24
  47. package/lib/cjs/utilities/errors.d.ts +1 -0
  48. package/lib/cjs/utilities/errors.js +5 -0
  49. package/lib/cjs/utilities/insert-items-at-target.js +10 -3
  50. package/lib/cjs/utilities/remove-items-from-parents.js +14 -8
  51. package/lib/cjs/utils.d.ts +3 -3
  52. package/lib/cjs/utils.js +6 -6
  53. package/lib/esm/core/build-proxified-instance.d.ts +2 -0
  54. package/lib/esm/core/build-proxified-instance.js +54 -0
  55. package/lib/esm/core/build-static-instance.d.ts +2 -0
  56. package/lib/esm/core/build-static-instance.js +22 -0
  57. package/lib/esm/core/create-tree.js +62 -40
  58. package/lib/esm/features/async-data-loader/feature.d.ts +1 -4
  59. package/lib/esm/features/async-data-loader/feature.js +35 -23
  60. package/lib/esm/features/async-data-loader/types.d.ts +4 -6
  61. package/lib/esm/features/drag-and-drop/feature.d.ts +2 -3
  62. package/lib/esm/features/drag-and-drop/feature.js +79 -44
  63. package/lib/esm/features/drag-and-drop/types.d.ts +15 -6
  64. package/lib/esm/features/drag-and-drop/utils.d.ts +2 -3
  65. package/lib/esm/features/drag-and-drop/utils.js +138 -34
  66. package/lib/esm/features/expand-all/feature.d.ts +1 -5
  67. package/lib/esm/features/expand-all/feature.js +12 -6
  68. package/lib/esm/features/hotkeys-core/feature.d.ts +1 -3
  69. package/lib/esm/features/main/types.d.ts +8 -2
  70. package/lib/esm/features/prop-memoization/feature.d.ts +2 -0
  71. package/lib/esm/features/prop-memoization/feature.js +45 -0
  72. package/lib/esm/features/prop-memoization/types.d.ts +10 -0
  73. package/lib/esm/features/prop-memoization/types.js +1 -0
  74. package/lib/esm/features/renaming/feature.d.ts +1 -4
  75. package/lib/esm/features/renaming/feature.js +36 -22
  76. package/lib/esm/features/renaming/types.d.ts +2 -2
  77. package/lib/esm/features/search/feature.d.ts +1 -4
  78. package/lib/esm/features/search/feature.js +38 -24
  79. package/lib/esm/features/search/types.d.ts +0 -1
  80. package/lib/esm/features/selection/feature.d.ts +1 -4
  81. package/lib/esm/features/selection/feature.js +54 -35
  82. package/lib/esm/features/selection/types.d.ts +1 -1
  83. package/lib/esm/features/sync-data-loader/feature.d.ts +1 -3
  84. package/lib/esm/features/sync-data-loader/feature.js +7 -2
  85. package/lib/esm/features/tree/feature.d.ts +1 -5
  86. package/lib/esm/features/tree/feature.js +98 -93
  87. package/lib/esm/features/tree/types.d.ts +5 -8
  88. package/lib/esm/index.d.ts +5 -1
  89. package/lib/esm/index.js +4 -1
  90. package/lib/esm/mddocs-entry.d.ts +10 -0
  91. package/lib/esm/test-utils/test-tree-do.d.ts +23 -0
  92. package/lib/esm/test-utils/test-tree-do.js +95 -0
  93. package/lib/esm/test-utils/test-tree-expect.d.ts +15 -0
  94. package/lib/esm/test-utils/test-tree-expect.js +58 -0
  95. package/lib/esm/test-utils/test-tree.d.ts +47 -0
  96. package/lib/esm/test-utils/test-tree.js +199 -0
  97. package/lib/esm/types/core.d.ts +39 -24
  98. package/lib/esm/utilities/errors.d.ts +1 -0
  99. package/lib/esm/utilities/errors.js +1 -0
  100. package/lib/esm/utilities/insert-items-at-target.js +10 -3
  101. package/lib/esm/utilities/remove-items-from-parents.js +14 -8
  102. package/lib/esm/utils.d.ts +3 -3
  103. package/lib/esm/utils.js +3 -3
  104. package/package.json +7 -3
  105. package/src/core/build-proxified-instance.ts +117 -0
  106. package/src/core/build-static-instance.ts +27 -0
  107. package/src/core/core.spec.ts +210 -0
  108. package/src/core/create-tree.ts +73 -78
  109. package/src/features/async-data-loader/async-data-loader.spec.ts +124 -0
  110. package/src/features/async-data-loader/feature.ts +34 -44
  111. package/src/features/async-data-loader/types.ts +4 -6
  112. package/src/features/drag-and-drop/drag-and-drop.spec.ts +717 -0
  113. package/src/features/drag-and-drop/feature.ts +88 -63
  114. package/src/features/drag-and-drop/types.ts +24 -10
  115. package/src/features/drag-and-drop/utils.ts +197 -56
  116. package/src/features/expand-all/expand-all.spec.ts +56 -0
  117. package/src/features/expand-all/feature.ts +9 -24
  118. package/src/features/hotkeys-core/feature.ts +5 -14
  119. package/src/features/main/types.ts +14 -1
  120. package/src/features/prop-memoization/feature.ts +51 -0
  121. package/src/features/prop-memoization/prop-memoization.spec.ts +68 -0
  122. package/src/features/prop-memoization/types.ts +11 -0
  123. package/src/features/renaming/feature.ts +37 -45
  124. package/src/features/renaming/renaming.spec.ts +127 -0
  125. package/src/features/renaming/types.ts +2 -2
  126. package/src/features/search/feature.ts +36 -46
  127. package/src/features/search/search.spec.ts +117 -0
  128. package/src/features/search/types.ts +0 -1
  129. package/src/features/selection/feature.ts +50 -53
  130. package/src/features/selection/selection.spec.ts +219 -0
  131. package/src/features/selection/types.ts +0 -2
  132. package/src/features/sync-data-loader/feature.ts +9 -18
  133. package/src/features/tree/feature.ts +101 -144
  134. package/src/features/tree/tree.spec.ts +475 -0
  135. package/src/features/tree/types.ts +5 -9
  136. package/src/index.ts +6 -1
  137. package/src/mddocs-entry.ts +13 -0
  138. package/src/test-utils/test-tree-do.ts +136 -0
  139. package/src/test-utils/test-tree-expect.ts +86 -0
  140. package/src/test-utils/test-tree.ts +227 -0
  141. package/src/types/core.ts +76 -108
  142. package/src/utilities/errors.ts +2 -0
  143. package/src/utilities/insert-items-at-target.ts +10 -3
  144. package/src/utilities/remove-items-from-parents.ts +15 -10
  145. package/src/utils.spec.ts +89 -0
  146. package/src/utils.ts +6 -6
  147. package/tsconfig.json +1 -0
  148. package/vitest.config.ts +6 -0
@@ -0,0 +1,56 @@
1
+ import { describe, it } from "vitest";
2
+ import { TestTree } from "../../test-utils/test-tree";
3
+ import { expandAllFeature } from "./feature";
4
+ import { propMemoizationFeature } from "../prop-memoization/feature";
5
+
6
+ const factory = TestTree.default({}).withFeatures(
7
+ expandAllFeature,
8
+ propMemoizationFeature,
9
+ );
10
+
11
+ describe("core-feature/expand-all", () => {
12
+ factory.forSuits((tree) => {
13
+ describe("tree instance calls", () => {
14
+ it("expands all", async () => {
15
+ const expandPromise = tree.instance.expandAll();
16
+ await tree.resolveAsyncVisibleItems();
17
+ await expandPromise;
18
+ tree.expect.foldersExpanded("x12", "x13", "x14", "x4", "x41", "x44");
19
+ });
20
+
21
+ it("collapses all", () => {
22
+ tree.instance.collapseAll();
23
+ tree.expect.foldersCollapsed("x1", "x2", "x3", "x4");
24
+ });
25
+
26
+ it("cancels expanding all", async () => {
27
+ const token = { current: true };
28
+ const expandPromise = tree.instance.expandAll(token);
29
+ token.current = false;
30
+ await tree.resolveAsyncVisibleItems();
31
+ await expandPromise;
32
+ tree.expect.foldersCollapsed("x2", "x3", "x4");
33
+ });
34
+ });
35
+
36
+ describe("item instance calls", () => {
37
+ it("expands all", async () => {
38
+ const expandPromise = Promise.all([
39
+ // not sure why all are needed...
40
+ tree.instance.getItemInstance("x1").expandAll(),
41
+ tree.instance.getItemInstance("x2").expandAll(),
42
+ tree.instance.getItemInstance("x3").expandAll(),
43
+ tree.instance.getItemInstance("x4").expandAll(),
44
+ ]);
45
+ await tree.resolveAsyncVisibleItems();
46
+ await expandPromise;
47
+ tree.expect.foldersExpanded("x2", "x21", "x24");
48
+ });
49
+
50
+ it("collapses all", () => {
51
+ tree.instance.collapseAll();
52
+ tree.expect.foldersCollapsed("x1", "x2", "x3", "x4");
53
+ });
54
+ });
55
+ });
56
+ });
@@ -1,39 +1,24 @@
1
1
  import { FeatureImplementation } from "../../types/core";
2
- import { ExpandAllFeatureDef } from "./types";
3
- import { MainFeatureDef } from "../main/types";
4
- import { TreeFeatureDef } from "../tree/types";
5
- import { SyncDataLoaderFeatureDef } from "../sync-data-loader/types";
6
2
  import { poll } from "../../utils";
7
3
 
8
- export const expandAllFeature: FeatureImplementation<
9
- any,
10
- ExpandAllFeatureDef,
11
- | MainFeatureDef
12
- | TreeFeatureDef<any>
13
- | SyncDataLoaderFeatureDef<any>
14
- | ExpandAllFeatureDef
15
- > = {
4
+ export const expandAllFeature: FeatureImplementation = {
16
5
  key: "expand-all",
17
6
 
18
- createTreeInstance: (prev, tree) => ({
19
- ...prev,
20
-
21
- expandAll: async (cancelToken) => {
7
+ treeInstance: {
8
+ expandAll: async ({ tree }, cancelToken) => {
22
9
  await Promise.all(
23
10
  tree.getItems().map((item) => item.expandAll(cancelToken)),
24
11
  );
25
12
  },
26
13
 
27
- collapseAll: () => {
14
+ collapseAll: ({ tree }) => {
28
15
  tree.applySubStateUpdate("expandedItems", []);
29
16
  tree.rebuildTree();
30
17
  },
31
- }),
32
-
33
- createItemInstance: (prev, item, tree) => ({
34
- ...prev,
18
+ },
35
19
 
36
- expandAll: async (cancelToken) => {
20
+ itemInstance: {
21
+ expandAll: async ({ tree, item }, cancelToken) => {
37
22
  if (cancelToken?.current) {
38
23
  return;
39
24
  }
@@ -53,11 +38,11 @@ export const expandAllFeature: FeatureImplementation<
53
38
  );
54
39
  },
55
40
 
56
- collapseAll: () => {
41
+ collapseAll: ({ item }) => {
57
42
  for (const child of item.getChildren()) {
58
43
  child?.collapseAll();
59
44
  }
60
45
  item.collapse();
61
46
  },
62
- }),
47
+ },
63
48
  };
@@ -3,12 +3,7 @@ import {
3
3
  HotkeysConfig,
4
4
  TreeInstance,
5
5
  } from "../../types/core";
6
- import {
7
- HotkeyConfig,
8
- HotkeysCoreDataRef,
9
- HotkeysCoreFeatureDef,
10
- } from "./types";
11
- import { MainFeatureDef } from "../main/types";
6
+ import { HotkeyConfig, HotkeysCoreDataRef } from "./types";
12
7
 
13
8
  const specialKeys: Record<string, RegExp> = {
14
9
  Letter: /^[a-z]$/,
@@ -34,19 +29,15 @@ const testHotkeyMatch = (
34
29
  const findHotkeyMatch = (
35
30
  pressedKeys: Set<string>,
36
31
  tree: TreeInstance<any>,
37
- config1: HotkeysConfig<any, any>,
38
- config2: HotkeysConfig<any, any>,
32
+ config1: HotkeysConfig<any>,
33
+ config2: HotkeysConfig<any>,
39
34
  ) => {
40
35
  return Object.entries({ ...config1, ...config2 }).find(([, hotkey]) =>
41
36
  testHotkeyMatch(pressedKeys, tree, hotkey),
42
- )?.[0];
37
+ )?.[0] as keyof HotkeysConfig<any> | undefined;
43
38
  };
44
39
 
45
- export const hotkeysCoreFeature: FeatureImplementation<
46
- any,
47
- HotkeysCoreFeatureDef<any>,
48
- MainFeatureDef | HotkeysCoreFeatureDef<any>
49
- > = {
40
+ export const hotkeysCoreFeature: FeatureImplementation = {
50
41
  key: "hotkeys-core",
51
42
 
52
43
  onTreeMount: (tree, element) => {
@@ -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 */
@@ -0,0 +1,51 @@
1
+ import { FeatureImplementation } from "../../types/core";
2
+ import { PropMemoizationDataRef } from "./types";
3
+
4
+ const memoize = (
5
+ props: Record<string, any>,
6
+ dataRef: PropMemoizationDataRef,
7
+ ) => {
8
+ dataRef.memoizedProps ??= {};
9
+ for (const key in props) {
10
+ if (typeof props[key] === "function") {
11
+ if (key in dataRef.memoizedProps) {
12
+ props[key] = dataRef.memoizedProps[key];
13
+ } else {
14
+ dataRef.memoizedProps[key] = props[key];
15
+ }
16
+ }
17
+ }
18
+ return props;
19
+ };
20
+
21
+ export const propMemoizationFeature: FeatureImplementation = {
22
+ key: "prop-memoization",
23
+
24
+ overwrites: [
25
+ "main",
26
+ "async-data-loader",
27
+ "sync-data-loader",
28
+ "drag-and-drop",
29
+ "expand-all",
30
+ "hotkeys-core",
31
+ "renaming",
32
+ "search",
33
+ "selection",
34
+ ],
35
+
36
+ treeInstance: {
37
+ getContainerProps: ({ tree, prev }) => {
38
+ const dataRef = tree.getDataRef<PropMemoizationDataRef>();
39
+ const props = prev?.() ?? {};
40
+ return memoize(props, dataRef.current);
41
+ },
42
+ },
43
+
44
+ itemInstance: {
45
+ getProps: ({ item, prev }) => {
46
+ const dataRef = item.getDataRef<PropMemoizationDataRef>();
47
+ const props = prev?.() ?? {};
48
+ return memoize(props, dataRef.current);
49
+ },
50
+ },
51
+ };
@@ -0,0 +1,68 @@
1
+ import { describe, expect, it, vi } from "vitest";
2
+ import { TestTree } from "../../test-utils/test-tree";
3
+ import { propMemoizationFeature } from "./feature";
4
+ import { FeatureImplementation } from "../../types/core";
5
+
6
+ const itemHandler = vi.fn();
7
+ const treeHandler = vi.fn();
8
+ const createItemValue = vi.fn();
9
+ const createTreeValue = vi.fn();
10
+
11
+ const customFeature: FeatureImplementation = {
12
+ itemInstance: {
13
+ getProps: ({ prev }) => ({
14
+ ...prev?.(),
15
+ customValue: createItemValue(),
16
+ onCustomEvent: () => itemHandler(),
17
+ }),
18
+ },
19
+ treeInstance: {
20
+ getContainerProps: ({ prev }) => ({
21
+ ...prev?.(),
22
+ customValue: createTreeValue(),
23
+ onCustomEvent: () => treeHandler(),
24
+ }),
25
+ },
26
+ };
27
+
28
+ const factory = TestTree.default({}).withFeatures(
29
+ customFeature,
30
+ propMemoizationFeature,
31
+ );
32
+
33
+ describe("core-feature/prop-memoization", () => {
34
+ it("memoizes props", async () => {
35
+ const tree = await factory.suits.sync().tree.createTestCaseTree();
36
+ createTreeValue.mockReturnValue(123);
37
+ expect(tree.instance.getContainerProps().onCustomEvent).toBe(
38
+ tree.instance.getContainerProps().onCustomEvent,
39
+ );
40
+ expect(tree.instance.getContainerProps().customValue).toBe(123);
41
+ expect(tree.instance.getContainerProps().customValue).toBe(123);
42
+ });
43
+ factory.forSuits((tree) => {
44
+ describe("tree props", () => {
45
+ it("memoizes props", async () => {
46
+ createTreeValue.mockReturnValue(123);
47
+ expect(tree.instance.getContainerProps().onCustomEvent).toBe(
48
+ tree.instance.getContainerProps().onCustomEvent,
49
+ );
50
+ expect(tree.instance.getContainerProps().customValue).toBe(123);
51
+ expect(tree.instance.getContainerProps().customValue).toBe(123);
52
+ });
53
+
54
+ it("doesnt return stale values", async () => {
55
+ createTreeValue.mockReturnValueOnce(123);
56
+ createTreeValue.mockReturnValueOnce(456);
57
+ expect(tree.instance.getContainerProps().customValue).toBe(123);
58
+ expect(tree.instance.getContainerProps().customValue).toBe(456);
59
+ });
60
+
61
+ it("propagates calls properly", async () => {
62
+ tree.instance.getContainerProps().onCustomEvent();
63
+ tree.instance.getContainerProps().onCustomEvent();
64
+ expect(treeHandler).toHaveBeenCalledTimes(2);
65
+ });
66
+ });
67
+ });
68
+ });
@@ -0,0 +1,11 @@
1
+ export type PropMemoizationDataRef = {
2
+ memoizedProps?: Record<string, any>;
3
+ };
4
+
5
+ export type PropMemoizationFeatureDef = {
6
+ state: {};
7
+ config: {};
8
+ treeInstance: {};
9
+ itemInstance: {};
10
+ hotkeys: never;
11
+ };
@@ -1,14 +1,7 @@
1
1
  import { FeatureImplementation, ItemInstance } from "../../types/core";
2
- import { RenamingFeatureDef } from "./types";
3
- import { MainFeatureDef } from "../main/types";
4
- import { TreeFeatureDef } from "../tree/types";
5
2
  import { makeStateUpdater } from "../../utils";
6
3
 
7
- export const renamingFeature: FeatureImplementation<
8
- any,
9
- RenamingFeatureDef<any>,
10
- MainFeatureDef | TreeFeatureDef<any> | RenamingFeatureDef<any>
11
- > = {
4
+ export const renamingFeature: FeatureImplementation = {
12
5
  key: "renaming",
13
6
 
14
7
  getDefaultConfig: (defaultConfig, tree) => ({
@@ -23,64 +16,63 @@ export const renamingFeature: FeatureImplementation<
23
16
  renamingValue: "setRenamingValue",
24
17
  },
25
18
 
26
- createTreeInstance: (prev, instance) => ({
27
- ...prev,
28
-
29
- startRenamingItem: (itemId) => {
30
- const item = instance.getItemInstance(itemId);
31
-
32
- if (!item.canRename()) {
33
- return;
34
- }
35
-
36
- instance.applySubStateUpdate("renamingItem", itemId);
37
- instance.applySubStateUpdate("renamingValue", item.getItemName());
38
- },
39
-
40
- getRenamingItem: () => {
41
- const itemId = instance.getState().renamingItem;
42
- return itemId ? instance.getItemInstance(itemId) : null;
19
+ treeInstance: {
20
+ getRenamingItem: ({ tree }) => {
21
+ const itemId = tree.getState().renamingItem;
22
+ return itemId ? tree.getItemInstance(itemId) : null;
43
23
  },
44
24
 
45
- getRenamingValue: () => instance.getState().renamingValue || "",
25
+ getRenamingValue: ({ tree }) => tree.getState().renamingValue || "",
46
26
 
47
- abortRenaming: () => {
48
- instance.applySubStateUpdate("renamingItem", null);
27
+ abortRenaming: ({ tree }) => {
28
+ tree.applySubStateUpdate("renamingItem", null);
29
+ tree.updateDomFocus();
49
30
  },
50
31
 
51
- completeRenaming: () => {
52
- const config = instance.getConfig();
53
- const item = instance.getRenamingItem();
32
+ completeRenaming: ({ tree }) => {
33
+ const config = tree.getConfig();
34
+ const item = tree.getRenamingItem();
54
35
  if (item) {
55
- config.onRename?.(item, instance.getState().renamingValue || "");
36
+ config.onRename?.(item, tree.getState().renamingValue || "");
56
37
  }
57
- instance.applySubStateUpdate("renamingItem", null);
38
+ tree.applySubStateUpdate("renamingItem", null);
39
+ tree.updateDomFocus();
58
40
  },
59
41
 
60
- isRenamingItem: () => !!instance.getState().renamingItem,
61
- }),
42
+ isRenamingItem: ({ tree }) => !!tree.getState().renamingItem,
43
+ },
44
+
45
+ itemInstance: {
46
+ startRenaming: ({ tree, item, itemId }) => {
47
+ if (!item.canRename()) {
48
+ return;
49
+ }
62
50
 
63
- createItemInstance: (prev, instance, tree) => ({
64
- ...prev,
65
- getRenameInputProps: () => ({
51
+ tree.applySubStateUpdate("renamingItem", itemId);
52
+ tree.applySubStateUpdate("renamingValue", item.getItemName());
53
+ },
54
+
55
+ getRenameInputProps: ({ tree }) => ({
66
56
  onBlur: () => tree.abortRenaming(),
67
57
  value: tree.getRenamingValue(),
68
- onChange: (e) => {
69
- tree.applySubStateUpdate("renamingValue", e.target.value);
58
+ onChange: (e: any) => {
59
+ // TODO custom type with e.target.value
60
+ tree.applySubStateUpdate("renamingValue", e.target?.value);
70
61
  },
71
62
  }),
72
63
 
73
- canRename: () =>
74
- tree.getConfig().canRename?.(instance as ItemInstance<any>) ?? true,
64
+ canRename: ({ tree, item }) =>
65
+ tree.getConfig().canRename?.(item as ItemInstance<any>) ?? true,
75
66
 
76
- isRenaming: () => instance.getId() === tree.getState().renamingItem,
77
- }),
67
+ isRenaming: ({ tree, item }) =>
68
+ item.getId() === tree.getState().renamingItem,
69
+ },
78
70
 
79
71
  hotkeys: {
80
72
  renameItem: {
81
73
  hotkey: "F2",
82
74
  handler: (e, tree) => {
83
- tree.startRenamingItem(tree.getFocusedItem().getId());
75
+ tree.getFocusedItem().startRenaming();
84
76
  },
85
77
  },
86
78
 
@@ -0,0 +1,127 @@
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
+ import { propMemoizationFeature } from "../prop-memoization/feature";
6
+
7
+ const factory = TestTree.default({}).withFeatures(
8
+ renamingFeature,
9
+ selectionFeature,
10
+ propMemoizationFeature,
11
+ );
12
+
13
+ describe("core-feature/renaming", () => {
14
+ factory.forSuits((tree) => {
15
+ it("starts and aborts renaming", () => {
16
+ tree.item("x1").startRenaming();
17
+ expect(tree.instance.isRenamingItem()).toBe(true);
18
+ expect(tree.instance.getRenamingValue()).toBe("x1");
19
+ tree.instance.abortRenaming();
20
+ expect(tree.instance.isRenamingItem()).toBe(false);
21
+ });
22
+
23
+ it("stops renaming by blurring", () => {
24
+ tree.item("x1").startRenaming();
25
+ tree.instance.getRenamingItem()!.getRenameInputProps().onBlur();
26
+ expect(tree.instance.isRenamingItem()).toBe(false);
27
+ });
28
+
29
+ it("completes renaming programmatically", () => {
30
+ const onRename = tree.mockedHandler("onRename");
31
+
32
+ tree.item("x1").startRenaming();
33
+ expect(tree.instance.getRenamingItem()!.getRenameInputProps().value).toBe(
34
+ "x1",
35
+ );
36
+ tree.instance
37
+ .getRenamingItem()!
38
+ .getRenameInputProps()
39
+ .onChange({
40
+ target: { value: "renamed" },
41
+ });
42
+ expect(tree.instance.getRenamingItem()!.getRenameInputProps().value).toBe(
43
+ "renamed",
44
+ );
45
+ tree.instance.completeRenaming();
46
+ expect(onRename).toHaveBeenCalledWith(
47
+ tree.instance.getItemInstance("x1"),
48
+ "renamed",
49
+ );
50
+ expect(tree.instance.isRenamingItem()).toBe(false);
51
+ });
52
+
53
+ it("invokes state setters when aborting", () => {
54
+ const setRenamingItem = tree.mockedHandler("setRenamingItem");
55
+ const setRenamingValue = tree.mockedHandler("setRenamingValue");
56
+ tree.item("x1").startRenaming();
57
+ expect(setRenamingItem).toHaveBeenCalledWith("x1");
58
+ expect(setRenamingValue).toHaveBeenCalledWith("x1");
59
+ tree.instance.abortRenaming();
60
+ expect(setRenamingItem).toHaveBeenCalledWith(null);
61
+ });
62
+
63
+ it("invokes state setters when completing", () => {
64
+ const setRenamingItem = tree.mockedHandler("setRenamingItem");
65
+ const setRenamingValue = tree.mockedHandler("setRenamingValue");
66
+ tree.item("x1").startRenaming();
67
+ tree.instance
68
+ .getRenamingItem()!
69
+ .getRenameInputProps()
70
+ .onChange({
71
+ target: { value: "renamed" },
72
+ });
73
+ expect(setRenamingItem).toHaveBeenCalledWith("x1");
74
+ expect(setRenamingValue).toHaveBeenCalledWith("renamed");
75
+ tree.instance.completeRenaming();
76
+ expect(setRenamingItem).toHaveBeenCalledWith(null);
77
+ });
78
+
79
+ it("changes renaming input content with input props", () => {
80
+ const setRenamingValue = tree.mockedHandler("setRenamingValue");
81
+ tree.item("x1").startRenaming();
82
+ tree.instance
83
+ .getRenamingItem()!
84
+ .getRenameInputProps()
85
+ .onChange({ target: { value: "New Name" } });
86
+ expect(setRenamingValue).toHaveBeenCalledWith("New Name");
87
+ });
88
+
89
+ it("aborts renaming with input props", () => {
90
+ const setRenamingItem = tree.mockedHandler("setRenamingItem");
91
+ tree.item("x1").startRenaming();
92
+ tree.instance.getRenamingItem()!.getRenameInputProps().onBlur();
93
+ expect(setRenamingItem).toHaveBeenCalledWith(null);
94
+ });
95
+
96
+ describe("hotkeys", () => {
97
+ it("starts renaming", () => {
98
+ const setRenamingItem = tree.mockedHandler("setRenamingItem");
99
+ tree.do.hotkey("renameItem");
100
+ expect(setRenamingItem).toHaveBeenCalledWith("x1");
101
+ });
102
+
103
+ it("aborts renaming with Escape key", () => {
104
+ const setRenamingItem = tree.mockedHandler("setRenamingItem");
105
+ tree.item("x1").startRenaming();
106
+ tree.do.hotkey("abortRenaming");
107
+ expect(setRenamingItem).toHaveBeenCalledWith(null);
108
+ });
109
+
110
+ it("completes renaming with Enter key", () => {
111
+ const onRename = tree.mockedHandler("onRename");
112
+ tree.item("x1").startRenaming();
113
+ tree.instance
114
+ .getRenamingItem()!
115
+ .getRenameInputProps()
116
+ .onChange({
117
+ target: { value: "renamed" },
118
+ });
119
+ tree.do.hotkey("completeRenaming");
120
+ expect(onRename).toHaveBeenCalledWith(
121
+ tree.instance.getItemInstance("x1"),
122
+ "renamed",
123
+ );
124
+ });
125
+ });
126
+ });
127
+ });
@@ -6,13 +6,12 @@ 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;
13
13
  };
14
14
  treeInstance: {
15
- startRenamingItem: (itemId: string) => void;
16
15
  getRenamingItem: () => ItemInstance<T> | null;
17
16
  getRenamingValue: () => string;
18
17
  abortRenaming: () => void;
@@ -23,6 +22,7 @@ export type RenamingFeatureDef<T> = {
23
22
  getRenameInputProps: () => any;
24
23
  canRename: () => boolean;
25
24
  isRenaming: () => boolean;
25
+ startRenaming: () => void;
26
26
  };
27
27
  hotkeys: "renameItem" | "abortRenaming" | "completeRenaming";
28
28
  };