UNPKG

react-keyhub

Version:

A lightweight, scalable keyboard shortcut manager for React applications with TypeScript support

667 lines (530 loc) 16.8 kB
# React KeyHub A lightweight, scalable keyboard shortcut manager for React applications with TypeScript support. [![npm version](https://img.shields.io/npm/v/react-keyhub.svg)](https://www.npmjs.com/package/react-keyhub) [![npm downloads](https://img.shields.io/npm/dm/react-keyhub.svg)](https://www.npmjs.com/package/react-keyhub) [![license](https://img.shields.io/npm/l/react-keyhub.svg)](https://github.com/xenral/react-keyhub/blob/main/LICENSE) [![CI](https://github.com/xenral/react-keyhub/actions/workflows/ci.yml/badge.svg)](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