Skip to content

feat: useHotkeySequences hooks#80

Merged
KevinVandy merged 4 commits intomainfrom
hotkeysequences
Mar 25, 2026
Merged

feat: useHotkeySequences hooks#80
KevinVandy merged 4 commits intomainfrom
hotkeysequences

Conversation

@KevinVandy
Copy link
Copy Markdown
Member

@KevinVandy KevinVandy commented Mar 24, 2026

🎯 Changes

✅ Checklist

  • I have followed the steps in the Contributing guide.
  • I have tested this code locally with pnpm run test:pr.

🚀 Release Impact

  • This change affects published code, and I have generated a changeset.
  • This change is docs/CI/dev-only (no release).

Summary by CodeRabbit

  • New Features

    • Multi-sequence hotkey APIs added across React/Preact/Vue/Solid/Svelte/Angular, plus a Svelte element-attachment helper; supports dynamic, variable-length lists
  • Behavior Change

    • Disabling a sequence now keeps it registered/visible (devtools) while suppressing execution; toggling enabled updates registrations in-place
  • Examples

    • New demo apps for React, Preact, Solid, Svelte, Vue, and Angular
  • Documentation

    • Expanded guides, reference pages, and examples across all frameworks
  • Tests

    • New cross-framework test suites covering multi-sequence behavior
  • Chores

    • Changeset entries marking minor releases for affected packages

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Mar 24, 2026

🚀 Changeset Version Preview

6 package(s) bumped directly, 0 bumped as dependents.

🟨 Minor bumps

Package Version Reason
@tanstack/angular-hotkeys 0.6.0 → 0.7.0 Changeset
@tanstack/preact-hotkeys 0.6.0 → 0.7.0 Changeset
@tanstack/react-hotkeys 0.6.0 → 0.7.0 Changeset
@tanstack/solid-hotkeys 0.6.0 → 0.7.0 Changeset
@tanstack/svelte-hotkeys 0.6.0 → 0.7.0 Changeset
@tanstack/vue-hotkeys 0.6.0 → 0.7.0 Changeset

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented Mar 24, 2026

Open in StackBlitz

@tanstack/angular-hotkeys

npm i https://pkg.pr.new/@tanstack/angular-hotkeys@80

@tanstack/hotkeys

npm i https://pkg.pr.new/@tanstack/hotkeys@80

@tanstack/hotkeys-devtools

npm i https://pkg.pr.new/@tanstack/hotkeys-devtools@80

@tanstack/preact-hotkeys

npm i https://pkg.pr.new/@tanstack/preact-hotkeys@80

@tanstack/preact-hotkeys-devtools

npm i https://pkg.pr.new/@tanstack/preact-hotkeys-devtools@80

@tanstack/react-hotkeys

npm i https://pkg.pr.new/@tanstack/react-hotkeys@80

@tanstack/react-hotkeys-devtools

npm i https://pkg.pr.new/@tanstack/react-hotkeys-devtools@80

@tanstack/solid-hotkeys

npm i https://pkg.pr.new/@tanstack/solid-hotkeys@80

@tanstack/solid-hotkeys-devtools

npm i https://pkg.pr.new/@tanstack/solid-hotkeys-devtools@80

@tanstack/svelte-hotkeys

npm i https://pkg.pr.new/@tanstack/svelte-hotkeys@80

@tanstack/vue-hotkeys

npm i https://pkg.pr.new/@tanstack/vue-hotkeys@80

@tanstack/vue-hotkeys-devtools

npm i https://pkg.pr.new/@tanstack/vue-hotkeys-devtools@80

commit: d3d57ef

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Mar 24, 2026

📝 Walkthrough

Walkthrough

Adds plural hotkey-sequence registration APIs across frameworks (React/Preact/Vue/Solid/Svelte/Angular), core manager behavior changes for enabled (soft-disable + handle updates), tests, documentation, examples, and package re-exports.

Changes

Cohort / File(s) Summary
Changeset & Docs Config
\.changeset/plural-sequences.md, docs/config.json
Adds a changeset and navigation entries for new plural-sequence APIs and example pages.
React
packages/react-hotkeys/src/useHotkeySequences.ts, packages/react-hotkeys/src/index.ts, packages/react-hotkeys/tests/*, docs/framework/react/*, examples/react/useHotkeySequences/*
Implements useHotkeySequences, tests, re-exports, docs, and a full example; changes hook registration logic to reuse handles and sync options/callbacks.
Preact
packages/preact-hotkeys/src/useHotkeySequences.ts, packages/preact-hotkeys/src/index.ts, packages/preact-hotkeys/tests/*, docs/framework/preact/*, examples/preact/useHotkeySequences/*
Adds useHotkeySequences hook, tests, re-export, docs, and example; updates registration lifecycle to avoid churn on enabled changes.
Vue
packages/vue-hotkeys/src/useHotkeySequences.ts, packages/vue-hotkeys/src/index.ts, docs/framework/vue/*, examples/vue/useHotkeySequences/*
Adds useHotkeySequences composable (MaybeRef/getter support), re-export, docs, and example; watcher-based diffing with handle reuse and option sync.
Solid
packages/solid-hotkeys/src/createHotkeySequences.ts, packages/solid-hotkeys/src/index.ts, packages/solid-hotkeys/tests/*, docs/framework/solid/*, examples/solid/createHotkeySequences/*
Adds createHotkeySequences primitive, tests, re-export, docs, and example; reactive createEffect diffs and updates registrations in-place when possible.
Svelte
packages/svelte-hotkeys/src/createHotkeySequences.svelte.ts, packages/svelte-hotkeys/src/index.ts, docs/framework/svelte/*, examples/svelte/create-hotkey-sequences/*
Adds createHotkeySequences and createHotkeySequencesAttachment, re-export, docs, examples, and component-level attachment API; lifecycle/update behavior mirrors other frameworks.
Angular
packages/angular-hotkeys/src/injectHotkeySequences.ts, packages/angular-hotkeys/src/index.ts, docs/framework/angular/*, examples/angular/injectHotkeySequences/*
Adds DI-based injectHotkeySequences, re-export, docs, and complete Angular example; uses DestroyRef/effect() and merges options with precedence rules.
Hotkeys core & semantics
packages/hotkeys/src/hotkey-manager.ts, packages/hotkeys/tests/*
Documents and tests soft-disable semantics for enabled (registration retained; execution suppressed) and verifies handle setOptions updates.
Framework primitives & hooks (behavior changes)
packages/*-hotkeys/src/*hotkey*.ts, packages/*-hotkeys/src/*sequence*.ts
Consolidates registration effects to avoid re-registering on enabled toggles: reuse handles when key/target unchanged, explicitly unregister when target/sequence absent, and sync callback/options each render.
Docs & Examples
docs/framework/*, docs/reference/*, examples/*/*/*
Adds reference pages, guides for "many sequences at once", updates enabled docs, adjusts "Defined in" line refs, and adds per-framework example projects and tooling configs.

Sequence Diagram(s)

sequenceDiagram
    participant Component as Component / Hook
    participant FrameworkEffect as Framework Effect / watcher
    participant SequenceManager as SequenceManager (singleton)
    participant Target as Target (document | element)

    Component->>FrameworkEffect: provide definitions + commonOptions
    FrameworkEffect->>FrameworkEffect: resolve getters/refs\nmerge options (provider < common < per-definition)
    FrameworkEffect->>SequenceManager: diff registrations\nregister / update / unregister handles
    SequenceManager->>Target: attach listeners to target
    Target->>SequenceManager: key events
    SequenceManager->>SequenceManager: detect sequence match\ninvoke callback
    SequenceManager->>Component: callback runs (updates state)
    FrameworkEffect->>SequenceManager: on update -> setOptions/update callback or re-register
    Component->>FrameworkEffect: unmount/destroy
    FrameworkEffect->>SequenceManager: unregister remaining handles
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰 I hopped through code and docs so fleet,

many sequences now land in one seat.
React to Svelte, Solid to Angular too,
one call binds many chords — a rabbit cheers you! 🎩✨

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch hotkeysequences

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 12

🧹 Nitpick comments (3)
docs/framework/preact/guides/sequences.md (1)

38-38: Optional: add a commonOptions example snippet.

Since this line explains second-argument precedence, adding a tiny example with useHotkeySequences(definitions, commonOptions) would make the behavior immediately actionable.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/framework/preact/guides/sequences.md` at line 38, Add a short example
demonstrating the second-argument precedence for useHotkeySequences so readers
can see how HotkeysProvider defaults, the second-argument commonOptions, and
each definition’s options merge (e.g., show calling
useHotkeySequences(definitions, commonOptions) with one shared option overridden
by a per-definition options entry). Refer to useHotkeySequences,
HotkeysProvider, commonOptions, definitions, and each definition’s options in
the example so it clearly shows the precedence order in practice.
examples/angular/injectHotkeySequences/src/app/app.component.html (1)

72-74: Announce triggered sequence updates to assistive tech.

At Line 73, this status changes dynamically; adding aria-live="polite" improves screen-reader feedback.

♿ Suggested tweak
-      <div class="info-box success"><strong>Triggered:</strong> {{ seq }}</div>
+      <div class="info-box success" aria-live="polite">
+        <strong>Triggered:</strong> {{ seq }}
+      </div>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/angular/injectHotkeySequences/src/app/app.component.html` around
lines 72 - 74, The dynamic "Triggered" status rendered via the lastSequence()
template binding (as seq) should announce updates to assistive tech; add
aria-live="polite" to the div with class "info-box success" that displays {{ seq
}} so screen readers are notified of changes without interrupting. Target the
template block using lastSequence() / seq and update that div to include the
aria-live attribute.
packages/angular-hotkeys/src/injectHotkeySequences.ts (1)

106-110: Inconsistent enabled handling compared to other framework implementations.

The Angular implementation skips registration when enabled: false at lines 106-110, and always passes enabled: true when registering (line 158). Other framework implementations (React, Vue, Solid, Preact, Svelte) pass the resolved enabled value through to setOptions() or the registration call, allowing the underlying manager to handle enable/disable state.

This inconsistency means:

  • Angular: Toggling enabled causes unregister/re-register cycles
  • Other frameworks: Toggling enabled updates the existing handle via setOptions()

Consider aligning with other frameworks by passing the enabled value through to the handle:

♻️ Suggested alignment with other frameworks
-      const { enabled = true, ...sequenceOpts } = mergedOptions
+      const { enabled = true, target, ...restOptions } = mergedOptions

-      if (!enabled || resolvedSequence.length === 0) {
+      if (resolvedSequence.length === 0) {
         continue
       }

+      const resolvedTarget =
+        target ?? (typeof document !== 'undefined' ? document : null)
+
+      if (!resolvedTarget) {
+        continue
+      }
+
+      const sequenceOpts = { ...restOptions, enabled }

And when registering:

       const handle = manager.register(p.resolvedSequence, p.def.callback, {
         ...p.sequenceOpts,
-        enabled: true,
         target: p.resolvedTarget,
       })
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/angular-hotkeys/src/injectHotkeySequences.ts` around lines 106 -
110, The Angular implementation currently destructures enabled from
mergedOptions and skips registration when enabled is false (using const {
enabled = true, ...sequenceOpts } = mergedOptions and continue), and later
always registers with enabled: true; instead, preserve and pass the resolved
enabled value through like other frameworks: do not early-continue on enabled
=== false (still register the sequence handle), and when calling the
registration or setOptions helper (the code path that currently uses
sequenceOpts/registration call), include enabled (the actual resolved enabled)
rather than hardcoding true so the manager can toggle state via
setOptions/update instead of unregistering/re-registering; adjust uses of
mergedOptions, sequenceOpts, and the register/setOptions call accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@docs/framework/angular/guides/sequences.md`:
- Around line 28-48: The example is missing the import for the Angular decorator
used: add an import for Component from '@angular/core' at the top of the snippet
so the `@Component` decorator on AppComponent resolves; keep the existing import
of injectHotkeySequences from '@tanstack/angular-hotkeys' and the AppComponent
class/constructor unchanged.

In `@examples/angular/injectHotkeySequences/src/app/app.component.ts`:
- Line 12: The history property uses the bracket-array form which violates the
lint rule; change its type to the generic form by replacing any occurrences of
string[] with Array<string> (e.g., update history = signal<string[]>([]) to
history = signal<Array<string>>([]) and adjust any other annotations or uses of
history in the AppComponent class accordingly).

In `@examples/angular/injectHotkeySequences/src/app/app.config.ts`:
- Around line 1-6: The file currently imports provideHotkeys but doesn't use it
and pulls in ApplicationConfig as a value import; change the ApplicationConfig
import to a type-only import (import type { ApplicationConfig }) and add
provideHotkeys(...) into the providers array of the exported appConfig so the
example demonstrates hotkey setup; reference the symbols ApplicationConfig,
provideHotkeys, appConfig and provideZoneChangeDetection when making these
edits.

In `@examples/angular/injectHotkeySequences/src/styles.css`:
- Around line 22-27: Remove the duplicate margin declaration in the CSS rule for
selector "header p": delete the earlier "margin: 0" and keep the later "margin:
0 auto" so only one margin declaration remains in the "header p" rule to satisfy
Stylelint and avoid redundancy.

In `@examples/preact/useHotkeySequences/src/index.css`:
- Around line 26-30: The CSS block for the selector "header p" contains a
duplicate margin declaration; remove the redundant "margin: 0;" so the rule only
keeps the intended layout declaration ("margin: 0 auto") in the header p rule to
avoid dead code and ensure the paragraph is centered horizontally.

In `@examples/react/useHotkeySequences/src/index.css`:
- Around line 26-31: The CSS selector "header p" contains a duplicate margin
declaration: remove the redundant "margin: 0" so only "margin: 0 auto" remains
(or consolidate into a single margin rule) within the header p block to satisfy
Stylelint and prevent the overwritten property.

In `@examples/solid/createHotkeySequences/src/index.css`:
- Around line 22-27: The selector "header p" contains a duplicated margin
declaration; remove the redundant "margin: 0" so only the intended "margin: 0
auto" remains (or merge them into a single "margin: 0 auto") alongside the
existing "max-width: 500px" and other properties to satisfy Stylelint and avoid
the overridden rule.

In `@examples/solid/createHotkeySequences/src/index.tsx`:
- Around line 3-8: Reorder the named imports to satisfy the ESLint sort-imports
rule: in the first import list swap createSignal and Show so Show comes before
createSignal, and in the second import list reorder the three symbols so
HotkeysProvider appears before createHotkey and createHotkeySequences
(preserving the relative order between the two create* names).

In `@examples/svelte/create-hotkey-sequences/README.md`:
- Around line 16-19: Update the recreate command in the README so the generated
project name matches the example directory: replace the erroneous
"create-hotkey" token in the shell command with "create-hotkey-sequences" (the
command string in the README's recreate example).

In `@examples/svelte/create-hotkey-sequences/src/index.css`:
- Around line 26-31: The CSS rule for the selector "header p" contains a
duplicate margin declaration; remove the redundant "margin: 0;" and keep the
intended "margin: 0 auto;" in the header p rule (locate the header p block in
index.css) so only a single margin declaration remains and the stylelint error
is resolved.

In `@examples/vue/useHotkeySequences/src/index.css`:
- Around line 26-31: The CSS rule for selector "header p" contains a duplicate
margin declaration; remove the redundant "margin: 0;" and keep a single
consolidated margin declaration (e.g., "margin: 0 auto;") in the "header p" rule
so there is no overridden/unused property.

In `@packages/solid-hotkeys/tests/createHotkeySequences.test.tsx`:
- Around line 2-10: Reorder and split the imports to satisfy ESLint rules:
alphabetize named members inside each import, move type-only imports to
top-level "import type" statements, and place the "solid-js" import before the
relative module import. Specifically, convert "type
CreateHotkeySequenceDefinition" and "type Component" to top-level import type
lines, alphabetize members in the vitest import (afterEach, beforeEach,
describe, expect, it, vi) and in the `@tanstack/hotkeys` import (SequenceManager),
ensure "createSignal" and "Component" from "solid-js" are imported before
"../src/createHotkeySequences", and keep "render" from
"@solidjs/testing-library" ordered appropriately.

---

Nitpick comments:
In `@docs/framework/preact/guides/sequences.md`:
- Line 38: Add a short example demonstrating the second-argument precedence for
useHotkeySequences so readers can see how HotkeysProvider defaults, the
second-argument commonOptions, and each definition’s options merge (e.g., show
calling useHotkeySequences(definitions, commonOptions) with one shared option
overridden by a per-definition options entry). Refer to useHotkeySequences,
HotkeysProvider, commonOptions, definitions, and each definition’s options in
the example so it clearly shows the precedence order in practice.

In `@examples/angular/injectHotkeySequences/src/app/app.component.html`:
- Around line 72-74: The dynamic "Triggered" status rendered via the
lastSequence() template binding (as seq) should announce updates to assistive
tech; add aria-live="polite" to the div with class "info-box success" that
displays {{ seq }} so screen readers are notified of changes without
interrupting. Target the template block using lastSequence() / seq and update
that div to include the aria-live attribute.

In `@packages/angular-hotkeys/src/injectHotkeySequences.ts`:
- Around line 106-110: The Angular implementation currently destructures enabled
from mergedOptions and skips registration when enabled is false (using const {
enabled = true, ...sequenceOpts } = mergedOptions and continue), and later
always registers with enabled: true; instead, preserve and pass the resolved
enabled value through like other frameworks: do not early-continue on enabled
=== false (still register the sequence handle), and when calling the
registration or setOptions helper (the code path that currently uses
sequenceOpts/registration call), include enabled (the actual resolved enabled)
rather than hardcoding true so the manager can toggle state via
setOptions/update instead of unregistering/re-registering; adjust uses of
mergedOptions, sequenceOpts, and the register/setOptions call accordingly.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 216e776a-35f5-4f4b-97a0-20356289fa95

📥 Commits

Reviewing files that changed from the base of the PR and between d058c76 and 1f650ae.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (95)
  • .changeset/plural-sequences.md
  • docs/config.json
  • docs/framework/angular/guides/sequences.md
  • docs/framework/angular/reference/functions/injectHotkeySequences.md
  • docs/framework/angular/reference/index.md
  • docs/framework/angular/reference/interfaces/InjectHotkeySequenceDefinition.md
  • docs/framework/preact/guides/sequences.md
  • docs/framework/preact/reference/functions/useHotkeySequences.md
  • docs/framework/preact/reference/index.md
  • docs/framework/preact/reference/interfaces/UseHotkeySequenceDefinition.md
  • docs/framework/react/guides/sequences.md
  • docs/framework/react/reference/functions/useHotkeySequences.md
  • docs/framework/react/reference/index.md
  • docs/framework/react/reference/interfaces/UseHotkeySequenceDefinition.md
  • docs/framework/solid/guides/sequences.md
  • docs/framework/solid/reference/functions/createHotkeySequences.md
  • docs/framework/solid/reference/index.md
  • docs/framework/solid/reference/interfaces/CreateHotkeySequenceDefinition.md
  • docs/framework/svelte/guides/sequences.md
  • docs/framework/svelte/reference/functions/createHotkeySequences.md
  • docs/framework/svelte/reference/functions/createHotkeySequencesAttachment.md
  • docs/framework/svelte/reference/index.md
  • docs/framework/svelte/reference/interfaces/CreateHotkeySequenceDefinition.md
  • docs/framework/vue/guides/sequences.md
  • docs/framework/vue/quick-start.md
  • docs/framework/vue/reference/functions/useHotkeySequences.md
  • docs/framework/vue/reference/index.md
  • docs/framework/vue/reference/interfaces/UseHotkeySequenceDefinition.md
  • examples/angular/injectHotkeySequences/angular.json
  • examples/angular/injectHotkeySequences/package.json
  • examples/angular/injectHotkeySequences/src/app/app.component.css
  • examples/angular/injectHotkeySequences/src/app/app.component.html
  • examples/angular/injectHotkeySequences/src/app/app.component.ts
  • examples/angular/injectHotkeySequences/src/app/app.config.ts
  • examples/angular/injectHotkeySequences/src/index.html
  • examples/angular/injectHotkeySequences/src/main.ts
  • examples/angular/injectHotkeySequences/src/styles.css
  • examples/angular/injectHotkeySequences/tsconfig.json
  • examples/preact/useHotkeySequences/eslint.config.js
  • examples/preact/useHotkeySequences/index.html
  • examples/preact/useHotkeySequences/package.json
  • examples/preact/useHotkeySequences/src/index.css
  • examples/preact/useHotkeySequences/src/index.tsx
  • examples/preact/useHotkeySequences/tsconfig.json
  • examples/preact/useHotkeySequences/vite.config.ts
  • examples/react/useHotkeySequences/eslint.config.js
  • examples/react/useHotkeySequences/index.html
  • examples/react/useHotkeySequences/package.json
  • examples/react/useHotkeySequences/src/index.css
  • examples/react/useHotkeySequences/src/index.tsx
  • examples/react/useHotkeySequences/tsconfig.json
  • examples/react/useHotkeySequences/vite.config.ts
  • examples/solid/createHotkeySequences/index.html
  • examples/solid/createHotkeySequences/package.json
  • examples/solid/createHotkeySequences/src/index.css
  • examples/solid/createHotkeySequences/src/index.tsx
  • examples/solid/createHotkeySequences/tsconfig.json
  • examples/solid/createHotkeySequences/vite.config.ts
  • examples/svelte/create-hotkey-sequences/.gitignore
  • examples/svelte/create-hotkey-sequences/.npmrc
  • examples/svelte/create-hotkey-sequences/README.md
  • examples/svelte/create-hotkey-sequences/index.html
  • examples/svelte/create-hotkey-sequences/package.json
  • examples/svelte/create-hotkey-sequences/src/App.svelte
  • examples/svelte/create-hotkey-sequences/src/Root.svelte
  • examples/svelte/create-hotkey-sequences/src/index.css
  • examples/svelte/create-hotkey-sequences/src/main.ts
  • examples/svelte/create-hotkey-sequences/static/robots.txt
  • examples/svelte/create-hotkey-sequences/svelte.config.js
  • examples/svelte/create-hotkey-sequences/tsconfig.json
  • examples/svelte/create-hotkey-sequences/vite.config.ts
  • examples/vue/useHotkeySequences/eslint.config.js
  • examples/vue/useHotkeySequences/index.html
  • examples/vue/useHotkeySequences/package.json
  • examples/vue/useHotkeySequences/src/App.vue
  • examples/vue/useHotkeySequences/src/index.css
  • examples/vue/useHotkeySequences/src/index.ts
  • examples/vue/useHotkeySequences/src/vue.d.ts
  • examples/vue/useHotkeySequences/tsconfig.json
  • examples/vue/useHotkeySequences/vite.config.ts
  • packages/angular-hotkeys/src/index.ts
  • packages/angular-hotkeys/src/injectHotkeySequences.ts
  • packages/preact-hotkeys/src/index.ts
  • packages/preact-hotkeys/src/useHotkeySequences.ts
  • packages/preact-hotkeys/tests/useHotkeySequences.test.tsx
  • packages/react-hotkeys/src/index.ts
  • packages/react-hotkeys/src/useHotkeySequences.ts
  • packages/react-hotkeys/tests/useHotkeySequences.test.tsx
  • packages/solid-hotkeys/src/createHotkeySequences.ts
  • packages/solid-hotkeys/src/index.ts
  • packages/solid-hotkeys/tests/createHotkeySequences.test.tsx
  • packages/svelte-hotkeys/src/createHotkeySequences.svelte.ts
  • packages/svelte-hotkeys/src/index.ts
  • packages/vue-hotkeys/src/index.ts
  • packages/vue-hotkeys/src/useHotkeySequences.ts

Comment thread docs/framework/angular/guides/sequences.md
Comment thread examples/angular/injectHotkeySequences/src/app/app.component.ts Outdated
Comment thread examples/angular/injectHotkeySequences/src/app/app.config.ts Outdated
Comment thread examples/angular/injectHotkeySequences/src/styles.css
Comment thread examples/preact/useHotkeySequences/src/index.css
Comment thread examples/solid/createHotkeySequences/src/index.tsx Outdated
Comment thread examples/svelte/create-hotkey-sequences/README.md
Comment thread examples/svelte/create-hotkey-sequences/src/index.css
Comment thread examples/vue/useHotkeySequences/src/index.css
Comment thread packages/solid-hotkeys/tests/createHotkeySequences.test.tsx Outdated
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@examples/angular/injectHotkeySequences/src/app/app.component.html`:
- Line 112: Escape the '>' in arrow functions inside the HTML example: replace
each arrow token "=>" in the callback expressions with "=&gt;" so the snippets
like "callback: () => scrollToTop()" become "callback: () =&gt; scrollToTop()";
apply the same change for the other occurrences referenced (lines containing the
callback arrow at the sequence entries on the same block).
- Line 112: The code example uses uppercase sequence ['G','G'] but the
docs/table describe lowercase "g g" (Vim semantics: gg -> top); update the
sequence in the example (the object containing "sequence: ['G','G'], callback:
() => scrollToTop()") to use lowercase ['g','g'] so the example matches the
table and expected behavior.

In `@examples/angular/injectHotkeySequences/src/app/app.config.ts`:
- Around line 1-3: Reorder the imports so the regular imports come before the
type-only import: place the existing `import { provideZoneChangeDetection } from
'@angular/core'` and `import { provideHotkeys } from
'@tanstack/angular-hotkeys'` first, and then move the type import `import type {
ApplicationConfig } from '@angular/core'` after them to satisfy the project's
`import/order` ESLint rule; update any references to `ApplicationConfig`,
`provideZoneChangeDetection`, or `provideHotkeys` accordingly.

In `@packages/solid-hotkeys/tests/createHotkeySequences.test.tsx`:
- Around line 5-8: The import order violates ESLint import/order: move all value
imports (createSignal from 'solid-js' and createHotkeySequences from
'../src/createHotkeySequences') before type-only imports, and group type imports
together (Component from 'solid-js' and CreateHotkeySequenceDefinition from
'../src/createHotkeySequences'); update the import statements so value imports
appear first then type imports to satisfy the rule.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: f44e66a4-e2be-4fcf-8ae7-ad91880b1242

📥 Commits

Reviewing files that changed from the base of the PR and between 1f650ae and 4767bbb.

📒 Files selected for processing (27)
  • .changeset/plural-sequences.md
  • docs/framework/angular/guides/sequences.md
  • docs/framework/angular/reference/functions/injectHotkeySequences.md
  • docs/framework/angular/reference/interfaces/InjectHotkeySequenceDefinition.md
  • docs/framework/preact/reference/functions/useHotkeySequences.md
  • docs/framework/preact/reference/interfaces/UseHotkeySequenceDefinition.md
  • docs/framework/react/reference/functions/useHotkeySequences.md
  • docs/framework/react/reference/interfaces/UseHotkeySequenceDefinition.md
  • docs/framework/solid/reference/functions/createHotkeySequences.md
  • docs/framework/solid/reference/interfaces/CreateHotkeySequenceDefinition.md
  • docs/framework/svelte/reference/functions/createHotkeySequences.md
  • docs/framework/svelte/reference/functions/createHotkeySequencesAttachment.md
  • docs/framework/svelte/reference/interfaces/CreateHotkeySequenceDefinition.md
  • docs/framework/vue/reference/functions/useHotkeySequences.md
  • docs/framework/vue/reference/interfaces/UseHotkeySequenceDefinition.md
  • examples/angular/injectHotkeySequences/src/app/app.component.html
  • examples/angular/injectHotkeySequences/src/app/app.component.ts
  • examples/angular/injectHotkeySequences/src/app/app.config.ts
  • examples/angular/injectHotkeySequences/src/styles.css
  • examples/preact/useHotkeySequences/src/index.css
  • examples/react/useHotkeySequences/src/index.css
  • examples/solid/createHotkeySequences/src/index.css
  • examples/solid/createHotkeySequences/src/index.tsx
  • examples/svelte/create-hotkey-sequences/README.md
  • examples/svelte/create-hotkey-sequences/src/index.css
  • examples/vue/useHotkeySequences/src/index.css
  • packages/solid-hotkeys/tests/createHotkeySequences.test.tsx
✅ Files skipped from review due to trivial changes (23)
  • docs/framework/angular/guides/sequences.md
  • docs/framework/preact/reference/interfaces/UseHotkeySequenceDefinition.md
  • docs/framework/svelte/reference/functions/createHotkeySequences.md
  • docs/framework/react/reference/interfaces/UseHotkeySequenceDefinition.md
  • docs/framework/svelte/reference/functions/createHotkeySequencesAttachment.md
  • docs/framework/angular/reference/interfaces/InjectHotkeySequenceDefinition.md
  • .changeset/plural-sequences.md
  • examples/svelte/create-hotkey-sequences/README.md
  • docs/framework/react/reference/functions/useHotkeySequences.md
  • docs/framework/angular/reference/functions/injectHotkeySequences.md
  • docs/framework/vue/reference/interfaces/UseHotkeySequenceDefinition.md
  • docs/framework/svelte/reference/interfaces/CreateHotkeySequenceDefinition.md
  • examples/react/useHotkeySequences/src/index.css
  • docs/framework/solid/reference/functions/createHotkeySequences.md
  • examples/solid/createHotkeySequences/src/index.tsx
  • examples/angular/injectHotkeySequences/src/styles.css
  • examples/preact/useHotkeySequences/src/index.css
  • docs/framework/preact/reference/functions/useHotkeySequences.md
  • docs/framework/vue/reference/functions/useHotkeySequences.md
  • docs/framework/solid/reference/interfaces/CreateHotkeySequenceDefinition.md
  • examples/solid/createHotkeySequences/src/index.css
  • examples/svelte/create-hotkey-sequences/src/index.css
  • examples/vue/useHotkeySequences/src/index.css
🚧 Files skipped from review as they are similar to previous changes (1)
  • examples/angular/injectHotkeySequences/src/app/app.component.ts


// In constructor or injection context:
injectHotkeySequences([
{{ '{' }} sequence: ['G', 'G'], callback: () => scrollToTop() {{ '}' }},
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Escape arrow function > characters in the code example.

The > in arrow functions should be escaped as &gt; for strict HTML compliance within the <pre> block. You've already escaped @ as &#64; on line 108, but the > characters on lines 112, 115, 118, and 121 also need escaping.

🔧 Proposed fix
 injectHotkeySequences([
-  { sequence: ['G', 'G'], callback: () => scrollToTop() },
+  { sequence: ['G', 'G'], callback: () =&gt; scrollToTop() },
   {
     sequence: ['ArrowUp', 'ArrowUp', 'ArrowDown', 'ArrowDown'],
-    callback: () => activateCheatMode(),
+    callback: () =&gt; activateCheatMode(),
     options: { timeout: 1500 },
   },
-  { sequence: ['C', 'I', 'W'], callback: () => changeInnerWord() },
-  { sequence: ['Shift+R', 'Shift+T'], callback: () => doSomething() },
+  { sequence: ['C', 'I', 'W'], callback: () =&gt; changeInnerWord() },
+  { sequence: ['Shift+R', 'Shift+T'], callback: () =&gt; doSomething() },
 ])

Also applies to: 115-115, 118-118, 121-121

🧰 Tools
🪛 HTMLHint (1.9.2)

[error] 112-112: Special characters must be escaped : [ > ].

(spec-char-escape)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/angular/injectHotkeySequences/src/app/app.component.html` at line
112, Escape the '>' in arrow functions inside the HTML example: replace each
arrow token "=>" in the callback expressions with "=&gt;" so the snippets like
"callback: () => scrollToTop()" become "callback: () =&gt; scrollToTop()"; apply
the same change for the other occurrences referenced (lines containing the
callback arrow at the sequence entries on the same block).

⚠️ Potential issue | 🟡 Minor

Fix inconsistency between table and code example.

The table at lines 22-24 shows g g (lowercase) for "Go to top", but the code example uses ['G', 'G'] (uppercase). In Vim semantics, gg goes to top and G goes to bottom. The code example should use ['g', 'g'] to match the documented behavior.

📝 Proposed fix
 injectHotkeySequences([
-  { sequence: ['G', 'G'], callback: () => scrollToTop() },
+  { sequence: ['g', 'g'], callback: () => scrollToTop() },
   {
🧰 Tools
🪛 HTMLHint (1.9.2)

[error] 112-112: Special characters must be escaped : [ > ].

(spec-char-escape)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/angular/injectHotkeySequences/src/app/app.component.html` at line
112, The code example uses uppercase sequence ['G','G'] but the docs/table
describe lowercase "g g" (Vim semantics: gg -> top); update the sequence in the
example (the object containing "sequence: ['G','G'], callback: () =>
scrollToTop()") to use lowercase ['g','g'] so the example matches the table and
expected behavior.

Comment on lines +1 to +3
import type { ApplicationConfig } from '@angular/core'
import { provideZoneChangeDetection } from '@angular/core'
import { provideHotkeys } from '@tanstack/angular-hotkeys'
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Fix import order to satisfy ESLint.

The type import from @angular/core should be placed after regular imports per the project's import/order rule.

📝 Proposed fix
-import type { ApplicationConfig } from '@angular/core'
 import { provideZoneChangeDetection } from '@angular/core'
 import { provideHotkeys } from '@tanstack/angular-hotkeys'
+import type { ApplicationConfig } from '@angular/core'
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import type { ApplicationConfig } from '@angular/core'
import { provideZoneChangeDetection } from '@angular/core'
import { provideHotkeys } from '@tanstack/angular-hotkeys'
import { provideZoneChangeDetection } from '@angular/core'
import { provideHotkeys } from '@tanstack/angular-hotkeys'
import type { ApplicationConfig } from '@angular/core'
🧰 Tools
🪛 ESLint

[error] 1-1: @angular/core type import should occur after import of @tanstack/angular-hotkeys

(import/order)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/angular/injectHotkeySequences/src/app/app.config.ts` around lines 1
- 3, Reorder the imports so the regular imports come before the type-only
import: place the existing `import { provideZoneChangeDetection } from
'@angular/core'` and `import { provideHotkeys } from
'@tanstack/angular-hotkeys'` first, and then move the type import `import type {
ApplicationConfig } from '@angular/core'` after them to satisfy the project's
`import/order` ESLint rule; update any references to `ApplicationConfig`,
`provideZoneChangeDetection`, or `provideHotkeys` accordingly.

Comment on lines +5 to +8
import { createSignal } from 'solid-js'
import type { Component } from 'solid-js'
import { createHotkeySequences } from '../src/createHotkeySequences'
import type { CreateHotkeySequenceDefinition } from '../src/createHotkeySequences'
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Import order still violates ESLint import/order rule.

The value import from ../src/createHotkeySequences (line 7) should occur before the type import from solid-js (line 6). Group all value imports together, then all type imports.

Proposed fix
 import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
 import { render } from '@solidjs/testing-library'
 import { SequenceManager } from '@tanstack/hotkeys'
 import { createSignal } from 'solid-js'
-import type { Component } from 'solid-js'
 import { createHotkeySequences } from '../src/createHotkeySequences'
+import type { Component } from 'solid-js'
 import type { CreateHotkeySequenceDefinition } from '../src/createHotkeySequences'
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import { createSignal } from 'solid-js'
import type { Component } from 'solid-js'
import { createHotkeySequences } from '../src/createHotkeySequences'
import type { CreateHotkeySequenceDefinition } from '../src/createHotkeySequences'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { render } from '@solidjs/testing-library'
import { SequenceManager } from '@tanstack/hotkeys'
import { createSignal } from 'solid-js'
import { createHotkeySequences } from '../src/createHotkeySequences'
import type { Component } from 'solid-js'
import type { CreateHotkeySequenceDefinition } from '../src/createHotkeySequences'
🧰 Tools
🪛 ESLint

[error] 7-7: ../src/createHotkeySequences import should occur before type import of solid-js

(import/order)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/solid-hotkeys/tests/createHotkeySequences.test.tsx` around lines 5 -
8, The import order violates ESLint import/order: move all value imports
(createSignal from 'solid-js' and createHotkeySequences from
'../src/createHotkeySequences') before type-only imports, and group type imports
together (Component from 'solid-js' and CreateHotkeySequenceDefinition from
'../src/createHotkeySequences'); update the import statements so value imports
appear first then type imports to satisfy the rule.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
packages/svelte-hotkeys/src/createHotkeySequence.svelte.ts (1)

72-76: ⚠️ Potential issue | 🟡 Minor

Callback reference remains stale if only the callback changes without sequence or options changing.

Unlike React and Preact implementations which update a callbackRef every render (outside effects), the Svelte implementation accesses callback directly and only synchronizes it when the effect re-runs (lines 106, 119). The effect depends only on resolveMaybeGetter(sequence) and resolveMaybeGetter(options), so if only the callback reference changes—while sequence and options remain the same—the effect won't re-run and line 106 won't execute, leaving the registration with a stale callback.

React and Preact prevent this by updating their callback refs on every render outside the effect, ensuring the latest callback is always available. Consider adopting a similar pattern or explicitly including callback tracking in Svelte.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/svelte-hotkeys/src/createHotkeySequence.svelte.ts` around lines 72 -
76, The effect in createHotkeySequence registers handlers that capture the
original callback and doesn't update when only callback changes; fix by
introducing and updating a mutable reference (e.g., callbackRef) for the
callback at the top of createHotkeySequence on every invocation and have the
registered handlers call callbackRef.current (or equivalent) instead of the
captured callback; keep the effect dependency on resolveMaybeGetter(sequence)
and resolveMaybeGetter(options) but ensure lines that register listeners (the
handlers referenced at registration sites around the existing lines ~106 and
~119) use the up-to-date callbackRef so callback changes are honored without
re-running the effect.
packages/angular-hotkeys/src/injectHotkeySequence.ts (1)

13-19: ⚠️ Potential issue | 🟠 Major

Sequence targets can't safely wait for mount yet.

sequenceOptions.target ?? document means a getter like () => ({ target: this.editorRef()?.nativeElement ?? null }) registers on document until the element exists, instead of staying inactive. That is inconsistent with injectHotkey, which preserves an explicit missing target.

Suggested fix
-export interface InjectHotkeySequenceOptions extends Omit<
-  SequenceOptions,
-  'enabled'
-> {
+export interface InjectHotkeySequenceOptions extends Omit<
+  SequenceOptions,
+  'enabled' | 'target'
+> {
   /** Whether the sequence is enabled. Defaults to true. */
   enabled?: boolean
+  target?: HTMLElement | Document | Window | null
 }
...
-    const resolvedTarget =
-      sequenceOptions.target ??
-      (typeof document !== 'undefined' ? document : undefined)
+    const resolvedTarget =
+      'target' in sequenceOptions
+        ? (sequenceOptions.target ?? null)
+        : (typeof document !== 'undefined' ? document : null)

Also applies to: 89-93

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/angular-hotkeys/src/injectHotkeySequence.ts` around lines 13 - 19,
The code currently uses "sequenceOptions.target ?? document" which treats an
explicit null target (e.g. a getter that returns null until mount) as missing
and falls back to document; change the fallback logic in injectHotkeySequence so
it only defaults to document when the target property is truly absent/undefined
(use a presence check like "'target' in sequenceOptions ? sequenceOptions.target
: document" or Object.prototype.hasOwnProperty.call(sequenceOptions, 'target') ?
sequenceOptions.target : document"), and apply the same change to the similar
logic at the block referenced around lines 89-93 so explicit null targets remain
preserved and sequences stay inactive until the real element exists.
🧹 Nitpick comments (11)
examples/preact/useHotkeySequence/src/index.tsx (1)

148-153: Consider exposing toggle state with aria-pressed.

The control works, but adding toggle semantics will improve screen reader behavior.

♿ Optional accessibility tweak
               <button
                 type="button"
+                aria-pressed={helloSequenceEnabled}
                 onClick={() => setHelloSequenceEnabled((v) => !v)}
               >
                 {helloSequenceEnabled ? 'Disable' : 'Enable'} sequence
               </button>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/preact/useHotkeySequence/src/index.tsx` around lines 148 - 153, The
toggle button currently toggles state via setHelloSequenceEnabled and reflects
it with helloSequenceEnabled but lacks ARIA toggle semantics; update the button
element (the element using onClick={() => setHelloSequenceEnabled((v) => !v)})
to include aria-pressed={helloSequenceEnabled} so assistive tech understands its
pressed/toggled state; keep the element as a native <button> (no role change
required) and ensure the aria-pressed value is kept in sync with
helloSequenceEnabled.
examples/angular/injectHotkeySequence/src/app/app.component.html (1)

68-77: Consider exposing toggle/state changes to assistive tech.

Nice UX addition. For accessibility, consider aria-pressed on the toggle button and aria-live="polite" on the status text so screen readers announce state changes.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/angular/injectHotkeySequence/src/app/app.component.html` around
lines 68 - 77, Add accessibility attributes so state changes are announced: add
aria-live="polite" to the status element (sequence-toggle-status paragraph) and
add aria-pressed="{{ helloSequenceEnabled() }}" to the toggle button used by
toggleHelloSequence(); ensure the aria-pressed value is derived from
helloSequenceEnabled() so assistive tech reflects the current enabled/disabled
state when the button is clicked.
examples/svelte/create-hotkey-sequence/src/App.svelte (1)

142-147: Consider marking the toggle button as pressed/unpressed for assistive tech.

This is a true toggle control; adding aria-pressed would improve screen-reader semantics.

♿ Optional accessibility tweak
           <button
             type="button"
+            aria-pressed={helloSequenceEnabled}
             onclick={() => (helloSequenceEnabled = !helloSequenceEnabled)}
           >
             {helloSequenceEnabled ? 'Disable' : 'Enable'} sequence
           </button>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/svelte/create-hotkey-sequence/src/App.svelte` around lines 142 -
147, The toggle button controlling helloSequenceEnabled is missing aria-pressed
which would expose its pressed state to assistive tech; update the button
element that toggles helloSequenceEnabled to include aria-pressed bound to that
boolean (e.g., aria-pressed={helloSequenceEnabled}) so screen readers receive
the current state while keeping the existing click handler and visible label
logic intact.
examples/solid/createHotkeySequence/src/index.tsx (1)

149-154: Optional: expose toggle state with aria-pressed.

Since this is a true on/off toggle, adding pressed state improves assistive-tech semantics.

Suggested tweak
               <button
                 type="button"
+                aria-pressed={helloSequenceEnabled()}
                 onClick={() => setHelloSequenceEnabled(!helloSequenceEnabled())}
               >
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/solid/createHotkeySequence/src/index.tsx` around lines 149 - 154,
The button is a true on/off toggle but lacks an accessibility pressed state;
update the toggle button (the element using helloSequenceEnabled() and
setHelloSequenceEnabled) to include an aria-pressed attribute bound to the
current state (aria-pressed={helloSequenceEnabled()}), ensuring assistive tech
knows when the sequence is enabled or disabled while preserving the existing
onClick behavior.
examples/vue/useHotkeySequence/src/App.vue (1)

153-165: Add toggle semantics for assistive technologies.

Consider exposing the button state with aria-pressed (and optionally polite live updates on status) so state changes are conveyed more reliably.

♿ Suggested accessibility refinement
-              <p class="sequence-toggle-status">
+              <p class="sequence-toggle-status" aria-live="polite">
                 This sequence is
                 <strong>{{
                   helloSequenceEnabled ? 'enabled' : 'disabled'
                 }}</strong
                 >.
               </p>
               <button
                 type="button"
+                :aria-pressed="helloSequenceEnabled"
                 `@click`="helloSequenceEnabled = !helloSequenceEnabled"
               >
                 {{ helloSequenceEnabled ? 'Disable' : 'Enable' }} sequence
               </button>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/vue/useHotkeySequence/src/App.vue` around lines 153 - 165, The
toggle button isn't exposing its state to assistive tech; update the button that
toggles helloSequenceEnabled to include an aria-pressed attribute bound to
helloSequenceEnabled (e.g., aria-pressed="helloSequenceEnabled") and mark the
status paragraph with a polite live region (e.g., aria-live="polite") so screen
readers are notified of changes; ensure the button text still updates based on
helloSequenceEnabled and that the status paragraph (sequence-toggle-status)
reflects the same state.
packages/svelte-hotkeys/src/createHotkey.svelte.ts (1)

35-49: Minor: Redundant hotkey normalization.

The callers of registerHotkey (at lines 85 and 153) already call normalizeHotkey to compute hotkeyStr for comparison. Then registerHotkey calls normalizeHotkey again at line 42. Since string hotkeys pass through unchanged, this is harmless but slightly redundant.

Consider accepting the already-normalized Hotkey string instead of RegisterableHotkey to avoid the extra call:

♻️ Optional refactor to avoid double normalization
 function registerHotkey(
   target: HTMLElement | Document | Window,
-  hotkey: RegisterableHotkey,
+  hotkey: Hotkey,
   callback: HotkeyCallback,
   mergedOptions: CreateHotkeyOptions,
 ) {
   return getHotkeyManager().register(
-    normalizeHotkey(hotkey, mergedOptions),
+    hotkey,
     callback,
     {
       ...mergedOptions,
       target,
     },
   )
 }

Then update callers to pass hotkeyStr instead of resolvedHotkey.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/svelte-hotkeys/src/createHotkey.svelte.ts` around lines 35 - 49, The
registerHotkey function currently accepts a RegisterableHotkey and calls
normalizeHotkey again; change registerHotkey to accept an already-normalized
Hotkey string (or hotkeyStr) instead of RegisterableHotkey, remove the internal
normalizeHotkey call, and pass that hotkey string directly into
getHotkeyManager().register; then update the callers that currently compute
hotkeyStr (the places that call registerHotkey with resolvedHotkey) to pass
hotkeyStr into registerHotkey so no double-normalization occurs (references:
function registerHotkey, normalizeHotkey, and getHotkeyManager().register).
examples/vue/useHotkeySequences/src/App.vue (1)

223-223: Set explicit button type for “Clear History”.

Low-risk hardening: add type="button" to avoid accidental submit behavior if this block is later moved inside a <form>.

Suggested patch
-          <button `@click`="history = []">Clear History</button>
+          <button type="button" `@click`="history = []">Clear History</button>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/vue/useHotkeySequences/src/App.vue` at line 223, The "Clear History"
button currently lacks an explicit type which can cause it to act as a submit
button if moved inside a form; update the button element that sets "history =
[]" (the Clear History button) to include type="button" so it will not trigger
form submission unintentionally.
packages/svelte-hotkeys/src/createHotkeySequence.svelte.ts (1)

156-199: Consider extracting shared registration logic to reduce duplication.

The effect logic in createHotkeySequenceAttachment (lines 160-199) is nearly identical to createHotkeySequence (lines 80-123), differing only in the target (element vs document). This duplication could make future maintenance of the caching/update logic error-prone if changes need to be applied in both places.

Consider extracting a shared internal helper:

// Example internal helper signature
function createSequenceEffect(
  target: HTMLElement | Document | Window,
  sequence: MaybeGetter<HotkeySequence>,
  callback: HotkeyCallback,
  options: MaybeGetter<CreateHotkeySequenceOptions>,
): { cleanup: () => void }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/svelte-hotkeys/src/createHotkeySequence.svelte.ts` around lines 156
- 199, The two effect blocks in createHotkeySequence and
createHotkeySequenceAttachment duplicate registration/update/unregister logic
(variables registration, lastSequenceKey, sequenceKey and calls to
registerHotkeySequence), so extract that shared logic into a single helper
(e.g., createSequenceEffect) that accepts the target (element vs
document/window), the resolved sequence getter, callback, and options, and
performs the resolve, comparison (by formatHotkeySequence), register, update
(setOptions + update callback), and unregister behavior; then replace the
duplicated $effect bodies in createHotkeySequence and
createHotkeySequenceAttachment to call that helper and store/cleanup its
returned handle.
packages/solid-hotkeys/src/createHotkey.ts (1)

165-168: Redundant callback/options sync immediately after registration.

The manager.register() call on lines 160-163 already receives callback and optionsWithoutTarget. Immediately calling registration.callback = callback and registration.setOptions(optionsWithoutTarget) on lines 166-167 is redundant since the registration was just created with these exact values.

♻️ Proposed fix to remove redundant sync
     registration = manager.register(hotkeyString, callback, {
       ...optionsWithoutTarget,
       target: resolvedTarget,
     })

-    if (registration.isActive) {
-      registration.callback = callback
-      registration.setOptions(optionsWithoutTarget)
-    }
-
     lastHotkeyString = hotkeyString
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/solid-hotkeys/src/createHotkey.ts` around lines 165 - 168, Remove
the redundant immediate sync after creating a registration: when you call
manager.register(callback, optionsWithoutTarget) you already pass the callback
and options, so drop the extra assignments guarded by registration.isActive;
specifically remove or skip setting registration.callback and calling
registration.setOptions(optionsWithoutTarget) right after manager.register(...)
(references: manager.register, registration, registration.isActive,
registration.callback, registration.setOptions, optionsWithoutTarget, callback).
packages/solid-hotkeys/src/createHotkeySequence.ts (1)

142-145: Redundant callback/options sync immediately after registration.

Same issue as in createHotkey.ts: the manager.register() call already receives the callback and merged options. The immediate sync on lines 143-144 is unnecessary for a freshly created registration.

♻️ Proposed fix to remove redundant sync
     registration = manager.register(resolvedSequence, callback, {
       ...mergedOptions,
       target: resolvedTarget,
     })

-    if (registration.isActive) {
-      registration.callback = callback
-      registration.setOptions(optionsWithoutTarget)
-    }
-
     lastSequenceKey = sequenceKey
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/solid-hotkeys/src/createHotkeySequence.ts` around lines 142 - 145,
The registration callback/options are being redundantly re-assigned right after
creating a new registration; remove the immediate sync that sets
registration.callback and calls registration.setOptions in
createHotkeySequence.ts (the block guarded by registration.isActive) because
manager.register(...) already receives the callback and merged options—leave
manager.register(...) to initialize the registration and delete the extra lines
that overwrite registration.callback and call registration.setOptions.
packages/solid-hotkeys/tests/createHotkeys.test.tsx (1)

149-156: Tests rely on internal manager state structure.

Accessing manager.registrations.state.values() couples this test to implementation details of HotkeyManager. If the internal state structure changes, these tests will break even if the public API behavior is correct.

This is acceptable for verifying soft-disable semantics, but consider documenting this coupling or providing a public API for introspection if this pattern expands.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/solid-hotkeys/tests/createHotkeys.test.tsx` around lines 149 - 156,
The test currently reaches into HotkeyManager.registrations.state.values() which
couples it to internal structure; change the test to use a public API (or add
one) on HotkeyManager such as getRegistrations() or getRegistration(hotkey) that
returns registration objects or an enabled flag, then assert via
HotkeyManager.getInstance().getRegistration('Mod+Z') or iterate
getRegistrations() and check options.enabled; update the test to call
HotkeyManager.getInstance(), use getRegistrationCount() for the count assertion,
and replace the internal-state access with the new public accessor (or document
the coupling if you prefer not to add an API).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@examples/vue/useHotkeySequences/src/App.vue`:
- Around line 63-76: The usageCode example incorrectly shows a React-style
component function (VimEditor) wrapping the Vue composable; update the usageCode
string so it demonstrates calling useHotkeySequences at top-level in a <script
setup> context instead of inside function VimEditor. Replace the existing
snippet so it imports useHotkeySequences and invokes useHotkeySequences([...])
directly in a <script setup> block (preserving the same sequence entries and
options), ensuring the example matches Vue Composition API usage.

In `@packages/angular-hotkeys/src/injectHotkeySequences.ts`:
- Around line 114-120: The code currently falls back to document for sequence
registration by using sequenceOpts.target ?? document which converts a
missing/temporarily-unset scoped target into a global registration; change the
logic to treat a null/undefined sequenceOpts.target as “skip registration” the
same way injectHotkey does: remove the document fallback and only use
sequenceOpts.target if it is non-null, continuing the loop when it is missing;
also update the type InjectHotkeySequenceOptions['target'] in
injectHotkeySequence.ts to be nullable (match InjectHotkeyOptions) so callers
can explicitly pass null/undefined to indicate “no target yet.”

In `@packages/preact-hotkeys/src/useHotkeySequences.ts`:
- Around line 97-187: The effect currently clears registrationsRef.current
during every cleanup and only depends on sequenceKey, causing unnecessary full
re-registers and stale target bindings; update the reconciliation so
registration identity includes resolved target (e.g., incorporate resolvedTarget
into registrationKey or the effect's dependency key) and do not wipe
registrationsRef.current inside this effect's cleanup—instead create a separate
unmount-only useEffect (empty deps) that iterates registrationsRef.current and
unregisters active handles and then clears the map; keep the existing
managerRef.current.register/unregister logic but compute/compare keys using both
sequence string and resolvedTarget (or a stable identifier) so target-only
changes trigger correct re-registration without losing unchanged rows.

In `@packages/vue-hotkeys/src/useHotkeySequence.ts`:
- Around line 114-124: The code currently collapses explicit null and undefined
for resolvedTarget via the ?? operator, causing an explicit null target to be
treated as document; change the fallback logic so only undefined falls back to
document (preserve explicit null). Specifically, update the finalTarget
assignment in useHotkeySequence (where resolvedTarget is used) to use a
conditional that checks resolvedTarget === undefined and only then sets document
(or null), leaving resolvedTarget === null untouched so the existing
!finalTarget cleanup branch (registration, lastSequenceKey, lastTarget) behaves
correctly.

---

Outside diff comments:
In `@packages/angular-hotkeys/src/injectHotkeySequence.ts`:
- Around line 13-19: The code currently uses "sequenceOptions.target ??
document" which treats an explicit null target (e.g. a getter that returns null
until mount) as missing and falls back to document; change the fallback logic in
injectHotkeySequence so it only defaults to document when the target property is
truly absent/undefined (use a presence check like "'target' in sequenceOptions ?
sequenceOptions.target : document" or
Object.prototype.hasOwnProperty.call(sequenceOptions, 'target') ?
sequenceOptions.target : document"), and apply the same change to the similar
logic at the block referenced around lines 89-93 so explicit null targets remain
preserved and sequences stay inactive until the real element exists.

In `@packages/svelte-hotkeys/src/createHotkeySequence.svelte.ts`:
- Around line 72-76: The effect in createHotkeySequence registers handlers that
capture the original callback and doesn't update when only callback changes; fix
by introducing and updating a mutable reference (e.g., callbackRef) for the
callback at the top of createHotkeySequence on every invocation and have the
registered handlers call callbackRef.current (or equivalent) instead of the
captured callback; keep the effect dependency on resolveMaybeGetter(sequence)
and resolveMaybeGetter(options) but ensure lines that register listeners (the
handlers referenced at registration sites around the existing lines ~106 and
~119) use the up-to-date callbackRef so callback changes are honored without
re-running the effect.

---

Nitpick comments:
In `@examples/angular/injectHotkeySequence/src/app/app.component.html`:
- Around line 68-77: Add accessibility attributes so state changes are
announced: add aria-live="polite" to the status element (sequence-toggle-status
paragraph) and add aria-pressed="{{ helloSequenceEnabled() }}" to the toggle
button used by toggleHelloSequence(); ensure the aria-pressed value is derived
from helloSequenceEnabled() so assistive tech reflects the current
enabled/disabled state when the button is clicked.

In `@examples/preact/useHotkeySequence/src/index.tsx`:
- Around line 148-153: The toggle button currently toggles state via
setHelloSequenceEnabled and reflects it with helloSequenceEnabled but lacks ARIA
toggle semantics; update the button element (the element using onClick={() =>
setHelloSequenceEnabled((v) => !v)}) to include
aria-pressed={helloSequenceEnabled} so assistive tech understands its
pressed/toggled state; keep the element as a native <button> (no role change
required) and ensure the aria-pressed value is kept in sync with
helloSequenceEnabled.

In `@examples/solid/createHotkeySequence/src/index.tsx`:
- Around line 149-154: The button is a true on/off toggle but lacks an
accessibility pressed state; update the toggle button (the element using
helloSequenceEnabled() and setHelloSequenceEnabled) to include an aria-pressed
attribute bound to the current state (aria-pressed={helloSequenceEnabled()}),
ensuring assistive tech knows when the sequence is enabled or disabled while
preserving the existing onClick behavior.

In `@examples/svelte/create-hotkey-sequence/src/App.svelte`:
- Around line 142-147: The toggle button controlling helloSequenceEnabled is
missing aria-pressed which would expose its pressed state to assistive tech;
update the button element that toggles helloSequenceEnabled to include
aria-pressed bound to that boolean (e.g., aria-pressed={helloSequenceEnabled})
so screen readers receive the current state while keeping the existing click
handler and visible label logic intact.

In `@examples/vue/useHotkeySequence/src/App.vue`:
- Around line 153-165: The toggle button isn't exposing its state to assistive
tech; update the button that toggles helloSequenceEnabled to include an
aria-pressed attribute bound to helloSequenceEnabled (e.g.,
aria-pressed="helloSequenceEnabled") and mark the status paragraph with a polite
live region (e.g., aria-live="polite") so screen readers are notified of
changes; ensure the button text still updates based on helloSequenceEnabled and
that the status paragraph (sequence-toggle-status) reflects the same state.

In `@examples/vue/useHotkeySequences/src/App.vue`:
- Line 223: The "Clear History" button currently lacks an explicit type which
can cause it to act as a submit button if moved inside a form; update the button
element that sets "history = []" (the Clear History button) to include
type="button" so it will not trigger form submission unintentionally.

In `@packages/solid-hotkeys/src/createHotkey.ts`:
- Around line 165-168: Remove the redundant immediate sync after creating a
registration: when you call manager.register(callback, optionsWithoutTarget) you
already pass the callback and options, so drop the extra assignments guarded by
registration.isActive; specifically remove or skip setting registration.callback
and calling registration.setOptions(optionsWithoutTarget) right after
manager.register(...) (references: manager.register, registration,
registration.isActive, registration.callback, registration.setOptions,
optionsWithoutTarget, callback).

In `@packages/solid-hotkeys/src/createHotkeySequence.ts`:
- Around line 142-145: The registration callback/options are being redundantly
re-assigned right after creating a new registration; remove the immediate sync
that sets registration.callback and calls registration.setOptions in
createHotkeySequence.ts (the block guarded by registration.isActive) because
manager.register(...) already receives the callback and merged options—leave
manager.register(...) to initialize the registration and delete the extra lines
that overwrite registration.callback and call registration.setOptions.

In `@packages/solid-hotkeys/tests/createHotkeys.test.tsx`:
- Around line 149-156: The test currently reaches into
HotkeyManager.registrations.state.values() which couples it to internal
structure; change the test to use a public API (or add one) on HotkeyManager
such as getRegistrations() or getRegistration(hotkey) that returns registration
objects or an enabled flag, then assert via
HotkeyManager.getInstance().getRegistration('Mod+Z') or iterate
getRegistrations() and check options.enabled; update the test to call
HotkeyManager.getInstance(), use getRegistrationCount() for the count assertion,
and replace the internal-state access with the new public accessor (or document
the coupling if you prefer not to add an API).

In `@packages/svelte-hotkeys/src/createHotkey.svelte.ts`:
- Around line 35-49: The registerHotkey function currently accepts a
RegisterableHotkey and calls normalizeHotkey again; change registerHotkey to
accept an already-normalized Hotkey string (or hotkeyStr) instead of
RegisterableHotkey, remove the internal normalizeHotkey call, and pass that
hotkey string directly into getHotkeyManager().register; then update the callers
that currently compute hotkeyStr (the places that call registerHotkey with
resolvedHotkey) to pass hotkeyStr into registerHotkey so no double-normalization
occurs (references: function registerHotkey, normalizeHotkey, and
getHotkeyManager().register).

In `@packages/svelte-hotkeys/src/createHotkeySequence.svelte.ts`:
- Around line 156-199: The two effect blocks in createHotkeySequence and
createHotkeySequenceAttachment duplicate registration/update/unregister logic
(variables registration, lastSequenceKey, sequenceKey and calls to
registerHotkeySequence), so extract that shared logic into a single helper
(e.g., createSequenceEffect) that accepts the target (element vs
document/window), the resolved sequence getter, callback, and options, and
performs the resolve, comparison (by formatHotkeySequence), register, update
(setOptions + update callback), and unregister behavior; then replace the
duplicated $effect bodies in createHotkeySequence and
createHotkeySequenceAttachment to call that helper and store/cleanup its
returned handle.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: efa73896-fc73-4914-8290-fe28359b1091

📥 Commits

Reviewing files that changed from the base of the PR and between 4767bbb and afec5a4.

📒 Files selected for processing (91)
  • .changeset/plural-sequences.md
  • docs/framework/angular/guides/hotkeys.md
  • docs/framework/angular/guides/sequences.md
  • docs/framework/angular/reference/functions/injectHotkey.md
  • docs/framework/angular/reference/functions/injectHotkeySequence.md
  • docs/framework/angular/reference/functions/injectHotkeySequences.md
  • docs/framework/angular/reference/interfaces/InjectHotkeyOptions.md
  • docs/framework/angular/reference/interfaces/InjectHotkeySequenceOptions.md
  • docs/framework/preact/guides/hotkeys.md
  • docs/framework/preact/guides/sequences.md
  • docs/framework/preact/reference/functions/useHotkey.md
  • docs/framework/preact/reference/functions/useHotkeySequence.md
  • docs/framework/preact/reference/functions/useHotkeySequences.md
  • docs/framework/preact/reference/functions/useHotkeys.md
  • docs/framework/react/guides/hotkeys.md
  • docs/framework/react/guides/sequences.md
  • docs/framework/react/reference/functions/useHotkey.md
  • docs/framework/react/reference/functions/useHotkeySequence.md
  • docs/framework/react/reference/functions/useHotkeySequences.md
  • docs/framework/react/reference/functions/useHotkeys.md
  • docs/framework/solid/guides/hotkeys.md
  • docs/framework/solid/guides/sequences.md
  • docs/framework/svelte/guides/hotkeys.md
  • docs/framework/svelte/guides/sequences.md
  • docs/framework/svelte/reference/functions/createHotkey.md
  • docs/framework/svelte/reference/functions/createHotkeyAttachment.md
  • docs/framework/svelte/reference/functions/createHotkeySequence.md
  • docs/framework/svelte/reference/functions/createHotkeySequenceAttachment.md
  • docs/framework/svelte/reference/interfaces/CreateHotkeyOptions.md
  • docs/framework/svelte/reference/interfaces/CreateHotkeySequenceOptions.md
  • docs/framework/vue/guides/hotkeys.md
  • docs/framework/vue/guides/sequences.md
  • docs/reference/classes/HotkeyManager.md
  • docs/reference/functions/getHotkeyManager.md
  • docs/reference/interfaces/HotkeyOptions.md
  • docs/reference/interfaces/HotkeyRegistration.md
  • docs/reference/interfaces/HotkeyRegistrationHandle.md
  • docs/reference/interfaces/SequenceOptions.md
  • examples/angular/injectHotkeySequence/src/app/app.component.html
  • examples/angular/injectHotkeySequence/src/app/app.component.ts
  • examples/angular/injectHotkeySequence/src/styles.css
  • examples/angular/injectHotkeySequences/src/app/app.component.html
  • examples/angular/injectHotkeySequences/src/app/app.component.ts
  • examples/angular/injectHotkeySequences/src/styles.css
  • examples/preact/useHotkeySequence/src/index.css
  • examples/preact/useHotkeySequence/src/index.tsx
  • examples/preact/useHotkeySequences/src/index.css
  • examples/preact/useHotkeySequences/src/index.tsx
  • examples/react/useHotkeySequence/src/index.css
  • examples/react/useHotkeySequence/src/index.tsx
  • examples/react/useHotkeySequences/src/index.css
  • examples/react/useHotkeySequences/src/index.tsx
  • examples/solid/createHotkeySequence/src/index.css
  • examples/solid/createHotkeySequence/src/index.tsx
  • examples/solid/createHotkeySequences/src/index.css
  • examples/solid/createHotkeySequences/src/index.tsx
  • examples/svelte/create-hotkey-sequence/src/App.svelte
  • examples/svelte/create-hotkey-sequence/src/index.css
  • examples/svelte/create-hotkey-sequences/src/App.svelte
  • examples/svelte/create-hotkey-sequences/src/index.css
  • examples/vue/useHotkeySequence/src/App.vue
  • examples/vue/useHotkeySequence/src/index.css
  • examples/vue/useHotkeySequences/src/App.vue
  • examples/vue/useHotkeySequences/src/index.css
  • packages/angular-hotkeys/src/injectHotkey.ts
  • packages/angular-hotkeys/src/injectHotkeySequence.ts
  • packages/angular-hotkeys/src/injectHotkeySequences.ts
  • packages/hotkeys/src/hotkey-manager.ts
  • packages/hotkeys/tests/sequence-manager.test.ts
  • packages/preact-hotkeys/src/useHotkey.ts
  • packages/preact-hotkeys/src/useHotkeySequence.ts
  • packages/preact-hotkeys/src/useHotkeySequences.ts
  • packages/preact-hotkeys/src/useHotkeys.ts
  • packages/preact-hotkeys/tests/useHotkey.test.tsx
  • packages/preact-hotkeys/tests/useHotkeySequences.test.tsx
  • packages/preact-hotkeys/tests/useHotkeys.test.tsx
  • packages/react-hotkeys/src/useHotkey.ts
  • packages/react-hotkeys/src/useHotkeySequence.ts
  • packages/react-hotkeys/src/useHotkeySequences.ts
  • packages/react-hotkeys/src/useHotkeys.ts
  • packages/react-hotkeys/tests/useHotkey.test.tsx
  • packages/react-hotkeys/tests/useHotkeySequences.test.tsx
  • packages/react-hotkeys/tests/useHotkeys.test.tsx
  • packages/solid-hotkeys/src/createHotkey.ts
  • packages/solid-hotkeys/src/createHotkeySequence.ts
  • packages/solid-hotkeys/tests/createHotkeySequences.test.tsx
  • packages/solid-hotkeys/tests/createHotkeys.test.tsx
  • packages/svelte-hotkeys/src/createHotkey.svelte.ts
  • packages/svelte-hotkeys/src/createHotkeySequence.svelte.ts
  • packages/vue-hotkeys/src/useHotkey.ts
  • packages/vue-hotkeys/src/useHotkeySequence.ts
✅ Files skipped from review due to trivial changes (50)
  • docs/framework/angular/guides/hotkeys.md
  • docs/framework/vue/guides/hotkeys.md
  • docs/framework/svelte/guides/hotkeys.md
  • examples/angular/injectHotkeySequence/src/styles.css
  • docs/framework/preact/guides/hotkeys.md
  • docs/framework/angular/reference/interfaces/InjectHotkeyOptions.md
  • docs/framework/angular/reference/functions/injectHotkey.md
  • docs/framework/svelte/reference/interfaces/CreateHotkeySequenceOptions.md
  • examples/svelte/create-hotkey-sequence/src/index.css
  • docs/reference/functions/getHotkeyManager.md
  • docs/framework/preact/reference/functions/useHotkey.md
  • docs/framework/preact/reference/functions/useHotkeySequence.md
  • examples/react/useHotkeySequence/src/index.css
  • docs/framework/react/reference/functions/useHotkey.md
  • examples/solid/createHotkeySequence/src/index.css
  • examples/vue/useHotkeySequence/src/index.css
  • docs/framework/svelte/reference/functions/createHotkeySequence.md
  • docs/framework/solid/guides/hotkeys.md
  • docs/framework/svelte/reference/functions/createHotkey.md
  • docs/framework/svelte/reference/functions/createHotkeyAttachment.md
  • docs/framework/svelte/reference/interfaces/CreateHotkeyOptions.md
  • docs/framework/angular/reference/functions/injectHotkeySequence.md
  • docs/reference/interfaces/HotkeyRegistration.md
  • examples/preact/useHotkeySequence/src/index.css
  • docs/framework/svelte/reference/functions/createHotkeySequenceAttachment.md
  • docs/framework/react/reference/functions/useHotkeySequence.md
  • packages/hotkeys/tests/sequence-manager.test.ts
  • docs/reference/interfaces/HotkeyRegistrationHandle.md
  • .changeset/plural-sequences.md
  • docs/framework/solid/guides/sequences.md
  • docs/framework/react/reference/functions/useHotkeys.md
  • docs/reference/classes/HotkeyManager.md
  • docs/framework/svelte/guides/sequences.md
  • docs/framework/angular/reference/functions/injectHotkeySequences.md
  • docs/framework/preact/reference/functions/useHotkeys.md
  • packages/hotkeys/src/hotkey-manager.ts
  • docs/framework/vue/guides/sequences.md
  • examples/solid/createHotkeySequences/src/index.tsx
  • docs/framework/preact/reference/functions/useHotkeySequences.md
  • examples/svelte/create-hotkey-sequences/src/index.css
  • examples/react/useHotkeySequences/src/index.css
  • examples/vue/useHotkeySequences/src/index.css
  • docs/framework/react/reference/functions/useHotkeySequences.md
  • examples/angular/injectHotkeySequences/src/styles.css
  • examples/solid/createHotkeySequences/src/index.css
  • docs/framework/react/guides/hotkeys.md
  • packages/preact-hotkeys/tests/useHotkeySequences.test.tsx
  • docs/framework/angular/reference/interfaces/InjectHotkeySequenceOptions.md
  • examples/preact/useHotkeySequences/src/index.tsx
  • examples/preact/useHotkeySequences/src/index.css
🚧 Files skipped from review as they are similar to previous changes (6)
  • docs/framework/angular/guides/sequences.md
  • docs/framework/react/guides/sequences.md
  • examples/svelte/create-hotkey-sequences/src/App.svelte
  • packages/solid-hotkeys/tests/createHotkeySequences.test.tsx
  • packages/react-hotkeys/tests/useHotkeySequences.test.tsx
  • docs/framework/preact/guides/sequences.md

Comment thread examples/vue/useHotkeySequences/src/App.vue Outdated
Comment on lines +114 to +120
const resolvedTarget =
sequenceOpts.target ??
(typeof document !== 'undefined' ? document : null)

if (!resolvedTarget) {
continue
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Don't fall back to document when a scoped target isn't ready.

sequenceOpts.target ?? document turns an explicit or temporary “no target yet” state into a global registration, so element-scoped sequences can fire on document while their host is still mounting. injectHotkey already treats an explicit missing target as “skip registration”; this bulk API should mirror that contract.

Suggested fix
-      const resolvedTarget =
-        sequenceOpts.target ??
-        (typeof document !== 'undefined' ? document : null)
+      const resolvedTarget =
+        'target' in sequenceOpts
+          ? (sequenceOpts.target ?? null)
+          : (typeof document !== 'undefined' ? document : null)

Also make InjectHotkeySequenceOptions['target'] nullable in packages/angular-hotkeys/src/injectHotkeySequence.ts, like InjectHotkeyOptions.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const resolvedTarget =
sequenceOpts.target ??
(typeof document !== 'undefined' ? document : null)
if (!resolvedTarget) {
continue
}
const resolvedTarget =
'target' in sequenceOpts
? (sequenceOpts.target ?? null)
: (typeof document !== 'undefined' ? document : null)
if (!resolvedTarget) {
continue
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/angular-hotkeys/src/injectHotkeySequences.ts` around lines 114 -
120, The code currently falls back to document for sequence registration by
using sequenceOpts.target ?? document which converts a missing/temporarily-unset
scoped target into a global registration; change the logic to treat a
null/undefined sequenceOpts.target as “skip registration” the same way
injectHotkey does: remove the document fallback and only use sequenceOpts.target
if it is non-null, continuing the loop when it is missing; also update the type
InjectHotkeySequenceOptions['target'] in injectHotkeySequence.ts to be nullable
(match InjectHotkeyOptions) so callers can explicitly pass null/undefined to
indicate “no target yet.”

Comment on lines +97 to +187
const sequenceKey = sequenceStrings.join('\0')

useEffect(() => {
const prevRegistrations = registrationsRef.current
const nextRegistrations = new Map<string, RegistrationRecord>()

const rows: Array<{
registrationKey: string
def: (typeof definitionsRef.current)[number]
seq: HotkeySequence
seqStr: string
mergedOptions: UseHotkeySequenceOptions
resolvedTarget: Document | HTMLElement | Window
}> = []

for (let i = 0; i < definitionsRef.current.length; i++) {
const def = definitionsRef.current[i]!
const seqStr = sequenceStringsRef.current[i]!
const seq = def.sequence
if (seq.length === 0) {
continue
}

const mergedOptions = {
...defaultOptionsRef.current,
...commonOptionsRef.current,
...def.options,
} as UseHotkeySequenceOptions

const resolvedTarget = isRef(mergedOptions.target)
? mergedOptions.target.current
: (mergedOptions.target ??
(typeof document !== 'undefined' ? document : null))

if (!resolvedTarget) {
continue
}

const registrationKey = `${i}:${seqStr}`
rows.push({
registrationKey,
def,
seq,
seqStr,
mergedOptions,
resolvedTarget,
})
}

const nextKeys = new Set(rows.map((r) => r.registrationKey))

for (const [key, record] of prevRegistrations) {
if (!nextKeys.has(key) && record.handle.isActive) {
record.handle.unregister()
}
}

for (const row of rows) {
const { registrationKey, def, seq, mergedOptions, resolvedTarget } = row

const existing = prevRegistrations.get(registrationKey)
if (existing?.handle.isActive && existing.target === resolvedTarget) {
nextRegistrations.set(registrationKey, existing)
continue
}

if (existing?.handle.isActive) {
existing.handle.unregister()
}

const handle = managerRef.current.register(seq, def.callback, {
...mergedOptions,
target: resolvedTarget,
})
nextRegistrations.set(registrationKey, {
handle,
target: resolvedTarget,
})
}

registrationsRef.current = nextRegistrations

return () => {
for (const { handle } of registrationsRef.current.values()) {
if (handle.isActive) {
handle.unregister()
}
}
registrationsRef.current = new Map()
}
}, [sequenceKey])
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

In Preact, does useEffect run the previous cleanup before re-running the effect when dependencies change, and do ref.current mutations trigger the effect when the dependency array itself stays the same?

💡 Result:

Yes, in Preact, useEffect runs the previous cleanup function before re-running the effect when dependencies change, matching React's behavior. Mutations to ref.current do not trigger the effect if the dependency array remains the same, as refs are designed for mutable values that do not cause re-renders or dependency changes.

Citations:


Reconciliation has the same lifecycle gap as the React version.

Line 179 clears registrationsRef.current during effect cleanup, so any sequenceKey change forces a full re-register on the next run instead of reusing unchanged rows. The effect is also keyed only by sequenceKey, while line 202 strips target from setOptions, so a target-only change leaves the sequence bound to the old element. Move the full teardown to an unmount-only effect and include resolved target identity in the reconciliation key.

Also applies to: 195-203

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/preact-hotkeys/src/useHotkeySequences.ts` around lines 97 - 187, The
effect currently clears registrationsRef.current during every cleanup and only
depends on sequenceKey, causing unnecessary full re-registers and stale target
bindings; update the reconciliation so registration identity includes resolved
target (e.g., incorporate resolvedTarget into registrationKey or the effect's
dependency key) and do not wipe registrationsRef.current inside this effect's
cleanup—instead create a separate unmount-only useEffect (empty deps) that
iterates registrationsRef.current and unregisters active handles and then clears
the map; keep the existing managerRef.current.register/unregister logic but
compute/compare keys using both sequence string and resolvedTarget (or a stable
identifier) so target-only changes trigger correct re-registration without
losing unchanged rows.

Comment thread packages/vue-hotkeys/src/useHotkeySequence.ts
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (3)
packages/react-hotkeys/src/useHotkeySequences.ts (1)

133-133: Index-based registration key causes full re-registration on array reordering.

Using ${i}:${seqStr} as the registration key means reordering definitions (e.g., swapping positions) will unregister and re-register all affected sequences, even if their content hasn't changed. Per the context snippets, register() always generates a new ID, so this creates ID instability.

Consider using a stable key based solely on the sequence string, or allowing definitions to provide an optional key property:

+ // In UseHotkeySequenceDefinition interface:
+ /** Optional stable key for reconciliation (defaults to sequence string) */
+ key?: string
- const registrationKey = `${i}:${seqStr}`
+ const registrationKey = def.key ?? seqStr

Note: Using only seqStr without the index would require handling duplicate sequences differently (e.g., appending a counter for duplicates).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/react-hotkeys/src/useHotkeySequences.ts` at line 133, The
registration key construction using `${i}:${seqStr}` in registrationKey causes
re-registration when definitions are reordered; change registrationKey to be
stable by deriving it from the sequence content (e.g., use seqStr) or an
explicit per-definition key property (support an optional definitions[].key),
and handle duplicate sequences by appending a deterministic counter or
occurrence index to seqStr when necessary; update any logic that reads
registrationKey (including the register() call and deregistration logic) to use
this new stable key so IDs remain stable across reorders.
packages/react-hotkeys/tests/useHotkeySequences.test.tsx (2)

110-113: Sequence matching uses array indexing instead of formatHotkeySequence.

The test finds the disabled registration by checking r.sequence[0] === 'D' && r.sequence[1] === 'D'. This works but is fragile if the sequence format changes. Consider using a more robust approach:

const disabledView = [...manager.registrations.state.values()].find(
  (r) => r.sequence.join(',') === 'D,D',
)

However, this is a minor concern for test code and the current approach is readable.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/react-hotkeys/tests/useHotkeySequences.test.tsx` around lines 110 -
113, The test locates the disabled registration by indexing into r.sequence
(r.sequence[0] === 'D' && r.sequence[1] === 'D'), which is fragile; update the
predicate to compare the whole sequence string instead (e.g., use
formatHotkeySequence(r.sequence) === 'D,D' or r.sequence.join(',') === 'D,D')
when searching via manager.registrations.state.values() so the test relies on a
stable sequence format rather than fixed indices.

116-149: Missing test for removing sequences from the array.

The test suite covers adding sequences dynamically but doesn't explicitly test removing sequences (like the useHotkeys tests do with "should handle dynamic array changes (remove hotkey)"). Consider adding a test to verify that removing a definition from the array properly unregisters it:

it('should handle dynamic array changes (remove sequence)', () => {
  const gg = vi.fn()
  const dd = vi.fn()

  const { rerender } = renderHook(
    ({ defs }: { defs: Array<UseHotkeySequenceDefinition> }) =>
      useHotkeySequences(defs),
    {
      initialProps: {
        defs: [
          { sequence: ['G', 'G'], callback: gg },
          { sequence: ['D', 'D'], callback: dd },
        ],
      },
    },
  )

  expect(SequenceManager.getInstance().getRegistrationCount()).toBe(2)

  rerender({
    defs: [{ sequence: ['G', 'G'], callback: gg }],
  })

  expect(SequenceManager.getInstance().getRegistrationCount()).toBe(1)

  dispatchKey('d')
  dispatchKey('d')
  expect(dd).not.toHaveBeenCalled()
})
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/react-hotkeys/tests/useHotkeySequences.test.tsx` around lines 116 -
149, Add a test that mirrors the existing "add sequence" test but verifies
removal: render the hook via useHotkeySequences with two definitions (sequence
G,G and D,D), assert SequenceManager.getInstance().getRegistrationCount() is 2,
call rerender with only the G,G definition, assert registration count is 1, then
dispatchKey('d') twice and assert the dd callback was not called; reference the
existing test's use of renderHook, rerender, dispatchKey, and
SequenceManager.getInstance().getRegistrationCount() to locate where to add this
new "should handle dynamic array changes (remove sequence)" case.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@packages/react-hotkeys/src/useHotkeySequences.ts`:
- Line 133: The registration key construction using `${i}:${seqStr}` in
registrationKey causes re-registration when definitions are reordered; change
registrationKey to be stable by deriving it from the sequence content (e.g., use
seqStr) or an explicit per-definition key property (support an optional
definitions[].key), and handle duplicate sequences by appending a deterministic
counter or occurrence index to seqStr when necessary; update any logic that
reads registrationKey (including the register() call and deregistration logic)
to use this new stable key so IDs remain stable across reorders.

In `@packages/react-hotkeys/tests/useHotkeySequences.test.tsx`:
- Around line 110-113: The test locates the disabled registration by indexing
into r.sequence (r.sequence[0] === 'D' && r.sequence[1] === 'D'), which is
fragile; update the predicate to compare the whole sequence string instead
(e.g., use formatHotkeySequence(r.sequence) === 'D,D' or r.sequence.join(',')
=== 'D,D') when searching via manager.registrations.state.values() so the test
relies on a stable sequence format rather than fixed indices.
- Around line 116-149: Add a test that mirrors the existing "add sequence" test
but verifies removal: render the hook via useHotkeySequences with two
definitions (sequence G,G and D,D), assert
SequenceManager.getInstance().getRegistrationCount() is 2, call rerender with
only the G,G definition, assert registration count is 1, then dispatchKey('d')
twice and assert the dd callback was not called; reference the existing test's
use of renderHook, rerender, dispatchKey, and
SequenceManager.getInstance().getRegistrationCount() to locate where to add this
new "should handle dynamic array changes (remove sequence)" case.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 9fa2fc56-22a5-4cab-8139-570bb41c59de

📥 Commits

Reviewing files that changed from the base of the PR and between afec5a4 and d3d57ef.

📒 Files selected for processing (15)
  • docs/framework/angular/reference/functions/injectHotkeySequence.md
  • docs/framework/angular/reference/interfaces/InjectHotkeySequenceOptions.md
  • examples/vue/useHotkeySequences/src/App.vue
  • packages/angular-hotkeys/src/injectHotkeySequence.ts
  • packages/angular-hotkeys/src/injectHotkeySequences.ts
  • packages/preact-hotkeys/src/useHotkeySequences.ts
  • packages/preact-hotkeys/src/useHotkeys.ts
  • packages/preact-hotkeys/tests/useHotkeySequences.test.tsx
  • packages/preact-hotkeys/tests/useHotkeys.test.tsx
  • packages/react-hotkeys/src/useHotkeySequences.ts
  • packages/react-hotkeys/src/useHotkeys.ts
  • packages/react-hotkeys/tests/useHotkeySequences.test.tsx
  • packages/react-hotkeys/tests/useHotkeys.test.tsx
  • packages/vue-hotkeys/src/useHotkey.ts
  • packages/vue-hotkeys/src/useHotkeySequence.ts
✅ Files skipped from review due to trivial changes (2)
  • docs/framework/angular/reference/functions/injectHotkeySequence.md
  • packages/preact-hotkeys/tests/useHotkeySequences.test.tsx
🚧 Files skipped from review as they are similar to previous changes (6)
  • packages/vue-hotkeys/src/useHotkeySequence.ts
  • packages/react-hotkeys/src/useHotkeys.ts
  • examples/vue/useHotkeySequences/src/App.vue
  • packages/preact-hotkeys/src/useHotkeys.ts
  • packages/vue-hotkeys/src/useHotkey.ts
  • packages/preact-hotkeys/src/useHotkeySequences.ts

@KevinVandy KevinVandy merged commit 8d5c31a into main Mar 25, 2026
5 of 6 checks passed
@KevinVandy KevinVandy deleted the hotkeysequences branch March 25, 2026 15:09
@github-actions github-actions Bot mentioned this pull request Mar 25, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant