Skip to content

Commit 85dd9fd

Browse files
authored
fix: arrow key navigation in search skips keyword buttons (#1704)
1 parent 365bd9f commit 85dd9fd

File tree

3 files changed

+59
-2
lines changed

3 files changed

+59
-2
lines changed

app/components/Package/Card.vue

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,6 @@ const numberFormatter = useNumberFormatter()
172172
size="small"
173173
:aria-pressed="props.filters?.keywords.includes(keyword)"
174174
:title="`Filter by ${keyword}`"
175-
:data-result-index="index"
176175
@click.stop="emit('clickKeyword', keyword)"
177176
>
178177
{{ keyword }}

app/pages/search.vue

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -453,6 +453,16 @@ watch(displayResults, results => {
453453
}
454454
})
455455
456+
/**
457+
* Focus the header search input
458+
*/
459+
function focusSearchInput() {
460+
const searchInput = document.querySelector<HTMLInputElement>(
461+
'input[type="search"], input[name="q"]',
462+
)
463+
searchInput?.focus()
464+
}
465+
456466
function handleResultsKeydown(e: KeyboardEvent) {
457467
// If the active element is an input, navigate to exact match or wait for results
458468
if (e.key === 'Enter' && document.activeElement?.tagName === 'INPUT') {
@@ -489,7 +499,12 @@ function handleResultsKeydown(e: KeyboardEvent) {
489499
490500
if (e.key === 'ArrowUp') {
491501
e.preventDefault()
492-
const nextIndex = currentIndex < 0 ? 0 : Math.max(currentIndex - 1, 0)
502+
// At first result or no result focused: return focus to search input
503+
if (currentIndex <= 0) {
504+
focusSearchInput()
505+
return
506+
}
507+
const nextIndex = currentIndex - 1
493508
const el = elements[nextIndex]
494509
if (el) focusElement(el)
495510
return

test/e2e/interactions.spec.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,49 @@ test.describe('Search Pages', () => {
7373
await expect(page).toHaveURL(/\/(package|org|user)\/vue/)
7474
})
7575

76+
test('/search?q=vue → ArrowDown navigates only between results, not keyword buttons', async ({
77+
page,
78+
goto,
79+
}) => {
80+
await goto('/search?q=vue', { waitUntil: 'hydration' })
81+
82+
await expect(page.locator('text=/found \\d+|showing \\d+/i').first()).toBeVisible({
83+
timeout: 15000,
84+
})
85+
86+
const firstResult = page.locator('[data-result-index="0"]').first()
87+
const secondResult = page.locator('[data-result-index="1"]').first()
88+
await expect(firstResult).toBeVisible()
89+
await expect(secondResult).toBeVisible()
90+
91+
// ArrowDown from input focuses the first result
92+
await page.keyboard.press('ArrowDown')
93+
await expect(firstResult).toBeFocused()
94+
95+
// Second ArrowDown focuses the second result (not a keyword button within the first)
96+
await page.keyboard.press('ArrowDown')
97+
await expect(secondResult).toBeFocused()
98+
})
99+
100+
test('/search?q=vue → ArrowUp from first result returns focus to search input', async ({
101+
page,
102+
goto,
103+
}) => {
104+
await goto('/search?q=vue', { waitUntil: 'hydration' })
105+
106+
await expect(page.locator('text=/found \\d+|showing \\d+/i').first()).toBeVisible({
107+
timeout: 15000,
108+
})
109+
110+
// Navigate to first result
111+
await page.keyboard.press('ArrowDown')
112+
await expect(page.locator('[data-result-index="0"]').first()).toBeFocused()
113+
114+
// ArrowUp returns to the search input
115+
await page.keyboard.press('ArrowUp')
116+
await expect(page.locator('input[type="search"]')).toBeFocused()
117+
})
118+
76119
test('/search?q=vue → "/" focuses the search input from results', async ({ page, goto }) => {
77120
await goto('/search?q=vue', { waitUntil: 'hydration' })
78121

0 commit comments

Comments
 (0)