All files / hooks useCollectionsWorkspaceShortcuts.ts

74.5% Statements 38/51
75% Branches 36/48
100% Functions 5/5
85% Lines 34/40

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              8x 8x 8x     16x   16x 16x 3x   3x 3x         3x 3x 3x   3x                                     87x   87x   12x 12x 12x     2x               2x 1x 1x     1x 1x 1x     1x 1x     1x 1x 1x             87x 31x 30x 30x            
/**
 * Keyboard shortcuts for the Collections workspace.
 * Only active when focus is not in an editable field.
 */
 
import { useEffect, useCallback } from 'react';
 
export const COLLECTION_BROWSER_SEARCH_ID = 'collection-browser-search';
export const SCHEMA_ADD_FIELD_TEST_ID = 'schema-add-field';
export const SHORTCUTS_HELP_TRIGGER_ID = 'collections-shortcuts-help';
 
function isEditableTarget(target: EventTarget | null): boolean {
  Iif (!(target instanceof HTMLElement)) return false;
 
  const tag = target.tagName;
  if (tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT') return true;
  Iif (target.isContentEditable) return true;
 
  const role = target.getAttribute('role');
  Iif (role === 'textbox' || role === 'combobox' || role === 'searchbox') {
    return true;
  }
 
  // Radix select / cmdk often focus nested elements
  Iif (target.closest('[contenteditable="true"]')) return true;
  Iif (target.closest('[role="textbox"]')) return true;
  Iif (target.closest('[role="combobox"]')) return true;
 
  return false;
}
 
export interface UseCollectionsWorkspaceShortcutsOptions {
  /** When false, listeners are not attached. Default true. */
  enabled?: boolean;
  /** Called when Shift+/ opens help (optional; button can also open). */
  onOpenHelp?: () => void;
}
 
/**
 * Register workspace shortcuts:
 * - `/` focus collection browser search
 * - `a` click Add Field when present
 * - `?` open shortcuts help
 */
export function useCollectionsWorkspaceShortcuts(
  options: UseCollectionsWorkspaceShortcutsOptions = {},
): void {
  const { enabled = true, onOpenHelp } = options;
 
  const handleKeyDown = useCallback(
    (event: KeyboardEvent) => {
      Iif (!enabled) return;
      Iif (event.metaKey || event.ctrlKey || event.altKey) return;
      if (isEditableTarget(event.target)) return;
 
      // Shift+/ produces '?' on most layouts
      Iif (event.key === '?' || (event.key === '/' && event.shiftKey)) {
        event.preventDefault();
        onOpenHelp?.();
        const trigger = document.getElementById(SHORTCUTS_HELP_TRIGGER_ID);
        trigger?.click();
        return;
      }
 
      if (event.key === '/') {
        event.preventDefault();
        const search = document.getElementById(
          COLLECTION_BROWSER_SEARCH_ID,
        ) as HTMLInputElement | null;
        search?.focus();
        search?.select();
        return;
      }
 
      Eif (event.key === 'a' || event.key === 'A') {
        const addBtn = document.querySelector<HTMLButtonElement>(
          `[data-testid="${SCHEMA_ADD_FIELD_TEST_ID}"]`,
        );
        Eif (addBtn && !addBtn.disabled) {
          event.preventDefault();
          addBtn.click();
        }
      }
    },
    [enabled, onOpenHelp],
  );
 
  useEffect(() => {
    if (!enabled) return;
    window.addEventListener('keydown', handleKeyDown);
    return () => window.removeEventListener('keydown', handleKeyDown);
  }, [enabled, handleKeyDown]);
}
 
/** Exported for unit tests */
export { isEditableTarget };