refactor(ui): enforce shortcut registration IDs
This commit is contained in:
@@ -0,0 +1,21 @@
|
||||
import { expect, test } from 'bun:test';
|
||||
import type { ShortcutHandler } from '@/lib/shortcuts';
|
||||
import type { ShortcutBindings } from './useKeybind';
|
||||
|
||||
const handler: ShortcutHandler = () => {};
|
||||
const validBindings = {
|
||||
open_session_list: handler,
|
||||
};
|
||||
const mixedBindingsWithTypo = {
|
||||
open_session_list: handler,
|
||||
open_session_lsit: handler,
|
||||
};
|
||||
|
||||
const acceptedBindings: ShortcutBindings<typeof validBindings> = validBindings;
|
||||
// @ts-expect-error A misspelled key must fail even when the object also contains a valid ID.
|
||||
const rejectedBindings: ShortcutBindings<typeof mixedBindingsWithTypo> = mixedBindingsWithTypo;
|
||||
void rejectedBindings;
|
||||
|
||||
test('accepts bindings whose IDs are declared in the shortcut schema', () => {
|
||||
expect(Object.keys(acceptedBindings)).toEqual(['open_session_list']);
|
||||
});
|
||||
@@ -8,9 +8,13 @@ export function useKeybind(actionId: ShortcutActionId, handler: ShortcutHandler)
|
||||
React.useEffect(() => shortcutRegistry.register(actionId, (event) => handlerRef.current(event)), [actionId]);
|
||||
}
|
||||
|
||||
export function useKeybinds(
|
||||
bindings: Partial<Record<ShortcutActionId, ShortcutHandler>>,
|
||||
): void {
|
||||
export type ShortcutBindings<
|
||||
Bindings extends Partial<Record<ShortcutActionId, ShortcutHandler>>,
|
||||
> = Bindings & Record<Exclude<keyof Bindings, ShortcutActionId>, never>;
|
||||
|
||||
export function useKeybinds<
|
||||
const Bindings extends Partial<Record<ShortcutActionId, ShortcutHandler>>,
|
||||
>(bindings: ShortcutBindings<Bindings>): void {
|
||||
const handlersRef = React.useRef(bindings);
|
||||
handlersRef.current = bindings;
|
||||
const actionIdsKey = Object.keys(bindings).sort().join('\0');
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Registration boundary
|
||||
|
||||
Application commands use `useKeybind(actionId, handler)` or `useKeybinds(bindings)`. Both register with the shared `shortcutRegistry`, so components never receive a registry. The first registration for an action ID wins until it unregisters, then the next mounted registration takes over. A component-local interaction, such as editor navigation or an open menu, remains local event handling rather than a registered application command.
|
||||
Application commands use `useKeybind(actionId, handler)` or `useKeybinds(bindings)`. Both accept only action IDs derived from `SHORTCUT_SCHEMA`. Batch registration also rejects undeclared keys in prebuilt objects, including objects that mix valid and misspelled IDs. Both hooks use the shared `shortcutRegistry`, so components never receive a registry. The first registration for an action ID wins until it unregisters, then the next mounted registration takes over. A component-local interaction, such as editor navigation or an open menu, remains local event handling rather than a registered application command.
|
||||
|
||||
Do not add a component-level `window` or `document` keydown listener for an application command. Declare the action in `config.ts`, then register its handler near the state or UI it owns. This keeps definitions and dispatch centralized without lifting component state or passing callbacks through unrelated components.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user