UNPKG

@tanstack/hotkeys

Version:

Type-safe, framework-agnostic keyboard hotkey management for the browser

181 lines • 7.39 kB
import { Hotkey, HotkeyChord, HotkeySequence, ParsedHotkeyChord } from "./hotkey.types.js"; import { HotkeyConflict, HotkeyConflictOptions } from "./conflicts.js"; import { Store } from "@tanstack/store"; //#region src/hotkey-recorder.d.ts /** Event property used to record a binding. Physical codes are the default. */ export type RecorderKeyMode = 'code' | 'key'; export interface RecorderRejection { reason: 'missing-code' | 'alt-graph' | 'invalid' | 'conflict' | 'validation'; message: string; /** The rejected chord or sequence, when one was recorded. */ hotkey?: Hotkey; conflicts?: Array<HotkeyConflict>; } export interface RecorderOptions { /** Physical code recording is the default. Choose key to record logical characters. */ recordBy?: RecorderKeyMode; /** Platform used for Mod and AltGraph handling. Defaults to platform detection. */ platform?: 'mac' | 'windows' | 'linux'; /** Opt in to live-registry conflicts, including sequence prefixes. Default: false. */ detectConflicts?: boolean | HotkeyConflictOptions; /** Rejected candidates leave recording active. */ onReject?: (rejection: RecorderRejection) => void; } /** Validation context for a recorded chord. */ export interface HotkeyChordRecorderValidationContext { kind: 'chord'; /** The event that recorded the chord. */ event: KeyboardEvent; /** The chord with `Mod` resolved for the recorder's platform. */ parsedChord: ParsedHotkeyChord; } /** Validation context for a recorded sequence. */ export interface HotkeySequenceRecorderValidationContext { kind: 'sequence'; /** The event that recorded each step. */ events: ReadonlyArray<KeyboardEvent>; /** Each step with `Mod` resolved for the recorder's platform. */ parsedSequence: Array<ParsedHotkeyChord>; } /** * Validation context for a recorded chord or sequence. Narrow with `kind` to * read `parsedChord` or `parsedSequence`. */ export type HotkeyRecorderValidationContext = HotkeyChordRecorderValidationContext | HotkeySequenceRecorderValidationContext; /** * State interface for the HotkeyRecorder. */ export interface HotkeyRecorderState { /** Whether recording is currently active */ isRecording: boolean; /** Chords recorded so far in a multi-step recording (empty for `maxSteps: 1`) */ steps: HotkeySequence; /** The last committed hotkey: a chord, or a sequence of two or more steps */ recordedHotkey: Hotkey | null; } /** Which keys commit a multi-step recording before it reaches `maxSteps`. */ export type HotkeyRecorderCommitKeys = 'enter' | 'none'; interface HotkeyRecorderBaseOptions extends RecorderOptions { /** Optional callback when recording is cancelled (Escape pressed) */ onCancel?: () => void; /** * Optional callback when the recording is cleared: Backspace/Delete with no * modifiers and no recorded steps */ onClear?: () => void; /** * Whether to ignore keyboard events from input-like elements (text inputs, * textarea, select, contenteditable) while recording. When true, typing in * inputs passes through normally (Escape still cancels). Defaults to true. */ ignoreInputs?: boolean; } /** Options for recording a single chord. This is the default (`maxSteps: 1`). */ export interface HotkeyChordRecorderOptions extends HotkeyRecorderBaseOptions { /** Maximum number of chords to record. Default: 1 */ maxSteps?: 1; /** Return true to accept, or false/a message to reject while staying in recording mode. */ validate?: (hotkey: HotkeyChord, context: HotkeyChordRecorderValidationContext) => boolean | string; /** Callback when a chord is successfully recorded */ onRecord: (hotkey: HotkeyChord) => void; } /** * Options for recording up to `maxSteps` chords. A one-step recording commits * as a chord string; two or more steps commit as a sequence. */ export interface HotkeySequenceRecorderOptions extends HotkeyRecorderBaseOptions { /** Maximum number of chords to record. Reaching it commits automatically. Use `Infinity` for no limit. */ maxSteps: number; /** Return true to accept, or false/a message to reject while staying in recording mode. */ validate?: (hotkey: Hotkey, context: HotkeyRecorderValidationContext) => boolean | string; /** Callback when a hotkey is successfully recorded */ onRecord: (hotkey: Hotkey) => void; /** Whether plain Enter commits the recorded steps. Default: 'enter' */ commitKeys?: HotkeyRecorderCommitKeys; /** Commit automatically after this many milliseconds without a new step. Disabled when unset or 0. */ idleTimeoutMs?: number; } /** * Options for HotkeyRecorder. `maxSteps` (default 1) selects between recording * a single chord and recording a sequence. */ export type HotkeyRecorderOptions = HotkeyChordRecorderOptions | HotkeySequenceRecorderOptions; /** * Framework-agnostic class for recording keyboard shortcuts. * * With the default `maxSteps: 1`, the first valid chord commits. With a larger * `maxSteps`, each chord adds a step; the recording commits on Enter (unless * `commitKeys: 'none'`), after `idleTimeoutMs`, or when it reaches `maxSteps`. * Backspace/Delete removes the last step, and on an empty recording calls * `onClear`. Escape cancels. A one-step result is a chord string. * * State Management: * - Uses TanStack Store for reactive state management * - State can be accessed via `recorder.store.state` when using the class directly * - When using framework adapters, use the `useHotkeyRecorder` hook for reactive state * * @example * ```ts * const recorder = new HotkeyRecorder({ * onRecord: (hotkey) => { * console.log('Recorded:', hotkey) // 'Mod+S' * }, * }) * * const sequenceRecorder = new HotkeyRecorder({ * maxSteps: Infinity, * onRecord: (hotkey) => { * console.log('Recorded:', hotkey) // 'Mod+S' or ['G', 'H'] * }, * }) * * recorder.start() * // ... later * recorder.destroy() * ``` */ export declare class HotkeyRecorder { #private; readonly store: Store<HotkeyRecorderState>; constructor(options: HotkeyChordRecorderOptions | (() => HotkeyChordRecorderOptions)); constructor(options: HotkeySequenceRecorderOptions | (() => HotkeySequenceRecorderOptions)); /** * Updates the recorder options, including callbacks. * This allows framework adapters to sync callback changes without recreating the recorder. */ setOptions(options: Partial<HotkeyRecorderOptions>): void; /** * Start recording a new hotkey. * * Sets up a keydown event listener that captures keyboard events * and records chords according to `maxSteps`. */ start(): void; /** * Commits the recorded steps after validation and conflict checks. * Called automatically at `maxSteps`, on Enter, or after `idleTimeoutMs`. * A rejected commit leaves recording active with its steps intact. */ commit(): void; /** * Stop recording (same as cancel, but doesn't call onCancel). * * Removes the event listener and resets the recording state. */ stop(): void; /** * Cancel recording without saving. * * Removes the event listener, resets the recording state, and calls * the onCancel callback if provided. */ cancel(): void; /** * Clean up event listeners and reset state. * * Call this when you're done with the recorder to ensure * all event listeners are properly removed. */ destroy(): void; } //#endregion