use-simple-keyboard 0.0.1 → 0.0.2
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 +8 -0
- package/dist/hooks/useHotkey.d.ts +6 -2
- package/dist/hooks/useHotkey.js +29 -18
- package/dist/hooks/useKeySequence.d.ts +2 -1
- package/dist/hooks/useKeySequence.js +13 -9
- package/dist/utils/domUtils.d.ts +2 -0
- package/dist/utils/domUtils.js +3 -0
- package/package.json +29 -12
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,13 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.0.2] - 2026-07-07
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
|
|
12
|
+
- **`useHotkeys` catch-all ordering**: Specific hotkeys now correctly take precedence over the empty string (`""`) catch-all, regardless of definition order in the record object.
|
|
13
|
+
- **`useKeySequence` re-triggering**: When `resetOnComplete` is set to `false`, completing a sequence now properly resets the internal tracking index to `0` while maintaining `matchedKeys` history, allowing the sequence to be re-entered and re-triggered without unmounting.
|
|
14
|
+
|
|
8
15
|
## [0.0.1] - 2026-05-13
|
|
9
16
|
|
|
10
17
|
### Added
|
|
@@ -24,4 +31,5 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
24
31
|
- Comprehensive test suite (72 tests, ~95% line coverage)
|
|
25
32
|
- Interactive demo application (`demos/`)
|
|
26
33
|
|
|
34
|
+
[0.0.2]: https://github.com/SkorpionG/use-simple-keyboard/compare/v0.0.1...v0.0.2
|
|
27
35
|
[0.0.1]: https://github.com/SkorpionG/use-simple-keyboard/releases/tag/v0.0.1
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type KeyboardEventTarget } from '../utils/domUtils';
|
|
1
2
|
import type { HotkeyInput, HotkeysRecord, HotkeyCallback } from '../types/keys';
|
|
2
3
|
export interface UseHotkeyOptions {
|
|
3
4
|
/**
|
|
@@ -19,7 +20,7 @@ export interface UseHotkeyOptions {
|
|
|
19
20
|
* Target element to attach the event listener to
|
|
20
21
|
* @default document
|
|
21
22
|
*/
|
|
22
|
-
target?:
|
|
23
|
+
target?: KeyboardEventTarget;
|
|
23
24
|
/**
|
|
24
25
|
* Event type to listen for
|
|
25
26
|
* @default 'keydown'
|
|
@@ -29,7 +30,10 @@ export interface UseHotkeyOptions {
|
|
|
29
30
|
/**
|
|
30
31
|
* React hook for handling keyboard shortcuts/hotkeys
|
|
31
32
|
*
|
|
32
|
-
* @param keys - Array of keys that should be pressed to trigger the callback
|
|
33
|
+
* @param keys - Array of keys or string containing keys that should be pressed to trigger the callback.
|
|
34
|
+
* Note: When passing an array, ensure it is a stable reference (e.g. via useMemo or defined outside the component)
|
|
35
|
+
* to prevent unnecessary effect executions on every render. If an array contains a single string with a '+',
|
|
36
|
+
* it will be parsed as a combined string (e.g., `['ctrl+c']` is identical to `'ctrl+c'`).
|
|
33
37
|
* @param callback - Function to execute when the hotkey is pressed
|
|
34
38
|
* @param options - Additional options for the hotkey behavior
|
|
35
39
|
*
|
package/dist/hooks/useHotkey.js
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
import { useEffect, useCallback, useRef } from 'react';
|
|
2
2
|
import { matchesHotkey, parseHotkey, createKeyInfo, } from '../utils/keyUtils';
|
|
3
|
+
import { getDefaultTarget } from '../utils/domUtils';
|
|
3
4
|
/**
|
|
4
5
|
* React hook for handling keyboard shortcuts/hotkeys
|
|
5
6
|
*
|
|
6
|
-
* @param keys - Array of keys that should be pressed to trigger the callback
|
|
7
|
+
* @param keys - Array of keys or string containing keys that should be pressed to trigger the callback.
|
|
8
|
+
* Note: When passing an array, ensure it is a stable reference (e.g. via useMemo or defined outside the component)
|
|
9
|
+
* to prevent unnecessary effect executions on every render. If an array contains a single string with a '+',
|
|
10
|
+
* it will be parsed as a combined string (e.g., `['ctrl+c']` is identical to `'ctrl+c'`).
|
|
7
11
|
* @param callback - Function to execute when the hotkey is pressed
|
|
8
12
|
* @param options - Additional options for the hotkey behavior
|
|
9
13
|
*
|
|
@@ -26,7 +30,7 @@ import { matchesHotkey, parseHotkey, createKeyInfo, } from '../utils/keyUtils';
|
|
|
26
30
|
* ```
|
|
27
31
|
*/
|
|
28
32
|
export const useHotkey = (keys, callback, options = {}) => {
|
|
29
|
-
const { enabled = true, preventDefault = true, stopPropagation = false, target =
|
|
33
|
+
const { enabled = true, preventDefault = true, stopPropagation = false, target = getDefaultTarget(), eventType = 'keydown', } = options;
|
|
30
34
|
// Convert keys to array and normalize
|
|
31
35
|
const normalizedKeys = useRef([]);
|
|
32
36
|
const callbackRef = useRef(callback);
|
|
@@ -50,6 +54,7 @@ export const useHotkey = (keys, callback, options = {}) => {
|
|
|
50
54
|
}
|
|
51
55
|
}, [keys]);
|
|
52
56
|
const handleKeyEvent = useCallback((event) => {
|
|
57
|
+
/* istanbul ignore next */
|
|
53
58
|
if (!enabled)
|
|
54
59
|
return;
|
|
55
60
|
const keyInfo = createKeyInfo(event);
|
|
@@ -114,13 +119,14 @@ export const useHotkey = (keys, callback, options = {}) => {
|
|
|
114
119
|
* ```
|
|
115
120
|
*/
|
|
116
121
|
export const useHotkeys = (hotkeys, options = {}) => {
|
|
117
|
-
const { enabled = true, preventDefault = true, stopPropagation = false, target =
|
|
122
|
+
const { enabled = true, preventDefault = true, stopPropagation = false, target = getDefaultTarget(), eventType = 'keydown', } = options;
|
|
118
123
|
const hotkeysRef = useRef(hotkeys);
|
|
119
124
|
// Update hotkeys ref when hotkeys change
|
|
120
125
|
useEffect(() => {
|
|
121
126
|
hotkeysRef.current = hotkeys;
|
|
122
127
|
}, [hotkeys]);
|
|
123
128
|
const handleKeyEvent = useCallback((event) => {
|
|
129
|
+
/* istanbul ignore next */
|
|
124
130
|
if (!enabled)
|
|
125
131
|
return;
|
|
126
132
|
const keyInfo = createKeyInfo(event);
|
|
@@ -132,10 +138,14 @@ export const useHotkeys = (hotkeys, options = {}) => {
|
|
|
132
138
|
altKey: event.altKey,
|
|
133
139
|
shiftKey: event.shiftKey,
|
|
134
140
|
};
|
|
135
|
-
//
|
|
141
|
+
// First pass: check specific hotkeys (non-empty keys)
|
|
142
|
+
// This ensures the catch-all never steals events that have a specific match.
|
|
143
|
+
let matched = false;
|
|
136
144
|
for (const [keys, callback] of Object.entries(hotkeysRef.current)) {
|
|
137
|
-
|
|
138
|
-
|
|
145
|
+
if (keys === '')
|
|
146
|
+
continue; // skip catch-all in first pass
|
|
147
|
+
const keyArray = keys.includes('+') ? parseHotkey(keys) : [keys];
|
|
148
|
+
if (matchesHotkey(keyEvent, keyArray)) {
|
|
139
149
|
if (preventDefault) {
|
|
140
150
|
event.preventDefault();
|
|
141
151
|
}
|
|
@@ -143,20 +153,21 @@ export const useHotkeys = (hotkeys, options = {}) => {
|
|
|
143
153
|
event.stopPropagation();
|
|
144
154
|
}
|
|
145
155
|
callback(event, keyInfo);
|
|
146
|
-
|
|
156
|
+
matched = true;
|
|
157
|
+
break;
|
|
147
158
|
}
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
break; // Only trigger the first matching hotkey
|
|
159
|
+
}
|
|
160
|
+
// Second pass: fire catch-all only if no specific hotkey matched
|
|
161
|
+
if (!matched) {
|
|
162
|
+
const catchAllCallback = hotkeysRef.current[''];
|
|
163
|
+
if (catchAllCallback !== undefined) {
|
|
164
|
+
if (preventDefault) {
|
|
165
|
+
event.preventDefault();
|
|
166
|
+
}
|
|
167
|
+
if (stopPropagation) {
|
|
168
|
+
event.stopPropagation();
|
|
159
169
|
}
|
|
170
|
+
catchAllCallback(event, keyInfo);
|
|
160
171
|
}
|
|
161
172
|
}
|
|
162
173
|
}, [enabled, preventDefault, stopPropagation]);
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type KeyboardEventTarget } from '../utils/domUtils';
|
|
1
2
|
import type { HotkeyInput, HotkeyCallback } from '../types/keys';
|
|
2
3
|
export interface UseKeySequenceOptions {
|
|
3
4
|
/**
|
|
@@ -19,7 +20,7 @@ export interface UseKeySequenceOptions {
|
|
|
19
20
|
* Target element to attach the event listener to
|
|
20
21
|
* @default document
|
|
21
22
|
*/
|
|
22
|
-
target?:
|
|
23
|
+
target?: KeyboardEventTarget;
|
|
23
24
|
/**
|
|
24
25
|
* Event type to listen for
|
|
25
26
|
* @default 'keydown'
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { useEffect, useCallback, useRef, useState } from 'react';
|
|
2
2
|
import { matchesHotkey, parseHotkey, createKeyInfo, } from '../utils/keyUtils';
|
|
3
|
+
import { getDefaultTarget } from '../utils/domUtils';
|
|
3
4
|
/**
|
|
4
5
|
* Hook for handling key sequences (e.g., Konami code, custom shortcuts)
|
|
5
6
|
*
|
|
@@ -28,7 +29,7 @@ import { matchesHotkey, parseHotkey, createKeyInfo, } from '../utils/keyUtils';
|
|
|
28
29
|
* ```
|
|
29
30
|
*/
|
|
30
31
|
export const useKeySequence = (sequence, callback, options = {}) => {
|
|
31
|
-
const { enabled = true, preventDefault = true, stopPropagation = false, target =
|
|
32
|
+
const { enabled = true, preventDefault = true, stopPropagation = false, target = getDefaultTarget(), eventType = 'keydown', timeout = 1000, resetOnComplete = true, strict = false, } = options;
|
|
32
33
|
const [state, setState] = useState({
|
|
33
34
|
currentIndex: 0,
|
|
34
35
|
isActive: false,
|
|
@@ -57,10 +58,9 @@ export const useKeySequence = (sequence, callback, options = {}) => {
|
|
|
57
58
|
});
|
|
58
59
|
}, [sequence]);
|
|
59
60
|
const resetSequence = useCallback(() => {
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
}
|
|
61
|
+
// clearTimeout is always safe to call even when no timer is active
|
|
62
|
+
clearTimeout(timeoutRef.current);
|
|
63
|
+
timeoutRef.current = null;
|
|
64
64
|
setState((prev) => ({
|
|
65
65
|
...prev,
|
|
66
66
|
currentIndex: 0,
|
|
@@ -96,9 +96,11 @@ export const useKeySequence = (sequence, callback, options = {}) => {
|
|
|
96
96
|
setState((prevState) => {
|
|
97
97
|
const { currentIndex } = prevState;
|
|
98
98
|
const expectedKeys = sequenceRef.current[currentIndex];
|
|
99
|
-
|
|
99
|
+
// Defensive guard: expectedKeys should always be defined here since
|
|
100
|
+
// currentIndex is always bounded to [0, sequence.length - 1].
|
|
101
|
+
/* istanbul ignore next */
|
|
102
|
+
if (!expectedKeys)
|
|
100
103
|
return prevState;
|
|
101
|
-
}
|
|
102
104
|
// Check if current key matches the expected key in sequence
|
|
103
105
|
const isMatch = matchesHotkey(keyEvent, expectedKeys);
|
|
104
106
|
if (isMatch) {
|
|
@@ -133,9 +135,11 @@ export const useKeySequence = (sequence, callback, options = {}) => {
|
|
|
133
135
|
};
|
|
134
136
|
}
|
|
135
137
|
else {
|
|
138
|
+
// Keep matchedKeys for history but reset currentIndex to 0 so
|
|
139
|
+
// the sequence can be re-entered and re-triggered.
|
|
136
140
|
return {
|
|
137
|
-
currentIndex:
|
|
138
|
-
isActive:
|
|
141
|
+
currentIndex: 0,
|
|
142
|
+
isActive: false,
|
|
139
143
|
matchedKeys: newMatchedKeys,
|
|
140
144
|
timeoutId: null,
|
|
141
145
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "use-simple-keyboard",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.2",
|
|
4
4
|
"description": "React hooks for handling keyboard inputs and hotkeys across different platforms",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "SkorpionG",
|
|
@@ -25,14 +25,22 @@
|
|
|
25
25
|
"build": "tsc",
|
|
26
26
|
"dev": "tsc --watch",
|
|
27
27
|
"typecheck": "tsc --project tsconfig.typecheck.json",
|
|
28
|
+
"typecheck:all": "npm run typecheck && npm --prefix demos run typecheck",
|
|
28
29
|
"test": "jest --config jest.config.ts",
|
|
29
30
|
"test:watch": "jest --config jest.config.ts --watch",
|
|
30
31
|
"test:coverage": "jest --config jest.config.ts --coverage",
|
|
32
|
+
"test:coverage:text": "jest --config jest.config.ts --coverage --coverageReporters='text'",
|
|
31
33
|
"lint": "eslint .",
|
|
34
|
+
"lint:all": "npm run lint && npm --prefix demos run lint",
|
|
32
35
|
"lint:fix": "eslint . --fix",
|
|
33
|
-
"
|
|
34
|
-
"format
|
|
35
|
-
"
|
|
36
|
+
"lint:fix:all": "npm run lint:fix && npm --prefix demos run lint:fix",
|
|
37
|
+
"format": "prettier --write .",
|
|
38
|
+
"format:check": "prettier --check .",
|
|
39
|
+
"format:all": "npm run format && npm --prefix demos run format",
|
|
40
|
+
"format:check:all": "npm run format:check && npm --prefix demos run format:check",
|
|
41
|
+
"prepublishOnly": "npm run format && npm run typecheck && npm run lint && npm run test && npm run build",
|
|
42
|
+
"prepare": "husky",
|
|
43
|
+
"husky:chmod": "chmod +x .husky/pre-commit"
|
|
36
44
|
},
|
|
37
45
|
"keywords": [
|
|
38
46
|
"react",
|
|
@@ -45,25 +53,34 @@
|
|
|
45
53
|
"peerDependencies": {
|
|
46
54
|
"react": ">=16.8.0"
|
|
47
55
|
},
|
|
56
|
+
"overrides": {
|
|
57
|
+
"eslint": "$eslint"
|
|
58
|
+
},
|
|
48
59
|
"devDependencies": {
|
|
49
|
-
"@eslint/js": "^
|
|
60
|
+
"@eslint/js": "^10.0.1",
|
|
61
|
+
"@testing-library/dom": "^10.4.1",
|
|
50
62
|
"@testing-library/jest-dom": "^6.9.1",
|
|
51
63
|
"@testing-library/react": "^16.3.2",
|
|
52
64
|
"@testing-library/user-event": "^14.6.1",
|
|
53
65
|
"@types/jest": "^30.0.0",
|
|
54
|
-
"@types/react": "^19.2.
|
|
55
|
-
"@typescript-eslint/eslint-plugin": "^8.
|
|
56
|
-
"@typescript-eslint/parser": "^8.
|
|
57
|
-
"eslint": "^
|
|
66
|
+
"@types/react": "^19.2.17",
|
|
67
|
+
"@typescript-eslint/eslint-plugin": "^8.63.0",
|
|
68
|
+
"@typescript-eslint/parser": "^8.63.0",
|
|
69
|
+
"eslint": "^10.6.0",
|
|
58
70
|
"eslint-config-prettier": "^10.1.8",
|
|
59
|
-
"eslint-plugin-prettier": "^5.5.
|
|
71
|
+
"eslint-plugin-prettier": "^5.5.6",
|
|
60
72
|
"eslint-plugin-react": "^7.37.5",
|
|
61
73
|
"eslint-plugin-react-hooks": "^7.1.1",
|
|
74
|
+
"husky": "^9.1.7",
|
|
62
75
|
"jest": "^30.4.2",
|
|
63
76
|
"jest-environment-jsdom": "^30.4.1",
|
|
64
77
|
"jiti": "^2.7.0",
|
|
65
|
-
"
|
|
66
|
-
"
|
|
78
|
+
"lint-staged": "^16.4.0",
|
|
79
|
+
"prettier": "^3.9.4",
|
|
80
|
+
"react": "^19.2.7",
|
|
81
|
+
"react-dom": "^19.2.7",
|
|
82
|
+
"ts-jest": "^29.4.11",
|
|
83
|
+
"ts-node": "^10.9.2",
|
|
67
84
|
"typescript": "^6.0.3"
|
|
68
85
|
}
|
|
69
86
|
}
|