@tanstack/hotkeys 0.19.0 → 0.21.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -42,8 +42,8 @@ export declare function validateHotkey(hotkey: ValidatableHotkey): ValidationRes
42
42
  */
43
43
  export declare function assertValidHotkey(hotkey: ValidatableHotkey): void;
44
44
  /**
45
- * Validates a hotkey and logs warnings to the console.
46
- * Useful for development-time feedback.
45
+ * Validates a hotkey and logs errors and warnings to the console.
46
+ * Useful for development-time feedback. Logs nothing in production builds.
47
47
  *
48
48
  * @param hotkey - The hotkey string to validate
49
49
  * @returns True if the hotkey is valid (may still have warnings)
package/dist/validate.js CHANGED
@@ -1,5 +1,4 @@
1
- import { ALL_KEYS, normalizeKeyName } from "./constants.js";
2
- import { modifierAlias, physicalCodeOf, splitChordParts } from "./_keyboard-event.js";
1
+ import { isKeyNameShape, modifierAlias, physicalCodeOf, splitChordParts } from "./_keyboard-event.js";
3
2
 
4
3
  //#region src/validate.ts
5
4
  /**
@@ -62,17 +61,13 @@ function validateChord(hotkey) {
62
61
  const keyPart = parts[parts.length - 1];
63
62
  for (const modifier of modifierParts) if (!modifierAlias(modifier)) errors.push(`Unknown modifier: '${modifier}'`);
64
63
  if (keyPart.length > 1 && (keyPart.startsWith("[") || keyPart.endsWith("]")) && physicalCodeOf(keyPart) === void 0) errors.push("Invalid physical key: use a bracketed KeyboardEvent.code, such as [KeyS]");
65
- if (!isKnownKey(normalizeKeyName(keyPart))) warnings.push(`Unknown key: '${keyPart}'. This may still work but won't have type-safe autocomplete.`);
64
+ if (physicalCodeOf(keyPart) === void 0 && !isKeyNameShape(keyPart)) warnings.push(`Unknown key: '${keyPart}'. Key names are case-sensitive, such as 'Escape' and 'F1'.`);
66
65
  return {
67
66
  valid: errors.length === 0,
68
67
  warnings,
69
68
  errors
70
69
  };
71
70
  }
72
- /** Physical codes, single characters, and named keys are known. */
73
- function isKnownKey(key) {
74
- return physicalCodeOf(key) !== void 0 || Array.from(key).length === 1 || ALL_KEYS.has(key);
75
- }
76
71
  /**
77
72
  * Validates a hotkey and throws an error if invalid.
78
73
  * Useful for development-time validation.
@@ -91,8 +86,8 @@ function assertValidHotkey(hotkey) {
91
86
  if (!result.valid) throw new Error(`Invalid hotkey '${String(hotkey)}': ${result.errors.join(", ")}`);
92
87
  }
93
88
  /**
94
- * Validates a hotkey and logs warnings to the console.
95
- * Useful for development-time feedback.
89
+ * Validates a hotkey and logs errors and warnings to the console.
90
+ * Useful for development-time feedback. Logs nothing in production builds.
96
91
  *
97
92
  * @param hotkey - The hotkey string to validate
98
93
  * @returns True if the hotkey is valid (may still have warnings)
@@ -106,8 +101,10 @@ function assertValidHotkey(hotkey) {
106
101
  */
107
102
  function checkHotkey(hotkey) {
108
103
  const result = validateHotkey(hotkey);
109
- if (result.errors.length > 0) console.error(`Hotkey '${String(hotkey)}' errors:`, result.errors.join("; "));
110
- if (result.warnings.length > 0) console.warn(`Hotkey '${String(hotkey)}' warnings:`, result.warnings.join("; "));
104
+ if (process.env.NODE_ENV !== "production") {
105
+ if (result.errors.length > 0) console.error(`Hotkey '${String(hotkey)}' errors:`, result.errors.join("; "));
106
+ if (result.warnings.length > 0) console.warn(`Hotkey '${String(hotkey)}' warnings:`, result.warnings.join("; "));
107
+ }
111
108
  return result.valid;
112
109
  }
113
110
 
@@ -0,0 +1,23 @@
1
+ import { Observer, Subscription } from "./types.js";
2
+ //#region ../../node_modules/.pnpm/@tanstack+store@0.11.2/node_modules/@tanstack/store/dist/store.d.ts
3
+ //#region src/store.d.ts
4
+ type StoreAction = (...args: Array<any>) => any;
5
+ type StoreActionMap = Record<string, StoreAction>;
6
+ type StoreActionsFactory<T, TActions extends StoreActionMap> = (store: {
7
+ setState: Store<T>['setState'];
8
+ get: Store<T>['get'];
9
+ }) => TActions;
10
+ type NonFunction<T> = T extends ((...args: Array<any>) => any) ? never : T;
11
+ declare class Store<T, TActions extends StoreActionMap = never> {
12
+ private atom;
13
+ readonly actions: TActions;
14
+ constructor(getValue: (prev?: NoInfer<T>) => T);
15
+ constructor(initialValue: T);
16
+ constructor(initialValue: NonFunction<T>, actionsFactory: StoreActionsFactory<T, TActions>);
17
+ setState(updater: (prev: T) => T): void;
18
+ get state(): T;
19
+ get(): T;
20
+ subscribe(observerOrFn: Observer<T> | ((value: T) => void)): Subscription;
21
+ }
22
+ //#endregion
23
+ export { Store, StoreAction, StoreActionMap, StoreActionsFactory };
@@ -0,0 +1,11 @@
1
+ //#region ../../node_modules/.pnpm/@tanstack+store@0.11.2/node_modules/@tanstack/store/dist/types.d.ts
2
+ type Observer<T> = {
3
+ next?: (value: T) => void;
4
+ error?: (err: unknown) => void;
5
+ complete?: () => void;
6
+ };
7
+ interface Subscription {
8
+ unsubscribe: () => void;
9
+ }
10
+ //#endregion
11
+ export { Observer, Subscription };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tanstack/hotkeys",
3
- "version": "0.19.0",
3
+ "version": "0.21.0",
4
4
  "description": "Type-safe, framework-agnostic keyboard hotkey management for the browser",
5
5
  "author": "Tanner Linsley",
6
6
  "license": "MIT",
@@ -39,9 +39,12 @@
39
39
  "dist",
40
40
  "skills"
41
41
  ],
42
- "dependencies": {
42
+ "devDependencies": {
43
43
  "@tanstack/store": "^0.11.2"
44
44
  },
45
+ "inlinedDependencies": {
46
+ "@tanstack/store": "0.11.2"
47
+ },
45
48
  "scripts": {
46
49
  "clean": "premove ./build ./dist",
47
50
  "lint": "eslint ./src",
@@ -46,12 +46,11 @@ export const appHotkeys = defineAppHotkeys({
46
46
  })
47
47
  ```
48
48
 
49
- Augment `Register` in the adapter module you import from, such as `declare module '@tanstack/react-hotkeys' { interface Register { appHotkeys: typeof appHotkeys } }`, to type ids. Lit, Alpine, and Ember have no provider: call `provideAppHotkeys(appHotkeys, options)` once at startup.
49
+ Augment `Register` in the adapter module you import from, such as `declare module '@tanstack/react-hotkeys' { interface Register { appHotkeys: typeof appHotkeys } }`, to type ids. Lit, Alpine, and Ember have no provider: call `provideAppHotkeys(appHotkeys, options)` once at startup. Pass saved data that loads later to `store.setOptions({ customHotkeys })` on the returned store; providers apply a changed `customHotkeys` prop themselves. `undefined` leaves custom hotkeys unchanged while data loads; `null` clears them, such as on sign-out.
50
50
 
51
51
  ## Pitfalls
52
52
 
53
53
  - **Register the app hotkey, not its value.** Pass `appHotkeys['doc.save']`. Passing `.hotkey` registers the declared default, so the user's rebinding never applies, and the list reports the shortcut as `'inactive'`.
54
- - **The provider reads `customHotkeys` once.** When saved shortcuts load asynchronously, render the provider after they arrive, or key it by user. Change shortcuts afterwards through the adapter's custom hotkeys API, not by updating the prop.
55
54
  - **Save what `onCustomHotkeysChange` receives.** It holds only the shortcuts the user changed, so users still get new defaults. Saving every effective hotkey freezes today's defaults into their data.
56
55
  - **Ids are a storage contract.** Saved data is keyed by id, and unknown ids are dropped with a warning, so renaming an id discards users' customizations.
57
56
  - **`setCustomHotkey` returns conflicts and changes nothing when there are any.** Check the result before reporting success, or reject the key press earlier by calling `findConflicts` in the recorder's `validate`.
@@ -33,7 +33,7 @@ The library owns the keyboard. Register every shortcut with your framework adapt
33
33
  | Instead of | Use |
34
34
  | --------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
35
35
  | A `keydown` listener that checks `event.key`, `metaKey`, or `ctrlKey` | A registered hotkey such as `'Mod+S'`. Inside a widget's own key handler, `matchesKeyboardEvent(event, 'Mod+Enter')`. |
36
- | `isMac` checks and `⌘`/`Ctrl` label maps | `Mod` in the hotkey, and `formatForDisplay(hotkey)` for labels |
36
+ | `isMac` checks and `⌘`/`Ctrl` label maps | `Mod` in the hotkey, `formatForDisplay(hotkey)` for labels, or `formatForDisplay({ key })` for held keys |
37
37
  | Skipping events from text fields, calling `preventDefault` | The defaults: single keys and Shift/Alt chords are skipped while typing, `Mod` chords and `Escape` still fire, and matched keys are prevented |
38
38
  | A shortcut registry, service, context, or wrapper hook | The adapter API, called where each action lives. For one list of every shortcut, load `@tanstack/hotkeys#app-hotkeys`. |
39
39
  | A "shortcuts enabled" flag or topmost-dialog tracking | Scopes, with `exclusive` for dialogs |
@@ -59,6 +59,7 @@ When nesting scopes, stacking dialogs, or keeping some hotkeys working during an
59
59
  - **A disabled hotkey doesn't block the browser.** With `enabled: false`, `Mod+S` opens the Save Page dialog. Add `preventDefault: 'always'` to keep the action blocked while the hotkey can't fire, whether it's disabled or its scope is off. Skip it for hotkeys turned off to give the key back to the browser, such as `Mod+C`, `Mod+Z`, `Tab`, or single keys behind a user's off switch. Don't keep the hotkey enabled and return early from the callback; the hotkey would also stay `active` in app hotkey lists and show its hint.
60
60
  - **Modifiers match exactly.** `ArrowDown` never fires while Shift is held, so register `Shift+ArrowDown` instead of checking `event.shiftKey`. Shift that a symbol needs is the exception: `?` matches Shift+/.
61
61
  - **Chords repeat while held.** Set `requireReset: true` on toggles, or holding the keys flickers them.
62
+ - **Key names are case-sensitive.** Write `Escape`, `ArrowUp`, `F1`, and `+` as the types spell them; `escape`, `Esc`, `f1`, and `Plus` never match. Only letters match in any case. Hotkeys from plain JavaScript or saved data skip the types, so check them with `validateHotkey`.
62
63
  - **Letters follow the typed character.** For position-based controls such as WASD, use physical bindings such as `'[KeyW]'`. Keep letters for mnemonic commands such as `Mod+S`.
63
64
  - **Each open exclusive scope needs its own name.** A reusable dialog with a fixed scope name breaks when one dialog opens another: both share one scope and both handle `Escape`. Build the name per instance.
64
65
  - **A chord on a sequence's first key runs every time the sequence starts.** With `G` and `['G', 'G']`, the first `G` runs the chord. Later keys belong to the sequence. To show "waiting for the next key", read `matchedStepCount` from the adapter's registrations list.