Skip to content

Commit 0b0fdaa

Browse files
committed
feat: update tests and fix handling of selected item
Selected item was jumping when scrolling up because of focus. Hook to the up/down arrows on a document level instead of section level.
1 parent bedb01b commit 0b0fdaa

3 files changed

Lines changed: 24 additions & 35 deletions

File tree

app/components/AppHeader.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ onKeyStroke(',', e => {
9292
<div class="flex-1 flex items-center justify-center gap-4 sm:gap-6">
9393
<!-- Search bar (shown on all pages except home and search) -->
9494
<search v-if="showSearchBar" class="hidden sm:block flex-1 max-w-md">
95-
<form method="GET" action="/search" class="relative" @submit.prevent="handleSearchInput">
95+
<form method="GET" action="/search" class="relative">
9696
<label for="header-search" class="sr-only">
9797
{{ $t('search.label') }}
9898
</label>

app/pages/search.vue

Lines changed: 6 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
<script setup lang="ts">
22
import { formatNumber } from '#imports'
3+
import { onKeyDown } from '@vueuse/core'
34
import { debounce } from 'perfect-debounce'
45
import { isValidNewPackageName, checkPackageExists } from '~/utils/package-name'
56
import { isPlatformSpecificPackage } from '~/utils/platform-packages'
@@ -595,50 +596,22 @@ function scrollToSelectedItem() {
595596
}
596597
}
597598
598-
function focusSelectedItem() {
599-
const suggIdx = toSuggestionIndex(unifiedSelectedIndex.value)
600-
const pkgIdx = toPackageIndex(unifiedSelectedIndex.value)
601-
602-
nextTick(() => {
603-
if (suggIdx !== null) {
604-
const el = document.querySelector<HTMLElement>(`[data-suggestion-index="${suggIdx}"]`)
605-
el?.focus()
606-
} else if (pkgIdx !== null) {
607-
scrollToSelectedItem()
608-
nextTick(() => {
609-
const el = document.querySelector<HTMLElement>(`[data-result-index="${pkgIdx}"]`)
610-
el?.focus()
611-
})
612-
}
613-
})
614-
}
615-
616599
function handleResultsKeydown(e: KeyboardEvent) {
617600
if (totalSelectableCount.value <= 0) return
618601
619-
const isFromInput = (e.target as HTMLElement).tagName === 'INPUT'
620-
621602
if (e.key === 'ArrowDown') {
622603
e.preventDefault()
623604
userHasNavigated.value = true
624605
unifiedSelectedIndex.value = clampUnifiedIndex(unifiedSelectedIndex.value + 1)
625-
if (isFromInput) {
626-
scrollToSelectedItem()
627-
} else {
628-
focusSelectedItem()
629-
}
606+
scrollToSelectedItem()
630607
return
631608
}
632609
633610
if (e.key === 'ArrowUp') {
634611
e.preventDefault()
635612
userHasNavigated.value = true
636613
unifiedSelectedIndex.value = clampUnifiedIndex(unifiedSelectedIndex.value - 1)
637-
if (isFromInput) {
638-
scrollToSelectedItem()
639-
} else {
640-
focusSelectedItem()
641-
}
614+
scrollToSelectedItem()
642615
return
643616
}
644617
@@ -664,6 +637,8 @@ function handleResultsKeydown(e: KeyboardEvent) {
664637
}
665638
}
666639
640+
onKeyDown(['ArrowDown', 'ArrowUp', 'Enter'], handleResultsKeydown)
641+
667642
function handleSuggestionSelect(index: number) {
668643
// Convert suggestion index to unified index
669644
unifiedSelectedIndex.value = -(suggestionCount.value - index)
@@ -688,7 +663,7 @@ defineOgImageComponent('Default', {
688663
<main class="overflow-x-hidden">
689664
<!-- Results area with container padding -->
690665
<div class="container-sm py-6">
691-
<section v-if="query" :aria-label="$t('search.results')" @keydown="handleResultsKeydown">
666+
<section v-if="query" :aria-label="$t('search.results')">
692667
<!-- Initial loading (only after user interaction, not during view transition) -->
693668
<LoadingSpinner v-if="showSearching" :text="$t('search.searching')" />
694669

tests/interactions.spec.ts

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,13 @@ test.describe('Search Pages', () => {
66

77
await expect(page.locator('text=/found \\d+/i')).toBeVisible()
88

9-
const searchInput = page.locator('input[type="search"]')
10-
await expect(searchInput).toBeFocused()
11-
129
const firstResult = page.locator('[data-result-index="0"]').first()
1310
await expect(firstResult).toBeVisible()
1411

12+
const searchInput = page.locator('input[type="search"]')
13+
// manually focus search
14+
await searchInput.focus()
15+
1516
// ArrowDown changes visual selection but keeps focus in input
1617
await page.keyboard.press('ArrowDown')
1718
await expect(searchInput).toBeFocused()
@@ -35,4 +36,17 @@ test.describe('Search Pages', () => {
3536
await page.keyboard.press('/')
3637
await expect(page.locator('input[type="search"]')).toBeFocused()
3738
})
39+
40+
test('/settings → search, keeps focus on search input', async ({ page, goto }) => {
41+
await goto('/settings', { waitUntil: 'domcontentloaded' })
42+
43+
const searchInput = page.locator('input[type="search"]')
44+
await searchInput.fill('vue')
45+
46+
await page.waitForLoadState('domcontentloaded')
47+
48+
await expect(page.locator('text=/found \\d+/i')).toBeVisible()
49+
50+
await expect(searchInput).toBeFocused()
51+
})
3852
})

0 commit comments

Comments
 (0)