Skip to content

Fix useHotkeySequence resets progress when modifier key is pressed before a key combo e.g. zA (Vim-like hotkeys)#74

Merged
KevinVandy merged 6 commits intoTanStack:mainfrom
klaskow:73-vim-like-hotkeys
Mar 22, 2026
Merged

Fix useHotkeySequence resets progress when modifier key is pressed before a key combo e.g. zA (Vim-like hotkeys)#74
KevinVandy merged 6 commits intoTanStack:mainfrom
klaskow:73-vim-like-hotkeys

Conversation

@klaskow
Copy link
Contributor

@klaskow klaskow commented Mar 20, 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

  • Bug Fixes

    • Modifier-only key presses (e.g., Shift) no longer reset or disrupt multi-step hotkey sequences, so Vim-style combos behave reliably.
  • Tests

    • Added tests verifying modifier-key handling and that incorrect intermediate keys still break sequences.
  • Documentation

    • Updated sequence-related docs to match current sources.
  • New Features

    • Added demo/example entries showcasing chained Shift+letter sequences.
  • Chores

    • Added a changeset recording the patch release.

@klaskow klaskow changed the title 73 vim like hotkeys Fix useHotkeySequence resets progress when modifier key is pressed before a key combo e.g. zA (Vim-like hotkeys) Mar 20, 2026
@coderabbitai
Copy link

coderabbitai bot commented Mar 20, 2026

📝 Walkthrough

Walkthrough

SequenceManager now ignores modifier-only KeyboardEvents during sequence processing, preventing modifier presses (e.g., Shift) between steps from advancing or resetting combo progress. Changes include implementation update, tests, docs reference adjustments, examples, and a changeset entry.

Changes

Cohort / File(s) Summary
Core Implementation
packages/hotkeys/src/sequence-manager.ts
Added early return in #processTargetEvent to skip processing when isModifierKey(event) is true; imported isModifierKey.
Tests
packages/hotkeys/tests/sequence-manager.test.ts
Added tests ensuring modifier-only keys do not disrupt in-progress sequences and that incorrect non-modifier keys still reset sequences.
Docs (references updated)
docs/reference/classes/SequenceManager.md, docs/reference/functions/createSequenceMatcher.md, docs/reference/functions/getSequenceManager.md
Updated "Defined in" source line references (line numbers shifted); no API/signature changes.
Examples (demos + usage snippets)
examples/.../useHotkeySequence/src/index.tsx, examples/.../createHotkeySequence/src/index.tsx, examples/angular/.../app.component.html, examples/angular/.../app.component.ts, examples/react/.../index.tsx, examples/preact/.../index.tsx, examples/solid/.../index.tsx, examples/svelte/.../App.svelte, examples/vue/.../App.vue
Added new "Chained Shift+letter sequences" demo and usage examples registering ['Shift+R', 'Shift+T'] across frameworks; UI text and usage snippets updated to note that modifier-only presses do not reset progress.
Changeset / Changelog
.changeset/lovely-boats-grin.md
New changeset recording a patch release that fixes modifier handling in useHotkeySequence/sequence processing.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐇 I hopped through keys both shy and bright,
A wandering Shift tried to spoil the night.
I learned to ignore the feathered press,
So chains stay steady, not a noisy mess. ✨

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Description check ⚠️ Warning The PR description uses the template structure but the critical 'Changes' section is empty, providing no explanation of the implementation or motivation. Fill in the 'Changes' section with a detailed description of what was modified and why, including the bug fix and example updates.
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (1 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically describes the main bug fix: modifier keys no longer reset hotkey sequence progress, enabling Vim-like sequences.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@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: 1

🧹 Nitpick comments (1)
packages/hotkeys/tests/sequence-manager.test.ts (1)

516-549: Good coverage for the reported bug; add one regression case for modifier-only sequence steps.

Please add a case like ['Shift', 'C'] to ensure modifier-only key steps still progress (and don’t get swallowed globally).

🧪 Suggested test addition
   describe('modifier key handling', () => {
+    it('should allow modifier-only step when explicitly part of the sequence', () => {
+      const manager = SequenceManager.getInstance()
+      const callback = vi.fn()
+
+      manager.register(['Shift', 'C'], callback)
+
+      document.dispatchEvent(
+        new KeyboardEvent('keydown', { key: 'Shift', bubbles: true }),
+      )
+      document.dispatchEvent(
+        new KeyboardEvent('keydown', { key: 'c', bubbles: true }),
+      )
+
+      expect(callback).toHaveBeenCalledTimes(1)
+    })
+
     it('should not reset sequence when modifier key is pressed between steps', () => {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/hotkeys/tests/sequence-manager.test.ts` around lines 516 - 549, Add
a regression test that registers a sequence containing a modifier-only step
(e.g., SequenceManager.getInstance().register(['Shift', 'C'], callback)) and
asserts the sequence still progresses when the modifier key is pressed alone
then the non-modifier step is pressed; specifically, simulate dispatching a
'Shift' keydown (modifier-only step) then a 'c' keydown with shiftKey: true and
assert callback was called once, and include a negative case where an unrelated
key between steps prevents the callback (use dispatchKey helper and the existing
describe 'modifier key handling' block to place the test near register and
dispatchKey usages).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.changeset/lovely-boats-grin.md:
- Line 2: The release type in the changeset entry "'@tanstack/hotkeys': minor"
is too high for a bug-fix; update that entry to use "patch" instead of "minor"
so the changeset reads "'@tanstack/hotkeys': patch" to reflect a patch-level
release.

---

Nitpick comments:
In `@packages/hotkeys/tests/sequence-manager.test.ts`:
- Around line 516-549: Add a regression test that registers a sequence
containing a modifier-only step (e.g.,
SequenceManager.getInstance().register(['Shift', 'C'], callback)) and asserts
the sequence still progresses when the modifier key is pressed alone then the
non-modifier step is pressed; specifically, simulate dispatching a 'Shift'
keydown (modifier-only step) then a 'c' keydown with shiftKey: true and assert
callback was called once, and include a negative case where an unrelated key
between steps prevents the callback (use dispatchKey helper and the existing
describe 'modifier key handling' block to place the test near register and
dispatchKey usages).

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: cd9a2baa-24e2-491b-86c8-78878623dfb4

📥 Commits

Reviewing files that changed from the base of the PR and between bbbb4ed and f61a53b.

📒 Files selected for processing (3)
  • .changeset/lovely-boats-grin.md
  • packages/hotkeys/src/sequence-manager.ts
  • packages/hotkeys/tests/sequence-manager.test.ts

@pkg-pr-new
Copy link

pkg-pr-new bot commented Mar 22, 2026

Open in StackBlitz

@tanstack/angular-hotkeys

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

@tanstack/hotkeys

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

@tanstack/hotkeys-devtools

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

@tanstack/preact-hotkeys

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

@tanstack/preact-hotkeys-devtools

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

@tanstack/react-hotkeys

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

@tanstack/react-hotkeys-devtools

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

@tanstack/solid-hotkeys

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

@tanstack/solid-hotkeys-devtools

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

@tanstack/svelte-hotkeys

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

@tanstack/vue-hotkeys

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

@tanstack/vue-hotkeys-devtools

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

commit: 22ed15c

Copy link

@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: 1

🤖 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/injectHotkeySequence/src/app/app.component.html`:
- Around line 117-119: The `<pre>` snippet contains unescaped '>' characters in
the arrow functions which violates HTMLHint's spec-char-escape rule; update the
two occurrences inside the snippet that call injectHotkeySequence (the lines
referencing injectHotkeySequence(['C', 'I', 'W'], () => changeInnerWord()) and
injectHotkeySequence(['Shift+R', 'Shift+T'], () => doSomething())) by replacing
each '>' in the arrow `=>` with `&gt;` so the HTML is properly escaped.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: afeaf62e-4d16-4d87-a5b7-2d7cccb15828

📥 Commits

Reviewing files that changed from the base of the PR and between 6296c17 and 22ed15c.

📒 Files selected for processing (7)
  • examples/angular/injectHotkeySequence/src/app/app.component.html
  • examples/angular/injectHotkeySequence/src/app/app.component.ts
  • examples/preact/useHotkeySequence/src/index.tsx
  • examples/react/useHotkeySequence/src/index.tsx
  • examples/solid/createHotkeySequence/src/index.tsx
  • examples/svelte/create-hotkey-sequence/src/App.svelte
  • examples/vue/useHotkeySequence/src/App.vue
✅ Files skipped from review due to trivial changes (1)
  • examples/angular/injectHotkeySequence/src/app/app.component.ts

Comment on lines +117 to +119
injectHotkeySequence(['C', 'I', 'W'], () => changeInnerWord())

injectHotkeySequence(['Shift+R', 'Shift+T'], () => doSomething())</pre
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

git ls-files | grep -E "examples/angular/injectHotkeySequence/src/app/app.component.html"

Repository: TanStack/hotkeys

Length of output: 124


🏁 Script executed:

if [ -f "examples/angular/injectHotkeySequence/src/app/app.component.html" ]; then
  wc -l "examples/angular/injectHotkeySequence/src/app/app.component.html"
fi

Repository: TanStack/hotkeys

Length of output: 128


🏁 Script executed:

if [ -f "examples/angular/injectHotkeySequence/src/app/app.component.html" ]; then
  sed -n '110,125p' "examples/angular/injectHotkeySequence/src/app/app.component.html"
fi

Repository: TanStack/hotkeys

Length of output: 456


🏁 Script executed:

find . -name ".htmlhintrc" -o -name "htmlhint*" -o -name ".htmlhintignore" 2>/dev/null | head -20

Repository: TanStack/hotkeys

Length of output: 73


🏁 Script executed:

rg "htmlhint\|spec-char-escape" --type json --type js --type yaml -A 3 | head -50

Repository: TanStack/hotkeys

Length of output: 42


🏁 Script executed:

cat .htmlhintrc

Repository: TanStack/hotkeys

Length of output: 213


Escape > in the <pre> usage snippet to satisfy HTMLHint linting.

The code contains unescaped > characters within => operators inside <pre> tags, which violates the spec-char-escape rule enabled in .htmlhintrc. Replace > with &gt; in both lines:

Suggested fix
-injectHotkeySequence(['C', 'I', 'W'], () => changeInnerWord())
+injectHotkeySequence(['C', 'I', 'W'], () =&gt; changeInnerWord())

-injectHotkeySequence(['Shift+R', 'Shift+T'], () => doSomething())
+injectHotkeySequence(['Shift+R', 'Shift+T'], () =&gt; doSomething())
📝 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
injectHotkeySequence(['C', 'I', 'W'], () => changeInnerWord())
injectHotkeySequence(['Shift+R', 'Shift+T'], () => doSomething())</pre
injectHotkeySequence(['C', 'I', 'W'], () =&gt; changeInnerWord())
injectHotkeySequence(['Shift+R', 'Shift+T'], () =&gt; doSomething())</pre
🧰 Tools
🪛 HTMLHint (1.9.2)

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

(spec-char-escape)


[error] 119-119: 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/injectHotkeySequence/src/app/app.component.html` around
lines 117 - 119, The `<pre>` snippet contains unescaped '>' characters in the
arrow functions which violates HTMLHint's spec-char-escape rule; update the two
occurrences inside the snippet that call injectHotkeySequence (the lines
referencing injectHotkeySequence(['C', 'I', 'W'], () => changeInnerWord()) and
injectHotkeySequence(['Shift+R', 'Shift+T'], () => doSomething())) by replacing
each '>' in the arrow `=>` with `&gt;` so the HTML is properly escaped.

@KevinVandy KevinVandy merged commit a3aa4f3 into TanStack:main Mar 22, 2026
5 of 6 checks passed
@github-actions github-actions bot mentioned this pull request Mar 22, 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.

2 participants