useHotkeys.tsx 2.9 KB

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