@headless-tree/core 0.0.0-20250507225212 → 0.0.0-20250508233207

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.
package/CHANGELOG.md CHANGED
@@ -1,11 +1,17 @@
1
1
  # @headless-tree/core
2
2
 
3
- ## 0.0.0-20250507225212
3
+ ## 0.0.0-20250508233207
4
4
 
5
5
  ### Minor Changes
6
6
 
7
7
  - 64d8e2a: add getChildrenWithData method to data loader to support fetching all children of an item at once
8
8
 
9
+ ### Patch Changes
10
+
11
+ - 29b2c64: improved key-handling behavior for hotkeys while input elements are focused (#98)
12
+ - da1e757: fixed a bug where alt-tabbing out of browser will break hotkeys feature
13
+ - 29b2c64: added option to completely ignore hotkey events while input elements are focused (`ignoreHotkeysOnInput`) (#98)
14
+
9
15
  ## 1.0.1
10
16
 
11
17
  ### Patch Changes
@@ -25,16 +25,25 @@ exports.hotkeysCoreFeature = {
25
25
  onTreeMount: (tree, element) => {
26
26
  const data = tree.getDataRef();
27
27
  const keydown = (e) => {
28
- var _a, _b, _c, _d;
29
- var _e;
28
+ var _a;
29
+ var _b;
30
+ const { ignoreHotkeysOnInputs, onTreeHotkey, hotkeys } = tree.getConfig();
31
+ if (e.target instanceof HTMLInputElement && ignoreHotkeysOnInputs) {
32
+ return;
33
+ }
30
34
  const key = e.key.toLowerCase();
31
- (_a = (_e = data.current).pressedKeys) !== null && _a !== void 0 ? _a : (_e.pressedKeys = new Set());
35
+ (_a = (_b = data.current).pressedKeys) !== null && _a !== void 0 ? _a : (_b.pressedKeys = new Set());
32
36
  const newMatch = !data.current.pressedKeys.has(key);
33
37
  data.current.pressedKeys.add(key);
34
- const hotkeyName = findHotkeyMatch(data.current.pressedKeys, tree, tree.getHotkeyPresets(), tree.getConfig().hotkeys);
38
+ const hotkeyName = findHotkeyMatch(data.current.pressedKeys, tree, tree.getHotkeyPresets(), hotkeys);
39
+ if (e.target instanceof HTMLInputElement) {
40
+ // JS respects composite keydowns while input elements are focused, and
41
+ // doesnt send the associated keyup events with the same key name
42
+ data.current.pressedKeys.delete(key);
43
+ }
35
44
  if (!hotkeyName)
36
45
  return;
37
- const hotkeyConfig = Object.assign(Object.assign({}, tree.getHotkeyPresets()[hotkeyName]), (_b = tree.getConfig().hotkeys) === null || _b === void 0 ? void 0 : _b[hotkeyName]);
46
+ const hotkeyConfig = Object.assign(Object.assign({}, tree.getHotkeyPresets()[hotkeyName]), hotkeys === null || hotkeys === void 0 ? void 0 : hotkeys[hotkeyName]);
38
47
  if (!hotkeyConfig)
39
48
  return;
40
49
  if (!hotkeyConfig.allowWhenInputFocused &&
@@ -45,7 +54,7 @@ exports.hotkeysCoreFeature = {
45
54
  if (hotkeyConfig.preventDefault)
46
55
  e.preventDefault();
47
56
  hotkeyConfig.handler(e, tree);
48
- (_d = (_c = tree.getConfig()).onTreeHotkey) === null || _d === void 0 ? void 0 : _d.call(_c, hotkeyName, e);
57
+ onTreeHotkey === null || onTreeHotkey === void 0 ? void 0 : onTreeHotkey(hotkeyName, e);
49
58
  };
50
59
  const keyup = (e) => {
51
60
  var _a;
@@ -53,13 +62,18 @@ exports.hotkeysCoreFeature = {
53
62
  (_a = (_b = data.current).pressedKeys) !== null && _a !== void 0 ? _a : (_b.pressedKeys = new Set());
54
63
  data.current.pressedKeys.delete(e.key.toLowerCase());
55
64
  };
65
+ const reset = () => {
66
+ data.current.pressedKeys = new Set();
67
+ };
56
68
  // keyup is registered on document, because some hotkeys shift
57
69
  // the focus away from the tree (i.e. search)
58
70
  // and then we wouldn't get the keyup event anymore
59
71
  element.addEventListener("keydown", keydown);
60
72
  document.addEventListener("keyup", keyup);
73
+ window.addEventListener("focus", reset);
61
74
  data.current.keydownHandler = keydown;
62
75
  data.current.keyupHandler = keyup;
76
+ data.current.resetHandler = reset;
63
77
  },
64
78
  onTreeUnmount: (tree, element) => {
65
79
  const data = tree.getDataRef();
@@ -71,5 +85,9 @@ exports.hotkeysCoreFeature = {
71
85
  element.removeEventListener("keydown", data.current.keydownHandler);
72
86
  delete data.current.keydownHandler;
73
87
  }
88
+ if (data.current.resetHandler) {
89
+ window.removeEventListener("focus", data.current.resetHandler);
90
+ delete data.current.resetHandler;
91
+ }
74
92
  },
75
93
  };
@@ -10,6 +10,7 @@ export interface HotkeyConfig<T> {
10
10
  export interface HotkeysCoreDataRef {
11
11
  keydownHandler?: (e: KeyboardEvent) => void;
12
12
  keyupHandler?: (e: KeyboardEvent) => void;
13
+ resetHandler?: (e: FocusEvent) => void;
13
14
  pressedKeys: Set<string>;
14
15
  }
15
16
  export type HotkeysCoreFeatureDef<T> = {
@@ -17,6 +18,8 @@ export type HotkeysCoreFeatureDef<T> = {
17
18
  config: {
18
19
  hotkeys?: CustomHotkeysConfig<T>;
19
20
  onTreeHotkey?: (name: string, e: KeyboardEvent) => void;
21
+ /** Do not handle key inputs while an HTML input element is focused */
22
+ ignoreHotkeysOnInputs?: boolean;
20
23
  };
21
24
  treeInstance: {};
22
25
  itemInstance: {};
@@ -22,16 +22,25 @@ export const hotkeysCoreFeature = {
22
22
  onTreeMount: (tree, element) => {
23
23
  const data = tree.getDataRef();
24
24
  const keydown = (e) => {
25
- var _a, _b, _c, _d;
26
- var _e;
25
+ var _a;
26
+ var _b;
27
+ const { ignoreHotkeysOnInputs, onTreeHotkey, hotkeys } = tree.getConfig();
28
+ if (e.target instanceof HTMLInputElement && ignoreHotkeysOnInputs) {
29
+ return;
30
+ }
27
31
  const key = e.key.toLowerCase();
28
- (_a = (_e = data.current).pressedKeys) !== null && _a !== void 0 ? _a : (_e.pressedKeys = new Set());
32
+ (_a = (_b = data.current).pressedKeys) !== null && _a !== void 0 ? _a : (_b.pressedKeys = new Set());
29
33
  const newMatch = !data.current.pressedKeys.has(key);
30
34
  data.current.pressedKeys.add(key);
31
- const hotkeyName = findHotkeyMatch(data.current.pressedKeys, tree, tree.getHotkeyPresets(), tree.getConfig().hotkeys);
35
+ const hotkeyName = findHotkeyMatch(data.current.pressedKeys, tree, tree.getHotkeyPresets(), hotkeys);
36
+ if (e.target instanceof HTMLInputElement) {
37
+ // JS respects composite keydowns while input elements are focused, and
38
+ // doesnt send the associated keyup events with the same key name
39
+ data.current.pressedKeys.delete(key);
40
+ }
32
41
  if (!hotkeyName)
33
42
  return;
34
- const hotkeyConfig = Object.assign(Object.assign({}, tree.getHotkeyPresets()[hotkeyName]), (_b = tree.getConfig().hotkeys) === null || _b === void 0 ? void 0 : _b[hotkeyName]);
43
+ const hotkeyConfig = Object.assign(Object.assign({}, tree.getHotkeyPresets()[hotkeyName]), hotkeys === null || hotkeys === void 0 ? void 0 : hotkeys[hotkeyName]);
35
44
  if (!hotkeyConfig)
36
45
  return;
37
46
  if (!hotkeyConfig.allowWhenInputFocused &&
@@ -42,7 +51,7 @@ export const hotkeysCoreFeature = {
42
51
  if (hotkeyConfig.preventDefault)
43
52
  e.preventDefault();
44
53
  hotkeyConfig.handler(e, tree);
45
- (_d = (_c = tree.getConfig()).onTreeHotkey) === null || _d === void 0 ? void 0 : _d.call(_c, hotkeyName, e);
54
+ onTreeHotkey === null || onTreeHotkey === void 0 ? void 0 : onTreeHotkey(hotkeyName, e);
46
55
  };
47
56
  const keyup = (e) => {
48
57
  var _a;
@@ -50,13 +59,18 @@ export const hotkeysCoreFeature = {
50
59
  (_a = (_b = data.current).pressedKeys) !== null && _a !== void 0 ? _a : (_b.pressedKeys = new Set());
51
60
  data.current.pressedKeys.delete(e.key.toLowerCase());
52
61
  };
62
+ const reset = () => {
63
+ data.current.pressedKeys = new Set();
64
+ };
53
65
  // keyup is registered on document, because some hotkeys shift
54
66
  // the focus away from the tree (i.e. search)
55
67
  // and then we wouldn't get the keyup event anymore
56
68
  element.addEventListener("keydown", keydown);
57
69
  document.addEventListener("keyup", keyup);
70
+ window.addEventListener("focus", reset);
58
71
  data.current.keydownHandler = keydown;
59
72
  data.current.keyupHandler = keyup;
73
+ data.current.resetHandler = reset;
60
74
  },
61
75
  onTreeUnmount: (tree, element) => {
62
76
  const data = tree.getDataRef();
@@ -68,5 +82,9 @@ export const hotkeysCoreFeature = {
68
82
  element.removeEventListener("keydown", data.current.keydownHandler);
69
83
  delete data.current.keydownHandler;
70
84
  }
85
+ if (data.current.resetHandler) {
86
+ window.removeEventListener("focus", data.current.resetHandler);
87
+ delete data.current.resetHandler;
88
+ }
71
89
  },
72
90
  };
@@ -10,6 +10,7 @@ export interface HotkeyConfig<T> {
10
10
  export interface HotkeysCoreDataRef {
11
11
  keydownHandler?: (e: KeyboardEvent) => void;
12
12
  keyupHandler?: (e: KeyboardEvent) => void;
13
+ resetHandler?: (e: FocusEvent) => void;
13
14
  pressedKeys: Set<string>;
14
15
  }
15
16
  export type HotkeysCoreFeatureDef<T> = {
@@ -17,6 +18,8 @@ export type HotkeysCoreFeatureDef<T> = {
17
18
  config: {
18
19
  hotkeys?: CustomHotkeysConfig<T>;
19
20
  onTreeHotkey?: (name: string, e: KeyboardEvent) => void;
21
+ /** Do not handle key inputs while an HTML input element is focused */
22
+ ignoreHotkeysOnInputs?: boolean;
20
23
  };
21
24
  treeInstance: {};
22
25
  itemInstance: {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@headless-tree/core",
3
- "version": "0.0.0-20250507225212",
3
+ "version": "0.0.0-20250508233207",
4
4
  "type": "module",
5
5
  "main": "lib/cjs/index.js",
6
6
  "module": "lib/esm/index.js",
@@ -126,5 +126,49 @@ describe("core-feature/selections", () => {
126
126
  });
127
127
  });
128
128
 
129
- // TODO - add tests for getChildrenWithData
129
+ describe("getChildrenWithData", () => {
130
+ const getChildrenWithData = vi.fn(async (id) => [
131
+ { id: `${id}1`, data: `${id}1-data` },
132
+ { id: `${id}2`, data: `${id}2-data` },
133
+ ]);
134
+ const getItem = vi.fn();
135
+ const suiteTree = tree.with({
136
+ dataLoader: { getItem, getChildrenWithData },
137
+ });
138
+ suiteTree.resetBeforeEach();
139
+
140
+ it("loads children with data", async () => {
141
+ getChildrenWithData.mockClear();
142
+ suiteTree.do.selectItem("x12");
143
+ await suiteTree.resolveAsyncVisibleItems();
144
+ expect(getChildrenWithData).toHaveBeenCalledWith("x12");
145
+ suiteTree.expect.hasChildren("x12", ["x121", "x122"]);
146
+ expect(suiteTree.item("x121").getItemData()).toBe("x121-data");
147
+ expect(suiteTree.item("x122").getItemData()).toBe("x122-data");
148
+ });
149
+
150
+ it.skip("invalidates children and reloads with data", async () => {
151
+ await suiteTree.resolveAsyncVisibleItems();
152
+ suiteTree.item("x").invalidateChildrenIds();
153
+ getChildrenWithData.mockResolvedValueOnce([
154
+ { id: "new1", data: "new1-data" },
155
+ { id: "new2", data: "new2-data" },
156
+ ]);
157
+ getChildrenWithData.mockClear();
158
+ await suiteTree.resolveAsyncVisibleItems();
159
+ expect(getChildrenWithData).toHaveBeenCalledTimes(1);
160
+ suiteTree.expect.hasChildren("x", ["new1", "new2"]);
161
+ expect(suiteTree.item("new1").getItemData()).toBe("new1-data");
162
+ expect(suiteTree.item("new2").getItemData()).toBe("new2-data");
163
+ });
164
+
165
+ it("does not call getChildrenWithData twice unnecessarily", async () => {
166
+ await suiteTree.resolveAsyncVisibleItems();
167
+ getChildrenWithData.mockClear();
168
+ suiteTree.item("x").invalidateChildrenIds();
169
+ await suiteTree.resolveAsyncVisibleItems();
170
+ suiteTree.expect.hasChildren("x", ["x1", "x2"]);
171
+ expect(getChildrenWithData).toHaveBeenCalledTimes(1);
172
+ });
173
+ });
130
174
  });
@@ -45,6 +45,11 @@ export const hotkeysCoreFeature: FeatureImplementation = {
45
45
  onTreeMount: (tree, element) => {
46
46
  const data = tree.getDataRef<HotkeysCoreDataRef>();
47
47
  const keydown = (e: KeyboardEvent) => {
48
+ const { ignoreHotkeysOnInputs, onTreeHotkey, hotkeys } = tree.getConfig();
49
+ if (e.target instanceof HTMLInputElement && ignoreHotkeysOnInputs) {
50
+ return;
51
+ }
52
+
48
53
  const key = e.key.toLowerCase();
49
54
  data.current.pressedKeys ??= new Set();
50
55
  const newMatch = !data.current.pressedKeys.has(key);
@@ -54,14 +59,20 @@ export const hotkeysCoreFeature: FeatureImplementation = {
54
59
  data.current.pressedKeys,
55
60
  tree as any,
56
61
  tree.getHotkeyPresets(),
57
- tree.getConfig().hotkeys as HotkeysConfig<any>,
62
+ hotkeys as HotkeysConfig<any>,
58
63
  );
59
64
 
65
+ if (e.target instanceof HTMLInputElement) {
66
+ // JS respects composite keydowns while input elements are focused, and
67
+ // doesnt send the associated keyup events with the same key name
68
+ data.current.pressedKeys.delete(key);
69
+ }
70
+
60
71
  if (!hotkeyName) return;
61
72
 
62
73
  const hotkeyConfig: HotkeyConfig<any> = {
63
74
  ...tree.getHotkeyPresets()[hotkeyName],
64
- ...tree.getConfig().hotkeys?.[hotkeyName],
75
+ ...hotkeys?.[hotkeyName],
65
76
  };
66
77
 
67
78
  if (!hotkeyConfig) return;
@@ -74,7 +85,7 @@ export const hotkeysCoreFeature: FeatureImplementation = {
74
85
  if (hotkeyConfig.preventDefault) e.preventDefault();
75
86
 
76
87
  hotkeyConfig.handler(e, tree as any);
77
- tree.getConfig().onTreeHotkey?.(hotkeyName, e);
88
+ onTreeHotkey?.(hotkeyName, e);
78
89
  };
79
90
 
80
91
  const keyup = (e: KeyboardEvent) => {
@@ -82,13 +93,19 @@ export const hotkeysCoreFeature: FeatureImplementation = {
82
93
  data.current.pressedKeys.delete(e.key.toLowerCase());
83
94
  };
84
95
 
96
+ const reset = () => {
97
+ data.current.pressedKeys = new Set();
98
+ };
99
+
85
100
  // keyup is registered on document, because some hotkeys shift
86
101
  // the focus away from the tree (i.e. search)
87
102
  // and then we wouldn't get the keyup event anymore
88
103
  element.addEventListener("keydown", keydown);
89
104
  document.addEventListener("keyup", keyup);
105
+ window.addEventListener("focus", reset);
90
106
  data.current.keydownHandler = keydown;
91
107
  data.current.keyupHandler = keyup;
108
+ data.current.resetHandler = reset;
92
109
  },
93
110
 
94
111
  onTreeUnmount: (tree, element) => {
@@ -101,5 +118,9 @@ export const hotkeysCoreFeature: FeatureImplementation = {
101
118
  element.removeEventListener("keydown", data.current.keydownHandler);
102
119
  delete data.current.keydownHandler;
103
120
  }
121
+ if (data.current.resetHandler) {
122
+ window.removeEventListener("focus", data.current.resetHandler);
123
+ delete data.current.resetHandler;
124
+ }
104
125
  },
105
126
  };
@@ -12,6 +12,7 @@ export interface HotkeyConfig<T> {
12
12
  export interface HotkeysCoreDataRef {
13
13
  keydownHandler?: (e: KeyboardEvent) => void;
14
14
  keyupHandler?: (e: KeyboardEvent) => void;
15
+ resetHandler?: (e: FocusEvent) => void;
15
16
  pressedKeys: Set<string>;
16
17
  }
17
18
 
@@ -20,6 +21,9 @@ export type HotkeysCoreFeatureDef<T> = {
20
21
  config: {
21
22
  hotkeys?: CustomHotkeysConfig<T>;
22
23
  onTreeHotkey?: (name: string, e: KeyboardEvent) => void;
24
+
25
+ /** Do not handle key inputs while an HTML input element is focused */
26
+ ignoreHotkeysOnInputs?: boolean;
23
27
  };
24
28
  treeInstance: {};
25
29
  itemInstance: {};