@supunlakmal/hooks
Version:
A collection of reusable React hooks
97 lines • 4.15 kB
JavaScript
import { useEffect, useCallback, useRef } from 'react';
const parseCombo = (combo) => {
return new Set(combo
.toLowerCase()
.split('+')
.map((key) => key.trim()));
};
/**
* Detects specific keyboard combinations (shortcuts) being pressed.
*
* @param combo The key combination string (e.g., 'ctrl+s', 'alt+shift+k'). Case-insensitive.
* @param callback The function to call when the combo is detected.
* @param options Optional configuration for the event listener.
*/
export const useKeyCombo = (combo, callback, options = {}) => {
const { target = typeof window !== 'undefined' ? window.document : null, preventDefault = false, stopPropagation = false, event = 'keydown', } = options;
const targetElement = typeof target === 'function' ? target() : target;
const requiredKeys = useRef(parseCombo(combo));
const pressedKeys = useRef(new Set());
const callbackRef = useRef(callback);
useEffect(() => {
callbackRef.current = callback;
}, [callback]);
const handleKeyEvent = useCallback((e) => {
const key = e.key.toLowerCase();
if (event === 'keydown') {
pressedKeys.current.add(key);
// Add modifiers if they are pressed
if (e.ctrlKey)
pressedKeys.current.add('ctrl');
if (e.altKey)
pressedKeys.current.add('alt');
if (e.shiftKey)
pressedKeys.current.add('shift');
if (e.metaKey)
pressedKeys.current.add('meta'); // Handle Cmd key on Mac
let comboMatched = true;
// Check if all required keys are currently pressed
// Use Array.from to convert Set to array to avoid iteration error
Array.from(requiredKeys.current).forEach((reqKey) => {
if (!pressedKeys.current.has(reqKey)) {
comboMatched = false;
}
});
// Ensure *only* the required keys are pressed (stricter check)
if (comboMatched &&
pressedKeys.current.size !== requiredKeys.current.size) {
comboMatched = false;
}
if (comboMatched) {
if (preventDefault)
e.preventDefault();
if (stopPropagation)
e.stopPropagation();
callbackRef.current(e);
// Optional: Reset keys after combo is triggered if it shouldn't repeat while holding
// pressedKeys.current.clear();
}
}
}, [preventDefault, stopPropagation, event]); // Dependencies for the handler
const handleKeyUp = useCallback((e) => {
const key = e.key.toLowerCase();
pressedKeys.current.delete(key);
// Also remove modifiers if they are released
if (key === 'control')
pressedKeys.current.delete('ctrl');
if (key === 'alt')
pressedKeys.current.delete('alt');
if (key === 'shift')
pressedKeys.current.delete('shift');
if (key === 'meta')
pressedKeys.current.delete('meta');
}, []);
useEffect(() => {
if (!targetElement)
return;
// Function to handle attaching listeners
const addListeners = () => {
targetElement.addEventListener(event, handleKeyEvent);
targetElement.addEventListener('keyup', handleKeyUp);
};
// Function to handle removing listeners
const removeListeners = () => {
targetElement.removeEventListener(event, handleKeyEvent);
targetElement.removeEventListener('keyup', handleKeyUp);
};
// Add listeners
addListeners();
// Update required keys if combo changes
requiredKeys.current = parseCombo(combo);
return () => {
removeListeners();
pressedKeys.current.clear(); // Clear keys on unmount
};
}, [targetElement, event, handleKeyEvent, handleKeyUp, combo]); // Re-attach if target, event type, handlers, or combo changes
};
//# sourceMappingURL=useKeyCombo.js.map