Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

use-keyboard-shortcut

Package Overview
Dependencies
Maintainers
1
Versions
16
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

use-keyboard-shortcut - npm Package Compare versions

Comparing version 1.0.6 to 1.1.0

.github/workflows/npm-publish.yml

162

lib/useKeyboardShortcut.js

@@ -1,23 +0,20 @@

import { useEffect, useCallback, useReducer } from "react";
import { disabledEventPropagation } from './utils'
import { useEffect, useCallback, useRef, useMemo } from "react";
import {
overrideSystemHandling,
checkHeldKeysRecursive,
uniq_fast
} from "./utils";
const blacklistedTargets = ["INPUT", "TEXTAREA"];
const BLACKLISTED_DOM_TARGETS = ["TEXTAREA", "INPUT"];
const keysReducer = (state, action) => {
switch (action.type) {
case "set-key-down":
const keydownState = { ...state, [action.key]: true };
return keydownState;
case "set-key-up":
const keyUpState = { ...state, [action.key]: false };
return keyUpState;
case "reset-keys":
const resetState = { ...action.data };
return resetState;
default:
return state;
}
const DEFAULT_OPTIONS = {
overrideSystem: false,
ignoreInputFields: true
};
const useKeyboardShortcut = (shortcutKeys, callback, options) => {
const useKeyboardShortcut = (
shortcutKeys,
callback,
options = DEFAULT_OPTIONS
) => {
if (!Array.isArray(shortcutKeys))

@@ -38,69 +35,110 @@ throw new Error(

const { overrideSystem } = options || {}
const initalKeyMapping = shortcutKeys.reduce((currentKeys, key) => {
currentKeys[key.toLowerCase()] = false;
return currentKeys;
}, {});
// Normalizes the shortcut keys a deduplicated array of lowercased keys.
const shortcutArray = useMemo(
() => uniq_fast(shortcutKeys).map((key) => String(key).toLowerCase()),
// While using JSON.stringify() is bad for most larger objects, this shortcut
// array is fine as it's small, according to the answer below.
// https://github.com/facebook/react/issues/14476#issuecomment-471199055
// eslint-disable-next-line react-hooks/exhaustive-deps
[JSON.stringify(shortcutKeys)]
);
// useRef to avoid a constant re-render on keydown and keyup.
const heldKeys = useRef([]);
const [keys, setKeys] = useReducer(keysReducer, initalKeyMapping);
const keydownListener = useCallback(
assignedKey => keydownEvent => {
const loweredKey = assignedKey.toLowerCase();
if (keydownEvent.repeat) return
if (blacklistedTargets.includes(keydownEvent.target.tagName)) return;
if (loweredKey !== keydownEvent.key.toLowerCase()) return;
if (keys[loweredKey] === undefined) return;
(keydownEvent) => {
const loweredKey = String(keydownEvent.key).toLowerCase();
if (!(shortcutArray.indexOf(loweredKey) >= 0)) return;
if (overrideSystem) {
keydownEvent.preventDefault();
disabledEventPropagation(keydownEvent);
if (keydownEvent.repeat) return;
// This needs to be checked as soon as possible to avoid
// all option checks that might prevent default behavior
// of the key press.
//
// I.E If shortcut is "Shift + A", we shouldn't prevent the
// default browser behavior of Select All Text just because
// "A" is being observed for our custom behavior shortcut.
const isHeldKeyCombinationValid = checkHeldKeysRecursive(
loweredKey,
null,
shortcutArray,
heldKeys.current
);
if (!isHeldKeyCombinationValid) {
return;
}
setKeys({ type: "set-key-down", key: loweredKey });
if (
options.ignoreInputFields &&
BLACKLISTED_DOM_TARGETS.indexOf(keydownEvent.target.tagName) >= 0
) {
return;
}
if (options.overrideSystem) {
overrideSystemHandling(keydownEvent);
}
heldKeys.current = [...heldKeys.current, loweredKey];
if (heldKeys.current.length === shortcutArray.length) {
callback(shortcutKeys);
}
return false;
},
[keys, overrideSystem]
// eslint-disable-next-line react-hooks/exhaustive-deps
[shortcutArray, callback, options.overrideSystem, options.ignoreInputFields]
);
const keyupListener = useCallback(
assignedKey => keyupEvent => {
const raisedKey = assignedKey.toLowerCase();
(keyupEvent) => {
const raisedKey = String(keyupEvent.key).toLowerCase();
if (!(shortcutArray.indexOf(raisedKey) >= 0)) return;
if (blacklistedTargets.includes(keyupEvent.target.tagName)) return;
if (keyupEvent.key.toLowerCase() !== raisedKey) return;
if (keys[raisedKey] === undefined) return;
const raisedKeyHeldIndex = heldKeys.current.indexOf(raisedKey);
if (!(raisedKeyHeldIndex >= 0)) return;
if (overrideSystem) {
keyupEvent.preventDefault();
disabledEventPropagation(keyupEvent);
if (
options.ignoreInputFields &&
BLACKLISTED_DOM_TARGETS.indexOf(keyupEvent.target.tagName) >= 0
) {
return;
}
if (options.overrideSystem) {
overrideSystemHandling(keyupEvent);
}
setKeys({ type: "set-key-up", key: raisedKey });
let newHeldKeys = [];
let loopIndex;
for (loopIndex = 0; loopIndex < heldKeys.current.length; ++loopIndex) {
if (loopIndex !== raisedKeyHeldIndex) {
newHeldKeys.push(heldKeys.current[loopIndex]);
}
}
heldKeys.current = newHeldKeys;
return false;
},
[keys, overrideSystem]
[shortcutArray, options.overrideSystem, options.ignoreInputFields]
);
useEffect(() => {
if (!Object.values(keys).filter(value => !value).length) {
callback(keys);
setKeys({ type: "reset-keys", data: initalKeyMapping });
} else {
setKeys({ type: null })
}
}, [callback, keys]);
window.addEventListener("keydown", keydownListener);
window.addEventListener("keyup", keyupListener);
return () => {
window.removeEventListener("keydown", keydownListener);
window.removeEventListener("keyup", keyupListener);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [keydownListener, keyupListener, shortcutArray]);
// Resets the held keys array if the shortcut keys are changed.
useEffect(() => {
shortcutKeys.forEach(k => window.addEventListener("keydown", keydownListener(k)));
return () => shortcutKeys.forEach(k => window.removeEventListener("keydown", keydownListener(k)));
}, []);
useEffect(() => {
shortcutKeys.forEach(k => window.addEventListener("keyup", keyupListener(k)));
return () => shortcutKeys.forEach(k => window.removeEventListener("keyup", keyupListener(k)));
}, []);
heldKeys.current = [];
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [shortcutArray]);
};
export default useKeyboardShortcut;

@@ -1,9 +0,87 @@

export function disabledEventPropagation(e){
if(e){
if(e.stopPropagation){
export const overrideSystemHandling = (e) => {
if (e) {
if (e.preventDefault) e.preventDefault();
if (e.stopPropagation) {
e.stopPropagation();
} else if(window.event){
} else if (window.event) {
window.event.cancelBubble = true;
}
}
}
};
// Function stolen from this Stack Overflow answer:
// https: stackoverflow.com/a/9229821
export const uniq_fast = (a) => {
var seen = {};
var out = [];
var len = a.length;
var j = 0;
for (var i = 0; i < len; i++) {
var item = a[i];
if (seen[item] !== 1) {
seen[item] = 1;
out[j++] = item;
}
}
return out;
};
// The goal for this recursive function is to check to ensure
// that the keys are held down in the correct order of the shortcut.
// I.E if the shortcut array is ["Shift", "E", "A"], this function will ensure
// that "E" is held down before "A", and "Shift" is held down before "E".
export const checkHeldKeysRecursive = (
shortcutKey,
// Tracks the call interation for the recursive function,
// based on the previous index;
shortcutKeyRecursionIndex = 0,
shortcutArray,
heldKeysArray
) => {
const shortcutIndexOfKey = shortcutArray.indexOf(shortcutKey);
const keyPartOfShortCut = shortcutArray.indexOf(shortcutKey) >= 0;
// Early exit if they key isn't even in the shortcut combination.
if (!keyPartOfShortCut) return false;
// While holding down one of the keys, if another is to be let go, the shortcut
// should be void. Shortcut keys must be held down in a specifc order.
// This function is always called before a key is added to held keys on keydown,
// this will ensure that heldKeys only contains the prefixing keys
const comparisonIndex = Math.max(heldKeysArray.length - 1, 0);
if (
heldKeysArray.length &&
heldKeysArray[comparisonIndex] !== shortcutArray[comparisonIndex]
) {
return false;
}
// Early exit for the first held down key in the shortcut,
// except if this is a recursive call
if (shortcutIndexOfKey === 0) {
// If this isn't the first interation of this recursive function, and we're
// recursively calling this function, we should always be checking the
// currently held down keys instead of returning true
if (shortcutKeyRecursionIndex > 0)
return heldKeysArray.indexOf(shortcutKey) >= 0;
return true;
}
const previousShortcutKeyIndex = shortcutIndexOfKey - 1;
const previousShortcutKey = shortcutArray[previousShortcutKeyIndex];
const previousShortcutKeyHeld =
heldKeysArray[previousShortcutKeyIndex] === previousShortcutKey;
// Early exit if the key just before the currently checked shortcut key
// isn't being held down.
if (!previousShortcutKeyHeld) return false;
// Recursively call this function with the previous key as the new shortcut key
// but the index of the current shortcut key.
return checkHeldKeysRecursive(
previousShortcutKey,
shortcutIndexOfKey,
shortcutArray,
heldKeysArray
);
};
{
"name": "use-keyboard-shortcut",
"version": "1.0.6",
"version": "1.1.0",
"description": "A custom React hook for adding keyboard shortcuts to your application",

@@ -8,3 +8,3 @@ "main": "index.js",

"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
"test": "cypress open"
},

@@ -19,2 +19,6 @@ "repository": {

},
"devDependencies": {
"cypress": "^9.1.1",
"wait-on": "^6.0.0"
},
"keywords": [

@@ -33,2 +37,2 @@ "react",

"homepage": "https://github.com/arthurtyukayev/use-keyboard-shortcut#readme"
}
}

@@ -25,11 +25,12 @@ ## useKeyboardShortcut

### Documentation
`useKeyboardShortcut(keysArray, callback)`
```javascript
useKeyboardShortcut(shortcutArray, callback, options)
```
`keysArray` should be an array of `KeyboardEvent.key` strings. A full list of strings can be seen [here](https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key/Key_Values)
| Parameter | Type | Description |
|--------------|-----------|------------|
| `shortcutArray` | `Array` | Array of `KeyboardEvent.key` strings. A full list of strings can be seen [here](https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key/Key_Values) |
| `callback` | `Function` | Function that is called once the keys have been pressed. |
| `options` | `Object` | Object containing some configuration options. [See options section](https://github.com/arthurtyukayev/use-keyboard-shortcut#options) |
`callback` should be a function that is called once the keys have been pressed.
`options` an object containing some configuration options.
### Options

@@ -39,5 +40,8 @@

`overrideSystem` overrides the default browser behavior for that specific keyboard shortcut
| Option | Default | Description |
|--------------|-----------|------------|
| `overrideSystem` | `false` | Overrides the default browser behavior for that specific keyboard shortcut. |
| `ignoreInputFields` | `true` | Allows disabling and disabling the keyboard shortcuts when pressed inside of input fields. |
## Bugs / Problems
[Please create an issue](https://github.com/arthurtyukayev/use-keyboard-shortcut/issues/new).
SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc