116 lines
3.3 KiB
JavaScript
116 lines
3.3 KiB
JavaScript
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 (
|
|
<Popover
|
|
content={
|
|
<Text keyboard className='keyboard-shortcut-tooltip'>
|
|
{hint}
|
|
</Text>
|
|
}
|
|
arrow={false}
|
|
>
|
|
{element}
|
|
</Popover>
|
|
)
|
|
}
|
|
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
|