All files / src/hooks useKeyboardShortcuts.ts

93.75% Statements 30/32
94.73% Branches 18/19
100% Functions 6/6
93.75% Lines 30/32

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 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121                                                                                              54x   54x 54x   54x 34x 34x           54x 8x 1x     7x   7x 8x 1x     7x         7x     7x 5x           5x 4x   5x       5x 5x                   54x 34x 1x     33x   33x   33x 33x 33x        
/**
 * Custom hook for keyboard shortcut handling
 * 
 * @module hooks/useKeyboardShortcuts
 */
 
import { useEffect, useCallback, useRef } from 'react';
import { ShortcutMap, UseKeyboardShortcutsOptions } from '../types/keyboard';
import {
  getKeyCombination,
  shortcutsMatch,
  shouldIgnoreKeyboardEvent,
  getPlatformShortcut,
  areKeyboardShortcutsSupported,
} from '../utils/keyboardUtils';
import logger from '../utils/logger';
 
/**
 * Custom hook for registering and handling keyboard shortcuts
 * 
 * @param options - Configuration options for keyboard shortcuts
 * 
 * @example
 * ```tsx
 * const shortcuts = {
 *   'save': {
 *     id: 'save',
 *     keys: 'ctrl+s',
 *     description: 'Save document',
 *     category: 'Actions',
 *     handler: () => handleSave(),
 *   }
 * };
 * 
 * useKeyboardShortcuts({
 *   shortcuts,
 *   enabled: true,
 *   preventDefault: true,
 * });
 * ```
 */
export function useKeyboardShortcuts(options: UseKeyboardShortcutsOptions): void {
  const {
    shortcuts,
    enabled = true,
    preventDefault = true,
    stopPropagation = false,
  } = options;
 
  const shortcutsRef = useRef<ShortcutMap>(shortcuts);
  const optionsRef = useRef({ preventDefault, stopPropagation });
 
  useEffect(() => {
    shortcutsRef.current = shortcuts;
    optionsRef.current = { preventDefault, stopPropagation };
  }, [shortcuts, preventDefault, stopPropagation]);
 
  /**
   * Handle keyboard event and match against registered shortcuts
   */
  const handleKeyDown = useCallback((event: KeyboardEvent): void => {
    if (!areKeyboardShortcutsSupported() || shouldIgnoreKeyboardEvent(event)) {
      return;
    }
 
    const keyCombination = getKeyCombination(event);
 
    const matchingShortcut = Object.values(shortcutsRef.current).find(shortcut => {
      if (shortcut.enabled === false) {
        return false;
      }
 
      const shortcutKeys = getPlatformShortcut(
        shortcut.keys,
        shortcut.platformKeys
      );
 
      return shortcutsMatch(keyCombination, shortcutKeys);
    });
 
    if (matchingShortcut) {
      logger.debug('Keyboard shortcut triggered', {
        id: matchingShortcut.id,
        keys: keyCombination,
        description: matchingShortcut.description,
      });
 
      if (optionsRef.current.preventDefault) {
        event.preventDefault();
      }
      Iif (optionsRef.current.stopPropagation) {
        event.stopPropagation();
      }
 
      try {
        matchingShortcut.handler();
      } catch (error) {
        logger.error('Error executing keyboard shortcut handler', {
          id: matchingShortcut.id,
          error,
        });
      }
    }
  }, []);
 
  useEffect(() => {
    if (!enabled || !areKeyboardShortcutsSupported()) {
      return;
    }
 
    logger.debug('Registering keyboard shortcuts');
 
    window.addEventListener('keydown', handleKeyDown);
 
    return () => {
      logger.debug('Unregistering keyboard shortcuts');
      window.removeEventListener('keydown', handleKeyDown);
    };
  }, [enabled, handleKeyDown]);
}