@accelint/hotkey-manager
v1.0.1
Published
A React-focused global hotkey system that prevents conflicts and manages keyboard shortcuts efficiently.
Maintainers
Keywords
Readme
@accelint/hotkey-manager
A React-focused global hotkey system that prevents conflicts and manages keyboard shortcuts efficiently.
Features
- Singleton Management: Each hotkey is bound only once, regardless of how many components use it
- Automatic Cleanup: Hotkeys are automatically removed when components unmount
- Conflict Prevention: Ensures each keyboard shortcut triggers only one action
- Cross-Platform Support: Adapts to both Windows and macOS keyboard conventions
- Input Field Safety: Disables hotkeys when typing in forms and text fields
- SSR Compatible: Works with Server-Side Rendering and React Server Components
- OS Hotkey Detection: Identifies conflicts with system shortcuts (Coming Soon)
Quick Start
The hotkey manager works with React's component lifecycle. Hotkeys are bound when components mount and only unbound when all components using that hotkey unmount. This reference-counting approach means a hotkey remains active as long as at least one component needs it:
// 1. Enable the hotkey bindings
// src/apps/layout.tsx
import { globalBind } from '@accelint/hotkey-manager';
globalBind();// 2. Register your hotkey and define what it does
// src/hotkeys/toggle-grid.ts
import { registerHotkey, Keycode } from '@accelint/hotkey-manager';
import { gridStore } from '../stores/grid';
export const toggleGridManager = registerHotkey({
id: 'toggle-grid',
key: {
code: Keycode.KeyG
},
// Using onKeyUp is recommended for most simple actions
onKeyUp: () => gridStore().toggleVisibility(),
});// 3. Then use the hotkey in your component
// src/layers/grid-layer.tsx
import { useHotkey } from '@accelint/hotkey-manager';
import { toggleGridManager } from '../hotkeys/toggle-grid';
export function GridLayer() {
// This activates the hotkey when the component mounts
// and deactivates it when unmounted
useHotkey(toggleGridManager);
// Rest of component code...
}How to Use
Key Concept: Shared Hotkey Instances
An important feature of this library is that it maintains a single instance of each hotkey, even when multiple components use it. For example:
- If
ComponentAandComponentBboth use the same hotkey manager (liketoggleGridManager) - The hotkey is bound when the first component mounts
- It remains active even if one component unmounts
- It's only fully unbound when all components using it unmount
This reference-counting system ensures efficient hotkey management and prevents duplicate listeners.
Step 1: Register Your Hotkey
Use registerHotkey to create a reusable hotkey manager:
import { registerHotkey, Keycode } from '@accelint/hotkey-manager';
export const toggleTagsManager = registerHotkey({
id: 'toggle-tags', // An identifier for this hotkey
key: {
code: Keycode.KeyT, // The 'T' key
alt: true // Must press Alt+T
},
onKeyUp: () => someAction() // Function to run when key is released
});Step 2: Use the Hotkey in Components
Use useHotkey with your manager to automatically handle binding and unbinding:
import { useHotkey } from '@accelint/hotkey-manager';
import { toggleTagsManager } from '../hotkeys/toggle-tags';
function TextLabelLayer() {
// This activates the Alt+T hotkey
useHotkey(toggleTagsManager);
return (
// DeckGL Layer...
);
}Manual Control
For more control, useHotkey returns the manager with methods for manual binding:
import { useHotkey } from '@accelint/hotkey-manager';
import { toggleTagsManager } from '../hotkeys/toggle-tags';
function ConditionalHotkeyComponent() {
const manager = useHotkey(toggleTagsManager);
const [isEnabled, setIsEnabled] = useState(false);
useEffect(() => {
if (isEnabled) {
manager.forceBind();
} else {
manager.forceUnbind();
}
}, [isEnabled, manager]);
return (
<div>
<button onClick={() => setIsEnabled(!isEnabled)}>
{isEnabled ? 'Disable' : 'Enable'} Hotkey
</button>
</div>
);
}Key Combinations
Basic Keys
The key property defines what keyboard input triggers your hotkey. The code field is required and uses standard keyboard event codes (available in the Keycode enum).
// Define a simple 'T' key hotkey
key: {
code: Keycode.KeyT
}Using code instead of character values ensures consistency across keyboard layouts and languages.
Modifier Keys
Add modifiers to create key combinations:
// Define Shift+T
key: {
code: Keycode.KeyT,
shift: true
}
// Define Alt+T
key: {
code: Keycode.KeyT,
alt: true
}
// Define Shift+Alt+T
key: {
code: Keycode.KeyT,
alt: true,
shift: true
}Available modifiers:
shift: The Shift keyalt: The Alt key (Option on macOS)ctrl: The Control keymeta: The Windows key or Command (⌘) key on macOS
Multiple Key Bindings
Bind the same action to multiple keys by using an array:
key: [
{ code: Keycode.KeyS, ctrl: true }, // Ctrl+S
{ code: Keycode.F12 } // F12
]Your action receives information about which key triggered it:
onKeyDown: (event, key) => {
if (key.code === Keycode.KeyS) {
// Ctrl+S was pressed
} else if (key.code === Keycode.F12) {
// F12 was pressed
}
}OS and Browser Conflicts
Be cautious when using ctrl and meta key combinations, as many of these are reserved by operating systems and browsers. The hotkey manager will still attempt to handle these combinations, but the OS or browser may intercept them before your application receives the event. When possible, prefer combinations with alt or custom keys that don't conflict with system shortcuts.
Platform Adaptation
Automatic macOS Conversion
By default, the library automatically converts Windows-style shortcuts to macOS conventions:
ctrl+sbecomescmd+son macOSmeta+sbecomesctrl+son macOS
To disable this behavior for a specific hotkey:
key: {
code: Keycode.KeyS,
ctrl: true,
autoMacStyle: false // Keep as Ctrl+S on all platforms
}Custom Platform Shortcuts
For more control over platform differences:
import { registerHotkey, Keycode, isMac } from '@accelint/hotkey-manager';
export const platformSaveManager = registerHotkey({
id: 'platform-save',
key: isMac
? { code: Keycode.KeyS, meta: true, autoMacStyle: false } // Cmd+S on macOS
: { code: Keycode.KeyS, ctrl: true, autoMacStyle: false }, // Ctrl+S on other platforms
onKeyUp: () => someAction()
});Actions
You can define up to three types of actions for each hotkey:
onKeyDown: Triggered immediately when the key is pressedonKeyHeld: Triggered when the key is held down for a certain timeonKeyUp: Triggered when the key is released
Important Behavior: By default, if both
onKeyHeldandonKeyUpare defined for a hotkey and the held action is triggered, theonKeyUpaction will not fire when the key is released. You can override this with thealwaysTriggerKeyUp: trueoption.
Best Practice: Use
onKeyUprather thanonKeyDownfor simple actions. This maintains UX consistency withonKeyHeldbehavior, as most actions should happen either on long press or release, not on initial press.
Example with All Actions
export const zoomManager = registerHotkey({
id: 'zoom',
key: { code: Keycode.KeyZ },
// Triggered on initial press
onKeyDown: () => startZoom(),
// Triggered after holding for 1 second (default)
onKeyHeld: () => activateContinuousZoom(),
// Triggered on release (unless onKeyHeld fired)
onKeyUp: () => finishZoom(),
// Configure hold detection time (in milliseconds)
heldThresholdMs: 500,
// Also trigger onKeyUp even if onKeyHeld was triggered
alwaysTriggerKeyUp: true
})Using in Libraries
When including this package in a library:
- List
@accelint/hotkey-manageras a peer dependency - Allow users to customize your hotkey bindings
- Avoid hardcoding hotkeys that might conflict with common OS shortcuts
This ensures only one hotkey manager instance exists in the application, preventing conflicts between different libraries.
