@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
@@ -1,14 +1,8 @@
1
1
  import { FeatureImplementation } from "../../types/core";
2
- import { SearchFeatureDataRef, SearchFeatureDef } from "./types";
3
- import { MainFeatureDef } from "../main/types";
4
- import { TreeFeatureDef } from "../tree/types";
2
+ import { SearchFeatureDataRef } from "./types";
5
3
  import { makeStateUpdater, memo } from "../../utils";
6
4
 
7
- export const searchFeature: FeatureImplementation<
8
- any,
9
- SearchFeatureDef<any>,
10
- MainFeatureDef | TreeFeatureDef<any> | SearchFeatureDef<any>
11
- > = {
5
+ export const searchFeature: FeatureImplementation = {
12
6
  key: "search",
13
7
 
14
8
  getInitialState: (initialState) => ({
@@ -28,65 +22,61 @@ export const searchFeature: FeatureImplementation<
28
22
  search: "setSearch",
29
23
  },
30
24
 
31
- createTreeInstance: (prev, instance) => ({
32
- ...prev,
33
-
34
- setSearch: (search) => {
35
- instance.applySubStateUpdate("search", search);
36
- instance
25
+ treeInstance: {
26
+ setSearch: ({ tree }, search) => {
27
+ tree.applySubStateUpdate("search", search);
28
+ tree
37
29
  .getItems()
38
30
  .find((item) =>
39
- instance
40
- .getConfig()
41
- .isSearchMatchingItem?.(instance.getSearchValue(), item),
31
+ tree.getConfig().isSearchMatchingItem?.(tree.getSearchValue(), item),
42
32
  )
43
33
  ?.setFocused();
44
34
  },
45
- openSearch: (initialValue = "") => {
46
- instance.setSearch(initialValue);
35
+ openSearch: ({ tree }, initialValue = "") => {
36
+ tree.setSearch(initialValue);
37
+ tree.getConfig().onOpenSearch?.();
47
38
  setTimeout(() => {
48
- instance
49
- .getDataRef<SearchFeatureDataRef>()
50
- .current.searchInput?.focus();
39
+ tree.getDataRef<SearchFeatureDataRef>().current.searchInput?.focus();
51
40
  });
52
41
  },
53
- closeSearch: () => {
54
- instance.setSearch(null);
55
- instance.updateDomFocus();
42
+ closeSearch: ({ tree }) => {
43
+ tree.setSearch(null);
44
+ tree.getConfig().onCloseSearch?.();
45
+ tree.updateDomFocus();
56
46
  },
57
- isSearchOpen: () => instance.getState().search !== null,
58
- getSearchValue: () => instance.getState().search || "",
59
- registerSearchInputElement: (element) => {
60
- const dataRef = instance.getDataRef<SearchFeatureDataRef>();
47
+ isSearchOpen: ({ tree }) => tree.getState().search !== null,
48
+ getSearchValue: ({ tree }) => tree.getState().search || "",
49
+ registerSearchInputElement: ({ tree }, element) => {
50
+ const dataRef = tree.getDataRef<SearchFeatureDataRef>();
61
51
  dataRef.current.searchInput = element;
62
52
  if (element && dataRef.current.keydownHandler) {
63
53
  element.addEventListener("keydown", dataRef.current.keydownHandler);
64
54
  }
65
55
  },
66
- getSearchInputElement: () =>
67
- instance.getDataRef<SearchFeatureDataRef>().current.searchInput ?? null,
56
+ getSearchInputElement: ({ tree }) =>
57
+ tree.getDataRef<SearchFeatureDataRef>().current.searchInput ?? null,
68
58
 
69
- getSearchInputElementProps: () => ({
70
- value: instance.getSearchValue(),
71
- onChange: (e: any) => instance.setSearch(e.target.value),
72
- onBlur: () => instance.closeSearch(),
59
+ getSearchInputElementProps: ({ tree }) => ({
60
+ value: tree.getSearchValue(),
61
+ onChange: (e: any) => tree.setSearch(e.target.value),
62
+ onBlur: () => tree.closeSearch(),
73
63
  }),
74
64
 
75
65
  getSearchMatchingItems: memo(
76
- (search, items) =>
77
- items.filter(
78
- (item) =>
79
- search && instance.getConfig().isSearchMatchingItem?.(search, item),
80
- ),
81
- () => [instance.getSearchValue(), instance.getItems()],
66
+ ({ tree }) => [
67
+ tree.getSearchValue(),
68
+ tree.getItems(),
69
+ tree.getConfig().isSearchMatchingItem,
70
+ ],
71
+ (search, items, isSearchMatchingItem) =>
72
+ items.filter((item) => search && isSearchMatchingItem?.(search, item)),
82
73
  ),
83
- }),
74
+ },
84
75
 
85
- createItemInstance: (prev, item, tree) => ({
86
- ...prev,
87
- isMatchingSearch: () =>
76
+ itemInstance: {
77
+ isMatchingSearch: ({ tree, item }) =>
88
78
  tree.getSearchMatchingItems().some((i) => i.getId() === item.getId()),
89
- }),
79
+ },
90
80
 
91
81
  hotkeys: {
92
82
  openSearch: {
@@ -0,0 +1,117 @@
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
+ import { propMemoizationFeature } from "../prop-memoization/feature";
6
+
7
+ const factory = TestTree.default({}).withFeatures(
8
+ searchFeature,
9
+ selectionFeature,
10
+ propMemoizationFeature,
11
+ );
12
+
13
+ describe("core-feature/search", () => {
14
+ factory.forSuits((tree) => {
15
+ it("opens and closes search", () => {
16
+ tree.instance.openSearch();
17
+ expect(tree.instance.isSearchOpen()).toBe(true);
18
+ expect(tree.instance.getSearchValue()).toBe("");
19
+ tree.instance.closeSearch();
20
+ expect(tree.instance.isSearchOpen()).toBe(false);
21
+ });
22
+
23
+ it("opens and closes search with initial value", () => {
24
+ tree.instance.openSearch("test");
25
+ expect(tree.instance.isSearchOpen()).toBe(true);
26
+ expect(tree.instance.getSearchValue()).toBe("test");
27
+ tree.instance.closeSearch();
28
+ expect(tree.instance.isSearchOpen()).toBe(false);
29
+ });
30
+
31
+ it("invokes open and close handlers", () => {
32
+ const onOpenSearch = tree.mockedHandler("onOpenSearch");
33
+ const onCloseSearch = tree.mockedHandler("onCloseSearch");
34
+ tree.instance.openSearch("test");
35
+ expect(onOpenSearch).toHaveBeenCalled();
36
+ tree.instance.closeSearch();
37
+ expect(onCloseSearch).toHaveBeenCalled();
38
+ });
39
+
40
+ it("invokes state setter", () => {
41
+ const setSearch = tree.mockedHandler("setSearch");
42
+ tree.instance.openSearch("test");
43
+ expect(setSearch).toHaveBeenCalledWith("test");
44
+ tree.instance.closeSearch();
45
+ expect(setSearch).toHaveBeenCalledWith(null);
46
+ });
47
+
48
+ it("can search and return matches", () => {
49
+ tree.instance.setSearch("12");
50
+ expect(tree.instance.getSearchMatchingItems().length).toBe(2);
51
+ expect(tree.instance.getSearchMatchingItems()[0].getId()).toBe("x112");
52
+ expect(tree.instance.getSearchMatchingItems()[1].getId()).toBe("x12");
53
+ });
54
+
55
+ it("uses isSearchMatchingItem handler", () => {
56
+ const isSearchMatchingItem = tree.mockedHandler("isSearchMatchingItem");
57
+ isSearchMatchingItem.mockImplementation(
58
+ (_, item) => item.getId() === "x3",
59
+ );
60
+ tree.instance.setSearch("xyz");
61
+ expect(tree.instance.getSearchMatchingItems().length).toBe(1);
62
+ expect(tree.instance.getSearchMatchingItems()[0].getId()).toBe("x3");
63
+ });
64
+
65
+ it("changes search input contentwith input props", () => {
66
+ const setSearch = tree.mockedHandler("setSearch");
67
+ tree.instance
68
+ .getSearchInputElementProps()
69
+ .onChange({ target: { value: "test" } });
70
+ expect(setSearch).toHaveBeenCalledWith("test");
71
+ });
72
+
73
+ it("closes search input with input props", () => {
74
+ const onCloseSearch = tree.mockedHandler("onCloseSearch");
75
+ tree.instance.openSearch();
76
+ tree.instance.getSearchInputElementProps().onBlur();
77
+ expect(onCloseSearch).toHaveBeenCalled();
78
+ });
79
+
80
+ describe("hotkeys", () => {
81
+ it("opens search", () => {
82
+ const setSearch = tree.mockedHandler("setSearch");
83
+ tree.do.hotkey("openSearch");
84
+ expect(setSearch).toHaveBeenCalledWith("");
85
+ });
86
+
87
+ it("selects focused item when search is submitted", () => {
88
+ const setSelectedItems = tree.mockedHandler("setSelectedItems");
89
+ const onCloseSearch = tree.mockedHandler("onCloseSearch");
90
+ tree.item("x1").setFocused();
91
+ tree.do.hotkey("openSearch");
92
+ tree.do.hotkey("submitSearch");
93
+ expect(setSelectedItems).toHaveBeenCalledWith(["x1"]);
94
+ expect(onCloseSearch).toHaveBeenCalled();
95
+ });
96
+
97
+ it("scrolls through searched items", () => {
98
+ const setFocusedItem = tree.mockedHandler("setFocusedItem");
99
+ tree.instance.setSearch("12");
100
+ tree.do.hotkey("nextSearchItem");
101
+ expect(setFocusedItem).toHaveBeenCalledWith("x112");
102
+
103
+ tree.do.hotkey("nextSearchItem");
104
+ expect(setFocusedItem).toHaveBeenCalledWith("x12");
105
+
106
+ tree.do.hotkey("nextSearchItem");
107
+ expect(setFocusedItem).toHaveBeenCalledTimes(2);
108
+
109
+ tree.do.hotkey("previousSearchItem");
110
+ tree.do.hotkey("previousSearchItem");
111
+ tree.do.hotkey("previousSearchItem");
112
+ expect(setFocusedItem).toHaveBeenCalledTimes(3);
113
+ expect(setFocusedItem).nthCalledWith(3, "x112");
114
+ });
115
+ });
116
+ });
117
+ });
@@ -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: {
@@ -1,14 +1,7 @@
1
1
  import { FeatureImplementation } from "../../types/core";
2
- import { SelectionFeatureDef } 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 selectionFeature: FeatureImplementation<
8
- any,
9
- SelectionFeatureDef<any>,
10
- MainFeatureDef | TreeFeatureDef<any> | SelectionFeatureDef<any>
11
- > = {
4
+ export const selectionFeature: FeatureImplementation = {
12
5
  key: "selection",
13
6
 
14
7
  getInitialState: (initialState) => ({
@@ -25,44 +18,38 @@ export const selectionFeature: FeatureImplementation<
25
18
  selectedItems: "setSelectedItems",
26
19
  },
27
20
 
28
- createTreeInstance: (prev, instance) => ({
29
- ...prev,
30
-
31
- setSelectedItems: (selectedItems) => {
32
- instance.applySubStateUpdate("selectedItems", selectedItems);
21
+ treeInstance: {
22
+ setSelectedItems: ({ tree }, selectedItems) => {
23
+ tree.applySubStateUpdate("selectedItems", selectedItems);
33
24
  },
34
25
 
35
26
  // TODO memo
36
- getSelectedItems: () => {
37
- return instance.getState().selectedItems.map(instance.getItemInstance);
27
+ getSelectedItems: ({ tree }) => {
28
+ return tree.getState().selectedItems.map(tree.getItemInstance);
38
29
  },
39
- }),
40
-
41
- createItemInstance: (prev, item, tree) => ({
42
- ...prev,
30
+ },
43
31
 
44
- select: () => {
32
+ itemInstance: {
33
+ select: ({ tree, itemId }) => {
45
34
  const { selectedItems } = tree.getState();
46
35
  tree.setSelectedItems(
47
- selectedItems.includes(item.getItemMeta().itemId)
36
+ selectedItems.includes(itemId)
48
37
  ? selectedItems
49
- : [...selectedItems, item.getItemMeta().itemId],
38
+ : [...selectedItems, itemId],
50
39
  );
51
40
  },
52
41
 
53
- deselect: () => {
42
+ deselect: ({ tree, itemId }) => {
54
43
  const { selectedItems } = tree.getState();
55
- tree.setSelectedItems(
56
- selectedItems.filter((id) => id !== item.getItemMeta().itemId),
57
- );
44
+ tree.setSelectedItems(selectedItems.filter((id) => id !== itemId));
58
45
  },
59
46
 
60
- isSelected: () => {
47
+ isSelected: ({ tree, item }) => {
61
48
  const { selectedItems } = tree.getState();
62
49
  return selectedItems.includes(item.getItemMeta().itemId);
63
50
  },
64
51
 
65
- selectUpTo: (ctrl: boolean) => {
52
+ selectUpTo: ({ tree, item }, ctrl: boolean) => {
66
53
  const indexA = item.getItemMeta().index;
67
54
  // TODO dont use focused item as anchor, but last primary-clicked item
68
55
  const indexB = tree.getFocusedItem().getItemMeta().index;
@@ -84,7 +71,7 @@ export const selectionFeature: FeatureImplementation<
84
71
  tree.setSelectedItems(uniqueSelectedItems);
85
72
  },
86
73
 
87
- toggleSelect: () => {
74
+ toggleSelect: ({ item }) => {
88
75
  if (item.isSelected()) {
89
76
  item.deselect();
90
77
  } else {
@@ -92,10 +79,10 @@ export const selectionFeature: FeatureImplementation<
92
79
  }
93
80
  },
94
81
 
95
- getProps: () => ({
96
- ...prev.getProps(),
82
+ getProps: ({ tree, item, prev }) => ({
83
+ ...prev?.(),
97
84
  "aria-selected": item.isSelected() ? "true" : "false",
98
- onClick: item.getMemoizedProp("selection/onClick", () => (e) => {
85
+ onClick: (e: MouseEvent) => {
99
86
  if (e.shiftKey) {
100
87
  item.selectUpTo(e.ctrlKey || e.metaKey);
101
88
  } else if (e.ctrlKey || e.metaKey) {
@@ -104,10 +91,10 @@ export const selectionFeature: FeatureImplementation<
104
91
  tree.setSelectedItems([item.getItemMeta().itemId]);
105
92
  }
106
93
 
107
- prev.getProps().onClick?.(e);
108
- }),
94
+ prev?.()?.onClick?.(e);
95
+ },
109
96
  }),
110
- }),
97
+ },
111
98
 
112
99
  hotkeys: {
113
100
  // setSelectedItem: {
@@ -123,27 +110,37 @@ export const selectionFeature: FeatureImplementation<
123
110
  },
124
111
  },
125
112
  selectUpwards: {
126
- hotkey: "shift+ArrowUp",
127
- handler: () => {
128
- // TODO
113
+ hotkey: "Shift+ArrowUp",
114
+ handler: (e, tree) => {
115
+ const focused = tree.getFocusedItem();
116
+ const above = focused.getItemAbove();
117
+ if (!above) return;
118
+
119
+ if (focused.isSelected() && above.isSelected()) {
120
+ focused.deselect();
121
+ } else {
122
+ above.select();
123
+ }
124
+
125
+ above.setFocused();
126
+ tree.updateDomFocus();
129
127
  },
130
128
  },
131
129
  selectDownwards: {
132
- hotkey: "shift+ArrowDown",
133
- handler: () => {
134
- // TODO
135
- },
136
- },
137
- selectUpwardsCtrl: {
138
- hotkey: "shift+ctrl+ArrowUp",
139
- handler: () => {
140
- // TODO
141
- },
142
- },
143
- selectDownwardsCtrl: {
144
- hotkey: "shift+ctrl+ArrowUp",
145
- handler: () => {
146
- // TODO
130
+ hotkey: "Shift+ArrowDown",
131
+ handler: (e, tree) => {
132
+ const focused = tree.getFocusedItem();
133
+ const below = focused.getItemBelow();
134
+ if (!below) return;
135
+
136
+ if (focused.isSelected() && below.isSelected()) {
137
+ focused.deselect();
138
+ } else {
139
+ below.select();
140
+ }
141
+
142
+ below.setFocused();
143
+ tree.updateDomFocus();
147
144
  },
148
145
  },
149
146
  selectAll: {
@@ -0,0 +1,219 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { TestTree } from "../../test-utils/test-tree";
3
+ import { selectionFeature } from "./feature";
4
+ import { propMemoizationFeature } from "../prop-memoization/feature";
5
+
6
+ const factory = TestTree.default({}).withFeatures(
7
+ selectionFeature,
8
+ propMemoizationFeature,
9
+ );
10
+
11
+ describe("core-feature/selections", () => {
12
+ it("test", async () => {
13
+ const tree = await factory.suits.proxifiedSync().tree.createTestCaseTree();
14
+ tree.do.selectItem("x111");
15
+ expect(
16
+ tree.instance.getItemInstance("x111").getProps()["aria-selected"],
17
+ ).toBe("true");
18
+ });
19
+
20
+ factory.forSuits((tree) => {
21
+ it("sets aria-selected to false", () => {
22
+ expect(
23
+ tree.instance.getItemInstance("x111").getProps()["aria-selected"],
24
+ ).toBe("false");
25
+ });
26
+
27
+ it("sets aria-selected to true", () => {
28
+ tree.do.selectItem("x111");
29
+ expect(
30
+ tree.instance.getItemInstance("x111").getProps()["aria-selected"],
31
+ ).toBe("true");
32
+ });
33
+
34
+ it("resets old selection after new select", () => {
35
+ tree.do.selectItem("x111");
36
+ tree.do.selectItem("x112");
37
+ expect(
38
+ tree.instance.getItemInstance("x111").getProps()["aria-selected"],
39
+ ).toBe("false");
40
+ expect(
41
+ tree.instance.getItemInstance("x112").getProps()["aria-selected"],
42
+ ).toBe("true");
43
+ expect(tree.instance.getItemInstance("x111").isSelected()).toBe(false);
44
+ expect(tree.instance.getItemInstance("x112").isSelected()).toBe(true);
45
+ });
46
+
47
+ describe("select and control select", () => {
48
+ it("should make isSelected true", () => {
49
+ tree.do.selectItem("x111");
50
+ tree.do.ctrlSelectItem("x112");
51
+ tree.do.ctrlSelectItem("x113");
52
+
53
+ expect(tree.instance.getItemInstance("x111").isSelected()).toBe(true);
54
+ expect(tree.instance.getItemInstance("x112").isSelected()).toBe(true);
55
+ expect(tree.instance.getItemInstance("x113").isSelected()).toBe(true);
56
+ });
57
+
58
+ it("should call individual state setters", () => {
59
+ const setSelectedItems = tree.mockedHandler("setSelectedItems");
60
+ const setFocusedItem = tree.mockedHandler("setFocusedItem");
61
+
62
+ tree.do.selectItem("x111");
63
+ tree.do.ctrlSelectItem("x112");
64
+ tree.do.ctrlSelectItem("x113");
65
+
66
+ expect(setSelectedItems).toHaveBeenCalledWith(["x111", "x112", "x113"]);
67
+ expect(setFocusedItem).toHaveBeenCalledWith("x113");
68
+ });
69
+
70
+ it("should call joint state setter", () => {
71
+ const setState = tree.mockedHandler("setState");
72
+
73
+ tree.do.selectItem("x111");
74
+ tree.do.ctrlSelectItem("x112");
75
+ tree.do.ctrlSelectItem("x113");
76
+
77
+ expect(setState).toHaveBeenCalledWith(
78
+ expect.objectContaining({
79
+ selectedItems: ["x111", "x112", "x113"],
80
+ focusedItem: "x113",
81
+ }),
82
+ );
83
+ });
84
+ });
85
+
86
+ describe("shift select", () => {
87
+ it("should make isSelected true", () => {
88
+ tree.do.selectItem("x111");
89
+ tree.do.shiftSelectItem("x113");
90
+ tree.do.ctrlSelectItem("x2");
91
+
92
+ expect(tree.instance.getItemInstance("x111").isSelected()).toBe(true);
93
+ expect(tree.instance.getItemInstance("x112").isSelected()).toBe(true);
94
+ expect(tree.instance.getItemInstance("x113").isSelected()).toBe(true);
95
+ expect(tree.instance.getItemInstance("x2").isSelected()).toBe(true);
96
+ });
97
+
98
+ it("should call individual state setters", () => {
99
+ const setSelectedItems = tree.mockedHandler("setSelectedItems");
100
+ const setFocusedItem = tree.mockedHandler("setFocusedItem");
101
+
102
+ tree.do.selectItem("x111");
103
+ tree.do.shiftSelectItem("x113");
104
+ expect(setFocusedItem).toHaveBeenCalledWith("x113");
105
+
106
+ tree.do.ctrlSelectItem("x2");
107
+
108
+ expect(setSelectedItems).toHaveBeenCalledWith([
109
+ "x111",
110
+ "x112",
111
+ "x113",
112
+ "x2",
113
+ ]);
114
+
115
+ expect(setFocusedItem).toHaveBeenCalledWith("x2");
116
+ });
117
+
118
+ it("should call joint state setter", () => {
119
+ const setState = tree.mockedHandler("setState");
120
+
121
+ tree.do.selectItem("x111");
122
+ tree.do.shiftSelectItem("x113");
123
+ expect(setState).toHaveBeenCalledWith(
124
+ expect.objectContaining({
125
+ selectedItems: ["x111", "x112", "x113"],
126
+ focusedItem: "x113",
127
+ }),
128
+ );
129
+
130
+ tree.do.ctrlSelectItem("x2");
131
+
132
+ expect(setState).toHaveBeenCalledWith(
133
+ expect.objectContaining({
134
+ selectedItems: ["x111", "x112", "x113", "x2"],
135
+ focusedItem: "x2",
136
+ }),
137
+ );
138
+ });
139
+ });
140
+
141
+ describe("programmatic select", () => {
142
+ describe("item instance actions", () => {
143
+ it("should handle select", () => {
144
+ const setSelectedItems = tree.mockedHandler("setSelectedItems");
145
+ tree.instance.getItemInstance("x111").select();
146
+ expect(setSelectedItems).toHaveBeenCalledWith(["x111"]);
147
+ expect(tree.instance.getItemInstance("x111").isSelected()).toBe(true);
148
+ });
149
+
150
+ it("should handle deselect", () => {
151
+ const setSelectedItems = tree.mockedHandler("setSelectedItems");
152
+ tree.instance.getItemInstance("x111").select();
153
+ tree.instance.getItemInstance("x111").deselect();
154
+ expect(setSelectedItems).toHaveBeenCalledWith([]);
155
+ expect(tree.instance.getItemInstance("x111").isSelected()).toBe(
156
+ false,
157
+ );
158
+ });
159
+
160
+ it("should handle toggle select on", () => {
161
+ const setSelectedItems = tree.mockedHandler("setSelectedItems");
162
+ tree.instance.getItemInstance("x111").toggleSelect();
163
+ expect(setSelectedItems).toHaveBeenCalledWith(["x111"]);
164
+ expect(tree.instance.getItemInstance("x111").isSelected()).toBe(true);
165
+ });
166
+
167
+ it("should handle toggle select off", () => {
168
+ const setSelectedItems = tree.mockedHandler("setSelectedItems");
169
+ tree.instance.getItemInstance("x111").select();
170
+ tree.instance.getItemInstance("x111").toggleSelect();
171
+ expect(setSelectedItems).toHaveBeenCalledWith([]);
172
+ expect(tree.instance.getItemInstance("x111").isSelected()).toBe(
173
+ false,
174
+ );
175
+ });
176
+
177
+ it("should handle selectUpTo without ctrl", () => {
178
+ const setSelectedItems = tree.mockedHandler("setSelectedItems");
179
+ tree.instance.getItemInstance("x111").toggleSelect();
180
+ tree.instance.getItemInstance("x112").toggleSelect();
181
+ tree.instance.getItemInstance("x112").setFocused();
182
+ tree.instance.getItemInstance("x114").selectUpTo(false);
183
+ expect(setSelectedItems).toHaveBeenCalledWith([
184
+ "x112",
185
+ "x113",
186
+ "x114",
187
+ ]);
188
+ expect(tree.instance.getItemInstance("x112").isSelected()).toBe(true);
189
+ });
190
+
191
+ it("should handle selectUpTo with ctrl", () => {
192
+ const setSelectedItems = tree.mockedHandler("setSelectedItems");
193
+ tree.instance.getItemInstance("x111").toggleSelect();
194
+ tree.instance.getItemInstance("x112").toggleSelect();
195
+ tree.instance.getItemInstance("x112").setFocused();
196
+ tree.instance.getItemInstance("x114").selectUpTo(true);
197
+ expect(setSelectedItems).toHaveBeenCalledWith([
198
+ "x111",
199
+ "x112",
200
+ "x113",
201
+ "x114",
202
+ ]);
203
+ expect(tree.instance.getItemInstance("x112").isSelected()).toBe(true);
204
+ });
205
+ });
206
+
207
+ it("should handle getters", () => {
208
+ const setSelectedItems = tree.mockedHandler("setSelectedItems");
209
+ tree.instance.setSelectedItems(["x111", "x112"]);
210
+ expect(setSelectedItems).toHaveBeenCalledWith(["x111", "x112"]);
211
+ expect(tree.instance.getItemInstance("x111").isSelected()).toBe(true);
212
+ expect(tree.instance.getItemInstance("x112").isSelected()).toBe(true);
213
+ expect(tree.instance.getSelectedItems()[0].getId()).toEqual("x111");
214
+ expect(tree.instance.getSelectedItems()[1].getId()).toEqual("x112");
215
+ expect(tree.instance.getSelectedItems().length).toBe(2);
216
+ });
217
+ });
218
+ });
219
+ });
@@ -22,7 +22,5 @@ export type SelectionFeatureDef<T> = {
22
22
  | "toggleSelectItem"
23
23
  | "selectUpwards"
24
24
  | "selectDownwards"
25
- | "selectUpwardsCtrl"
26
- | "selectDownwardsCtrl"
27
25
  | "selectAll";
28
26
  };
@@ -1,13 +1,7 @@
1
1
  import { FeatureImplementation } from "../../types/core";
2
- import { SyncDataLoaderFeatureDef } from "./types";
3
- import { MainFeatureDef } from "../main/types";
4
2
  import { makeStateUpdater } from "../../utils";
5
3
 
6
- export const syncDataLoaderFeature: FeatureImplementation<
7
- any,
8
- SyncDataLoaderFeatureDef<any>,
9
- MainFeatureDef | SyncDataLoaderFeatureDef<any>
10
- > = {
4
+ export const syncDataLoaderFeature: FeatureImplementation = {
11
5
  key: "sync-data-loader",
12
6
 
13
7
  getInitialState: (initialState) => ({
@@ -24,18 +18,15 @@ export const syncDataLoaderFeature: FeatureImplementation<
24
18
  loadingItems: "setLoadingItems",
25
19
  },
26
20
 
27
- createTreeInstance: (prev, instance) => ({
28
- ...prev,
21
+ treeInstance: {
22
+ retrieveItemData: ({ tree }, itemId) =>
23
+ tree.getConfig().dataLoader!.getItem(itemId),
29
24
 
30
- retrieveItemData: (itemId) =>
31
- instance.getConfig().dataLoader!.getItem(itemId),
32
-
33
- retrieveChildrenIds: (itemId) =>
34
- instance.getConfig().dataLoader!.getChildren(itemId),
35
- }),
25
+ retrieveChildrenIds: ({ tree }, itemId) =>
26
+ tree.getConfig().dataLoader!.getChildren(itemId),
27
+ },
36
28
 
37
- createItemInstance: (prev) => ({
38
- ...prev,
29
+ itemInstance: {
39
30
  isLoading: () => false,
40
- }),
31
+ },
41
32
  };