diff --git a/.changeset/physical-recording-hints-display.md b/.changeset/physical-recording-hints-display.md new file mode 100644 index 00000000..03c6d362 --- /dev/null +++ b/.changeset/physical-recording-hints-display.md @@ -0,0 +1,45 @@ +--- +'@tanstack/angular-hotkeys': minor +'@tanstack/hotkeys': minor +'@tanstack/hotkeys-devtools': minor +'@tanstack/lit-hotkeys': minor +'@tanstack/preact-hotkeys': minor +'@tanstack/preact-hotkeys-devtools': minor +'@tanstack/react-hotkeys': minor +'@tanstack/react-hotkeys-devtools': minor +'@tanstack/solid-hotkeys': minor +'@tanstack/solid-hotkeys-devtools': minor +'@tanstack/svelte-hotkeys': minor +'@tanstack/vue-hotkeys': minor +'@tanstack/vue-hotkeys-devtools': minor +--- + +- breaking: `RawHotkey` and `ParsedHotkey` are now unions containing either `key` or `code`. Use type intersections instead of extending them with interfaces. +- breaking: Recorders default to physical codes. Set `recordBy: 'key'` to record logical keys. +- breaking: Clearing a recording calls only `onClear`, without calling `onRecord` with an empty value. +- feat: Support typed physical bindings such as `Mod+[KeyS]` and `{ code: 'KeyS', mod: true }`. +- feat: Support F1–F24, additional named keys, and more punctuation shortcuts. +- feat: Add a platform option to `parseKeyboardEvent`. +- feat: Add recorder `recordBy`, `validate`, `detectConflicts`, and `onReject` options. +- feat: Add `findHotkeyConflicts`, including sequence-prefix checks. +- feat: Add `HotkeyMeta.group`. +- feat: Add `matchesHeldModifiers` and shortcut hint helpers for every framework adapter. +- feat: Add `formatForDisplay` support for parsed bindings, `parts`, separate modifier/key symbols, `keyLabels`, and a resolved `layoutMap`. +- fix: Improve shortcut matching across keyboard layouts, macOS Option keys, shifted punctuation, and numpad keys. +- fix: Prefer exact matches over physical-key fallbacks in hotkeys and sequences. +- fix: Handle literal plus shortcuts such as `Mod++` and Unicode key names correctly. +- fix: Avoid triggering shortcuts during IME composition and AltGraph character entry. +- fix: Clear held-key and `requireReset` state correctly when released keys produce different characters. +- fix: Prevent recording keystrokes, repeats, and releases from triggering registered shortcuts. +- fix: Display readable physical-key labels, such as `S` instead of `KeyS`. +- fix: Recognize equivalent hotkey aliases in registration lookups and conflict checks, including sequences. +- fix: Avoid unnecessary registration updates and handle target changes correctly in React and Preact. +- fix: Reject bracketed physical codes in logical `key` fields. +- fix: Reset shortcuts when keys are released inside inputs or while disabled. +- fix: Respect recorder `ignoreInputs` inside shadow roots. +- fix: Preserve Shift when recording logical AltGraph shortcuts. +- fix: Ignore modifier and composition events in standalone sequence matching. +- fix: Ignore repeated keydowns when matching sequences. +- fix: Prevent continuous rerenders when reading Preact hotkey registrations. +- fix: Allow Svelte recorder controls to be passed directly to event handlers. +- fix: Update devtools conflict reporting, plus-key formatting, group search, and key/code labels. diff --git a/docs/config.json b/docs/config.json index 8c4ce859..cebf6ddb 100644 --- a/docs/config.json +++ b/docs/config.json @@ -935,6 +935,14 @@ { "label": "IndividualKey", "to": "reference/type-aliases/IndividualKey" + }, + { + "label": "matchesHeldModifiers", + "to": "reference/functions/matchesHeldModifiers" + }, + { + "label": "HeldModifierOptions", + "to": "reference/interfaces/HeldModifierOptions" } ], "frameworks": [ @@ -952,6 +960,10 @@ { "label": "useHeldKeyCodes", "to": "framework/react/reference/functions/useHeldKeyCodes" + }, + { + "label": "useHotkeyHint", + "to": "framework/react/reference/functions/useHotkeyHint" } ] }, @@ -969,6 +981,10 @@ { "label": "useHeldKeyCodes", "to": "framework/preact/reference/functions/useHeldKeyCodes" + }, + { + "label": "useHotkeyHint", + "to": "framework/preact/reference/functions/useHotkeyHint" } ] }, @@ -986,6 +1002,10 @@ { "label": "createHeldKeyCodes", "to": "framework/solid/reference/functions/createHeldKeyCodes" + }, + { + "label": "createHotkeyHint", + "to": "framework/solid/reference/functions/createHotkeyHint" } ] }, @@ -1003,6 +1023,10 @@ { "label": "injectHeldKeyCodes", "to": "framework/angular/reference/functions/injectHeldKeyCodes" + }, + { + "label": "injectHotkeyHint", + "to": "framework/angular/reference/functions/injectHotkeyHint" } ] }, @@ -1020,6 +1044,10 @@ { "label": "useHeldKeyCodes", "to": "framework/vue/reference/functions/useHeldKeyCodes" + }, + { + "label": "useHotkeyHint", + "to": "framework/vue/reference/functions/useHotkeyHint" } ] }, @@ -1037,6 +1065,10 @@ { "label": "HeldKeyCodesController", "to": "framework/lit/reference/classes/HeldKeyCodesController" + }, + { + "label": "HotkeyHintController", + "to": "framework/lit/reference/classes/HotkeyHintController" } ] }, @@ -1066,6 +1098,10 @@ { "label": "SvelteHeldKeyCodesMap", "to": "framework/svelte/reference/interfaces/SvelteHeldKeyCodesMap" + }, + { + "label": "getHotkeyHint", + "to": "framework/svelte/reference/functions/getHotkeyHint" } ] } @@ -1087,6 +1123,34 @@ { "label": "HotkeyRecorderState", "to": "reference/interfaces/HotkeyRecorderState" + }, + { + "label": "findHotkeyConflicts", + "to": "reference/functions/findHotkeyConflicts" + }, + { + "label": "HotkeyConflictOptions", + "to": "reference/interfaces/HotkeyConflictOptions" + }, + { + "label": "HotkeyConflict", + "to": "reference/type-aliases/HotkeyConflict" + }, + { + "label": "RecorderRejection", + "to": "reference/interfaces/RecorderRejection" + }, + { + "label": "RecorderKeyMode", + "to": "reference/type-aliases/RecorderKeyMode" + }, + { + "label": "RecorderOptions", + "to": "reference/interfaces/RecorderOptions" + }, + { + "label": "HotkeyRecorderValidationContext", + "to": "reference/interfaces/HotkeyRecorderValidationContext" } ], "frameworks": [ @@ -1199,6 +1263,10 @@ { "label": "HotkeySequenceRecorderCommitKeys", "to": "reference/type-aliases/HotkeySequenceRecorderCommitKeys" + }, + { + "label": "HotkeySequenceRecorderValidationContext", + "to": "reference/interfaces/HotkeySequenceRecorderValidationContext" } ], "frameworks": [ @@ -1363,21 +1431,105 @@ { "label": "MODIFIER_KEYS", "to": "reference/variables/MODIFIER_KEYS" + }, + { + "label": "parseRegisterableHotkey", + "to": "reference/functions/parseRegisterableHotkey" + }, + { + "label": "areHotkeysEqual", + "to": "reference/functions/areHotkeysEqual" + }, + { + "label": "PhysicalKey", + "to": "reference/type-aliases/PhysicalKey" + }, + { + "label": "Hotkey", + "to": "reference/type-aliases/Hotkey" + }, + { + "label": "RegisterableHotkey", + "to": "reference/type-aliases/RegisterableHotkey" + }, + { + "label": "RawHotkey", + "to": "reference/type-aliases/RawHotkey" + }, + { + "label": "ParsedHotkey", + "to": "reference/type-aliases/ParsedHotkey" + }, + { + "label": "LogicalKey", + "to": "reference/type-aliases/LogicalKey" + }, + { + "label": "PhysicalKeyCode", + "to": "reference/type-aliases/PhysicalKeyCode" + }, + { + "label": "DisplayHotkey", + "to": "reference/type-aliases/DisplayHotkey" + }, + { + "label": "formatHotkeySequence", + "to": "reference/functions/formatHotkeySequence" + }, + { + "label": "parseHotkey", + "to": "reference/functions/parseHotkey" + }, + { + "label": "rawHotkeyToParsedHotkey", + "to": "reference/functions/rawHotkeyToParsedHotkey" + }, + { + "label": "parseKeyboardEvent", + "to": "reference/functions/parseKeyboardEvent" + }, + { + "label": "matchesKeyboardEvent", + "to": "reference/functions/matchesKeyboardEvent" + }, + { + "label": "validateHotkey", + "to": "reference/functions/validateHotkey" + }, + { + "label": "checkHotkey", + "to": "reference/functions/checkHotkey" + }, + { + "label": "assertValidHotkey", + "to": "reference/functions/assertValidHotkey" + }, + { + "label": "ValidationResult", + "to": "reference/interfaces/ValidationResult" } ] }, { "label": "Examples", - "children": [ - { - "label": "formatForDisplay", - "to": "framework/vanilla/examples/formatForDisplay" - } - ], + "children": [], "frameworks": [ + { + "label": "vanilla", + "children": [ + { + "label": "formatForDisplay", + "to": "framework/vanilla/examples/formatForDisplay" + } + ] + }, { "label": "react", "children": [ + { + "label": "Kitchen Sink", + "to": "framework/react/examples/kitchen-sink" + }, { "label": "useHotkey", "to": "framework/react/examples/useHotkey" @@ -1415,6 +1567,10 @@ { "label": "preact", "children": [ + { + "label": "Kitchen Sink", + "to": "framework/preact/examples/kitchen-sink" + }, { "label": "useHotkey", "to": "framework/preact/examples/useHotkey" @@ -1452,6 +1608,10 @@ { "label": "solid", "children": [ + { + "label": "Kitchen Sink", + "to": "framework/solid/examples/kitchen-sink" + }, { "label": "createHotkey", "to": "framework/solid/examples/createHotkey" @@ -1489,6 +1649,10 @@ { "label": "angular", "children": [ + { + "label": "Kitchen Sink", + "to": "framework/angular/examples/kitchen-sink" + }, { "label": "injectHotkey", "to": "framework/angular/examples/injectHotkey" @@ -1526,6 +1690,10 @@ { "label": "vue", "children": [ + { + "label": "Kitchen Sink", + "to": "framework/vue/examples/kitchen-sink" + }, { "label": "useHotkey", "to": "framework/vue/examples/useHotkey" @@ -1563,6 +1731,10 @@ { "label": "lit", "children": [ + { + "label": "Kitchen Sink", + "to": "framework/lit/examples/kitchen-sink" + }, { "label": "hotkey", "to": "framework/lit/examples/hotkey" @@ -1592,6 +1764,10 @@ { "label": "svelte", "children": [ + { + "label": "Kitchen Sink", + "to": "framework/svelte/examples/kitchen-sink" + }, { "label": "createHotkey", "to": "framework/svelte/examples/create-hotkey" diff --git a/docs/devtools.md b/docs/devtools.md index a6a2d90f..2a3aabb3 100644 --- a/docs/devtools.md +++ b/docs/devtools.md @@ -3,19 +3,19 @@ title: Devtools id: devtools --- -TanStack Hotkeys provides devtools for debugging and monitoring all your registered hotkeys in real-time. The devtools integrate seamlessly within the [TanStack Devtools](https://tanstack.com/devtools) multi-panel UI. +TanStack Hotkeys ships devtools for debugging and monitoring your registered hotkeys in real time, as a panel inside the [TanStack Devtools](https://tanstack.com/devtools) multi-panel UI. > [!NOTE] -> By default, the TanStack Devtools and TanStack Hotkeys Devtools will only be included in development mode. This helps keep your production bundle size minimal. If you need to include devtools in production builds (e.g., for debugging production issues), you can use the alternative "production" imports. +> By default, TanStack Devtools and the Hotkeys devtools are only included in development mode, so they add nothing to your production bundle. If you need devtools in a production build (say, to debug a production-only issue), use the alternative "production" imports. ## Features -The Hotkeys devtools panel provides: +The Hotkeys devtools panel lets you: -- **Registered Hotkeys List** - View all currently registered hotkeys with their options and status -- **Held Keys Display** - See which keys are currently being held down in real-time -- **Trigger Hotkeys** - Programmatically trigger hotkey callbacks for testing without pressing keys -- **Registration Details** - Inspect individual hotkey registrations including their target, event type, and conflict behavior +- View all currently registered hotkeys with their options and status +- See which keys are held down in real time +- Trigger hotkey callbacks for testing, without pressing the keys +- Inspect individual registrations, including their target, event type, and conflict behavior ## Installation @@ -49,7 +49,7 @@ Angular and Lit do not currently ship a dedicated hotkeys devtools adapter. ## Setup -### React Setup +### React setup ```tsx import { TanStackDevtools } from '@tanstack/react-devtools' @@ -60,7 +60,7 @@ function App() { } ``` -### Preact Setup +### Preact setup ```tsx import { TanStackDevtools } from '@tanstack/preact-devtools' @@ -71,7 +71,7 @@ export function App() { } ``` -### Solid Setup +### Solid setup ```tsx import { TanStackDevtools } from '@tanstack/solid-devtools' @@ -82,7 +82,7 @@ export function App() { } ``` -### Vue Setup +### Vue setup ```vue +`useSymbols` accepts a boolean or independent `modifiers` and `keys` settings. Omitted fields default to true. Modifier symbols apply on macOS; Windows and Linux retain modifier labels. -{formatForDisplay(hotkey)} +```ts +formatForDisplay('Shift+[ArrowUp]', { + platform: 'mac', + useSymbols: { modifiers: false, keys: true }, + parts: true, +}) // ['Shift', '↑'] + +formatForDisplay('Mod+[KeyS]', { + platform: 'mac', useSymbols: false, +}) // 'Cmd+S' + +formatForDisplay('Control++', { + platform: 'windows', separatorToken: ' · ', +}) // 'Ctrl · +' ``` -### Menu Items with Hotkeys +An empty separator joins labels directly. `undefined` or `null` uses the platform default. `formatWithLabels(binding, options)` is the shorthand for `formatForDisplay` with `useSymbols: false`. -```svelte - +```ts +const layoutMap = new Map([['KeyQ', 'a']]) -
+formatForDisplay('Mod+[KeyQ]', { platform: 'mac', layoutMap }) // '⌘ A' +formatForDisplay('Mod+Q', { platform: 'mac', layoutMap }) // '⌘ Q' ``` -## Validation +Any object with `get(code): string | undefined` works, including a browser `KeyboardLayoutMap`. Formatting stays synchronous. Your app owns loading, errors, and refreshing the map: render fallback labels while loading, then pass the resolved map on the next render. The library never requests it. Logical bindings ignore `layoutMap`. + +Use `keyLabels` for explicit labels keyed by physical code or normalized logical key: ```ts -import { validateHotkey } from '@tanstack/svelte-hotkeys' +formatForDisplay('Mod+[KeyQ]', { + platform: 'mac', layoutMap, keyLabels: { KeyQ: 'Action' }, +}) // '⌘ Action' +``` + +The precedence is `keyLabels`, then a layout entry, then the fallback label. Layout entries receive normal letter casing and key symbols; missing or empty entries fall back. Explicit labels are final. All of these options affect display only. + +## Parse and store bindings + +`parseHotkey` returns either a logical `key` or a physical `code`, plus resolved modifier flags. Narrow the union before inspecting the identity: -const result = validateHotkey('Alt+A') +```ts +import { parseHotkey, normalizeHotkeyFromParsed } from '@tanstack/svelte-hotkeys' + +const parsed = parseHotkey('Mod+[KeyS]', 'mac') +if (parsed.code !== undefined) { + console.log(parsed.code) // 'KeyS'; parsed.key is undefined +} +const stored = normalizeHotkeyFromParsed(parsed, 'mac') // 'Mod+[KeyS]' +formatForDisplay(stored, { platform: 'windows' }) // 'Ctrl+S' ``` + +Parsed modifiers are already resolved. To display a portable `Mod` binding on another platform, serialize with the original platform first, as above. `normalizeRegisterableHotkey` accepts strings or raw objects and preserves the logical/physical distinction. Do not store display labels or put a bracketed code in a logical `key` field. + +Use `validateHotkey` when accepting strings from an external source. It returns `valid`, `errors`, and `warnings`; it does not guarantee that a browser or operating system will deliver the shortcut. Recorder validation and live conflict checks are covered in the [recording guide](./hotkey-recording.md#validation-and-conflicts). + +Try these options together in the [vanilla formatter playground](../../../framework/vanilla/examples/formatForDisplay). diff --git a/docs/framework/svelte/guides/hotkey-recording.md b/docs/framework/svelte/guides/hotkey-recording.md index 93069ad1..34fa87fb 100644 --- a/docs/framework/svelte/guides/hotkey-recording.md +++ b/docs/framework/svelte/guides/hotkey-recording.md @@ -5,7 +5,9 @@ id: hotkey-recording TanStack Hotkeys provides the `createHotkeyRecorder` function for building shortcut customization UIs in Svelte. -## Basic Usage +Recorders default to physical codes: recording a shortcut stores a string such as `Mod+[KeyS]`. Pass it directly to your hotkey registration and use `formatForDisplay` for the label. Set `recordBy: 'key'` when you intentionally want the produced character instead. + +## Basic usage ```svelte ``` +### Changing a binding + +Pass a new logical or physical binding through your framework's normal state mechanism. A recorder result such as `Alt+[KeyS]` can be passed directly to the same registration API. Keep an initial binding in application state if you want a reset button; the library does not need a separate preferences store. + ## Default options Set defaults explicitly with `setHotkeysContext` when a subtree needs shared behavior: @@ -96,7 +117,7 @@ Set defaults explicitly with `setHotkeysContext` when a subtree needs shared beh ``` -## Common Options +## Common options ### `requireReset` @@ -123,13 +144,13 @@ createHotkey('Mod+S', () => save(), { conflictBehavior: 'replace' }) createHotkey('Mod+S', () => save(), { platform: 'mac' }) ``` -## Automatic Cleanup +## Automatic cleanup Global hotkeys are automatically unregistered when the owning component unmounts. Attachment-based hotkeys clean themselves up when the attached element is removed or when reactive inputs change. -## Registering Multiple Hotkeys +## Registering multiple hotkeys -When you need to register several hotkeys at once — or a dynamic, variable-length list — use `createHotkeys` (plural) for global shortcuts and `createHotkeysAttachment` for element-scoped shortcuts: +When you need to register several hotkeys at once, or a dynamic, variable-length list, use `createHotkeys` (plural) for global shortcuts and `createHotkeysAttachment` for element-scoped shortcuts: ```svelte ``` -### Common Options with Per-Hotkey Overrides +### Common options with per-hotkey overrides Pass shared options as the second argument. Per-definition options override the common ones: @@ -157,7 +178,7 @@ createHotkeys( ) ``` -### Dynamic Hotkey Lists +### Dynamic hotkey lists Pass a getter for reactive arrays: @@ -176,7 +197,7 @@ Pass a getter for reactive arrays: ``` -### Scoped Multi-Hotkeys +### Scoped multi-hotkeys Use `createHotkeysAttachment` to scope multiple hotkeys to a specific element: @@ -193,9 +214,9 @@ Use `createHotkeysAttachment` to scope multiple hotkeys to a specific element:{reg.meta.description}
+ {#if reg.options.meta?.description} +{reg.options.meta.description}
{/if}{displayedSteps.map((step) => formatForDisplay(step)).join(' → ')}
+{#if recorder.isRecording} + + +{/if} +``` + +## State and controls + +The recorder exposes `isRecording`, `steps`, and `recordedSequence` as reactive getters. `steps` contains the current attempt; `recordedSequence` contains the last committed result. `startRecording()` begins a new session. `commitRecording()` saves a nonempty attempt, while `cancelRecording()` discards it and calls `onCancel`. `stopRecording()` resets recorder state without calling `onRecord` or `onCancel`. + +## Options and keyboard behavior + +- `recordBy`: `'code'` by default; `'key'` records produced logical characters. +- `commitKeys`: `'enter'` by default; plain Enter commits a nonempty sequence. `'none'` lets Enter become a step and requires manual or idle commit. +- `commitOnEnter: false`: also permits Enter as a step when `commitKeys` is `'enter'`. +- `idleTimeoutMs`: optionally commits after inactivity following a completed step. No timer runs before the first step. +- `ignoreInputs`: true by default, including shadow-root inputs. Set false to record from editable fields. Escape still cancels from an input. + +Escape cancels. Unmodified Backspace/Delete removes the last step; when already empty it stops and calls only `onClear`. Modifier-only presses, automatic repeats, and IME composition do not append steps. Recorded events and their releases do not trigger application shortcuts. + +Set provider defaults through `HotkeysProvider` with `defaultOptions.hotkeySequenceRecorder`. + +## Validation and conflicts + +`validate(sequence, { events, parsedSequence })` runs at commit. Return true to accept, or false/a message to reject. `detectConflicts` checks live bindings and sequence prefixes, including physical/logical overlap established by the recorded events. `onReject` receives feedback; rejected steps remain editable with Backspace. + +The shared options and exclusions are described in the [hotkey recording guide](./hotkey-recording.md#validation-and-conflicts). The application owns reset, persistence, and any binding being edited; clearing never calls `onRecord([])`. diff --git a/docs/framework/svelte/guides/sequences.md b/docs/framework/svelte/guides/sequences.md index fb2aaba0..b2d26c69 100644 --- a/docs/framework/svelte/guides/sequences.md +++ b/docs/framework/svelte/guides/sequences.md @@ -5,6 +5,8 @@ id: sequences TanStack Hotkeys supports multi-key sequences in Svelte, where keys are pressed one after another rather than simultaneously. +Sequence steps use the same string syntax as single hotkeys. For example, `['[KeyG]', '[KeyG]']` follows a physical position, while `['G', 'G']` follows the logical letter. A sequence can mix forms, such as `['Mod+[KeyK]', 'C']`. Display steps with `sequence.map((step) => formatForDisplay(step)).join(' → ')`. + ## Global sequences ```svelte @@ -50,6 +52,10 @@ Use `createHotkeySequenceAttachment` when a sequence should only be active while ``` +## Matching steps + +Both `SequenceManager` and `createSequenceMatcher` ignore modifier-only events, IME composition, and automatic keydown repeats. These events neither advance the sequence nor refresh its timeout: holding G does not complete a two-press G sequence. The manager prefers exact matches over weaker logical-key fallbacks while preserving equally strong matches. + ## Sequence options ```ts @@ -61,7 +67,7 @@ createHotkeySequence(['G', 'G'], callback, { ### Reactive `enabled` -When disabled, the sequence **stays registered** (visible in devtools); only execution is suppressed. +When disabled, the sequence stays registered (visible in devtools); only execution is suppressed. ```svelte ``` -## Next Steps +## Next steps - [Hotkeys Guide](./guides/hotkeys) - [Sequences Guide](./guides/sequences) diff --git a/docs/framework/svelte/reference/functions/createHotkeyRecorder.md b/docs/framework/svelte/reference/functions/createHotkeyRecorder.md index feb27572..265d7e7a 100644 --- a/docs/framework/svelte/reference/functions/createHotkeyRecorder.md +++ b/docs/framework/svelte/reference/functions/createHotkeyRecorder.md @@ -7,7 +7,7 @@ title: createHotkeyRecorder function createHotkeyRecorder(options): SvelteHotkeyRecorder; ``` -Defined in: [packages/svelte-hotkeys/src/createHotkeyRecorder.svelte.ts:98](https://github.com/TanStack/hotkeys/blob/main/packages/svelte-hotkeys/src/createHotkeyRecorder.svelte.ts#L98) +Defined in: [packages/svelte-hotkeys/src/createHotkeyRecorder.svelte.ts:101](https://github.com/TanStack/hotkeys/blob/main/packages/svelte-hotkeys/src/createHotkeyRecorder.svelte.ts#L101) ## Parameters diff --git a/docs/framework/svelte/reference/functions/createHotkeySequenceRecorder.md b/docs/framework/svelte/reference/functions/createHotkeySequenceRecorder.md index aae69ff4..263480aa 100644 --- a/docs/framework/svelte/reference/functions/createHotkeySequenceRecorder.md +++ b/docs/framework/svelte/reference/functions/createHotkeySequenceRecorder.md @@ -7,7 +7,7 @@ title: createHotkeySequenceRecorder function createHotkeySequenceRecorder(options): SvelteHotkeySequenceRecorder; ``` -Defined in: [packages/svelte-hotkeys/src/createHotkeySequenceRecorder.svelte.ts:73](https://github.com/TanStack/hotkeys/blob/main/packages/svelte-hotkeys/src/createHotkeySequenceRecorder.svelte.ts#L73) +Defined in: [packages/svelte-hotkeys/src/createHotkeySequenceRecorder.svelte.ts:77](https://github.com/TanStack/hotkeys/blob/main/packages/svelte-hotkeys/src/createHotkeySequenceRecorder.svelte.ts#L77) Svelte helper for recording multi-chord sequences (Vim-style shortcuts). diff --git a/docs/framework/svelte/reference/functions/getHotkeyHint.md b/docs/framework/svelte/reference/functions/getHotkeyHint.md new file mode 100644 index 00000000..f654df60 --- /dev/null +++ b/docs/framework/svelte/reference/functions/getHotkeyHint.md @@ -0,0 +1,30 @@ +--- +id: getHotkeyHint +title: getHotkeyHint +--- + +```ts +function getHotkeyHint(hotkey, options): SvelteHotkeyHint; +``` + +Defined in: [packages/svelte-hotkeys/src/getHotkeyHint.svelte.ts:18](https://github.com/TanStack/hotkeys/blob/main/packages/svelte-hotkeys/src/getHotkeyHint.svelte.ts#L18) + +Returns an object with a reactive `visible` getter indicating whether held modifiers reveal this shortcut. +Pass getters to track changing bindings and options. +Uses the nonempty-subset/AltGr rules of `matchesHeldModifiers`; `exact` requires +every modifier. Combine with the action's enabled state before showing a badge. +This helper does not register a shortcut or check whether its target is focused. + +## Parameters + +### hotkey + +`MaybeGetter`\<`RegisterableHotkey`\> + +### options + +`MaybeGetter`\<`HeldModifierOptions`\> = `{}` + +## Returns + +[`SvelteHotkeyHint`](../interfaces/SvelteHotkeyHint.md) diff --git a/docs/framework/svelte/reference/index.md b/docs/framework/svelte/reference/index.md index 603a9da0..a3277bff 100644 --- a/docs/framework/svelte/reference/index.md +++ b/docs/framework/svelte/reference/index.md @@ -14,6 +14,7 @@ title: "@tanstack/svelte-hotkeys" - [SvelteHeldKeyCodesMap](interfaces/SvelteHeldKeyCodesMap.md) - [SvelteHeldKeys](interfaces/SvelteHeldKeys.md) - [SvelteHeldKeyState](interfaces/SvelteHeldKeyState.md) +- [SvelteHotkeyHint](interfaces/SvelteHotkeyHint.md) - [SvelteHotkeyRecorder](interfaces/SvelteHotkeyRecorder.md) - [SvelteHotkeyRegistrations](interfaces/SvelteHotkeyRegistrations.md) - [SvelteHotkeySequenceRecorder](interfaces/SvelteHotkeySequenceRecorder.md) @@ -42,6 +43,7 @@ title: "@tanstack/svelte-hotkeys" - [getDefaultHotkeysOptions](functions/getDefaultHotkeysOptions.md) - [getHeldKeyCodesMap](functions/getHeldKeyCodesMap.md) - [getHeldKeys](functions/getHeldKeys.md) +- [getHotkeyHint](functions/getHotkeyHint.md) - [getHotkeyRegistrations](functions/getHotkeyRegistrations.md) - [getHotkeysContext](functions/getHotkeysContext.md) - [getIsKeyHeld](functions/getIsKeyHeld.md) diff --git a/docs/framework/svelte/reference/interfaces/SvelteHotkeyHint.md b/docs/framework/svelte/reference/interfaces/SvelteHotkeyHint.md new file mode 100644 index 00000000..7e2e0447 --- /dev/null +++ b/docs/framework/svelte/reference/interfaces/SvelteHotkeyHint.md @@ -0,0 +1,16 @@ +--- +id: SvelteHotkeyHint +title: SvelteHotkeyHint +--- + +Defined in: [packages/svelte-hotkeys/src/getHotkeyHint.svelte.ts:7](https://github.com/TanStack/hotkeys/blob/main/packages/svelte-hotkeys/src/getHotkeyHint.svelte.ts#L7) + +## Properties + +### visible + +```ts +readonly visible: boolean; +``` + +Defined in: [packages/svelte-hotkeys/src/getHotkeyHint.svelte.ts:8](https://github.com/TanStack/hotkeys/blob/main/packages/svelte-hotkeys/src/getHotkeyHint.svelte.ts#L8) diff --git a/docs/framework/vue/guides/formatting-display.md b/docs/framework/vue/guides/formatting-display.md index cb71a9b4..baca7bf5 100644 --- a/docs/framework/vue/guides/formatting-display.md +++ b/docs/framework/vue/guides/formatting-display.md @@ -3,69 +3,108 @@ title: Formatting & Display Guide id: formatting-display --- -TanStack Hotkeys includes utilities for turning hotkey strings into display-friendly labels. These utilities are framework-agnostic, but they pair naturally with Vue templates and computed UI. +Use `formatForDisplay` whenever a binding appears in a menu, button, hint, or shortcut settings panel. It accepts logical strings, bracketed physical strings, raw objects, and parsed bindings. Store the original binding and format it at render time: the display label is not a registration string. -## `formatForDisplay` +## Format a binding ```ts import { formatForDisplay } from '@tanstack/vue-hotkeys' -formatForDisplay('Mod+S') -formatForDisplay('Mod+Shift+Z') +formatForDisplay('Mod+S', { platform: 'mac' }) // '⌘ S' +formatForDisplay('Mod+[KeyS]', { platform: 'mac' }) // '⌘ S' +formatForDisplay({ code: 'KeyS', mod: true }, { platform: 'windows' }) // 'Ctrl+S' +formatForDisplay('Mod+[Digit2]', { platform: 'windows' }) // 'Ctrl+2' ``` -## `formatWithLabels` +The same label can represent different bindings. `Mod+S` follows a logical letter; `Mod+[KeyS]` follows a physical position. Physical labels shorten `KeyS` to `S` and `Digit2` to `2`, preserve readable numpad labels, and reuse punctuation and special-key symbols. This does not change the stored code or infer the user's layout. -```ts -import { formatWithLabels } from '@tanstack/vue-hotkeys' +Omit `platform` to use detection. On macOS the default joins modifier symbols with spaces; Windows and Linux use labels joined with `+`. + +## Render individual keycaps + +Set `parts: true` to get one label per key instead of a joined string: -formatWithLabels('Mod+S') -formatWithLabels('Mod+Shift+Z') +```ts +const binding = 'Mod+[KeyS]' +const parts = formatForDisplay(binding, { platform: 'mac', parts: true }) +// ['⌘', 'S'] — render each part in its own ``` -## Using Formatted Hotkeys in Vue +With `parts` omitted or false, the result is a string. A runtime boolean returns `string | string[]`. Parts preserve literal plus keys and ignore `separatorToken`. -### Keyboard Shortcut Badges +Sequences are arrays of bindings. Format their steps individually: -```vue - +const sequence: HotkeySequence = ['Mod+[KeyK]', 'C'] +const label = sequence.map((step) => formatForDisplay(step)).join(' → ') +``` + +`formatHotkeySequence` only joins stored strings with spaces. It intentionally retains brackets and code names, so use the code above for user-facing labels. + +## Choose symbols and separators - - {{ formatForDisplay(hotkey) }} - +`useSymbols` accepts a boolean or independent `modifiers` and `keys` settings. Omitted fields default to true. Modifier symbols apply on macOS; Windows and Linux retain modifier labels. + +```ts +formatForDisplay('Shift+[ArrowUp]', { + platform: 'mac', + useSymbols: { modifiers: false, keys: true }, + parts: true, +}) // ['Shift', '↑'] + +formatForDisplay('Mod+[KeyS]', { + platform: 'mac', useSymbols: false, +}) // 'Cmd+S' + +formatForDisplay('Control++', { + platform: 'windows', separatorToken: ' · ', +}) // 'Ctrl · +' ``` -### Menu Items with Hotkeys +An empty separator joins labels directly. `undefined` or `null` uses the platform default. `formatWithLabels(binding, options)` is the shorthand for `formatForDisplay` with `useSymbols: false`. -```vue - +```ts +const layoutMap = new Map([['KeyQ', 'a']]) - - - +formatForDisplay('Mod+[KeyQ]', { platform: 'mac', layoutMap }) // '⌘ A' +formatForDisplay('Mod+Q', { platform: 'mac', layoutMap }) // '⌘ Q' ``` -## Validation +Any object with `get(code): string | undefined` works, including a browser `KeyboardLayoutMap`. Formatting stays synchronous. Your app owns loading, errors, and refreshing the map: render fallback labels while loading, then pass the resolved map on the next render. The library never requests it. Logical bindings ignore `layoutMap`. + +Use `keyLabels` for explicit labels keyed by physical code or normalized logical key: ```ts -import { validateHotkey } from '@tanstack/vue-hotkeys' +formatForDisplay('Mod+[KeyQ]', { + platform: 'mac', layoutMap, keyLabels: { KeyQ: 'Action' }, +}) // '⌘ Action' +``` + +The precedence is `keyLabels`, then a layout entry, then the fallback label. Layout entries receive normal letter casing and key symbols; missing or empty entries fall back. Explicit labels are final. All of these options affect display only. + +## Parse and store bindings -const result = validateHotkey('Alt+A') +`parseHotkey` returns either a logical `key` or a physical `code`, plus resolved modifier flags. Narrow the union before inspecting the identity: + +```ts +import { parseHotkey, normalizeHotkeyFromParsed } from '@tanstack/vue-hotkeys' + +const parsed = parseHotkey('Mod+[KeyS]', 'mac') +if (parsed.code !== undefined) { + console.log(parsed.code) // 'KeyS'; parsed.key is undefined +} +const stored = normalizeHotkeyFromParsed(parsed, 'mac') // 'Mod+[KeyS]' +formatForDisplay(stored, { platform: 'windows' }) // 'Ctrl+S' ``` + +Parsed modifiers are already resolved. To display a portable `Mod` binding on another platform, serialize with the original platform first, as above. `normalizeRegisterableHotkey` accepts strings or raw objects and preserves the logical/physical distinction. Do not store display labels or put a bracketed code in a logical `key` field. + +Use `validateHotkey` when accepting strings from an external source. It returns `valid`, `errors`, and `warnings`; it does not guarantee that a browser or operating system will deliver the shortcut. Recorder validation and live conflict checks are covered in the [recording guide](./hotkey-recording.md#validation-and-conflicts). + +Try these options together in the [vanilla formatter playground](../../../framework/vanilla/examples/formatForDisplay). diff --git a/docs/framework/vue/guides/hotkey-recording.md b/docs/framework/vue/guides/hotkey-recording.md index ee7cd7d1..6900da1c 100644 --- a/docs/framework/vue/guides/hotkey-recording.md +++ b/docs/framework/vue/guides/hotkey-recording.md @@ -5,7 +5,9 @@ id: hotkey-recording TanStack Hotkeys provides the `useHotkeyRecorder` composable for building shortcut customization UIs in Vue. -## Basic Usage +Recorders default to physical codes: recording a shortcut stores a string such as `Mod+[KeyS]`. Pass it directly to your hotkey registration and use `formatForDisplay` for the label. Set `recordBy: 'key'` when you intentionally want the produced character instead. + +## Basic usage ```vue ``` -### Common Options with Per-Hotkey Overrides +### Common options with per-hotkey overrides Pass shared options as the second argument. Per-definition options override the common ones: @@ -159,7 +180,7 @@ useHotkeys( ) ``` -### Dynamic Hotkey Lists +### Dynamic hotkey lists Pass a getter or computed ref as the first argument for reactive arrays: @@ -181,9 +202,9 @@ useHotkeys( The composable watches for changes and diffs registrations automatically. -## Metadata (name & description) +## Metadata (name, description, and group) -Every hotkey registration can carry a `meta` object with a `name` and `description`. This metadata is informational only -- it does not affect hotkey behavior -- but it flows through to registrations and devtools, making it easy to build shortcut palettes and help screens. +Every hotkey registration can carry a `meta` object with a `name`, `description`, and `group`. Metadata never affects hotkey behavior, but it flows through to registrations and devtools, so you can build shortcut palettes and help screens from it. ```ts useHotkey('Mod+S', () => save(), { @@ -191,13 +212,12 @@ useHotkey('Mod+S', () => save(), { }) ``` -The `meta` option is typed as `HotkeyMeta`, which ships with `name` and `description` fields. You can extend it with additional properties using TypeScript declaration merging: +The `meta` option is typed as `HotkeyMeta`, which ships with `name`, `description`, and `group` fields. You can extend it with additional properties using TypeScript declaration merging: ```ts declare module '@tanstack/hotkeys' { interface HotkeyMeta { icon?: string - group?: string } } @@ -206,13 +226,15 @@ useHotkey('Mod+S', () => save(), { }) ``` -## Introspecting Registrations +Group is descriptive metadata, not an execution scope. A shortcuts panel can group live registration views directly. Disabled registrations remain listed; unmounted registrations disappear. + +## Introspecting registrations -Use the `useHotkeyRegistrations` composable to get a live view of all hotkey and sequence registrations. This is useful for building shortcut palettes, help dialogs, or devtools. +Use the `useHotkeyRegistrations` composable to get a live view of all hotkey and sequence registrations. It works well for shortcut palettes, help dialogs, and devtools. ```vue @@ -221,18 +243,18 @@ const { hotkeys, sequences } = useHotkeyRegistrations(){{ reg.meta.description }}
+{{ reg.options.meta.description }}