From 791c1c521b42291afe003001f3a01cf07a385672 Mon Sep 17 00:00:00 2001 From: Evgeniy Date: Thu, 29 Feb 2024 11:00:41 +0200 Subject: [PATCH 01/13] Migrate from Mousetrap to Ctrl-keys --- package.json | 3 +- src/Hotkey.story.tsx | 180 ++++++++++-------------------------------- src/useHotkeyState.ts | 113 -------------------------- src/useHotkeys.ts | 123 ++++++++++++++++++++++++----- 4 files changed, 144 insertions(+), 275 deletions(-) delete mode 100644 src/useHotkeyState.ts diff --git a/package.json b/package.json index 9b31330..37c5f07 100644 --- a/package.json +++ b/package.json @@ -45,7 +45,7 @@ }, "homepage": "https://github.com/reaviz/reakeys#readme", "dependencies": { - "mousetrap": "^1.6.5" + "ctrl-keys": "^1.0.1" }, "peerDependencies": { "react": ">=16", @@ -61,7 +61,6 @@ "@storybook/react-vite": "^7.0.18", "@storybook/theming": "7.0.18", "@types/classnames": "^2.3.1", - "@types/mousetrap": "^1.6.11", "@types/react": "^18.2.7", "@types/react-dom": "^18.2.4", "@typescript-eslint/eslint-plugin": "^5.43.0", diff --git a/src/Hotkey.story.tsx b/src/Hotkey.story.tsx index 9ce92a4..dc006cb 100644 --- a/src/Hotkey.story.tsx +++ b/src/Hotkey.story.tsx @@ -1,4 +1,3 @@ -import Mousetrap from 'mousetrap'; import React, { useEffect, useRef, useState } from 'react'; import { useHotkeys } from './useHotkeys'; @@ -7,20 +6,24 @@ export default { }; export const Simple = () => { - const hotkeys = useHotkeys([ - { name: 'Test', keys: 'SHIFT+A', callback: () => alert('holla') }, - ]); + useHotkeys([{ keys: ['shift+a'], callback: () => alert('SHIFT + A pressed') }]); + + return ( +
+ Press SHIFT + A
+
+ ); +}; + +export const Disable = () => { + const [disabled, setDisabled] = useState(false); + + useHotkeys([{ keys: ['shift+a'], callback: () => alert('SHIFT + A pressed'), disabled }]); return (
Press SHIFT + A
-
-        {JSON.stringify(
-          hotkeys.map(({ ref: element, ...rest }) => rest),
-          null,
-          2
-        )}
-      
+
); }; @@ -28,10 +31,9 @@ export const Simple = () => { export const Refs = () => { const [color, setColor] = useState('blue'); - const hotkeys = useHotkeys([ + useHotkeys([ { - name: 'Test', - keys: 'SHIFT+A', + keys: ['shift+a'], callback: () => { alert(`color: ${color}`); }, @@ -46,68 +48,38 @@ export const Refs = () => { -
-        {JSON.stringify(
-          hotkeys.map(({ ref: element, ...rest }) => rest),
-          null,
-          2
-        )}
-      
); }; export const Multiple = () => { - const hotkeys = useHotkeys([ - { name: 'Test', keys: 'SHIFT+A', callback: () => alert('holla') }, - ]); - useHotkeys([ - { name: 'Test 2', keys: 'mod+b', callback: () => alert('baller') }, + { keys: ['shift+a'], callback: () => alert('SHIFT + A pressed') }, + { keys: ['meta+b'], callback: () => alert('META + B pressed') }, ]); return (
- Press SHIFT + A
- Press MOD + b
+ Press SHIFT + A
-
-        {JSON.stringify(
-          hotkeys.map(({ ref: element, ...rest }) => rest),
-          null,
-          2
-        )}
-      
+ Press META + B
); }; const NestedComponent = () => { - useHotkeys([ - { name: 'Test 2', keys: 'mod+b', callback: () => alert('baller') }, - ]); + useHotkeys([{ keys: ['meta+b'], callback: () => alert('META + B (child)') }]); return

Press MOD + b

; }; export const Nested = () => { - const hotkeys = useHotkeys([ - { name: 'Test', keys: 'SHIFT+A', callback: () => alert('holla') }, - ]); + useHotkeys([{ keys: ['shift+a'], callback: () => alert('SHIFT + A (parent)') }]); return (
Press SHIFT + A
-
-
-
-        {JSON.stringify(
-          hotkeys.map(({ ref: element, ...rest }) => rest),
-          null,
-          2
-        )}
-      
); }; @@ -117,19 +89,14 @@ export const Focus = () => { const elmRef = useRef(null); const elmRef2 = useRef(null); - const hotkeys = useHotkeys([ + useHotkeys([ { - name: 'Test 3', - keys: 'SHIFT+C', + keys: ['shift+c'], callback: () => alert(`first, counter: ${counter}`), ref: elmRef, }, - ]); - - useHotkeys([ { - name: 'Test 3', - keys: 'SHIFT+C', + keys: ['shift+c'], callback: () => alert(`second, counter: ${counter}`), ref: elmRef2, }, @@ -137,17 +104,11 @@ export const Focus = () => { return (
- {counter} -
@@ -159,50 +120,28 @@ export const Focus = () => { focus me and press SHIFT+C -
-
-        {JSON.stringify(
-          hotkeys.map(({ ref: element, ...rest }) => rest),
-          null,
-          2
-        )}
-      
); }; export const Action = () => { - const hotkeys = useHotkeys([ + useHotkeys([ { - name: 'Pay respects', - keys: 'f', + keys: ['f'], callback: () => alert("You've been promoted!"), action: 'keyup', }, ]); - return ( -
- Press f to pay respects -
-
-        {JSON.stringify(
-          hotkeys.map(({ ref: element, ...rest }) => rest),
-          null,
-          2
-        )}
-      
-
- ); + return
Press "f" to pay respects
; }; export const Asynchronous = () => { const elmRef = useRef(null); const [loaded, setLoaded] = useState(false); - const hotkeys = useHotkeys([ + useHotkeys([ { - name: 'Loaded', - keys: 'l', + keys: ['l'], callback: () => alert('Hey!'), action: 'keyup', ref: elmRef, @@ -223,35 +162,26 @@ export const Asynchronous = () => { return (
- {loaded - ? 'Loaded' - : 'Loading (pressing "l" is disabled until the element is shown and focused)...'} + {loaded ? 'Loaded' : 'Loading (pressing "l" is disabled until the element is shown and focused)...'} +

{loaded && (
- Click me and press "l`" + Click me and press "l"
)} -
-        {JSON.stringify(
-          hotkeys.map(({ ref: element, ...rest }) => rest),
-          null,
-          2
-        )}
-      
); }; const Counter = () => { const [counter, setCounter] = useState(0); - const hotkeys = useHotkeys([ + useHotkeys([ { - name: 'Generate a random number', - keys: 'g', + keys: ['g'], callback: () => setCounter(Math.random()), }, ]); @@ -261,22 +191,16 @@ const Counter = () => {
  1. Press "g" to generate a random number: {counter}
  2. Open the modal, press "g" and close the modal
  3. -
  4. - Press "g" once the modal is closed, it should generate - random number -
  5. +
  6. Press "g" once the modal is closed, it should generate random number
-
-
{JSON.stringify(hotkeys, null, 2)}
); }; const ModalComponent = ({ onClose }: { onClose: () => void }) => { - const hotkeys = useHotkeys([ + useHotkeys([ { - name: 'Modal shortcut', - keys: 'g', + keys: ['g'], callback: () => alert('This shortcut is bound through the modal'), }, ]); @@ -297,8 +221,6 @@ const ModalComponent = ({ onClose }: { onClose: () => void }) => {

Press g

-
-
{JSON.stringify(hotkeys, null, 2)}
); @@ -325,25 +247,3 @@ export const Modal = () => { ); }; - -export const Trigger = () => { - const hotkeys = useHotkeys([ - { name: 'Test', keys: 'SHIFT+A', callback: () => alert('holla') }, - ]); - - return ( -
- - Press SHIFT + A
-
-        {JSON.stringify(
-          hotkeys.map(({ ref: element, ...rest }) => rest),
-          null,
-          2
-        )}
-      
-
- ); -}; diff --git a/src/useHotkeyState.ts b/src/useHotkeyState.ts deleted file mode 100644 index 9443409..0000000 --- a/src/useHotkeyState.ts +++ /dev/null @@ -1,113 +0,0 @@ -import { RefObject, useEffect, useState } from 'react'; -import Mousetrap, { - ExtendedKeyboardEvent, - MousetrapInstance, - MousetrapStatic, -} from 'mousetrap'; - -export interface HotkeyShortcuts { - name: string; - category?: string; - description?: string; - keys: string | string[]; - ref?: RefObject; - hidden?: boolean; - disabled?: boolean; - callback: (e: ExtendedKeyboardEvent, combo: string) => void; - action?: 'keypress' | 'keydown' | 'keyup'; -} - -/** - * Creates a global state singleton. - */ -const createStateHook = () => { - const mousetraps = new Map< - HTMLElement | undefined, - MousetrapStatic | MousetrapInstance - >(); - let keys: HotkeyShortcuts[] = []; - - const bindKeys = (nextKeys: HotkeyShortcuts[]) => { - nextKeys.forEach((k) => { - if (k.disabled) { - return; - } - - if (k.ref) { - if (!k.ref.current) { - // exit early if ref is provided but null - // we do not want to attach global event handlers in this case - return; - } - - const element = k.ref.current; - - if (!mousetraps.has(element)) { - mousetraps.set(element, new Mousetrap(element)); - } - - mousetraps.get(element)!.bind(k.keys, k.callback, k.action); - } else { - if (!mousetraps.get(undefined)) { - mousetraps.set(undefined, Mousetrap); - } - - mousetraps.get(undefined)!.bind(k.keys, k.callback, k.action); - } - }); - }; - - const addKeys = (nextKeys: HotkeyShortcuts[]) => { - keys = [...keys, ...nextKeys]; - - bindKeys(nextKeys); - }; - - const removeKeys = (nextKeys: HotkeyShortcuts[]) => { - // remove keys from the array - keys = keys.filter((k) => !nextKeys.includes(k)); - - // unbind mousetrap events - nextKeys.forEach((k) => { - if (k.ref) { - if (!k.ref.current) { - return; - } - - mousetraps.get(k.ref.current)?.unbind(k.keys, k.action); - } else { - mousetraps.get(undefined)?.unbind(k.keys, k.action); - } - }); - - // drop mousetrap instances that have no keys attached anymore - for (const [element] of mousetraps) { - if (element === undefined) { - if (keys.some((k) => k.ref === undefined)) { - continue; - } - } else { - if (keys.some((k) => k.ref?.current === element)) { - continue; - } - } - - mousetraps.delete(element); - } - - // re-bind keys to restore listeners that were overwritten by the ones we just removed - bindKeys(keys); - }; - - return () => { - const [state, setState] = useState([]); - - useEffect(() => { - setState(keys); - }, []); - - return [state, addKeys, removeKeys] as const; - }; -}; - -export const useHotkeyState = createStateHook(); diff --git a/src/useHotkeys.ts b/src/useHotkeys.ts index 47c1cc5..61efc5e 100644 --- a/src/useHotkeys.ts +++ b/src/useHotkeys.ts @@ -1,29 +1,112 @@ -import { useLayoutEffect, useMemo } from 'react'; -import { HotkeyShortcuts, useHotkeyState } from './useHotkeyState'; +import { RefObject, useLayoutEffect } from 'react'; +import keys, { Callback, Handler, Key } from 'ctrl-keys'; -export const useHotkeys = (shortcuts?: HotkeyShortcuts[]) => { - const [keys, addKeys, removeKeys] = useHotkeyState(); +export interface HotkeyShortcut { + keys: [Key] | [Key, Key] | [Key, Key, Key] | [Key, Key, Key, Key]; + ref?: RefObject; + disabled?: boolean; + callback: Callback; + action?: 'keypress' | 'keydown' | 'keyup'; + /** @deprecated */ + name?: string; + /** @deprecated */ + description?: string; +} +let isGlobalListenersBinded = false; + +const keypressGlobalHandler = keys(); +const keyupGlobalHandler = keys(); +const keydownGlobalHandler = keys(); + +/** + * Map of specific elements handlers + */ +const handlers = new Map(); + +const registerGlobalShortcut = (shortcut: HotkeyShortcut) => { + if (!shortcut.action || shortcut.action === 'keypress') { + keypressGlobalHandler.add(...shortcut.keys, shortcut.callback); + } + if (shortcut.action === 'keyup') { + keyupGlobalHandler.add(...shortcut.keys, shortcut.callback); + } + if (shortcut.action === 'keydown') { + keydownGlobalHandler.add(...shortcut.keys, shortcut.callback); + } +}; + +const removeGlobalShortcut = (shortcut: HotkeyShortcut) => { + if (!shortcut.action || shortcut.action === 'keypress') { + keypressGlobalHandler.remove(...shortcut.keys, shortcut.callback); + } + if (shortcut.action === 'keyup') { + keyupGlobalHandler.remove(...shortcut.keys, shortcut.callback); + } + if (shortcut.action === 'keydown') { + keydownGlobalHandler.remove(...shortcut.keys, shortcut.callback); + } +}; + +const registerElementShortcut = (shortcut: HotkeyShortcut) => { + const handler = keys(); + + handler.add(...shortcut.keys, shortcut.callback); + + shortcut.ref?.current?.addEventListener(shortcut.action ?? 'keypress', handler.handle); + + handlers.set(shortcut.ref?.current as HTMLElement, handler); +}; + +const removeElementShortcut = (shortcut: HotkeyShortcut) => { + if (shortcut.ref?.current && !shortcut.disabled) { + const handler = handlers.get(shortcut.ref?.current) as Handler; + + handler.remove(...shortcut.keys, shortcut.callback); + + shortcut.ref?.current?.removeEventListener(shortcut.action ?? 'keypress', handler.handle); + } +}; + +export const useHotkeys = (shortcuts: HotkeyShortcut[]) => { + /** + * Register global listeners for "keypress", "keyup" and "keydown" events + */ useLayoutEffect(() => { - if (shortcuts) { - addKeys(shortcuts); + if (!isGlobalListenersBinded && window !== undefined) { + window.addEventListener('keypress', keypressGlobalHandler.handle); + window.addEventListener('keyup', keyupGlobalHandler.handle); + window.addEventListener('keydown', keydownGlobalHandler.handle); + + isGlobalListenersBinded = true; } + }, []); - return () => { - if (shortcuts) { - removeKeys(shortcuts); + /** + * Register shortcuts + */ + useLayoutEffect(() => { + shortcuts.map((shortcut) => { + if (shortcut.disabled) { + return; } - }; - }, [addKeys, removeKeys, shortcuts]); - return useMemo( - () => - keys.reduce((agg, cur) => { - if (!agg.find((a) => a.keys === cur.keys && a.ref && cur.ref)) { - agg.push(cur); + if (shortcut.ref?.current) { + registerElementShortcut(shortcut); + } else if (!shortcut.ref) { + registerGlobalShortcut(shortcut); + } + }); + + // Remove all shortcuts on destroy + return () => { + shortcuts.map((shortcut) => { + if (shortcut.disabled) { + return; } - return agg; - }, []), - [keys] - ); + removeGlobalShortcut(shortcut); + removeElementShortcut(shortcut); + }); + }; + }, [shortcuts]); }; From ced7e42c587426f394a11755f23f87a106cb843a Mon Sep 17 00:00:00 2001 From: Evgeniy Date: Thu, 29 Feb 2024 11:28:39 +0200 Subject: [PATCH 02/13] Avoid hotkey trigger if some input is active --- src/Hotkey.story.tsx | 12 ++++++++++++ src/useHotkeys.ts | 27 +++++++++++++++++++-------- 2 files changed, 31 insertions(+), 8 deletions(-) diff --git a/src/Hotkey.story.tsx b/src/Hotkey.story.tsx index dc006cb..0cc6097 100644 --- a/src/Hotkey.story.tsx +++ b/src/Hotkey.story.tsx @@ -15,6 +15,18 @@ export const Simple = () => { ); }; +export const Input = () => { + useHotkeys([{ keys: ['shift+a'], callback: () => alert('SHIFT + A pressed') }]); + + return ( +
+ Press SHIFT + A (shouldn't trigger if input is focused) +
+ +
+ ); +}; + export const Disable = () => { const [disabled, setDisabled] = useState(false); diff --git a/src/useHotkeys.ts b/src/useHotkeys.ts index 61efc5e..71059eb 100644 --- a/src/useHotkeys.ts +++ b/src/useHotkeys.ts @@ -24,34 +24,45 @@ const keydownGlobalHandler = keys(); */ const handlers = new Map(); +const filter = (callback: Callback) => (event: any) => { + const target = event.target; + + const isInput = target.tagName === 'INPUT' && !['checkbox', 'radio', 'range', 'button', 'file', 'reset', 'submit', 'color'].includes(target.type); + if (target.isContentEditable || ((isInput || target.tagName === 'TEXTAREA' || target.tagName === 'SELECT') && !target.readOnly)) { + return; + } + + return callback(event); +}; + const registerGlobalShortcut = (shortcut: HotkeyShortcut) => { if (!shortcut.action || shortcut.action === 'keypress') { - keypressGlobalHandler.add(...shortcut.keys, shortcut.callback); + keypressGlobalHandler.add(...shortcut.keys, filter(shortcut.callback)); } if (shortcut.action === 'keyup') { - keyupGlobalHandler.add(...shortcut.keys, shortcut.callback); + keyupGlobalHandler.add(...shortcut.keys, filter(shortcut.callback)); } if (shortcut.action === 'keydown') { - keydownGlobalHandler.add(...shortcut.keys, shortcut.callback); + keydownGlobalHandler.add(...shortcut.keys, filter(shortcut.callback)); } }; const removeGlobalShortcut = (shortcut: HotkeyShortcut) => { if (!shortcut.action || shortcut.action === 'keypress') { - keypressGlobalHandler.remove(...shortcut.keys, shortcut.callback); + keypressGlobalHandler.remove(...shortcut.keys, filter(shortcut.callback)); } if (shortcut.action === 'keyup') { - keyupGlobalHandler.remove(...shortcut.keys, shortcut.callback); + keyupGlobalHandler.remove(...shortcut.keys, filter(shortcut.callback)); } if (shortcut.action === 'keydown') { - keydownGlobalHandler.remove(...shortcut.keys, shortcut.callback); + keydownGlobalHandler.remove(...shortcut.keys, filter(shortcut.callback)); } }; const registerElementShortcut = (shortcut: HotkeyShortcut) => { const handler = keys(); - handler.add(...shortcut.keys, shortcut.callback); + handler.add(...shortcut.keys, filter(shortcut.callback)); shortcut.ref?.current?.addEventListener(shortcut.action ?? 'keypress', handler.handle); @@ -62,7 +73,7 @@ const removeElementShortcut = (shortcut: HotkeyShortcut) => { if (shortcut.ref?.current && !shortcut.disabled) { const handler = handlers.get(shortcut.ref?.current) as Handler; - handler.remove(...shortcut.keys, shortcut.callback); + handler.remove(...shortcut.keys, filter(shortcut.callback)); shortcut.ref?.current?.removeEventListener(shortcut.action ?? 'keypress', handler.handle); } From 75b094862602c41ac253de1dec97b8e8f7ce3cd8 Mon Sep 17 00:00:00 2001 From: Evgeniy Date: Thu, 29 Feb 2024 12:00:37 +0200 Subject: [PATCH 03/13] Deprecate category --- src/useHotkeys.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/useHotkeys.ts b/src/useHotkeys.ts index 71059eb..ee3e1b1 100644 --- a/src/useHotkeys.ts +++ b/src/useHotkeys.ts @@ -11,6 +11,8 @@ export interface HotkeyShortcut { name?: string; /** @deprecated */ description?: string; + /** @deprecated */ + category?: string; } let isGlobalListenersBinded = false; From aa46e5ee6b049569a8d12930bc026250f806f1a8 Mon Sep 17 00:00:00 2001 From: Evgeniy Date: Thu, 29 Feb 2024 13:50:53 +0200 Subject: [PATCH 04/13] Allow to pass single shortcut instead of array --- src/Hotkey.story.tsx | 24 ++++++++++++++++-------- src/useHotkeys.ts | 34 +++++++++++++++++----------------- 2 files changed, 33 insertions(+), 25 deletions(-) diff --git a/src/Hotkey.story.tsx b/src/Hotkey.story.tsx index 0cc6097..0c6906e 100644 --- a/src/Hotkey.story.tsx +++ b/src/Hotkey.story.tsx @@ -6,7 +6,7 @@ export default { }; export const Simple = () => { - useHotkeys([{ keys: ['shift+a'], callback: () => alert('SHIFT + A pressed') }]); + useHotkeys([{ name: 'Simple', keys: 'shift+a', callback: () => alert('SHIFT + A pressed') }]); return (
@@ -16,7 +16,7 @@ export const Simple = () => { }; export const Input = () => { - useHotkeys([{ keys: ['shift+a'], callback: () => alert('SHIFT + A pressed') }]); + useHotkeys([{ name: 'Input', keys: 'shift+a', callback: () => alert('SHIFT + A pressed') }]); return (
@@ -30,7 +30,7 @@ export const Input = () => { export const Disable = () => { const [disabled, setDisabled] = useState(false); - useHotkeys([{ keys: ['shift+a'], callback: () => alert('SHIFT + A pressed'), disabled }]); + useHotkeys([{ name: 'Disable', keys: 'shift+a', callback: () => alert('SHIFT + A pressed'), disabled }]); return (
@@ -45,7 +45,8 @@ export const Refs = () => { useHotkeys([ { - keys: ['shift+a'], + name: 'InRefsput', + keys: 'shift+a', callback: () => { alert(`color: ${color}`); }, @@ -66,8 +67,8 @@ export const Refs = () => { export const Multiple = () => { useHotkeys([ - { keys: ['shift+a'], callback: () => alert('SHIFT + A pressed') }, - { keys: ['meta+b'], callback: () => alert('META + B pressed') }, + { name: 'Nested A', keys: ['shift+a'], callback: () => alert('SHIFT + A pressed') }, + { name: 'Nested B', keys: ['meta+b'], callback: () => alert('META + B pressed') }, ]); return ( @@ -80,13 +81,13 @@ export const Multiple = () => { }; const NestedComponent = () => { - useHotkeys([{ keys: ['meta+b'], callback: () => alert('META + B (child)') }]); + useHotkeys([{ name: 'Child', keys: ['meta+b'], callback: () => alert('META + B (child)') }]); return

Press MOD + b

; }; export const Nested = () => { - useHotkeys([{ keys: ['shift+a'], callback: () => alert('SHIFT + A (parent)') }]); + useHotkeys([{ name: 'Parent', keys: ['shift+a'], callback: () => alert('SHIFT + A (parent)') }]); return (
@@ -103,11 +104,13 @@ export const Focus = () => { useHotkeys([ { + name: 'Focus A', keys: ['shift+c'], callback: () => alert(`first, counter: ${counter}`), ref: elmRef, }, { + name: 'Focus b', keys: ['shift+c'], callback: () => alert(`second, counter: ${counter}`), ref: elmRef2, @@ -139,6 +142,7 @@ export const Focus = () => { export const Action = () => { useHotkeys([ { + name: 'Action', keys: ['f'], callback: () => alert("You've been promoted!"), action: 'keyup', @@ -153,6 +157,7 @@ export const Asynchronous = () => { const [loaded, setLoaded] = useState(false); useHotkeys([ { + name: 'Asynchronous', keys: ['l'], callback: () => alert('Hey!'), action: 'keyup', @@ -191,8 +196,10 @@ export const Asynchronous = () => { const Counter = () => { const [counter, setCounter] = useState(0); + useHotkeys([ { + name: 'Counter', keys: ['g'], callback: () => setCounter(Math.random()), }, @@ -212,6 +219,7 @@ const Counter = () => { const ModalComponent = ({ onClose }: { onClose: () => void }) => { useHotkeys([ { + name: 'ModalComponent', keys: ['g'], callback: () => alert('This shortcut is bound through the modal'), }, diff --git a/src/useHotkeys.ts b/src/useHotkeys.ts index ee3e1b1..fef1f9f 100644 --- a/src/useHotkeys.ts +++ b/src/useHotkeys.ts @@ -1,17 +1,16 @@ -import { RefObject, useLayoutEffect } from 'react'; +import { RefObject, useLayoutEffect, useRef, useState } from 'react'; import keys, { Callback, Handler, Key } from 'ctrl-keys'; +type Keys = [Key] | [Key, Key] | [Key, Key, Key] | [Key, Key, Key, Key]; + export interface HotkeyShortcut { - keys: [Key] | [Key, Key] | [Key, Key, Key] | [Key, Key, Key, Key]; + name: string; + keys: Key | Keys; ref?: RefObject; disabled?: boolean; callback: Callback; action?: 'keypress' | 'keydown' | 'keyup'; - /** @deprecated */ - name?: string; - /** @deprecated */ description?: string; - /** @deprecated */ category?: string; } @@ -26,6 +25,10 @@ const keydownGlobalHandler = keys(); */ const handlers = new Map(); +const extractKeys = (keys: Key | Keys): Keys => { + return Array.isArray(keys) ? keys : [keys]; +}; + const filter = (callback: Callback) => (event: any) => { const target = event.target; @@ -39,32 +42,32 @@ const filter = (callback: Callback) => (event: any) => { const registerGlobalShortcut = (shortcut: HotkeyShortcut) => { if (!shortcut.action || shortcut.action === 'keypress') { - keypressGlobalHandler.add(...shortcut.keys, filter(shortcut.callback)); + keypressGlobalHandler.add(...extractKeys(shortcut.keys), filter(shortcut.callback)); } if (shortcut.action === 'keyup') { - keyupGlobalHandler.add(...shortcut.keys, filter(shortcut.callback)); + keyupGlobalHandler.add(...extractKeys(shortcut.keys), filter(shortcut.callback)); } if (shortcut.action === 'keydown') { - keydownGlobalHandler.add(...shortcut.keys, filter(shortcut.callback)); + keydownGlobalHandler.add(...extractKeys(shortcut.keys), filter(shortcut.callback)); } }; const removeGlobalShortcut = (shortcut: HotkeyShortcut) => { if (!shortcut.action || shortcut.action === 'keypress') { - keypressGlobalHandler.remove(...shortcut.keys, filter(shortcut.callback)); + keypressGlobalHandler.remove(...extractKeys(shortcut.keys), filter(shortcut.callback)); } if (shortcut.action === 'keyup') { - keyupGlobalHandler.remove(...shortcut.keys, filter(shortcut.callback)); + keyupGlobalHandler.remove(...extractKeys(shortcut.keys), filter(shortcut.callback)); } if (shortcut.action === 'keydown') { - keydownGlobalHandler.remove(...shortcut.keys, filter(shortcut.callback)); + keydownGlobalHandler.remove(...extractKeys(shortcut.keys), filter(shortcut.callback)); } }; const registerElementShortcut = (shortcut: HotkeyShortcut) => { const handler = keys(); - handler.add(...shortcut.keys, filter(shortcut.callback)); + handler.add(...extractKeys(shortcut.keys), filter(shortcut.callback)); shortcut.ref?.current?.addEventListener(shortcut.action ?? 'keypress', handler.handle); @@ -75,7 +78,7 @@ const removeElementShortcut = (shortcut: HotkeyShortcut) => { if (shortcut.ref?.current && !shortcut.disabled) { const handler = handlers.get(shortcut.ref?.current) as Handler; - handler.remove(...shortcut.keys, filter(shortcut.callback)); + handler.remove(...extractKeys(shortcut.keys), filter(shortcut.callback)); shortcut.ref?.current?.removeEventListener(shortcut.action ?? 'keypress', handler.handle); } @@ -114,9 +117,6 @@ export const useHotkeys = (shortcuts: HotkeyShortcut[]) => { // Remove all shortcuts on destroy return () => { shortcuts.map((shortcut) => { - if (shortcut.disabled) { - return; - } removeGlobalShortcut(shortcut); removeElementShortcut(shortcut); }); From 50e53d670c24e4df79dad6c836ee668fbe4fe885 Mon Sep 17 00:00:00 2001 From: Evgeniy Date: Thu, 29 Feb 2024 15:48:31 +0200 Subject: [PATCH 05/13] Fix input filtering --- src/Hotkey.story.tsx | 92 +++++++++++++++++++++++++++++++++++++++----- src/useHotkeys.ts | 38 ++++++++++++------ 2 files changed, 108 insertions(+), 22 deletions(-) diff --git a/src/Hotkey.story.tsx b/src/Hotkey.story.tsx index 0c6906e..b305e15 100644 --- a/src/Hotkey.story.tsx +++ b/src/Hotkey.story.tsx @@ -6,23 +6,37 @@ export default { }; export const Simple = () => { - useHotkeys([{ name: 'Simple', keys: 'shift+a', callback: () => alert('SHIFT + A pressed') }]); + const hotkeys = useHotkeys([{ name: 'Simple', keys: 'shift+a', callback: () => alert('SHIFT + A pressed') }]); return (
Press SHIFT + A
+
+        {JSON.stringify(
+          hotkeys.map(({ ref: element, ...rest }) => rest),
+          null,
+          2
+        )}
+      
); }; export const Input = () => { - useHotkeys([{ name: 'Input', keys: 'shift+a', callback: () => alert('SHIFT + A pressed') }]); + const hotkeys = useHotkeys([{ name: 'Input', keys: 'shift+a', callback: () => alert('SHIFT + A pressed') }]); return (
Press SHIFT + A (shouldn't trigger if input is focused)
+
+        {JSON.stringify(
+          hotkeys.map(({ ref: element, ...rest }) => rest),
+          null,
+          2
+        )}
+      
); }; @@ -30,12 +44,19 @@ export const Input = () => { export const Disable = () => { const [disabled, setDisabled] = useState(false); - useHotkeys([{ name: 'Disable', keys: 'shift+a', callback: () => alert('SHIFT + A pressed'), disabled }]); + const hotkeys = useHotkeys([{ name: 'Disable', keys: 'shift+a', callback: () => alert('SHIFT + A pressed'), disabled }]); return (
Press SHIFT + A
+
+        {JSON.stringify(
+          hotkeys.map(({ ref: element, disabled, ...rest }) => rest),
+          null,
+          2
+        )}
+      
); }; @@ -43,9 +64,9 @@ export const Disable = () => { export const Refs = () => { const [color, setColor] = useState('blue'); - useHotkeys([ + const hotkeys = useHotkeys([ { - name: 'InRefsput', + name: 'Refs', keys: 'shift+a', callback: () => { alert(`color: ${color}`); @@ -61,12 +82,19 @@ export const Refs = () => { +
+        {JSON.stringify(
+          hotkeys.map(({ ref: element, ...rest }) => rest),
+          null,
+          2
+        )}
+      
); }; export const Multiple = () => { - useHotkeys([ + const hotkeys = useHotkeys([ { name: 'Nested A', keys: ['shift+a'], callback: () => alert('SHIFT + A pressed') }, { name: 'Nested B', keys: ['meta+b'], callback: () => alert('META + B pressed') }, ]); @@ -76,6 +104,14 @@ export const Multiple = () => { Press SHIFT + A
Press META + B +
+
+        {JSON.stringify(
+          hotkeys.map(({ ref: element, ...rest }) => rest),
+          null,
+          2
+        )}
+      
); }; @@ -87,12 +123,20 @@ const NestedComponent = () => { }; export const Nested = () => { - useHotkeys([{ name: 'Parent', keys: ['shift+a'], callback: () => alert('SHIFT + A (parent)') }]); + const hotkeys = useHotkeys([{ name: 'Parent', keys: ['shift+a'], callback: () => alert('SHIFT + A (parent)') }]); return (
Press SHIFT + A
+
+
+        {JSON.stringify(
+          hotkeys.map(({ ref: element, ...rest }) => rest),
+          null,
+          2
+        )}
+      
); }; @@ -102,7 +146,7 @@ export const Focus = () => { const elmRef = useRef(null); const elmRef2 = useRef(null); - useHotkeys([ + const hotkeys = useHotkeys([ { name: 'Focus A', keys: ['shift+c'], @@ -135,12 +179,20 @@ export const Focus = () => { focus me and press SHIFT+C +
+
+        {JSON.stringify(
+          hotkeys.map(({ ref: element, ...rest }) => rest),
+          null,
+          2
+        )}
+      
); }; export const Action = () => { - useHotkeys([ + const hotkeys = useHotkeys([ { name: 'Action', keys: ['f'], @@ -149,7 +201,19 @@ export const Action = () => { }, ]); - return
Press "f" to pay respects
; + return ( + <> +
Press "f" to pay respects
+
+
+        {JSON.stringify(
+          hotkeys.map(({ ref: element, ...rest }) => rest),
+          null,
+          2
+        )}
+      
+ + ); }; export const Asynchronous = () => { @@ -260,10 +324,18 @@ const ModalToggle = () => { }; export const Modal = () => { + const hotkeys = useHotkeys(); return (
+
+        {JSON.stringify(
+          hotkeys.map(({ ref: element, ...rest }) => rest),
+          null,
+          2
+        )}
+      
); }; diff --git a/src/useHotkeys.ts b/src/useHotkeys.ts index fef1f9f..b4ebd15 100644 --- a/src/useHotkeys.ts +++ b/src/useHotkeys.ts @@ -1,4 +1,4 @@ -import { RefObject, useLayoutEffect, useRef, useState } from 'react'; +import { RefObject, useEffect, useLayoutEffect, useState } from 'react'; import keys, { Callback, Handler, Key } from 'ctrl-keys'; type Keys = [Key] | [Key, Key] | [Key, Key, Key] | [Key, Key, Key, Key]; @@ -24,12 +24,13 @@ const keydownGlobalHandler = keys(); * Map of specific elements handlers */ const handlers = new Map(); +let hotkeys: HotkeyShortcut[] = []; const extractKeys = (keys: Key | Keys): Keys => { return Array.isArray(keys) ? keys : [keys]; }; -const filter = (callback: Callback) => (event: any) => { +const focusInputWrapper = (callback: Callback) => (event: any) => { const target = event.target; const isInput = target.tagName === 'INPUT' && !['checkbox', 'radio', 'range', 'button', 'file', 'reset', 'submit', 'color'].includes(target.type); @@ -42,32 +43,32 @@ const filter = (callback: Callback) => (event: any) => { const registerGlobalShortcut = (shortcut: HotkeyShortcut) => { if (!shortcut.action || shortcut.action === 'keypress') { - keypressGlobalHandler.add(...extractKeys(shortcut.keys), filter(shortcut.callback)); + keypressGlobalHandler.add(...extractKeys(shortcut.keys), shortcut.callback); } if (shortcut.action === 'keyup') { - keyupGlobalHandler.add(...extractKeys(shortcut.keys), filter(shortcut.callback)); + keyupGlobalHandler.add(...extractKeys(shortcut.keys), shortcut.callback); } if (shortcut.action === 'keydown') { - keydownGlobalHandler.add(...extractKeys(shortcut.keys), filter(shortcut.callback)); + keydownGlobalHandler.add(...extractKeys(shortcut.keys), shortcut.callback); } }; const removeGlobalShortcut = (shortcut: HotkeyShortcut) => { if (!shortcut.action || shortcut.action === 'keypress') { - keypressGlobalHandler.remove(...extractKeys(shortcut.keys), filter(shortcut.callback)); + keypressGlobalHandler.remove(...extractKeys(shortcut.keys), shortcut.callback); } if (shortcut.action === 'keyup') { - keyupGlobalHandler.remove(...extractKeys(shortcut.keys), filter(shortcut.callback)); + keyupGlobalHandler.remove(...extractKeys(shortcut.keys), shortcut.callback); } if (shortcut.action === 'keydown') { - keydownGlobalHandler.remove(...extractKeys(shortcut.keys), filter(shortcut.callback)); + keydownGlobalHandler.remove(...extractKeys(shortcut.keys), shortcut.callback); } }; const registerElementShortcut = (shortcut: HotkeyShortcut) => { const handler = keys(); - handler.add(...extractKeys(shortcut.keys), filter(shortcut.callback)); + handler.add(...extractKeys(shortcut.keys), shortcut.callback); shortcut.ref?.current?.addEventListener(shortcut.action ?? 'keypress', handler.handle); @@ -78,13 +79,14 @@ const removeElementShortcut = (shortcut: HotkeyShortcut) => { if (shortcut.ref?.current && !shortcut.disabled) { const handler = handlers.get(shortcut.ref?.current) as Handler; - handler.remove(...extractKeys(shortcut.keys), filter(shortcut.callback)); + handler.remove(...extractKeys(shortcut.keys), shortcut.callback); shortcut.ref?.current?.removeEventListener(shortcut.action ?? 'keypress', handler.handle); } }; -export const useHotkeys = (shortcuts: HotkeyShortcut[]) => { +export const useHotkeys = (shortcuts: HotkeyShortcut[] = []) => { + const [registered, setRegistered] = useState([]); /** * Register global listeners for "keypress", "keyup" and "keydown" events */ @@ -107,19 +109,31 @@ export const useHotkeys = (shortcuts: HotkeyShortcut[]) => { return; } + // Wrap callback in input focus wrapper to avoid trigger shortcut for input + shortcut.callback = focusInputWrapper(shortcut.callback); + if (shortcut.ref?.current) { registerElementShortcut(shortcut); + hotkeys = [...hotkeys, shortcut]; } else if (!shortcut.ref) { registerGlobalShortcut(shortcut); + hotkeys = [...hotkeys, shortcut]; } }); // Remove all shortcuts on destroy return () => { shortcuts.map((shortcut) => { - removeGlobalShortcut(shortcut); removeElementShortcut(shortcut); + removeGlobalShortcut(shortcut); + hotkeys = hotkeys.filter((hotkey) => shortcut !== hotkey); }); }; }, [shortcuts]); + + useEffect(() => { + setRegistered(hotkeys); + }, []); + + return registered; }; From 41c472cae8d5374bc7e56c719bc5d3e80a0f2502 Mon Sep 17 00:00:00 2001 From: Evgeniy Date: Thu, 29 Feb 2024 15:53:53 +0200 Subject: [PATCH 06/13] Readme update --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 5fda607..1b1fca8 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@

⌨️ reakeys


- React Hook for Mousetrap Hotkeys + React Hook for Ctrl-keys Hotkeys

@@ -12,7 +12,7 @@ - + @@ -85,11 +85,11 @@ const hotkeys = useHotkeys(); This is useful for creating a dialog to present the user with all the options. Below is an example of how to make -a dialog using [realayers](https://github.com/reaviz/realayers): +a dialog using [reablocks](https://github.com/reaviz/reablocks): ```jsx import React, { useState, FC, useCallback, useMemo } from 'react'; -import { Dialog } from 'realayers'; +import { Dialog } from 'reablocks'; import { useHotkeys, getHotkeyText } from 'reakeys'; import groupBy from 'lodash/groupBy'; import sortBy from 'lodash/sortBy'; From 0aa7c808d343811c172eca5a1e09444bf3039752 Mon Sep 17 00:00:00 2001 From: Evgeniy Date: Thu, 29 Feb 2024 15:55:53 +0200 Subject: [PATCH 07/13] yarn.lock update --- yarn.lock | 48 ++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 38 insertions(+), 10 deletions(-) diff --git a/yarn.lock b/yarn.lock index 3796bb1..fc2fb06 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3148,11 +3148,6 @@ resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-5.1.2.tgz#07508b45797cb81ec3f273011b054cd0755eddca" integrity sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA== -"@types/mousetrap@^1.6.11": - version "1.6.11" - resolved "https://registry.yarnpkg.com/@types/mousetrap/-/mousetrap-1.6.11.tgz#ef9620160fdcefcb85bccda8aaa3e84d7429376d" - integrity sha512-F0oAily9Q9QQpv9JKxKn0zMKfOo36KHCW7myYsmUyf2t0g+sBTbG3UleTPoguHdE1z3GLFr3p7/wiOio52QFjQ== - "@types/ms@*": version "0.7.31" resolved "https://registry.yarnpkg.com/@types/ms/-/ms-0.7.31.tgz#31b7ca6407128a3d2bbc27fe2d21b345397f6197" @@ -3176,6 +3171,13 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-16.18.34.tgz#62d2099b30339dec4b1b04a14c96266459d7c8b2" integrity sha512-VmVm7gXwhkUimRfBwVI1CHhwp86jDWR04B5FGebMMyxV90SlCmFujwUHrxTD4oO+SOYU86SoxvhgeRQJY7iXFg== +"@types/node@^18.15.11": + version "18.19.21" + resolved "https://registry.yarnpkg.com/@types/node/-/node-18.19.21.tgz#f4ca1ac8ffb05ee4b89163c2d6fac9a1a59ee149" + integrity sha512-2Q2NeB6BmiTFQi4DHBzncSoq/cJMLDdhPaAoJFnFCyD9a8VPZRf7a1GAwp1Edb7ROaZc5Jz/tnZyL6EsWMRaqw== + dependencies: + undici-types "~5.26.4" + "@types/normalize-package-data@^2.4.0": version "2.4.0" resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.0.tgz#e486d0d97396d79beedd0a6e33f4534ff6b4973e" @@ -4485,6 +4487,13 @@ csstype@^3.0.2: resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.0.3.tgz#2b410bbeba38ba9633353aff34b05d9755d065f8" integrity sha512-jPl+wbWPOWJ7SXsWyqGRk3lGecbar0Cb0OvZF/r/ZU011R4YqiRehgkQ9p4eQfo9DSDLqLL3wHwfxeJiuIsNag== +ctrl-keys@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/ctrl-keys/-/ctrl-keys-1.0.1.tgz#7d59a84a8175c11dd590371c1430e328630271a3" + integrity sha512-0cfxE8pQDSAl9pkkbksf0RlHJDcreK1b/wUuPtmDGtbVF8z39mNb6+UcXxSjrdYTAqncjShEz8KgtDX+EiRN7A== + dependencies: + just-types "^2.0.0-alpha.2" + date-time@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/date-time/-/date-time-3.1.0.tgz#0d1e934d170579f481ed8df1e2b8ff70ee845e1e" @@ -6511,6 +6520,15 @@ jsonfile@^6.0.1: array-includes "^3.1.5" object.assign "^4.1.3" +just-types@^2.0.0-alpha.2: + version "2.0.0-alpha.2" + resolved "https://registry.yarnpkg.com/just-types/-/just-types-2.0.0-alpha.2.tgz#672d4ab31b4124afd43f7f9f74b278133979403e" + integrity sha512-RvudbWEC0Eo8uijI3HUb0EVehSPxrxmWVUPzU/KPZDXoQosnDo0A9ZeynBFGOJbI1QqYvz4ZC4XXL4yAexrtvw== + dependencies: + "@types/node" "^18.15.11" + tslib "^2.5.0" + typescript "^5.0.3" + kind-of@^6.0.2: version "6.0.3" resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd" @@ -7369,11 +7387,6 @@ mlly@^1.2.0: pkg-types "^1.0.3" ufo "^1.1.2" -mousetrap@^1.6.5: - version "1.6.5" - resolved "https://registry.yarnpkg.com/mousetrap/-/mousetrap-1.6.5.tgz#8a766d8c272b08393d5f56074e0b5ec183485bf9" - integrity sha512-QNo4kEepaIBwiT8CDhP98umTetp+JNfQYBWvC1pc6/OAibuXtRcxZ58Qz8skvEHYvURne/7R8T5VoOI7rDsEUA== - mri@^1.1.0, mri@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/mri/-/mri-1.2.0.tgz#6721480fec2a11a4889861115a48b6cbe7cc8f0b" @@ -9195,6 +9208,11 @@ tslib@^2.1.0, tslib@^2.4.0: resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.5.2.tgz#1b6f07185c881557b0ffa84b111a0106989e8338" integrity sha512-5svOrSA2w3iGFDs1HibEVBGbDrAY82bFQ3HZ3ixB+88nsbsWQoKqDRb5UBYAUPEzbBn6dAp5gRNXglySbx1MlA== +tslib@^2.5.0: + version "2.6.2" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.2.tgz#703ac29425e7b37cd6fd456e92404d46d1f3e4ae" + integrity sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q== + tsutils@^3.21.0: version "3.21.0" resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.21.0.tgz#b48717d394cea6c1e096983eed58e9d61715b623" @@ -9278,6 +9296,11 @@ typescript@*, typescript@~5.0.4: resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.0.4.tgz#b217fd20119bd61a94d4011274e0ab369058da3b" integrity sha512-cW9T5W9xY37cc+jfEnaUvX91foxtHkza3Nw3wkoF4sSlKn0MONdkdEndig/qPBWXNkmplh3NzayQzCiHM4/hqw== +typescript@^5.0.3: + version "5.3.3" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.3.3.tgz#b3ce6ba258e72e6305ba66f5c9b452aaee3ffe37" + integrity sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw== + ufo@^1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/ufo/-/ufo-1.1.2.tgz#d0d9e0fa09dece0c31ffd57bd363f030a35cfe76" @@ -9298,6 +9321,11 @@ unbox-primitive@^1.0.2: has-symbols "^1.0.3" which-boxed-primitive "^1.0.2" +undici-types@~5.26.4: + version "5.26.5" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617" + integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA== + unfetch@^4.2.0: version "4.2.0" resolved "https://registry.yarnpkg.com/unfetch/-/unfetch-4.2.0.tgz#7e21b0ef7d363d8d9af0fb929a5555f6ef97a3be" From fd96bb52ce1dd53e6c21db808db788cf6f8b89df Mon Sep 17 00:00:00 2001 From: Evgeniy Date: Thu, 29 Feb 2024 15:57:47 +0200 Subject: [PATCH 08/13] Fix export --- src/index.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/index.ts b/src/index.ts index d1bf4c7..0031f23 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,3 +1,2 @@ export * from './useHotkeys'; -export * from './useHotkeyState'; export * from './utils'; From 0193b9e94b4c9609df4841391df7eda4f75893ca Mon Sep 17 00:00:00 2001 From: Evgeniy Date: Thu, 29 Feb 2024 16:06:45 +0200 Subject: [PATCH 09/13] Add hidden property --- src/useHotkeys.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/useHotkeys.ts b/src/useHotkeys.ts index b4ebd15..d3218cb 100644 --- a/src/useHotkeys.ts +++ b/src/useHotkeys.ts @@ -12,6 +12,7 @@ export interface HotkeyShortcut { action?: 'keypress' | 'keydown' | 'keyup'; description?: string; category?: string; + hidden?: boolean; } let isGlobalListenersBinded = false; From 06efccb11d64170ddf7f4d0f21823d6c6c6e2bfa Mon Sep 17 00:00:00 2001 From: Evgeniy Date: Thu, 29 Feb 2024 16:08:33 +0200 Subject: [PATCH 10/13] Rename interface --- src/useHotkeys.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/useHotkeys.ts b/src/useHotkeys.ts index d3218cb..32ce180 100644 --- a/src/useHotkeys.ts +++ b/src/useHotkeys.ts @@ -3,7 +3,7 @@ import keys, { Callback, Handler, Key } from 'ctrl-keys'; type Keys = [Key] | [Key, Key] | [Key, Key, Key] | [Key, Key, Key, Key]; -export interface HotkeyShortcut { +export interface HotkeyShortcuts { name: string; keys: Key | Keys; ref?: RefObject; @@ -25,7 +25,7 @@ const keydownGlobalHandler = keys(); * Map of specific elements handlers */ const handlers = new Map(); -let hotkeys: HotkeyShortcut[] = []; +let hotkeys: HotkeyShortcuts[] = []; const extractKeys = (keys: Key | Keys): Keys => { return Array.isArray(keys) ? keys : [keys]; @@ -42,7 +42,7 @@ const focusInputWrapper = (callback: Callback) => (event: any) => { return callback(event); }; -const registerGlobalShortcut = (shortcut: HotkeyShortcut) => { +const registerGlobalShortcut = (shortcut: HotkeyShortcuts) => { if (!shortcut.action || shortcut.action === 'keypress') { keypressGlobalHandler.add(...extractKeys(shortcut.keys), shortcut.callback); } @@ -54,7 +54,7 @@ const registerGlobalShortcut = (shortcut: HotkeyShortcut) => { } }; -const removeGlobalShortcut = (shortcut: HotkeyShortcut) => { +const removeGlobalShortcut = (shortcut: HotkeyShortcuts) => { if (!shortcut.action || shortcut.action === 'keypress') { keypressGlobalHandler.remove(...extractKeys(shortcut.keys), shortcut.callback); } @@ -66,7 +66,7 @@ const removeGlobalShortcut = (shortcut: HotkeyShortcut) => { } }; -const registerElementShortcut = (shortcut: HotkeyShortcut) => { +const registerElementShortcut = (shortcut: HotkeyShortcuts) => { const handler = keys(); handler.add(...extractKeys(shortcut.keys), shortcut.callback); @@ -76,7 +76,7 @@ const registerElementShortcut = (shortcut: HotkeyShortcut) => { handlers.set(shortcut.ref?.current as HTMLElement, handler); }; -const removeElementShortcut = (shortcut: HotkeyShortcut) => { +const removeElementShortcut = (shortcut: HotkeyShortcuts) => { if (shortcut.ref?.current && !shortcut.disabled) { const handler = handlers.get(shortcut.ref?.current) as Handler; @@ -86,8 +86,8 @@ const removeElementShortcut = (shortcut: HotkeyShortcut) => { } }; -export const useHotkeys = (shortcuts: HotkeyShortcut[] = []) => { - const [registered, setRegistered] = useState([]); +export const useHotkeys = (shortcuts: HotkeyShortcuts[] = []) => { + const [registered, setRegistered] = useState([]); /** * Register global listeners for "keypress", "keyup" and "keydown" events */ From 23faeb1589c9b84188f7c308cec6242120433cc0 Mon Sep 17 00:00:00 2001 From: Evgeniy Date: Thu, 29 Feb 2024 16:32:00 +0200 Subject: [PATCH 11/13] Handle uppecased hotkey --- src/Hotkey.story.tsx | 32 ++++++++++++++++---------------- src/useHotkeys.ts | 6 +++--- 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/src/Hotkey.story.tsx b/src/Hotkey.story.tsx index b305e15..40ed4a7 100644 --- a/src/Hotkey.story.tsx +++ b/src/Hotkey.story.tsx @@ -6,7 +6,7 @@ export default { }; export const Simple = () => { - const hotkeys = useHotkeys([{ name: 'Simple', keys: 'shift+a', callback: () => alert('SHIFT + A pressed') }]); + const hotkeys = useHotkeys([{ name: 'Simple', keys: 'SHIFT+A', callback: () => alert('SHIFT + A pressed') }]); return (
@@ -23,7 +23,7 @@ export const Simple = () => { }; export const Input = () => { - const hotkeys = useHotkeys([{ name: 'Input', keys: 'shift+a', callback: () => alert('SHIFT + A pressed') }]); + const hotkeys = useHotkeys([{ name: 'Input', keys: 'SHIFT+A', callback: () => alert('SHIFT + A pressed') }]); return (
@@ -44,7 +44,7 @@ export const Input = () => { export const Disable = () => { const [disabled, setDisabled] = useState(false); - const hotkeys = useHotkeys([{ name: 'Disable', keys: 'shift+a', callback: () => alert('SHIFT + A pressed'), disabled }]); + const hotkeys = useHotkeys([{ name: 'Disable', keys: 'SHIFT+A', callback: () => alert('SHIFT + A pressed'), disabled }]); return (
@@ -67,7 +67,7 @@ export const Refs = () => { const hotkeys = useHotkeys([ { name: 'Refs', - keys: 'shift+a', + keys: 'SHIFT+A', callback: () => { alert(`color: ${color}`); }, @@ -95,8 +95,8 @@ export const Refs = () => { export const Multiple = () => { const hotkeys = useHotkeys([ - { name: 'Nested A', keys: ['shift+a'], callback: () => alert('SHIFT + A pressed') }, - { name: 'Nested B', keys: ['meta+b'], callback: () => alert('META + B pressed') }, + { name: 'Nested A', keys: ['SHIFT+A'], callback: () => alert('SHIFT + A pressed') }, + { name: 'Nested B', keys: ['META+B'], callback: () => alert('META + B pressed') }, ]); return ( @@ -117,13 +117,13 @@ export const Multiple = () => { }; const NestedComponent = () => { - useHotkeys([{ name: 'Child', keys: ['meta+b'], callback: () => alert('META + B (child)') }]); + useHotkeys([{ name: 'Child', keys: ['META+B'], callback: () => alert('META + B (child)') }]); - return

Press MOD + b

; + return

Press META + B

; }; export const Nested = () => { - const hotkeys = useHotkeys([{ name: 'Parent', keys: ['shift+a'], callback: () => alert('SHIFT + A (parent)') }]); + const hotkeys = useHotkeys([{ name: 'Parent', keys: ['SHIFT+A'], callback: () => alert('SHIFT + A (parent)') }]); return (
@@ -149,13 +149,13 @@ export const Focus = () => { const hotkeys = useHotkeys([ { name: 'Focus A', - keys: ['shift+c'], + keys: ['SHIFT+C'], callback: () => alert(`first, counter: ${counter}`), ref: elmRef, }, { name: 'Focus b', - keys: ['shift+c'], + keys: ['SHIFT+C'], callback: () => alert(`second, counter: ${counter}`), ref: elmRef2, }, @@ -195,7 +195,7 @@ export const Action = () => { const hotkeys = useHotkeys([ { name: 'Action', - keys: ['f'], + keys: ['F'], callback: () => alert("You've been promoted!"), action: 'keyup', }, @@ -222,7 +222,7 @@ export const Asynchronous = () => { useHotkeys([ { name: 'Asynchronous', - keys: ['l'], + keys: ['L'], callback: () => alert('Hey!'), action: 'keyup', ref: elmRef, @@ -243,7 +243,7 @@ export const Asynchronous = () => { return (
- {loaded ? 'Loaded' : 'Loading (pressing "l" is disabled until the element is shown and focused)...'} + {loaded ? 'Loaded' : 'Loading (pressing "L" is disabled until the element is shown and focused)...'}