Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions packages/core/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,16 @@
# @keybindy/core

## [2.0.1] - 2026-08-30

### Improvements & Fixes

- **Contextual Typing for Handler Event**:
- Unified `ShortcutHandler` to `(event: KeyboardEvent, state?: HoldState) => void`, ensuring 1-parameter handler callbacks `(event) => ...` infer `KeyboardEvent` with full autocomplete rather than `any`.
- **Per-Shortcut Hooks**:
- Added `beforeEach` and `afterEach` options directly inside `ShortcutOptions`.
- **Sequential Hook Order Preservation**:
- Fixed hook filter matching to preserve exact key order for sequential shortcuts (`['G', 'D']` vs `['D', 'G']`).

## [2.0.0] - 2026-08-30

### Major Features & Improvements
Expand Down
2 changes: 1 addition & 1 deletion packages/core/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@keybindy/core",
"version": "2.0.0",
"version": "2.0.1",
"description": "A lightweight and framework-agnostic keyboard shortcut manager for web apps. Define, register, and handle keybindings with ease.",
"author": {
"name": "PRASSamin",
Expand Down
45 changes: 31 additions & 14 deletions packages/core/src/ShortcutManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import type {
ShortcutOptions,
Shortcut,
ShortcutBinding,
HoldShortcutHandler,
ShortcutManagerOptions,
BeforeEachHook,
AfterEachHook,
Expand Down Expand Up @@ -50,7 +49,12 @@ export class ShortcutManager extends ScopeManager {
options?: HookOptions;
}[] = [];

constructor({ onShortcutFired, silent = false, ignoreInputs = false, scopeMode }: ShortcutManagerOptions = {}) {
constructor({
onShortcutFired,
silent = false,
ignoreInputs = false,
scopeMode,
}: ShortcutManagerOptions = {}) {
super();
this.onShortcutFired = onShortcutFired || (() => {});
this.logger = new Logger({ silent });
Expand Down Expand Up @@ -110,9 +114,9 @@ export class ShortcutManager extends ScopeManager {

// Keys check
if (options.keys && options.keys.length > 0) {
const targetBindings = (
Array.isArray(options.keys[0]) ? options.keys : [options.keys]
) as unknown as Keys[][];
const targetBindings = (Array.isArray(options.keys[0])
? options.keys
: [options.keys]) as unknown as Keys[][];

const isSequential = Boolean(shortcut.options?.sequential);
const normalizeCombo = (combo: Keys[]): string => {
Expand Down Expand Up @@ -151,6 +155,12 @@ export class ShortcutManager extends ScopeManager {
}
}
}
if (shortcut.options?.beforeEach) {
const result = shortcut.options.beforeEach(shortcut, event);
if (result === false) {
return false;
}
}
return true;
}

Expand All @@ -168,6 +178,13 @@ export class ShortcutManager extends ScopeManager {
}
}
}
if (shortcut.options?.afterEach) {
try {
shortcut.options.afterEach(shortcut, event);
} catch (err) {
this.logger.error(err);
}
}
}

/**
Expand Down Expand Up @@ -330,7 +347,7 @@ export class ShortcutManager extends ScopeManager {
if (!this.activeHoldShortcuts.has(bestHold.id)) {
if (this.runBeforeHooks(bestHold, e)) {
if (bestHold.options?.preventDefault) e.preventDefault();
(bestHold.handler as HoldShortcutHandler)(e, 'down');
bestHold.handler(e, 'down');
this.activeHoldShortcuts.add(bestHold.id);
this.onShortcutFired(bestHold);
this.runAfterHooks(bestHold, e);
Expand Down Expand Up @@ -426,10 +443,14 @@ export class ShortcutManager extends ScopeManager {
// Handle hold shortcuts
for (const shortcutId of this.activeHoldShortcuts) {
const shortcut = this.shortcuts.find(s => s.id === shortcutId);
if (shortcut && !this.shouldIgnoreForInput(shortcut, e.target) && shortcut.keys.map(k => k.toLowerCase()).includes(key)) {
if (
shortcut &&
!this.shouldIgnoreForInput(shortcut, e.target) &&
shortcut.keys.map(k => k.toLowerCase()).includes(key)
) {
if (this.runBeforeHooks(shortcut, e)) {
if (shortcut.options?.preventDefault) e.preventDefault();
(shortcut.handler as HoldShortcutHandler)(e, 'up');
shortcut.handler(e, 'up');
this.activeHoldShortcuts.delete(shortcutId);
this.runAfterHooks(shortcut, e);
}
Expand Down Expand Up @@ -481,9 +502,7 @@ export class ShortcutManager extends ScopeManager {
* @param options - Optional configuration including scope, ID, and metadata.
*/
register(binding: ShortcutBinding, handler: ShortcutHandler, options?: ShortcutOptions) {
const bindings = (
Array.isArray(binding[0]) ? binding : [binding]
) as unknown as Keys[][];
const bindings = (Array.isArray(binding[0]) ? binding : [binding]) as unknown as Keys[][];

const id = options?.data?.id || generateUID();
const targetScope = options?.scope || 'global';
Expand Down Expand Up @@ -528,9 +547,7 @@ export class ShortcutManager extends ScopeManager {
* @param scope - The scope in which the shortcut was registered (default: "global").
*/
unregister(keys: ShortcutBinding, scope: string = 'global') {
const bindings = (
Array.isArray(keys[0]) ? keys : [keys]
) as unknown as Keys[][];
const bindings = (Array.isArray(keys[0]) ? keys : [keys]) as unknown as Keys[][];

for (const binding of bindings) {
const expandedCombos = expandAliases(binding as any);
Expand Down
11 changes: 9 additions & 2 deletions packages/core/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -181,12 +181,11 @@ export type Keys =
| 'Brightness Down';

export type HoldState = 'down' | 'up';
export type HoldShortcutHandler = (event: KeyboardEvent, state: HoldState) => void;

/**
* Function to handle a keyboard event triggered by a shortcut.
*/
export type ShortcutHandler = ((event: KeyboardEvent) => void) | HoldShortcutHandler;
export type ShortcutHandler = (event: KeyboardEvent, state?: HoldState) => void;

/**
* Configuration options for a shortcut.
Expand Down Expand Up @@ -240,6 +239,14 @@ export interface ShortcutOptions {
* Takes precedence over manager-level `ignoreInputs: true`.
*/
enableInInput?: boolean;
/**
* Guard hook executed before this specific shortcut runs. Returning `false` aborts execution.
*/
beforeEach?: BeforeEachHook;
/**
* Interceptor hook executed after this specific shortcut runs.
*/
afterEach?: AfterEachHook;
}

/**
Expand Down
11 changes: 11 additions & 0 deletions packages/react/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,16 @@
# @keybindy/react

## [2.0.1] - 2026-08-30

### Improvements & Fixes

- **Isolated Hook Guard & Interceptor Scoping**:
- `useShortcuts` now automatically scopes its `beforeEach` and `afterEach` options specifically to the key combinations defined in that hook call, allowing multiple `useShortcuts` hooks under the same scope to use different guards without cross-interference.
- **Enhanced Contextual Typing**:
- Handlers passed to `useShortcut` and `useShortcuts` strictly infer `(event: KeyboardEvent)` rather than `any`.
- **Synchronized with `@keybindy/core@2.0.1`**:
- Inherits per-shortcut `beforeEach` and `afterEach` options and sequential key order improvements.

## [2.0.0] - 2026-08-30

### Major Features & Improvements
Expand Down
2 changes: 1 addition & 1 deletion packages/react/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@keybindy/react",
"version": "2.0.0",
"version": "2.0.1",
"description": "Keybindy for React: Simple, scoped keyboard shortcuts that require little setup. designed to smoothly blend in with your React applications, allowing for robust keybinding functionality without the overhead.",
"author": {
"name": "PRASSamin",
Expand Down
8 changes: 6 additions & 2 deletions packages/react/src/useShortcuts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -196,16 +196,20 @@ export const useShortcuts = (
let unregisterBefore: (() => void) | undefined;
let unregisterAfter: (() => void) | undefined;

const hookKeys = stableShortcuts.flatMap(s =>
Array.isArray(s.keys[0]) ? (s.keys as any) : [s.keys]
);

if (beforeEach) {
unregisterBefore = manager.beforeEach((shortcut, event) => {
return beforeEachRef.current ? beforeEachRef.current(shortcut, event) : undefined;
}, { scope });
}, { scope, keys: hookKeys.length > 0 ? hookKeys : undefined });
}

if (afterEach) {
unregisterAfter = manager.afterEach((shortcut, event) => {
if (afterEachRef.current) afterEachRef.current(shortcut, event);
}, { scope });
}, { scope, keys: hookKeys.length > 0 ? hookKeys : undefined });
}

// Register shortcuts using the stable definitions.
Expand Down
2 changes: 2 additions & 0 deletions tests/sample-react/src/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ function App() {
return (
<Keybindy
scope="global"
beforeEach={()=> {console.log("before 1")}}
shortcuts={[
{
keys: [['Ctrl (Left)'], ['Alt']],
Expand Down Expand Up @@ -52,6 +53,7 @@ function App() {
>
<Keybindy
scope="global"
beforeEach={()=> {console.log("before 2")}}
shortcuts={() => {
let variable = 1;
return [
Expand Down
Loading