Skip to content

Fix: Use j/k instead of arrow keys for page navigation - #3165

Open
elracing wants to merge 2 commits into
npmx-dev:mainfrom
elracing:fix-issue-2648
Open

Fix: Use j/k instead of arrow keys for page navigation#3165
elracing wants to merge 2 commits into
npmx-dev:mainfrom
elracing:fix-issue-2648

Conversation

@elracing

Copy link
Copy Markdown

🔗 Linked issue

Resolves #2648

🧭 Context

The search results page was using arrow keys for custom result navigation, which conflicted with normal page scrolling and made the interaction feel inconsistent. This change updates the behavior so keyboard navigation uses j/k for moving between results while keeping Enter for activation.

📚 Description

This PR updates search result keyboard navigation to use j and k instead of the arrow keys. The new behavior makes the search experience more predictable and keeps regular arrow-key scrolling intact outside of this custom result-navigation flow.

I also extracted the mapping into a small helper and added regression coverage so the keyboard behavior stays protected going forward. The change includes unit tests for the new navigation mapping and key handling.

@vercel

vercel Bot commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
docs.npmx.dev Ready Ready Preview Aug 10, 2026 6:04am
npmx.dev Ready Ready Preview Aug 10, 2026 6:04am
1 Skipped Deployment
Project Deployment Actions Updated (UTC)
npmx-lunaria Ignored Ignored Aug 10, 2026 6:04am

Request Review

@coderabbitai

coderabbitai Bot commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Summary by CodeRabbit

  • New Features
    • Added j/k keyboard shortcuts for moving through search suggestions and results.
    • Press Enter to continue navigation or select the focused result.
    • Focus now moves smoothly between suggestions, results and the search field.
  • Bug Fixes
    • Keyboard navigation no longer interferes while typing in editable fields.
    • Unrelated keys are ignored during search navigation.

Walkthrough

Search result keyboard navigation now uses j and k, including uppercase variants, instead of arrow keys. Editable targets bypass navigation. Shared helpers define key handling, and tests cover the new mappings.

Changes

Search navigation

Layer / File(s) Summary
Navigation helper contract and tests
app/utils/search-navigation.ts, test/unit/app/utils/search-navigation.spec.ts
The shared helpers map j/k to navigation directions, recognise Enter, and include unit coverage for supported and unsupported keys.
Search page keyboard integration
app/pages/search.vue
The search page skips editable targets, applies direction-based focus traversal, preserves Enter activation and focus return, and registers j, k, and Enter as handled shortcuts.

Possibly related PRs

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarises the main change from arrow-key navigation to j/k navigation.
Description check ✅ Passed The description directly explains the j/k navigation change, preserved Enter activation, and added test coverage.
Linked Issues check ✅ Passed The changes implement j/k navigation for search results, preserve Enter activation, and retain normal arrow-key behaviour.
Out of Scope Changes check ✅ Passed The helper extraction, search-page updates, and unit tests directly support the linked issue objectives.
✨ 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.

@github-actions

Copy link
Copy Markdown

Hello! Thank you for opening your first PR to npmx, @elracing! 🚀

Here’s what will happen next:

  1. Our GitHub bots will run to check your changes.
    If they spot any issues you will see some error messages on this PR.
    Don’t hesitate to ask any questions if you’re not sure what these mean!

  2. In a few minutes, you’ll be able to see a preview of your changes on Vercel

  3. One or more of our maintainers will take a look and may ask you to make changes.
    We try to be responsive, but don’t worry if this takes a few days.

@codecov

codecov Bot commented Aug 10, 2026

Copy link
Copy Markdown

❌ 3 Tests Failed:

Tests completed Failed Passed Skipped
3109 3 3106 5
View the top 3 failed test(s) by shortest run time
interactions.spec.ts::Search Pages › /search?q=vue → ArrowUp from first result returns focus to search input
Stack Traces | 18.5s run time
expect(locator).toBeFocused() failed
interactions.spec.ts::Search Pages › /search?q=vue → ArrowDown navigates only between results, not keyword buttons
Stack Traces | 18.6s run time
expect(locator).toBeFocused() failed
interactions.spec.ts::Search Pages › /search?q=vue → keyboard navigation (arrow keys + enter)
Stack Traces | 18.8s run time
expect(page).toHaveURL(expected) failed

To view more test analytics, go to the Test Analytics Dashboard
📋 Got 3 mins? Take this short survey to help us improve Test Analytics.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@app/pages/search.vue`:
- Around line 522-524: Update the keyboard handling around
getSearchResultNavigationDirection so events with Ctrl, Meta, or Alt modifiers
return before calling preventDefault or performing custom navigation. Continue
allowing Shift-modified j/k events for uppercase J/K handling.
- Around line 495-497: Update the keyboard shortcut guard in the search page’s
keydown handler so Enter events from the search input can reach the existing
input-Enter handling, while retaining the early return for other editable
elements and disabled shortcuts. Preserve the exact-match and deferred
navigation behavior implemented by the downstream Enter case.
- Line 565: Update the search result shortcut registration in onKeyDown to
include uppercase J and K alongside the existing j, k, and Enter keys, ensuring
getSearchResultNavigationDirection can receive uppercase navigation input. Add
regression coverage that dispatches key values J and K and verifies they reach
handleResultsKeydown.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: d8ba0bd1-de0e-458d-aa32-4351740c9ca8

📥 Commits

Reviewing files that changed from the base of the PR and between 4367c84 and fdefe4f.

📒 Files selected for processing (3)
  • app/pages/search.vue
  • app/utils/search-navigation.ts
  • test/unit/app/utils/search-navigation.spec.ts

Comment thread app/pages/search.vue
Comment on lines +495 to 497
if (!keyboardShortcuts.value || isEditableElement(e.target)) {
return
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Keep the search-input Enter flow reachable.

An Enter keydown from the search input has that input as e.target. isEditableElement(e.target) then returns true, so this guard exits before the input Enter handling at Line 500. Exact-match navigation and deferred navigation after results arrive no longer run.

Allow the existing input Enter case through this guard.

Proposed fix
-  if (!keyboardShortcuts.value || isEditableElement(e.target)) {
+  const isInputEnter =
+    e.key === 'Enter' && document.activeElement?.tagName === 'INPUT'
+  if (!keyboardShortcuts.value || (isEditableElement(e.target) && !isInputEnter)) {
     return
   }
📝 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
if (!keyboardShortcuts.value || isEditableElement(e.target)) {
return
}
const isInputEnter =
e.key === 'Enter' && document.activeElement?.tagName === 'INPUT'
if (!keyboardShortcuts.value || (isEditableElement(e.target) && !isInputEnter)) {
return
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/pages/search.vue` around lines 495 - 497, Update the keyboard shortcut
guard in the search page’s keydown handler so Enter events from the search input
can reach the existing input-Enter handling, while retaining the early return
for other editable elements and disabled shortcuts. Preserve the exact-match and
deferred navigation behavior implemented by the downstream Enter case.

Comment thread app/pages/search.vue
Comment on lines +522 to +524
const direction = getSearchResultNavigationDirection(e.key)
if (direction) {
e.preventDefault()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Do not handle modified shortcut chords.

getSearchResultNavigationDirection(e.key) ignores modifier state. Ctrl, Meta, or Alt with j/k therefore enters this branch and calls preventDefault(). Keep Shift available for uppercase J/K, but return before custom handling when Ctrl, Meta, or Alt is active.

Proposed fix
+  if (e.metaKey || e.ctrlKey || e.altKey) return
+
   const direction = getSearchResultNavigationDirection(e.key)
📝 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 direction = getSearchResultNavigationDirection(e.key)
if (direction) {
e.preventDefault()
if (e.metaKey || e.ctrlKey || e.altKey) return
const direction = getSearchResultNavigationDirection(e.key)
if (direction) {
e.preventDefault()
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/pages/search.vue` around lines 522 - 524, Update the keyboard handling
around getSearchResultNavigationDirection so events with Ctrl, Meta, or Alt
modifiers return before calling preventDefault or performing custom navigation.
Continue allowing Shift-modified j/k events for uppercase J/K handling.

Comment thread app/pages/search.vue
}

onKeyDown(['ArrowDown', 'ArrowUp', 'Enter'], handleResultsKeydown)
onKeyDown(['j', 'k', 'Enter'], handleResultsKeydown)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== files =="
git ls-files | rg '(^|/)search\.vue$|package(-lock)?\.json$|pnpm-lock\.yaml$|yarn\.lock$' || true

echo "== outline search.vue =="
if [ -f app/pages/search.vue ]; then
  wc -l app/pages/search.vue
  ast-grep outline app/pages/search.vue --match onKeyDown --view compact || true
  ast-grep outline app/pages/search.vue --match handleResultsKeydown --view compact || true
fi

echo "== relevant snippets =="
if [ -f app/pages/search.vue ]; then
  sed -n '470,580p' app/pages/search.vue
fi

echo "== onKeyDown registrations and helper refs =="
rg -n "onKeyDown\\(|handleResultsKeydown|getSearchResultNavigationDirection|j|k|Enter" app/pages/search.vue || true

echo "== dependency versions =="
for f in package.json app/package.json; do
  [ -f "$f" ] && { echo "--- $f"; jq -r '.dependencies, .devDependencies' "$f" 2>/dev/null | rg -i "vueuse|`@vueuse`" || cat "$f"; }
done

Repository: npmx-dev/npmx.dev

Length of output: 13424


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== getSearchResultNavigationDirection implementation =="
fd -a 'search-navigation\.(ts|js)$' . | while read -r f; do
  echo "--- $f"
  sed -n '1,220p' "$f"
done

echo "== onKeyDown source in installed npm package if present =="
node - <<'JS'
const path = 'node_modules/@vueuse/core/index.mjs'
try {
  const fs = require('fs')
  const p = require.resolve('`@vueuse/core`')
  console.log('resolved:', p)
  const m = fs.readFileSync(p, 'utf8')
  const idx = m.indexOf('function onKeyDown')
  const end = m.indexOf('\n\n', idx + 10)
  console.log(m.slice(Math.max(0, idx - 300), Math.min(m.length, idx + 1500)))
} catch (err) {
  console.log('not available:', err.message)
}
JS

echo "== package manager lock version entries =="
if [ -f pnpm-lock.yaml ]; then
  rg -n '`@vueuse/core`|`@vueuse/shared`|`@vueuse/integrations`' pnpm-lock.yaml | head -80 || true
fi

Repository: npmx-dev/npmx.dev

Length of output: 2837


🌐 Web query:

VueUse onKeyDown uppercase keys key as literal string documentation

💡 Result:

In VueUse, the onKeyDown function (and the related onKeyStroke utility) uses the KeyboardEvent.key property to perform key filtering [1][2]. When you pass a literal string to represent a key, you should use the exact string value provided by the browser's KeyboardEvent.key property [2]. For alphabetical keys, this value is case-sensitive and corresponds to the character produced by the key press [2]. For example: - To listen for the lowercase "a" key, you pass 'a' [2]. - To listen for the uppercase "A" key, you pass 'A' [2]. If you want to listen to both uppercase and lowercase versions of a key, you can pass an array of strings to the key parameter [2]. Example: import { onKeyDown } from '@vueuse/core' // Listens for both lowercase 's' and uppercase 'S' onKeyDown(['s', 'S'], (e) => { console.log('S or s was pressed') }) If you need more complex logic, such as ensuring a specific modifier key is held down (e.g., Shift+A), it is recommended to use a custom predicate function, which provides full access to the event object [2]: onKeyDown( (e) => e.key === 'A' && e.shiftKey, (e) => { console.log('Shift+A pressed') }) For a complete reference of standard key values (such as 'ArrowDown', 'Enter', etc.), refer to the MDN documentation for KeyboardEvent key values, as VueUse relies on these standard strings for its key filtering [2].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== search for page-level tests mentioning search navigation keys =="
git ls-files | rg '(^|/)(\btest\b|tests?|__tests__|e2e|cypress|playwright|vitest|vite|nuxi)' || true
rg -n "Search|search|J|K|j|k|result navigation|getSearchResultNavigationDirection|handleResultsKeydown" . \
  -g '!node_modules' -g '!pnpm-lock.yaml' \
  -g '!**/dist/**' -g '!**/.output/**' \
  | head -200

echo "== locate test files =="
git ls-files | rg '(test|spec|scenario|integration|e2e)' | head -200

Repository: npmx-dev/npmx.dev

Length of output: 38004


Add uppercase J and K to the search result shortcut registration.

getSearchResultNavigationDirection supports J and K, but onKeyDown(['j', 'k', 'Enter'], handleResultsKeydown) filters those cases at the listener level, so uppercase navigation no longer reaches the page handler. Register both cases and add a regression test that dispatches key: 'J' and key: 'K'.

Proposed fix
-onKeyDown(['j', 'k', 'Enter'], handleResultsKeydown)
+onKeyDown(['j', 'J', 'k', 'K', 'Enter'], handleResultsKeydown)
📝 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
onKeyDown(['j', 'k', 'Enter'], handleResultsKeydown)
onKeyDown(['j', 'J', 'k', 'K', 'Enter'], handleResultsKeydown)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/pages/search.vue` at line 565, Update the search result shortcut
registration in onKeyDown to include uppercase J and K alongside the existing j,
k, and Enter keys, ensuring getSearchResultNavigationDirection can receive
uppercase navigation input. Add regression coverage that dispatches key values J
and K and verifies they reach handleResultsKeydown.

Source: MCP tools

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.

Use j/k instead of arrow keys for page navigation

1 participant