@tanstack/hotkeys 0.17.0 → 0.19.0
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/dist/_event-target.js +209 -120
- package/dist/_match.js +23 -1
- package/dist/app-hotkeys.js +2 -3
- package/dist/hotkey-manager.d.ts +29 -3
- package/dist/hotkey-manager.js +48 -8
- package/dist/hotkey-recorder.js +1 -1
- package/dist/index.d.ts +2 -2
- package/package.json +1 -1
- package/skills/hotkeys-core/SKILL.md +1 -1
- package/skills/hotkeys-core/references/scopes.md +2 -0
package/dist/_event-target.js
CHANGED
|
@@ -1,67 +1,169 @@
|
|
|
1
1
|
import { normalizeKeyName } from "./constants.js";
|
|
2
2
|
|
|
3
3
|
//#region src/_event-target.ts
|
|
4
|
-
function
|
|
5
|
-
return
|
|
4
|
+
function isElement(node) {
|
|
5
|
+
return node?.nodeType === 1;
|
|
6
6
|
}
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
function isWindow(target) {
|
|
11
|
-
return "document" in target;
|
|
7
|
+
/** Whether a target is a document or window, which receive every key press in their document. */
|
|
8
|
+
function isDocumentLevel(target) {
|
|
9
|
+
return "document" in target || target.nodeType === 9;
|
|
12
10
|
}
|
|
13
11
|
/** Returns the document a listener target belongs to, in any frame. */
|
|
14
12
|
function getTargetDocument(target) {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
13
|
+
return "document" in target ? target.document : target.ownerDocument ?? target;
|
|
14
|
+
}
|
|
15
|
+
/** The focused element, looking inside open shadow roots. */
|
|
16
|
+
function getDeepActiveElement(doc) {
|
|
17
|
+
let active = doc.activeElement;
|
|
18
|
+
while (active?.shadowRoot?.activeElement) active = active.shadowRoot.activeElement;
|
|
19
|
+
return active;
|
|
20
|
+
}
|
|
21
|
+
/** The next node a key event bubbles to: a slot, a shadow host, or the parent. */
|
|
22
|
+
function composedParent(node) {
|
|
23
|
+
return node.assignedSlot ?? node.parentNode ?? (node.host || null);
|
|
18
24
|
}
|
|
19
25
|
/**
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
26
|
+
* Whether key presses reach a listener on `target` right now. Document and
|
|
27
|
+
* window listeners receive every key press in their document. An element
|
|
28
|
+
* receives them only while focus is inside it, including inside a shadow
|
|
29
|
+
* root it contains or on content slotted into it.
|
|
30
|
+
*/
|
|
31
|
+
function targetHasFocus(target) {
|
|
32
|
+
if (isDocumentLevel(target)) return true;
|
|
33
|
+
for (let node = getDeepActiveElement(target.ownerDocument); node; node = composedParent(node)) if (node === target) return true;
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
const VERTICAL = ["ArrowUp", "ArrowDown"];
|
|
37
|
+
const HORIZONTAL = ["ArrowLeft", "ArrowRight"];
|
|
38
|
+
const ENTER = ["Enter"];
|
|
39
|
+
const SPACE = ["Space"];
|
|
40
|
+
const ACTIVATION = ["Enter", "Space"];
|
|
41
|
+
const TYPEAHEAD = "typeahead";
|
|
42
|
+
const MENU = [
|
|
43
|
+
"ArrowUp",
|
|
44
|
+
"ArrowDown",
|
|
45
|
+
"ArrowLeft",
|
|
46
|
+
"ArrowRight",
|
|
47
|
+
"Home",
|
|
48
|
+
"End",
|
|
49
|
+
"Enter",
|
|
50
|
+
"Space",
|
|
51
|
+
TYPEAHEAD
|
|
52
|
+
];
|
|
53
|
+
const COMBOBOX = [
|
|
54
|
+
"ArrowUp",
|
|
55
|
+
"ArrowDown",
|
|
56
|
+
"Home",
|
|
57
|
+
"End",
|
|
58
|
+
"Enter",
|
|
59
|
+
"Space",
|
|
60
|
+
TYPEAHEAD
|
|
61
|
+
];
|
|
62
|
+
const GRID = [
|
|
63
|
+
"ArrowUp",
|
|
64
|
+
"ArrowDown",
|
|
65
|
+
"ArrowLeft",
|
|
66
|
+
"ArrowRight",
|
|
67
|
+
"Home",
|
|
68
|
+
"End",
|
|
69
|
+
"PageUp",
|
|
70
|
+
"PageDown",
|
|
71
|
+
"Enter",
|
|
72
|
+
"Space"
|
|
73
|
+
];
|
|
74
|
+
const RADIOS = [
|
|
75
|
+
"ArrowUp",
|
|
76
|
+
"ArrowDown",
|
|
77
|
+
"ArrowLeft",
|
|
78
|
+
"ArrowRight",
|
|
79
|
+
"Space"
|
|
80
|
+
];
|
|
81
|
+
const NAVIGATION = [
|
|
82
|
+
"ArrowUp",
|
|
83
|
+
"ArrowDown",
|
|
84
|
+
"ArrowLeft",
|
|
85
|
+
"ArrowRight",
|
|
86
|
+
"Home",
|
|
87
|
+
"End",
|
|
88
|
+
"PageUp",
|
|
89
|
+
"PageDown"
|
|
90
|
+
];
|
|
91
|
+
const SPIN = [
|
|
92
|
+
"ArrowUp",
|
|
93
|
+
"ArrowDown",
|
|
94
|
+
"Home",
|
|
95
|
+
"End",
|
|
96
|
+
"PageUp",
|
|
97
|
+
"PageDown"
|
|
98
|
+
];
|
|
99
|
+
const CARET = [
|
|
100
|
+
"ArrowLeft",
|
|
101
|
+
"ArrowRight",
|
|
102
|
+
"Home",
|
|
103
|
+
"End"
|
|
104
|
+
];
|
|
105
|
+
/**
|
|
106
|
+
* Input types that aren't text fields, with the keys each one keeps. Browsers
|
|
107
|
+
* report unknown types as `'text'`.
|
|
108
|
+
*/
|
|
109
|
+
const INPUT_KEYS = {
|
|
110
|
+
button: ACTIVATION,
|
|
111
|
+
submit: ACTIVATION,
|
|
112
|
+
reset: ACTIVATION,
|
|
113
|
+
image: ACTIVATION,
|
|
114
|
+
color: ACTIVATION,
|
|
115
|
+
file: ACTIVATION,
|
|
116
|
+
checkbox: SPACE,
|
|
117
|
+
radio: RADIOS,
|
|
118
|
+
range: NAVIGATION
|
|
119
|
+
};
|
|
120
|
+
function getRole(element) {
|
|
121
|
+
return element.getAttribute("role")?.trim().split(/\s+/)[0];
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Checks if an element is a text field that hotkeys should not type over:
|
|
125
|
+
* - text-entry `<input>` types (text, number, date, and similar) that aren't read-only
|
|
126
|
+
* - `<textarea>` elements that aren't read-only, and `<select>` elements
|
|
127
|
+
* - elements with `role="textbox"` or `role="searchbox"`
|
|
128
|
+
* - contenteditable elements
|
|
27
129
|
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
130
|
+
* Checkbox, radio, range, color, file, and button-type inputs are not text
|
|
131
|
+
* fields. Hotkeys skip only the keys those controls use themselves.
|
|
30
132
|
*/
|
|
31
133
|
function isInputElement(element) {
|
|
32
134
|
if (!isElement(element)) return false;
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
135
|
+
switch (element.localName) {
|
|
136
|
+
case "input": {
|
|
137
|
+
const input = element;
|
|
138
|
+
return !INPUT_KEYS[input.type] && !input.readOnly;
|
|
139
|
+
}
|
|
140
|
+
case "textarea": return !element.readOnly;
|
|
141
|
+
case "select": return true;
|
|
36
142
|
}
|
|
37
|
-
|
|
38
|
-
return element.isContentEditable
|
|
143
|
+
const role = getRole(element);
|
|
144
|
+
return role === "textbox" || role === "searchbox" || element.isContentEditable;
|
|
39
145
|
}
|
|
40
146
|
/**
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
147
|
+
* Whether the focused custom element may be a text field whose contents can't
|
|
148
|
+
* be seen from outside:
|
|
149
|
+
* - a host with focus inside a closed shadow root. Without a `tabindex` it
|
|
150
|
+
* can't hold focus itself, and an open root would have shown the focused
|
|
151
|
+
* element inside it.
|
|
152
|
+
* - a form-associated element without a `role` attribute naming another control
|
|
45
153
|
*/
|
|
46
|
-
function
|
|
47
|
-
if (
|
|
48
|
-
return
|
|
154
|
+
function isHiddenTextField(element) {
|
|
155
|
+
if (!element.localName.includes("-")) return false;
|
|
156
|
+
return element.constructor.formAssociated === true ? !element.hasAttribute("role") : !element.hasAttribute("tabindex");
|
|
49
157
|
}
|
|
50
|
-
const VERTICAL = ["ArrowUp", "ArrowDown"];
|
|
51
|
-
const HORIZONTAL = ["ArrowLeft", "ArrowRight"];
|
|
52
|
-
const ARROWS = [...VERTICAL, ...HORIZONTAL];
|
|
53
|
-
const EDGES = ["Home", "End"];
|
|
54
|
-
const PAGES = ["PageUp", "PageDown"];
|
|
55
|
-
const ACTIVATION = ["Enter", "Space"];
|
|
56
|
-
const TYPEAHEAD = "typeahead";
|
|
57
158
|
/**
|
|
58
159
|
* Returns the keys a control handles itself: activation for buttons and
|
|
59
160
|
* links, and navigation for ARIA widgets, following the WAI-ARIA Authoring
|
|
60
161
|
* Practices keyboard patterns (https://www.w3.org/WAI/ARIA/apg/patterns/).
|
|
162
|
+
* Editable text fields never get here: they block every key that types text.
|
|
61
163
|
*/
|
|
62
164
|
function getOwnedKeys(element) {
|
|
63
165
|
const orientation = element.getAttribute("aria-orientation");
|
|
64
|
-
switch (element
|
|
166
|
+
switch (getRole(element)) {
|
|
65
167
|
case "button":
|
|
66
168
|
case "tab":
|
|
67
169
|
case "option":
|
|
@@ -69,64 +171,47 @@ function getOwnedKeys(element) {
|
|
|
69
171
|
case "menuitem":
|
|
70
172
|
case "menuitemcheckbox":
|
|
71
173
|
case "menuitemradio": return ACTIVATION;
|
|
72
|
-
case "link": return
|
|
174
|
+
case "link": return ENTER;
|
|
73
175
|
case "checkbox":
|
|
74
176
|
case "switch":
|
|
75
|
-
case "radio": return
|
|
177
|
+
case "radio": return SPACE;
|
|
76
178
|
case "menu":
|
|
77
179
|
case "menubar":
|
|
78
|
-
case "tree": return
|
|
79
|
-
...ARROWS,
|
|
80
|
-
...EDGES,
|
|
81
|
-
...ACTIVATION,
|
|
82
|
-
TYPEAHEAD
|
|
83
|
-
];
|
|
180
|
+
case "tree": return MENU;
|
|
84
181
|
case "listbox": return [
|
|
85
182
|
...orientation === "horizontal" ? HORIZONTAL : VERTICAL,
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
case "combobox": return [
|
|
91
|
-
...VERTICAL,
|
|
92
|
-
...EDGES,
|
|
93
|
-
...ACTIVATION,
|
|
183
|
+
"Home",
|
|
184
|
+
"End",
|
|
185
|
+
"Enter",
|
|
186
|
+
"Space",
|
|
94
187
|
TYPEAHEAD
|
|
95
188
|
];
|
|
189
|
+
case "combobox": return COMBOBOX;
|
|
96
190
|
case "tablist": return [
|
|
97
191
|
...orientation === "vertical" ? VERTICAL : HORIZONTAL,
|
|
98
|
-
|
|
99
|
-
|
|
192
|
+
"Home",
|
|
193
|
+
"End",
|
|
194
|
+
"Enter",
|
|
195
|
+
"Space"
|
|
100
196
|
];
|
|
101
|
-
case "toolbar": return [
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
...EDGES,
|
|
106
|
-
...PAGES,
|
|
107
|
-
...ACTIVATION
|
|
108
|
-
];
|
|
109
|
-
case "radiogroup": return [...ARROWS, "Space"];
|
|
110
|
-
case "slider": return [
|
|
111
|
-
...ARROWS,
|
|
112
|
-
...EDGES,
|
|
113
|
-
...PAGES
|
|
114
|
-
];
|
|
115
|
-
case "spinbutton": return [
|
|
116
|
-
...VERTICAL,
|
|
117
|
-
...EDGES,
|
|
118
|
-
...PAGES
|
|
197
|
+
case "toolbar": return [
|
|
198
|
+
...orientation === "vertical" ? VERTICAL : HORIZONTAL,
|
|
199
|
+
"Home",
|
|
200
|
+
"End"
|
|
119
201
|
];
|
|
202
|
+
case "grid":
|
|
203
|
+
case "treegrid": return GRID;
|
|
204
|
+
case "radiogroup": return RADIOS;
|
|
205
|
+
case "slider": return NAVIGATION;
|
|
206
|
+
case "spinbutton": return SPIN;
|
|
120
207
|
}
|
|
121
208
|
switch (element.localName) {
|
|
122
209
|
case "button":
|
|
123
210
|
case "summary": return ACTIVATION;
|
|
124
|
-
case "input":
|
|
125
|
-
|
|
126
|
-
return type === "button" || type === "submit" || type === "reset" ? ACTIVATION : [];
|
|
127
|
-
}
|
|
211
|
+
case "input": return INPUT_KEYS[element.type] ?? CARET;
|
|
212
|
+
case "textarea": return NAVIGATION;
|
|
128
213
|
case "a":
|
|
129
|
-
case "area": return element.hasAttribute("href") ?
|
|
214
|
+
case "area": return element.hasAttribute("href") ? ENTER : [];
|
|
130
215
|
}
|
|
131
216
|
return [];
|
|
132
217
|
}
|
|
@@ -139,56 +224,60 @@ function ownsKey(element, event, key) {
|
|
|
139
224
|
if (event.ctrlKey || event.metaKey || event.altKey) return false;
|
|
140
225
|
const owned = getOwnedKeys(element);
|
|
141
226
|
if (key === "Enter" || key === "Space") return !event.shiftKey && owned.includes(key);
|
|
142
|
-
return owned.includes(key) || owned.includes(TYPEAHEAD) &&
|
|
227
|
+
return owned.includes(key) || owned.includes(TYPEAHEAD) && /^.$/u.test(event.key);
|
|
143
228
|
}
|
|
144
229
|
/**
|
|
145
|
-
*
|
|
146
|
-
*
|
|
147
|
-
*
|
|
148
|
-
*
|
|
230
|
+
* Finds the first element on the event's way out that blocks it, walking out
|
|
231
|
+
* from the focused element. A key event starts there in a browser. One sent
|
|
232
|
+
* by a test may start at an ancestor of the focused element or at the
|
|
233
|
+
* document, and the walk from focus passes through those too. Only an event
|
|
234
|
+
* that starts where focus can't be seen, such as inside a closed shadow root,
|
|
235
|
+
* uses its own path.
|
|
149
236
|
*/
|
|
150
|
-
function
|
|
151
|
-
const
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
const
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
237
|
+
function findBlocker(event, doc, test) {
|
|
238
|
+
const focused = getDeepActiveElement(doc);
|
|
239
|
+
let elements = [];
|
|
240
|
+
for (let node = focused; node; node = composedParent(node)) if (isElement(node)) elements.push(node);
|
|
241
|
+
const origin = event.composedPath()[0];
|
|
242
|
+
if (isElement(origin) && !elements.includes(origin)) elements = event.composedPath().filter(isElement);
|
|
243
|
+
const index = elements.findIndex((element) => test(element) || element === focused && isHiddenTextField(element));
|
|
244
|
+
return {
|
|
245
|
+
elements,
|
|
246
|
+
index
|
|
247
|
+
};
|
|
160
248
|
}
|
|
161
|
-
/**
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
* - text inputs, textareas, selects, and contenteditable elements (typing)
|
|
165
|
-
* - buttons, links, and `<summary>` elements (Enter and Space activation)
|
|
166
|
-
* - ARIA widgets such as menus, listboxes, tabs, and grids (navigation keys)
|
|
167
|
-
*/
|
|
168
|
-
function shouldIgnoreInputEvent(event, listenerTarget, registrationTarget) {
|
|
169
|
-
const key = normalizeKeyName(event.key);
|
|
170
|
-
return someElementInsideTarget(event, listenerTarget, registrationTarget, (element) => isInputElement(element) || ownsKey(element, event, key));
|
|
249
|
+
/** Whether the blocking element is inside `target`. A target never blocks itself. */
|
|
250
|
+
function blocks({ elements, index }, target) {
|
|
251
|
+
return index >= 0 && (isDocumentLevel(target) || index < elements.indexOf(target));
|
|
171
252
|
}
|
|
253
|
+
const ignoreCache = /* @__PURE__ */ new WeakMap();
|
|
172
254
|
/**
|
|
173
|
-
* Returns whether
|
|
174
|
-
*
|
|
175
|
-
*
|
|
255
|
+
* Returns whether a hotkey on `target` should skip an event because a control
|
|
256
|
+
* inside the target handles the key itself:
|
|
257
|
+
* - text fields, including ARIA text roles and custom elements that hide one (typing)
|
|
258
|
+
* - buttons, links, `<summary>`, and button-like inputs (Enter and Space activation)
|
|
259
|
+
* - checkbox, radio, and range inputs, and ARIA widgets such as menus,
|
|
260
|
+
* listboxes, tabs, and grids (the keys they use)
|
|
261
|
+
*
|
|
262
|
+
* A hotkey targeted at a control receives that control's keys.
|
|
176
263
|
*/
|
|
177
|
-
function
|
|
178
|
-
|
|
264
|
+
function shouldIgnoreInputEvent(event, target) {
|
|
265
|
+
let blocker = ignoreCache.get(event);
|
|
266
|
+
if (!blocker) {
|
|
267
|
+
const key = normalizeKeyName(event.key);
|
|
268
|
+
blocker = findBlocker(event, getTargetDocument(target), (element) => isInputElement(element) || ownsKey(element, event, key));
|
|
269
|
+
ignoreCache.set(event, blocker);
|
|
270
|
+
}
|
|
271
|
+
return blocks(blocker, target);
|
|
179
272
|
}
|
|
180
273
|
/**
|
|
181
|
-
*
|
|
182
|
-
*
|
|
183
|
-
*
|
|
184
|
-
* currentTarget to handle Brave and other browsers where currentTarget may be
|
|
185
|
-
* documentElement instead of document when listeners are attached to document.
|
|
274
|
+
* Returns whether an event comes from a text field inside `target`, as
|
|
275
|
+
* `isInputElement` and hidden custom text fields define them. Buttons, links,
|
|
276
|
+
* and other controls don't count.
|
|
186
277
|
*/
|
|
187
|
-
function
|
|
188
|
-
|
|
189
|
-
if (isWindow(target) || target.nodeType === 9) return event.currentTarget === getTargetDocument(target).documentElement;
|
|
190
|
-
return isNode(event.target) && target.contains(event.target);
|
|
278
|
+
function isTypingEvent(event, target) {
|
|
279
|
+
return blocks(findBlocker(event, getTargetDocument(target), isInputElement), target);
|
|
191
280
|
}
|
|
192
281
|
|
|
193
282
|
//#endregion
|
|
194
|
-
export {
|
|
283
|
+
export { getTargetDocument, isDocumentLevel, isInputElement, isTypingEvent, shouldIgnoreInputEvent, targetHasFocus };
|
package/dist/_match.js
CHANGED
|
@@ -22,7 +22,7 @@ function matchKeyboardEvent(event, hotkey, platform = detectPlatform()) {
|
|
|
22
22
|
const direct = keysEqual(normalized.key, parsed.key);
|
|
23
23
|
const implicitShift = direct && isPrintableKey(normalized.key) && !/^\p{Letter}$/u.test(normalized.key) && !parsed.shift;
|
|
24
24
|
if (direct && modifiersMatch(normalized, parsed, implicitShift)) return matched("key", implicitShift ? 2 : 3);
|
|
25
|
-
if (
|
|
25
|
+
if (!positionStandsIn(normalized)) return noMatch;
|
|
26
26
|
if (normalized.altGraph) return noMatch;
|
|
27
27
|
if (!modifiersMatch(normalized, parsed, false)) return noMatch;
|
|
28
28
|
const fallbackKey = codeToLogicalKey(normalized.code);
|
|
@@ -39,6 +39,28 @@ function matchKeyboardEvent(event, hotkey, platform = detectPlatform()) {
|
|
|
39
39
|
};
|
|
40
40
|
}
|
|
41
41
|
}
|
|
42
|
+
/**
|
|
43
|
+
* Whether a key's conventional position may stand in for what it typed. Only
|
|
44
|
+
* output that can't name a shortcut falls back:
|
|
45
|
+
* - non-Latin letters and symbols, such as Russian `ы` or macOS Option's `–`
|
|
46
|
+
* - dead keys
|
|
47
|
+
* - anything on the digit row, where AZERTY types `&` and `é` for 1 and 2
|
|
48
|
+
* - ASCII symbols typed with Shift, such as `!` for Shift+1
|
|
49
|
+
* - Latin letters on punctuation keys with Control or Command, so German
|
|
50
|
+
* Cmd+ü still fires `Mod+[` (German types `[` with Option)
|
|
51
|
+
*
|
|
52
|
+
* A named key (`End` from a NumLock-off numpad), any other ASCII character,
|
|
53
|
+
* or a Latin letter typed on its own is the key the user meant, wherever it
|
|
54
|
+
* sits. AZERTY's `+` on the US `/` position is `+`, not `/`.
|
|
55
|
+
*/
|
|
56
|
+
function positionStandsIn({ key, code, ctrl, meta, shift, alt }) {
|
|
57
|
+
if (key !== "Dead" && !isPrintableKey(key)) return false;
|
|
58
|
+
if (code.startsWith("Digit")) return true;
|
|
59
|
+
if (isAsciiLetter(key)) return false;
|
|
60
|
+
if (/^[!-~]$/.test(key)) return shift;
|
|
61
|
+
if (!isLatinLetter(key) || alt) return true;
|
|
62
|
+
return (ctrl || meta) && !code.startsWith("Key");
|
|
63
|
+
}
|
|
42
64
|
/** Compares modifiers, excluding synthetic AltGr flags and optionally allowing Shift used to type a glyph. */
|
|
43
65
|
function modifiersMatch(event, parsed, allowImplicitShift) {
|
|
44
66
|
const ctrl = event.altGraph ? false : event.ctrl;
|
package/dist/app-hotkeys.js
CHANGED
|
@@ -227,13 +227,12 @@ var AppHotkeysStore = class {
|
|
|
227
227
|
if (list) list.push(registration);
|
|
228
228
|
else byId.set(id, [registration]);
|
|
229
229
|
}
|
|
230
|
-
const manager = getHotkeyManager();
|
|
231
230
|
return Object.values(this.store.state.appHotkeys).map((appHotkey) => {
|
|
232
231
|
const registered = byId.get(appHotkey.id) ?? [];
|
|
233
|
-
const
|
|
232
|
+
const has = (status) => registered.some((r) => r.status === status);
|
|
234
233
|
return {
|
|
235
234
|
appHotkey,
|
|
236
|
-
status: appHotkey.hotkey === null ? "unbound" : registered.length === 0 ? "inactive" :
|
|
235
|
+
status: appHotkey.hotkey === null ? "unbound" : registered.length === 0 ? "inactive" : has("active") ? "active" : has("suspended") ? "suspended" : "disabled",
|
|
237
236
|
isCustom: isCustomHotkey(appHotkey, this.#platform),
|
|
238
237
|
registrations: registered
|
|
239
238
|
};
|
package/dist/hotkey-manager.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { ConflictBehavior, HotkeyCallback, HotkeyCallbackContextFor, HotkeyChord, HotkeyMeta, HotkeySequence, ParsedHotkeyChord, RegisterableHotkey, RegisterableHotkeyChord, RegisterableHotkeySequence } from "./hotkey.types.js";
|
|
2
|
-
import { AppHotkeysStore } from "./app-hotkeys.js";
|
|
2
|
+
import { AppHotkey, AppHotkeysStore } from "./app-hotkeys.js";
|
|
3
3
|
import { Store } from "@tanstack/store";
|
|
4
4
|
//#region src/hotkey-manager.d.ts
|
|
5
5
|
/** An element, document, or window that hotkeys can listen on. */
|
|
@@ -41,9 +41,14 @@ export interface HotkeyOptions {
|
|
|
41
41
|
platform?: 'mac' | 'windows' | 'linux';
|
|
42
42
|
/**
|
|
43
43
|
* Prevent the default browser action when the hotkey matches. For a
|
|
44
|
-
* sequence, every step that advances it does. Defaults to true
|
|
44
|
+
* sequence, every step that advances it does. Defaults to true.
|
|
45
|
+
*
|
|
46
|
+
* `'always'` also prevents it while a chord can't fire because it is
|
|
47
|
+
* disabled or its scope is inactive or suspended, unless another hotkey
|
|
48
|
+
* handles the key. It never stops propagation for such a chord. Sequences
|
|
49
|
+
* treat `'always'` as `true`.
|
|
45
50
|
*/
|
|
46
|
-
preventDefault?: boolean;
|
|
51
|
+
preventDefault?: boolean | 'always';
|
|
47
52
|
/**
|
|
48
53
|
* Stop event propagation when the hotkey matches. For a sequence, only the
|
|
49
54
|
* last step does, so earlier steps reach other listeners. Defaults to true
|
|
@@ -118,8 +123,18 @@ export interface HotkeySequenceRegistration extends RegistrationBase<HotkeySeque
|
|
|
118
123
|
}
|
|
119
124
|
/** A registered hotkey in the HotkeyManager. */
|
|
120
125
|
export type HotkeyRegistration = HotkeyChordRegistration | HotkeySequenceRegistration;
|
|
126
|
+
/**
|
|
127
|
+
* Whether a registration is on. Focus isn't checked: a registration on an
|
|
128
|
+
* element also needs focus inside that element to fire.
|
|
129
|
+
* - `'active'`: enabled, and its scope is active
|
|
130
|
+
* - `'suspended'`: enabled, but its scope is inactive or suspended by an exclusive scope
|
|
131
|
+
* - `'disabled'`: registered with `enabled: false`
|
|
132
|
+
*/
|
|
133
|
+
export type HotkeyRegistrationStatus = 'active' | 'suspended' | 'disabled';
|
|
121
134
|
interface RegistrationViewBase {
|
|
122
135
|
id: string;
|
|
136
|
+
/** Whether the registration is enabled and in an active scope. */
|
|
137
|
+
status: HotkeyRegistrationStatus;
|
|
123
138
|
target: Target;
|
|
124
139
|
triggerCount: number;
|
|
125
140
|
hasFired: boolean;
|
|
@@ -372,6 +387,17 @@ export declare class HotkeyManager {
|
|
|
372
387
|
* @returns True if a matching registration exists
|
|
373
388
|
*/
|
|
374
389
|
isRegistered(hotkey: RegisterableHotkey, target?: Target): boolean;
|
|
390
|
+
/**
|
|
391
|
+
* Checks whether a hotkey can fire now: at least one registration is
|
|
392
|
+
* enabled, in an active scope, and listening where key presses go. A
|
|
393
|
+
* registration on `document` or `window` always is; one on an element is
|
|
394
|
+
* only while focus is inside that element. An app hotkey matches
|
|
395
|
+
* registrations by id, so custom hotkeys apply.
|
|
396
|
+
*
|
|
397
|
+
* @param hotkey - A chord, sequence, or app hotkey
|
|
398
|
+
* @returns True if a registration that can fire exists
|
|
399
|
+
*/
|
|
400
|
+
hasActiveRegistration(hotkey: RegisterableHotkey | AppHotkey): boolean;
|
|
375
401
|
/**
|
|
376
402
|
* Destroys the manager and removes all listeners.
|
|
377
403
|
*/
|
package/dist/hotkey-manager.js
CHANGED
|
@@ -5,7 +5,7 @@ import { isHotkeySequence, isModifierKey, parseHotkey, simplifyHotkey } from "./
|
|
|
5
5
|
import { formatHotkey } from "./format.js";
|
|
6
6
|
import { matchKeyboardEvent, parsedChordsEqual } from "./_match.js";
|
|
7
7
|
import { DEFAULT_SEQUENCE_TIMEOUT, areHotkeysEqual } from "./match.js";
|
|
8
|
-
import { getTargetDocument,
|
|
8
|
+
import { getTargetDocument, isDocumentLevel, shouldIgnoreInputEvent, targetHasFocus } from "./_event-target.js";
|
|
9
9
|
import { TargetListeners } from "./_target-listeners.js";
|
|
10
10
|
import { managerRef } from "./_manager-ref.js";
|
|
11
11
|
import { isRecordingEvent } from "./_recording-guard.js";
|
|
@@ -13,10 +13,16 @@ import { GLOBAL_SCOPE, defaultHotkeyOptions, getDefaultIgnoreInputs, handleConfl
|
|
|
13
13
|
import { Store } from "@tanstack/store";
|
|
14
14
|
|
|
15
15
|
//#region src/hotkey-manager.ts
|
|
16
|
+
/** Whether a registration can fire now. Without a manager, scopes don't apply. */
|
|
17
|
+
function getRegistrationStatus(options, manager) {
|
|
18
|
+
if (options.enabled === false) return "disabled";
|
|
19
|
+
return manager?.isScopeActive(options.scope) === false ? "suspended" : "active";
|
|
20
|
+
}
|
|
16
21
|
/** Creates a public view from an internal registration, stripping the callback. */
|
|
17
22
|
function toHotkeyRegistrationView(reg) {
|
|
18
23
|
const base = {
|
|
19
24
|
id: reg.id,
|
|
25
|
+
status: getRegistrationStatus(reg.options, managerRef.current),
|
|
20
26
|
target: reg.target,
|
|
21
27
|
triggerCount: reg.triggerCount,
|
|
22
28
|
hasFired: reg.hasFired
|
|
@@ -270,7 +276,20 @@ var HotkeyManager = class HotkeyManager {
|
|
|
270
276
|
}
|
|
271
277
|
/** Whether a registration can fire now: enabled, in an active scope. */
|
|
272
278
|
#isLive(registration) {
|
|
273
|
-
return registration.options
|
|
279
|
+
return getRegistrationStatus(registration.options, this) === "active";
|
|
280
|
+
}
|
|
281
|
+
/**
|
|
282
|
+
* Whether a hotkey that can fire would handle this key on any listener the
|
|
283
|
+
* event reaches, including other targets and phases. A sequence counts when
|
|
284
|
+
* the key matches its next step or its first step.
|
|
285
|
+
*/
|
|
286
|
+
#handledByLiveHotkey(event, eventType) {
|
|
287
|
+
const path = event.composedPath();
|
|
288
|
+
for (const registration of this.registrations.state.values()) {
|
|
289
|
+
if (!this.#isLive(registration) || registration.options.eventType !== eventType || !path.includes(registration.target) || ignoresInputs(registration) && shouldIgnoreInputEvent(event, registration.target)) continue;
|
|
290
|
+
if ((registration.kind === "chord" ? [registration.parsedChord] : [registration.parsedSequence[registration.currentIndex], registration.parsedSequence[0]]).some((step) => matchKeyboardEvent(event, step, registration.options.platform).matched)) return true;
|
|
291
|
+
}
|
|
292
|
+
return false;
|
|
274
293
|
}
|
|
275
294
|
/** Notifies subscribers after in-place updates such as counts and sequence progress. */
|
|
276
295
|
#notify() {
|
|
@@ -455,14 +474,20 @@ var HotkeyManager = class HotkeyManager {
|
|
|
455
474
|
if (isRecordingEvent(event) || this.#sequenceOwned.get(event)) return;
|
|
456
475
|
const matches = /* @__PURE__ */ new Map();
|
|
457
476
|
let bestScore = 0;
|
|
477
|
+
let reserved = false;
|
|
458
478
|
for (const registration of registrations) {
|
|
459
|
-
|
|
479
|
+
const live = this.#isLive(registration);
|
|
480
|
+
if (!live && registration.options.preventDefault !== "always" || registration.options.eventType !== eventType || ignoresInputs(registration) && shouldIgnoreInputEvent(event, target)) continue;
|
|
460
481
|
const result = matchKeyboardEvent(event, registration.parsedChord, registration.options.platform);
|
|
461
|
-
if (result.matched)
|
|
462
|
-
|
|
463
|
-
|
|
482
|
+
if (!result.matched) continue;
|
|
483
|
+
if (!live) {
|
|
484
|
+
reserved = true;
|
|
485
|
+
continue;
|
|
464
486
|
}
|
|
487
|
+
matches.set(registration.id, result);
|
|
488
|
+
bestScore = Math.max(bestScore, result.score);
|
|
465
489
|
}
|
|
490
|
+
if (reserved && matches.size === 0 && !this.#handledByLiveHotkey(event, eventType)) event.preventDefault();
|
|
466
491
|
let bestRank = -1;
|
|
467
492
|
for (const registration of registrations) if (matches.get(registration.id)?.score === bestScore) bestRank = Math.max(bestRank, this.#scopeRank(registration.options.scope));
|
|
468
493
|
for (const snapshot of registrations) {
|
|
@@ -508,7 +533,7 @@ var HotkeyManager = class HotkeyManager {
|
|
|
508
533
|
for (const registration of this.registrations.state.values()) {
|
|
509
534
|
if (registration.kind !== "sequence" || registration.currentIndex === 0 || !this.#isLive(registration) || registration.options.eventType !== eventType || now - registration.lastKeyTime > (registration.options.timeout ?? 1e3)) continue;
|
|
510
535
|
const target = registration.target;
|
|
511
|
-
if (path.includes(target
|
|
536
|
+
if (path.includes(isDocumentLevel(target) ? getTargetDocument(target) : target) && !(ignoresInputs(registration) && shouldIgnoreInputEvent(event, target)) && matchKeyboardEvent(event, registration.parsedSequence[registration.currentIndex], registration.options.platform).matched) return true;
|
|
512
537
|
}
|
|
513
538
|
return false;
|
|
514
539
|
}
|
|
@@ -531,7 +556,7 @@ var HotkeyManager = class HotkeyManager {
|
|
|
531
556
|
const candidates = /* @__PURE__ */ new Map();
|
|
532
557
|
let bestScore = 0;
|
|
533
558
|
for (const registration of registrations) {
|
|
534
|
-
if (!
|
|
559
|
+
if (!this.#isLive(registration) || ignoresInputs(registration) && shouldIgnoreInputEvent(event, target) || registration.options.eventType !== eventType) continue;
|
|
535
560
|
const timeout = registration.options.timeout ?? 1e3;
|
|
536
561
|
if (registration.currentIndex > 0 && now - registration.lastKeyTime > timeout) {
|
|
537
562
|
registration.currentIndex = 0;
|
|
@@ -679,6 +704,21 @@ var HotkeyManager = class HotkeyManager {
|
|
|
679
704
|
return false;
|
|
680
705
|
}
|
|
681
706
|
/**
|
|
707
|
+
* Checks whether a hotkey can fire now: at least one registration is
|
|
708
|
+
* enabled, in an active scope, and listening where key presses go. A
|
|
709
|
+
* registration on `document` or `window` always is; one on an element is
|
|
710
|
+
* only while focus is inside that element. An app hotkey matches
|
|
711
|
+
* registrations by id, so custom hotkeys apply.
|
|
712
|
+
*
|
|
713
|
+
* @param hotkey - A chord, sequence, or app hotkey
|
|
714
|
+
* @returns True if a registration that can fire exists
|
|
715
|
+
*/
|
|
716
|
+
hasActiveRegistration(hotkey) {
|
|
717
|
+
const id = typeof hotkey === "object" && "~appHotkey" in hotkey ? hotkey.id : void 0;
|
|
718
|
+
for (const registration of this.registrations.state.values()) if (this.#isLive(registration) && targetHasFocus(registration.target) && (id === void 0 ? areHotkeysEqual(registration.hotkey, hotkey, registration.options.platform) : registration.options.appHotkeyId === id)) return true;
|
|
719
|
+
return false;
|
|
720
|
+
}
|
|
721
|
+
/**
|
|
682
722
|
* Destroys the manager and removes all listeners.
|
|
683
723
|
*/
|
|
684
724
|
destroy() {
|
package/dist/hotkey-recorder.js
CHANGED
|
@@ -101,7 +101,7 @@ var HotkeyRecorder = class {
|
|
|
101
101
|
* and records chords according to `maxSteps`.
|
|
102
102
|
*/
|
|
103
103
|
start() {
|
|
104
|
-
if (this.#keydownHandler) return;
|
|
104
|
+
if (this.#keydownHandler || typeof document === "undefined") return;
|
|
105
105
|
this.#clearIdleTimer();
|
|
106
106
|
this.#events = [];
|
|
107
107
|
beginRecording(this);
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { CanonicalModifier, EditingKey, FunctionKey, IndividualKey, Key, LetterKey, LogicalKey, Modifier, NamedKey, NavigationKey, NonPunctuationKey, NormalizedKeyboardEvent, NumberKey, PhysicalKey, PhysicalKeyCode, PunctuationKey } from "./key.types.js";
|
|
2
2
|
import { ConflictBehavior, FourModifierHotkeyChord, Hotkey, HotkeyCallback, HotkeyCallbackContext, HotkeyCallbackContextFor, HotkeyChord, HotkeyChordCallbackContext, HotkeyMeta, HotkeySequence, HotkeySequenceCallbackContext, ParsedHotkeyChord, ParsedModifiers, RawHotkeyChord, RawModifiers, RegisterableHotkey, RegisterableHotkeyChord, RegisterableHotkeySequence, SingleModifierHotkeyChord, ThreeModifierHotkeyChord, TwoModifierHotkeyChord } from "./hotkey.types.js";
|
|
3
|
-
import { ActivateHotkeyScopeOptions, HotkeyChordOptions, HotkeyChordRegistration, HotkeyChordRegistrationView, HotkeyManager, HotkeyOptions, HotkeyOptionsFor, HotkeyRegistration, HotkeyRegistrationHandle, HotkeyRegistrationView, HotkeyScopesState, HotkeySequenceOptions, HotkeySequenceRegistration, HotkeySequenceRegistrationView, Target, getHotkeyManager, toHotkeyRegistrationView } from "./hotkey-manager.js";
|
|
3
|
+
import { ActivateHotkeyScopeOptions, HotkeyChordOptions, HotkeyChordRegistration, HotkeyChordRegistrationView, HotkeyManager, HotkeyOptions, HotkeyOptionsFor, HotkeyRegistration, HotkeyRegistrationHandle, HotkeyRegistrationStatus, HotkeyRegistrationView, HotkeyScopesState, HotkeySequenceOptions, HotkeySequenceRegistration, HotkeySequenceRegistrationView, Target, getHotkeyManager, toHotkeyRegistrationView } from "./hotkey-manager.js";
|
|
4
4
|
import { AppHotkey, AppHotkeyChordOptions, AppHotkeyDefaultKind, AppHotkeyId, AppHotkeyInput, AppHotkeyListItem, AppHotkeyOptions, AppHotkeySequenceOptions, AppHotkeyStatus, AppHotkeys, AppHotkeysState, AppHotkeysStore, AppHotkeysStoreOptions, CustomHotkeys, DefinedAppHotkeys, Register, RegisteredAppHotkeys, defineAppHotkeys, getAppHotkeyRegistrationOptions, isAppHotkey, isCustomHotkey, provideAppHotkeys, publishAppHotkeysStore, resolveAppHotkey } from "./app-hotkeys.js";
|
|
5
5
|
import { ALL_KEYS, EDITING_KEYS, FUNCTION_KEYS, LETTER_KEYS, MODIFIER_ALIASES, MODIFIER_KEYS, MODIFIER_ORDER, NAVIGATION_KEYS, NUMBER_KEYS, PUNCTUATION_CODE_MAP, PUNCTUATION_KEYS, isSingleLetterKey, normalizeKeyName } from "./constants.js";
|
|
6
6
|
import { KEY_DISPLAY_SYMBOLS, LINUX_MODIFIER_LABELS, MAC_MODIFIER_LABELS, MAC_MODIFIER_SYMBOLS, PUNCTUATION_KEY_DISPLAY_LABELS, WINDOWS_MODIFIER_LABELS } from "./display-labels.js";
|
|
@@ -13,4 +13,4 @@ import { HeldModifierOptions, matchesHeldModifiers } from "./hint.js";
|
|
|
13
13
|
import { HotkeyConflict, HotkeyConflictOptions, findHotkeyConflicts } from "./conflicts.js";
|
|
14
14
|
import { KeyStateTracker, KeyStateTrackerState, getKeyStateTracker } from "./key-state-tracker.js";
|
|
15
15
|
import { HotkeyChordRecorderOptions, HotkeyChordRecorderValidationContext, HotkeyRecorder, HotkeyRecorderCommitKeys, HotkeyRecorderOptions, HotkeyRecorderState, HotkeyRecorderValidationContext, HotkeySequenceRecorderOptions, HotkeySequenceRecorderValidationContext, RecorderKeyMode, RecorderOptions, RecorderRejection } from "./hotkey-recorder.js";
|
|
16
|
-
export { ALL_KEYS, ActivateHotkeyScopeOptions, AppHotkey, AppHotkeyChordOptions, AppHotkeyDefaultKind, AppHotkeyId, AppHotkeyInput, AppHotkeyListItem, AppHotkeyOptions, AppHotkeySequenceOptions, AppHotkeyStatus, AppHotkeys, AppHotkeysState, AppHotkeysStore, AppHotkeysStoreOptions, CanonicalModifier, ConflictBehavior, CreateHotkeyChordHandlerOptions, CustomHotkeys, DEFAULT_SEQUENCE_TIMEOUT, DefinedAppHotkeys, DisplayHotkey, DisplayHotkeyChord, EDITING_KEYS, EditingKey, FUNCTION_KEYS, FormatDisplayOptions, FourModifierHotkeyChord, FunctionKey, HeldModifierOptions, Hotkey, HotkeyCallback, HotkeyCallbackContext, HotkeyCallbackContextFor, HotkeyChord, HotkeyChordCallbackContext, HotkeyChordOptions, HotkeyChordRecorderOptions, HotkeyChordRecorderValidationContext, HotkeyChordRegistration, HotkeyChordRegistrationView, HotkeyConflict, HotkeyConflictOptions, HotkeyManager, HotkeyMeta, HotkeyOptions, HotkeyOptionsFor, HotkeyRecorder, HotkeyRecorderCommitKeys, HotkeyRecorderOptions, HotkeyRecorderState, HotkeyRecorderValidationContext, HotkeyRegistration, HotkeyRegistrationHandle, HotkeyRegistrationView, HotkeyScopesState, HotkeySequence, HotkeySequenceCallbackContext, HotkeySequenceOptions, HotkeySequenceRecorderOptions, HotkeySequenceRecorderValidationContext, HotkeySequenceRegistration, HotkeySequenceRegistrationView, IndividualKey, KEY_DISPLAY_SYMBOLS, Key, KeyStateTracker, KeyStateTrackerState, KeyboardEventMatch, LETTER_KEYS, LINUX_MODIFIER_LABELS, LetterKey, LogicalKey, MAC_MODIFIER_LABELS, MAC_MODIFIER_SYMBOLS, MODIFIER_ALIASES, MODIFIER_KEYS, MODIFIER_ORDER, Modifier, MultiHotkeyChordHandler, NAVIGATION_KEYS, NUMBER_KEYS, NamedKey, NavigationKey, NonPunctuationKey, NormalizedKeyboardEvent, NumberKey, PUNCTUATION_CODE_MAP, PUNCTUATION_KEYS, PUNCTUATION_KEY_DISPLAY_LABELS, ParsedHotkeyChord, ParsedModifiers, PhysicalKey, PhysicalKeyCode, PunctuationKey, RawHotkeyChord, RawModifiers, RecorderKeyMode, RecorderOptions, RecorderRejection, Register, RegisterableHotkey, RegisterableHotkeyChord, RegisterableHotkeySequence, RegisteredAppHotkeys, SingleModifierHotkeyChord, Target, ThreeModifierHotkeyChord, TwoModifierHotkeyChord, ValidatableHotkey, ValidationResult, WINDOWS_MODIFIER_LABELS, areHotkeysEqual, assertValidHotkey, checkHotkey, createHotkeyChordHandler, createMultiHotkeyChordHandler, createSequenceMatcher, defineAppHotkeys, detectPlatform, findHotkeyConflicts, formatForDisplay, formatHotkey, getAppHotkeyRegistrationOptions, getHotkeyManager, getKeyStateTracker, hasNonModifierKey, isAppHotkey, isCustomHotkey, isHotkeySequence, isModifierKey, isSingleLetterKey, matchesHeldModifiers, matchesKeyboardEvent, normalizeHotkey, normalizeKeyName, parseHotkey, parseKeyboardEvent, provideAppHotkeys, publishAppHotkeysStore, resolveAppHotkey, resolveModifier, simplifyHotkey, toHotkeyRegistrationView, validateHotkey };
|
|
16
|
+
export { ALL_KEYS, ActivateHotkeyScopeOptions, AppHotkey, AppHotkeyChordOptions, AppHotkeyDefaultKind, AppHotkeyId, AppHotkeyInput, AppHotkeyListItem, AppHotkeyOptions, AppHotkeySequenceOptions, AppHotkeyStatus, AppHotkeys, AppHotkeysState, AppHotkeysStore, AppHotkeysStoreOptions, CanonicalModifier, ConflictBehavior, CreateHotkeyChordHandlerOptions, CustomHotkeys, DEFAULT_SEQUENCE_TIMEOUT, DefinedAppHotkeys, DisplayHotkey, DisplayHotkeyChord, EDITING_KEYS, EditingKey, FUNCTION_KEYS, FormatDisplayOptions, FourModifierHotkeyChord, FunctionKey, HeldModifierOptions, Hotkey, HotkeyCallback, HotkeyCallbackContext, HotkeyCallbackContextFor, HotkeyChord, HotkeyChordCallbackContext, HotkeyChordOptions, HotkeyChordRecorderOptions, HotkeyChordRecorderValidationContext, HotkeyChordRegistration, HotkeyChordRegistrationView, HotkeyConflict, HotkeyConflictOptions, HotkeyManager, HotkeyMeta, HotkeyOptions, HotkeyOptionsFor, HotkeyRecorder, HotkeyRecorderCommitKeys, HotkeyRecorderOptions, HotkeyRecorderState, HotkeyRecorderValidationContext, HotkeyRegistration, HotkeyRegistrationHandle, HotkeyRegistrationStatus, HotkeyRegistrationView, HotkeyScopesState, HotkeySequence, HotkeySequenceCallbackContext, HotkeySequenceOptions, HotkeySequenceRecorderOptions, HotkeySequenceRecorderValidationContext, HotkeySequenceRegistration, HotkeySequenceRegistrationView, IndividualKey, KEY_DISPLAY_SYMBOLS, Key, KeyStateTracker, KeyStateTrackerState, KeyboardEventMatch, LETTER_KEYS, LINUX_MODIFIER_LABELS, LetterKey, LogicalKey, MAC_MODIFIER_LABELS, MAC_MODIFIER_SYMBOLS, MODIFIER_ALIASES, MODIFIER_KEYS, MODIFIER_ORDER, Modifier, MultiHotkeyChordHandler, NAVIGATION_KEYS, NUMBER_KEYS, NamedKey, NavigationKey, NonPunctuationKey, NormalizedKeyboardEvent, NumberKey, PUNCTUATION_CODE_MAP, PUNCTUATION_KEYS, PUNCTUATION_KEY_DISPLAY_LABELS, ParsedHotkeyChord, ParsedModifiers, PhysicalKey, PhysicalKeyCode, PunctuationKey, RawHotkeyChord, RawModifiers, RecorderKeyMode, RecorderOptions, RecorderRejection, Register, RegisterableHotkey, RegisterableHotkeyChord, RegisterableHotkeySequence, RegisteredAppHotkeys, SingleModifierHotkeyChord, Target, ThreeModifierHotkeyChord, TwoModifierHotkeyChord, ValidatableHotkey, ValidationResult, WINDOWS_MODIFIER_LABELS, areHotkeysEqual, assertValidHotkey, checkHotkey, createHotkeyChordHandler, createMultiHotkeyChordHandler, createSequenceMatcher, defineAppHotkeys, detectPlatform, findHotkeyConflicts, formatForDisplay, formatHotkey, getAppHotkeyRegistrationOptions, getHotkeyManager, getKeyStateTracker, hasNonModifierKey, isAppHotkey, isCustomHotkey, isHotkeySequence, isModifierKey, isSingleLetterKey, matchesHeldModifiers, matchesKeyboardEvent, normalizeHotkey, normalizeKeyName, parseHotkey, parseKeyboardEvent, provideAppHotkeys, publishAppHotkeysStore, resolveAppHotkey, resolveModifier, simplifyHotkey, toHotkeyRegistrationView, validateHotkey };
|
package/package.json
CHANGED
|
@@ -56,7 +56,7 @@ When nesting scopes, stacking dialogs, or keeping some hotkeys working during an
|
|
|
56
56
|
|
|
57
57
|
## Pitfalls
|
|
58
58
|
|
|
59
|
-
- **A disabled hotkey doesn't block the browser.** With `enabled: false`, `Mod+S` opens the Save Page dialog.
|
|
59
|
+
- **A disabled hotkey doesn't block the browser.** With `enabled: false`, `Mod+S` opens the Save Page dialog. Add `preventDefault: 'always'` to keep the action blocked while the hotkey can't fire, whether it's disabled or its scope is off. Skip it for hotkeys turned off to give the key back to the browser, such as `Mod+C`, `Mod+Z`, `Tab`, or single keys behind a user's off switch. Don't keep the hotkey enabled and return early from the callback; the hotkey would also stay `active` in app hotkey lists and show its hint.
|
|
60
60
|
- **Modifiers match exactly.** `ArrowDown` never fires while Shift is held, so register `Shift+ArrowDown` instead of checking `event.shiftKey`. Shift that a symbol needs is the exception: `?` matches Shift+/.
|
|
61
61
|
- **Chords repeat while held.** Set `requireReset: true` on toggles, or holding the keys flickers them.
|
|
62
62
|
- **Letters follow the typed character.** For position-based controls such as WASD, use physical bindings such as `'[KeyW]'`. Keep letters for mnemonic commands such as `Mod+S`.
|
|
@@ -9,3 +9,5 @@
|
|
|
9
9
|
- A hotkey's `scope` option, or an app hotkey's declared `scope`, overrides the provider's.
|
|
10
10
|
- Scopes follow the component tree, not the DOM, so a portaled dialog keeps its scope.
|
|
11
11
|
- The hotkey that opens an exclusive dialog is suspended with the rest of the page. To close the dialog with the same keys, register that hotkey inside the dialog too.
|
|
12
|
+
- To show which scope owns the keyboard, use the adapter's scope state API, such as `useHotkeyScopesState()` in React. Don't subscribe to `manager.scopes` by hand.
|
|
13
|
+
- To list only hotkeys that are on, filter the adapter's registrations by `status === 'active'`. `'suspended'` means the hotkey's scope is inactive or suspended, and `'disabled'` means `enabled: false`.
|