npm package: @nkemtasoft-react/keybindings
Description: A customizable React hook for handling keyboard events. Features dynamic binding management, modifier key support, input field prevention, and is built with TypeScript for excellent developer experience.
Author: Nkemtasoft
Latest version: 1.0.2

# 🎹 useKeyboardBindings

[![npm version](https://badge.fury.io/js/@nkemtasoft-react/keybindings.svg)](https://badge.fury.io/js/@nkemtasoft-react/keybindings)
[![codecov](https://codecov.io/gh/nkemtasoft-react/keybindings/branch/main/graph/badge.svg)](https://codecov.io/gh/nkemtasoft-react/keybindings)
[![Build Status](https://github.com/nkemtasoft-react/keybindings/workflows/CI/badge.svg)](https://github.com/nkemtasoft-react/keybindings/actions)
[![TypeScript](https://img.shields.io/badge/%3C%2F%3E-TypeScript-%230074c1.svg)](http://www.typescriptlang.org/)
[![Bundle Size](https://img.shields.io/bundlephobia/minzip/@nkemtasoft-react/keybindings)](https://bundlephobia.com/package/@nkemtasoft-react/keybindings)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)

A powerful, lightweight React hook for managing dynamic keyboard shortcuts and key bindings with TypeScript support. Perfect for building keyboard-driven applications, shortcuts, and hotkey systems.

## ✨ Features

-   🚀 **Dynamic Management** - Add, remove, update, enable/disable bindings at runtime
-   ⌨️ **Full Modifier Support** - Ctrl, Shift, Alt, Meta (Cmd) keys with smart aliases
-   🎯 **Smart Input Filtering** - Automatically prevents conflicts in text inputs/textareas
-   🎨 **Flexible Targeting** - Document-wide or specific element targeting via refs
-   🔧 **Event Control** - Fine-grained preventDefault and stopPropagation control
-   🌐 **Global Controls** - Enable/disable all bindings with a single toggle
-   📝 **TypeScript First** - Complete type safety and IntelliSense support
-   🪶 **Lightweight** - Under 4KB minified, zero dependencies (except React)
-   ⚡ **Optimized Performance** - Efficient event handling with minimal re-renders
-   🔍 **Developer Friendly** - Comprehensive debugging and introspection methods

## 📦 Installation

```bash
npm install @nkemtasoft-react/keybindings
```

```bash
yarn add @nkemtasoft-react/keybindings
```

```bash
pnpm add @nkemtasoft-react/keybindings
```

## 🚀 Quick Start

```tsx
import React, { useState } from "react";
import { useKeyboardBindings } from "@nkemtasoft-react/keybindings";

function MyApp() {
    const [message, setMessage] = useState("");

    const { addBinding, removeBinding } = useKeyboardBindings({
        // Initial bindings
        "ctrl+s": () => setMessage("Saved!"),
        "ctrl+n": () => setMessage("New document"),
        esc: () => setMessage(""),
    });

    // Add bindings dynamically
    const addCustomBinding = () => {
        addBinding("ctrl+k", () => setMessage("Custom shortcut!"));
    };

    return (
        <div>
            <p>{message}</p>
            <button onClick={addCustomBinding}>Add Ctrl+K shortcut</button>
            <p>Try: Ctrl+S, Ctrl+N, or Escape</p>
        </div>
    );
}
```

## 📚 Advanced Examples

### 🎯 Text Editor with Multiple Shortcuts

```tsx
import React, { useState, useRef } from "react";
import { useKeyboardBindings } from "@nkemtasoft-react/keybindings";

function TextEditor() {
    const [content, setContent] = useState("");
    const [saved, setSaved] = useState(false);
    const editorRef = useRef<HTMLDivElement>(null);

    const { addBinding, disableAllBindings, enableAllBindings, bindings } = useKeyboardBindings(
        {
            // File operations
            "ctrl+s": () => {
                console.log("Saving...", content);
                setSaved(true);
                setTimeout(() => setSaved(false), 2000);
            },
            "ctrl+n": () => {
                setContent("");
                setSaved(false);
            },
            "ctrl+o": () => {
                // Open file logic
                console.log("Opening file...");
            },

            // Editing operations
            "ctrl+z": () => console.log("Undo"),
            "ctrl+y": () => console.log("Redo"),
            "ctrl+a": () => console.log("Select all"),

            // Navigation
            "ctrl+g": () => console.log("Go to line"),
            "ctrl+f": () => console.log("Find"),
            f3: () => console.log("Find next"),

            // View
            "ctrl+=": () => console.log("Zoom in"),
            "ctrl+-": () => console.log("Zoom out"),
            f11: () => console.log("Toggle fullscreen"),
        },
        {
            // Global options
            preventDefault: true,
            target: editorRef, // Scope to editor only
        },
    );

    return (
        <div ref={editorRef} className="editor">
            <div className="toolbar">
                <span className={saved ? "saved" : "unsaved"}>{saved ? "✅ Saved" : "⚠️ Unsaved"}</span>
                <button onClick={() => disableAllBindings()}>Disable Shortcuts</button>
                <button onClick={() => enableAllBindings()}>Enable Shortcuts</button>
            </div>

            <textarea
                value={content}
                onChange={(e) => setContent(e.target.value)}
                placeholder="Start typing... (shortcuts work outside this textarea)"
                className="content"
            />

            <div className="shortcuts">
                <h3>Active Shortcuts:</h3>
                {Object.entries(bindings).map(([combo, binding]) => (
                    <span key={combo} className={`shortcut ${binding.enabled ? "enabled" : "disabled"}`}>
                        {combo}
                    </span>
                ))}
            </div>
        </div>
    );
}
```

### 🎮 Game Controls with Dynamic Binding

```tsx
import React, { useState, useEffect } from "react";
import { useKeyboardBindings } from "@nkemtasoft-react/keybindings";

function GameController() {
    const [score, setScore] = useState(0);
    const [gameState, setGameState] = useState<"menu" | "playing" | "paused">("menu");

    const { addBinding, removeBinding, updateBinding, disableAllBindings, enableAllBindings } = useKeyboardBindings();

    // Dynamic controls based on game state
    useEffect(() => {
        if (gameState === "menu") {
            addBinding("space", () => setGameState("playing"));
            addBinding("enter", () => setGameState("playing"));
            removeBinding("p"); // Remove pause in menu
        } else if (gameState === "playing") {
            // Game controls
            addBinding("w", () => console.log("Move up"));
            addBinding("a", () => console.log("Move left"));
            addBinding("s", () => console.log("Move down"));
            addBinding("d", () => console.log("Move right"));
            addBinding("space", () => console.log("Jump/Action"));
            addBinding("p", () => setGameState("paused"));
            addBinding("esc", () => setGameState("menu"));
        } else if (gameState === "paused") {
            disableAllBindings(); // Disable game controls
            addBinding("p", () => setGameState("playing"));
            addBinding("esc", () => setGameState("menu"));
        }
    }, [gameState]);

    // Power-up: temporarily change controls
    const activatePowerUp = () => {
        updateBinding("space", () => {
            console.log("SUPER JUMP!");
            setScore((prev) => prev + 100);
        });

        // Revert after 5 seconds
        setTimeout(() => {
            updateBinding("space", () => console.log("Normal jump"));
        }, 5000);
    };

    return (
        <div className="game">
            <div className="hud">
                <div>Score: {score}</div>
                <div>State: {gameState}</div>
            </div>

            {gameState === "menu" && (
                <div className="menu">
                    <h1>Game Menu</h1>
                    <p>Press SPACE or ENTER to start</p>
                </div>
            )}

            {gameState === "playing" && (
                <div className="game-area">
                    <p>Use WASD to move, SPACE to jump</p>
                    <p>Press P to pause, ESC for menu</p>
                    <button onClick={activatePowerUp}>Activate Super Jump</button>
                </div>
            )}

            {gameState === "paused" && (
                <div className="pause-menu">
                    <h2>PAUSED</h2>
                    <p>Press P to continue, ESC for menu</p>
                </div>
            )}
        </div>
    );
}
```

### 🔧 Advanced Configuration

```tsx
import { useKeyboardBindings } from "@nkemtasoft-react/keybindings";

function AdvancedExample() {
    const containerRef = useRef<HTMLDivElement>(null);

    const { addBinding, updateBinding, getBinding, hasBinding, toggleBinding } = useKeyboardBindings(
        {
            // Initial bindings with individual options
            "ctrl+shift+d": () => console.log("Debug mode"),
        },
        {
            // Global options
            preventDefault: false, // Don't prevent default globally
            stopPropagation: true, // Stop propagation globally
            disableSingleFromTextInput: false, // Allow single keys in inputs
            target: containerRef, // Scope to specific element
            globalDisabled: false, // Start enabled
        },
    );

    // Add binding with specific options
    const addCustomBinding = () => {
        addBinding(
            "alt+enter",
            (event) => {
                console.log("Custom action with event:", event);
            },
            {
                preventDefault: true, // Override global setting
                stopPropagation: false, // Override global setting
                disableSingleFromTextInput: true, // Don't trigger in inputs
            },
        );
    };

    // Conditional binding management
    const toggleDebugMode = () => {
        if (hasBinding("f12")) {
            removeBinding("f12");
        } else {
            addBinding("f12", () => {
                const binding = getBinding("ctrl+shift+d");
                console.log("Debug binding enabled:", binding?.enabled);
            });
        }
    };

    return (
        <div ref={containerRef}>
            <button onClick={addCustomBinding}>Add Alt+Enter</button>
            <button onClick={toggleDebugMode}>Toggle F12 Debug</button>
            <button onClick={() => toggleBinding("ctrl+shift+d")}>Toggle Debug Binding</button>
        </div>
    );
}
```

## 📖 API Reference

### Hook Signature

```typescript
const bindings = useKeyboardBindings(
  initialBindings?: Record<string, (event: KeyboardEvent) => void>,
  options?: HookOptions
);
```

### Options

```typescript
interface HookOptions {
    preventDefault?: boolean; // Default: true
    stopPropagation?: boolean; // Default: false
    disableSingleFromTextInput?: boolean; // Default: true
    target?: Document | React.RefObject<HTMLElement>; // Default: document
    globalDisabled?: boolean; // Default: false
}

interface BindingOptions {
    preventDefault?: boolean;
    stopPropagation?: boolean;
    disableSingleFromTextInput?: boolean;
}
```

### Return Value

The hook returns an object with these methods and properties:

#### Binding Management

```typescript
// Add a new binding
addBinding(combination: string, callback: (event: KeyboardEvent) => void, options?: BindingOptions): void;

// Remove a binding
removeBinding(combination: string): void;

// Update existing binding or create new one
updateBinding(combination: string, callback: (event: KeyboardEvent) => void, options?: BindingOptions): void;
```

#### Enable/Disable Controls

```typescript
// Enable specific binding
enableBinding(combination: string): void;

// Disable specific binding
disableBinding(combination: string): void;

// Toggle binding state
toggleBinding(combination: string): void;

// Bulk operations
enableAllBindings(): void;
disableAllBindings(): void;
clearAllBindings(): void;
```

#### Query Methods

```typescript
// Get binding details
getBinding(combination: string): Binding | null;

// Get all bindings
getAllBindings(): Record<string, Binding>;

// Check if binding exists
hasBinding(combination: string): boolean;

// Read-only bindings object (without callbacks)
bindings: Record<string, Omit<Binding, "callback">>;
```

### Key Combination Format

The hook supports various key combination formats:

#### Modifier Keys

-   `ctrl` or `control` - Control key
-   `shift` - Shift key
-   `alt` - Alt key
-   `meta`, `cmd`, or `command` - Meta/Command key (⌘ on Mac)

#### Special Keys

-   `space` - Space bar
-   `esc` or `escape` - Escape key
-   `enter` - Enter key
-   `tab` - Tab key
-   `up`, `down`, `left`, `right` - Arrow keys
-   `f1` through `f12` - Function keys

#### Examples

```typescript
"ctrl+s"; // Ctrl + S
"cmd+shift+z"; // Cmd + Shift + Z (Mac)
"alt+f4"; // Alt + F4
"ctrl+shift+alt+d"; // All modifiers + D
"space"; // Just spacebar
"esc"; // Just escape
"f11"; // Just F11
"+"; // Plus key (special handling)
"ctrl++"; // Ctrl + Plus
```

## 🎯 Key Features Explained

### 🛡️ Smart Input Filtering

By default, single key bindings (without modifiers) are disabled in text inputs to prevent interference with typing:

```typescript
// ✅ These work in text inputs (have modifiers)
"ctrl+s"; // Save
"alt+tab"; // Switch
"meta+c"; // Copy

// ❌ These are blocked in text inputs (no modifiers)
"space"; // Would interfere with typing
"enter"; // Would interfere with form submission
"a"; // Would interfere with typing

// 🔧 Override per binding
addBinding("enter", handleSubmit, {
    disableSingleFromTextInput: false, // Allow in inputs
});
```

### 🎯 Flexible Targeting

Scope bindings to specific elements:

```typescript
const editorRef = useRef<HTMLDivElement>(null);

// Only listen for events within the editor
const { addBinding } = useKeyboardBindings(
    {},
    {
        target: editorRef,
    },
);

// Document-wide (default)
const { addBinding } = useKeyboardBindings(
    {},
    {
        target: document, // or omit for default
    },
);
```

### ⚡ Performance Optimized

-   **Minimal re-renders** - Callbacks are memoized and stable
-   **Efficient event handling** - Single event listener with smart matching
-   **Memory management** - Proper cleanup on unmount
-   **Bundle size** - Under 4KB minified

### 🔧 Event Control

Fine-grained control over event behavior:

```typescript
addBinding("ctrl+s", handleSave, {
    preventDefault: true, // Prevent browser save dialog
    stopPropagation: false, // Allow event to bubble
});
```

## 🧪 Testing

The library includes comprehensive test coverage:

```bash
npm test              # Run tests
npm run test:coverage # Coverage report
npm run test:watch    # Watch mode
```

### Test Coverage

-   ✅ **98%+ Code Coverage** across all features
-   ✅ **Unit Tests** for all hook methods
-   ✅ **Integration Tests** with React components
-   ✅ **Performance Tests** for large binding sets
-   ✅ **Edge Case Testing** for error scenarios

## 🏗️ Browser Support

-   ✅ Chrome 61+
-   ✅ Firefox 60+
-   ✅ Safari 10.1+
-   ✅ Edge 16+
-   ✅ React 16.8+ (Hooks support)

### Development Setup

```bash
git clone https://github.com/nkemtasoft-react/keybindings
cd keybindings
npm install

# Development
npm run test:watch    # Run tests in watch mode
npm run build     # Build development version
```

## 📄 License

MIT © [Nkemtasoft](https://nkemtasoft.org)

---

## 🌟 Star History

If this library helps you build awesome keyboard-driven experiences, please consider giving it a star! ⭐

[![Star History Chart](https://api.star-history.com/svg?repos=nkemtasoft-react/keybindings&type=Date)](https://star-history.com/#nkemtasoft-react/keybindings&Date)

---

<details>
<summary>📊 Package Stats</summary>

-   **Bundle Size**: < 4KB minified
-   **Dependencies**: Zero (except React peer dependency)
-   **TypeScript**: Full support with complete type definitions
-   **Test Coverage**: 98%+
-   **Build**: Dual CJS/ESM output with source maps
-   **Performance**: Optimized for 1000+ bindings

</details>