From 96cba7a82ed363830ab440014847e56d52c4b56a Mon Sep 17 00:00:00 2001 From: PRASSamin Date: Sun, 30 Aug 2026 22:13:50 +0600 Subject: [PATCH] chore(release): v2.0.1 - contextual typing, hook-level guard scoping, and per-shortcut hooks --- packages/core/CHANGELOG.md | 11 +++++++ packages/core/package.json | 2 +- packages/core/src/ShortcutManager.ts | 45 +++++++++++++++++++--------- packages/core/src/types.ts | 11 +++++-- packages/react/CHANGELOG.md | 11 +++++++ packages/react/package.json | 2 +- packages/react/src/useShortcuts.ts | 8 +++-- tests/sample-react/src/App.jsx | 2 ++ 8 files changed, 72 insertions(+), 20 deletions(-) diff --git a/packages/core/CHANGELOG.md b/packages/core/CHANGELOG.md index 32f904c..9221dbf 100644 --- a/packages/core/CHANGELOG.md +++ b/packages/core/CHANGELOG.md @@ -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 diff --git a/packages/core/package.json b/packages/core/package.json index 6fe9054..4cf894d 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -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", diff --git a/packages/core/src/ShortcutManager.ts b/packages/core/src/ShortcutManager.ts index 35e9091..f36dda2 100644 --- a/packages/core/src/ShortcutManager.ts +++ b/packages/core/src/ShortcutManager.ts @@ -4,7 +4,6 @@ import type { ShortcutOptions, Shortcut, ShortcutBinding, - HoldShortcutHandler, ShortcutManagerOptions, BeforeEachHook, AfterEachHook, @@ -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 }); @@ -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 => { @@ -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; } @@ -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); + } + } } /** @@ -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); @@ -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); } @@ -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'; @@ -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); diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index d6867fb..e599450 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -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. @@ -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; } /** diff --git a/packages/react/CHANGELOG.md b/packages/react/CHANGELOG.md index 33f7005..d904093 100644 --- a/packages/react/CHANGELOG.md +++ b/packages/react/CHANGELOG.md @@ -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 diff --git a/packages/react/package.json b/packages/react/package.json index e90e6e1..50d3cc8 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -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", diff --git a/packages/react/src/useShortcuts.ts b/packages/react/src/useShortcuts.ts index e2414df..3cd509b 100644 --- a/packages/react/src/useShortcuts.ts +++ b/packages/react/src/useShortcuts.ts @@ -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. diff --git a/tests/sample-react/src/App.jsx b/tests/sample-react/src/App.jsx index 461d537..df16f3e 100644 --- a/tests/sample-react/src/App.jsx +++ b/tests/sample-react/src/App.jsx @@ -14,6 +14,7 @@ function App() { return ( {console.log("before 1")}} shortcuts={[ { keys: [['Ctrl (Left)'], ['Alt']], @@ -52,6 +53,7 @@ function App() { > {console.log("before 2")}} shortcuts={() => { let variable = 1; return [