@tanstack/hotkeys
Version:
Type-safe, framework-agnostic keyboard hotkey management for the browser
242 lines • 11.5 kB
TypeScript
import { Hotkey, HotkeyChord, HotkeyMeta, HotkeySequence } from "./hotkey.types.js";
import { HotkeyChordOptions, HotkeyRegistrationView, HotkeySequenceOptions } from "./hotkey-manager.js";
import { Store } from "@tanstack/store";
//#region src/app-hotkeys.d.ts
type Platform = 'mac' | 'windows' | 'linux';
type DeclaredOptionKeys = 'capture' | 'conflictBehavior' | 'eventType' | 'ignoreInputs' | 'preventDefault' | 'stopPropagation';
/** Declared options for an app hotkey whose default is a chord. */
export type AppHotkeyChordOptions = Pick<HotkeyChordOptions, DeclaredOptionKeys | 'requireReset'>;
/** Declared options for an app hotkey whose default is a sequence. */
export type AppHotkeySequenceOptions = Pick<HotkeySequenceOptions, DeclaredOptionKeys | 'timeout'>;
/** Every declared option. Kind-specific options apply only while the effective hotkey has that kind. */
export type AppHotkeyOptions = AppHotkeyChordOptions & AppHotkeySequenceOptions;
/** One declaration passed to `defineAppHotkeys`. */
export interface AppHotkeyInput {
/** Default hotkey: a chord, a sequence, or `null` for an action without a default. */
hotkey: Hotkey | null;
/** Display metadata for cheat sheets and settings screens. */
meta?: HotkeyMeta;
/**
* Scope this hotkey fires in, such as `'editor'`. Overrides the scope of the
* provider it registers under. App hotkeys in different scopes may share a
* hotkey; one in the `'global'` scope conflicts with any app hotkey that
* shares its hotkey.
*/
scope?: string;
/**
* Options that describe the hotkey itself. Chord defaults accept
* `requireReset`; sequence defaults accept `timeout`.
*/
options?: AppHotkeyOptions;
}
/**
* A declared app hotkey. It never holds a callback: register it with
* `useHotkey(appHotkey, callback)` or the equivalent adapter API.
*
* `TDefault` is the kind of the declared default (`HotkeyChord`,
* `HotkeySequence`, or `null`). Adapters use it to type kind-specific options.
*/
export interface AppHotkey<TId extends string = string, TDefault extends HotkeyChord | HotkeySequence | null = HotkeyChord | HotkeySequence | null> {
/** Brand that distinguishes app hotkeys from raw hotkey objects. */
readonly '~appHotkey': true;
/** Stable id. Saved custom hotkeys are keyed by this value. */
readonly id: TId;
/** Effective hotkey: the user's custom hotkey, or the default. `null` means unbound. */
readonly hotkey: Hotkey | null;
/** Hotkey declared in `defineAppHotkeys`. */
readonly defaultHotkey: TDefault;
readonly meta?: HotkeyMeta;
readonly scope?: string;
readonly options?: AppHotkeyOptions;
}
/** A map of app hotkeys keyed by id. */
export type AppHotkeys = Readonly<Record<string, AppHotkey>>;
type AllowedOptions<THotkey> = THotkey extends HotkeySequence ? AppHotkeySequenceOptions : THotkey extends null ? AppHotkeyOptions : AppHotkeyChordOptions;
type StrictOptions<TOptions, TAllowed> = TOptions extends object ? { [P in Exclude<keyof TOptions, keyof TAllowed>]: never; } & TAllowed : TAllowed;
/** Rejects options that do not apply to each declaration's default kind. */
type ValidatedAppHotkeyInputs<T> = { [K in keyof T]: T[K] extends {
hotkey: infer THotkey;
} ? Omit<T[K], 'options'> & {
options?: StrictOptions<T[K] extends {
options: infer TOptions;
} ? TOptions : {}, AllowedOptions<THotkey>>;
} : never; };
/** The kind of a declared default hotkey; a one-step sequence is a chord. */
export type AppHotkeyDefaultKind<THotkey> = THotkey extends null ? null : THotkey extends readonly [unknown] ? HotkeyChord : THotkey extends ReadonlyArray<unknown> ? HotkeySequence : HotkeyChord;
/** The app hotkeys returned by `defineAppHotkeys` for a declaration object. */
export type DefinedAppHotkeys<T extends Record<string, AppHotkeyInput>> = { readonly [K in keyof T & string]: AppHotkey<K, AppHotkeyDefaultKind<T[K]['hotkey']>>; };
/**
* Register your app hotkeys to type `useAppHotkeys()` and `CustomHotkeys`.
*
* @example
* ```ts
* declare module '@tanstack/react-hotkeys' {
* interface Register {
* appHotkeys: typeof appHotkeys
* }
* }
* ```
*/
export interface Register {}
/** The app hotkeys type from `Register`, or a generic map when none is registered. */
export type RegisteredAppHotkeys = Register extends {
appHotkeys: infer T extends AppHotkeys;
} ? T : AppHotkeys;
/** Ids of the registered app hotkeys. */
export type AppHotkeyId = keyof RegisteredAppHotkeys & string;
/**
* The user's hotkeys that differ from the declared defaults, keyed by app
* hotkey id. This is the shape to save. `null` means the user unbound it.
*/
export type CustomHotkeys<T extends AppHotkeys = RegisteredAppHotkeys> = { -readonly [K in keyof T]?: Hotkey | null; };
/**
* Declares every app hotkey up front, without registering any, so cheat
* sheets and settings screens can list hotkeys whose components have not
* mounted. Pass the result to `HotkeysProvider` as `appHotkeys`.
*
* @example
* ```ts
* export const appHotkeys = defineAppHotkeys({
* 'doc.save': { hotkey: 'Mod+S', meta: { name: 'Save', group: 'Document' } },
* 'nav.home': { hotkey: ['G', 'H'], meta: { name: 'Go home' }, options: { timeout: 800 } },
* 'palette.open': { hotkey: null, meta: { name: 'Command palette' } },
* })
* ```
*/
export declare function defineAppHotkeys<const T extends Record<string, AppHotkeyInput>>(definitions: T & ValidatedAppHotkeyInputs<T>): DefinedAppHotkeys<T>;
/** Whether a value is an app hotkey created by `defineAppHotkeys`. */
export declare function isAppHotkey(value: unknown): value is AppHotkey;
/** Whether an app hotkey's effective hotkey differs from its default. */
export declare function isCustomHotkey(appHotkey: AppHotkey, platform?: Platform): boolean;
/**
* Status of an app hotkey in the running app.
* - `'active'`: at least one enabled registration
* - `'suspended'`: has an enabled registration, but none in an active scope
* - `'disabled'`: registered, but every registration has `enabled: false`
* - `'inactive'`: has a hotkey, but nothing has registered it
* - `'unbound'`: no hotkey assigned
*/
export type AppHotkeyStatus = 'active' | 'suspended' | 'disabled' | 'inactive' | 'unbound';
/** One row of an app hotkey cheat sheet. */
export interface AppHotkeyListItem<T extends AppHotkey = AppHotkey> {
appHotkey: T;
status: AppHotkeyStatus;
/** Whether the effective hotkey differs from the declared default. */
isCustom: boolean;
/** Live registrations of this app hotkey (for example, one per list row). */
registrations: Array<HotkeyRegistrationView>;
}
/** State of an `AppHotkeysStore`. */
export interface AppHotkeysState<T extends AppHotkeys = AppHotkeys> {
/** Effective app hotkeys: declarations with custom hotkeys applied. */
appHotkeys: T;
/** The user's hotkeys that differ from the defaults. */
customHotkeys: CustomHotkeys<T>;
}
/** Options for `AppHotkeysStore`. */
export interface AppHotkeysStoreOptions<T extends AppHotkeys = AppHotkeys> {
/** Starting custom hotkeys, such as saved data. Invalid entries are ignored with a warning. */
customHotkeys?: CustomHotkeys<T> | null;
/** Called with the custom hotkeys after every change, ready to save. */
onCustomHotkeysChange?: (customHotkeys: CustomHotkeys<T>) => void;
/** Platform used to compare hotkeys. Defaults to detection. */
platform?: Platform;
}
/**
* Holds declared app hotkeys and the user's custom hotkeys. Framework
* providers create one and expose it through hooks; devtools read it from
* `HotkeyManager.appHotkeys`.
*
* @example
* ```ts
* const store = new AppHotkeysStore(appHotkeys, {
* customHotkeys: JSON.parse(localStorage.getItem('hotkeys') ?? '{}'),
* onCustomHotkeysChange: (custom) =>
* localStorage.setItem('hotkeys', JSON.stringify(custom)),
* })
* store.setCustomHotkey('doc.save', 'Mod+Shift+S')
* ```
*/
export declare class AppHotkeysStore<T extends AppHotkeys = AppHotkeys> {
#private;
readonly store: Store<AppHotkeysState<T>>;
constructor(appHotkeys: T, options?: AppHotkeysStoreOptions<T>);
/** Effective app hotkeys. */
get appHotkeys(): T;
/** The user's hotkeys that differ from the defaults. */
get customHotkeys(): CustomHotkeys<T>;
/** Updates callbacks and other options without resetting custom hotkeys. */
setOptions(options: Omit<AppHotkeysStoreOptions<T>, 'customHotkeys'>): void;
/**
* Replaces the declarations, for example after hot module replacement.
* Custom hotkeys for ids that still exist are kept.
*/
setAppHotkeys(appHotkeys: T): void;
/** Effective app hotkey for an id, or `undefined` when it is not declared. */
get(id: string): AppHotkey | undefined;
/**
* Other app hotkeys that would collide if `id` used `hotkey`. App hotkeys
* collide when their scopes match or either is `'global'`, and their
* hotkeys are equal or one sequence starts with the other.
*/
findConflicts(id: keyof T & string, hotkey: Hotkey): Array<AppHotkey>;
/**
* Sets the user's hotkey for an app hotkey. `null` unbinds it. A hotkey
* equal to the default removes the customization.
*
* @returns Conflicting app hotkeys. When any exist, nothing changes.
*/
setCustomHotkey(id: keyof T & string, hotkey: Hotkey | null): Array<AppHotkey>;
/** Restores the default hotkey for an app hotkey. */
resetCustomHotkey(id: keyof T & string): void;
/** Restores every default hotkey. */
resetCustomHotkeys(): void;
/**
* Joins app hotkeys with live registrations, in declaration order.
* Adapters call this with the manager's registration views.
*/
getList(registrations: Iterable<HotkeyRegistrationView>): Array<AppHotkeyListItem<T[keyof T]>>;
}
/**
* Returns the provider's version of an app hotkey, so custom hotkeys apply
* even when a component imports the declaration directly. Falls back to the
* given app hotkey when no store declares it.
*/
export declare function resolveAppHotkey(appHotkey: AppHotkey, store: AppHotkeysStore | null | undefined): AppHotkey;
/**
* Options that adapters pass to the manager when registering an app hotkey:
* declared options, scope, meta, and the app hotkey id. The manager drops options
* that do not apply to the effective hotkey's kind.
*/
export declare function getAppHotkeyRegistrationOptions(appHotkey: AppHotkey): AppHotkeyOptions & {
meta?: HotkeyMeta;
appHotkeyId: string;
scope?: string;
};
/**
* Publishes a provider's store on the HotkeyManager so devtools can list
* declared hotkeys. Returns a cleanup function that unpublishes it if it is
* still the published store.
*/
export declare function publishAppHotkeysStore(store: AppHotkeysStore): () => void;
/**
* Creates an app hotkeys store and publishes it on the HotkeyManager, for
* adapters without a provider component (such as Lit and Alpine). Hotkey
* APIs in those adapters resolve app hotkeys and custom hotkeys from the
* published store. Call it once at startup. Returns the store and a function
* that unpublishes it.
*
* @example
* ```ts
* provideAppHotkeys(appHotkeys, {
* customHotkeys: JSON.parse(localStorage.getItem('hotkeys') ?? '{}'),
* onCustomHotkeysChange: (custom) =>
* localStorage.setItem('hotkeys', JSON.stringify(custom)),
* })
* ```
*/
export declare function provideAppHotkeys<T extends AppHotkeys>(appHotkeys: T, options?: AppHotkeysStoreOptions<T>): {
store: AppHotkeysStore<T>;
dispose: () => void;
};
//#endregion