-
-
Notifications
You must be signed in to change notification settings - Fork 518
Fix: Use j/k instead of arrow keys for page navigation #3165
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -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({ | ||||||||||||||||||
|
|
@@ -490,9 +492,10 @@ function focusSearchInput() { | |||||||||||||||||
| const keyboardShortcuts = useKeyboardShortcuts() | ||||||||||||||||||
|
|
||||||||||||||||||
| function handleResultsKeydown(e: KeyboardEvent) { | ||||||||||||||||||
| if (!keyboardShortcuts.value) { | ||||||||||||||||||
| if (!keyboardShortcuts.value || isEditableElement(e.target)) { | ||||||||||||||||||
| return | ||||||||||||||||||
| } | ||||||||||||||||||
|
|
||||||||||||||||||
| // 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) | ||||||||||||||||||
|
|
@@ -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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win Do not handle modified shortcut chords.
Proposed fix+ if (e.metaKey || e.ctrlKey || e.altKey) return
+
const direction = getSearchResultNavigationDirection(e.key)📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||
| 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() | ||||||||||||||||||
|
|
@@ -555,7 +562,7 @@ function handleResultsKeydown(e: KeyboardEvent) { | |||||||||||||||||
| } | ||||||||||||||||||
| } | ||||||||||||||||||
|
|
||||||||||||||||||
| onKeyDown(['ArrowDown', 'ArrowUp', 'Enter'], handleResultsKeydown) | ||||||||||||||||||
| onKeyDown(['j', 'k', 'Enter'], handleResultsKeydown) | ||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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"; }
doneRepository: 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
fiRepository: npmx-dev/npmx.dev Length of output: 2837 🌐 Web query:
💡 Result: In VueUse, the 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 -200Repository: npmx-dev/npmx.dev Length of output: 38004 Add uppercase J and K to the search result shortcut registration.
Proposed fix-onKeyDown(['j', 'k', 'Enter'], handleResultsKeydown)
+onKeyDown(['j', 'J', 'k', 'K', 'Enter'], handleResultsKeydown)📝 Committable suggestion
Suggested change
🤖 Prompt for AI AgentsSource: MCP tools |
||||||||||||||||||
|
|
||||||||||||||||||
| useSeoMeta({ | ||||||||||||||||||
| title: () => | ||||||||||||||||||
|
|
||||||||||||||||||
| 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' | ||
| } |
| 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) | ||
| }) | ||
| }) |
There was a problem hiding this comment.
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
keydownfrom the search input has that input ase.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
📝 Committable suggestion
🤖 Prompt for AI Agents