useHotkeys.tsx 2.5 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889
  1. import {useCallback, useEffect, useMemo} from 'react';
  2. import {getKeyCode} from './getKeyCode';
  3. const isKeyPressed = (key: string, evt: KeyboardEvent): boolean => {
  4. const keyCode = getKeyCode(key);
  5. switch (keyCode) {
  6. case getKeyCode('command'):
  7. return evt.metaKey;
  8. case getKeyCode('shift'):
  9. return evt.shiftKey;
  10. case getKeyCode('ctrl'):
  11. return evt.ctrlKey;
  12. case getKeyCode('alt'):
  13. return evt.altKey;
  14. default:
  15. return keyCode === evt.keyCode;
  16. }
  17. };
  18. type Hotkey = {
  19. /**
  20. * The callback triggered when the matching key is pressed
  21. */
  22. callback: (e: KeyboardEvent) => void;
  23. /**
  24. * Defines the matching shortcuts.
  25. */
  26. match: string[] | string;
  27. /**
  28. * Allow shortcuts to be triggered while a text input is foccused
  29. */
  30. includeInputs?: boolean;
  31. /**
  32. * Do not call preventDefault on the keydown event
  33. */
  34. skipPreventDefault?: boolean;
  35. };
  36. /**
  37. * Pass in the hotkey combinations under match and the corresponding callback
  38. * function to be called. Separate key names with +. For example,
  39. * 'command+alt+shift+x'
  40. *
  41. * Alternate matchings as an array: ['command+alt+backspace', 'ctrl+alt+delete']
  42. *
  43. * Note: you can only use one non-modifier (keys other than shift, ctrl, alt, command) key at a time.
  44. */
  45. export function useHotkeys(hotkeys: Hotkey[], deps: React.DependencyList): void {
  46. // eslint-disable-next-line react-hooks/exhaustive-deps
  47. const memoizedHotkeys = useMemo(() => hotkeys, deps);
  48. const onKeyDown = useCallback(
  49. (evt: KeyboardEvent) => {
  50. for (const hotkey of memoizedHotkeys) {
  51. const preventDefault = !hotkey.skipPreventDefault;
  52. const keysets = Array.isArray(hotkey.match) ? hotkey.match : [hotkey.match];
  53. for (const keyset of keysets) {
  54. const keys = keyset.split('+');
  55. const allKeysPressed = keys.every(key => isKeyPressed(key, evt));
  56. const inputHasFocus =
  57. !hotkey.includeInputs && evt.target instanceof HTMLElement
  58. ? ['textarea', 'input'].includes(evt.target.tagName.toLowerCase())
  59. : false;
  60. if (allKeysPressed && !inputHasFocus) {
  61. if (preventDefault) {
  62. evt.preventDefault();
  63. }
  64. hotkey.callback(evt);
  65. return;
  66. }
  67. }
  68. }
  69. },
  70. [memoizedHotkeys]
  71. );
  72. useEffect(() => {
  73. document.addEventListener('keydown', onKeyDown);
  74. return () => {
  75. document.removeEventListener('keydown', onKeyDown);
  76. };
  77. }, [onKeyDown]);
  78. }