import React, { useEffect, useRef } from 'react' import PropTypes from 'prop-types' import { Popover, Typography } from 'antd' // Utility to parse shortcut string like 'cmd+shift+p' or 'ctrl+s' 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] } } const { Text } = Typography const KeyboardShortcut = ({ shortcut, children, hint, onTrigger }) => { const childRef = useRef() const shortcutObj = parseShortcut(shortcut) let pressedKeys = new Set() // Helper to get the set of keys required for the shortcut function getShortcutKeySet(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) useEffect(() => { pressedKeys = new Set() 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) } if ( shortcutKeySet.size && [...shortcutKeySet].every((k) => pressedKeys.has(k)) ) { if (typeof onTrigger === 'function') { onTrigger(event) } 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()) } } window.addEventListener('keydown', handleKeyDown) window.addEventListener('keyup', handleKeyUp) return () => { window.removeEventListener('keydown', handleKeyDown) window.removeEventListener('keyup', handleKeyUp) } }, [shortcut, shortcutObj, onTrigger]) // Clone the child to attach a ref const element = React.cloneElement(children, { ref: childRef }) if (hint) { return ( {hint} } arrow={false} > {element} ) } return element } 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' } export default KeyboardShortcut