@tanstack/hotkeys 0.15.0 → 0.16.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.
package/dist/parse.d.ts CHANGED
@@ -1,56 +1,42 @@
1
1
  import { Key } from "./key.types.js";
2
- import { Hotkey, HotkeyChord, HotkeySequence, ParsedHotkey, RawHotkey, RegisterableHotkey, RegisterableHotkeyChord } from "./hotkey.types.js";
3
- import { MODIFIER_ALIASES } from "./constants.js";
2
+ import { Hotkey, HotkeyChord, HotkeySequence, ParsedHotkeyChord, RegisterableHotkey, RegisterableHotkeyChord, RegisterableHotkeySequence } from "./hotkey.types.js";
4
3
  //#region src/parse.d.ts
4
+ type Platform = 'mac' | 'windows' | 'linux';
5
+ /** A chord in any form: a string, a chord object, or an already-parsed chord. */
6
+ type AnyChord = RegisterableHotkeyChord | ParsedHotkeyChord | (string & {});
5
7
  /**
6
8
  * Whether a hotkey is a sequence. Use this instead of `Array.isArray`, which
7
9
  * does not narrow readonly arrays.
8
10
  */
9
- export declare function isHotkeySequence(hotkey: RegisterableHotkey): hotkey is HotkeySequence;
11
+ export declare function isHotkeySequence<T extends RegisterableHotkey>(hotkey: T): hotkey is Extract<T, ReadonlyArray<unknown>>;
10
12
  /**
11
13
  * Collapses a one-step sequence to its chord without otherwise changing the hotkey.
12
14
  * Registration APIs treat `['Mod+S']` exactly like `'Mod+S'`.
13
15
  */
14
- export declare function simplifyHotkey<T extends RegisterableHotkey>(hotkey: T): T extends HotkeySequence ? Hotkey : T;
16
+ export declare function simplifyHotkey<T extends RegisterableHotkey>(hotkey: T): T extends HotkeySequence ? Hotkey : T extends RegisterableHotkeySequence ? RegisterableHotkey : T;
15
17
  /**
16
- * Parses a hotkey string into its component parts.
18
+ * Parses a hotkey, resolving `Mod` for the platform. A chord, written as a
19
+ * string or an object, returns one parsed chord. A sequence returns one
20
+ * parsed chord per step. Physical tokens such as `[KeyQ]` keep `code`; they
21
+ * never become a logical Q.
17
22
  *
18
- * @param hotkey - The hotkey string to parse (e.g., 'Mod+Shift+S')
23
+ * @param hotkey - A chord string such as `'Mod+S'`, a chord object, or a sequence
19
24
  * @param platform - The target platform for resolving 'Mod' (defaults to auto-detection)
20
- * @returns A ParsedHotkey with either logical key or physical code and modifier flags
21
25
  *
22
26
  * @example
23
27
  * ```ts
24
- * parseHotkey('Mod+S') // On Mac: { key: 'S', ctrl: false, shift: false, alt: false, meta: true, modifiers: ['Meta'] }
25
- * parseHotkey('Mod+S') // On Windows: { key: 'S', ctrl: true, shift: false, alt: false, meta: false, modifiers: ['Control'] }
26
- * parseHotkey('Control+Shift+A') // { key: 'A', ctrl: true, shift: true, alt: false, meta: false, modifiers: ['Control', 'Shift'] }
28
+ * parseHotkey('Mod+S', 'mac') // { key: 'S', ctrl: false, shift: false, alt: false, meta: true, modifiers: ['Meta'] }
29
+ * parseHotkey({ key: 'S', mod: true }, 'windows') // { key: 'S', ctrl: true, ..., modifiers: ['Control'] }
30
+ * parseHotkey(['G', 'Shift+G']) // [{ key: 'G', ... }, { key: 'G', shift: true, ... }]
27
31
  * ```
28
32
  */
29
- export declare function parseHotkey(hotkey: HotkeyChord | (string & {}), platform?: 'mac' | 'windows' | 'linux'): ParsedHotkey;
33
+ export declare function parseHotkey(hotkey: AnyChord, platform?: Platform): ParsedHotkeyChord;
34
+ export declare function parseHotkey(hotkey: ReadonlyArray<AnyChord>, platform?: Platform): Array<ParsedHotkeyChord>;
35
+ export declare function parseHotkey(hotkey: AnyChord | ReadonlyArray<AnyChord>, platform?: Platform): ParsedHotkeyChord | Array<ParsedHotkeyChord>;
30
36
  /**
31
- * Converts a RawHotkey object to a ParsedHotkey.
32
- * Optional modifier booleans default to false; modifiers array is derived from them.
33
- * When `mod` is true, it is resolved to Control or Meta based on platform.
34
- *
35
- * @param raw - The raw hotkey object
36
- * @param platform - The target platform for resolving 'Mod' (defaults to auto-detection)
37
- * @returns A ParsedHotkey suitable for matching and formatting
38
- *
39
- * @example
40
- * ```ts
41
- * rawHotkeyToParsedHotkey({ key: 'Escape' })
42
- * // { key: 'Escape', ctrl: false, shift: false, alt: false, meta: false, modifiers: [] }
43
- *
44
- * rawHotkeyToParsedHotkey({ key: 'S', mod: true }, 'mac')
45
- * // { key: 'S', ctrl: false, shift: false, alt: false, meta: true, modifiers: ['Meta'] }
46
- *
47
- * rawHotkeyToParsedHotkey({ key: 'S', mod: true, shift: true }, 'windows')
48
- * // { key: 'S', ctrl: true, shift: true, alt: false, meta: false, modifiers: ['Control', 'Shift'] }
49
- * ```
50
- */
51
- export declare function rawHotkeyToParsedHotkey(raw: RawHotkey, platform?: 'mac' | 'windows' | 'linux'): ParsedHotkey;
52
- /**
53
- * Normalizes a hotkey string to its canonical form.
37
+ * Normalizes a hotkey to its canonical string form. Accepts a chord string, a
38
+ * chord object, a parsed chord, or a sequence of any of these. Each sequence
39
+ * step is normalized, and a one-step sequence becomes its chord.
54
40
  *
55
41
  * - When `Mod` is allowed for the platform (Command on Mac without Control;
56
42
  * Control on Windows/Linux without Meta): emits `Mod`, then `Alt`, then `Shift`,
@@ -58,29 +44,20 @@ export declare function rawHotkeyToParsedHotkey(raw: RawHotkey, platform?: 'mac'
58
44
  * - Otherwise: literal modifiers in `Control+Alt+Shift+Meta` order, then the key.
59
45
  * - Resolves aliases and normalizes key casing (e.g. `esc` → `Escape`, `a` → `A`).
60
46
  *
61
- * @param hotkey - The hotkey string to normalize
47
+ * @param hotkey - The hotkey to normalize
62
48
  * @param platform - The target platform for resolving `Mod` (defaults to auto-detection)
63
- * @returns The normalized hotkey string
64
49
  *
65
50
  * @example
66
51
  * ```ts
67
52
  * normalizeHotkey('shift+meta+e', 'mac') // 'Mod+Shift+E'
68
53
  * normalizeHotkey('ctrl+a', 'windows') // 'Mod+A'
69
- * normalizeHotkey('esc') // 'Escape'
54
+ * normalizeHotkey({ key: 's', mod: true }) // 'Mod+S'
55
+ * normalizeHotkey(['g', 'shift+g']) // ['G', 'Shift+G']
56
+ * normalizeHotkey(parseKeyboardEvent(event)) // The chord the event pressed
70
57
  * ```
71
58
  */
72
- export declare function normalizeHotkey(hotkey: Key | (string & {}), platform?: 'mac' | 'windows' | 'linux'): HotkeyChord;
73
- /**
74
- * Same canonical string as {@link normalizeHotkey}, but from an already-parsed hotkey.
75
- */
76
- export declare function normalizeHotkeyFromParsed(parsed: ParsedHotkey, platform?: 'mac' | 'windows' | 'linux'): HotkeyChord;
77
- /**
78
- * Normalizes a chord (string or {@link RawHotkey} object) or a sequence to canonical form.
79
- * Each sequence step is normalized, and a one-step sequence becomes its chord.
80
- * Use this in framework adapters instead of branching on `formatHotkey(rawHotkeyToParsedHotkey(...))`.
81
- */
82
- export declare function normalizeRegisterableHotkey(hotkey: RegisterableHotkeyChord, platform?: 'mac' | 'windows' | 'linux'): HotkeyChord;
83
- export declare function normalizeRegisterableHotkey(hotkey: RegisterableHotkey, platform?: 'mac' | 'windows' | 'linux'): Hotkey;
59
+ export declare function normalizeHotkey(hotkey: AnyChord, platform?: Platform): HotkeyChord;
60
+ export declare function normalizeHotkey(hotkey: AnyChord | ReadonlyArray<AnyChord>, platform?: Platform): Hotkey;
84
61
  /**
85
62
  * Checks if a string is a recognized modifier token (including aliases).
86
63
  *
@@ -90,16 +67,14 @@ export declare function normalizeRegisterableHotkey(hotkey: RegisterableHotkey,
90
67
  * @param key - Key name or alias (e.g. from a hotkey string or `event.key`)
91
68
  * @returns True if the string is a recognized modifier
92
69
  */
93
- export declare function isModifierKey(key: Key | (string & {})): key is keyof typeof MODIFIER_ALIASES;
70
+ export declare function isModifierKey(key: Key | (string & {})): boolean;
94
71
  /**
95
- * Parses a KeyboardEvent into a ParsedHotkey object.
96
- *
97
- * This function extracts the key and modifier state from a keyboard event
98
- * and converts it into the same format used by `parseHotkey()`.
72
+ * Parses a KeyboardEvent into the chord it pressed, in the same format as
73
+ * `parseHotkey()`. Pass the result to `normalizeHotkey` for its canonical string.
99
74
  *
100
75
  * @param event - The KeyboardEvent to parse
101
76
  * @param platform - The target platform for resolving modifiers (defaults to auto-detection)
102
- * @returns A ParsedHotkey object representing the keyboard event
77
+ * @returns The parsed chord for the keyboard event
103
78
  *
104
79
  * @example
105
80
  * ```ts
@@ -109,23 +84,16 @@ export declare function isModifierKey(key: Key | (string & {})): key is keyof ty
109
84
  * })
110
85
  * ```
111
86
  */
112
- export declare function parseKeyboardEvent(event: KeyboardEvent, platform?: 'mac' | 'windows' | 'linux'): ParsedHotkey;
87
+ export declare function parseKeyboardEvent(event: KeyboardEvent, platform?: Platform): ParsedHotkeyChord;
113
88
  /**
114
- * Normalizes a keyboard event to the same canonical hotkey string as {@link normalizeHotkey}.
89
+ * Checks if a chord contains at least one non-modifier key.
115
90
  *
116
- * @param event - The keyboard event (typically `keydown`)
117
- * @param platform - Target platform for `Mod` eligibility
118
- */
119
- export declare function normalizeHotkeyFromEvent(event: KeyboardEvent, platform?: 'mac' | 'windows' | 'linux'): HotkeyChord;
120
- /**
121
- * Checks if a hotkey or ParsedHotkey contains at least one non-modifier key.
122
- *
123
- * This is useful for validating that a recorded hotkey is complete and not
91
+ * This is useful for validating that a recorded chord is complete and not
124
92
  * just a combination of modifiers without an action key.
125
93
  *
126
- * @param hotkey - The hotkey string or ParsedHotkey to check
94
+ * @param chord - The chord string or parsed chord to check
127
95
  * @param platform - The target platform for parsing (defaults to auto-detection)
128
- * @returns True if the hotkey contains at least one non-modifier key
96
+ * @returns True if the chord contains at least one non-modifier key
129
97
  *
130
98
  * @example
131
99
  * ```ts
@@ -134,10 +102,5 @@ export declare function normalizeHotkeyFromEvent(event: KeyboardEvent, platform?
134
102
  * hasNonModifierKey(parseHotkey('Mod+A')) // true
135
103
  * ```
136
104
  */
137
- export declare function hasNonModifierKey(hotkey: HotkeyChord | ParsedHotkey | (string & {}), platform?: 'mac' | 'windows' | 'linux'): boolean;
138
- /**
139
- * Parses a string or raw binding, resolving Mod for the supplied platform.
140
- * Physical tokens such as `[KeyQ]` retain `code`; they never become logical Q.
141
- */
142
- export declare function parseRegisterableHotkey(hotkey: RegisterableHotkeyChord, platform?: "mac" | "windows" | "linux"): ParsedHotkey;
105
+ export declare function hasNonModifierKey(chord: HotkeyChord | ParsedHotkeyChord | (string & {}), platform?: Platform): boolean;
143
106
  //#endregion
package/dist/parse.js CHANGED
@@ -1,8 +1,25 @@
1
- import { MODIFIER_ALIASES, MODIFIER_ORDER, normalizeKeyName } from "./constants.js";
1
+ import { MODIFIER_ORDER, normalizeKeyName } from "./constants.js";
2
2
  import { detectPlatform, resolveModifier } from "./platform.js";
3
- import { assertLogicalKey, normalizeKeyboardEvent, splitHotkeyParts } from "./_keyboard-event.js";
3
+ import { assertLogicalKey, modifierAlias, normalizeKeyboardEvent, physicalCodeOf, splitChordParts } from "./_keyboard-event.js";
4
4
 
5
5
  //#region src/parse.ts
6
+ /** Builds a ParsedHotkeyChord, listing held modifiers in canonical order. */
7
+ function toParsedChord(identity, ctrl, shift, alt, meta) {
8
+ const held = {
9
+ Control: ctrl,
10
+ Alt: alt,
11
+ Shift: shift,
12
+ Meta: meta
13
+ };
14
+ return {
15
+ ...identity,
16
+ ctrl,
17
+ shift,
18
+ alt,
19
+ meta,
20
+ modifiers: MODIFIER_ORDER.filter((modifier) => held[modifier])
21
+ };
22
+ }
6
23
  /**
7
24
  * Whether a hotkey is a sequence. Use this instead of `Array.isArray`, which
8
25
  * does not narrow readonly arrays.
@@ -13,29 +30,15 @@ function isHotkeySequence(hotkey) {
13
30
  function simplifyHotkey(hotkey) {
14
31
  return isHotkeySequence(hotkey) && hotkey.length === 1 ? hotkey[0] : hotkey;
15
32
  }
16
- /**
17
- * Parses a hotkey string into its component parts.
18
- *
19
- * @param hotkey - The hotkey string to parse (e.g., 'Mod+Shift+S')
20
- * @param platform - The target platform for resolving 'Mod' (defaults to auto-detection)
21
- * @returns A ParsedHotkey with either logical key or physical code and modifier flags
22
- *
23
- * @example
24
- * ```ts
25
- * parseHotkey('Mod+S') // On Mac: { key: 'S', ctrl: false, shift: false, alt: false, meta: true, modifiers: ['Meta'] }
26
- * parseHotkey('Mod+S') // On Windows: { key: 'S', ctrl: true, shift: false, alt: false, meta: false, modifiers: ['Control'] }
27
- * parseHotkey('Control+Shift+A') // { key: 'A', ctrl: true, shift: true, alt: false, meta: false, modifiers: ['Control', 'Shift'] }
28
- * ```
29
- */
30
- function parseHotkey(hotkey, platform = detectPlatform()) {
31
- const parts = splitHotkeyParts(hotkey);
33
+ function parseChordString(chord, platform) {
34
+ const parts = splitChordParts(chord);
32
35
  const modifiers = /* @__PURE__ */ new Set();
33
36
  let key = "";
34
37
  for (let i = 0; i < parts.length; i++) {
35
38
  const part = parts[i].trim();
36
39
  if (i === parts.length - 1) key = normalizeKeyName(part);
37
40
  else {
38
- const alias = MODIFIER_ALIASES[part] ?? MODIFIER_ALIASES[part.toLowerCase()];
41
+ const alias = modifierAlias(part);
39
42
  if (alias) {
40
43
  const resolved = resolveModifier(alias, platform);
41
44
  modifiers.add(resolved);
@@ -43,71 +46,31 @@ function parseHotkey(hotkey, platform = detectPlatform()) {
43
46
  }
44
47
  }
45
48
  if (!key && parts.length > 0) key = normalizeKeyName(parts[parts.length - 1].trim());
46
- const code = /^\[([A-Za-z][A-Za-z0-9]*)\]$/.exec(key)?.[1];
47
- return {
48
- ...code !== void 0 ? { code } : { key },
49
- ctrl: modifiers.has("Control"),
50
- shift: modifiers.has("Shift"),
51
- alt: modifiers.has("Alt"),
52
- meta: modifiers.has("Meta"),
53
- modifiers: MODIFIER_ORDER.filter((m) => modifiers.has(m))
54
- };
49
+ const code = physicalCodeOf(key);
50
+ return toParsedChord(code !== void 0 ? { code } : { key }, modifiers.has("Control"), modifiers.has("Shift"), modifiers.has("Alt"), modifiers.has("Meta"));
55
51
  }
56
- /**
57
- * Converts a RawHotkey object to a ParsedHotkey.
58
- * Optional modifier booleans default to false; modifiers array is derived from them.
59
- * When `mod` is true, it is resolved to Control or Meta based on platform.
60
- *
61
- * @param raw - The raw hotkey object
62
- * @param platform - The target platform for resolving 'Mod' (defaults to auto-detection)
63
- * @returns A ParsedHotkey suitable for matching and formatting
64
- *
65
- * @example
66
- * ```ts
67
- * rawHotkeyToParsedHotkey({ key: 'Escape' })
68
- * // { key: 'Escape', ctrl: false, shift: false, alt: false, meta: false, modifiers: [] }
69
- *
70
- * rawHotkeyToParsedHotkey({ key: 'S', mod: true }, 'mac')
71
- * // { key: 'S', ctrl: false, shift: false, alt: false, meta: true, modifiers: ['Meta'] }
72
- *
73
- * rawHotkeyToParsedHotkey({ key: 'S', mod: true, shift: true }, 'windows')
74
- * // { key: 'S', ctrl: true, shift: true, alt: false, meta: false, modifiers: ['Control', 'Shift'] }
75
- * ```
76
- */
77
- function rawHotkeyToParsedHotkey(raw, platform = detectPlatform()) {
78
- if (raw.code === void 0) assertLogicalKey(raw.key);
79
- let ctrl = raw.ctrl ?? false;
80
- const shift = raw.shift ?? false;
81
- const alt = raw.alt ?? false;
82
- let meta = raw.meta ?? false;
83
- if (raw.mod) {
52
+ /** Resolves a chord object's modifiers. Omitted modifiers are false. */
53
+ function parseChordObject(chord, platform) {
54
+ if (chord.code === void 0) assertLogicalKey(chord.key);
55
+ let ctrl = chord.ctrl ?? false;
56
+ const shift = chord.shift ?? false;
57
+ const alt = chord.alt ?? false;
58
+ let meta = chord.meta ?? false;
59
+ if (chord.mod) {
84
60
  if (resolveModifier("Mod", platform) === "Control") ctrl = true;
85
61
  else meta = true;
86
62
  }
87
- const modifiers = MODIFIER_ORDER.filter((m) => {
88
- switch (m) {
89
- case "Control": return ctrl;
90
- case "Shift": return shift;
91
- case "Alt": return alt;
92
- case "Meta": return meta;
93
- default: return false;
94
- }
95
- });
96
- return {
97
- ...raw.code !== void 0 ? { code: raw.code } : { key: normalizeKeyName(raw.key) },
98
- ctrl,
99
- shift,
100
- alt,
101
- meta,
102
- modifiers
103
- };
63
+ return toParsedChord(chord.code !== void 0 ? { code: chord.code } : { key: normalizeKeyName(chord.key) }, ctrl, shift, alt, meta);
64
+ }
65
+ function parseChord(chord, platform) {
66
+ return typeof chord === "string" ? parseChordString(chord, platform) : parseChordObject(chord, platform);
104
67
  }
105
68
  /**
106
- * Serializes a parsed hotkey to the canonical string form used for registration
107
- * and storage: `Mod` first when the platform allows, then `Alt`, then `Shift`;
108
- * otherwise full `Control+Alt+Shift+Meta` order.
69
+ * The canonical string for a parsed chord: `Mod` first when the platform
70
+ * allows, then `Alt`, then `Shift`; otherwise full `Control+Alt+Shift+Meta`
71
+ * order.
109
72
  */
110
- function normalizedHotkeyStringFromParsed(parsed, platform) {
73
+ function normalizeParsedChord(parsed, platform) {
111
74
  if (parsed.code === void 0) assertLogicalKey(parsed.key);
112
75
  const canUseMod = platform === "mac" ? parsed.meta && !parsed.ctrl : parsed.ctrl && !parsed.meta;
113
76
  const parts = [];
@@ -119,41 +82,15 @@ function normalizedHotkeyStringFromParsed(parsed, platform) {
119
82
  parts.push(parsed.code !== void 0 ? `[${parsed.code}]` : normalizeKeyName(parsed.key));
120
83
  return parts.join("+");
121
84
  }
122
- /**
123
- * Normalizes a hotkey string to its canonical form.
124
- *
125
- * - When `Mod` is allowed for the platform (Command on Mac without Control;
126
- * Control on Windows/Linux without Meta): emits `Mod`, then `Alt`, then `Shift`,
127
- * then the key (e.g. `Mod+Shift+E`, `Mod+S`).
128
- * - Otherwise: literal modifiers in `Control+Alt+Shift+Meta` order, then the key.
129
- * - Resolves aliases and normalizes key casing (e.g. `esc` → `Escape`, `a` → `A`).
130
- *
131
- * @param hotkey - The hotkey string to normalize
132
- * @param platform - The target platform for resolving `Mod` (defaults to auto-detection)
133
- * @returns The normalized hotkey string
134
- *
135
- * @example
136
- * ```ts
137
- * normalizeHotkey('shift+meta+e', 'mac') // 'Mod+Shift+E'
138
- * normalizeHotkey('ctrl+a', 'windows') // 'Mod+A'
139
- * normalizeHotkey('esc') // 'Escape'
140
- * ```
141
- */
142
- function normalizeHotkey(hotkey, platform = detectPlatform()) {
143
- return normalizedHotkeyStringFromParsed(parseHotkey(hotkey, platform), platform);
144
- }
145
- /**
146
- * Same canonical string as {@link normalizeHotkey}, but from an already-parsed hotkey.
147
- */
148
- function normalizeHotkeyFromParsed(parsed, platform = detectPlatform()) {
149
- return normalizedHotkeyStringFromParsed(parsed, platform);
85
+ function parseHotkey(hotkey, platform = detectPlatform()) {
86
+ return Array.isArray(hotkey) ? hotkey.map((step) => parseChord(step, platform)) : parseChord(hotkey, platform);
150
87
  }
151
- function normalizeRegisterableHotkey(hotkey, platform = detectPlatform()) {
152
- if (isHotkeySequence(hotkey)) {
153
- const steps = hotkey.map((step) => normalizeHotkey(step, platform));
88
+ function normalizeHotkey(hotkey, platform = detectPlatform()) {
89
+ if (Array.isArray(hotkey)) {
90
+ const steps = hotkey.map((step) => normalizeParsedChord(parseChord(step, platform), platform));
154
91
  return steps.length === 1 ? steps[0] : steps;
155
92
  }
156
- return typeof hotkey === "string" ? normalizeHotkey(hotkey, platform) : normalizeHotkeyFromParsed(rawHotkeyToParsedHotkey(hotkey, platform), platform);
93
+ return normalizeParsedChord(parseChord(hotkey, platform), platform);
157
94
  }
158
95
  /**
159
96
  * Checks if a string is a recognized modifier token (including aliases).
@@ -165,17 +102,15 @@ function normalizeRegisterableHotkey(hotkey, platform = detectPlatform()) {
165
102
  * @returns True if the string is a recognized modifier
166
103
  */
167
104
  function isModifierKey(key) {
168
- return key in MODIFIER_ALIASES || key.toLowerCase() in MODIFIER_ALIASES;
105
+ return modifierAlias(key) !== void 0;
169
106
  }
170
107
  /**
171
- * Parses a KeyboardEvent into a ParsedHotkey object.
172
- *
173
- * This function extracts the key and modifier state from a keyboard event
174
- * and converts it into the same format used by `parseHotkey()`.
108
+ * Parses a KeyboardEvent into the chord it pressed, in the same format as
109
+ * `parseHotkey()`. Pass the result to `normalizeHotkey` for its canonical string.
175
110
  *
176
111
  * @param event - The KeyboardEvent to parse
177
112
  * @param platform - The target platform for resolving modifiers (defaults to auto-detection)
178
- * @returns A ParsedHotkey object representing the keyboard event
113
+ * @returns The parsed chord for the keyboard event
179
114
  *
180
115
  * @example
181
116
  * ```ts
@@ -190,38 +125,17 @@ function parseKeyboardEvent(event, platform) {
190
125
  const ctrl = normalized.altGraph ? false : normalized.ctrl;
191
126
  const alt = normalized.altGraph ? false : normalized.alt;
192
127
  const shift = normalized.shift;
193
- const modifiers = [];
194
- if (ctrl) modifiers.push("Control");
195
- if (alt) modifiers.push("Alt");
196
- if (shift) modifiers.push("Shift");
197
- if (normalized.meta) modifiers.push("Meta");
198
- return {
199
- key: normalized.key,
200
- ctrl,
201
- shift,
202
- alt,
203
- meta: normalized.meta,
204
- modifiers
205
- };
128
+ return toParsedChord({ key: normalized.key }, ctrl, shift, alt, normalized.meta);
206
129
  }
207
130
  /**
208
- * Normalizes a keyboard event to the same canonical hotkey string as {@link normalizeHotkey}.
131
+ * Checks if a chord contains at least one non-modifier key.
209
132
  *
210
- * @param event - The keyboard event (typically `keydown`)
211
- * @param platform - Target platform for `Mod` eligibility
212
- */
213
- function normalizeHotkeyFromEvent(event, platform = detectPlatform()) {
214
- return normalizedHotkeyStringFromParsed(parseKeyboardEvent(event, platform), platform);
215
- }
216
- /**
217
- * Checks if a hotkey or ParsedHotkey contains at least one non-modifier key.
218
- *
219
- * This is useful for validating that a recorded hotkey is complete and not
133
+ * This is useful for validating that a recorded chord is complete and not
220
134
  * just a combination of modifiers without an action key.
221
135
  *
222
- * @param hotkey - The hotkey string or ParsedHotkey to check
136
+ * @param chord - The chord string or parsed chord to check
223
137
  * @param platform - The target platform for parsing (defaults to auto-detection)
224
- * @returns True if the hotkey contains at least one non-modifier key
138
+ * @returns True if the chord contains at least one non-modifier key
225
139
  *
226
140
  * @example
227
141
  * ```ts
@@ -230,18 +144,11 @@ function normalizeHotkeyFromEvent(event, platform = detectPlatform()) {
230
144
  * hasNonModifierKey(parseHotkey('Mod+A')) // true
231
145
  * ```
232
146
  */
233
- function hasNonModifierKey(hotkey, platform = detectPlatform()) {
234
- const parsed = typeof hotkey === "string" ? parseHotkey(hotkey, platform) : hotkey;
147
+ function hasNonModifierKey(chord, platform = detectPlatform()) {
148
+ const parsed = typeof chord === "string" ? parseChordString(chord, platform) : chord;
235
149
  if (parsed.code !== void 0) return parsed.code.length > 0 && !/^(Control|Shift|Alt|Meta)(Left|Right)$/.test(parsed.code);
236
150
  return !isModifierKey(parsed.key) && parsed.key.length > 0;
237
151
  }
238
- /**
239
- * Parses a string or raw binding, resolving Mod for the supplied platform.
240
- * Physical tokens such as `[KeyQ]` retain `code`; they never become logical Q.
241
- */
242
- function parseRegisterableHotkey(hotkey, platform = detectPlatform()) {
243
- return typeof hotkey === "string" ? parseHotkey(hotkey, platform) : rawHotkeyToParsedHotkey(hotkey, platform);
244
- }
245
152
 
246
153
  //#endregion
247
- export { hasNonModifierKey, isHotkeySequence, isModifierKey, normalizeHotkey, normalizeHotkeyFromEvent, normalizeHotkeyFromParsed, normalizeRegisterableHotkey, parseHotkey, parseKeyboardEvent, parseRegisterableHotkey, rawHotkeyToParsedHotkey, simplifyHotkey };
154
+ export { hasNonModifierKey, isHotkeySequence, isModifierKey, normalizeHotkey, parseHotkey, parseKeyboardEvent, simplifyHotkey };
package/dist/validate.js CHANGED
@@ -1,5 +1,5 @@
1
- import { ALL_KEYS, MODIFIER_ALIASES, normalizeKeyName } from "./constants.js";
2
- import { splitHotkeyParts } from "./_keyboard-event.js";
1
+ import { ALL_KEYS, normalizeKeyName } from "./constants.js";
2
+ import { modifierAlias, physicalCodeOf, splitChordParts } from "./_keyboard-event.js";
3
3
 
4
4
  //#region src/validate.ts
5
5
  /**
@@ -52,7 +52,7 @@ function validateChord(hotkey) {
52
52
  warnings: [],
53
53
  errors: ["Hotkey cannot be empty"]
54
54
  };
55
- const parts = splitHotkeyParts(hotkey).map((p) => p.trim());
55
+ const parts = splitChordParts(hotkey).map((p) => p.trim());
56
56
  if (parts.length === 0 || parts.some((p) => p === "")) return {
57
57
  valid: false,
58
58
  warnings: [],
@@ -60,34 +60,18 @@ function validateChord(hotkey) {
60
60
  };
61
61
  const modifierParts = parts.slice(0, -1);
62
62
  const keyPart = parts[parts.length - 1];
63
- for (const modifier of modifierParts) if (!(MODIFIER_ALIASES[modifier] ?? MODIFIER_ALIASES[modifier.toLowerCase()])) errors.push(`Unknown modifier: '${modifier}'`);
64
- if (keyPart.length > 1 && (keyPart.startsWith("[") || keyPart.endsWith("]")) && !/^\[[A-Za-z][A-Za-z0-9]*\]$/.test(keyPart)) errors.push("Invalid physical key: use a bracketed KeyboardEvent.code, such as [KeyS]");
65
- if (!isKnownKey(normalizeKeyName(keyPart)) && !isKnownKey(keyPart)) warnings.push(`Unknown key: '${keyPart}'. This may still work but won't have type-safe autocomplete.`);
63
+ for (const modifier of modifierParts) if (!modifierAlias(modifier)) errors.push(`Unknown modifier: '${modifier}'`);
64
+ 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.`);
66
66
  return {
67
67
  valid: errors.length === 0,
68
68
  warnings,
69
69
  errors
70
70
  };
71
71
  }
72
- /**
73
- * Checks if a key is in the known keys set.
74
- */
72
+ /** Physical codes, single characters, and named keys are known. */
75
73
  function isKnownKey(key) {
76
- if (/^\[[A-Za-z][A-Za-z0-9]*\]$/.test(key)) return true;
77
- if (Array.from(key).length === 1) return true;
78
- if (ALL_KEYS.has(key)) return true;
79
- if (key.length === 1 && ALL_KEYS.has(key.toUpperCase())) return true;
80
- return key in {
81
- Esc: true,
82
- Return: true,
83
- Space: true,
84
- " ": true,
85
- Del: true,
86
- Up: true,
87
- Down: true,
88
- Left: true,
89
- Right: true
90
- };
74
+ return physicalCodeOf(key) !== void 0 || Array.from(key).length === 1 || ALL_KEYS.has(key);
91
75
  }
92
76
  /**
93
77
  * Validates a hotkey and throws an error if invalid.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tanstack/hotkeys",
3
- "version": "0.15.0",
3
+ "version": "0.16.0",
4
4
  "description": "Type-safe, framework-agnostic keyboard hotkey management for the browser",
5
5
  "author": "Tanner Linsley",
6
6
  "license": "MIT",
@@ -1,34 +0,0 @@
1
- import { Hotkey, ParsedHotkey } from "./hotkey.types.js";
2
- import { HotkeyConflict, HotkeyConflictOptions } from "./conflicts.js";
3
- //#region src/recorder-options.d.ts
4
- /** Event property used to record a binding. Physical codes are the default. */
5
- export type RecorderKeyMode = 'code' | 'key';
6
- export interface RecorderRejection {
7
- reason: 'missing-code' | 'alt-graph' | 'invalid' | 'conflict' | 'validation';
8
- message: string;
9
- /** The rejected chord or sequence, when one was recorded. */
10
- hotkey?: Hotkey;
11
- conflicts?: Array<HotkeyConflict>;
12
- }
13
- export interface RecorderOptions {
14
- /** Physical code recording is the default. Choose key to record logical characters. */
15
- recordBy?: RecorderKeyMode;
16
- /** Platform used for Mod and AltGraph handling. Defaults to platform detection. */
17
- platform?: 'mac' | 'windows' | 'linux';
18
- /** Opt in to live-registry conflicts, including sequence prefixes. Default: false. */
19
- detectConflicts?: boolean | HotkeyConflictOptions;
20
- /** Rejected candidates leave recording active. */
21
- onReject?: (rejection: RecorderRejection) => void;
22
- }
23
- /** Validation context for a recorded chord or sequence. */
24
- export interface HotkeyRecorderValidationContext {
25
- /** The event that recorded the last chord. */
26
- event: KeyboardEvent;
27
- /** The last chord, parsed. */
28
- parsedHotkey: ParsedHotkey;
29
- /** The event that recorded each step. */
30
- events: ReadonlyArray<KeyboardEvent>;
31
- /** Every step, parsed. */
32
- parsedSequence: ReadonlyArray<ParsedHotkey>;
33
- }
34
- //#endregion