All files / webview-app/src/hooks useKeyboardShortcuts.ts

96.96% Statements 32/33
95.23% Branches 40/42
100% Functions 5/5
96.96% Lines 32/33

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                                              8x   8x 5x     3x                                   7x 5x 8x 3x     5x 5x   5x 1x 1x 1x     4x       4x 8x 8x   8x 2x 2x 2x     2x 1x 1x 1x     1x 1x 1x       5x   5x 5x                              
import { useEffect } from "react";
 
import type { UseUndoRedoResult } from "@/hooks/useUndoRedo";
 
type KeyboardShortcutHandler = UseUndoRedoResult<unknown>["undo"];
type KeyboardShortcutAvailability = UseUndoRedoResult<unknown>["canUndo"];
 
type KeyboardEventTarget = Pick<Window, "addEventListener" | "removeEventListener">;
 
export interface UseKeyboardShortcutsOptions {
  onUndo: KeyboardShortcutHandler;
  onRedo: KeyboardShortcutHandler;
  onDelete?: KeyboardShortcutHandler;
  onSave?: KeyboardShortcutHandler;
  canUndo?: KeyboardShortcutAvailability;
  canRedo?: KeyboardShortcutAvailability;
  canDelete?: KeyboardShortcutAvailability;
  canSave?: KeyboardShortcutAvailability;
  target?: KeyboardEventTarget;
  shouldIgnoreEvent?: (event: KeyboardEvent) => boolean;
}
 
function isEditableTarget(event: KeyboardEvent): boolean {
  const target = event.target;
 
  if (!(target instanceof Element)) {
    return false;
  }
 
  return (
    (target instanceof HTMLElement && target.isContentEditable) ||
    target.closest("input, textarea, [contenteditable='true'], [contenteditable='']") !== null
  );
}
 
export function useKeyboardShortcuts({
  onUndo,
  onRedo,
  onDelete,
  onSave,
  canUndo = true,
  canRedo = true,
  canDelete = false,
  canSave = true,
  target = window,
  shouldIgnoreEvent = isEditableTarget,
}: UseKeyboardShortcutsOptions): void {
  useEffect(() => {
    const handleKeyDown = (event: KeyboardEvent) => {
      if (shouldIgnoreEvent(event)) {
        return;
      }
 
      const key = event.key.toLowerCase();
      const isDeleteShortcut = key === "delete";
 
      if (isDeleteShortcut && canDelete && onDelete) {
        event.preventDefault();
        onDelete();
        return;
      }
 
      Iif (!event.ctrlKey && !event.metaKey) {
        return;
      }
 
      const isSaveShortcut = key === "s" && !event.shiftKey;
      const isUndoShortcut = key === "z" && !event.shiftKey;
      const isRedoShortcut = key === "y" || (key === "z" && event.shiftKey);
 
      if (isSaveShortcut && canSave && onSave) {
        event.preventDefault();
        onSave();
        return;
      }
 
      if (isUndoShortcut && canUndo) {
        event.preventDefault();
        onUndo();
        return;
      }
 
      Eif (isRedoShortcut && canRedo) {
        event.preventDefault();
        onRedo();
      }
    };
 
    target.addEventListener("keydown", handleKeyDown);
 
    return () => {
      target.removeEventListener("keydown", handleKeyDown);
    };
  }, [
    canDelete,
    canRedo,
    canSave,
    canUndo,
    onDelete,
    onRedo,
    onSave,
    onUndo,
    shouldIgnoreEvent,
    target,
  ]);
}