react-hotkeys-hook 5.0.0-0 → 5.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.
@@ -1,5 +1,6 @@
1
1
  export default function useRecordHotkeys(): readonly [Set<string>, {
2
2
  readonly start: () => void;
3
3
  readonly stop: () => void;
4
+ readonly resetKeys: () => void;
4
5
  readonly isRecording: boolean;
5
6
  }];
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "react-hotkeys-hook",
3
3
  "description": "React hook for handling keyboard shortcuts",
4
- "version": "5.0.0-0",
4
+ "version": "5.0.0-1",
5
5
  "repository": {
6
6
  "type": "git",
7
7
  "url": "https://github.com/JohannesKlauss/react-keymap-hook.git"
@@ -88,7 +88,7 @@
88
88
  "@testing-library/react": "14.1.2",
89
89
  "@testing-library/user-event": "14.5.2",
90
90
  "@types/jest": "29.5.11",
91
- "@types/react": "18.2.46",
91
+ "@types/react": "18.2.47",
92
92
  "@types/react-dom": "18.2.18",
93
93
  "@typescript-eslint/eslint-plugin": "5.60.0",
94
94
  "@typescript-eslint/parser": "5.60.0",
@@ -2,23 +2,21 @@ import { isHotkeyModifier, mapKey } from './parseHotkeys'
2
2
  ;(() => {
3
3
  if (typeof document !== 'undefined') {
4
4
  document.addEventListener('keydown', (e) => {
5
- if (e.key === undefined) {
5
+ if (e.code === undefined) {
6
6
  // Synthetic event (e.g., Chrome autofill). Ignore.
7
7
  return
8
8
  }
9
9
 
10
- console.log('keydown', e.key, mapKey(e.key), e.key.length)
11
-
12
- pushToCurrentlyPressedKeys([mapKey(e.key)])
10
+ pushToCurrentlyPressedKeys([mapKey(e.code)])
13
11
  })
14
12
 
15
13
  document.addEventListener('keyup', (e) => {
16
- if (e.key === undefined) {
14
+ if (e.code === undefined) {
17
15
  // Synthetic event (e.g., Chrome autofill). Ignore.
18
16
  return
19
17
  }
20
18
 
21
- removeFromCurrentlyPressedKeys([mapKey(e.key)])
19
+ removeFromCurrentlyPressedKeys([mapKey(e.code)])
22
20
  })
23
21
  }
24
22
 
@@ -36,8 +34,8 @@ export function isReadonlyArray(value: unknown): value is readonly unknown[] {
36
34
  return Array.isArray(value)
37
35
  }
38
36
 
39
- export function isHotkeyPressed(key: string | readonly string[], splitKey = ','): boolean {
40
- const hotkeyArray = isReadonlyArray(key) ? key : key.split(splitKey)
37
+ export function isHotkeyPressed(key: string | readonly string[], delimiter = ','): boolean {
38
+ const hotkeyArray = isReadonlyArray(key) ? key : key.split(delimiter)
41
39
 
42
40
  return hotkeyArray.every((hotkey) => currentlyPressedKeys.has(hotkey.trim().toLowerCase()))
43
41
  }
@@ -1,6 +1,6 @@
1
1
  import { Hotkey, KeyboardModifiers } from './types'
2
2
 
3
- const reservedModifierKeywords = ['shift', 'alt', 'meta', 'mod', 'ctrl']
3
+ const reservedModifierKeywords = ['shift', 'alt', 'meta', 'mod', 'ctrl', 'control']
4
4
 
5
5
  const mappedKeys: Record<string, string> = {
6
6
  esc: 'escape',
@@ -9,7 +9,6 @@ const mappedKeys: Record<string, string> = {
9
9
  right: 'arrowright',
10
10
  up: 'arrowup',
11
11
  down: 'arrowdown',
12
- space: ' ',
13
12
  ShiftLeft: 'shift',
14
13
  ShiftRight: 'shift',
15
14
  AltLeft: 'alt',
@@ -30,14 +29,14 @@ export function isHotkeyModifier(key: string) {
30
29
  return reservedModifierKeywords.includes(key)
31
30
  }
32
31
 
33
- export function parseKeysHookInput(keys: string, splitKey = ','): string[] {
34
- return keys.split(splitKey)
32
+ export function parseKeysHookInput(keys: string, delimiter = ','): string[] {
33
+ return keys.toLowerCase().split(delimiter)
35
34
  }
36
35
 
37
- export function parseHotkey(hotkey: string, combinationKey = '+', description?: string): Hotkey {
36
+ export function parseHotkey(hotkey: string, splitKey = '+', useKey = false, description?: string): Hotkey {
38
37
  const keys = hotkey
39
38
  .toLocaleLowerCase()
40
- .split(combinationKey)
39
+ .split(splitKey)
41
40
  .map((k) => mapKey(k))
42
41
 
43
42
  const modifiers: KeyboardModifiers = {
@@ -46,6 +45,7 @@ export function parseHotkey(hotkey: string, combinationKey = '+', description?:
46
45
  shift: keys.includes('shift'),
47
46
  meta: keys.includes('meta'),
48
47
  mod: keys.includes('mod'),
48
+ useKey,
49
49
  }
50
50
 
51
51
  const singleCharKeys = keys.filter((k) => !reservedModifierKeywords.includes(k))
package/src/types.ts CHANGED
@@ -12,6 +12,7 @@ export type KeyboardModifiers = {
12
12
  meta?: boolean
13
13
  shift?: boolean
14
14
  mod?: boolean
15
+ useKey?: boolean // Custom modifier to listen to the produced key instead of the code
15
16
  }
16
17
 
17
18
  export type Hotkey = KeyboardModifiers & {
@@ -27,19 +28,34 @@ export type HotkeyCallback = (keyboardEvent: KeyboardEvent, hotkeysEvent: Hotkey
27
28
  export type Trigger = boolean | ((keyboardEvent: KeyboardEvent, hotkeysEvent: HotkeysEvent) => boolean)
28
29
 
29
30
  export type Options = {
30
- enabled?: Trigger // Main setting that determines if the hotkey is enabled or not. (Default: true)
31
- enableOnFormTags?: readonly FormTags[] | boolean // Enable hotkeys on a list of tags. (Default: false)
32
- enableOnContentEditable?: boolean // Enable hotkeys on tags with contentEditable props. (Default: false)
33
- ignoreEventWhen?: (e: KeyboardEvent) => boolean // Ignore evenets based on a condition (Default: undefined)
34
- combinationKey?: string // Character to split keys in hotkeys combinations. (Default: +)
35
- splitKey?: string // Character to separate different hotkeys. (Default: ,)
36
- scopes?: Scopes // Scope
37
- keyup?: boolean // Trigger on keyup event? (Default: undefined)
38
- keydown?: boolean // Trigger on keydown event? (Default: true)
39
- preventDefault?: Trigger // Prevent default browser behavior? (Default: false)
40
- description?: string // Use this option to describe what the hotkey does. (Default: undefined)
41
- document?: Document // Listen to events on the document instead of the window. (Default: false)
42
- ignoreModifiers?: boolean // Ignore modifiers when matching hotkeys. (Default: false)
31
+ // Main setting that determines if the hotkey is enabled or not. (Default: true)
32
+ enabled?: Trigger
33
+ // Enable hotkeys on a list of tags. (Default: false)
34
+ enableOnFormTags?: readonly FormTags[] | boolean
35
+ // Enable hotkeys on tags with contentEditable props. (Default: false)
36
+ enableOnContentEditable?: boolean
37
+ // Ignore evenets based on a condition (Default: undefined)
38
+ ignoreEventWhen?: (e: KeyboardEvent) => boolean
39
+ // Character to split keys in hotkeys combinations. (Default: +)
40
+ splitKey?: string
41
+ // Character to separate different hotkeys. (Default: ,)
42
+ delimiter?: string
43
+ // Scope of the hotkey. (Default: undefined)
44
+ scopes?: Scopes
45
+ // Trigger on keyup event? (Default: undefined)
46
+ keyup?: boolean
47
+ // Trigger on keydown event? (Default: true)
48
+ keydown?: boolean
49
+ // Prevent default browser behavior? (Default: false)
50
+ preventDefault?: Trigger
51
+ // Use this option to describe what the hotkey does. (Default: undefined)
52
+ description?: string
53
+ // Listen to events on the document instead of the window. (Default: false)
54
+ document?: Document
55
+ // Ignore modifiers when matching hotkeys. (Default: false)
56
+ ignoreModifiers?: boolean
57
+ // Listen to the produced key instead of the code. (Default: false)
58
+ useKey?: boolean
43
59
  }
44
60
 
45
61
  export type OptionsOrDependencyArray = Options | DependencyList
package/src/useHotkeys.ts CHANGED
@@ -36,7 +36,7 @@ export default function useHotkeys<T extends HTMLElement>(
36
36
  : !(dependencies instanceof Array)
37
37
  ? (dependencies as Options)
38
38
  : undefined
39
- const _keys: string = isReadonlyArray(keys) ? keys.join(_options?.splitKey) : keys
39
+ const _keys: string = isReadonlyArray(keys) ? keys.join(_options?.delimiter) : keys
40
40
  const _deps: DependencyList | undefined =
41
41
  options instanceof Array ? options : dependencies instanceof Array ? dependencies : undefined
42
42
 
@@ -83,8 +83,8 @@ export default function useHotkeys<T extends HTMLElement>(
83
83
  return
84
84
  }
85
85
 
86
- parseKeysHookInput(_keys, memoisedOptions?.splitKey).forEach((key) => {
87
- const hotkey = parseHotkey(key, memoisedOptions?.combinationKey)
86
+ parseKeysHookInput(_keys, memoisedOptions?.delimiter).forEach((key) => {
87
+ const hotkey = parseHotkey(key, memoisedOptions?.splitKey, memoisedOptions?.useKey)
88
88
 
89
89
  if (isHotkeyMatchingKeyboardEvent(e, hotkey, memoisedOptions?.ignoreModifiers) || hotkey.keys?.includes('*')) {
90
90
  if (memoisedOptions?.ignoreEventWhen?.(e)) {
@@ -114,12 +114,12 @@ export default function useHotkeys<T extends HTMLElement>(
114
114
  }
115
115
 
116
116
  const handleKeyDown = (event: KeyboardEvent) => {
117
- if (event.key === undefined) {
117
+ if (event.code === undefined) {
118
118
  // Synthetic event (e.g., Chrome autofill). Ignore.
119
119
  return
120
120
  }
121
121
 
122
- pushToCurrentlyPressedKeys(mapKey(event.key))
122
+ pushToCurrentlyPressedKeys(mapKey(event.code))
123
123
 
124
124
  if ((memoisedOptions?.keydown === undefined && memoisedOptions?.keyup !== true) || memoisedOptions?.keydown) {
125
125
  listener(event)
@@ -127,12 +127,12 @@ export default function useHotkeys<T extends HTMLElement>(
127
127
  }
128
128
 
129
129
  const handleKeyUp = (event: KeyboardEvent) => {
130
- if (event.key === undefined) {
130
+ if (event.code === undefined) {
131
131
  // Synthetic event (e.g., Chrome autofill). Ignore.
132
132
  return
133
133
  }
134
134
 
135
- removeFromCurrentlyPressedKeys(mapKey(event.key))
135
+ removeFromCurrentlyPressedKeys(mapKey(event.code))
136
136
 
137
137
  hasTriggeredRef.current = false
138
138
 
@@ -149,8 +149,10 @@ export default function useHotkeys<T extends HTMLElement>(
149
149
  domNode.addEventListener('keydown', handleKeyDown)
150
150
 
151
151
  if (proxy) {
152
- parseKeysHookInput(_keys, memoisedOptions?.splitKey).forEach((key) =>
153
- proxy.addHotkey(parseHotkey(key, memoisedOptions?.combinationKey, memoisedOptions?.description))
152
+ parseKeysHookInput(_keys, memoisedOptions?.delimiter).forEach((key) =>
153
+ proxy.addHotkey(
154
+ parseHotkey(key, memoisedOptions?.splitKey, memoisedOptions?.useKey, memoisedOptions?.description)
155
+ )
154
156
  )
155
157
  }
156
158
 
@@ -161,8 +163,10 @@ export default function useHotkeys<T extends HTMLElement>(
161
163
  domNode.removeEventListener('keydown', handleKeyDown)
162
164
 
163
165
  if (proxy) {
164
- parseKeysHookInput(_keys, memoisedOptions?.splitKey).forEach((key) =>
165
- proxy.removeHotkey(parseHotkey(key, memoisedOptions?.combinationKey, memoisedOptions?.description))
166
+ parseKeysHookInput(_keys, memoisedOptions?.delimiter).forEach((key) =>
167
+ proxy.removeHotkey(
168
+ parseHotkey(key, memoisedOptions?.splitKey, memoisedOptions?.useKey, memoisedOptions?.description)
169
+ )
166
170
  )
167
171
  }
168
172
  }
@@ -6,7 +6,7 @@ export default function useRecordHotkeys() {
6
6
  const [isRecording, setIsRecording] = useState(false)
7
7
 
8
8
  const handler = useCallback((event: KeyboardEvent) => {
9
- if (event.key === undefined) {
9
+ if (event.code === undefined) {
10
10
  // Synthetic event (e.g., Chrome autofill). Ignore.
11
11
  return
12
12
  }
@@ -17,7 +17,7 @@ export default function useRecordHotkeys() {
17
17
  setKeys((prev) => {
18
18
  const newKeys = new Set(prev)
19
19
 
20
- newKeys.add(mapKey(event.key))
20
+ newKeys.add(mapKey(event.code))
21
21
 
22
22
  return newKeys
23
23
  })
@@ -43,5 +43,9 @@ export default function useRecordHotkeys() {
43
43
  }
44
44
  }, [handler, stop])
45
45
 
46
- return [keys, { start, stop, isRecording }] as const
46
+ const resetKeys = useCallback(() => {
47
+ setKeys(new Set<string>())
48
+ }, [])
49
+
50
+ return [keys, { start, stop, resetKeys, isRecording }] as const
47
51
  }
package/src/validators.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { FormTags, Hotkey, Scopes, Trigger } from './types'
2
2
  import { isHotkeyPressed, isReadonlyArray } from './isHotkeyPressed'
3
+ import { mapKey } from './parseHotkeys'
3
4
 
4
5
  export function maybePreventDefault(e: KeyboardEvent, hotkey: Hotkey, preventDefault?: Trigger): void {
5
6
  if ((typeof preventDefault === 'function' && preventDefault(e, hotkey)) || preventDefault === true) {
@@ -31,7 +32,7 @@ export function isHotkeyEnabledOnTag(
31
32
  )
32
33
  }
33
34
 
34
- return Boolean(targetTagName && enabledOnTags && enabledOnTags === true)
35
+ return Boolean(targetTagName && enabledOnTags && enabledOnTags)
35
36
  }
36
37
 
37
38
  export function isScopeActive(activeScopes: string[], scopes?: Scopes): boolean {
@@ -51,25 +52,29 @@ export function isScopeActive(activeScopes: string[], scopes?: Scopes): boolean
51
52
  }
52
53
 
53
54
  export const isHotkeyMatchingKeyboardEvent = (e: KeyboardEvent, hotkey: Hotkey, ignoreModifiers = false): boolean => {
54
- const { alt, meta, mod, shift, ctrl, keys } = hotkey
55
- const { key: pressedKeyUppercase, ctrlKey, metaKey, shiftKey, altKey } = e
55
+ const { alt, meta, mod, shift, ctrl, keys, useKey } = hotkey
56
+ const { code, key: producedKey, ctrlKey, metaKey, shiftKey, altKey } = e
56
57
 
57
- const pressedKey = pressedKeyUppercase.toLowerCase()
58
+ const mappedCode = mapKey(code)
59
+
60
+ if (useKey && keys?.length === 1 && keys.includes(producedKey)) {
61
+ return true
62
+ }
58
63
 
59
64
  if (
60
- !keys?.includes(pressedKey) &&
61
- !['ctrl', 'control', 'unknown', 'meta', 'alt', 'shift', 'os'].includes(pressedKey)
65
+ !keys?.includes(mappedCode) &&
66
+ !['ctrl', 'control', 'unknown', 'meta', 'alt', 'shift', 'os'].includes(mappedCode)
62
67
  ) {
63
68
  return false
64
69
  }
65
70
 
66
71
  if (!ignoreModifiers) {
67
72
  // We check the pressed keys for compatibility with the keyup event. In keyup events the modifier flags are not set.
68
- if (alt === !altKey && pressedKey !== 'alt') {
73
+ if (alt !== altKey && mappedCode !== 'alt') {
69
74
  return false
70
75
  }
71
76
 
72
- if (shift === !shiftKey && pressedKey !== 'shift') {
77
+ if (shift !== shiftKey && mappedCode !== 'shift') {
73
78
  return false
74
79
  }
75
80
 
@@ -79,11 +84,11 @@ export const isHotkeyMatchingKeyboardEvent = (e: KeyboardEvent, hotkey: Hotkey,
79
84
  return false
80
85
  }
81
86
  } else {
82
- if (meta === !metaKey && pressedKey !== 'meta' && pressedKey !== 'os') {
87
+ if (meta !== metaKey && mappedCode !== 'meta' && mappedCode !== 'os') {
83
88
  return false
84
89
  }
85
90
 
86
- if (ctrl === !ctrlKey && pressedKey !== 'ctrl' && pressedKey !== 'control') {
91
+ if (ctrl !== ctrlKey && mappedCode !== 'ctrl' && mappedCode !== 'control') {
87
92
  return false
88
93
  }
89
94
  }
@@ -91,7 +96,7 @@ export const isHotkeyMatchingKeyboardEvent = (e: KeyboardEvent, hotkey: Hotkey,
91
96
 
92
97
  // All modifiers are correct, now check the key
93
98
  // If the key is set, we check for the key
94
- if (keys && keys.length === 1 && keys.includes(pressedKey)) {
99
+ if (keys && keys.length === 1 && keys.includes(mappedCode)) {
95
100
  return true
96
101
  } else if (keys) {
97
102
  // Check if all keys are present in pressedDownKeys set