VUE

useKeyboardShortcut Composable

Reactive keyboard shortcut handling with modifier keys and scoped targets

Vue3ComposablesKeyboardShortcutsEvents

Code

import { ref, onMounted, onUnmounted, type Ref } from 'vue';

type ModifierKey = 'ctrl' | 'shift' | 'alt' | 'meta';
interface ShortcutOptions {
  modifiers?: ModifierKey[];
  target?: HTMLElement | Window;
  preventDefault?: boolean;
  stopPropagation?: boolean;
  scoped?: boolean; // Only trigger when target is focused
}

export function useKeyboardShortcut(
  key: string | string[],
  callback: (e: KeyboardEvent) => void,
  options: ShortcutOptions = { preventDefault: true, stopPropagation: false, scoped: false }
) {
  const isListening = ref(true);
  const keys = Array.isArray(key) ? key : [key];
  const target = options.target || window;

  const isModifierKeyPressed = (e: KeyboardEvent, modifier: ModifierKey) => {
    switch (modifier) {
      case 'ctrl': return e.ctrlKey;
      case 'shift': return e.shiftKey;
      case 'alt': return e.altKey;
      case 'meta': return e.metaKey;
      default: return false;
    }
  };

  const areModifiersPressed = (e: KeyboardEvent) => {
    const requiredModifiers = options.modifiers || [];
    __TOKEN_47__ (requiredModifiers.length === 0) return true;

    // Check all required modifiers are pressed
    return requiredModifiers.every(mod => isModifierKeyPressed(e, mod));
  };

  const handleKeyDown = (e: KeyboardEvent) => {
    __TOKEN_51__ (!isListening.value) return;

    // Scoped mode: only trigger if target is focused
    __TOKEN_53__ (options.scoped && target !== window) {
      const focusedElement = document.activeElement;
      __TOKEN_55__ (!focusedElement || !target.contains(focusedElement)) return;
    }

    // Check if key matches
    const keyMatches = keys.some(k => {
      // Normalize key (case insensitive, handle special keys)
      const normalizedKey = k.toLowerCase();
      const eventKey = e.key.toLowerCase();
      
      // Handle special cases (e.g. 'enter' vs 'return')
      __TOKEN_60__ (normalizedKey === 'enter' && eventKey === 'return') return true;
      __TOKEN_62__ (normalizedKey === 'esc' && eventKey === 'escape') return true;
      
      return eventKey === normalizedKey;
    });

    __TOKEN_65__ (keyMatches && areModifiersPressed(e)) {
      __TOKEN_66__ (options.preventDefault) e.preventDefault();
      __TOKEN_67__ (options.stopPropagation) e.stopPropagation();
      callback(e);
    }
  };

  const enable = () => {
    isListening.value = true;
  };

  const disable = () => {
    isListening.value = false;
  };

  const toggle = () => {
    isListening.value = !isListening.value;
  };

  onMounted(() => {
    target.addEventListener('keydown', handleKeyDown);
  });

  onUnmounted(() => {
    target.removeEventListener('keydown', handleKeyDown);
  });

  return {
    isListening,
    enable,
    disable,
    toggle
  };
}

// Usage example
// const { disable } = useKeyboardShortcut(
//   's',
//   () => saveDocument(),
//   { modifiers: ['ctrl'], preventDefault: true }
// );
// // Disable shortcut temporarily
// disable();