Skip to content

Commit f916120

Browse files
committed
fix(search): prevent scroll jump during pagination
1 parent 26d967e commit f916120

1 file changed

Lines changed: 31 additions & 26 deletions

File tree

app/pages/search.vue

Lines changed: 31 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,16 @@ const {
2121
} = usePackageListPreferences()
2222
2323
// Debounced URL update for page (less aggressive to avoid too many URL changes)
24+
//Use History API directly to update URL without triggering Router's scroll-to-top
2425
const updateUrlPage = debounce((page: number) => {
25-
router.replace({
26-
query: {
27-
...route.query,
28-
page: page > 1 ? page : undefined,
29-
},
30-
})
26+
const url = new URL(window.location.href)
27+
if (page > 1) {
28+
url.searchParams.set('page', page.toString())
29+
} else {
30+
url.searchParams.delete('page')
31+
}
32+
// This updates the address bar "silently"
33+
window.history.replaceState({}, '', url)
3134
}, 500)
3235
3336
const { model: searchQuery, provider: searchProvider } = useGlobalSearch()
@@ -276,7 +279,8 @@ function handlePageChange(page: number) {
276279
}
277280
278281
// Reset page when query changes
279-
watch(query, () => {
282+
watch(query, (newQuery, oldQuery) => {
283+
if (newQuery.trim() === (oldQuery || '').trim()) return
280284
currentPage.value = 1
281285
hasInteracted.value = true
282286
})
@@ -536,7 +540,7 @@ defineOgImageComponent('Default', {
536540
</script>
537541

538542
<template>
539-
<main class="flex-1 py-8" :class="{ 'overflow-x-hidden': viewMode !== 'table' }">
543+
<main class="flex-1 py-8 search-page" :class="{ 'overflow-x-hidden': viewMode !== 'table' }">
540544
<div class="container-sm">
541545
<div class="flex items-center justify-between gap-4 mb-4">
542546
<h1 class="font-mono text-2xl sm:text-3xl font-medium">
@@ -545,12 +549,13 @@ defineOgImageComponent('Default', {
545549
<SearchProviderToggle />
546550
</div>
547551

548-
<section v-if="query">
549-
<!-- Initial loading (only after user interaction, not during view transition) -->
550-
<LoadingSpinner v-if="showSearching" :text="$t('search.searching')" />
552+
<section v-if="query" class="results-layout">
553+
<LoadingSpinner
554+
v-if="showSearching && displayResults.length === 0"
555+
:text="$t('search.searching')"
556+
/>
551557

552-
<div v-else-if="visibleResults">
553-
<!-- User/Org search suggestions -->
558+
<div v-show="results || displayResults.length > 0">
554559
<div v-if="validatedSuggestions.length > 0" class="mb-6 space-y-3">
555560
<SearchSuggestionCard
556561
v-for="(suggestion, idx) in validatedSuggestions"
@@ -565,9 +570,8 @@ defineOgImageComponent('Default', {
565570
/>
566571
</div>
567572

568-
<!-- Claim prompt - shown at top when valid name but no exact match -->
569573
<div
570-
v-if="showClaimPrompt && visibleResults.total > 0"
574+
v-if="showClaimPrompt && visibleResults && visibleResults.total > 0"
571575
class="mb-6 p-4 bg-bg-subtle border border-border rounded-lg sm:flex hidden flex-row sm:items-center gap-3 sm:gap-4"
572576
>
573577
<div class="flex-1 min-w-0">
@@ -585,15 +589,13 @@ defineOgImageComponent('Default', {
585589
</button>
586590
</div>
587591

588-
<!-- Rate limited by npm - check FIRST before showing any results -->
589592
<div v-if="isRateLimited" role="status" class="py-12">
590593
<p class="text-fg-muted font-mono mb-6 text-center">
591594
{{ $t('search.rate_limited') }}
592595
</p>
593596
</div>
594597

595-
<!-- Enhanced toolbar -->
596-
<div v-else-if="visibleResults.total > 0" class="mb-6">
598+
<div v-else-if="visibleResults && visibleResults.total > 0" class="mb-6">
597599
<PackageListToolbar
598600
:filters="filters"
599601
v-model:sort-option="sortOption"
@@ -618,7 +620,6 @@ defineOgImageComponent('Default', {
618620
@update:updated-within="setUpdatedWithin"
619621
@toggle-keyword="toggleKeyword"
620622
/>
621-
<!-- Show count status (infinite scroll mode only) -->
622623
<p
623624
v-if="viewMode === 'cards' && paginationMode === 'infinite'"
624625
role="status"
@@ -642,7 +643,6 @@ defineOgImageComponent('Default', {
642643
$t('search.updating')
643644
}}</span>
644645
</p>
645-
<!-- Show "x of y" (paginated/table mode only) -->
646646
<p
647647
v-if="viewMode === 'table' || paginationMode === 'paginated'"
648648
role="status"
@@ -664,13 +664,11 @@ defineOgImageComponent('Default', {
664664
</p>
665665
</div>
666666

667-
<!-- No results found -->
668667
<div v-else-if="status === 'success' || status === 'error'" role="status" class="py-12">
669668
<p class="text-fg-muted font-mono mb-6 text-center">
670669
{{ $t('search.no_results', { query }) }}
671670
</p>
672671

673-
<!-- User/Org suggestions when no packages found -->
674672
<div v-if="validatedSuggestions.length > 0" class="max-w-md mx-auto mb-6 space-y-3">
675673
<SearchSuggestionCard
676674
v-for="(suggestion, idx) in validatedSuggestions"
@@ -685,7 +683,6 @@ defineOgImageComponent('Default', {
685683
/>
686684
</div>
687685

688-
<!-- Offer to claim the package name if it's valid -->
689686
<div v-if="showClaimPrompt" class="max-w-md mx-auto text-center hidden sm:block">
690687
<div class="p-4 bg-bg-subtle border border-border rounded-lg">
691688
<p class="text-sm text-fg-muted mb-3">{{ $t('search.want_to_claim') }}</p>
@@ -701,7 +698,7 @@ defineOgImageComponent('Default', {
701698
</div>
702699

703700
<PackageList
704-
v-if="displayResults.length > 0 && !isRateLimited"
701+
v-show="displayResults.length > 0 && !isRateLimited"
705702
:results="displayResults"
706703
:search-query="query"
707704
:filters="filters"
@@ -722,7 +719,6 @@ defineOgImageComponent('Default', {
722719
@click-keyword="toggleKeyword"
723720
/>
724721

725-
<!-- Pagination controls -->
726722
<PaginationControls
727723
v-if="displayResults.length > 0 && !isRateLimited"
728724
v-model:mode="paginationMode"
@@ -739,7 +735,6 @@ defineOgImageComponent('Default', {
739735
</section>
740736
</div>
741737

742-
<!-- Claim package modal -->
743738
<PackageClaimPackageModal
744739
ref="claimPackageModalRef"
745740
:package-name="query"
@@ -748,3 +743,13 @@ defineOgImageComponent('Default', {
748743
/>
749744
</main>
750745
</template>
746+
747+
<style scoped>
748+
.search-page {
749+
overflow-anchor: none;
750+
}
751+
752+
.results-layout {
753+
min-height: 100vh;
754+
}
755+
</style>

0 commit comments

Comments
 (0)