From 748b5896e8e04874dde2fb6370a43b3054ec721e Mon Sep 17 00:00:00 2001 From: Patrick Fic <> Date: Thu, 8 Apr 2021 14:32:15 -0700 Subject: [PATCH] IO-177 Added key modifier hooks. --- .../jobs-detail.page.component.jsx | 5 + client/src/utils/useKeyboardShortcut.jsx | 129 ++++++++++++++++++ 2 files changed, 134 insertions(+) create mode 100644 client/src/utils/useKeyboardShortcut.jsx diff --git a/client/src/pages/jobs-detail/jobs-detail.page.component.jsx b/client/src/pages/jobs-detail/jobs-detail.page.component.jsx index fcdb417f0..3e4e2d47c 100644 --- a/client/src/pages/jobs-detail/jobs-detail.page.component.jsx +++ b/client/src/pages/jobs-detail/jobs-detail.page.component.jsx @@ -45,6 +45,9 @@ import ScheduleJobModalContainer from "../../components/schedule-job-modal/sched import { selectJobReadOnly } from "../../redux/application/application.selectors"; import { setModalContext } from "../../redux/modals/modals.actions"; import { selectBodyshop } from "../../redux/user/user.selectors"; +import useKeyboardShortcut, { + useKeyboardSaveShortcut, +} from "../../utils/useKeyboardShortcut"; const mapStateToProps = createStructuredSelector({ bodyshop: selectBodyshop, @@ -77,6 +80,8 @@ export function JobsDetailPage({ form.resetFields(); }, [form, job]); + useKeyboardSaveShortcut(form.submit); + const handleFinish = async (values) => { setLoading(true); const result = await mutationUpdateJob({ diff --git a/client/src/utils/useKeyboardShortcut.jsx b/client/src/utils/useKeyboardShortcut.jsx new file mode 100644 index 000000000..1f31def21 --- /dev/null +++ b/client/src/utils/useKeyboardShortcut.jsx @@ -0,0 +1,129 @@ +import { useEffect, useCallback, useReducer } from "react"; +//Based on https://www.fullstacklabs.co/blog/keyboard-shortcuts-with-react-hooks +const blacklistedTargets = []; // ["INPUT", "TEXTAREA"]; +export const useKeyboardSaveShortcut = (callback) => + useKeyboardShortcut(["Control", "S"], callback, { overrideSystem: true }); + +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 useKeyboardShortcut = (shortcutKeys, callback, options) => { + if (!Array.isArray(shortcutKeys)) + throw new Error( + "The first parameter to `useKeyboardShortcut` must be an ordered array of `KeyboardEvent.key` strings." + ); + + if (!shortcutKeys.length) + throw new Error( + "The first parameter to `useKeyboardShortcut` must contain atleast one `KeyboardEvent.key` string." + ); + + if (!callback || typeof callback !== "function") + throw new Error( + "The second parameter to `useKeyboardShortcut` must be a function that will be envoked when the keys are pressed." + ); + + const { overrideSystem } = options || {}; + const initalKeyMapping = shortcutKeys.reduce((currentKeys, key) => { + currentKeys[key.toLowerCase()] = false; + return currentKeys; + }, {}); + + 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; + + if (overrideSystem) { + keydownEvent.preventDefault(); + disabledEventPropagation(keydownEvent); + } + + setKeys({ type: "set-key-down", key: loweredKey }); + return false; + }, + [keys, overrideSystem] + ); + + const keyupListener = useCallback( + (assignedKey) => (keyupEvent) => { + const raisedKey = assignedKey.toLowerCase(); + + if (blacklistedTargets.includes(keyupEvent.target.tagName)) return; + if (keyupEvent.key.toLowerCase() !== raisedKey) return; + if (keys[raisedKey] === undefined) return; + + if (overrideSystem) { + keyupEvent.preventDefault(); + disabledEventPropagation(keyupEvent); + } + + setKeys({ type: "set-key-up", key: raisedKey }); + return false; + }, + [keys, overrideSystem] + ); + + useEffect(() => { + if (!Object.values(keys).filter((value) => !value).length) { + callback(keys); + setKeys({ type: "reset-keys", data: initalKeyMapping }); + } else { + setKeys({ type: null }); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [callback, keys]); + + useEffect(() => { + shortcutKeys.forEach((k) => + window.addEventListener("keydown", keydownListener(k)) + ); + return () => + shortcutKeys.forEach((k) => + window.removeEventListener("keydown", keydownListener(k)) + ); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + useEffect(() => { + shortcutKeys.forEach((k) => + window.addEventListener("keyup", keyupListener(k)) + ); + return () => + shortcutKeys.forEach((k) => + window.removeEventListener("keyup", keyupListener(k)) + ); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); +}; + +export default useKeyboardShortcut; + +function disabledEventPropagation(e) { + if (e) { + if (e.stopPropagation) { + e.stopPropagation(); + } else if (window.event) { + window.event.cancelBubble = true; + } + } +}