All files / src/utils hooks.ts

78.08% Statements 57/73
58.82% Branches 10/17
100% Functions 2/2
78.08% Lines 57/73

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 911x 1x   1x 6x 6x   6x 3x 3x 3x 3x 6x   6x 6x   1x 3x 3x   3x 3x 3x   3x 3x 2x 2x 2x 2x   2x 2x 2x 2x 2x 2x   2x       2x       2x 2x   2x 1x 1x       1x 1x 1x 1x 1x 1x               1x     1x 2x   2x     2x 3x     3x 3x   3x 3x 3x 3x 3x  
import { useCallback, useEffect, useLayoutEffect, useRef, useState } from "react"
import { detectMetaKeyChar, detectOS, unknown } from "./platformUtils"
 
export const useMetaKeyDetection = (platform: string) => {
	const [metaKeyChar, setMetaKeyChar] = useState(unknown)
	const [os, setOs] = useState(unknown)
 
	useEffect(() => {
		const detectedMetaKeyChar = detectMetaKeyChar(platform)
		const detectedOs = detectOS(platform)
		setMetaKeyChar(detectedMetaKeyChar)
		setOs(detectedOs)
	}, [platform])
 
	return [os, metaKeyChar]
}
 
export const useShortcut = (shortcut: string, callback: (...args: unknown[]) => void, options = { disableTextInputs: true }) => {
	const callbackRef = useRef(callback)
	const [keyCombo, setKeyCombo] = useState<string[]>([])
 
	useLayoutEffect(() => {
		callbackRef.current = callback
	})
 
	const handleKeyDown = useCallback(
		(event: KeyboardEvent) => {
			const isTextInput =
				event.target instanceof HTMLTextAreaElement ||
				(event.target instanceof HTMLInputElement && (!event.target.type || event.target.type === "text")) ||
				(event.target as HTMLElement).isContentEditable
 
			const modifierMap: { [key: string]: boolean } = {
				Control: event.ctrlKey,
				Alt: event.altKey,
				Meta: event.metaKey, // alias for Command
				Shift: event.shiftKey,
			}
 
			if (event.repeat) {
				return null
			}
 
			if (options.disableTextInputs && isTextInput) {
				return event.stopPropagation()
			}
 
			if (shortcut.includes("+")) {
				const keyArray = shortcut.split("+")
 
				if (Object.keys(modifierMap).includes(keyArray[0])) {
					const finalKey = keyArray.pop()
					if (!finalKey) {
						return
					}
 
					if (keyArray.every((k) => modifierMap[k]) && finalKey.toLowerCase() === event.key.toLowerCase()) {
						event.preventDefault()
						return callbackRef.current(event)
					}
				} else {
					if (keyArray[keyCombo.length] === event.key) {
						if (keyArray[keyArray.length - 1] === event.key && keyCombo.length === keyArray.length - 1) {
							callbackRef.current(event)
							return setKeyCombo([])
						}
 
						return setKeyCombo((prevCombo) => [...prevCombo, event.key])
					}
					if (keyCombo.length > 0) {
						return setKeyCombo([])
					}
				}
			}
 
			if (shortcut === event.key) {
				return callbackRef.current(event)
			}
		},
		[keyCombo.length, options.disableTextInputs, shortcut],
	)
 
	useEffect(() => {
		window.addEventListener("keydown", handleKeyDown)
 
		return () => {
			window.removeEventListener("keydown", handleKeyDown)
		}
	}, [handleKeyDown])
}