Design ideas
React hooks provide us with useState hooks, so we want to design a useKeyboardShortcut hook, think of it like this:
- Requirements: Provide shortcut keys, shortcut key combinations, and callback functions that are executed when the target key is pressed.
- UseKeyboardShortcut (shortcutString, callback)
- Parse the incoming key and process the string to match the corresponding key
- Write pressed and lifted listeners
- Register the listening event to the Window
The implementation process
Key parsing
Here we need to parse the string passed in, either as a single key :” F “, or as a combination of keys :” CTRL + F”
const usekeyboardShortcut = (command: string, callback: Function) = > {
const shortcutKeys = command.split("+") // Convert the passed symbol to an array
const initKeysMapping = shortcutKeys.reduce((pre, cur) = > {
pre[cur.toLowerCase()] = false;
return pre
},{});
}
Copy the code
After this preprocessing, we turn the incoming key into a map, {key: false}, which is converted into a key-press or not-press key-value pair.
Create the reducer
Here we need reducer to manage the key state
const keysReducer = (state: any, action: any) = > {
switch(action.type) {
case "key-down":
constkeyDownState = { ... state, [action.key]:true };
return keyDownState
case "key-up":
constkeyUpState = { ... state, [action.key]:false };
return keyUpState
case "reset-keys":
constresetState = { ... action.data };return resetState;
default:
return state
}
}
const [keys, setKeys] = useReducer(keysReducer, initKeysMapping);
Copy the code
Press to set the corresponding key to true and release to false. The following will explain why reducer is used to manage instead of state
The function that creates the corresponding key
const keydownListener = assignedKey= > (keydownEvent: any) = > {
const loweredKey = assignedKey.toLowerCase();
keydownEvent.stopPropagation();
keydownEvent.cancelBubble = true;
if (keydownEvent.repeat) return
if (blacklistedTargets.includes(keydownEvent.target.tagName)) return;
if(loweredKey ! == keydownEvent.key.toLowerCase())return;
if (keys[loweredKey] === undefined) return;
setKeys({ type: "key-down".key: loweredKey });
return false;
},
Copy the code
Compare the incoming key to the event. Note that repeate should be checked because pressing the key continuously triggers the keyDown, so repeat can be used to check that it is only executed once
Trigger when appropriate
useEffect(() = > {
if (!Object.values(keys).filter(value= >! value).length) { callback(keys) }else {
setKeys({ type: null });
}
}, [callback, keys, initKeysMapping]);
Copy the code
Useeffect is used to determine whether all keys are pressed when keys are changed and reducer is in effect. If all keys are pressed, a function is directly triggered, and the dependency list is clear: keys, callback function. I’m also going to explain why you can’t use state, because if you use state, state changes, useeffect, it creates an infinite loop, right
The React document explicitly states that useReducer is an alternative to useState
UseReducer can be more useful than useState in some situations, such as when the state logic is complex and contains multiple subvalues, or when the next state depends on the previous state
This problem can be solved by using the useReducer here. There is a setKeys here, but in fact, if you delete it, you can also do it without calling setKeys in useEffect
Add to the event listener
useEffect(() = > {
shortcutKeys.forEach(key= > window.addEventListener("keydown", keydownListener(key)));
return () = > shortcutKeys.forEach(key= > window.removeEventListener("keydown", keydownListener(key)))
}, [])
useEffect(() = > {
shortcutKeys.forEach(key= > window.addEventListener("keyup", keyupListener(key)));
return () = > shortcutKeys.forEach(key= > window.removeEventListener("keyup", keyupListener(key)))
}, [])
Copy the code
Add keyDown and keyUp Listener to make a deletion callback when stopping mount