CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/npm-react-hotkeys-hook

React hook for handling keyboard shortcuts in components in a declarative way

Pending
Overview
Eval results
Files

key-recording.mddocs/

Key Recording

Hook for recording keyboard input to discover key combinations, useful for hotkey configuration interfaces and debugging.

Capabilities

useRecordHotkeys Hook

Hook for recording keyboard input to capture key combinations as they are pressed.

/**
 * Hook for recording keyboard input to discover key combinations
 * @param useKey - Whether to record key names instead of key codes (default: false)
 * @returns Tuple with recorded keys set and control functions
 */
function useRecordHotkeys(useKey?: boolean): [
  Set<string>,
  {
    start: () => void;
    stop: () => void;
    resetKeys: () => void;
    isRecording: boolean;
  }
];

Usage Examples:

import { useRecordHotkeys } from 'react-hotkeys-hook';

function HotkeyRecorder() {
  const [keys, { start, stop, resetKeys, isRecording }] = useRecordHotkeys();
  
  return (
    <div>
      <div>
        Recorded keys: {Array.from(keys).join(' + ')}
      </div>
      
      <button onClick={start} disabled={isRecording}>
        Start Recording
      </button>
      <button onClick={stop} disabled={!isRecording}>
        Stop Recording
      </button>
      <button onClick={resetKeys}>
        Clear Keys
      </button>
      
      <div>
        Status: {isRecording ? 'Recording...' : 'Stopped'}
      </div>
    </div>
  );
}

Recording Key Names vs Key Codes

Control whether to record key names or key codes using the useKey parameter.

// Record key codes (default) - more reliable across keyboards
const [codes, codeControls] = useRecordHotkeys(false);
// Example output: ['ControlLeft', 'KeyK']

// Record key names - more readable but less reliable
const [names, nameControls] = useRecordHotkeys(true);
// Example output: ['Control', 'k']

Practical Examples

Hotkey Configuration Interface

function HotkeyConfig({ onSave }) {
  const [keys, { start, stop, resetKeys, isRecording }] = useRecordHotkeys();
  const [savedHotkey, setSavedHotkey] = useState('');
  
  const handleSave = () => {
    const combination = Array.from(keys).join('+');
    setSavedHotkey(combination);
    onSave(combination);
    resetKeys();
  };
  
  return (
    <div className="hotkey-config">
      <h3>Configure Hotkey</h3>
      
      <div className="recording-area">
        <p>Press keys to record combination:</p>
        <div className="key-display">
          {keys.size > 0 ? Array.from(keys).join(' + ') : 'No keys recorded'}
        </div>
      </div>
      
      <div className="controls">
        <button onClick={isRecording ? stop : start}>
          {isRecording ? 'Stop Recording' : 'Start Recording'}
        </button>
        <button onClick={resetKeys} disabled={keys.size === 0}>
          Clear
        </button>
        <button onClick={handleSave} disabled={keys.size === 0}>
          Save Hotkey
        </button>
      </div>
      
      {savedHotkey && (
        <div className="saved-hotkey">
          Saved hotkey: <code>{savedHotkey}</code>
        </div>
      )}
    </div>
  );
}

Live Hotkey Debugger

function HotkeyDebugger() {
  const [keys, { start, stop, isRecording }] = useRecordHotkeys();
  const [history, setHistory] = useState([]);
  
  useEffect(() => {
    // Auto-start recording for debugging
    start();
    return stop;
  }, [start, stop]);
  
  useEffect(() => {
    if (keys.size > 0) {
      const combination = Array.from(keys).join('+');
      setHistory(prev => [
        ...prev.slice(-9), // Keep last 10 entries
        { keys: combination, timestamp: Date.now() }
      ]);
    }
  }, [keys]);
  
  return (
    <div className="hotkey-debugger">
      <h3>Hotkey Debugger</h3>
      
      <div className="current-keys">
        <strong>Currently Pressed:</strong>
        <code>{keys.size > 0 ? Array.from(keys).join(' + ') : 'None'}</code>
      </div>
      
      <div className="history">
        <h4>Recent Combinations:</h4>
        {history.map(({ keys, timestamp }, index) => (
          <div key={index} className="history-entry">
            <code>{keys}</code>
            <span className="timestamp">
              {new Date(timestamp).toLocaleTimeString()}
            </span>
          </div>
        ))}
      </div>
      
      <div className="status">
        Recording: {isRecording ? '🔴 Active' : '⚫ Stopped'}
      </div>
    </div>
  );
}

Hotkey Conflict Detection

function HotkeyConflictDetector({ existingHotkeys }) {
  const [keys, { start, stop, resetKeys, isRecording }] = useRecordHotkeys();
  const [conflicts, setConflicts] = useState([]);
  
  useEffect(() => {
    if (keys.size > 0) {
      const combination = Array.from(keys).join('+');
      const foundConflicts = existingHotkeys.filter(hotkey => 
        hotkey.combination === combination
      );
      setConflicts(foundConflicts);
    } else {
      setConflicts([]);
    }
  }, [keys, existingHotkeys]);
  
  return (
    <div className="conflict-detector">
      <h3>Hotkey Conflict Detection</h3>
      
      <div className="input-area">
        <p>Press keys to check for conflicts:</p>
        <div className="key-display">
          {keys.size > 0 ? Array.from(keys).join(' + ') : 'No keys pressed'}
        </div>
        
        {conflicts.length > 0 && (
          <div className="conflicts">
            <strong>⚠️ Conflicts detected:</strong>
            <ul>
              {conflicts.map((conflict, index) => (
                <li key={index}>
                  <code>{conflict.combination}</code> - {conflict.description}
                </li>
              ))}
            </ul>
          </div>
        )}
      </div>
      
      <div className="controls">
        <button onClick={isRecording ? stop : start}>
          {isRecording ? 'Stop' : 'Start'} Checking
        </button>
        <button onClick={resetKeys}>
          Clear
        </button>
      </div>
    </div>
  );
}

Custom Hotkey Builder

function HotkeyBuilder({ onBuild }) {
  const [keys, { start, stop, resetKeys, isRecording }] = useRecordHotkeys();
  const [description, setDescription] = useState('');
  const [builtHotkeys, setBuiltHotkeys] = useState([]);
  
  const buildHotkey = () => {
    if (keys.size === 0) return;
    
    const combination = Array.from(keys).join('+');
    const hotkey = {
      keys: combination,
      description: description || 'Unnamed hotkey',
      id: Date.now()
    };
    
    setBuiltHotkeys(prev => [...prev, hotkey]);
    onBuild(hotkey);
    
    resetKeys();
    setDescription('');
  };
  
  return (
    <div className="hotkey-builder">
      <h3>Build Custom Hotkeys</h3>
      
      <div className="builder-form">
        <div className="key-input">
          <label>Key Combination:</label>
          <div className="key-display">
            {keys.size > 0 ? Array.from(keys).join(' + ') : 'Press keys...'}
          </div>
          <button onClick={isRecording ? stop : start}>
            {isRecording ? 'Stop Recording' : 'Record Keys'}
          </button>
        </div>
        
        <div className="description-input">
          <label>Description:</label>
          <input
            type="text"
            value={description}
            onChange={(e) => setDescription(e.target.value)}
            placeholder="What does this hotkey do?"
          />
        </div>
        
        <div className="actions">
          <button onClick={buildHotkey} disabled={keys.size === 0}>
            Add Hotkey
          </button>
          <button onClick={resetKeys}>
            Clear Keys
          </button>
        </div>
      </div>
      
      <div className="built-hotkeys">
        <h4>Built Hotkeys:</h4>
        {builtHotkeys.map(hotkey => (
          <div key={hotkey.id} className="hotkey-item">
            <code>{hotkey.keys}</code> - {hotkey.description}
          </div>
        ))}
      </div>
    </div>
  );
}

Implementation Notes

Event Handling

The recording system automatically:

  • Prevents default browser behavior during recording
  • Stops event propagation to avoid conflicts
  • Handles synthetic events (ignores Chrome autofill events)
  • Maps key codes to normalized key names

Browser Compatibility

The hook handles cross-browser differences in key event handling and provides consistent key naming across different platforms and keyboard layouts.

Memory Management

The recording system automatically cleans up event listeners when:

  • The component unmounts
  • Recording is stopped
  • The hook is re-initialized

Install with Tessl CLI

npx tessl i tessl/npm-react-hotkeys-hook

docs

index.md

key-checking.md

key-recording.md

main-hook.md

scope-management.md

tile.json