@tanstack/hotkeys 0.5.0 → 0.6.1

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.
Files changed (45) hide show
  1. package/README.md +1 -1
  2. package/dist/constants.cjs +70 -10
  3. package/dist/constants.cjs.map +1 -1
  4. package/dist/constants.d.cts +57 -4
  5. package/dist/constants.d.ts +57 -4
  6. package/dist/constants.js +67 -10
  7. package/dist/constants.js.map +1 -1
  8. package/dist/format.cjs +20 -125
  9. package/dist/format.cjs.map +1 -1
  10. package/dist/format.d.cts +8 -72
  11. package/dist/format.d.ts +8 -72
  12. package/dist/format.js +23 -127
  13. package/dist/format.js.map +1 -1
  14. package/dist/hotkey-manager.cjs.map +1 -1
  15. package/dist/hotkey-manager.d.cts +5 -1
  16. package/dist/hotkey-manager.d.ts +5 -1
  17. package/dist/hotkey-manager.js.map +1 -1
  18. package/dist/hotkey.d.cts +2 -0
  19. package/dist/hotkey.d.ts +2 -0
  20. package/dist/index.cjs +7 -5
  21. package/dist/index.d.cts +4 -4
  22. package/dist/index.d.ts +4 -4
  23. package/dist/index.js +4 -4
  24. package/dist/parse.cjs +53 -91
  25. package/dist/parse.cjs.map +1 -1
  26. package/dist/parse.d.cts +31 -72
  27. package/dist/parse.d.ts +31 -72
  28. package/dist/parse.js +51 -89
  29. package/dist/parse.js.map +1 -1
  30. package/dist/recorder-chord.cjs +3 -2
  31. package/dist/recorder-chord.cjs.map +1 -1
  32. package/dist/recorder-chord.js +4 -3
  33. package/dist/recorder-chord.js.map +1 -1
  34. package/dist/sequence-manager.cjs +1 -1
  35. package/dist/sequence-manager.cjs.map +1 -1
  36. package/dist/sequence-manager.js +2 -2
  37. package/dist/sequence-manager.js.map +1 -1
  38. package/package.json +2 -2
  39. package/src/constants.ts +75 -21
  40. package/src/format.ts +58 -196
  41. package/src/hotkey-manager.ts +5 -1
  42. package/src/hotkey.ts +2 -0
  43. package/src/parse.ts +91 -131
  44. package/src/recorder-chord.ts +4 -5
  45. package/src/sequence-manager.ts +2 -2
package/src/parse.ts CHANGED
@@ -11,6 +11,7 @@ import type {
11
11
  Key,
12
12
  ParsedHotkey,
13
13
  RawHotkey,
14
+ RegisterableHotkey,
14
15
  } from './hotkey'
15
16
 
16
17
  /**
@@ -137,53 +138,110 @@ export function rawHotkeyToParsedHotkey(
137
138
  }
138
139
  }
139
140
 
141
+ /**
142
+ * Serializes a parsed hotkey to the canonical string form used for registration
143
+ * and storage: `Mod` first when the platform allows, then `Alt`, then `Shift`;
144
+ * otherwise full `Control+Alt+Shift+Meta` order.
145
+ */
146
+ function normalizedHotkeyStringFromParsed(
147
+ parsed: ParsedHotkey,
148
+ platform: 'mac' | 'windows' | 'linux',
149
+ ): Hotkey {
150
+ const canUseMod =
151
+ platform === 'mac'
152
+ ? parsed.meta && !parsed.ctrl
153
+ : parsed.ctrl && !parsed.meta
154
+
155
+ const parts: Array<string> = []
156
+
157
+ if (canUseMod) {
158
+ parts.push('Mod')
159
+ if (parsed.alt) {
160
+ parts.push('Alt')
161
+ }
162
+ if (parsed.shift) {
163
+ parts.push('Shift')
164
+ }
165
+ } else {
166
+ for (const modifier of MODIFIER_ORDER) {
167
+ if (parsed.modifiers.includes(modifier)) {
168
+ parts.push(modifier)
169
+ }
170
+ }
171
+ }
172
+
173
+ parts.push(normalizeKeyName(parsed.key))
174
+ return parts.join('+') as Hotkey
175
+ }
176
+
140
177
  /**
141
178
  * Normalizes a hotkey string to its canonical form.
142
179
  *
143
- * The canonical form uses:
144
- * - Full modifier names (Control, Alt, Shift, Meta)
145
- * - Modifiers in order: Control+Alt+Shift+Meta
146
- * - Uppercase letters for single-character keys
147
- * - Proper casing for special keys (Escape, not escape)
180
+ * - When `Mod` is allowed for the platform (Command on Mac without Control;
181
+ * Control on Windows/Linux without Meta): emits `Mod`, then `Alt`, then `Shift`,
182
+ * then the key (e.g. `Mod+Shift+E`, `Mod+S`).
183
+ * - Otherwise: literal modifiers in `Control+Alt+Shift+Meta` order, then the key.
184
+ * - Resolves aliases and normalizes key casing (e.g. `esc` → `Escape`, `a` → `A`).
148
185
  *
149
186
  * @param hotkey - The hotkey string to normalize
150
- * @param platform - The target platform for resolving 'Mod' (defaults to auto-detection)
187
+ * @param platform - The target platform for resolving `Mod` (defaults to auto-detection)
151
188
  * @returns The normalized hotkey string
152
189
  *
153
190
  * @example
154
191
  * ```ts
155
- * normalizeHotkey('mod+shift+s') // On Mac: 'Shift+Meta+S'
156
- * normalizeHotkey('ctrl+a') // 'Control+A'
192
+ * normalizeHotkey('shift+meta+e', 'mac') // 'Mod+Shift+E'
193
+ * normalizeHotkey('ctrl+a', 'windows') // 'Mod+A'
157
194
  * normalizeHotkey('esc') // 'Escape'
158
195
  * ```
159
196
  */
160
197
  export function normalizeHotkey(
161
198
  hotkey: Key | (string & {}),
162
199
  platform: 'mac' | 'windows' | 'linux' = detectPlatform(),
163
- ): string {
164
- const parsed = parseHotkey(hotkey, platform)
165
- const parts: Array<string> = []
166
-
167
- // Add modifiers in canonical order
168
- for (const modifier of MODIFIER_ORDER) {
169
- if (parsed.modifiers.includes(modifier)) {
170
- parts.push(modifier)
171
- }
172
- }
200
+ ): Hotkey {
201
+ return normalizedHotkeyStringFromParsed(
202
+ parseHotkey(hotkey, platform),
203
+ platform,
204
+ )
205
+ }
173
206
 
174
- // Add the key
175
- parts.push(parsed.key)
207
+ /**
208
+ * Same canonical string as {@link normalizeHotkey}, but from an already-parsed hotkey.
209
+ */
210
+ export function normalizeHotkeyFromParsed(
211
+ parsed: ParsedHotkey,
212
+ platform: 'mac' | 'windows' | 'linux' = detectPlatform(),
213
+ ): Hotkey {
214
+ return normalizedHotkeyStringFromParsed(parsed, platform)
215
+ }
176
216
 
177
- return parts.join('+')
217
+ /**
218
+ * Normalizes a string or {@link RawHotkey} object to the same canonical hotkey string.
219
+ * Use this in framework adapters instead of branching on `formatHotkey(rawHotkeyToParsedHotkey(...))`.
220
+ */
221
+ export function normalizeRegisterableHotkey(
222
+ hotkey: RegisterableHotkey,
223
+ platform: 'mac' | 'windows' | 'linux' = detectPlatform(),
224
+ ): Hotkey {
225
+ return typeof hotkey === 'string'
226
+ ? normalizeHotkey(hotkey, platform)
227
+ : normalizeHotkeyFromParsed(
228
+ rawHotkeyToParsedHotkey(hotkey, platform),
229
+ platform,
230
+ )
178
231
  }
179
232
 
180
233
  /**
181
- * Checks if a string represents a modifier key.
234
+ * Checks if a string is a recognized modifier token (including aliases).
235
+ *
236
+ * For a `KeyboardEvent`, use `isModifier(normalizeKeyName(event.key))` so DOM
237
+ * spellings like `OS` / `Win` match the same alias table.
182
238
  *
183
- * @param key - The string to check
239
+ * @param key - Key name or alias (e.g. from a hotkey string or `event.key`)
184
240
  * @returns True if the string is a recognized modifier
185
241
  */
186
- export function isModifier(key: string): boolean {
242
+ export function isModifierKey(
243
+ key: Key | (string & {}),
244
+ ): key is keyof typeof MODIFIER_ALIASES {
187
245
  return key in MODIFIER_ALIASES || key.toLowerCase() in MODIFIER_ALIASES
188
246
  }
189
247
 
@@ -226,72 +284,16 @@ export function parseKeyboardEvent(event: KeyboardEvent): ParsedHotkey {
226
284
  }
227
285
 
228
286
  /**
229
- * Converts a KeyboardEvent directly to a hotkey string.
230
- *
231
- * This is a convenience function that combines `parseKeyboardEvent()` and formatting.
232
- * The resulting hotkey string uses canonical modifier names (Control, Alt, Shift, Meta)
233
- * and is suitable for use with `useHotkey()` and other hotkey functions.
234
- *
235
- * @param event - The KeyboardEvent to convert
236
- * @param platform - The target platform (defaults to auto-detection)
237
- * @returns A hotkey string in canonical form (e.g., 'Control+Shift+S')
287
+ * Normalizes a keyboard event to the same canonical hotkey string as {@link normalizeHotkey}.
238
288
  *
239
- * @example
240
- * ```ts
241
- * document.addEventListener('keydown', (event) => {
242
- * const hotkey = keyboardEventToHotkey(event)
243
- * console.log(hotkey) // 'Control+Shift+S'
244
- * useHotkey(hotkey, () => console.log('Shortcut triggered'))
245
- * })
246
- * ```
247
- */
248
- export function keyboardEventToHotkey(event: KeyboardEvent): Hotkey {
249
- const parsed = parseKeyboardEvent(event)
250
-
251
- // Build hotkey string in canonical order (same as formatHotkey)
252
- const parts: Array<string> = []
253
- for (const modifier of MODIFIER_ORDER) {
254
- if (parsed.modifiers.includes(modifier)) {
255
- parts.push(modifier)
256
- }
257
- }
258
- parts.push(parsed.key)
259
-
260
- return parts.join('+') as Hotkey
261
- }
262
-
263
- /**
264
- * Checks if a KeyboardEvent represents a modifier-only key press.
265
- *
266
- * Modifier-only keys are keys like 'Control', 'Shift', 'Alt', 'Meta', etc.
267
- * that don't have an associated character or action key. This is useful
268
- * for filtering out modifier key presses when recording shortcuts.
269
- *
270
- * @param event - The KeyboardEvent to check
271
- * @returns True if the event represents a modifier-only key
272
- *
273
- * @example
274
- * ```ts
275
- * document.addEventListener('keydown', (event) => {
276
- * if (isModifierKey(event)) {
277
- * console.log('Modifier key pressed, waiting for action key...')
278
- * return
279
- * }
280
- * // Process non-modifier key
281
- * })
282
- * ```
289
+ * @param event - The keyboard event (typically `keydown`)
290
+ * @param platform - Target platform for `Mod` eligibility
283
291
  */
284
- export function isModifierKey(event: KeyboardEvent): boolean {
285
- const key = event.key
286
- return (
287
- key === 'Control' ||
288
- key === 'Shift' ||
289
- key === 'Alt' ||
290
- key === 'Meta' ||
291
- key === 'Command' ||
292
- key === 'OS' ||
293
- key === 'Win'
294
- )
292
+ export function normalizeHotkeyFromEvent(
293
+ event: KeyboardEvent,
294
+ platform: 'mac' | 'windows' | 'linux' = detectPlatform(),
295
+ ): Hotkey {
296
+ return normalizedHotkeyStringFromParsed(parseKeyboardEvent(event), platform)
295
297
  }
296
298
 
297
299
  /**
@@ -319,50 +321,8 @@ export function hasNonModifierKey(
319
321
  typeof hotkey === 'string' ? parseHotkey(hotkey, platform) : hotkey
320
322
 
321
323
  // Check if the key part is actually a modifier
322
- const keyIsModifier = isModifier(parsed.key)
324
+ const keyIsModifier = isModifierKey(parsed.key)
323
325
 
324
326
  // A valid hotkey must have a non-modifier key
325
327
  return !keyIsModifier && parsed.key.length > 0
326
328
  }
327
-
328
- /**
329
- * Converts a hotkey string to use 'Mod' format for portability.
330
- *
331
- * On macOS, converts 'Meta' to 'Mod'. On Windows/Linux, converts 'Control' to 'Mod'.
332
- * This enables cross-platform hotkey definitions that work consistently.
333
- *
334
- * @param hotkey - The hotkey string to convert
335
- * @param platform - The target platform (defaults to auto-detection)
336
- * @returns The hotkey string with 'Mod' format applied
337
- *
338
- * @example
339
- * ```ts
340
- * convertToModFormat('Meta+S', 'mac') // 'Mod+S'
341
- * convertToModFormat('Control+S', 'windows') // 'Mod+S'
342
- * convertToModFormat('Control+Meta+S', 'mac') // 'Control+Meta+S' (both present, no conversion)
343
- * ```
344
- */
345
- export function convertToModFormat(
346
- hotkey: Hotkey | (string & {}),
347
- platform: 'mac' | 'windows' | 'linux' = detectPlatform(),
348
- ): Hotkey {
349
- const parsed = parseHotkey(hotkey, platform)
350
-
351
- // Only convert if we have exactly one primary modifier
352
- if (platform === 'mac' && parsed.meta && !parsed.ctrl) {
353
- // Convert Meta to Mod on Mac
354
- const parts = hotkey.split('+')
355
- return parts
356
- .map((part) => (part === 'Meta' ? 'Mod' : part))
357
- .join('+') as Hotkey
358
- } else if (platform !== 'mac' && parsed.ctrl && !parsed.meta) {
359
- // Convert Control to Mod on Windows/Linux
360
- const parts = hotkey.split('+')
361
- return parts
362
- .map((part) => (part === 'Control' ? 'Mod' : part))
363
- .join('+') as Hotkey
364
- }
365
-
366
- // No conversion needed
367
- return hotkey as Hotkey
368
- }
@@ -1,8 +1,8 @@
1
+ import { normalizeKeyName } from './constants'
1
2
  import {
2
- convertToModFormat,
3
3
  hasNonModifierKey,
4
4
  isModifierKey,
5
- keyboardEventToHotkey,
5
+ normalizeHotkeyFromEvent,
6
6
  } from './parse'
7
7
  import type { Hotkey } from './hotkey'
8
8
 
@@ -14,11 +14,10 @@ export function hotkeyChordFromKeydown(
14
14
  event: KeyboardEvent,
15
15
  platform: 'mac' | 'windows' | 'linux',
16
16
  ): Hotkey | null {
17
- if (isModifierKey(event)) {
17
+ if (isModifierKey(normalizeKeyName(event.key))) {
18
18
  return null
19
19
  }
20
- const hotkey = keyboardEventToHotkey(event)
21
- const finalHotkey = convertToModFormat(hotkey, platform)
20
+ const finalHotkey = normalizeHotkeyFromEvent(event, platform)
22
21
  if (!hasNonModifierKey(finalHotkey, platform)) {
23
22
  return null
24
23
  }
@@ -1,6 +1,6 @@
1
1
  import { Store } from '@tanstack/store'
2
2
  import { formatHotkeySequence } from './format'
3
- import { detectPlatform } from './constants'
3
+ import { detectPlatform, normalizeKeyName } from './constants'
4
4
  import { isModifierKey, parseHotkey } from './parse'
5
5
  import { matchesKeyboardEvent } from './match'
6
6
  import {
@@ -434,7 +434,7 @@ export class SequenceManager {
434
434
  eventType: 'keydown' | 'keyup',
435
435
  ): void {
436
436
  // Skip modifier-only events so pressing e.g. Shift before Shift+C does not reset the sequence.
437
- if (isModifierKey(event)) {
437
+ if (isModifierKey(normalizeKeyName(event.key))) {
438
438
  return
439
439
  }
440
440