Skip to content

Commit f33f797

Browse files
committed
fix: enable scrolling in virtual list
1 parent 8cee51b commit f33f797

5 files changed

Lines changed: 55 additions & 22 deletions

File tree

app/app.vue

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,11 @@ function handleGlobalKeydown(e: KeyboardEvent) {
1717
const isEditableTarget =
1818
target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.isContentEditable
1919
20-
if (e.key === '/' && !target.isContentEditable) {
20+
if (isEditableTarget) {
21+
return
22+
}
23+
24+
if (e.key === '/') {
2125
e.preventDefault()
2226
2327
// Try to find and focus search input on current page
@@ -31,11 +35,6 @@ function handleGlobalKeydown(e: KeyboardEvent) {
3135
}
3236
3337
router.push('/search')
34-
return
35-
}
36-
37-
if (isEditableTarget) {
38-
return
3938
}
4039
}
4140

app/components/PackageCard.vue

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,14 +19,17 @@ const emit = defineEmits<{
1919
</script>
2020

2121
<template>
22-
<article class="group card-interactive" :class="{ 'bg-bg-muted border-border-hover': selected }">
22+
<article
23+
class="group card-interactive scroll-mt-48 scroll-mb-6"
24+
:class="{ 'bg-bg-muted border-border-hover': selected }"
25+
>
2326
<NuxtLink
2427
:to="{ name: 'package', params: { package: result.package.name.split('/') } }"
2528
:prefetch-on="prefetch ? 'visibility' : 'interaction'"
26-
class="block focus:outline-none decoration-none"
29+
class="block focus:outline-none decoration-none scroll-mt-48 scroll-mb-6"
2730
:data-result-index="index"
28-
@focus="emit('focus', index)"
29-
@mouseenter="emit('focus', index)"
31+
@focus="index != null && emit('focus', index)"
32+
@mouseenter="index != null && emit('focus', index)"
3033
>
3134
<header class="flex items-start justify-between gap-4 mb-2">
3235
<component

app/components/PackageList.vue

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,14 @@ watch(
8282
}
8383
},
8484
)
85+
86+
function scrollToIndex(index: number, smooth = true) {
87+
listRef.value?.scrollToIndex(index, { align: 'center', smooth })
88+
}
89+
90+
defineExpose({
91+
scrollToIndex,
92+
})
8593
</script>
8694

8795
<template>

app/composables/useVirtualInfiniteScroll.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,10 @@ export interface WindowVirtualizerHandle {
55
readonly viewportSize: number
66
findItemIndex: (offset: number) => number
77
getItemOffset: (index: number) => number
8-
scrollToIndex: (index: number, opts?: { align?: 'start' | 'center' | 'end' }) => void
8+
scrollToIndex: (
9+
index: number,
10+
opts?: { align?: 'start' | 'center' | 'end'; smooth?: boolean },
11+
) => void
912
}
1013

1114
export interface UseVirtualInfiniteScrollOptions {

app/pages/search.vue

Lines changed: 31 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ const isSearchFocused = ref(false)
4848
const searchInputRef = ref<HTMLInputElement>()
4949
5050
const selectedIndex = ref(0)
51+
const packageListRef = useTemplateRef('packageListRef')
5152
5253
const resultCount = computed(() => visibleResults.value?.objects.length ?? 0)
5354
@@ -56,25 +57,46 @@ function clampIndex(next: number) {
5657
return Math.max(0, Math.min(resultCount.value - 1, next))
5758
}
5859
60+
function scrollToSelectedResult() {
61+
// Use virtualizer's scrollToIndex to ensure the item is rendered and visible
62+
packageListRef.value?.scrollToIndex(selectedIndex.value)
63+
}
64+
5965
function focusSelectedResult() {
60-
const el = document.querySelector<HTMLElement>(`[data-result-index="${selectedIndex.value}"]`)
61-
el?.focus()
66+
// First ensure the item is rendered by scrolling to it
67+
scrollToSelectedResult()
68+
// Then focus it after a tick to allow rendering
69+
nextTick(() => {
70+
const el = document.querySelector<HTMLElement>(`[data-result-index="${selectedIndex.value}"]`)
71+
el?.focus()
72+
})
6273
}
6374
6475
function handleResultsKeydown(e: KeyboardEvent) {
6576
if (resultCount.value <= 0) return
6677
78+
const isFromInput = (e.target as HTMLElement).tagName === 'INPUT'
79+
6780
if (e.key === 'ArrowDown') {
6881
e.preventDefault()
6982
selectedIndex.value = clampIndex(selectedIndex.value + 1)
70-
focusSelectedResult()
83+
// Only move focus if already in results, not when typing in search input
84+
if (isFromInput) {
85+
scrollToSelectedResult()
86+
} else {
87+
focusSelectedResult()
88+
}
7189
return
7290
}
7391
7492
if (e.key === 'ArrowUp') {
7593
e.preventDefault()
7694
selectedIndex.value = clampIndex(selectedIndex.value - 1)
77-
focusSelectedResult()
95+
if (isFromInput) {
96+
scrollToSelectedResult()
97+
} else {
98+
focusSelectedResult()
99+
}
78100
return
79101
}
80102
@@ -198,13 +220,10 @@ watch(query, () => {
198220
hasInteracted.value = true
199221
})
200222
201-
watch(
202-
() => visibleResults.value?.objects,
203-
objects => {
204-
if (!objects?.length) return
205-
selectedIndex.value = 0
206-
},
207-
)
223+
// Reset selection when query changes (new search)
224+
watch(query, () => {
225+
selectedIndex.value = 0
226+
})
208227
209228
// Check if current query could be a valid package name
210229
const isValidPackageName = computed(() => isValidNewPackageName(query.value.trim()))
@@ -423,6 +442,7 @@ defineOgImageComponent('Default', {
423442

424443
<PackageList
425444
v-if="visibleResults.objects.length > 0"
445+
ref="packageListRef"
426446
:results="visibleResults.objects"
427447
:selected-index="selectedIndex"
428448
heading-level="h2"

0 commit comments

Comments
 (0)