@headless-tree/core 0.0.11 → 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 (108) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/lib/cjs/core/build-static-instance.js +1 -2
  3. package/lib/cjs/core/create-tree.js +7 -4
  4. package/lib/cjs/features/async-data-loader/feature.d.ts +1 -4
  5. package/lib/cjs/features/async-data-loader/feature.js +5 -7
  6. package/lib/cjs/features/async-data-loader/types.d.ts +2 -5
  7. package/lib/cjs/features/drag-and-drop/feature.d.ts +2 -3
  8. package/lib/cjs/features/drag-and-drop/feature.js +27 -24
  9. package/lib/cjs/features/drag-and-drop/types.d.ts +3 -3
  10. package/lib/cjs/features/drag-and-drop/utils.d.ts +1 -1
  11. package/lib/cjs/features/drag-and-drop/utils.js +4 -4
  12. package/lib/cjs/features/expand-all/feature.d.ts +1 -5
  13. package/lib/cjs/features/hotkeys-core/feature.d.ts +1 -3
  14. package/lib/cjs/features/prop-memoization/feature.d.ts +2 -0
  15. package/lib/cjs/features/prop-memoization/feature.js +48 -0
  16. package/lib/cjs/features/prop-memoization/types.d.ts +10 -0
  17. package/lib/cjs/features/prop-memoization/types.js +2 -0
  18. package/lib/cjs/features/renaming/feature.d.ts +1 -4
  19. package/lib/cjs/features/renaming/feature.js +8 -9
  20. package/lib/cjs/features/renaming/types.d.ts +1 -1
  21. package/lib/cjs/features/search/feature.d.ts +1 -4
  22. package/lib/cjs/features/selection/feature.d.ts +1 -4
  23. package/lib/cjs/features/selection/feature.js +35 -25
  24. package/lib/cjs/features/selection/types.d.ts +1 -1
  25. package/lib/cjs/features/sync-data-loader/feature.d.ts +1 -3
  26. package/lib/cjs/features/tree/feature.d.ts +1 -6
  27. package/lib/cjs/features/tree/feature.js +40 -57
  28. package/lib/cjs/features/tree/types.d.ts +0 -5
  29. package/lib/cjs/index.d.ts +2 -0
  30. package/lib/cjs/index.js +2 -0
  31. package/lib/cjs/mddocs-entry.d.ts +10 -0
  32. package/lib/cjs/test-utils/test-tree-do.d.ts +1 -1
  33. package/lib/cjs/test-utils/test-tree-expect.d.ts +1 -1
  34. package/lib/cjs/test-utils/test-tree-expect.js +1 -1
  35. package/lib/cjs/test-utils/test-tree.d.ts +1 -1
  36. package/lib/cjs/test-utils/test-tree.js +9 -1
  37. package/lib/cjs/types/core.d.ts +29 -30
  38. package/lib/esm/core/build-static-instance.js +1 -2
  39. package/lib/esm/core/create-tree.js +7 -4
  40. package/lib/esm/features/async-data-loader/feature.d.ts +1 -4
  41. package/lib/esm/features/async-data-loader/feature.js +5 -7
  42. package/lib/esm/features/async-data-loader/types.d.ts +2 -5
  43. package/lib/esm/features/drag-and-drop/feature.d.ts +2 -3
  44. package/lib/esm/features/drag-and-drop/feature.js +27 -24
  45. package/lib/esm/features/drag-and-drop/types.d.ts +3 -3
  46. package/lib/esm/features/drag-and-drop/utils.d.ts +1 -1
  47. package/lib/esm/features/drag-and-drop/utils.js +4 -4
  48. package/lib/esm/features/expand-all/feature.d.ts +1 -5
  49. package/lib/esm/features/hotkeys-core/feature.d.ts +1 -3
  50. package/lib/esm/features/prop-memoization/feature.d.ts +2 -0
  51. package/lib/esm/features/prop-memoization/feature.js +45 -0
  52. package/lib/esm/features/prop-memoization/types.d.ts +10 -0
  53. package/lib/esm/features/prop-memoization/types.js +1 -0
  54. package/lib/esm/features/renaming/feature.d.ts +1 -4
  55. package/lib/esm/features/renaming/feature.js +8 -9
  56. package/lib/esm/features/renaming/types.d.ts +1 -1
  57. package/lib/esm/features/search/feature.d.ts +1 -4
  58. package/lib/esm/features/selection/feature.d.ts +1 -4
  59. package/lib/esm/features/selection/feature.js +35 -25
  60. package/lib/esm/features/selection/types.d.ts +1 -1
  61. package/lib/esm/features/sync-data-loader/feature.d.ts +1 -3
  62. package/lib/esm/features/tree/feature.d.ts +1 -6
  63. package/lib/esm/features/tree/feature.js +40 -57
  64. package/lib/esm/features/tree/types.d.ts +0 -5
  65. package/lib/esm/index.d.ts +2 -0
  66. package/lib/esm/index.js +2 -0
  67. package/lib/esm/mddocs-entry.d.ts +10 -0
  68. package/lib/esm/test-utils/test-tree-do.d.ts +1 -1
  69. package/lib/esm/test-utils/test-tree-expect.d.ts +1 -1
  70. package/lib/esm/test-utils/test-tree-expect.js +1 -1
  71. package/lib/esm/test-utils/test-tree.d.ts +1 -1
  72. package/lib/esm/test-utils/test-tree.js +9 -1
  73. package/lib/esm/types/core.d.ts +29 -30
  74. package/package.json +1 -1
  75. package/src/core/build-proxified-instance.ts +5 -3
  76. package/src/core/build-static-instance.ts +1 -2
  77. package/src/core/core.spec.ts +210 -0
  78. package/src/core/create-tree.ts +13 -16
  79. package/src/features/async-data-loader/async-data-loader.spec.ts +12 -31
  80. package/src/features/async-data-loader/feature.ts +8 -20
  81. package/src/features/async-data-loader/types.ts +2 -6
  82. package/src/features/drag-and-drop/drag-and-drop.spec.ts +4 -3
  83. package/src/features/drag-and-drop/feature.ts +87 -86
  84. package/src/features/drag-and-drop/types.ts +4 -4
  85. package/src/features/drag-and-drop/utils.ts +4 -4
  86. package/src/features/expand-all/expand-all.spec.ts +5 -1
  87. package/src/features/expand-all/feature.ts +1 -12
  88. package/src/features/hotkeys-core/feature.ts +4 -13
  89. package/src/features/prop-memoization/feature.ts +51 -0
  90. package/src/features/prop-memoization/prop-memoization.spec.ts +68 -0
  91. package/src/features/prop-memoization/types.ts +11 -0
  92. package/src/features/renaming/feature.ts +11 -20
  93. package/src/features/renaming/renaming.spec.ts +11 -9
  94. package/src/features/renaming/types.ts +1 -1
  95. package/src/features/search/feature.ts +2 -8
  96. package/src/features/search/search.spec.ts +3 -1
  97. package/src/features/selection/feature.ts +45 -47
  98. package/src/features/selection/selection.spec.ts +13 -14
  99. package/src/features/selection/types.ts +0 -2
  100. package/src/features/sync-data-loader/feature.ts +1 -7
  101. package/src/features/tree/feature.ts +47 -85
  102. package/src/features/tree/tree.spec.ts +24 -64
  103. package/src/features/tree/types.ts +0 -6
  104. package/src/index.ts +2 -0
  105. package/src/mddocs-entry.ts +13 -0
  106. package/src/test-utils/test-tree-expect.ts +1 -1
  107. package/src/test-utils/test-tree.ts +11 -1
  108. package/src/types/core.ts +56 -147
@@ -1,21 +1,17 @@
1
- import { FeatureDefs, FeatureImplementation } from "../../types/core";
2
- import { DndDataRef, DragAndDropFeatureDef, DragLineData } from "./types";
1
+ import { FeatureImplementation } from "../../types/core";
2
+ import { DndDataRef, DragLineData } from "./types";
3
3
  import { canDrop, getDragCode, getDropTarget } from "./utils";
4
4
  import { makeStateUpdater } from "../../utils";
5
5
 
6
- export const dragAndDropFeature: FeatureImplementation<
7
- any,
8
- DragAndDropFeatureDef<any>,
9
- FeatureDefs<any>
10
- > = {
11
- key: "dragAndDrop",
6
+ export const dragAndDropFeature: FeatureImplementation = {
7
+ key: "drag-and-drop",
12
8
  deps: ["selection"],
13
9
 
14
10
  getDefaultConfig: (defaultConfig, tree) => ({
15
11
  canDrop: (_, target) => target.item.isFolder(),
16
12
  canDropForeignDragObject: () => false,
17
13
  setDndState: makeStateUpdater("dnd", tree),
18
- canDropInbetween: true,
14
+ canReorder: true,
19
15
  ...defaultConfig,
20
16
  }),
21
17
 
@@ -29,11 +25,12 @@ export const dragAndDropFeature: FeatureImplementation<
29
25
  },
30
26
 
31
27
  getDragLineData: ({ tree }): DragLineData | null => {
32
- // TODO doesnt work if scrolled down!
33
28
  const target = tree.getDropTarget();
34
- const indent = (target?.item.getItemMeta().level ?? 0) + 1; // TODO rename to indent
29
+ const indent = (target?.item.getItemMeta().level ?? 0) + 1;
35
30
 
36
- if (!target || target.childIndex === null) return null;
31
+ const treeBb = tree.getElement()?.getBoundingClientRect();
32
+
33
+ if (!target || !treeBb || target.childIndex === null) return null;
37
34
 
38
35
  const leftOffset = target.dragLineLevel * (tree.getConfig().indent ?? 1);
39
36
  const targetItem = tree.getItems()[target.dragLineIndex];
@@ -47,9 +44,9 @@ export const dragAndDropFeature: FeatureImplementation<
47
44
  if (bb) {
48
45
  return {
49
46
  indent,
50
- top: bb.bottom,
51
- left: bb.left + leftOffset,
52
- right: bb.right,
47
+ top: bb.bottom - treeBb.bottom,
48
+ left: bb.left + leftOffset - treeBb.left,
49
+ width: bb.width - leftOffset,
53
50
  };
54
51
  }
55
52
  }
@@ -59,9 +56,9 @@ export const dragAndDropFeature: FeatureImplementation<
59
56
  if (bb) {
60
57
  return {
61
58
  indent,
62
- top: bb.top,
63
- left: bb.left + leftOffset,
64
- right: bb.right,
59
+ top: bb.top - treeBb.top,
60
+ left: bb.left + leftOffset - treeBb.left,
61
+ width: bb.width - leftOffset,
65
62
  };
66
63
  }
67
64
 
@@ -74,90 +71,94 @@ export const dragAndDropFeature: FeatureImplementation<
74
71
  ? {
75
72
  top: `${dragLine.top + topOffset}px`,
76
73
  left: `${dragLine.left + leftOffset}px`,
77
- width: `${dragLine.right - dragLine.left - leftOffset}px`,
74
+ width: `${dragLine.width - leftOffset}px`,
78
75
  pointerEvents: "none", // important to prevent capturing drag events
79
76
  }
80
77
  : { display: "none" };
81
78
  },
79
+
80
+ getContainerProps: ({ prev }) => {
81
+ const prevProps = prev?.();
82
+ return {
83
+ ...prevProps,
84
+ style: {
85
+ ...prevProps?.style,
86
+ position: "relative",
87
+ },
88
+ };
89
+ },
82
90
  },
83
91
 
84
92
  itemInstance: {
85
- // TODO instead of individual getMemoizedProp calls, use a wrapped getMemoizedProps or something (getProps: () => getMemoized({...})
86
93
  getProps: ({ tree, item, prev }) => ({
87
94
  ...prev?.(),
88
95
 
89
96
  draggable: tree.getConfig().isItemDraggable?.(item) ?? true,
90
97
 
91
- onDragStart: item.getMemoizedProp(
92
- "dnd/onDragStart",
93
- () => (e: DragEvent) => {
94
- const selectedItems = tree.getSelectedItems();
95
- const items = selectedItems.includes(item) ? selectedItems : [item];
96
- const config = tree.getConfig();
98
+ onDragStart: (e: DragEvent) => {
99
+ const selectedItems = tree.getSelectedItems();
100
+ const items = selectedItems.includes(item) ? selectedItems : [item];
101
+ const config = tree.getConfig();
97
102
 
98
- if (!selectedItems.includes(item)) {
99
- tree.setSelectedItems([item.getItemMeta().itemId]);
100
- }
103
+ if (!selectedItems.includes(item)) {
104
+ tree.setSelectedItems([item.getItemMeta().itemId]);
105
+ }
101
106
 
102
- if (!(config.canDrag?.(items) ?? true)) {
107
+ if (!(config.canDrag?.(items) ?? true)) {
108
+ e.preventDefault();
109
+ return;
110
+ }
111
+
112
+ if (config.createForeignDragObject) {
113
+ const { format, data } = config.createForeignDragObject(items);
114
+ e.dataTransfer?.setData(format, data);
115
+ }
116
+
117
+ tree.applySubStateUpdate("dnd", {
118
+ draggedItems: items,
119
+ draggingOverItem: tree.getFocusedItem(),
120
+ });
121
+ },
122
+
123
+ onDragOver: (e: DragEvent) => {
124
+ const dataRef = tree.getDataRef<DndDataRef>();
125
+ const nextDragCode = getDragCode(e, item, tree);
126
+ if (nextDragCode === dataRef.current.lastDragCode) {
127
+ if (dataRef.current.lastAllowDrop) {
103
128
  e.preventDefault();
104
- return;
105
129
  }
130
+ return;
131
+ }
132
+ dataRef.current.lastDragCode = nextDragCode;
106
133
 
107
- if (config.createForeignDragObject) {
108
- const { format, data } = config.createForeignDragObject(items);
109
- e.dataTransfer?.setData(format, data);
110
- }
134
+ const target = getDropTarget(e, item, tree);
111
135
 
112
- tree.applySubStateUpdate("dnd", {
113
- draggedItems: items,
114
- draggingOverItem: tree.getFocusedItem(),
115
- });
116
- },
117
- ),
118
-
119
- onDragOver: item.getMemoizedProp(
120
- "dnd/onDragOver",
121
- () => (e: DragEvent) => {
122
- const dataRef = tree.getDataRef<DndDataRef>();
123
- const nextDragCode = getDragCode(e, item, tree);
124
- if (nextDragCode === dataRef.current.lastDragCode) {
125
- if (dataRef.current.lastAllowDrop) {
126
- e.preventDefault();
127
- }
128
- return;
129
- }
130
- dataRef.current.lastDragCode = nextDragCode;
131
-
132
- const target = getDropTarget(e, item, tree);
133
-
134
- if (
135
- !tree.getState().dnd?.draggedItems &&
136
- (!e.dataTransfer ||
137
- !tree
138
- .getConfig()
139
- .canDropForeignDragObject?.(e.dataTransfer, target))
140
- ) {
141
- dataRef.current.lastAllowDrop = false;
142
- return;
143
- }
136
+ if (
137
+ !tree.getState().dnd?.draggedItems &&
138
+ (!e.dataTransfer ||
139
+ !tree
140
+ .getConfig()
141
+ .canDropForeignDragObject?.(e.dataTransfer, target))
142
+ ) {
143
+ dataRef.current.lastAllowDrop = false;
144
+ return;
145
+ }
144
146
 
145
- if (!canDrop(e.dataTransfer, target, tree)) {
146
- dataRef.current.lastAllowDrop = false;
147
- return;
148
- }
147
+ if (!canDrop(e.dataTransfer, target, tree)) {
148
+ dataRef.current.lastAllowDrop = false;
149
+ return;
150
+ }
149
151
 
150
- tree.applySubStateUpdate("dnd", (state) => ({
151
- ...state,
152
- dragTarget: target,
153
- draggingOverItem: item,
154
- }));
155
- dataRef.current.lastAllowDrop = true;
156
- e.preventDefault();
157
- },
158
- ),
152
+ tree.applySubStateUpdate("dnd", (state) => ({
153
+ ...state,
154
+ dragTarget: target,
155
+ draggingOverItem: item,
156
+ }));
157
+ dataRef.current.lastAllowDrop = true;
158
+ e.preventDefault();
159
+ },
159
160
 
160
- onDragLeave: item.getMemoizedProp("dnd/onDragLeave", () => () => {
161
+ onDragLeave: () => {
161
162
  const dataRef = tree.getDataRef<DndDataRef>();
162
163
  dataRef.current.lastDragCode = "no-drag";
163
164
  tree.applySubStateUpdate("dnd", (state) => ({
@@ -165,9 +166,9 @@ export const dragAndDropFeature: FeatureImplementation<
165
166
  draggingOverItem: undefined,
166
167
  dragTarget: undefined,
167
168
  }));
168
- }),
169
+ },
169
170
 
170
- onDragEnd: item.getMemoizedProp("dnd/onDragEnd", () => (e: DragEvent) => {
171
+ onDragEnd: (e: DragEvent) => {
171
172
  const draggedItems = tree.getState().dnd?.draggedItems;
172
173
  tree.applySubStateUpdate("dnd", null);
173
174
 
@@ -176,9 +177,9 @@ export const dragAndDropFeature: FeatureImplementation<
176
177
  }
177
178
 
178
179
  tree.getConfig().onCompleteForeignDrop?.(draggedItems);
179
- }),
180
+ },
180
181
 
181
- onDrop: item.getMemoizedProp("dnd/onDrop", () => (e: DragEvent) => {
182
+ onDrop: (e: DragEvent) => {
182
183
  const dataRef = tree.getDataRef<DndDataRef>();
183
184
  const target = getDropTarget(e, item, tree);
184
185
 
@@ -199,7 +200,7 @@ export const dragAndDropFeature: FeatureImplementation<
199
200
  config.onDropForeignDragObject?.(e.dataTransfer, target);
200
201
  }
201
202
  // TODO rebuild tree?
202
- }),
203
+ },
203
204
  }),
204
205
 
205
206
  isDropTarget: ({ tree, item }) => {
@@ -6,7 +6,7 @@ export type DndDataRef = {
6
6
  };
7
7
 
8
8
  export type DndState<T> = {
9
- draggedItems?: ItemInstance<T>[]; // TODO not used anymore?
9
+ draggedItems?: ItemInstance<T>[];
10
10
  draggingOverItem?: ItemInstance<T>;
11
11
  dragTarget?: DropTarget<T>;
12
12
  };
@@ -15,7 +15,7 @@ export type DragLineData = {
15
15
  indent: number;
16
16
  top: number;
17
17
  left: number;
18
- right: number;
18
+ width: number;
19
19
  };
20
20
 
21
21
  export type DropTarget<T> =
@@ -49,9 +49,9 @@ export type DragAndDropFeatureDef<T> = {
49
49
 
50
50
  /** Defines the size of the area at the top and bottom of an item where, when an item is dropped, the item willö
51
51
  * be placed above or below the item within the same parent, as opposed to being placed inside the item.
52
- * If `canDropInbetween` is `false`, this is ignored. */
52
+ * If `canReorder` is `false`, this is ignored. */
53
53
  reorderAreaPercentage?: number;
54
- canDropInbetween?: boolean;
54
+ canReorder?: boolean;
55
55
 
56
56
  // TODO better document difference to canDrag(), or unify both
57
57
  isItemDraggable?: (item: ItemInstance<T>) => boolean;
@@ -81,7 +81,7 @@ const getTargetPlacement = (
81
81
  ): TargetPlacement => {
82
82
  const config = tree.getConfig();
83
83
 
84
- if (!config.canDropInbetween) {
84
+ if (!config.canReorder) {
85
85
  return canMakeChild
86
86
  ? { type: PlacementType.MakeChild }
87
87
  : { type: PlacementType.ReorderBelow };
@@ -156,7 +156,7 @@ export const getDropTarget = (
156
156
  e: any,
157
157
  item: ItemInstance<any>,
158
158
  tree: TreeInstance<any>,
159
- canDropInbetween = tree.getConfig().canDropInbetween,
159
+ canReorder = tree.getConfig().canReorder,
160
160
  ): DropTarget<any> => {
161
161
  const draggedItems = tree.getState().dnd?.draggedItems ?? [];
162
162
  const itemMeta = item.getItemMeta();
@@ -184,7 +184,7 @@ export const getDropTarget = (
184
184
  const placement = getTargetPlacement(e, item, tree, canMakeChild);
185
185
 
186
186
  if (
187
- !canDropInbetween &&
187
+ !canReorder &&
188
188
  parent &&
189
189
  canBecomeSibling &&
190
190
  placement.type !== PlacementType.MakeChild
@@ -192,7 +192,7 @@ export const getDropTarget = (
192
192
  return parentTarget;
193
193
  }
194
194
 
195
- if (!canDropInbetween && parent && !canBecomeSibling) {
195
+ if (!canReorder && parent && !canBecomeSibling) {
196
196
  // TODO! this breaks in story DND/Can Drop. Maybe move this logic into a composable DropTargetStrategy[] ?
197
197
  return getDropTarget(e, parent, tree, false);
198
198
  }
@@ -1,8 +1,12 @@
1
1
  import { describe, it } from "vitest";
2
2
  import { TestTree } from "../../test-utils/test-tree";
3
3
  import { expandAllFeature } from "./feature";
4
+ import { propMemoizationFeature } from "../prop-memoization/feature";
4
5
 
5
- const factory = TestTree.default({}).withFeatures(expandAllFeature);
6
+ const factory = TestTree.default({}).withFeatures(
7
+ expandAllFeature,
8
+ propMemoizationFeature,
9
+ );
6
10
 
7
11
  describe("core-feature/expand-all", () => {
8
12
  factory.forSuits((tree) => {
@@ -1,18 +1,7 @@
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
7
  treeInstance: {
@@ -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
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) => {
@@ -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) => ({
@@ -24,17 +17,6 @@ export const renamingFeature: FeatureImplementation<
24
17
  },
25
18
 
26
19
  treeInstance: {
27
- startRenamingItem: ({ tree }, itemId) => {
28
- const item = tree.getItemInstance(itemId);
29
-
30
- if (!item.canRename()) {
31
- return;
32
- }
33
-
34
- tree.applySubStateUpdate("renamingItem", itemId);
35
- tree.applySubStateUpdate("renamingValue", item.getItemName());
36
- },
37
-
38
20
  getRenamingItem: ({ tree }) => {
39
21
  const itemId = tree.getState().renamingItem;
40
22
  return itemId ? tree.getItemInstance(itemId) : null;
@@ -61,6 +43,15 @@ export const renamingFeature: FeatureImplementation<
61
43
  },
62
44
 
63
45
  itemInstance: {
46
+ startRenaming: ({ tree, item, itemId }) => {
47
+ if (!item.canRename()) {
48
+ return;
49
+ }
50
+
51
+ tree.applySubStateUpdate("renamingItem", itemId);
52
+ tree.applySubStateUpdate("renamingValue", item.getItemName());
53
+ },
54
+
64
55
  getRenameInputProps: ({ tree }) => ({
65
56
  onBlur: () => tree.abortRenaming(),
66
57
  value: tree.getRenamingValue(),
@@ -81,7 +72,7 @@ export const renamingFeature: FeatureImplementation<
81
72
  renameItem: {
82
73
  hotkey: "F2",
83
74
  handler: (e, tree) => {
84
- tree.startRenamingItem(tree.getFocusedItem().getId());
75
+ tree.getFocusedItem().startRenaming();
85
76
  },
86
77
  },
87
78
 
@@ -2,16 +2,18 @@ import { describe, expect, it } from "vitest";
2
2
  import { TestTree } from "../../test-utils/test-tree";
3
3
  import { renamingFeature } from "./feature";
4
4
  import { selectionFeature } from "../selection/feature";
5
+ import { propMemoizationFeature } from "../prop-memoization/feature";
5
6
 
6
7
  const factory = TestTree.default({}).withFeatures(
7
8
  renamingFeature,
8
9
  selectionFeature,
10
+ propMemoizationFeature,
9
11
  );
10
12
 
11
13
  describe("core-feature/renaming", () => {
12
14
  factory.forSuits((tree) => {
13
15
  it("starts and aborts renaming", () => {
14
- tree.instance.startRenamingItem("x1");
16
+ tree.item("x1").startRenaming();
15
17
  expect(tree.instance.isRenamingItem()).toBe(true);
16
18
  expect(tree.instance.getRenamingValue()).toBe("x1");
17
19
  tree.instance.abortRenaming();
@@ -19,7 +21,7 @@ describe("core-feature/renaming", () => {
19
21
  });
20
22
 
21
23
  it("stops renaming by blurring", () => {
22
- tree.instance.startRenamingItem("x1");
24
+ tree.item("x1").startRenaming();
23
25
  tree.instance.getRenamingItem()!.getRenameInputProps().onBlur();
24
26
  expect(tree.instance.isRenamingItem()).toBe(false);
25
27
  });
@@ -27,7 +29,7 @@ describe("core-feature/renaming", () => {
27
29
  it("completes renaming programmatically", () => {
28
30
  const onRename = tree.mockedHandler("onRename");
29
31
 
30
- tree.instance.startRenamingItem("x1");
32
+ tree.item("x1").startRenaming();
31
33
  expect(tree.instance.getRenamingItem()!.getRenameInputProps().value).toBe(
32
34
  "x1",
33
35
  );
@@ -51,7 +53,7 @@ describe("core-feature/renaming", () => {
51
53
  it("invokes state setters when aborting", () => {
52
54
  const setRenamingItem = tree.mockedHandler("setRenamingItem");
53
55
  const setRenamingValue = tree.mockedHandler("setRenamingValue");
54
- tree.instance.startRenamingItem("x1");
56
+ tree.item("x1").startRenaming();
55
57
  expect(setRenamingItem).toHaveBeenCalledWith("x1");
56
58
  expect(setRenamingValue).toHaveBeenCalledWith("x1");
57
59
  tree.instance.abortRenaming();
@@ -61,7 +63,7 @@ describe("core-feature/renaming", () => {
61
63
  it("invokes state setters when completing", () => {
62
64
  const setRenamingItem = tree.mockedHandler("setRenamingItem");
63
65
  const setRenamingValue = tree.mockedHandler("setRenamingValue");
64
- tree.instance.startRenamingItem("x1");
66
+ tree.item("x1").startRenaming();
65
67
  tree.instance
66
68
  .getRenamingItem()!
67
69
  .getRenameInputProps()
@@ -76,7 +78,7 @@ describe("core-feature/renaming", () => {
76
78
 
77
79
  it("changes renaming input content with input props", () => {
78
80
  const setRenamingValue = tree.mockedHandler("setRenamingValue");
79
- tree.instance.startRenamingItem("x1");
81
+ tree.item("x1").startRenaming();
80
82
  tree.instance
81
83
  .getRenamingItem()!
82
84
  .getRenameInputProps()
@@ -86,7 +88,7 @@ describe("core-feature/renaming", () => {
86
88
 
87
89
  it("aborts renaming with input props", () => {
88
90
  const setRenamingItem = tree.mockedHandler("setRenamingItem");
89
- tree.instance.startRenamingItem("x1");
91
+ tree.item("x1").startRenaming();
90
92
  tree.instance.getRenamingItem()!.getRenameInputProps().onBlur();
91
93
  expect(setRenamingItem).toHaveBeenCalledWith(null);
92
94
  });
@@ -100,14 +102,14 @@ describe("core-feature/renaming", () => {
100
102
 
101
103
  it("aborts renaming with Escape key", () => {
102
104
  const setRenamingItem = tree.mockedHandler("setRenamingItem");
103
- tree.instance.startRenamingItem("x1");
105
+ tree.item("x1").startRenaming();
104
106
  tree.do.hotkey("abortRenaming");
105
107
  expect(setRenamingItem).toHaveBeenCalledWith(null);
106
108
  });
107
109
 
108
110
  it("completes renaming with Enter key", () => {
109
111
  const onRename = tree.mockedHandler("onRename");
110
- tree.instance.startRenamingItem("x1");
112
+ tree.item("x1").startRenaming();
111
113
  tree.instance
112
114
  .getRenamingItem()!
113
115
  .getRenameInputProps()