use-simple-keyboard 0.0.1

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 ADDED
@@ -0,0 +1,27 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [0.0.1] - 2026-05-13
9
+
10
+ ### Added
11
+
12
+ - `useHotkey` hook — listen for a single key or key combination (`ctrl+c`, `meta+s`, `escape`, etc.)
13
+ - `useHotkeys` hook — register multiple hotkeys at once via a key→callback record
14
+ - `useKeySequence` hook — detect sequential key presses (e.g. Konami code), with configurable timeout, strict mode, and reset-on-complete
15
+ - Full TypeScript definitions including `UseHotkeyOptions`, `UseKeySequenceOptions`, `KeySequenceState`, `HotkeyCallback`, `KeyInfo`, and all key types
16
+ - Cross-platform key normalization (`normalizeKey`, `parseHotkey`, `matchesHotkey`, `keysToHotkey`, `createKeyInfo`)
17
+ - `getPlatformModifier` utility to detect Cmd (macOS) vs Ctrl (Windows/Linux)
18
+ - Support for `keydown`, `keyup`, and `keypress` event types
19
+ - `enabled` option to conditionally activate/deactivate hooks without unmounting
20
+ - `preventDefault` and `stopPropagation` options per hook
21
+ - Custom `target` element support (defaults to `document`)
22
+ - Catch-all listener via empty string key (`""`)
23
+ - Zero runtime dependencies — only a React peer dependency (`>=16.8.0`)
24
+ - Comprehensive test suite (72 tests, ~95% line coverage)
25
+ - Interactive demo application (`demos/`)
26
+
27
+ [0.0.1]: https://github.com/SkorpionG/use-simple-keyboard/releases/tag/v0.0.1
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 SkorpionG2000
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,301 @@
1
+ # useSimpleKeyboard
2
+
3
+ A simple library of React hooks for handling keyboard inputs and hotkeys across different platforms and languages.
4
+
5
+ [![npm version](https://img.shields.io/npm/v/use-simple-keyboard.svg)](https://www.npmjs.com/package/use-simple-keyboard)
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
7
+
8
+ ## Features
9
+
10
+ - 🎯 **Cross-platform support** - Works consistently across Windows, macOS, and Linux
11
+ - 🌍 **Multi-language keyboard support** - Handles different keyboard layouts and languages
12
+ - ⚡ **Lightweight** - Zero dependencies (except React peer dependency)
13
+ - 🔧 **TypeScript support** - Full TypeScript definitions included
14
+ - 🎨 **Flexible API** - Support for single hotkeys and multiple hotkey combinations
15
+ - 🛡️ **Event handling** - Built-in preventDefault and stopPropagation options
16
+
17
+ ## Installation
18
+
19
+ ```bash
20
+ npm install use-simple-keyboard
21
+ ```
22
+
23
+ ## Quick Start
24
+
25
+ ```tsx
26
+ import React from 'react';
27
+ import { useHotkey } from 'use-simple-keyboard';
28
+
29
+ function App() {
30
+ // Simple hotkey
31
+ useHotkey(['ctrl', 'c'], () => {
32
+ console.log('Copy triggered!');
33
+ });
34
+
35
+ // Platform-specific hotkey (Cmd on Mac, Ctrl on others)
36
+ useHotkey(['meta', 's'], () => {
37
+ console.log('Save triggered!');
38
+ });
39
+
40
+ return <div>Press Ctrl+C or Cmd+S (Mac)</div>;
41
+ }
42
+ ```
43
+
44
+ ## API Reference
45
+
46
+ ### `useHotkey(keys, callback, options?)`
47
+
48
+ The main hook for handling single hotkey combinations.
49
+
50
+ #### Parameters
51
+
52
+ - **keys**: `string | string[]` - The key combination to listen for
53
+ - **callback**: `(event: KeyboardEvent) => void` - Function to execute when hotkey is pressed
54
+ - **options**: `UseHotkeyOptions` - Optional configuration
55
+
56
+ #### Key Formats
57
+
58
+ You can specify keys in multiple formats:
59
+
60
+ ```tsx
61
+ // Array format
62
+ useHotkey(['ctrl', 'c'], callback);
63
+ useHotkey(['ctrl', 'shift', 'z'], callback);
64
+
65
+ // String format with '+' separator
66
+ useHotkey('ctrl+c', callback);
67
+ useHotkey('ctrl+shift+z', callback);
68
+
69
+ // Single key
70
+ useHotkey('escape', callback);
71
+ useHotkey(['f1'], callback);
72
+ ```
73
+
74
+ #### Supported Keys
75
+
76
+ **Modifier Keys:**
77
+
78
+ - `ctrl` / `control` - Control key
79
+ - `meta` / `cmd` / `command` - Command key (Mac) / Windows key
80
+ - `alt` - Alt key
81
+ - `shift` - Shift key
82
+
83
+ **Special Keys:**
84
+
85
+ - `enter` / `return`
86
+ - `escape` / `esc`
87
+ - `space` / `spacebar`
88
+ - `tab`
89
+ - `backspace`
90
+ - `delete` / `del`
91
+ - `insert`
92
+ - `home`, `end`
93
+ - `pageup`, `pagedown`
94
+ - `arrowup`, `arrowdown`, `arrowleft`, `arrowright` (or `up`, `down`, `left`, `right`)
95
+
96
+ **Function Keys:**
97
+
98
+ - `f1` through `f12`
99
+
100
+ **Regular Keys:**
101
+
102
+ - Any letter: `a`, `b`, `c`, etc.
103
+ - Any number: `1`, `2`, `3`, etc.
104
+ - Any symbol: `+`, `-`, `=`, etc.
105
+
106
+ #### Options
107
+
108
+ ```tsx
109
+ interface UseHotkeyOptions {
110
+ enabled?: boolean; // Default: true
111
+ preventDefault?: boolean; // Default: true
112
+ stopPropagation?: boolean; // Default: false
113
+ target?: HTMLElement | Document | Window | null; // Default: document
114
+ eventType?: 'keydown' | 'keyup' | 'keypress'; // Default: 'keydown'
115
+ }
116
+ ```
117
+
118
+ ### `useHotkeys(hotkeys, options?)`
119
+
120
+ Hook for handling multiple hotkeys at once.
121
+
122
+ ```tsx
123
+ import { useHotkeys } from 'use-simple-keyboard';
124
+
125
+ function Editor() {
126
+ useHotkeys({
127
+ 'ctrl+c': () => copy(),
128
+ 'ctrl+v': () => paste(),
129
+ 'ctrl+z': () => undo(),
130
+ 'ctrl+shift+z': () => redo(),
131
+ 'ctrl+s': () => save(),
132
+ escape: () => closeModal(),
133
+ });
134
+
135
+ return <div>Text Editor</div>;
136
+ }
137
+ ```
138
+
139
+ ## Usage Examples
140
+
141
+ ### Basic Hotkeys
142
+
143
+ ```tsx
144
+ import { useHotkey } from 'use-simple-keyboard';
145
+
146
+ function BasicExample() {
147
+ useHotkey(['ctrl', 'c'], () => alert('Copy!'));
148
+ useHotkey(['ctrl', 'v'], () => alert('Paste!'));
149
+ useHotkey('escape', () => alert('Escape pressed!'));
150
+
151
+ return <div>Try Ctrl+C, Ctrl+V, or Escape</div>;
152
+ }
153
+ ```
154
+
155
+ ### Platform-Specific Hotkeys
156
+
157
+ ```tsx
158
+ function PlatformExample() {
159
+ // Use 'meta' for Cmd on Mac, Ctrl on others
160
+ useHotkey(['meta', 's'], () => save());
161
+
162
+ // Or handle both explicitly
163
+ useHotkey(['ctrl', 's'], () => save()); // Windows/Linux
164
+ useHotkey(['cmd', 's'], () => save()); // Mac
165
+
166
+ return <div>Press Cmd+S (Mac) or Ctrl+S (Windows/Linux)</div>;
167
+ }
168
+ ```
169
+
170
+ ### Conditional Hotkeys
171
+
172
+ ```tsx
173
+ function ConditionalExample() {
174
+ const [isEditing, setIsEditing] = useState(false);
175
+
176
+ useHotkey(['ctrl', 'e'], () => setIsEditing(true), {
177
+ enabled: !isEditing,
178
+ });
179
+
180
+ useHotkey('escape', () => setIsEditing(false), {
181
+ enabled: isEditing,
182
+ });
183
+
184
+ return (
185
+ <div>
186
+ {isEditing ? 'Editing mode - Press Escape' : 'Press Ctrl+E to edit'}
187
+ </div>
188
+ );
189
+ }
190
+ ```
191
+
192
+ ### Scoped Hotkeys
193
+
194
+ ```tsx
195
+ function ScopedExample() {
196
+ const modalRef = useRef<HTMLDivElement>(null);
197
+
198
+ // Only listen for hotkeys within the modal
199
+ useHotkey('escape', () => closeModal(), {
200
+ target: modalRef.current,
201
+ });
202
+
203
+ return (
204
+ <div ref={modalRef} tabIndex={-1}>
205
+ Modal content - Press Escape to close
206
+ </div>
207
+ );
208
+ }
209
+ ```
210
+
211
+ ### Complex Combinations
212
+
213
+ ```tsx
214
+ function ComplexExample() {
215
+ // Multiple modifier keys
216
+ useHotkey(['ctrl', 'shift', 'alt', 'r'], () => {
217
+ console.log('Super reload!');
218
+ });
219
+
220
+ // Function keys
221
+ useHotkey('f12', () => openDevTools());
222
+
223
+ // Number keys
224
+ useHotkey(['ctrl', '1'], () => switchToTab(1));
225
+ useHotkey(['ctrl', '2'], () => switchToTab(2));
226
+
227
+ return <div>Try various key combinations</div>;
228
+ }
229
+ ```
230
+
231
+ ### Global App Hotkeys
232
+
233
+ ```tsx
234
+ function App() {
235
+ useHotkeys({
236
+ // File operations
237
+ 'ctrl+n': () => newFile(),
238
+ 'ctrl+o': () => openFile(),
239
+ 'ctrl+s': () => saveFile(),
240
+
241
+ // Edit operations
242
+ 'ctrl+z': () => undo(),
243
+ 'ctrl+y': () => redo(),
244
+ 'ctrl+shift+z': () => redo(), // Alternative redo
245
+
246
+ // Navigation
247
+ 'ctrl+f': () => openSearch(),
248
+ 'ctrl+g': () => findNext(),
249
+ 'ctrl+shift+g': () => findPrevious(),
250
+
251
+ // Application
252
+ f11: () => toggleFullscreen(),
253
+ 'ctrl+shift+i': () => openDevTools(),
254
+ });
255
+
256
+ return <YourAppContent />;
257
+ }
258
+ ```
259
+
260
+ ## Cross-Platform Considerations
261
+
262
+ The library automatically handles platform differences:
263
+
264
+ - **macOS**: Uses `meta` key (Cmd) for system shortcuts
265
+ - **Windows/Linux**: Uses `ctrl` key for system shortcuts
266
+ - **Key normalization**: Handles different key representations across browsers
267
+ - **Language support**: Works with different keyboard layouts and languages
268
+
269
+ ## TypeScript Support
270
+
271
+ The library is written in TypeScript and includes full type definitions:
272
+
273
+ ```tsx
274
+ import { useHotkey, UseHotkeyOptions, KeyEvent } from 'use-simple-keyboard';
275
+
276
+ const options: UseHotkeyOptions = {
277
+ enabled: true,
278
+ preventDefault: true,
279
+ target: document.body,
280
+ };
281
+
282
+ useHotkey(
283
+ ['ctrl', 'c'],
284
+ (event: KeyboardEvent) => {
285
+ // event is properly typed
286
+ console.log(event.key);
287
+ },
288
+ options
289
+ );
290
+ ```
291
+
292
+ ## Browser Support
293
+
294
+ - Chrome/Chromium 60+
295
+ - Firefox 55+
296
+ - Safari 12+
297
+ - Edge 79+
298
+
299
+ ## License
300
+
301
+ MIT
@@ -0,0 +1,73 @@
1
+ import type { HotkeyInput, HotkeysRecord, HotkeyCallback } from '../types/keys';
2
+ export interface UseHotkeyOptions {
3
+ /**
4
+ * Whether the hotkey should be enabled
5
+ * @default true
6
+ */
7
+ enabled?: boolean;
8
+ /**
9
+ * Whether to prevent the default browser behavior when the hotkey is triggered
10
+ * @default true
11
+ */
12
+ preventDefault?: boolean;
13
+ /**
14
+ * Whether to stop event propagation when the hotkey is triggered
15
+ * @default false
16
+ */
17
+ stopPropagation?: boolean;
18
+ /**
19
+ * Target element to attach the event listener to
20
+ * @default document
21
+ */
22
+ target?: HTMLElement | Document | Window | null;
23
+ /**
24
+ * Event type to listen for
25
+ * @default 'keydown'
26
+ */
27
+ eventType?: 'keydown' | 'keyup' | 'keypress';
28
+ }
29
+ /**
30
+ * React hook for handling keyboard shortcuts/hotkeys
31
+ *
32
+ * @param keys - Array of keys that should be pressed to trigger the callback
33
+ * @param callback - Function to execute when the hotkey is pressed
34
+ * @param options - Additional options for the hotkey behavior
35
+ *
36
+ * @example
37
+ * ```tsx
38
+ * // Simple hotkey
39
+ * useHotkey(['ctrl', 'c'], () => console.log('Copy!'));
40
+ *
41
+ * // Multiple key combination
42
+ * useHotkey(['ctrl', 'shift', 'z'], () => console.log('Redo!'));
43
+ *
44
+ * // Platform-specific (cmd on Mac, ctrl on others)
45
+ * useHotkey(['meta', 's'], () => console.log('Save!'));
46
+ *
47
+ * // With options
48
+ * useHotkey(['escape'], handleEscape, {
49
+ * preventDefault: false,
50
+ * enabled: isModalOpen
51
+ * });
52
+ * ```
53
+ */
54
+ export declare const useHotkey: (keys: HotkeyInput, callback: HotkeyCallback, options?: UseHotkeyOptions) => void;
55
+ /**
56
+ * Hook for handling multiple hotkeys at once
57
+ *
58
+ * @param hotkeys - Object mapping hotkey combinations to their callbacks
59
+ * @param options - Global options applied to all hotkeys
60
+ *
61
+ * @example
62
+ * ```tsx
63
+ * useHotkeys({
64
+ * 'ctrl+c': () => copy(),
65
+ * 'ctrl+v': () => paste(),
66
+ * 'ctrl+z': () => undo(),
67
+ * 'ctrl+shift+z': () => redo(),
68
+ * 'escape': () => closeModal()
69
+ * });
70
+ * ```
71
+ */
72
+ export declare const useHotkeys: (hotkeys: HotkeysRecord, options?: UseHotkeyOptions) => void;
73
+ export default useHotkey;
@@ -0,0 +1,173 @@
1
+ import { useEffect, useCallback, useRef } from 'react';
2
+ import { matchesHotkey, parseHotkey, createKeyInfo, } from '../utils/keyUtils';
3
+ /**
4
+ * React hook for handling keyboard shortcuts/hotkeys
5
+ *
6
+ * @param keys - Array of keys that should be pressed to trigger the callback
7
+ * @param callback - Function to execute when the hotkey is pressed
8
+ * @param options - Additional options for the hotkey behavior
9
+ *
10
+ * @example
11
+ * ```tsx
12
+ * // Simple hotkey
13
+ * useHotkey(['ctrl', 'c'], () => console.log('Copy!'));
14
+ *
15
+ * // Multiple key combination
16
+ * useHotkey(['ctrl', 'shift', 'z'], () => console.log('Redo!'));
17
+ *
18
+ * // Platform-specific (cmd on Mac, ctrl on others)
19
+ * useHotkey(['meta', 's'], () => console.log('Save!'));
20
+ *
21
+ * // With options
22
+ * useHotkey(['escape'], handleEscape, {
23
+ * preventDefault: false,
24
+ * enabled: isModalOpen
25
+ * });
26
+ * ```
27
+ */
28
+ export const useHotkey = (keys, callback, options = {}) => {
29
+ const { enabled = true, preventDefault = true, stopPropagation = false, target = typeof document !== 'undefined' ? document : null, eventType = 'keydown', } = options;
30
+ // Convert keys to array and normalize
31
+ const normalizedKeys = useRef([]);
32
+ const callbackRef = useRef(callback);
33
+ // Update callback ref when callback changes
34
+ useEffect(() => {
35
+ callbackRef.current = callback;
36
+ }, [callback]);
37
+ // Parse and normalize keys
38
+ useEffect(() => {
39
+ const keyArray = Array.isArray(keys) ? keys : [keys];
40
+ // Handle empty string for all keys
41
+ if (keyArray.length === 1 && keyArray[0] === '') {
42
+ normalizedKeys.current = [''];
43
+ }
44
+ // If it's a single string with '+', parse it as a hotkey combination
45
+ else if (keyArray.length === 1 && keyArray[0].includes('+')) {
46
+ normalizedKeys.current = parseHotkey(keyArray[0]);
47
+ }
48
+ else {
49
+ normalizedKeys.current = keyArray.map((key) => key.toLowerCase().trim());
50
+ }
51
+ }, [keys]);
52
+ const handleKeyEvent = useCallback((event) => {
53
+ if (!enabled)
54
+ return;
55
+ const keyInfo = createKeyInfo(event);
56
+ // Check if this is a catch-all listener (empty string)
57
+ const isCatchAll = normalizedKeys.current.length === 1 && normalizedKeys.current[0] === '';
58
+ if (isCatchAll) {
59
+ // Trigger for all key presses
60
+ if (preventDefault) {
61
+ event.preventDefault();
62
+ }
63
+ if (stopPropagation) {
64
+ event.stopPropagation();
65
+ }
66
+ callbackRef.current(event, keyInfo);
67
+ }
68
+ else {
69
+ // Normal hotkey matching
70
+ const keyEvent = {
71
+ key: event.key,
72
+ code: event.code,
73
+ ctrlKey: event.ctrlKey,
74
+ metaKey: event.metaKey,
75
+ altKey: event.altKey,
76
+ shiftKey: event.shiftKey,
77
+ };
78
+ if (matchesHotkey(keyEvent, normalizedKeys.current)) {
79
+ if (preventDefault) {
80
+ event.preventDefault();
81
+ }
82
+ if (stopPropagation) {
83
+ event.stopPropagation();
84
+ }
85
+ callbackRef.current(event, keyInfo);
86
+ }
87
+ }
88
+ }, [enabled, preventDefault, stopPropagation]);
89
+ useEffect(() => {
90
+ if (!target || !enabled)
91
+ return;
92
+ const element = target;
93
+ element.addEventListener(eventType, handleKeyEvent);
94
+ return () => {
95
+ element.removeEventListener(eventType, handleKeyEvent);
96
+ };
97
+ }, [target, enabled, eventType, handleKeyEvent]);
98
+ };
99
+ /**
100
+ * Hook for handling multiple hotkeys at once
101
+ *
102
+ * @param hotkeys - Object mapping hotkey combinations to their callbacks
103
+ * @param options - Global options applied to all hotkeys
104
+ *
105
+ * @example
106
+ * ```tsx
107
+ * useHotkeys({
108
+ * 'ctrl+c': () => copy(),
109
+ * 'ctrl+v': () => paste(),
110
+ * 'ctrl+z': () => undo(),
111
+ * 'ctrl+shift+z': () => redo(),
112
+ * 'escape': () => closeModal()
113
+ * });
114
+ * ```
115
+ */
116
+ export const useHotkeys = (hotkeys, options = {}) => {
117
+ const { enabled = true, preventDefault = true, stopPropagation = false, target = typeof document !== 'undefined' ? document : null, eventType = 'keydown', } = options;
118
+ const hotkeysRef = useRef(hotkeys);
119
+ // Update hotkeys ref when hotkeys change
120
+ useEffect(() => {
121
+ hotkeysRef.current = hotkeys;
122
+ }, [hotkeys]);
123
+ const handleKeyEvent = useCallback((event) => {
124
+ if (!enabled)
125
+ return;
126
+ const keyInfo = createKeyInfo(event);
127
+ const keyEvent = {
128
+ key: event.key,
129
+ code: event.code,
130
+ ctrlKey: event.ctrlKey,
131
+ metaKey: event.metaKey,
132
+ altKey: event.altKey,
133
+ shiftKey: event.shiftKey,
134
+ };
135
+ // Check each hotkey combination
136
+ for (const [keys, callback] of Object.entries(hotkeysRef.current)) {
137
+ // Handle empty string for catch-all
138
+ if (keys === '') {
139
+ if (preventDefault) {
140
+ event.preventDefault();
141
+ }
142
+ if (stopPropagation) {
143
+ event.stopPropagation();
144
+ }
145
+ callback(event, keyInfo);
146
+ break; // Only trigger the first matching hotkey
147
+ }
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
+ }
161
+ }
162
+ }, [enabled, preventDefault, stopPropagation]);
163
+ useEffect(() => {
164
+ if (!target || !enabled)
165
+ return;
166
+ const element = target;
167
+ element.addEventListener(eventType, handleKeyEvent);
168
+ return () => {
169
+ element.removeEventListener(eventType, handleKeyEvent);
170
+ };
171
+ }, [target, enabled, eventType, handleKeyEvent]);
172
+ };
173
+ export default useHotkey;
@@ -0,0 +1,79 @@
1
+ import type { HotkeyInput, HotkeyCallback } from '../types/keys';
2
+ export interface UseKeySequenceOptions {
3
+ /**
4
+ * Whether the key sequence should be enabled
5
+ * @default true
6
+ */
7
+ enabled?: boolean;
8
+ /**
9
+ * Whether to prevent the default browser behavior when keys in the sequence are triggered
10
+ * @default true
11
+ */
12
+ preventDefault?: boolean;
13
+ /**
14
+ * Whether to stop event propagation when keys in the sequence are triggered
15
+ * @default false
16
+ */
17
+ stopPropagation?: boolean;
18
+ /**
19
+ * Target element to attach the event listener to
20
+ * @default document
21
+ */
22
+ target?: HTMLElement | Document | Window | null;
23
+ /**
24
+ * Event type to listen for
25
+ * @default 'keydown'
26
+ */
27
+ eventType?: 'keydown' | 'keyup';
28
+ /**
29
+ * Time in milliseconds to wait for the next key in the sequence
30
+ * If exceeded, the sequence resets
31
+ * @default 1000
32
+ */
33
+ timeout?: number;
34
+ /**
35
+ * Whether to reset the sequence after successful completion
36
+ * @default true
37
+ */
38
+ resetOnComplete?: boolean;
39
+ /**
40
+ * Whether to allow partial matches to continue the sequence
41
+ * If false, any non-matching key will reset the sequence
42
+ * @default false
43
+ */
44
+ strict?: boolean;
45
+ }
46
+ export interface KeySequenceState {
47
+ currentIndex: number;
48
+ isActive: boolean;
49
+ matchedKeys: string[];
50
+ timeoutId: number | null;
51
+ }
52
+ /**
53
+ * Hook for handling key sequences (e.g., Konami code, custom shortcuts)
54
+ *
55
+ * @param sequence - Array of keys/hotkey combinations that must be pressed in order
56
+ * @param callback - Function to call when the sequence is completed
57
+ * @param options - Configuration options
58
+ *
59
+ * @example
60
+ * ```tsx
61
+ * // Simple sequence: up, up, down, down
62
+ * useKeySequence(['arrowup', 'arrowup', 'arrowdown', 'arrowdown'], () => {
63
+ * console.log('Konami code entered!');
64
+ * });
65
+ *
66
+ * // Mixed sequence with hotkeys
67
+ * useKeySequence(['ctrl+c', 'ctrl+v', 'enter'], () => {
68
+ * console.log('Copy-paste-enter sequence!');
69
+ * });
70
+ *
71
+ * // With options
72
+ * useKeySequence(['a', 'b', 'c'], handleSequence, {
73
+ * timeout: 2000,
74
+ * strict: true,
75
+ * resetOnComplete: false
76
+ * });
77
+ * ```
78
+ */
79
+ export declare const useKeySequence: (sequence: HotkeyInput[], callback: HotkeyCallback, options?: UseKeySequenceOptions) => KeySequenceState;
@@ -0,0 +1,205 @@
1
+ import { useEffect, useCallback, useRef, useState } from 'react';
2
+ import { matchesHotkey, parseHotkey, createKeyInfo, } from '../utils/keyUtils';
3
+ /**
4
+ * Hook for handling key sequences (e.g., Konami code, custom shortcuts)
5
+ *
6
+ * @param sequence - Array of keys/hotkey combinations that must be pressed in order
7
+ * @param callback - Function to call when the sequence is completed
8
+ * @param options - Configuration options
9
+ *
10
+ * @example
11
+ * ```tsx
12
+ * // Simple sequence: up, up, down, down
13
+ * useKeySequence(['arrowup', 'arrowup', 'arrowdown', 'arrowdown'], () => {
14
+ * console.log('Konami code entered!');
15
+ * });
16
+ *
17
+ * // Mixed sequence with hotkeys
18
+ * useKeySequence(['ctrl+c', 'ctrl+v', 'enter'], () => {
19
+ * console.log('Copy-paste-enter sequence!');
20
+ * });
21
+ *
22
+ * // With options
23
+ * useKeySequence(['a', 'b', 'c'], handleSequence, {
24
+ * timeout: 2000,
25
+ * strict: true,
26
+ * resetOnComplete: false
27
+ * });
28
+ * ```
29
+ */
30
+ 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 [state, setState] = useState({
33
+ currentIndex: 0,
34
+ isActive: false,
35
+ matchedKeys: [],
36
+ timeoutId: null,
37
+ });
38
+ const sequenceRef = useRef([]);
39
+ const callbackRef = useRef(callback);
40
+ const timeoutRef = useRef(null);
41
+ // Update callback ref when callback changes
42
+ useEffect(() => {
43
+ callbackRef.current = callback;
44
+ }, [callback]);
45
+ // Parse and normalize sequence
46
+ useEffect(() => {
47
+ sequenceRef.current = sequence.map((keys) => {
48
+ if (Array.isArray(keys)) {
49
+ return keys.map((key) => key.toLowerCase().trim());
50
+ }
51
+ else if (typeof keys === 'string' && keys.includes('+')) {
52
+ return parseHotkey(keys);
53
+ }
54
+ else {
55
+ return [keys.toString().toLowerCase().trim()];
56
+ }
57
+ });
58
+ }, [sequence]);
59
+ const resetSequence = useCallback(() => {
60
+ if (timeoutRef.current) {
61
+ clearTimeout(timeoutRef.current);
62
+ timeoutRef.current = null;
63
+ }
64
+ setState((prev) => ({
65
+ ...prev,
66
+ currentIndex: 0,
67
+ isActive: false,
68
+ matchedKeys: [],
69
+ timeoutId: null,
70
+ }));
71
+ }, []);
72
+ const startTimeout = useCallback(() => {
73
+ if (timeoutRef.current) {
74
+ clearTimeout(timeoutRef.current);
75
+ }
76
+ timeoutRef.current = window.setTimeout(() => {
77
+ resetSequence();
78
+ }, timeout);
79
+ setState((prev) => ({
80
+ ...prev,
81
+ timeoutId: timeoutRef.current,
82
+ }));
83
+ }, [timeout, resetSequence]);
84
+ const handleKeyEvent = useCallback((event) => {
85
+ if (!enabled || sequenceRef.current.length === 0)
86
+ return;
87
+ const keyInfo = createKeyInfo(event);
88
+ const keyEvent = {
89
+ key: event.key,
90
+ code: event.code,
91
+ ctrlKey: event.ctrlKey,
92
+ metaKey: event.metaKey,
93
+ altKey: event.altKey,
94
+ shiftKey: event.shiftKey,
95
+ };
96
+ setState((prevState) => {
97
+ const { currentIndex } = prevState;
98
+ const expectedKeys = sequenceRef.current[currentIndex];
99
+ if (!expectedKeys) {
100
+ return prevState;
101
+ }
102
+ // Check if current key matches the expected key in sequence
103
+ const isMatch = matchesHotkey(keyEvent, expectedKeys);
104
+ if (isMatch) {
105
+ const newIndex = currentIndex + 1;
106
+ const newMatchedKeys = [
107
+ ...prevState.matchedKeys,
108
+ keyInfo.combination,
109
+ ];
110
+ // Prevent default and stop propagation if configured
111
+ if (preventDefault) {
112
+ event.preventDefault();
113
+ }
114
+ if (stopPropagation) {
115
+ event.stopPropagation();
116
+ }
117
+ // Check if sequence is complete
118
+ if (newIndex >= sequenceRef.current.length) {
119
+ // Sequence completed!
120
+ callbackRef.current(event, keyInfo);
121
+ // Clear timeout
122
+ if (timeoutRef.current) {
123
+ clearTimeout(timeoutRef.current);
124
+ timeoutRef.current = null;
125
+ }
126
+ // Reset or keep sequence based on options
127
+ if (resetOnComplete) {
128
+ return {
129
+ currentIndex: 0,
130
+ isActive: false,
131
+ matchedKeys: [],
132
+ timeoutId: null,
133
+ };
134
+ }
135
+ else {
136
+ return {
137
+ currentIndex: newIndex,
138
+ isActive: true,
139
+ matchedKeys: newMatchedKeys,
140
+ timeoutId: null,
141
+ };
142
+ }
143
+ }
144
+ else {
145
+ // Continue sequence
146
+ startTimeout();
147
+ return {
148
+ currentIndex: newIndex,
149
+ isActive: true,
150
+ matchedKeys: newMatchedKeys,
151
+ timeoutId: timeoutRef.current,
152
+ };
153
+ }
154
+ }
155
+ else {
156
+ // Key doesn't match
157
+ if (strict || currentIndex === 0) {
158
+ // Reset sequence in strict mode or if no progress made
159
+ if (timeoutRef.current) {
160
+ clearTimeout(timeoutRef.current);
161
+ timeoutRef.current = null;
162
+ }
163
+ return {
164
+ currentIndex: 0,
165
+ isActive: false,
166
+ matchedKeys: [],
167
+ timeoutId: null,
168
+ };
169
+ }
170
+ else {
171
+ // In non-strict mode, just continue without resetting
172
+ return prevState;
173
+ }
174
+ }
175
+ });
176
+ }, [
177
+ enabled,
178
+ preventDefault,
179
+ stopPropagation,
180
+ startTimeout,
181
+ strict,
182
+ resetOnComplete,
183
+ ]);
184
+ useEffect(() => {
185
+ if (!target || !enabled)
186
+ return;
187
+ const element = target;
188
+ element.addEventListener(eventType, handleKeyEvent);
189
+ return () => {
190
+ element.removeEventListener(eventType, handleKeyEvent);
191
+ if (timeoutRef.current) {
192
+ clearTimeout(timeoutRef.current);
193
+ }
194
+ };
195
+ }, [target, enabled, eventType, handleKeyEvent]);
196
+ // Cleanup timeout on unmount
197
+ useEffect(() => {
198
+ return () => {
199
+ if (timeoutRef.current) {
200
+ clearTimeout(timeoutRef.current);
201
+ }
202
+ };
203
+ }, []);
204
+ return state;
205
+ };
@@ -0,0 +1,5 @@
1
+ export { useHotkey, useHotkeys, type UseHotkeyOptions, } from './hooks/useHotkey';
2
+ export { useKeySequence, type UseKeySequenceOptions, type KeySequenceState, } from './hooks/useKeySequence';
3
+ export type { ModifierKey, SpecialKey, ArrowKey, LetterKey, NumberKey, FunctionKey, SymbolKey, SupportedKey, HotkeyString, HotkeyInput, HotkeysRecord, KeyInfo, HotkeyCallback, } from './types/keys';
4
+ export { normalizeKey, getPlatformModifier, parseHotkey, matchesHotkey, keysToHotkey, createKeyInfo, type KeyEvent, } from './utils/keyUtils';
5
+ export { default } from './hooks/useHotkey';
package/dist/index.js ADDED
@@ -0,0 +1,7 @@
1
+ // Main exports
2
+ export { useHotkey, useHotkeys, } from './hooks/useHotkey';
3
+ export { useKeySequence, } from './hooks/useKeySequence';
4
+ // Utility exports for advanced usage
5
+ export { normalizeKey, getPlatformModifier, parseHotkey, matchesHotkey, keysToHotkey, createKeyInfo, } from './utils/keyUtils';
6
+ // Re-export default
7
+ export { default } from './hooks/useHotkey';
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Type definitions for supported keyboard keys
3
+ */
4
+ export type ModifierKey = 'ctrl' | 'meta' | 'alt' | 'shift' | 'cmd' | 'command' | 'control';
5
+ export type SpecialKey = 'enter' | 'return' | 'escape' | 'esc' | 'space' | 'spacebar' | 'tab' | 'backspace' | 'delete' | 'del' | 'insert' | 'home' | 'end' | 'pageup' | 'pagedown';
6
+ export type ArrowKey = 'arrowup' | 'arrowdown' | 'arrowleft' | 'arrowright' | 'up' | 'down' | 'left' | 'right';
7
+ export type LetterKey = 'a' | 'b' | 'c' | 'd' | 'e' | 'f' | 'g' | 'h' | 'i' | 'j' | 'k' | 'l' | 'm' | 'n' | 'o' | 'p' | 'q' | 'r' | 's' | 't' | 'u' | 'v' | 'w' | 'x' | 'y' | 'z';
8
+ export type NumberKey = '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9';
9
+ export type FunctionKey = 'f1' | 'f2' | 'f3' | 'f4' | 'f5' | 'f6' | 'f7' | 'f8' | 'f9' | 'f10' | 'f11' | 'f12';
10
+ export type SymbolKey = '`' | '~' | '!' | '@' | '#' | '$' | '%' | '^' | '&' | '*' | '(' | ')' | '-' | '_' | '=' | '+' | '[' | '{' | ']' | '}' | '\\' | '|' | ';' | ':' | "'" | '"' | ',' | '<' | '.' | '>' | '/' | '?';
11
+ export type SupportedKey = ModifierKey | SpecialKey | ArrowKey | LetterKey | NumberKey | FunctionKey | SymbolKey;
12
+ export interface KeyInfo {
13
+ key: string;
14
+ code: string;
15
+ normalizedKey: string;
16
+ modifiers: {
17
+ ctrl: boolean;
18
+ meta: boolean;
19
+ alt: boolean;
20
+ shift: boolean;
21
+ };
22
+ combination: string;
23
+ }
24
+ export type HotkeyCallback = (event: KeyboardEvent, keyInfo: KeyInfo) => void;
25
+ export type HotkeyString = `${string}+${string}` | SupportedKey | '';
26
+ export type HotkeyInput = SupportedKey | SupportedKey[] | string | string[] | '';
27
+ export type HotkeysRecord = Record<HotkeyString, HotkeyCallback> | Record<string, HotkeyCallback>;
@@ -0,0 +1,4 @@
1
+ /**
2
+ * Type definitions for supported keyboard keys
3
+ */
4
+ export {};
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Utility functions for normalizing keyboard events across different platforms and languages
3
+ */
4
+ export interface KeyEvent {
5
+ key: string;
6
+ code: string;
7
+ ctrlKey: boolean;
8
+ metaKey: boolean;
9
+ altKey: boolean;
10
+ shiftKey: boolean;
11
+ }
12
+ /**
13
+ * Normalize key names to handle cross-platform differences
14
+ */
15
+ export declare const normalizeKey: (key: string) => string;
16
+ /**
17
+ * Get the platform-specific modifier key (Cmd on Mac, Ctrl on others)
18
+ */
19
+ export declare const getPlatformModifier: () => string;
20
+ /**
21
+ * Parse a hotkey string into normalized components
22
+ * Examples: "ctrl+c", "cmd+shift+z", "alt+f4"
23
+ */
24
+ export declare const parseHotkey: (hotkey: string) => string[];
25
+ /**
26
+ * Check if the current key event matches the target hotkey combination
27
+ */
28
+ export declare const matchesHotkey: (event: KeyEvent, targetKeys: string[]) => boolean;
29
+ /**
30
+ * Convert array of keys to a normalized hotkey string
31
+ */
32
+ export declare const keysToHotkey: (keys: string[]) => string;
33
+ /**
34
+ * Create KeyInfo object from a keyboard event
35
+ */
36
+ export declare const createKeyInfo: (event: KeyboardEvent) => {
37
+ key: string;
38
+ code: string;
39
+ normalizedKey: string;
40
+ modifiers: {
41
+ ctrl: boolean;
42
+ meta: boolean;
43
+ alt: boolean;
44
+ shift: boolean;
45
+ };
46
+ combination: string;
47
+ };
@@ -0,0 +1,157 @@
1
+ /**
2
+ * Utility functions for normalizing keyboard events across different platforms and languages
3
+ */
4
+ /**
5
+ * Normalize key names to handle cross-platform differences
6
+ */
7
+ export const normalizeKey = (key) => {
8
+ const keyMap = {
9
+ // Handle different representations of common keys
10
+ Control: 'ctrl',
11
+ Meta: 'meta',
12
+ Alt: 'alt',
13
+ Shift: 'shift',
14
+ Enter: 'enter',
15
+ Return: 'enter',
16
+ Escape: 'escape',
17
+ Esc: 'escape',
18
+ Space: 'space',
19
+ ' ': 'space',
20
+ Spacebar: 'space',
21
+ Tab: 'tab',
22
+ Backspace: 'backspace',
23
+ Delete: 'delete',
24
+ Del: 'delete',
25
+ Insert: 'insert',
26
+ Home: 'home',
27
+ End: 'end',
28
+ PageUp: 'pageup',
29
+ PageDown: 'pagedown',
30
+ ArrowUp: 'arrowup',
31
+ ArrowDown: 'arrowdown',
32
+ ArrowLeft: 'arrowleft',
33
+ ArrowRight: 'arrowright',
34
+ Up: 'arrowup',
35
+ Down: 'arrowdown',
36
+ Left: 'arrowleft',
37
+ Right: 'arrowright',
38
+ };
39
+ // Convert to lowercase for consistent comparison
40
+ const normalizedKey = key.toLowerCase();
41
+ // Return mapped key or original normalized key
42
+ return keyMap[key] || normalizedKey;
43
+ };
44
+ /**
45
+ * Get the platform-specific modifier key (Cmd on Mac, Ctrl on others)
46
+ */
47
+ export const getPlatformModifier = () => {
48
+ const isMac = typeof navigator !== 'undefined' &&
49
+ /Mac|iPod|iPhone|iPad/.test(navigator.platform);
50
+ return isMac ? 'meta' : 'ctrl';
51
+ };
52
+ /**
53
+ * Parse a hotkey string into normalized components
54
+ * Examples: "ctrl+c", "cmd+shift+z", "alt+f4"
55
+ */
56
+ export const parseHotkey = (hotkey) => {
57
+ return hotkey
58
+ .toLowerCase()
59
+ .split('+')
60
+ .map((key) => key.trim())
61
+ .map((key) => {
62
+ // Handle platform-specific aliases
63
+ if (key === 'cmd')
64
+ return 'meta';
65
+ if (key === 'command')
66
+ return 'meta';
67
+ if (key === 'control')
68
+ return 'ctrl';
69
+ return normalizeKey(key);
70
+ });
71
+ };
72
+ /**
73
+ * Check if the current key event matches the target hotkey combination
74
+ */
75
+ export const matchesHotkey = (event, targetKeys) => {
76
+ const normalizedEventKey = normalizeKey(event.key);
77
+ const normalizedTargetKeys = targetKeys.map((key) => normalizeKey(key));
78
+ // Check if all modifier keys match
79
+ const hasCtrl = event.ctrlKey;
80
+ const hasMeta = event.metaKey;
81
+ const hasAlt = event.altKey;
82
+ const hasShift = event.shiftKey;
83
+ const expectedCtrl = normalizedTargetKeys.includes('ctrl');
84
+ const expectedMeta = normalizedTargetKeys.includes('meta');
85
+ const expectedAlt = normalizedTargetKeys.includes('alt');
86
+ const expectedShift = normalizedTargetKeys.includes('shift');
87
+ // Modifier keys must match exactly
88
+ if (hasCtrl !== expectedCtrl ||
89
+ hasMeta !== expectedMeta ||
90
+ hasAlt !== expectedAlt ||
91
+ hasShift !== expectedShift) {
92
+ return false;
93
+ }
94
+ // Find the non-modifier key
95
+ const nonModifierKeys = normalizedTargetKeys.filter((key) => !['ctrl', 'meta', 'alt', 'shift'].includes(key));
96
+ // Should have exactly one non-modifier key
97
+ if (nonModifierKeys.length !== 1) {
98
+ return false;
99
+ }
100
+ // Check if the main key matches
101
+ return normalizedEventKey === nonModifierKeys[0];
102
+ };
103
+ /**
104
+ * Convert array of keys to a normalized hotkey string
105
+ */
106
+ export const keysToHotkey = (keys) => {
107
+ const normalizedKeys = keys.map((key) => normalizeKey(key));
108
+ // Sort modifiers in a consistent order
109
+ const modifiers = [];
110
+ const nonModifiers = [];
111
+ for (const key of normalizedKeys) {
112
+ if (['ctrl', 'meta', 'alt', 'shift'].includes(key)) {
113
+ modifiers.push(key);
114
+ }
115
+ else {
116
+ nonModifiers.push(key);
117
+ }
118
+ }
119
+ // Sort modifiers in standard order
120
+ const sortedModifiers = modifiers.sort((a, b) => {
121
+ const order = ['ctrl', 'meta', 'alt', 'shift'];
122
+ return order.indexOf(a) - order.indexOf(b);
123
+ });
124
+ return [...sortedModifiers, ...nonModifiers].join('+');
125
+ };
126
+ /**
127
+ * Create KeyInfo object from a keyboard event
128
+ */
129
+ export const createKeyInfo = (event) => {
130
+ const normalizedKey = normalizeKey(event.key);
131
+ const modifiers = {
132
+ ctrl: event.ctrlKey,
133
+ meta: event.metaKey,
134
+ alt: event.altKey,
135
+ shift: event.shiftKey,
136
+ };
137
+ // Build combination string
138
+ const modifierKeys = [];
139
+ if (modifiers.ctrl)
140
+ modifierKeys.push('ctrl');
141
+ if (modifiers.meta)
142
+ modifierKeys.push('meta');
143
+ if (modifiers.alt)
144
+ modifierKeys.push('alt');
145
+ if (modifiers.shift)
146
+ modifierKeys.push('shift');
147
+ const combination = modifierKeys.length > 0
148
+ ? `${modifierKeys.join('+')}+${normalizedKey}`
149
+ : normalizedKey;
150
+ return {
151
+ key: event.key,
152
+ code: event.code,
153
+ normalizedKey,
154
+ modifiers,
155
+ combination,
156
+ };
157
+ };
package/package.json ADDED
@@ -0,0 +1,69 @@
1
+ {
2
+ "name": "use-simple-keyboard",
3
+ "version": "0.0.1",
4
+ "description": "React hooks for handling keyboard inputs and hotkeys across different platforms",
5
+ "license": "MIT",
6
+ "author": "SkorpionG",
7
+ "homepage": "https://github.com/SkorpionG/use-simple-keyboard#readme",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/SkorpionG/use-simple-keyboard.git"
11
+ },
12
+ "bugs": {
13
+ "url": "https://github.com/SkorpionG/use-simple-keyboard/issues"
14
+ },
15
+ "type": "module",
16
+ "main": "dist/index.js",
17
+ "module": "dist/index.js",
18
+ "types": "dist/index.d.ts",
19
+ "files": [
20
+ "dist",
21
+ "README.md",
22
+ "CHANGELOG.md"
23
+ ],
24
+ "scripts": {
25
+ "build": "tsc",
26
+ "dev": "tsc --watch",
27
+ "typecheck": "tsc --project tsconfig.typecheck.json",
28
+ "test": "jest --config jest.config.ts",
29
+ "test:watch": "jest --config jest.config.ts --watch",
30
+ "test:coverage": "jest --config jest.config.ts --coverage",
31
+ "lint": "eslint .",
32
+ "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
+ },
37
+ "keywords": [
38
+ "react",
39
+ "hooks",
40
+ "keyboard",
41
+ "hotkeys",
42
+ "shortcuts",
43
+ "input"
44
+ ],
45
+ "peerDependencies": {
46
+ "react": ">=16.8.0"
47
+ },
48
+ "devDependencies": {
49
+ "@eslint/js": "^9.36.0",
50
+ "@testing-library/jest-dom": "^6.9.1",
51
+ "@testing-library/react": "^16.3.2",
52
+ "@testing-library/user-event": "^14.6.1",
53
+ "@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",
58
+ "eslint-config-prettier": "^10.1.8",
59
+ "eslint-plugin-prettier": "^5.5.5",
60
+ "eslint-plugin-react": "^7.37.5",
61
+ "eslint-plugin-react-hooks": "^7.1.1",
62
+ "jest": "^30.4.2",
63
+ "jest-environment-jsdom": "^30.4.1",
64
+ "jiti": "^2.7.0",
65
+ "prettier": "^3.8.3",
66
+ "ts-jest": "^29.4.9",
67
+ "typescript": "^6.0.3"
68
+ }
69
+ }