Launch Week Day 5: Introducing Reachability for PHP.Learn More
Socket
Book a DemoSign in
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
+32
.github/workflows/npm-publish.yml
# This workflow will run tests using node and then publish a package to GitHub Packages when a release is created
# For more information see: https://help.github.com/actions/language-and-framework-guides/publishing-nodejs-packages
name: Package to NPM
on:
release:
types: [created]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: 16
- name: Cypress.io
uses: cypress-io/github-action@v2.9.7
publish-npm:
needs: test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: 16
registry-url: https://registry.npmjs.org/
- run: npm ci
- run: npm publish
env:
NODE_AUTH_TOKEN: ${{secrets.npm_token}}
# This workflow will do a clean install of node dependencies, cache/restore them, build the source code and run tests across different versions of node
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
name: Testing
on:
push:
branches: [ develop ]
pull_request:
branches: [ develop ]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v2
with:
node-version: ${{ matrix.node-version }}
cache: 'npm'
- run: npm ci
- name: Cypress.io
uses: cypress-io/github-action@v2.9.7
import { checkHeldKeysRecursive } from '../../lib/utils'
describe('checkHeldKeysRecursive', () => {
it('return early with false if passed key is not in shortcut array', () => {
// Function expects all inputs to be lowercase normalized
const shortcutKey = 'a'
const shortcutArray = ['shift', 'k']
// By settings this to null, we make sure to return false before an error is thrown.
const heldKeysArray = null
const isKeyPartOfShortcutCheck = checkHeldKeysRecursive(shortcutKey, null, shortcutArray, heldKeysArray)
expect(isKeyPartOfShortcutCheck).to.equal(false)
})
it('return early with false if held keys array and shortcut array do not match', () => {
// Function expects all inputs to be lowercase normalized
const shortcutKey = 'j'
const shortcutArray = ['shift', 'f', 'j']
const heldKeysArray = ['shift']
expect(checkHeldKeysRecursive(shortcutKey, null, shortcutArray, heldKeysArray)).to.equal(false)
})
it('return early with true if shortcut key matches first index of shortcuy array', () => {
// Function expects all inputs to be lowercase normalized
const shortcutKey = 'f'
const shortcutArray = ['f']
const heldKeysArray = []
expect(checkHeldKeysRecursive(shortcutKey, null, shortcutArray, heldKeysArray)).to.equal(true)
})
it('return false if key previous to shortcut key is not being held down', () => {
// Function expects all inputs to be lowercase normalized
const shortcutKey = 'e'
const shortcutArray = ['d', 'a', 'e']
const heldKeysArray = ['d']
expect(checkHeldKeysRecursive(shortcutKey, null, shortcutArray, heldKeysArray)).to.equal(false)
})
it('return true while recursively checking large shortcut combination', () => {
// Function expects all inputs to be lowercase normalized
const shortcutKey = 'e'
const shortcutArray = ['d', 'a', 'c', 'e']
const heldKeysArray = ['d', 'a', 'c']
expect(checkHeldKeysRecursive(shortcutKey, null, shortcutArray, heldKeysArray)).to.equal(true)
})
})
/// <reference types="cypress" />
// ***********************************************************
// This example plugins/index.js can be used to load plugins
//
// You can change the location of this file or turn off loading
// the plugins file with the 'pluginsFile' configuration option.
//
// You can read more here:
// https://on.cypress.io/plugins-guide
// ***********************************************************
// This function is called when a project is opened or re-opened (e.g. due to
// the project's config changing)
/**
* @type {Cypress.PluginConfig}
*/
// eslint-disable-next-line no-unused-vars
module.exports = (on, config) => {
// `on` is used to hook into various events Cypress emits
// `config` is the resolved Cypress config
}
// ***********************************************
// This example commands.js shows you how to
// create various custom commands and overwrite
// existing commands.
//
// For more comprehensive examples of custom
// commands please read more here:
// https://on.cypress.io/custom-commands
// ***********************************************
//
//
// -- This is a parent command --
// Cypress.Commands.add('login', (email, password) => { ... })
//
//
// -- This is a child command --
// Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... })
//
//
// -- This is a dual command --
// Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... })
//
//
// -- This will overwrite an existing command --
// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... })
// ***********************************************************
// This example support/index.js is processed and
// loaded automatically before your test files.
//
// This is a great place to put global configuration and
// behavior that modifies Cypress.
//
// You can change the location of this file or turn off
// automatically serving support files with the
// 'supportFile' configuration option.
//
// You can read more here:
// https://on.cypress.io/configuration
// ***********************************************************
// Import commands.js using ES2015 syntax:
import './commands'
// Alternatively you can use CommonJS syntax:
// require('./commands')
MIT License
Copyright (c) 2021 Arthur Tyukayev
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+100
-62

@@ -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).

Sorry, the diff of this file is not supported yet