react-hotkeys-hook 3.4.5 → 4.0.0-0
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/README.md +1 -1
- package/dist/BoundHotkeysProxyProvider.d.ts +14 -0
- package/dist/HotkeysProvider.d.ts +4 -4
- package/dist/index.js +1 -1
- package/dist/index.js.map +3 -3
- package/dist/types.d.ts +2 -2
- package/dist/validators.d.ts +1 -1
- package/package.json +30 -39
- package/src/BoundHotkeysProxyProvider.tsx +23 -0
- package/src/HotkeysProvider.tsx +78 -0
- package/src/index.ts +9 -6
- package/src/isHotkeyPressed.ts +51 -0
- package/src/parseHotkeys.ts +33 -0
- package/src/types.ts +41 -0
- package/src/useHotkeys.ts +89 -77
- package/src/validators.ts +92 -0
- package/src/index.test.tsx +0 -230
- package/src/useIsHotkeyPressed.ts +0 -8
package/README.md
CHANGED
|
@@ -37,7 +37,7 @@ A React hook for using keyboard shortcuts in components.
|
|
|
37
37
|
## Quick Start
|
|
38
38
|
|
|
39
39
|
```jsx harmony
|
|
40
|
-
import { useHotkeys } from 'react-
|
|
40
|
+
import { useHotkeys } from 'react-hotkeys-hook'
|
|
41
41
|
|
|
42
42
|
export const ExampleComponent = () => {
|
|
43
43
|
const [count, setCount] = useState(0)
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { ReactNode } from 'react';
|
|
2
|
+
import { Hotkey } from './types';
|
|
3
|
+
declare type BoundHotkeysProxyProviderType = {
|
|
4
|
+
addHotkey: (hotkey: Hotkey) => void;
|
|
5
|
+
removeHotkey: (hotkey: Hotkey) => void;
|
|
6
|
+
};
|
|
7
|
+
export declare const useBoundHotkeysProxy: () => BoundHotkeysProxyProviderType | undefined;
|
|
8
|
+
interface Props {
|
|
9
|
+
children: ReactNode;
|
|
10
|
+
addHotkey: (hotkey: Hotkey) => void;
|
|
11
|
+
removeHotkey: (hotkey: Hotkey) => void;
|
|
12
|
+
}
|
|
13
|
+
export default function BoundHotkeysProxyProviderProvider({ addHotkey, removeHotkey, children }: Props): JSX.Element;
|
|
14
|
+
export {};
|
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
import { Hotkey } from './types';
|
|
2
2
|
import { ReactNode } from 'react';
|
|
3
3
|
declare type HotkeysContextType = {
|
|
4
|
-
hotkeys: Hotkey
|
|
4
|
+
hotkeys: ReadonlyArray<Hotkey>;
|
|
5
5
|
activeScopes: string[];
|
|
6
6
|
toggleScope: (scope: string) => void;
|
|
7
7
|
activateScope: (scope: string) => void;
|
|
8
8
|
deactivateScope: (scope: string) => void;
|
|
9
9
|
};
|
|
10
|
+
export declare const useHotkeysContext: () => HotkeysContextType;
|
|
10
11
|
interface Props {
|
|
11
|
-
|
|
12
|
+
initiallyActiveScopes?: string[];
|
|
12
13
|
children: ReactNode;
|
|
13
14
|
}
|
|
14
|
-
export declare const
|
|
15
|
-
export declare const HotkeysProvider: ({ initialActiveScopes, children }: Props) => JSX.Element;
|
|
15
|
+
export declare const HotkeysProvider: ({ initiallyActiveScopes, children }: Props) => JSX.Element;
|
|
16
16
|
export {};
|
package/dist/index.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{useCallback as
|
|
1
|
+
import{useCallback as _,useLayoutEffect as j,useRef as B}from"react";var O=["ctrl","shift","alt","meta","mod"];function v(t,o=","){return typeof t=="string"?t.split(o):t}function H(t,o="+"){let e=t.toLocaleLowerCase().split(o).map(c=>c.trim()),y={alt:e.includes("alt"),ctrl:e.includes("ctrl"),shift:e.includes("shift"),meta:e.includes("meta"),mod:e.includes("mod")},i=e.filter(c=>!O.includes(c));return{...y,keys:i}}function b(t,o,e){(typeof e=="function"&&e(t,o)||e===!0)&&t.preventDefault()}function h(t,o,e){return typeof e=="function"?e(t,o):e===!0||e===void 0}function P(t){return g(t,["input","textarea","select"])}function g({target:t},o=!1){let e=t&&t.tagName;return o instanceof Array?Boolean(e&&o&&o.some(y=>y.toLowerCase()===e.toLowerCase())):Boolean(e&&o&&o===!0)}function E(t,o){return t.length===0&&o?(console.warn('A hotkey has the "scopes" option set, however no active scopes were found. If you want to use the global scopes feature, you need to wrap your app in a <HotkeysProvider>'),!0):o?t.some(e=>o.includes(e))||t.includes("*"):!0}var C=(t,o,e)=>{let{alt:y,ctrl:i,meta:c,mod:r,shift:p,keys:a}=o,{altKey:k,ctrlKey:d,metaKey:l,shiftKey:s,key:u,code:n}=t,m=n.toLowerCase().replace("key",""),f=u.toLowerCase();if(k!==y&&f!=="alt"||s!==p&&f!=="shift")return!1;if(r){if(!l&&!d)return!1}else if(l!==c&&m!=="meta"||d!==i&&m!=="ctrl")return!1;return a&&a.length===1&&(a.includes(f)||a.includes(m))?!0:a?a.every(M=>e.has(M)):!a};import{createContext as I,useMemo as N,useState as T,useContext as F}from"react";import{createContext as R,useContext as D}from"react";var S=R(void 0),w=()=>D(S);function x({addHotkey:t,removeHotkey:o,children:e}){return React.createElement(S.Provider,{value:{addHotkey:t,removeHotkey:o}},e)}var A=I({hotkeys:[],activeScopes:[],toggleScope:()=>{},activateScope:()=>{},deactivateScope:()=>{}}),K=()=>F(A),U=({initiallyActiveScopes:t=["*"],children:o})=>{let[e,y]=T(t?.length>0?t:["*"]),[i,c]=T([]),r=N(()=>e.includes("*"),[e]),p=s=>{y(r?[s]:Array.from(new Set([...e,s])))},a=s=>{let u=e.filter(n=>n!==s);u.length===0?y(["*"]):y(u)},k=s=>{e.includes(s)?a(s):p(s)},d=s=>{c([...i,s])},l=s=>{c(i.filter(u=>u.keys!==s.keys))};return React.createElement(A.Provider,{value:{activeScopes:e,hotkeys:i,activateScope:p,deactivateScope:a,toggleScope:k}},React.createElement(x,{addHotkey:d,removeHotkey:l},o))};function L(t,o,e,y){let i=B(null),{current:c}=B(new Set),r=e instanceof Array?y instanceof Array?void 0:y:e,p=e instanceof Array?e:y instanceof Array?y:[],a=_(o,[...p]),k=K(),d=w();return j(()=>{if(r?.enabled===!1||!E(k.activeScopes,r?.scopes))return;let l=n=>{P(n)&&!g(n,r?.enableOnFormTags)||i.current!==null&&document.activeElement!==i.current&&!i.current.contains(document.activeElement)||n.target?.isContentEditable&&!r?.enableOnContentEditable||v(t,r?.splitKey).forEach(m=>{let f=H(m,r?.combinationKey);if(C(n,f,c)||f.keys?.includes("*")){if(b(n,f,r?.preventDefault),!h(n,f,r?.enabled))return;a(n,f)}})},s=n=>{c.add(n.key.toLowerCase()),(r?.keydown===void 0&&r?.keyup!==!0||r?.keydown)&&l(n)},u=n=>{c.delete(n.key.toLowerCase()),r?.keyup&&l(n)};return(i.current||document).addEventListener("keyup",u),(i.current||document).addEventListener("keydown",s),d&&v(t,r?.splitKey).forEach(n=>d.addHotkey(H(n,r?.combinationKey))),()=>{(i.current||document).removeEventListener("keyup",u),(i.current||document).removeEventListener("keydown",s),d&&v(t,r?.splitKey).forEach(n=>d.removeHotkey(H(n,r?.combinationKey)))}},[t,a,r]),i}export{U as HotkeysProvider,L as useHotkeys,K as useHotkeysContext};
|
|
2
2
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
|
-
"sources": ["../src/useHotkeys.ts", "../src/parseHotkeys.ts", "../src/validators.ts", "../src/HotkeysProvider.tsx"],
|
|
4
|
-
"sourcesContent": ["import { HotkeyCallback, Keys, OptionsOrDependencyArray, RefType } from './types'\nimport { useCallback, useLayoutEffect, useRef } from 'react'\nimport { parseHotkey, parseKeysHookInput } from './parseHotkeys'\nimport {\n isHotkeyEnabled,\n isHotkeyEnabledOnTag,\n isHotkeyMatchingKeyboardEvent,\n isKeyboardEventTriggeredByInput,\n isScopeActive,\n maybePreventDefault,\n} from './validators'\nimport { useHotkeysContext } from './HotkeysProvider'\n\nexport default function useHotkeys<T extends HTMLElement>(\n keys: Keys,\n callback: HotkeyCallback,\n options?: OptionsOrDependencyArray,\n dependencies?: OptionsOrDependencyArray\n) {\n const ref = useRef<RefType<T>>(null)\n const { current: pressedDownKeys } = useRef<Set<string>>(new Set())\n\n const _options = !(options instanceof Array) ? options : !(dependencies instanceof Array) ? dependencies : undefined\n const _deps = options instanceof Array ? options : dependencies instanceof Array ? dependencies : []\n\n const cb = useCallback(callback, [..._deps])\n const ctx = useHotkeysContext()\n\n useLayoutEffect(() => {\n if (_options?.enabled === false || !isScopeActive(ctx.activeScopes, _options?.scopes)) {\n return\n }\n\n const listener = (e: KeyboardEvent) => {\n if (isKeyboardEventTriggeredByInput(e) && !isHotkeyEnabledOnTag(e, _options?.enableOnTags)) {\n return\n }\n\n if (ref.current !== null && document.activeElement !== ref.current) {\n return\n }\n\n console.log('isContentEditable', ((e.target as HTMLElement)?.isContentEditable))\n\n if (((e.target as HTMLElement)?.isContentEditable && !_options?.enableOnContentEditable)) {\n return\n }\n\n parseKeysHookInput(keys, _options?.splitKey).forEach((key) => {\n const hotkey = parseHotkey(key, _options?.combinationKey)\n\n if (isHotkeyMatchingKeyboardEvent(e, hotkey, pressedDownKeys) || hotkey.keys?.includes('*')) {\n maybePreventDefault(e, hotkey, _options?.preventDefault)\n\n if (!isHotkeyEnabled(e, hotkey, _options?.enabled)) {\n return\n }\n\n cb(e, hotkey)\n }\n })\n }\n\n const handleKeyDown = (event: KeyboardEvent) => {\n pressedDownKeys.add(event.key.toLowerCase())\n\n if ((_options?.keydown === undefined && _options?.keyup !== true) || _options?.keydown) {\n listener(event)\n }\n }\n\n const handleKeyUp = (event: KeyboardEvent) => {\n if (_options?.keyup) {\n listener(event)\n }\n\n pressedDownKeys.delete(event.key.toLowerCase())\n }\n\n document.addEventListener('keyup', handleKeyUp)\n document.addEventListener('keydown', handleKeyDown)\n\n return () => {\n document.removeEventListener('keyup', handleKeyUp)\n document.removeEventListener('keydown', handleKeyDown)\n }\n }, [keys, cb, _options])\n\n return ref\n}\n", "import { Hotkey, KeyboardModifiers, Keys } from './types'\n\nconst reservedModifierKeywords = ['ctrl', 'shift', 'alt', 'meta', 'mod']\n\nexport function parseKeysHookInput(keys: Keys, splitKey: string = ','): string[] {\n if (typeof keys === 'string') {\n return keys.split(splitKey)\n }\n\n return keys\n}\n\nexport function parseHotkey(hotkey: string, combinationKey: string = '+'): Hotkey {\n const keys = hotkey\n .toLocaleLowerCase()\n .split(combinationKey)\n .map((k) => k.trim())\n\n const modifiers: KeyboardModifiers = {\n alt: keys.includes('alt'),\n ctrl: keys.includes('ctrl'),\n shift: keys.includes('shift'),\n meta: keys.includes('meta'),\n mod: keys.includes('mod'),\n }\n\n const singleCharKeys = keys.filter((k) => !reservedModifierKeywords.includes(k))\n\n return {\n ...modifiers,\n keys: singleCharKeys,\n }\n}\n", "import { FormTags, Hotkey, Scopes, Trigger } from './types'\n\nexport function maybePreventDefault(e: KeyboardEvent, hotkey: Hotkey, preventDefault?: Trigger): void {\n if ((typeof preventDefault === 'function' && preventDefault(e, hotkey)) || preventDefault === true) {\n e.preventDefault()\n }\n}\n\nexport function isHotkeyEnabled(e: KeyboardEvent, hotkey: Hotkey, enabled?: Trigger): boolean {\n if (typeof enabled === 'function') {\n return enabled(e, hotkey)\n }\n\n return enabled === true || enabled === undefined\n}\n\nexport function isKeyboardEventTriggeredByInput(ev: KeyboardEvent): boolean {\n return isHotkeyEnabledOnTag(ev, ['INPUT', 'TEXTAREA', 'SELECT'])\n}\n\nexport function isHotkeyEnabledOnTag({ target }: KeyboardEvent, enabledOnTags?: FormTags[]): boolean {\n const targetTagName = target && (target as HTMLElement).tagName\n\n return Boolean(targetTagName && enabledOnTags && enabledOnTags.includes(targetTagName as FormTags))\n}\n\nexport function isScopeActive(activeScopes: string[], scopes?: Scopes): boolean {\n if (activeScopes.length === 0 && scopes) {\n console.warn(\n 'A hotkey has a set scopes options, although no active scopes were found. If you want to use the global scopes feature, you need to wrap your app in a <HotkeysProvider>'\n )\n\n return true\n }\n\n if (!scopes) {\n return true\n }\n\n return activeScopes.some(scope => scopes.includes(scope)) || activeScopes.includes('*')\n}\n\nexport const isHotkeyMatchingKeyboardEvent = (e: KeyboardEvent, hotkey: Hotkey, pressedDownKeys: Set<string>): boolean => {\n const { alt, ctrl, meta, mod, shift, keys } = hotkey\n const { altKey, ctrlKey, metaKey, shiftKey, key: pressedKeyUppercase, code } = e\n\n const keyCode = code.toLowerCase().replace('key', '')\n const pressedKey = pressedKeyUppercase.toLowerCase()\n\n if (altKey !== alt && pressedKey !== 'alt') {\n return false\n }\n\n if (shiftKey !== shift && pressedKey !== 'shift') {\n return false\n }\n\n // Mod is a special key name that is checking for meta on macOS and ctrl on other platforms\n if (mod) {\n if (!metaKey && !ctrlKey) {\n return false\n }\n } else {\n if (metaKey !== meta && keyCode !== 'meta') {\n return false\n }\n\n if (ctrlKey !== ctrl && keyCode !== 'ctrl') {\n return false\n }\n }\n\n // All modifiers are correct, now check the key\n // If the key is set we check for the key\n if (keys && keys.length === 1 && (keys.includes(pressedKey) || keys.includes(keyCode))) {\n return true\n } else if (keys) {\n // Check if all keys are present in pressedDownKeys set\n return keys.every(key => pressedDownKeys.has(key))\n }\n else if (!keys) {\n // If the key is not set, we only listen for modifiers, that check went alright, so we return true\n return true\n }\n\n // There is nothing that matches.\n return false\n}\n", "import { Hotkey } from './types'\nimport { createContext, ReactNode, useMemo, useState, useContext } from 'react'\n\ntype HotkeysContextType = {\n hotkeys: Hotkey[]\n activeScopes: string[]\n toggleScope: (scope: string) => void\n activateScope: (scope: string) => void\n deactivateScope: (scope: string) => void\n}\n\nconst HotkeysContext = createContext<HotkeysContextType | undefined>(undefined)\n\ninterface Props {\n initialActiveScopes?: string[]\n children: ReactNode\n}\n\nexport const useHotkeysContext = () => {\n const context = useContext(HotkeysContext)\n\n // The context is only needed for special features like global scoping, so we don't throw an error if it's not defined\n if (context === undefined) {\n return {\n hotkeys: [],\n activeScopes: [],\n toggleScope: () => {},\n activateScope: () => {},\n deactivateScope: () => {},\n }\n }\n\n return context\n}\n\nexport const HotkeysProvider = ({initialActiveScopes = ['*'], children}: Props) => {\n const [activeScopes, setActiveScopes] = useState(initialActiveScopes?.length > 0 ? initialActiveScopes : ['*'])\n\n const isAllActive = useMemo(() => activeScopes.includes('*'), [activeScopes])\n\n const activateScope = (scope: string) => {\n if (isAllActive) {\n setActiveScopes([scope])\n } else {\n setActiveScopes([...activeScopes, scope])\n }\n }\n\n const deactivateScope = (scope: string) => {\n const scopes = activeScopes.filter(s => s !== scope)\n\n if (scopes.length === 0) {\n setActiveScopes(['*'])\n } else {\n setActiveScopes(scopes)\n }\n }\n\n const toggleScope = (scope: string) => {\n if (activeScopes.includes(scope)) {\n deactivateScope(scope)\n } else {\n activateScope(scope)\n }\n }\n\n return (\n <HotkeysContext.Provider value={{activeScopes, hotkeys: [], activateScope, deactivateScope, toggleScope}}>\n {children}\n </HotkeysContext.Provider>\n )\n}\n"],
|
|
5
|
-
"mappings": "AACA,qECCA,GAAM,GAA2B,CAAC,OAAQ,QAAS,MAAO,OAAQ,
|
|
3
|
+
"sources": ["../src/useHotkeys.ts", "../src/parseHotkeys.ts", "../src/validators.ts", "../src/HotkeysProvider.tsx", "../src/BoundHotkeysProxyProvider.tsx"],
|
|
4
|
+
"sourcesContent": ["import { HotkeyCallback, Keys, OptionsOrDependencyArray, RefType } from './types'\nimport { useCallback, useLayoutEffect, useRef } from 'react'\nimport { parseHotkey, parseKeysHookInput } from './parseHotkeys'\nimport {\n isHotkeyEnabled,\n isHotkeyEnabledOnTag,\n isHotkeyMatchingKeyboardEvent,\n isKeyboardEventTriggeredByInput,\n isScopeActive,\n maybePreventDefault,\n} from './validators'\nimport { useHotkeysContext } from './HotkeysProvider'\nimport { useBoundHotkeysProxy } from './BoundHotkeysProxyProvider'\n\nexport default function useHotkeys<T extends HTMLElement>(\n keys: Keys,\n callback: HotkeyCallback,\n options?: OptionsOrDependencyArray,\n dependencies?: OptionsOrDependencyArray,\n) {\n const ref = useRef<RefType<T>>(null)\n const { current: pressedDownKeys } = useRef<Set<string>>(new Set())\n\n const _options = !(options instanceof Array) ? options : !(dependencies instanceof Array) ? dependencies : undefined\n const _deps = options instanceof Array ? options : dependencies instanceof Array ? dependencies : []\n\n const cb = useCallback(callback, [..._deps])\n const ctx = useHotkeysContext()\n\n const proxy = useBoundHotkeysProxy()\n\n useLayoutEffect(() => {\n if (_options?.enabled === false || !isScopeActive(ctx.activeScopes, _options?.scopes)) {\n return\n }\n\n const listener = (e: KeyboardEvent) => {\n if (isKeyboardEventTriggeredByInput(e) && !isHotkeyEnabledOnTag(e, _options?.enableOnFormTags)) {\n return\n }\n\n if (ref.current !== null && document.activeElement !== ref.current && !ref.current.contains(document.activeElement)) {\n return\n }\n\n if (((e.target as HTMLElement)?.isContentEditable && !_options?.enableOnContentEditable)) {\n return\n }\n\n parseKeysHookInput(keys, _options?.splitKey).forEach((key) => {\n const hotkey = parseHotkey(key, _options?.combinationKey)\n\n if (isHotkeyMatchingKeyboardEvent(e, hotkey, pressedDownKeys) || hotkey.keys?.includes('*')) {\n maybePreventDefault(e, hotkey, _options?.preventDefault)\n\n if (!isHotkeyEnabled(e, hotkey, _options?.enabled)) {\n return\n }\n\n cb(e, hotkey)\n }\n })\n }\n\n const handleKeyDown = (event: KeyboardEvent) => {\n pressedDownKeys.add(event.key.toLowerCase())\n\n if ((_options?.keydown === undefined && _options?.keyup !== true) || _options?.keydown) {\n listener(event)\n }\n }\n\n const handleKeyUp = (event: KeyboardEvent) => {\n pressedDownKeys.delete(event.key.toLowerCase())\n\n if (_options?.keyup) {\n listener(event)\n }\n }\n\n // @ts-ignore\n (ref.current || document).addEventListener('keyup', handleKeyUp);\n // @ts-ignore\n (ref.current || document).addEventListener('keydown', handleKeyDown)\n\n if (proxy) {\n parseKeysHookInput(keys, _options?.splitKey).forEach((key) => proxy.addHotkey(parseHotkey(key, _options?.combinationKey)))\n }\n\n return () => {\n // @ts-ignore\n (ref.current || document).removeEventListener('keyup', handleKeyUp);\n // @ts-ignore\n (ref.current || document).removeEventListener('keydown', handleKeyDown)\n\n if (proxy) {\n parseKeysHookInput(keys, _options?.splitKey).forEach((key) => proxy.removeHotkey(parseHotkey(key, _options?.combinationKey)))\n }\n }\n }, [keys, cb, _options])\n\n return ref\n}\n", "import { Hotkey, KeyboardModifiers, Keys } from './types'\n\nconst reservedModifierKeywords = ['ctrl', 'shift', 'alt', 'meta', 'mod']\n\nexport function parseKeysHookInput(keys: Keys, splitKey: string = ','): string[] {\n if (typeof keys === 'string') {\n return keys.split(splitKey)\n }\n\n return keys\n}\n\nexport function parseHotkey(hotkey: string, combinationKey: string = '+'): Hotkey {\n const keys = hotkey\n .toLocaleLowerCase()\n .split(combinationKey)\n .map((k) => k.trim())\n\n const modifiers: KeyboardModifiers = {\n alt: keys.includes('alt'),\n ctrl: keys.includes('ctrl'),\n shift: keys.includes('shift'),\n meta: keys.includes('meta'),\n mod: keys.includes('mod'),\n }\n\n const singleCharKeys = keys.filter((k) => !reservedModifierKeywords.includes(k))\n\n return {\n ...modifiers,\n keys: singleCharKeys,\n }\n}\n", "import { FormTags, Hotkey, Scopes, Trigger } from './types'\n\nexport function maybePreventDefault(e: KeyboardEvent, hotkey: Hotkey, preventDefault?: Trigger): void {\n if ((typeof preventDefault === 'function' && preventDefault(e, hotkey)) || preventDefault === true) {\n e.preventDefault()\n }\n}\n\nexport function isHotkeyEnabled(e: KeyboardEvent, hotkey: Hotkey, enabled?: Trigger): boolean {\n if (typeof enabled === 'function') {\n return enabled(e, hotkey)\n }\n\n return enabled === true || enabled === undefined\n}\n\nexport function isKeyboardEventTriggeredByInput(ev: KeyboardEvent): boolean {\n return isHotkeyEnabledOnTag(ev, ['input', 'textarea', 'select'])\n}\n\nexport function isHotkeyEnabledOnTag({ target }: KeyboardEvent, enabledOnTags: FormTags[] | boolean = false): boolean {\n const targetTagName = target && (target as HTMLElement).tagName\n\n if (enabledOnTags instanceof Array) {\n return Boolean(targetTagName && enabledOnTags && enabledOnTags.some(tag => tag.toLowerCase() === targetTagName.toLowerCase()))\n }\n\n return Boolean(targetTagName && enabledOnTags && enabledOnTags === true)\n}\n\nexport function isScopeActive(activeScopes: string[], scopes?: Scopes): boolean {\n if (activeScopes.length === 0 && scopes) {\n console.warn(\n 'A hotkey has the \"scopes\" option set, however no active scopes were found. If you want to use the global scopes feature, you need to wrap your app in a <HotkeysProvider>'\n )\n\n return true\n }\n\n if (!scopes) {\n return true\n }\n\n return activeScopes.some(scope => scopes.includes(scope)) || activeScopes.includes('*')\n}\n\nexport const isHotkeyMatchingKeyboardEvent = (e: KeyboardEvent, hotkey: Hotkey, pressedDownKeys: Set<string>): boolean => {\n const { alt, ctrl, meta, mod, shift, keys } = hotkey\n const { altKey, ctrlKey, metaKey, shiftKey, key: pressedKeyUppercase, code } = e\n\n const keyCode = code.toLowerCase().replace('key', '')\n const pressedKey = pressedKeyUppercase.toLowerCase()\n\n if (altKey !== alt && pressedKey !== 'alt') {\n return false\n }\n\n if (shiftKey !== shift && pressedKey !== 'shift') {\n return false\n }\n\n // Mod is a special key name that is checking for meta on macOS and ctrl on other platforms\n if (mod) {\n if (!metaKey && !ctrlKey) {\n return false\n }\n } else {\n if (metaKey !== meta && keyCode !== 'meta') {\n return false\n }\n\n if (ctrlKey !== ctrl && keyCode !== 'ctrl') {\n return false\n }\n }\n\n // All modifiers are correct, now check the key\n // If the key is set we check for the key\n if (keys && keys.length === 1 && (keys.includes(pressedKey) || keys.includes(keyCode))) {\n return true\n } else if (keys) {\n // Check if all keys are present in pressedDownKeys set\n return keys.every(key => pressedDownKeys.has(key))\n }\n else if (!keys) {\n // If the key is not set, we only listen for modifiers, that check went alright, so we return true\n return true\n }\n\n // There is nothing that matches.\n return false\n}\n", "import { Hotkey } from './types'\nimport { createContext, ReactNode, useMemo, useState, useContext } from 'react'\nimport BoundHotkeysProxyProviderProvider from './BoundHotkeysProxyProvider'\n\ntype HotkeysContextType = {\n hotkeys: ReadonlyArray<Hotkey>\n activeScopes: string[]\n toggleScope: (scope: string) => void\n activateScope: (scope: string) => void\n deactivateScope: (scope: string) => void\n}\n\n// The context is only needed for special features like global scoping, so we use a graceful default fallback\nconst HotkeysContext = createContext<HotkeysContextType>({\n hotkeys: [],\n activeScopes: [], // This array has to be empty to check if the provider is set or not\n toggleScope: () => {},\n activateScope: () => {},\n deactivateScope: () => {},\n})\n\nexport const useHotkeysContext = () => {\n return useContext(HotkeysContext)\n}\n\ninterface Props {\n initiallyActiveScopes?: string[]\n children: ReactNode\n}\n\nexport const HotkeysProvider = ({initiallyActiveScopes = ['*'], children}: Props) => {\n const [internalActiveScopes, setInternalActiveScopes] = useState(initiallyActiveScopes?.length > 0 ? initiallyActiveScopes : ['*'])\n const [boundHotkeys, setBoundHotkeys] = useState<Hotkey[]>([]);\n\n const isAllActive = useMemo(() => internalActiveScopes.includes('*'), [internalActiveScopes])\n\n const activateScope = (scope: string) => {\n if (isAllActive) {\n setInternalActiveScopes([scope])\n } else {\n setInternalActiveScopes(Array.from(new Set([...internalActiveScopes, scope])))\n }\n }\n\n const deactivateScope = (scope: string) => {\n const scopes = internalActiveScopes.filter(s => s !== scope)\n\n if (scopes.length === 0) {\n setInternalActiveScopes(['*'])\n } else {\n setInternalActiveScopes(scopes)\n }\n }\n\n const toggleScope = (scope: string) => {\n if (internalActiveScopes.includes(scope)) {\n deactivateScope(scope)\n } else {\n activateScope(scope)\n }\n }\n\n const addBoundHotkey = (hotkey: Hotkey) => {\n setBoundHotkeys([...boundHotkeys, hotkey])\n }\n\n const removeBoundHotkey = (hotkey: Hotkey) => {\n setBoundHotkeys(boundHotkeys.filter(h => h.keys !== hotkey.keys))\n }\n\n return (\n <HotkeysContext.Provider value={{activeScopes: internalActiveScopes, hotkeys: boundHotkeys, activateScope, deactivateScope, toggleScope}}>\n <BoundHotkeysProxyProviderProvider addHotkey={addBoundHotkey} removeHotkey={removeBoundHotkey}>\n {children}\n </BoundHotkeysProxyProviderProvider>\n </HotkeysContext.Provider>\n )\n}\n", "import { createContext, ReactNode, useContext } from 'react'\nimport { Hotkey } from './types'\n\ntype BoundHotkeysProxyProviderType = {\n addHotkey: (hotkey: Hotkey) => void,\n removeHotkey: (hotkey: Hotkey) => void,\n}\n\nconst BoundHotkeysProxyProvider = createContext<BoundHotkeysProxyProviderType | undefined>(undefined)\n\nexport const useBoundHotkeysProxy = () => {\n return useContext(BoundHotkeysProxyProvider)\n}\n\ninterface Props {\n children: ReactNode\n addHotkey: (hotkey: Hotkey) => void\n removeHotkey: (hotkey: Hotkey) => void\n}\n\nexport default function BoundHotkeysProxyProviderProvider({ addHotkey, removeHotkey, children }: Props) {\n return <BoundHotkeysProxyProvider.Provider value={{addHotkey, removeHotkey}}>{children}</BoundHotkeysProxyProvider.Provider>\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/dist/types.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { DependencyList } from 'react';
|
|
2
|
-
export declare type FormTags = 'INPUT' | 'TEXTAREA' | 'SELECT';
|
|
2
|
+
export declare type FormTags = 'input' | 'textarea' | 'select' | 'INPUT' | 'TEXTAREA' | 'SELECT';
|
|
3
3
|
export declare type Keys = string | string[];
|
|
4
4
|
export declare type Scopes = string | string[];
|
|
5
5
|
export declare type RefType<T> = T | null;
|
|
@@ -19,7 +19,7 @@ export declare type HotkeyCallback = (keyboardEvent: KeyboardEvent, hotkeysEvent
|
|
|
19
19
|
export declare type Trigger = boolean | ((keyboardEvent: KeyboardEvent, hotkeysEvent: HotkeysEvent) => boolean);
|
|
20
20
|
export declare type Options = {
|
|
21
21
|
enabled?: Trigger;
|
|
22
|
-
|
|
22
|
+
enableOnFormTags?: FormTags[] | boolean;
|
|
23
23
|
enableOnContentEditable?: boolean;
|
|
24
24
|
combinationKey?: string;
|
|
25
25
|
splitKey?: string;
|
package/dist/validators.d.ts
CHANGED
|
@@ -2,6 +2,6 @@ import { FormTags, Hotkey, Scopes, Trigger } from './types';
|
|
|
2
2
|
export declare function maybePreventDefault(e: KeyboardEvent, hotkey: Hotkey, preventDefault?: Trigger): void;
|
|
3
3
|
export declare function isHotkeyEnabled(e: KeyboardEvent, hotkey: Hotkey, enabled?: Trigger): boolean;
|
|
4
4
|
export declare function isKeyboardEventTriggeredByInput(ev: KeyboardEvent): boolean;
|
|
5
|
-
export declare function isHotkeyEnabledOnTag({ target }: KeyboardEvent, enabledOnTags?: FormTags[]): boolean;
|
|
5
|
+
export declare function isHotkeyEnabledOnTag({ target }: KeyboardEvent, enabledOnTags?: FormTags[] | boolean): boolean;
|
|
6
6
|
export declare function isScopeActive(activeScopes: string[], scopes?: Scopes): boolean;
|
|
7
7
|
export declare const isHotkeyMatchingKeyboardEvent: (e: KeyboardEvent, hotkey: Hotkey, pressedDownKeys: Set<string>) => boolean;
|
package/package.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-hotkeys-hook",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "4.0.0-0",
|
|
4
4
|
"repository": "https://JohannesKlauss@github.com/JohannesKlauss/react-keymap-hook.git",
|
|
5
5
|
"homepage": "https://johannesklauss.github.io/react-hotkeys-hook/",
|
|
6
6
|
"author": "Johannes Klauss",
|
|
7
7
|
"main": "dist/index.js",
|
|
8
|
-
"
|
|
9
|
-
"module": "dist/
|
|
8
|
+
"types": "dist/index.d.ts",
|
|
9
|
+
"module": "dist/index.js",
|
|
10
10
|
"files": [
|
|
11
11
|
"dist",
|
|
12
12
|
"src"
|
|
@@ -25,8 +25,7 @@
|
|
|
25
25
|
],
|
|
26
26
|
"license": "MIT",
|
|
27
27
|
"scripts": {
|
|
28
|
-
"
|
|
29
|
-
"build": "tsdx build",
|
|
28
|
+
"build": "node ./esbuild.js && tsc --project tsconfig.build.json",
|
|
30
29
|
"test": "jest",
|
|
31
30
|
"publish": "np"
|
|
32
31
|
},
|
|
@@ -37,48 +36,40 @@
|
|
|
37
36
|
"@babel/preset-react"
|
|
38
37
|
]
|
|
39
38
|
},
|
|
40
|
-
"jest": {
|
|
41
|
-
"setupFilesAfterEnv": [
|
|
42
|
-
"./setupTests.js"
|
|
43
|
-
],
|
|
44
|
-
"testPathIgnorePatterns": [
|
|
45
|
-
"pkg",
|
|
46
|
-
".docz",
|
|
47
|
-
"docs"
|
|
48
|
-
]
|
|
49
|
-
},
|
|
50
39
|
"prettier": {
|
|
51
|
-
"printWidth":
|
|
52
|
-
"semi":
|
|
40
|
+
"printWidth": 120,
|
|
41
|
+
"semi": false,
|
|
53
42
|
"singleQuote": true,
|
|
54
43
|
"trailingComma": "es5"
|
|
55
44
|
},
|
|
56
|
-
"dependencies": {
|
|
57
|
-
"hotkeys-js": "3.9.3"
|
|
58
|
-
},
|
|
59
45
|
"devDependencies": {
|
|
60
|
-
"@babel/core": "7.
|
|
61
|
-
"@babel/plugin-proposal-class-properties": "7.
|
|
62
|
-
"@babel/
|
|
63
|
-
"@babel/preset-
|
|
64
|
-
"@babel/preset-
|
|
65
|
-
"@
|
|
66
|
-
"@testing-library/
|
|
67
|
-
"@testing-library/
|
|
68
|
-
"@
|
|
69
|
-
"@
|
|
70
|
-
"@types/
|
|
71
|
-
"
|
|
72
|
-
"
|
|
73
|
-
"
|
|
74
|
-
"
|
|
75
|
-
"
|
|
76
|
-
"
|
|
77
|
-
"
|
|
46
|
+
"@babel/core": "7.18.9",
|
|
47
|
+
"@babel/plugin-proposal-class-properties": "7.18.6",
|
|
48
|
+
"@babel/plugin-transform-react-jsx": "7.18.6",
|
|
49
|
+
"@babel/preset-env": "7.18.9",
|
|
50
|
+
"@babel/preset-react": "7.18.6",
|
|
51
|
+
"@babel/preset-typescript": "7.18.6",
|
|
52
|
+
"@testing-library/jest-dom": "5.16.4",
|
|
53
|
+
"@testing-library/react": "13.3.0",
|
|
54
|
+
"@testing-library/react-hooks": "8.0.1",
|
|
55
|
+
"@testing-library/user-event": "14.3.0",
|
|
56
|
+
"@types/jest": "28.1.6",
|
|
57
|
+
"@types/lodash": "^4.14.182",
|
|
58
|
+
"@types/react": "18.0.15",
|
|
59
|
+
"@types/react-dom": "18.0.6",
|
|
60
|
+
"esbuild": "0.14.49",
|
|
61
|
+
"eslint-plugin-prettier": "4.2.1",
|
|
62
|
+
"jest": "28.1.3",
|
|
63
|
+
"jest-environment-jsdom": "28.1.3",
|
|
64
|
+
"prettier": "2.7.1",
|
|
65
|
+
"react": "18.2.0",
|
|
66
|
+
"react-dom": "18.2.0",
|
|
67
|
+
"react-test-renderer": "18.2.0",
|
|
78
68
|
"tslib": "2.4.0",
|
|
79
|
-
"typescript": "4.
|
|
69
|
+
"typescript": "4.7.4"
|
|
80
70
|
},
|
|
81
71
|
"peerDependencies": {
|
|
72
|
+
"lodash": ">=4.17.21",
|
|
82
73
|
"react": ">=16.8.1",
|
|
83
74
|
"react-dom": ">=16.8.1"
|
|
84
75
|
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { createContext, ReactNode, useContext } from 'react'
|
|
2
|
+
import { Hotkey } from './types'
|
|
3
|
+
|
|
4
|
+
type BoundHotkeysProxyProviderType = {
|
|
5
|
+
addHotkey: (hotkey: Hotkey) => void,
|
|
6
|
+
removeHotkey: (hotkey: Hotkey) => void,
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const BoundHotkeysProxyProvider = createContext<BoundHotkeysProxyProviderType | undefined>(undefined)
|
|
10
|
+
|
|
11
|
+
export const useBoundHotkeysProxy = () => {
|
|
12
|
+
return useContext(BoundHotkeysProxyProvider)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface Props {
|
|
16
|
+
children: ReactNode
|
|
17
|
+
addHotkey: (hotkey: Hotkey) => void
|
|
18
|
+
removeHotkey: (hotkey: Hotkey) => void
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export default function BoundHotkeysProxyProviderProvider({ addHotkey, removeHotkey, children }: Props) {
|
|
22
|
+
return <BoundHotkeysProxyProvider.Provider value={{addHotkey, removeHotkey}}>{children}</BoundHotkeysProxyProvider.Provider>
|
|
23
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { Hotkey } from './types'
|
|
2
|
+
import { createContext, ReactNode, useMemo, useState, useContext } from 'react'
|
|
3
|
+
import BoundHotkeysProxyProviderProvider from './BoundHotkeysProxyProvider'
|
|
4
|
+
|
|
5
|
+
type HotkeysContextType = {
|
|
6
|
+
hotkeys: ReadonlyArray<Hotkey>
|
|
7
|
+
activeScopes: string[]
|
|
8
|
+
toggleScope: (scope: string) => void
|
|
9
|
+
activateScope: (scope: string) => void
|
|
10
|
+
deactivateScope: (scope: string) => void
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// The context is only needed for special features like global scoping, so we use a graceful default fallback
|
|
14
|
+
const HotkeysContext = createContext<HotkeysContextType>({
|
|
15
|
+
hotkeys: [],
|
|
16
|
+
activeScopes: [], // This array has to be empty to check if the provider is set or not
|
|
17
|
+
toggleScope: () => {},
|
|
18
|
+
activateScope: () => {},
|
|
19
|
+
deactivateScope: () => {},
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
export const useHotkeysContext = () => {
|
|
23
|
+
return useContext(HotkeysContext)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface Props {
|
|
27
|
+
initiallyActiveScopes?: string[]
|
|
28
|
+
children: ReactNode
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export const HotkeysProvider = ({initiallyActiveScopes = ['*'], children}: Props) => {
|
|
32
|
+
const [internalActiveScopes, setInternalActiveScopes] = useState(initiallyActiveScopes?.length > 0 ? initiallyActiveScopes : ['*'])
|
|
33
|
+
const [boundHotkeys, setBoundHotkeys] = useState<Hotkey[]>([]);
|
|
34
|
+
|
|
35
|
+
const isAllActive = useMemo(() => internalActiveScopes.includes('*'), [internalActiveScopes])
|
|
36
|
+
|
|
37
|
+
const activateScope = (scope: string) => {
|
|
38
|
+
if (isAllActive) {
|
|
39
|
+
setInternalActiveScopes([scope])
|
|
40
|
+
} else {
|
|
41
|
+
setInternalActiveScopes(Array.from(new Set([...internalActiveScopes, scope])))
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const deactivateScope = (scope: string) => {
|
|
46
|
+
const scopes = internalActiveScopes.filter(s => s !== scope)
|
|
47
|
+
|
|
48
|
+
if (scopes.length === 0) {
|
|
49
|
+
setInternalActiveScopes(['*'])
|
|
50
|
+
} else {
|
|
51
|
+
setInternalActiveScopes(scopes)
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const toggleScope = (scope: string) => {
|
|
56
|
+
if (internalActiveScopes.includes(scope)) {
|
|
57
|
+
deactivateScope(scope)
|
|
58
|
+
} else {
|
|
59
|
+
activateScope(scope)
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const addBoundHotkey = (hotkey: Hotkey) => {
|
|
64
|
+
setBoundHotkeys([...boundHotkeys, hotkey])
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const removeBoundHotkey = (hotkey: Hotkey) => {
|
|
68
|
+
setBoundHotkeys(boundHotkeys.filter(h => h.keys !== hotkey.keys))
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return (
|
|
72
|
+
<HotkeysContext.Provider value={{activeScopes: internalActiveScopes, hotkeys: boundHotkeys, activateScope, deactivateScope, toggleScope}}>
|
|
73
|
+
<BoundHotkeysProxyProviderProvider addHotkey={addBoundHotkey} removeHotkey={removeBoundHotkey}>
|
|
74
|
+
{children}
|
|
75
|
+
</BoundHotkeysProxyProviderProvider>
|
|
76
|
+
</HotkeysContext.Provider>
|
|
77
|
+
)
|
|
78
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
3
|
-
import
|
|
1
|
+
import useHotkeys from './useHotkeys'
|
|
2
|
+
import type { Options } from './types'
|
|
3
|
+
import { HotkeysProvider, useHotkeysContext } from './HotkeysProvider'
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
export {
|
|
6
|
+
useHotkeys,
|
|
7
|
+
useHotkeysContext,
|
|
8
|
+
HotkeysProvider,
|
|
9
|
+
Options,
|
|
10
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { Hotkey } from './types'
|
|
2
|
+
import { parseHotkey } from './parseHotkeys'
|
|
3
|
+
import isEqual from 'lodash/isEqual'
|
|
4
|
+
|
|
5
|
+
const currentlyPressedKeys: Set<Hotkey> = new Set<Hotkey>()
|
|
6
|
+
|
|
7
|
+
export function isHotkeyPressed(key: string | string[], splitKey: string = ','): boolean {
|
|
8
|
+
const hotkeyArray = Array.isArray(key) ? key : key.split(splitKey)
|
|
9
|
+
|
|
10
|
+
return hotkeyArray.every((hotkey) => {
|
|
11
|
+
const parsedHotkey = parseHotkey(hotkey)
|
|
12
|
+
|
|
13
|
+
for (const pressedHotkey of currentlyPressedKeys) {
|
|
14
|
+
if (isEqual(parsedHotkey, pressedHotkey)) {
|
|
15
|
+
return true
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
})
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function pushToCurrentlyPressedKeys(key: string | string[]): void {
|
|
22
|
+
const hotkeyArray = Array.isArray(key) ? key : [key]
|
|
23
|
+
|
|
24
|
+
hotkeyArray.forEach(hotkey => currentlyPressedKeys.add(parseHotkey(hotkey)))
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function removeFromCurrentlyPressedKeys(key: string | string[]): void {
|
|
28
|
+
const hotkeyArray = Array.isArray(key) ? key : [key]
|
|
29
|
+
|
|
30
|
+
hotkeyArray.forEach((hotkey) => {
|
|
31
|
+
const parsedHotkey = parseHotkey(hotkey)
|
|
32
|
+
|
|
33
|
+
for (const pressedHotkey of currentlyPressedKeys) {
|
|
34
|
+
if (pressedHotkey.keys?.every((key) => parsedHotkey.keys?.includes(key))) {
|
|
35
|
+
currentlyPressedKeys.delete(pressedHotkey)
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
})
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
(() => {
|
|
42
|
+
window.addEventListener('DOMContentLoaded', () => {
|
|
43
|
+
document.addEventListener('keydown', e => {
|
|
44
|
+
pushToCurrentlyPressedKeys(e.key)
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
document.addEventListener('keyup', e => {
|
|
48
|
+
removeFromCurrentlyPressedKeys(e.key)
|
|
49
|
+
})
|
|
50
|
+
})
|
|
51
|
+
})()
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { Hotkey, KeyboardModifiers, Keys } from './types'
|
|
2
|
+
|
|
3
|
+
const reservedModifierKeywords = ['ctrl', 'shift', 'alt', 'meta', 'mod']
|
|
4
|
+
|
|
5
|
+
export function parseKeysHookInput(keys: Keys, splitKey: string = ','): string[] {
|
|
6
|
+
if (typeof keys === 'string') {
|
|
7
|
+
return keys.split(splitKey)
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
return keys
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function parseHotkey(hotkey: string, combinationKey: string = '+'): Hotkey {
|
|
14
|
+
const keys = hotkey
|
|
15
|
+
.toLocaleLowerCase()
|
|
16
|
+
.split(combinationKey)
|
|
17
|
+
.map((k) => k.trim())
|
|
18
|
+
|
|
19
|
+
const modifiers: KeyboardModifiers = {
|
|
20
|
+
alt: keys.includes('alt'),
|
|
21
|
+
ctrl: keys.includes('ctrl'),
|
|
22
|
+
shift: keys.includes('shift'),
|
|
23
|
+
meta: keys.includes('meta'),
|
|
24
|
+
mod: keys.includes('mod'),
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const singleCharKeys = keys.filter((k) => !reservedModifierKeywords.includes(k))
|
|
28
|
+
|
|
29
|
+
return {
|
|
30
|
+
...modifiers,
|
|
31
|
+
keys: singleCharKeys,
|
|
32
|
+
}
|
|
33
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { DependencyList } from 'react'
|
|
2
|
+
|
|
3
|
+
export type FormTags = 'input' | 'textarea' | 'select' | 'INPUT' | 'TEXTAREA' | 'SELECT'
|
|
4
|
+
export type Keys = string | string[]
|
|
5
|
+
export type Scopes = string | string[]
|
|
6
|
+
|
|
7
|
+
export type RefType<T> = T | null
|
|
8
|
+
|
|
9
|
+
export type KeyboardModifiers = {
|
|
10
|
+
alt?: boolean
|
|
11
|
+
ctrl?: boolean
|
|
12
|
+
meta?: boolean
|
|
13
|
+
shift?: boolean
|
|
14
|
+
mod?: boolean
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export type Hotkey = KeyboardModifiers & {
|
|
18
|
+
keys?: string[]
|
|
19
|
+
scopes?: Scopes
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export type HotkeysEvent = Hotkey & {}
|
|
23
|
+
|
|
24
|
+
export type HotkeyCallback = (keyboardEvent: KeyboardEvent, hotkeysEvent: HotkeysEvent) => void
|
|
25
|
+
|
|
26
|
+
export type Trigger = boolean | ((keyboardEvent: KeyboardEvent, hotkeysEvent: HotkeysEvent) => boolean)
|
|
27
|
+
|
|
28
|
+
export type Options = {
|
|
29
|
+
enabled?: Trigger // Main setting that determines if the hotkey is enabled or not. (Default: true)
|
|
30
|
+
enableOnFormTags?: FormTags[] | boolean // Enable hotkeys on a list of tags. (Default: false)
|
|
31
|
+
enableOnContentEditable?: boolean // Enable hotkeys on tags with contentEditable props. (Default: false)
|
|
32
|
+
combinationKey?: string // Character to split keys in hotkeys combinations. (Default: +)
|
|
33
|
+
splitKey?: string // Character to separate different hotkeys. (Default: ,)
|
|
34
|
+
scopes?: Scopes // Scope
|
|
35
|
+
keyup?: boolean // Trigger on keyup event? (Default: undefined)
|
|
36
|
+
keydown?: boolean // Trigger on keydown event? (Default: true)
|
|
37
|
+
preventDefault?: Trigger // Prevent default browser behavior? (Default: true)
|
|
38
|
+
description?: string // Use this option to describe what the hotkey does. (Default: undefined)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export type OptionsOrDependencyArray = Options | DependencyList
|
package/src/useHotkeys.ts
CHANGED
|
@@ -1,91 +1,103 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
if (options instanceof Array) {
|
|
36
|
-
deps = options;
|
|
37
|
-
options = undefined;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
const {
|
|
41
|
-
enableOnTags,
|
|
42
|
-
filter,
|
|
43
|
-
keyup,
|
|
44
|
-
keydown,
|
|
45
|
-
filterPreventDefault = true,
|
|
46
|
-
enabled = true,
|
|
47
|
-
enableOnContentEditable = false,
|
|
48
|
-
} = options as Options || {};
|
|
49
|
-
const ref = useRef<T | null>(null);
|
|
50
|
-
|
|
51
|
-
// The return value of this callback determines if the browsers default behavior is prevented.
|
|
52
|
-
const memoisedCallback = useCallback((keyboardEvent: KeyboardEvent, hotkeysEvent: HotkeysEvent) => {
|
|
53
|
-
if (filter && !filter(keyboardEvent)) {
|
|
54
|
-
return !filterPreventDefault;
|
|
1
|
+
import { HotkeyCallback, Keys, OptionsOrDependencyArray, RefType } from './types'
|
|
2
|
+
import { useCallback, useLayoutEffect, useRef } from 'react'
|
|
3
|
+
import { parseHotkey, parseKeysHookInput } from './parseHotkeys'
|
|
4
|
+
import {
|
|
5
|
+
isHotkeyEnabled,
|
|
6
|
+
isHotkeyEnabledOnTag,
|
|
7
|
+
isHotkeyMatchingKeyboardEvent,
|
|
8
|
+
isKeyboardEventTriggeredByInput,
|
|
9
|
+
isScopeActive,
|
|
10
|
+
maybePreventDefault,
|
|
11
|
+
} from './validators'
|
|
12
|
+
import { useHotkeysContext } from './HotkeysProvider'
|
|
13
|
+
import { useBoundHotkeysProxy } from './BoundHotkeysProxyProvider'
|
|
14
|
+
|
|
15
|
+
export default function useHotkeys<T extends HTMLElement>(
|
|
16
|
+
keys: Keys,
|
|
17
|
+
callback: HotkeyCallback,
|
|
18
|
+
options?: OptionsOrDependencyArray,
|
|
19
|
+
dependencies?: OptionsOrDependencyArray,
|
|
20
|
+
) {
|
|
21
|
+
const ref = useRef<RefType<T>>(null)
|
|
22
|
+
const { current: pressedDownKeys } = useRef<Set<string>>(new Set())
|
|
23
|
+
|
|
24
|
+
const _options = !(options instanceof Array) ? options : !(dependencies instanceof Array) ? dependencies : undefined
|
|
25
|
+
const _deps = options instanceof Array ? options : dependencies instanceof Array ? dependencies : []
|
|
26
|
+
|
|
27
|
+
const cb = useCallback(callback, [..._deps])
|
|
28
|
+
const ctx = useHotkeysContext()
|
|
29
|
+
|
|
30
|
+
const proxy = useBoundHotkeysProxy()
|
|
31
|
+
|
|
32
|
+
useLayoutEffect(() => {
|
|
33
|
+
if (_options?.enabled === false || !isScopeActive(ctx.activeScopes, _options?.scopes)) {
|
|
34
|
+
return
|
|
55
35
|
}
|
|
56
36
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
37
|
+
const listener = (e: KeyboardEvent) => {
|
|
38
|
+
if (isKeyboardEventTriggeredByInput(e) && !isHotkeyEnabledOnTag(e, _options?.enableOnFormTags)) {
|
|
39
|
+
return
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (ref.current !== null && document.activeElement !== ref.current && !ref.current.contains(document.activeElement)) {
|
|
43
|
+
return
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (((e.target as HTMLElement)?.isContentEditable && !_options?.enableOnContentEditable)) {
|
|
47
|
+
return
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
parseKeysHookInput(keys, _options?.splitKey).forEach((key) => {
|
|
51
|
+
const hotkey = parseHotkey(key, _options?.combinationKey)
|
|
52
|
+
|
|
53
|
+
if (isHotkeyMatchingKeyboardEvent(e, hotkey, pressedDownKeys) || hotkey.keys?.includes('*')) {
|
|
54
|
+
maybePreventDefault(e, hotkey, _options?.preventDefault)
|
|
64
55
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
56
|
+
if (!isHotkeyEnabled(e, hotkey, _options?.enabled)) {
|
|
57
|
+
return
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
cb(e, hotkey)
|
|
61
|
+
}
|
|
62
|
+
})
|
|
68
63
|
}
|
|
69
64
|
|
|
70
|
-
|
|
71
|
-
|
|
65
|
+
const handleKeyDown = (event: KeyboardEvent) => {
|
|
66
|
+
pressedDownKeys.add(event.key.toLowerCase())
|
|
67
|
+
|
|
68
|
+
if ((_options?.keydown === undefined && _options?.keyup !== true) || _options?.keydown) {
|
|
69
|
+
listener(event)
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
72
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
hotkeys.unbind(keys, memoisedCallback);
|
|
73
|
+
const handleKeyUp = (event: KeyboardEvent) => {
|
|
74
|
+
pressedDownKeys.delete(event.key.toLowerCase())
|
|
76
75
|
|
|
77
|
-
|
|
76
|
+
if (_options?.keyup) {
|
|
77
|
+
listener(event)
|
|
78
|
+
}
|
|
78
79
|
}
|
|
79
80
|
|
|
80
|
-
//
|
|
81
|
-
|
|
82
|
-
|
|
81
|
+
// @ts-ignore
|
|
82
|
+
(ref.current || document).addEventListener('keyup', handleKeyUp);
|
|
83
|
+
// @ts-ignore
|
|
84
|
+
(ref.current || document).addEventListener('keydown', handleKeyDown)
|
|
85
|
+
|
|
86
|
+
if (proxy) {
|
|
87
|
+
parseKeysHookInput(keys, _options?.splitKey).forEach((key) => proxy.addHotkey(parseHotkey(key, _options?.combinationKey)))
|
|
83
88
|
}
|
|
84
89
|
|
|
85
|
-
|
|
90
|
+
return () => {
|
|
91
|
+
// @ts-ignore
|
|
92
|
+
(ref.current || document).removeEventListener('keyup', handleKeyUp);
|
|
93
|
+
// @ts-ignore
|
|
94
|
+
(ref.current || document).removeEventListener('keydown', handleKeyDown)
|
|
86
95
|
|
|
87
|
-
|
|
88
|
-
|
|
96
|
+
if (proxy) {
|
|
97
|
+
parseKeysHookInput(keys, _options?.splitKey).forEach((key) => proxy.removeHotkey(parseHotkey(key, _options?.combinationKey)))
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}, [keys, cb, _options])
|
|
89
101
|
|
|
90
|
-
return ref
|
|
102
|
+
return ref
|
|
91
103
|
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { FormTags, Hotkey, Scopes, Trigger } from './types'
|
|
2
|
+
|
|
3
|
+
export function maybePreventDefault(e: KeyboardEvent, hotkey: Hotkey, preventDefault?: Trigger): void {
|
|
4
|
+
if ((typeof preventDefault === 'function' && preventDefault(e, hotkey)) || preventDefault === true) {
|
|
5
|
+
e.preventDefault()
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function isHotkeyEnabled(e: KeyboardEvent, hotkey: Hotkey, enabled?: Trigger): boolean {
|
|
10
|
+
if (typeof enabled === 'function') {
|
|
11
|
+
return enabled(e, hotkey)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
return enabled === true || enabled === undefined
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function isKeyboardEventTriggeredByInput(ev: KeyboardEvent): boolean {
|
|
18
|
+
return isHotkeyEnabledOnTag(ev, ['input', 'textarea', 'select'])
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function isHotkeyEnabledOnTag({ target }: KeyboardEvent, enabledOnTags: FormTags[] | boolean = false): boolean {
|
|
22
|
+
const targetTagName = target && (target as HTMLElement).tagName
|
|
23
|
+
|
|
24
|
+
if (enabledOnTags instanceof Array) {
|
|
25
|
+
return Boolean(targetTagName && enabledOnTags && enabledOnTags.some(tag => tag.toLowerCase() === targetTagName.toLowerCase()))
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return Boolean(targetTagName && enabledOnTags && enabledOnTags === true)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function isScopeActive(activeScopes: string[], scopes?: Scopes): boolean {
|
|
32
|
+
if (activeScopes.length === 0 && scopes) {
|
|
33
|
+
console.warn(
|
|
34
|
+
'A hotkey has the "scopes" option set, however no active scopes were found. If you want to use the global scopes feature, you need to wrap your app in a <HotkeysProvider>'
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
return true
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (!scopes) {
|
|
41
|
+
return true
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return activeScopes.some(scope => scopes.includes(scope)) || activeScopes.includes('*')
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export const isHotkeyMatchingKeyboardEvent = (e: KeyboardEvent, hotkey: Hotkey, pressedDownKeys: Set<string>): boolean => {
|
|
48
|
+
const { alt, ctrl, meta, mod, shift, keys } = hotkey
|
|
49
|
+
const { altKey, ctrlKey, metaKey, shiftKey, key: pressedKeyUppercase, code } = e
|
|
50
|
+
|
|
51
|
+
const keyCode = code.toLowerCase().replace('key', '')
|
|
52
|
+
const pressedKey = pressedKeyUppercase.toLowerCase()
|
|
53
|
+
|
|
54
|
+
if (altKey !== alt && pressedKey !== 'alt') {
|
|
55
|
+
return false
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (shiftKey !== shift && pressedKey !== 'shift') {
|
|
59
|
+
return false
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Mod is a special key name that is checking for meta on macOS and ctrl on other platforms
|
|
63
|
+
if (mod) {
|
|
64
|
+
if (!metaKey && !ctrlKey) {
|
|
65
|
+
return false
|
|
66
|
+
}
|
|
67
|
+
} else {
|
|
68
|
+
if (metaKey !== meta && keyCode !== 'meta') {
|
|
69
|
+
return false
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (ctrlKey !== ctrl && keyCode !== 'ctrl') {
|
|
73
|
+
return false
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// All modifiers are correct, now check the key
|
|
78
|
+
// If the key is set we check for the key
|
|
79
|
+
if (keys && keys.length === 1 && (keys.includes(pressedKey) || keys.includes(keyCode))) {
|
|
80
|
+
return true
|
|
81
|
+
} else if (keys) {
|
|
82
|
+
// Check if all keys are present in pressedDownKeys set
|
|
83
|
+
return keys.every(key => pressedDownKeys.has(key))
|
|
84
|
+
}
|
|
85
|
+
else if (!keys) {
|
|
86
|
+
// If the key is not set, we only listen for modifiers, that check went alright, so we return true
|
|
87
|
+
return true
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// There is nothing that matches.
|
|
91
|
+
return false
|
|
92
|
+
}
|
package/src/index.test.tsx
DELETED
|
@@ -1,230 +0,0 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
|
-
import { useHotkeys } from './index';
|
|
3
|
-
import { renderHook } from '@testing-library/react-hooks';
|
|
4
|
-
import { fireEvent, render, screen } from '@testing-library/react';
|
|
5
|
-
import userEvent from '@testing-library/user-event';
|
|
6
|
-
import hotkeys from 'hotkeys-js';
|
|
7
|
-
|
|
8
|
-
const HotkeysOnInput = ({ onPress, useTags }: { onPress: () => void, useTags?: boolean }) => {
|
|
9
|
-
useHotkeys('a', onPress, { enableOnTags: useTags ? ['INPUT'] : undefined });
|
|
10
|
-
|
|
11
|
-
return (
|
|
12
|
-
<input type='text' data-testid={'input'} />
|
|
13
|
-
);
|
|
14
|
-
};
|
|
15
|
-
|
|
16
|
-
const HotkeysFilteredOnInput = ({ onPress, useTags }: { onPress: () => void, useTags?: boolean }) => {
|
|
17
|
-
useHotkeys('a', onPress, { enableOnTags: useTags ? ['TEXTAREA'] : undefined });
|
|
18
|
-
|
|
19
|
-
return (
|
|
20
|
-
<input type='text' data-testid={'input'} />
|
|
21
|
-
);
|
|
22
|
-
};
|
|
23
|
-
|
|
24
|
-
const HotkeysOnKeyup = ({ onPress, keyup, keydown }: { onPress: () => void, keyup?: boolean, keydown?: boolean }) => {
|
|
25
|
-
useHotkeys('a', onPress, { keyup, keydown });
|
|
26
|
-
|
|
27
|
-
return (
|
|
28
|
-
<input type='text' data-testid={'input'} />
|
|
29
|
-
);
|
|
30
|
-
};
|
|
31
|
-
|
|
32
|
-
const HotkeysWithRef = ({ onPress }: { onPress: () => void }) => {
|
|
33
|
-
const ref = useHotkeys<HTMLElement>('a', onPress);
|
|
34
|
-
|
|
35
|
-
return (
|
|
36
|
-
<section ref={ref} tabIndex={0} data-testid={'container'}>
|
|
37
|
-
<input type='text' data-testid={'input'} />
|
|
38
|
-
</section>
|
|
39
|
-
);
|
|
40
|
-
};
|
|
41
|
-
|
|
42
|
-
test('useHotkeys should only fire when element is focused if a ref is set.', () => {
|
|
43
|
-
const onPress = jest.fn();
|
|
44
|
-
|
|
45
|
-
render(<HotkeysWithRef onPress={onPress} />);
|
|
46
|
-
|
|
47
|
-
userEvent.keyboard('A');
|
|
48
|
-
|
|
49
|
-
expect(onPress).not.toBeCalled();
|
|
50
|
-
|
|
51
|
-
userEvent.click(screen.getByTestId('container'));
|
|
52
|
-
userEvent.keyboard('A');
|
|
53
|
-
|
|
54
|
-
expect(onPress).toBeCalled();
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
test('useHotkeys should listen to key presses', () => {
|
|
58
|
-
const callback = jest.fn();
|
|
59
|
-
|
|
60
|
-
renderHook(() => useHotkeys('a', callback));
|
|
61
|
-
|
|
62
|
-
userEvent.keyboard('A');
|
|
63
|
-
|
|
64
|
-
expect(callback).toHaveBeenCalledTimes(1);
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
test('useHotkeys correctly assign deps when used as third argument and options being omitted', async () => {
|
|
68
|
-
let count = 0;
|
|
69
|
-
const callback = jest.fn();
|
|
70
|
-
|
|
71
|
-
renderHook(() => useHotkeys('a', () => callback(++count), [count]));
|
|
72
|
-
|
|
73
|
-
userEvent.keyboard('A');
|
|
74
|
-
|
|
75
|
-
expect(callback).toHaveBeenCalledTimes(1);
|
|
76
|
-
expect(callback.mock.calls[0][0]).toEqual(1);
|
|
77
|
-
|
|
78
|
-
userEvent.keyboard('A');
|
|
79
|
-
|
|
80
|
-
expect(callback).toHaveBeenCalledTimes(2);
|
|
81
|
-
expect(callback.mock.calls[1][0]).toEqual(2);
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
test('useHotkeys should use correct char to split combinations', () => {
|
|
85
|
-
const callback = jest.fn();
|
|
86
|
-
|
|
87
|
-
renderHook(() => useHotkeys('Shift-A', callback, { splitKey: '-' }));
|
|
88
|
-
|
|
89
|
-
userEvent.keyboard('{Shift>}A{/Shift}');
|
|
90
|
-
|
|
91
|
-
expect(callback).toHaveBeenCalledTimes(1);
|
|
92
|
-
|
|
93
|
-
userEvent.keyboard('{Shift>}A{/Shift}');
|
|
94
|
-
|
|
95
|
-
expect(callback).toHaveBeenCalledTimes(2);
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
test('useHotkeys should use correctly assign options and deps argument when using all four arguments', () => {
|
|
99
|
-
const callback = jest.fn();
|
|
100
|
-
|
|
101
|
-
renderHook(() => useHotkeys('shift-a', callback, { splitKey: '-' }, []));
|
|
102
|
-
|
|
103
|
-
userEvent.keyboard('{Shift>}A{/Shift}');
|
|
104
|
-
|
|
105
|
-
expect(callback).toHaveBeenCalledTimes(1);
|
|
106
|
-
|
|
107
|
-
userEvent.keyboard('{Shift>}A{/Shift}');
|
|
108
|
-
|
|
109
|
-
expect(callback).toHaveBeenCalledTimes(2);
|
|
110
|
-
|
|
111
|
-
userEvent.keyboard('{Shift>}A{/Shift}');
|
|
112
|
-
|
|
113
|
-
expect(callback).toHaveBeenCalledTimes(3);
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
test('useHotkeys should only trigger once if neither keyup nor keydown are set', () => {
|
|
117
|
-
const onPress = jest.fn();
|
|
118
|
-
|
|
119
|
-
render(<HotkeysOnKeyup onPress={onPress} />);
|
|
120
|
-
|
|
121
|
-
fireEvent.keyUp(document.body, { key: 'a', keyCode: 65 });
|
|
122
|
-
|
|
123
|
-
expect(onPress).not.toHaveBeenCalled();
|
|
124
|
-
|
|
125
|
-
fireEvent.keyDown(document.body, { key: 'a', keyCode: 65 });
|
|
126
|
-
|
|
127
|
-
expect(onPress).toHaveBeenCalled();
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
test('useHotkeys should only trigger once if keyup is set and keydown is not', () => {
|
|
131
|
-
const onPress = jest.fn();
|
|
132
|
-
|
|
133
|
-
render(<HotkeysOnKeyup onPress={onPress} keyup={true} />);
|
|
134
|
-
|
|
135
|
-
fireEvent.keyDown(document.body, { key: 'a', keyCode: 65 });
|
|
136
|
-
|
|
137
|
-
expect(onPress).not.toHaveBeenCalled();
|
|
138
|
-
|
|
139
|
-
fireEvent.keyUp(document.body, { key: 'a', keyCode: 65 });
|
|
140
|
-
|
|
141
|
-
expect(onPress).toHaveBeenCalled();
|
|
142
|
-
});
|
|
143
|
-
|
|
144
|
-
test('useHotkeys should trigger twice if keyup and keydown is set to true', () => {
|
|
145
|
-
let called = false;
|
|
146
|
-
|
|
147
|
-
render(<HotkeysOnKeyup onPress={() => called = true} keyup={true} keydown={true} />);
|
|
148
|
-
|
|
149
|
-
userEvent.keyboard('A');
|
|
150
|
-
|
|
151
|
-
expect(called).toBe(true);
|
|
152
|
-
|
|
153
|
-
userEvent.keyboard('A');
|
|
154
|
-
|
|
155
|
-
expect(called).toBe(true);
|
|
156
|
-
});
|
|
157
|
-
|
|
158
|
-
test('useHotkeys should be enabled on given form tags', async () => {
|
|
159
|
-
const onPress = jest.fn();
|
|
160
|
-
render(<HotkeysOnInput onPress={onPress} useTags={true} />);
|
|
161
|
-
|
|
162
|
-
const input = document.querySelector('input');
|
|
163
|
-
|
|
164
|
-
expect(input).not.toBe(null);
|
|
165
|
-
|
|
166
|
-
userEvent.keyboard('A');
|
|
167
|
-
|
|
168
|
-
expect(onPress).toHaveBeenCalled();
|
|
169
|
-
});
|
|
170
|
-
|
|
171
|
-
test('useHotkeys should not be enabled on given form tags when filter specifies different input field', async () => {
|
|
172
|
-
const onPress = jest.fn();
|
|
173
|
-
render(<HotkeysFilteredOnInput onPress={onPress} useTags={true} />);
|
|
174
|
-
|
|
175
|
-
userEvent.type(screen.getByRole('textbox'), 'A');
|
|
176
|
-
|
|
177
|
-
expect(onPress).toHaveBeenCalledTimes(0);
|
|
178
|
-
});
|
|
179
|
-
|
|
180
|
-
test('useHotkeys should not be enabled on given form tags when tags is not set', async () => {
|
|
181
|
-
const onPress = jest.fn();
|
|
182
|
-
render(<HotkeysFilteredOnInput onPress={onPress} useTags={false} />);
|
|
183
|
-
|
|
184
|
-
userEvent.type(screen.getByRole('textbox'), 'A');
|
|
185
|
-
|
|
186
|
-
expect(onPress).toHaveBeenCalledTimes(0);
|
|
187
|
-
});
|
|
188
|
-
|
|
189
|
-
test('useHotkeys should use its own custom filter system instead of the global hotkeys one', () => {
|
|
190
|
-
const callback = jest.fn();
|
|
191
|
-
const { rerender } = renderHook((returnFilterVal: boolean = false) => useHotkeys('a', callback, { filter: () => returnFilterVal }));
|
|
192
|
-
|
|
193
|
-
userEvent.keyboard('A');
|
|
194
|
-
|
|
195
|
-
expect(callback).not.toHaveBeenCalled();
|
|
196
|
-
|
|
197
|
-
rerender(true);
|
|
198
|
-
|
|
199
|
-
userEvent.keyboard('A');
|
|
200
|
-
|
|
201
|
-
expect(callback).toHaveBeenCalledTimes(1);
|
|
202
|
-
});
|
|
203
|
-
|
|
204
|
-
test('useHotkeys should not be enabled when enabled flag is set to false', () => {
|
|
205
|
-
const callback = jest.fn();
|
|
206
|
-
|
|
207
|
-
const { rerender } = renderHook((enabled: boolean = false) => useHotkeys('a', callback, { enabled }));
|
|
208
|
-
|
|
209
|
-
userEvent.keyboard('A');
|
|
210
|
-
|
|
211
|
-
expect(callback).not.toHaveBeenCalled();
|
|
212
|
-
|
|
213
|
-
rerender(true);
|
|
214
|
-
|
|
215
|
-
userEvent.keyboard('A');
|
|
216
|
-
|
|
217
|
-
expect(callback).toHaveBeenCalledTimes(1);
|
|
218
|
-
});
|
|
219
|
-
|
|
220
|
-
test('useHotkeys should unbind the hotkey when enabled is set from true to false', () => {
|
|
221
|
-
hotkeys.unbind = jest.fn();
|
|
222
|
-
|
|
223
|
-
const { rerender } = renderHook((enabled: boolean = true) => useHotkeys('a', () => true, { enabled }));
|
|
224
|
-
|
|
225
|
-
expect(hotkeys.unbind).not.toHaveBeenCalled()
|
|
226
|
-
|
|
227
|
-
rerender(false);
|
|
228
|
-
|
|
229
|
-
expect(hotkeys.unbind).toHaveBeenCalledTimes(2)
|
|
230
|
-
})
|