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 +27 -0
- package/LICENSE +21 -0
- package/README.md +301 -0
- package/dist/hooks/useHotkey.d.ts +73 -0
- package/dist/hooks/useHotkey.js +173 -0
- package/dist/hooks/useKeySequence.d.ts +79 -0
- package/dist/hooks/useKeySequence.js +205 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +7 -0
- package/dist/types/keys.d.ts +27 -0
- package/dist/types/keys.js +4 -0
- package/dist/utils/keyUtils.d.ts +47 -0
- package/dist/utils/keyUtils.js +157 -0
- package/package.json +69 -0
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
|
+
[](https://www.npmjs.com/package/use-simple-keyboard)
|
|
6
|
+
[](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
|
+
};
|
package/dist/index.d.ts
ADDED
|
@@ -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,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
|
+
}
|