rx-hotkeys
v4.2.1
Published
Advanced Keyboard Shortcut Management library using rxjs
Readme
rx-hotkeys: Advanced Keyboard Shortcut Management with RxJS
rx-hotkeys is a powerful and flexible TypeScript library for managing keyboard shortcuts in web applications. It leverages the full power of RxJS to handle keyboard events, allowing for the registration of simple key combinations (e.g., Ctrl+S), complex key sequences (e.g., g -> i for "go to inbox"), and much more. It supports contexts for enabling/disabling shortcuts based on application state, element-scoped listeners, and provides a type-safe API for defining shortcuts.
✨ Features
- Official React Hooks: Provides an official wrapper (
HotkeysProvider,useHotkeys,useScopedHotkeysContext) for seamless, idiomatic integration with React. - Fully Observable API: Returns an RxJS
Observablefor each shortcut, allowing for powerful stream manipulation like chaining, filtering, debouncing, and merging with other streams. - Flexible Shortcut Definitions: Define shortcuts using simple, intuitive strings (e.g.,
"ctrl+s"or"g -> i") in addition to the classic object-based configuration. - Element-Scoped Listeners: Attach shortcuts to specific DOM elements, so they are only active within a certain component or area, not just on the global
document. keyupEvent Support: Trigger actions on key release (keyup) in addition to the default key press (keydown).- Key Combinations & Sequences: Supports both simultaneous key presses (
Ctrl+S) and ordered key sequences (g->c). - Context Management: Activate or deactivate groups of shortcuts based on the application's current state (e.g., "editor", "modal", "global").
- Stack-Based Context Management: Natively handles nested contexts with an
enter/leaveAPI, perfect for hierarchical UIs like pages, modals, and dropdowns. - Temporary Context Override: Safely override all contexts with a high-priority temporary context, ideal for global application states like "saving" or "loading".
- Strict Global Shortcuts: Option to register global shortcuts that only fire when no other context is active.
- Type-Safe Key Definitions: Uses an exported
Keysobject based on standardKeyboardEvent.keyvalues for a superior developer experience and fewer errors. - Sequence Timeouts: Optional timeout between key presses in a sequence to prevent accidental triggers.
- Debug Mode: Optional, detailed console logging for easier development and troubleshooting.
Installation
npm install rxjs rx-hotkeys⚠️ Breaking Changes (v4.0+)
Starting with v4.0, the context management API has been fundamentally redesigned into a more powerful and robust dual-mode system.
- The old
setContextmethod (which returned a boolean) has been replaced. - The library now offers two distinct ways to manage contexts:
- Context Stack (
enterContext/leaveContext): For hierarchical UI states. - Context Override (
setContextreturns arestorefunction): For temporary, global state overrides.
- Context Stack (
getContextmethod rename togetActiveContext.
Please review the "Context Management" section below for details.
Basic Usage
First, ensure you have the Hotkeys class and its helper Keys object imported:
import { Hotkeys, Keys } from "rx-hotkeys";1. Initialize Hotkeys
Create an instance of the Hotkeys class. You can optionally provide an initial context and enable debug mode.
const keyManager = new Hotkeys(); // No initial context, debug mode off
// Or with an initial context and debug mode enabled:
// const keyManager = new Hotkeys("editor", true);2. Add a Key Combination
Register a shortcut for a key combination, like Ctrl+S, by subscribing to the returned Observable.
const save$ = keyManager.addCombination({
id: "saveFile", // Unique ID for this shortcut
keys: { key: Keys.S, ctrlKey: true }, // Use Keys.S for "s" key
preventDefault: true, // Prevent browser's default save action
description: "Save the current file."
});
const saveSubscription = save$.subscribe((event) => {
console.log("Ctrl+S pressed: Save file action triggered!", event);
});3. Define Shortcuts with Strings (New)
You can also use more concise strings to define shortcuts.
// Combination
const open$ = keyManager.addCombination({ id: "openFile", keys: "ctrl+o" });
open$.subscribe(() => console.log("Opening file..."));
// Sequence
const command$ = keyManager.addSequence({ id: "showCommandPalette", sequence: "cmd+k" }); // Note: "cmd+k" is a combination, not a sequence. Let's fix this example.
const command$ = keyManager.addSequence({ id: "goToInbox", sequence: "g -> i" });
command$.subscribe(() => console.log("Navigating to Inbox..."));4. Add a Key Sequence
Register a shortcut for a sequence of keys, like the Konami code.
const konami$ = keyManager.addSequence({
id: "konamiCode",
sequence: "up -> up -> down -> down -> left -> right -> left -> right -> b -> a",
sequenceTimeoutMs: 3000, // User has 3 seconds between each key press
description: "Unlock special features."
});
konami$.subscribe((event) => { // The last KeyboardEvent of the sequence is emitted
console.log("Konami code entered!");
});5. Advanced Usage: Scopes and keyup
You can scope a shortcut to a specific element and trigger it on keyup.
const myInputField = document.getElementById("my-input");
const submit$ = keyManager.addCombination({
id: "submitOnEnter",
keys: Keys.Enter,
target: myInputField, // Only active on this element
event: "keyup", // Trigger on key release
preventDefault: true
});
submit$.subscribe(() => console.log("Form submitted on Enter keyup!"));6. Context Management
You now have two powerful tools for managing contexts.
A) Context Stack (enterContext / leaveContext)
Use this for nested UI scopes that follow a clear hierarchy.
// A shortcut with context: "editor" will NOT be active here.
console.log(keyManager.getActiveContext()); // null
// Activate the "editor" context
keyManager.enterContext("editor");
// Now, pressing Ctrl+S will trigger the "saveFile" shortcut.
console.log(keyManager.getActiveContext()); // 'editor'
// Imagine opening a dropdown menu inside the editor
keyManager.enterContext("dropdown-menu");
console.log(keyManager.getActiveContext()); // 'dropdown-menu'
// When the dropdown closes, leave its context
keyManager.leaveContext();
console.log(keyManager.getActiveContext()); // 'editor' (restored automatically)B) Context Override (setContext and restore)
Use this for temporary, high-priority states that should override everything else.
async function performSave() {
// Set a temporary "saving" context that overrides the stack.
const restore = keyManager.setContext('saving');
// Any shortcuts with context: 'saving' are now active.
// All other shortcuts (editor, etc.) are inactive.
console.log(keyManager.getActiveContext()); // 'saving'
try {
await someAsyncSaveOperation();
} finally {
// No matter what happens, call restore() to clear the override
// and return control to the context stack.
restore();
}
console.log(keyManager.getActiveContext()); // e.g., 'editor' (restored from the stack)
}7. Clean Up
When the Hotkeys instance is no longer needed (e.g., component unmount), call destroy() to clean up all internal streams and listeners, preventing memory leaks. This will also complete all active shortcut Observables.
// In a component lifecycle cleanup method or similar:
keyManager.destroy();Usage with React
The library provides a dedicated React wrapper for the best developer experience.
Step 1: Wrap Your App with HotkeysProvider
First, import HotkeysProvider and wrap your root application component with it. This creates a single, shared instance of the hotkeys manager for your entire app.
// In your main App.js or index.js
import React from 'react';
import ReactDOM from 'react-dom/client';
import { HotkeysProvider } from 'rx-hotkeys/react';
import App from './App';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<React.StrictMode>
<HotkeysProvider debugMode={true}>
<App />
</HotkeysProvider>
</React.StrictMode>
);Step 2: Use the useHotkeys Hook in Your Components
Now, you can use the useHotkeys and useSequence hooks anywhere in your component tree. The hook automatically handles registration, cleanup, and stale closures. You no longer need to provide a dependency array for your callback.
// src/components/Counter.jsx
import React, { useState } from 'react';
import { useHotkeys, useSequence } from 'rx-hotkeys/react';
export function Counter() {
const [count, setCount] = useState(0);
// The callback can safely use the latest component state (like `count`)
// without you needing to worry about stale closures or dependency arrays.
const handleIncrement = () => {
console.log(`Incrementing from ${count}...`);
setCount(count + 1);
};
// Register '+' key to increment.
useHotkeys('+', handleIncrement);
// Register 'c' key to increment, with options.
useHotkeys('c', handleIncrement, { preventDefault: true });
// Register a sequence to reset the counter.
useSequence('r -> e -> s -> e -> t', () => {
console.log('Resetting counter!');
setCount(0);
});
return (
<div>
<h2>Count: {count}</h2>
<p>Press '+' or 'c' to increment. Type 'reset' to reset.</p>
</div>
);
}Step 3: Manage Context with useScopedHotkeysContext
This hook allows a component (like a modal) to activate a specific context only while it is mounted.
// src/components/MyModal.jsx
import { useHotkeys, useScopedHotkeysContext } from 'rx-hotkeys/react';
export function MyModal({ onClose }) {
// This activates the 'modal' context for all children of this component.
// When MyModal unmounts, this context is automatically removed from the stack.
useScopedHotkeysContext('modal');
// This hotkey will only be active when the 'modal' context is active.
useHotkeys("escape", onClose, { context: 'modal' });
return (
<div className="modal">
<p>This is a modal. Press ESC to close.</p>
{/* ... other modal content ... */}
</div>
);
}API Reference
Keys Object & StandardKey Type
Keys: An exported constant object containing standardKeyboardEvent.keystring values (e.g.,Keys.Enter,Keys.ArrowUp,Keys.A). It's highly recommended to use these for type safety and to avoid typos.StandardKey: A TypeScript type representing any valid key string from theKeysobject.
Hotkeys Class (Core)
constructor(initialContext?: string | null, debugMode?: boolean)
Creates a new Hotkeys instance.
addCombination(config: KeyCombinationConfig): Observable<KeyboardEvent>
Registers a key combination shortcut.
config: TheKeyCombinationConfigobject.- Returns an
Observable<KeyboardEvent>that emits when the shortcut is triggered.
addSequence(config: KeySequenceConfig): Observable<KeyboardEvent>
Registers a key sequence shortcut.
config: TheKeySequenceConfigobject.- Returns an
Observable<KeyboardEvent>that emits the finalKeyboardEventwhen the sequence is completed.
enterContext(contextName: string | null): void
Pushes a context onto the context stack. It becomes active if no override is set.
leaveContext(): string | null | undefined
Pops a context from the context stack, returning the context that was left.
setContext(contextName: string | null): () => void
Sets a temporary override context. Returns a restore function to clear the override.
getActiveContext(): string | null
Returns the current active context (checks for an override first, then the stack top).
onContextChange$: Observable<string | null>
A public Observable property that emits the active context whenever it changes.
remove(id: string): boolean
Removes a registered shortcut by its ID. This will cause the corresponding Observable to complete.
- Returns
trueif found and removed,falseotherwise.
hasShortcut(id: string): boolean
Checks if a shortcut with the given ID is registered.
- Returns
trueif it exists,falseotherwise.
getActiveShortcuts(): { id: string; description?: string; context?: string | null; type: "combination" | "sequence" }[]
Returns an array of all currently registered shortcuts with their basic information.
setDebugMode(enable: boolean): void
Enables or disables console logging for debug purposes.
destroy(): void
Cleans up all subscriptions and resources. Essential to call to prevent memory leaks.
React Hooks (rx-hotkeys/react)
HotkeysProvider({ children, initialContext?, debugMode? })
A React component that provides the Hotkeys instance to its children.
useHotkeys(keys, callback, options?)
A React hook to register a key combination.
keys: KeyCombinationConfig["keys"]: The shortcut definition (e.g.,'ctrl+s').callback: (event: KeyboardEvent) => void: The function to execute.options?: HotkeyHookOptions: Optional config forpreventDefault,context,target, etc.
useSequence(sequence, callback, options?)
A React hook to register a key sequence.
sequence: KeySequenceConfig["sequence"]: The sequence definition (e.g.,'g -> i').callback: (event: KeyboardEvent) => void: The function to execute.options?: SequenceHookOptions: Optional config forpreventDefault,context, etc.
useScopedHotkeysContext(context, enabled: boolean = true)
A React hook to apply a specific context for the lifetime of the component.
useHotkeysManager(): Hotkeys
A hook to get direct access to the Hotkeys manager instance.
Configuration Interfaces
ShortcutConfigBase (Shared properties)
id: string: Unique identifier for the shortcut.context?: string | null: Specifies the context in which this shortcut is active. Ifnullorundefined, it's a global shortcut.preventDefault?: boolean: Iftrue,event.preventDefault()will be called when the shortcut triggers. Defaults tofalse.description?: string: An optional description for the shortcut (e.g., for help menus).strict?: boolean: Iftrueand the shortcut has nocontext, it will only fire when no other context is active. Defaults tofalse.target?: HTMLElement: The DOM element to attach the listener to. Defaults todocument.event?: "keydown" | "keyup": The keyboard event to listen for. Defaults to"keydown".options?: AddEventListenerOptions: Optional. Advanced options to pass directly to the underlyingaddEventListenercall. Use this to control behaviors likecapture,passive, oronce.
KeyCombinationConfig
keys: KeyCombinationTrigger | KeyCombinationTrigger[](required): Defines the key(s). Can be a string ("ctrl+s"), a shorthandStandardKey(Keys.Escape), an object ({ key: Keys.S, ctrlKey: true }), or an array of these.
type KeyCombinationTrigger = {
key: StandardKey;
ctrlKey?: boolean;
altKey?: boolean;
shiftKey?: boolean;
metaKey?: boolean;
} | StandardKey | string;KeySequenceConfig
sequence: string | StandardKey[](required): An array ofStandardKeyvalues or a string representation (e.g.,"g -> i").sequenceTimeoutMs?: number: Optional. Maximum time (in milliseconds) allowed between consecutive key presses in the sequence.
Key Matching & Normalization
- Case Insensitivity: The library automatically handles case for you.
keys: "a"will match both "a" and "A" presses.keys: "escape"will match an event whereevent.keyis"Escape". - Aliases: Common aliases are supported in string definitions, such as
cmdforMeta,optionforAlt, andescforEscape. - Special Keys: For full type-safety, it is recommended to use the exported
Keysobject (e.g.,Keys.Enter,Keys.ArrowUp).
Contributing
Contributions are welcome! Please feel free to submit issues, fork the repository, and create pull requests.
Development Setup
- Clone the repository.
- Install dependencies:
npm install. - Run tests:
npm test.
License
This project is licensed under the MIT License.
