Fix camera jumping when opening InfoPanel
All checks were successful
Build Docker Image / build (push) Successful in 4m17s

Replace setTimeout/debounce with event-based approach:
- Add isInfoLoading computed that tracks all info loading states
- Pass infoLoading prop through CatalogPage to CatalogMap
- Watch infoLoading transition from true->false to trigger fitBounds
- Remove setTimeout hack in favor of proper loading state detection
This commit is contained in:
Ruslan Bakiev
2026-01-27 12:25:15 +07:00
parent 497a80f0c6
commit f269c0daf0
3 changed files with 28 additions and 23 deletions

View File

@@ -51,6 +51,7 @@ const props = withDefaults(defineProps<{
entityType?: 'offer' | 'hub' | 'supplier'
initialCenter?: [number, number]
initialZoom?: number
infoLoading?: boolean
relatedPoints?: Array<{
uuid: string
name: string
@@ -64,6 +65,7 @@ const props = withDefaults(defineProps<{
initialCenter: () => [37.64, 55.76],
initialZoom: 2,
useServerClustering: false,
infoLoading: false,
items: () => [],
clusteredPoints: () => [],
relatedPoints: () => []
@@ -693,11 +695,8 @@ watch(() => props.hoveredItem, () => {
}
}, { deep: true })
// Debounced fitBounds to avoid camera jumping when points load incrementally
let fitBoundsTimeout: ReturnType<typeof setTimeout> | null = null
// Update related points layer when relatedPoints changes + fit bounds (debounced)
watch(() => props.relatedPoints, (points) => {
// Update related points layer when relatedPoints changes
watch(() => props.relatedPoints, () => {
if (!mapRef.value || !mapInitialized.value) return
// Update the source data immediately
@@ -705,26 +704,23 @@ watch(() => props.relatedPoints, (points) => {
if (source) {
source.setData(relatedPointsGeoJson.value)
}
// Debounce fitBounds - wait for all points to load before zooming
if (fitBoundsTimeout) {
clearTimeout(fitBoundsTimeout)
}
if (points && points.length > 0) {
fitBoundsTimeout = setTimeout(() => {
if (!mapRef.value) return
const bounds = new LngLatBounds()
points.forEach(p => {
bounds.extend([p.longitude, p.latitude])
})
if (!bounds.isEmpty()) {
mapRef.value.fitBounds(bounds, { padding: 80, maxZoom: 12 })
}
}, 300) // Wait 300ms for all points to load
}
}, { deep: true })
// Fit bounds when info loading finishes (all related data loaded)
watch(() => props.infoLoading, (loading, wasLoading) => {
// Only fit bounds when loading changes from true to false (data finished loading)
if (wasLoading && !loading && props.relatedPoints && props.relatedPoints.length > 0) {
if (!mapRef.value) return
const bounds = new LngLatBounds()
props.relatedPoints.forEach(p => {
bounds.extend([p.longitude, p.latitude])
})
if (!bounds.isEmpty()) {
mapRef.value.fitBounds(bounds, { padding: 80, maxZoom: 12 })
}
}
})
// Watch for pointColor or entityType changes - update colors and icons
watch([() => props.pointColor, () => props.entityType], async ([newColor, newType]) => {
if (!mapRef.value || !mapInitialized.value) return

View File

@@ -24,6 +24,7 @@
:hovered-item-id="hoveredId"
:hovered-item="hoveredItem"
:related-points="relatedPoints"
:info-loading="infoLoading"
@select-item="onMapSelect"
@bounds-change="onBoundsChange"
/>
@@ -250,6 +251,7 @@ const props = withDefaults(defineProps<{
items?: MapItem[]
showPanel?: boolean
filterByBounds?: boolean
infoLoading?: boolean
relatedPoints?: Array<{
uuid: string
name: string
@@ -266,6 +268,7 @@ const props = withDefaults(defineProps<{
items: () => [],
showPanel: false,
filterByBounds: false,
infoLoading: false,
relatedPoints: () => []
})

View File

@@ -11,6 +11,7 @@
:show-panel="showPanel"
:filter-by-bounds="filterByBounds"
:related-points="relatedPoints"
:info-loading="isInfoLoading"
@select="onMapSelect"
@bounds-change="onBoundsChange"
@update:filter-by-bounds="$event ? setBoundsInUrl(currentMapBounds) : clearBoundsFromUrl()"
@@ -356,6 +357,11 @@ watch(searchTrigger, () => {
// Loading state
const isLoading = computed(() => offersLoading.value || selectionLoading.value)
// Info loading state for map fitBounds (true while any info data is still loading)
const isInfoLoading = computed(() =>
infoLoading.value || isLoadingProducts.value || isLoadingHubs.value || isLoadingSuppliers.value || isLoadingOffers.value
)
// Show panel when selecting OR when showing info OR when showing quote results
const showPanel = computed(() => {
return selectMode.value !== null || infoId.value !== null || showQuoteResults.value