Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 30 additions & 23 deletions app/pages/search.vue
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import { onKeyDown } from '@vueuse/core'
import { debounce } from 'perfect-debounce'
import { isValidNewPackageName } from '~/utils/package-name'
import { isPlatformSpecificPackage } from '~/utils/platform-packages'
import { isEditableElement } from '~/utils/input'
import { getSearchResultNavigationDirection } from '~/utils/search-navigation'
import { normalizeSearchParam } from '#shared/utils/url'

definePageMeta({
Expand Down Expand Up @@ -490,9 +492,10 @@ function focusSearchInput() {
const keyboardShortcuts = useKeyboardShortcuts()

function handleResultsKeydown(e: KeyboardEvent) {
if (!keyboardShortcuts.value) {
if (!keyboardShortcuts.value || isEditableElement(e.target)) {
return
}
Comment on lines +495 to 497

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.


// If the active element is an input, navigate to exact match or wait for results
if (e.key === 'Enter' && document.activeElement?.tagName === 'INPUT') {
// Get value directly from input (not from route query, which may be debounced)
Expand All @@ -516,37 +519,41 @@ function handleResultsKeydown(e: KeyboardEvent) {

if (totalSelectableCount.value <= 0) return

const elements = getFocusableElements()
if (elements.length === 0) return
const direction = getSearchResultNavigationDirection(e.key)
if (direction) {
e.preventDefault()
Comment on lines +522 to +524

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.

const elements = getFocusableElements()
if (elements.length === 0) return

const currentIndex = elements.findIndex(el => el === document.activeElement)
const currentIndex = elements.findIndex(el => el === document.activeElement)

if (e.key === 'ArrowDown') {
e.preventDefault()
const nextIndex = currentIndex < 0 ? 0 : Math.min(currentIndex + 1, elements.length - 1)
const el = elements[nextIndex]
if (el) focusElement(el)
return
}
if (direction === 'next') {
const nextIndex = currentIndex < 0 ? 0 : Math.min(currentIndex + 1, elements.length - 1)
const el = elements[nextIndex]
if (el) focusElement(el)
return
}

if (e.key === 'ArrowUp') {
e.preventDefault()
// At first result or no result focused: return focus to search input
if (currentIndex <= 0) {
focusSearchInput()
if (direction === 'previous') {
// At first result or no result focused: return focus to search input
if (currentIndex <= 0) {
focusSearchInput()
return
}
const nextIndex = currentIndex - 1
const el = elements[nextIndex]
if (el) focusElement(el)
return
}
const nextIndex = currentIndex - 1
const el = elements[nextIndex]
if (el) focusElement(el)
return
}

if (e.key === 'Enter') {
// Browser handles Enter on focused links naturally, but handle for non-link elements
if (document.activeElement && elements.includes(document.activeElement as HTMLElement)) {
if (
document.activeElement &&
getFocusableElements().includes(document.activeElement as HTMLElement)
) {
const el = document.activeElement as HTMLElement
// Only prevent default and click if it's not already a link (links handle Enter natively)
if (el.tagName !== 'A') {
e.preventDefault()
el.click()
Expand All @@ -555,7 +562,7 @@ function handleResultsKeydown(e: KeyboardEvent) {
}
}

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


useSeoMeta({
title: () =>
Expand Down
20 changes: 20 additions & 0 deletions app/utils/search-navigation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
export type SearchResultNavigationDirection = 'next' | 'previous'

export function getSearchResultNavigationDirection(
key: string,
): SearchResultNavigationDirection | null {
switch (key) {
case 'j':
case 'J':
return 'next'
case 'k':
case 'K':
return 'previous'
default:
return null
}
}

export function isSearchResultNavigationKey(key: string): boolean {
return getSearchResultNavigationDirection(key) !== null || key === 'Enter'
}
35 changes: 35 additions & 0 deletions test/unit/app/utils/search-navigation.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { describe, expect, it } from 'vitest'
import {
getSearchResultNavigationDirection,
isSearchResultNavigationKey,
} from '../../../../app/utils/search-navigation'

describe('search navigation helper', () => {
it('returns next for j and J', () => {
expect(getSearchResultNavigationDirection('j')).toBe('next')
expect(getSearchResultNavigationDirection('J')).toBe('next')
})

it('returns previous for k and K', () => {
expect(getSearchResultNavigationDirection('k')).toBe('previous')
expect(getSearchResultNavigationDirection('K')).toBe('previous')
})

it('returns null for non-navigation keys', () => {
expect(getSearchResultNavigationDirection('ArrowDown')).toBeNull()
expect(getSearchResultNavigationDirection('Enter')).toBeNull()
expect(getSearchResultNavigationDirection('x')).toBeNull()
})

it('identifies j/k/Enter as navigation keys', () => {
expect(isSearchResultNavigationKey('j')).toBe(true)
expect(isSearchResultNavigationKey('k')).toBe(true)
expect(isSearchResultNavigationKey('Enter')).toBe(true)
})

it('does not identify other keys as navigation keys', () => {
expect(isSearchResultNavigationKey('ArrowDown')).toBe(false)
expect(isSearchResultNavigationKey('Escape')).toBe(false)
expect(isSearchResultNavigationKey(' ')).toBe(false)
})
})
Loading