useHotkeys.tsx 2.5 KB

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