From 9d3de7bc01a1a95a9017b1d529e0f19bc49ae7dc Mon Sep 17 00:00:00 2001 From: Tom Butcher Date: Sat, 1 Aug 2026 14:00:40 +0100 Subject: [PATCH] Refactor KeyboardShortcut component for improved key handling and logging - Updated the `parseShortcut` function to utilize a modifier aliases object for better readability and maintainability. - Introduced `getPressedKey` function to streamline key detection logic. - Replaced the previous shortcut key set management with a more efficient use of `useMemo` and `useState` for tracking pressed keys. - Added optional `log` prop to enable logging of key events for debugging purposes. - Enhanced the overall structure and performance of the KeyboardShortcut component. --- .../Dashboard/common/KeyboardShortcut.jsx | 124 ++++++++++-------- 1 file changed, 68 insertions(+), 56 deletions(-) diff --git a/src/components/Dashboard/common/KeyboardShortcut.jsx b/src/components/Dashboard/common/KeyboardShortcut.jsx index 515966b..9afd4d3 100644 --- a/src/components/Dashboard/common/KeyboardShortcut.jsx +++ b/src/components/Dashboard/common/KeyboardShortcut.jsx @@ -1,62 +1,76 @@ -import { useEffect, useRef, cloneElement, useCallback } from 'react' +import { useEffect, useRef, cloneElement, useMemo, useState } from 'react' import PropTypes from 'prop-types' import { Popover, Typography } from 'antd' -// Utility to parse shortcut string like 'cmd+shift+p' or 'ctrl+s' +const MODIFIER_ALIASES = { + cmd: 'meta', + meta: 'meta', + ctrl: 'control', + control: 'control', + alt: 'alt', + option: 'alt', + shift: 'shift' +} + function parseShortcut(shortcut) { - const parts = shortcut.toLowerCase().split('+') - return { - meta: parts.includes('cmd') || parts.includes('meta') || false, - ctrl: parts.includes('ctrl') || parts.includes('control') || false, - alt: parts.includes('alt') || parts.includes('option') || false, - shift: parts.includes('shift') || false, - key: parts.find( - (p) => - !['cmd', 'meta', 'ctrl', 'control', 'alt', 'option', 'shift'].includes( - p - ) - )[0] + return shortcut + .toLowerCase() + .split('+') + .map((part) => MODIFIER_ALIASES[part] ?? part) +} + +function getPressedKey(event) { + if ( + event.key === 'Meta' || + event.key === 'Control' || + event.key === 'Alt' || + event.key === 'Shift' + ) { + return event.key.toLowerCase() } + if (event.code) { + const code = event.code.toLowerCase() + if (code.startsWith('key')) { + return code.slice(3) + } + return code + } + return null } const { Text } = Typography -const KeyboardShortcut = ({ shortcut, children, hint, onTrigger }) => { +const KeyboardShortcut = ({ + shortcut, + children, + hint, + onTrigger, + log = false +}) => { const childRef = useRef() - const shortcutObj = parseShortcut(shortcut) - - // Helper to get the set of keys required for the shortcut - const getShortcutKeySet = useCallback((shortcutObj) => { - const keys = [] - if (shortcutObj.meta) keys.push('Meta') - if (shortcutObj.ctrl) keys.push('Control') - if (shortcutObj.alt) keys.push('Alt') - if (shortcutObj.shift) keys.push('Shift') - // shortcutObj.code is like 'keyp', so extract the last char - if (shortcutObj.key) { - keys.push('Key' + shortcutObj.key.toUpperCase()) - } - return new Set(keys) - }, []) - - const shortcutKeySet = getShortcutKeySet(shortcutObj) + const pressedKeysRef = useRef(new Set()) + const shortcutKeys = useMemo(() => parseShortcut(shortcut), [shortcut]) + const [pressedKeys, setPressedKeys] = useState([]) useEffect(() => { - const pressedKeys = new Set() + const syncPressedKeys = () => { + setPressedKeys([...pressedKeysRef.current]) + } + const handleKeyDown = (event) => { - if ( - event.key === 'Meta' || - event.key === 'Control' || - event.key === 'Alt' || - event.key === 'Shift' - ) { - pressedKeys.add(event.key) - } else if (event.code && event.code.startsWith('Key')) { - pressedKeys.add(event.code) + const key = getPressedKey(event) + if (key) { + pressedKeysRef.current.add(key) + syncPressedKeys() } + + if (log) { + console.log('Key Down:', key, 'Pressed Keys:', [...pressedKeysRef.current]) + } + if ( - shortcutKeySet.size && - [...shortcutKeySet].every((k) => pressedKeys.has(k)) + shortcutKeys.length && + shortcutKeys.every((k) => pressedKeysRef.current.has(k)) ) { if (typeof onTrigger === 'function') { onTrigger(event) @@ -64,27 +78,24 @@ const KeyboardShortcut = ({ shortcut, children, hint, onTrigger }) => { event.preventDefault() } } + const handleKeyUp = (event) => { - if ( - event.key === 'Meta' || - event.key === 'Control' || - event.key === 'Alt' || - event.key === 'Shift' - ) { - pressedKeys.delete(event.key) - } else if (event.code && event.code.startsWith('Key')) { - pressedKeys.delete(event.key.toUpperCase()) + const key = getPressedKey(event) + if (key) { + pressedKeysRef.current.delete(key) + syncPressedKeys() } } + window.addEventListener('keydown', handleKeyDown) window.addEventListener('keyup', handleKeyUp) + return () => { window.removeEventListener('keydown', handleKeyDown) window.removeEventListener('keyup', handleKeyUp) } - }, [shortcut, shortcutObj, onTrigger, shortcutKeySet]) + }, [shortcutKeys, onTrigger, log]) - // Clone the child to attach a ref const element = cloneElement(children, { ref: childRef }) if (hint) { @@ -108,7 +119,8 @@ KeyboardShortcut.propTypes = { shortcut: PropTypes.string.isRequired, // e.g. 'cmd+shift+p' onTrigger: PropTypes.func.isRequired, children: PropTypes.element.isRequired, - hint: PropTypes.string // Optional, e.g. '⌘ ⇧ P' + hint: PropTypes.string, // Optional, e.g. '⌘ ⇧ P' + log: PropTypes.bool // Optional, e.g. true } export default KeyboardShortcut