react-keyhub
Version:
A lightweight, scalable keyboard shortcut manager for React applications with TypeScript support
667 lines (530 loc) • 16.8 kB
Markdown
# React KeyHub
A lightweight, scalable keyboard shortcut manager for React applications with TypeScript support.
[](https://www.npmjs.com/package/react-keyhub)
[](https://www.npmjs.com/package/react-keyhub)
[](https://github.com/xenral/react-keyhub/blob/main/LICENSE)
[](https://github.com/xenral/react-keyhub/actions/workflows/ci.yml)
## Demo
- 🔗 **Live Shortcut Sheet Demo**: [KeyHub Example App](https://xenral.github.io/keyhub-example)
- 💻 **Example Source Code**: [GitHub Repository](https://github.com/xenral/keyhub-example)
## Features
- 🔑 **Central Configuration**: Define all keyboard shortcuts in one place
- 🔄 **Type Safety**: Full TypeScript support for shortcut definitions and hooks
- 🎯 **Optimized Performance**: Single event listener with efficient lookup
- 🧩 **Modular API**: Subscribe to shortcuts from any component
- 📋 **Built-in Shortcut Sheet**: Display all registered shortcuts in a user-friendly format
- 🔌 **Zero Dependencies**: No external dependencies (aside from React)
- 🔄 **Dynamic Updates**: Enable, disable, or modify shortcuts at runtime
- 🌐 **Context Awareness**: Define shortcuts that only work in specific contexts
- 🔢 **Sequence Support**: Create shortcuts that require a sequence of key presses
- 🎨 **Theming Support**: Light, dark, and auto themes for the shortcut sheet
- 📱 **Responsive Layouts**: Modal and sidebar layouts for the shortcut sheet
- 💡 **Type Suggestions**: Enhanced hooks with autocomplete for registered shortcuts
## Installation
```bash
npm install react-keyhub
# or
yarn add react-keyhub
```
## Quick Start
```tsx
import React, { useState } from 'react';
import {
KeyHubProvider,
useShortcut,
useKeyboardShortcut,
ShortcutSheet,
defaultShortcuts
} from 'react-keyhub';
// Define your shortcuts (or use the default ones)
const myShortcuts = {
...defaultShortcuts,
customAction: {
keyCombo: 'ctrl+k',
name: 'Custom Action',
description: 'Perform a custom action',
scope: 'global',
priority: 100,
status: 'enabled',
group: 'Custom',
type: 'regular'
},
};
// Your app component
function App() {
const [isShortcutSheetOpen, setShortcutSheetOpen] = useState(false);
// Use the enhanced hook with type suggestions
const isSaveRegistered = useShortcut('save', (e) => {
console.log('Save triggered!');
// Your save logic here
});
// You can also use the backward compatibility hooks
useKeyboardShortcut('customAction', (e) => {
console.log('Custom action triggered!');
// Your custom action logic here
});
// Toggle the shortcut sheet
useShortcut('showShortcuts', () => {
setShortcutSheetOpen(prev => !prev);
});
return (
<div>
<h1>My App</h1>
<p>Press Ctrl+/ to see all shortcuts</p>
<p>Save shortcut registered: {isSaveRegistered ? 'Yes' : 'No'}</p>
{/* Shortcut Sheet */}
<ShortcutSheet
isOpen={isShortcutSheetOpen}
onClose={() => setShortcutSheetOpen(false)}
/>
</div>
);
}
// Wrap your app with the provider
function Root() {
return (
<KeyHubProvider shortcuts={myShortcuts}>
<App />
</KeyHubProvider>
);
}
export default Root;
```
## API Reference
### `KeyHubProvider`
The provider component that makes shortcuts available throughout your application.
```tsx
<KeyHubProvider
shortcuts={myShortcuts}
options={{
preventDefault: true,
stopPropagation: true,
debounceTime: 0,
sequenceTimeout: 1000,
ignoreInputFields: true,
ignoreModifierOnlyEvents: true
}}
>
{children}
</KeyHubProvider>
```
#### Props
- `shortcuts`: A record of shortcut configurations
- `options` (optional):
- `preventDefault`: Whether to prevent the default browser behavior (default: `true`)
- `stopPropagation`: Whether to stop event propagation (default: `true`)
- `target`: The element to attach the event listener to (default: `document`)
- `debounceTime`: Debounce time in milliseconds (default: `0`)
- `sequenceTimeout`: Timeout for sequence shortcuts in milliseconds (default: `1000`)
- `ignoreInputFields`: Whether to ignore keyboard events from input fields (default: `true`)
- `ignoreModifierOnlyEvents`: Whether to ignore keyboard events that only contain modifier keys (default: `true`)
### `useShortcut`
A hook to subscribe to a keyboard shortcut with type suggestions.
```tsx
// The shortcutId will have type suggestions for all registered shortcuts
// TypeScript will show an error for non-existent shortcuts
const isSaveRegistered = useShortcut('save', (e) => {
console.log('Save shortcut triggered!');
// Your save logic here
});
// The hook returns a boolean indicating if the shortcut is registered
console.log('Is save shortcut registered?', isSaveRegistered);
```
#### Parameters
- `shortcutId`: The ID of the shortcut to subscribe to (with type suggestions based on provider shortcuts)
- `callback`: The callback to execute when the shortcut is triggered
#### Return Value
- `boolean`: Indicates if the shortcut is registered
#### Type Safety
The hook uses the actual shortcuts provided to the KeyHubProvider for type checking:
```tsx
import {
ShortcutScope,
ShortcutStatus,
ShortcutType,
ShortcutSettings
} from 'react-keyhub';
// Define custom shortcuts
const myShortcuts = {
...defaultShortcuts,
customAction: {
keyCombo: 'ctrl+shift+c',
name: 'Custom Action',
description: 'A custom action shortcut',
scope: ShortcutScope.GLOBAL,
priority: 100,
status: ShortcutStatus.ENABLED,
group: 'Custom',
type: ShortcutType.REGULAR
}
} as ShortcutSettings;
// In your component
function MyComponent() {
// This will work fine
useShortcut('customAction', () => {});
// This will cause a TypeScript error
useShortcut('nonExistentShortcut', () => {});
}
```
#### Error Handling
The hook checks if the shortcut is registered and provides a warning if it's not:
```tsx
// This will log a warning if 'nonExistentShortcut' is not registered
useShortcut('nonExistentShortcut', (e) => {});
// Warning: Shortcut "nonExistentShortcut" is not registered. Available shortcuts: save, saveAs, print, ...
```
### `useKeyboardShortcut` and `useKey`
For backward compatibility, `useKeyboardShortcut` and `useKey` are also available as aliases for `useShortcut`:
```tsx
// These are all equivalent
useShortcut('save', callback);
useKeyboardShortcut('save', callback);
useKey('save', callback);
```
### `AvailableShortcuts`
A type that provides suggestions for all registered shortcuts based on what's provided to the KeyHubProvider:
```tsx
import { AvailableShortcuts } from 'react-keyhub';
// This will have type suggestions for all registered shortcuts
// based on what's provided to the KeyHubProvider
const shortcutId: AvailableShortcuts = 'save';
// If you've added a custom shortcut, it will be included in the suggestions
const customShortcutId: AvailableShortcuts = 'customAction'; // Works if customAction is registered
```
### `getRegisteredShortcuts`
A function to get all registered shortcuts from the current provider:
```tsx
import { getRegisteredShortcuts } from 'react-keyhub';
function MyComponent() {
// This will return the shortcuts from the current provider
const shortcuts = getRegisteredShortcuts();
return (
<div>
<h2>Registered Shortcuts</h2>
<ul>
{Object.entries(shortcuts).map(([id, config]) => (
<li key={id}>
{id}: {config.name} - {config.type === 'regular' ? config.keyCombo : config.sequence}
</li>
))}
</ul>
</div>
);
}
```
### `useShortcutSheet`
A hook to get all registered shortcuts.
```tsx
const shortcuts = useShortcutSheet();
```
### `useShortcutStatus`
A hook to enable or disable a shortcut.
```tsx
useShortcutStatus('save', true); // Enable the "save" shortcut
useShortcutStatus('save', false); // Disable the "save" shortcut
```
### `useShortcutUpdate`
A hook to update a shortcut configuration.
```tsx
useShortcutUpdate('save', {
keyCombo: 'ctrl+shift+s',
priority: 200,
});
```
### `useShortcutRegister`
A hook to register a new shortcut dynamically.
```tsx
useShortcutRegister('myDynamicShortcut', {
keyCombo: 'ctrl+d',
name: 'Dynamic Shortcut',
description: 'A dynamically registered shortcut',
scope: 'global',
priority: 100,
status: 'enabled',
group: 'Dynamic',
type: 'regular'
});
```
### `useShortcutContext`
A hook to set the active context.
```tsx
useShortcutContext('editor'); // Set the active context to "editor"
useShortcutContext(null); // Clear the active context
```
### `useShortcutPause`
A hook to pause and resume the event bus.
```tsx
useShortcutPause(true); // Pause all shortcuts
useShortcutPause(false); // Resume all shortcuts
```
### `useShortcutGroups`
A hook to get all shortcut groups.
```tsx
const groups = useShortcutGroups(); // Returns an array of group names
```
### `useShortcutsByGroup`
A hook to get shortcuts by group.
```tsx
const fileShortcuts = useShortcutsByGroup('File'); // Returns all shortcuts in the "File" group
```
### `useKeyHub`
A hook to access the KeyHub event bus directly.
```tsx
const eventBus = useKeyHub();
// Now you can use the event bus methods
eventBus.on('save', callback);
eventBus.off(subscriptionId);
eventBus.enableShortcut('save');
eventBus.disableShortcut('save');
eventBus.updateShortcut('save', { priority: 200 });
eventBus.registerShortcut('myShortcut', { ... });
eventBus.unregisterShortcut('myShortcut');
eventBus.setContext('editor');
eventBus.getContext();
eventBus.pause();
eventBus.resume();
eventBus.getShortcuts();
eventBus.getShortcutsByGroup('File');
eventBus.getShortcutGroups();
```
### `ShortcutSheet`
A component to display all registered shortcuts.
```tsx
<ShortcutSheet
isOpen={isOpen}
onClose={handleClose}
theme="light" // 'light', 'dark', or 'auto'
layout="modal" // 'modal' or 'sidebar'
filter={{
scope: 'global',
search: 'save',
group: 'File',
context: 'editor'
}}
/>
```
#### Props
- `isOpen`: Whether to show the shortcut sheet
- `onClose`: Callback to close the shortcut sheet
- `theme` (optional): Theme for the shortcut sheet (`'light'`, `'dark'`, or `'auto'`)
- `layout` (optional): Layout for the shortcut sheet (`'modal'` or `'sidebar'`)
- `filter` (optional): Filter for the shortcuts to display
- `scope` (optional): Filter by scope (`'global'` or `'local'`)
- `search` (optional): Filter by search term
- `group` (optional): Filter by group
- `context` (optional): Filter by context
- `className` (optional): Custom class name for the shortcut sheet
### `ShortcutSheetStyles`
A string of CSS styles for the ShortcutSheet component.
```tsx
import { ShortcutSheetStyles } from 'react-keyhub';
// Add the styles to your app
const App = () => (
<>
<style>{ShortcutSheetStyles}</style>
{/* Your app content */}
</>
);
```
## Shortcut Configuration
Each shortcut is defined with the following properties:
### Regular Shortcut
```tsx
{
keyCombo: 'ctrl+s',
name: 'Save',
description: 'Save the current document',
scope: 'global',
priority: 100,
status: 'enabled',
group: 'File',
context: 'editor', // Optional
type: 'regular',
action: (e) => { /* Optional default action */ }
}
```
### Sequence Shortcut
```tsx
{
sequence: 'g c', // 'g' followed by 'c'
name: 'Git Commands',
description: 'Show git commands menu',
scope: 'global',
priority: 100,
status: 'enabled',
group: 'Git',
context: 'editor', // Optional
type: 'sequence',
action: (e) => { /* Optional default action */ }
}
```
### Properties
- `keyCombo` (for regular shortcuts): The key combination (e.g., `'ctrl+s'`, `'ctrl+shift+n'`)
- `sequence` (for sequence shortcuts): The sequence of key combinations (e.g., `'g c'` for "g" followed by "c")
- `name`: A human-readable name for the shortcut
- `description`: A detailed description of what the shortcut does
- `scope`: Either `'global'` or `'local'`
- `priority`: The priority of the shortcut (higher numbers take precedence)
- `status`: Either `'enabled'` or `'disabled'`
- `group`: A group for the shortcut (used for organizing in the shortcut sheet)
- `context` (optional): A context for the shortcut (only active when the context matches)
- `type`: Either `'regular'` or `'sequence'`
- `action` (optional): A default action to execute when the shortcut is triggered
## Default Shortcuts
React KeyHub comes with a set of default shortcuts organized by groups:
### File Operations
- `save`: Ctrl+S
- `saveAs`: Ctrl+Shift+S
- `print`: Ctrl+P
- `newWindow`: Ctrl+Shift+N
### Edit Operations
- `find`: Ctrl+F
- `replace`: Ctrl+H
- `undo`: Ctrl+Z
- `redo`: Ctrl+Y
- `cut`: Ctrl+X
- `copy`: Ctrl+C
- `paste`: Ctrl+V
- `selectAll`: Ctrl+A
### Navigation
- `goToLine`: Ctrl+G
- `goToFile`: Ctrl+P (lower priority than print)
### Help
- `help`: F1
- `showShortcuts`: Ctrl+/
### Git (Sequence Shortcuts)
- `gitCommands`: g c (press "g" then "c")
- `gitStatus`: g s (press "g" then "s")
### Vim Navigation (Context-Specific)
- `vimUp`: k (only active in "vim" context)
- `vimDown`: j (only active in "vim" context)
- `vimLeft`: h (only active in "vim" context)
- `vimRight`: l (only active in "vim" context)
You can use these as a starting point and override or extend them as needed.
## Advanced Usage
### Context-Aware Shortcuts
```tsx
// Define shortcuts with contexts
const myShortcuts = {
...defaultShortcuts,
editorSave: {
keyCombo: 'ctrl+s',
name: 'Save',
description: 'Save the current document',
scope: 'global',
priority: 100,
status: 'enabled',
group: 'File',
context: 'editor', // Only active in "editor" context
type: 'regular'
},
terminalClear: {
keyCombo: 'ctrl+l',
name: 'Clear Terminal',
description: 'Clear the terminal',
scope: 'global',
priority: 100,
status: 'enabled',
group: 'Terminal',
context: 'terminal', // Only active in "terminal" context
type: 'regular'
}
};
// In your component, set the active context
function EditorComponent() {
// Set the active context to "editor"
useShortcutContext('editor');
// ...
}
function TerminalComponent() {
// Set the active context to "terminal"
useShortcutContext('terminal');
// ...
}
```
### Sequence Shortcuts
```tsx
// Define sequence shortcuts
const myShortcuts = {
...defaultShortcuts,
gitCommit: {
sequence: 'g c', // Press "g" then "c"
name: 'Git Commit',
description: 'Open git commit dialog',
scope: 'global',
priority: 100,
status: 'enabled',
group: 'Git',
type: 'sequence'
}
};
// Subscribe to the sequence shortcut
function MyComponent() {
useShortcut('gitCommit', () => {
console.log('Git commit dialog opened');
});
// ...
}
```
### Dynamic Shortcut Registration
```tsx
function MyComponent() {
// Register a dynamic shortcut
useShortcutRegister('dynamicShortcut', {
keyCombo: 'ctrl+d',
name: 'Dynamic Shortcut',
description: 'A dynamically registered shortcut',
scope: 'global',
priority: 100,
status: 'enabled',
group: 'Dynamic',
type: 'regular',
action: () => {
console.log('Dynamic shortcut triggered!');
}
});
// The shortcut will be automatically unregistered when the component unmounts
// ...
}
```
### Pausing and Resuming Shortcuts
```tsx
function MyComponent() {
const [isPaused, setIsPaused] = useState(false);
// Pause or resume all shortcuts
useShortcutPause(isPaused);
return (
<div>
<button onClick={() => setIsPaused(!isPaused)}>
{isPaused ? 'Resume Shortcuts' : 'Pause Shortcuts'}
</button>
</div>
);
}
```
### Themed Shortcut Sheet
```tsx
function MyComponent() {
const [isOpen, setIsOpen] = useState(false);
const [theme, setTheme] = useState<'light' | 'dark' | 'auto'>('auto');
return (
<div>
<button onClick={() => setIsOpen(true)}>Show Shortcuts</button>
<select value={theme} onChange={(e) => setTheme(e.target.value as any)}>
<option value="light">Light</option>
<option value="dark">Dark</option>
<option value="auto">Auto (System)</option>
</select>
<ShortcutSheet
isOpen={isOpen}
onClose={() => setIsOpen(false)}
theme={theme}
/>
</div>
);
}
```
## Browser Support
React KeyHub works in all modern browsers that support React.
## License
MIT