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 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?: HTMLElement | Document | Window | null;
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
  *
@@ -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 = typeof document !== 'undefined' ? document : null, eventType = 'keydown', } = options;
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 = typeof document !== 'undefined' ? document : null, eventType = 'keydown', } = options;
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
- // Check each hotkey combination
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
- // Handle empty string for catch-all
138
- if (keys === '') {
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
- break; // Only trigger the first matching hotkey
156
+ matched = true;
157
+ break;
147
158
  }
148
- else {
149
- const keyArray = keys.includes('+') ? parseHotkey(keys) : [keys];
150
- if (matchesHotkey(keyEvent, keyArray)) {
151
- if (preventDefault) {
152
- event.preventDefault();
153
- }
154
- if (stopPropagation) {
155
- event.stopPropagation();
156
- }
157
- callback(event, keyInfo);
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?: HTMLElement | Document | Window | null;
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 = typeof document !== 'undefined' ? document : null, eventType = 'keydown', timeout = 1000, resetOnComplete = true, strict = false, } = options;
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
- if (timeoutRef.current) {
61
- clearTimeout(timeoutRef.current);
62
- timeoutRef.current = null;
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
- if (!expectedKeys) {
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: newIndex,
138
- isActive: true,
141
+ currentIndex: 0,
142
+ isActive: false,
139
143
  matchedKeys: newMatchedKeys,
140
144
  timeoutId: null,
141
145
  };
@@ -0,0 +1,2 @@
1
+ export type KeyboardEventTarget = HTMLElement | Document | Window | null;
2
+ export declare const getDefaultTarget: () => Document | null;
@@ -0,0 +1,3 @@
1
+ export const getDefaultTarget = () =>
2
+ /* istanbul ignore next -- SSR fallback is not reachable in the jsdom test environment */
3
+ typeof document !== 'undefined' ? document : null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "use-simple-keyboard",
3
- "version": "0.0.1",
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
- "format": "prettier --write \"{src,test}/**/*.{ts,tsx}\" \"*.{json,md}\"",
34
- "format:check": "prettier --check \"{src,test}/**/*.{ts,tsx}\" \"*.{json,md}\"",
35
- "prepublishOnly": "npm run format && npm run typecheck && npm run lint && npm run test && npm run build"
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": "^9.36.0",
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.14",
55
- "@typescript-eslint/eslint-plugin": "^8.59.3",
56
- "@typescript-eslint/parser": "^8.59.3",
57
- "eslint": "^9.36.0",
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.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
- "prettier": "^3.8.3",
66
- "ts-jest": "^29.4.9",
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
  }