diff --git a/client/src/components/Map/MapView.test.tsx b/client/src/components/Map/MapView.test.tsx
index ce27ae1c4..9149df22e 100644
--- a/client/src/components/Map/MapView.test.tsx
+++ b/client/src/components/Map/MapView.test.tsx
@@ -244,4 +244,22 @@ describe('MapView', () => {
rerender()
expect(mapMock.fitBounds.mock.calls.length).toBeGreaterThan(afterFirst)
})
+
+ it('FE-COMP-MAPVIEW-020: a day fit expands to include the route once it arrives (#1128)', async () => {
+ const L = ((await import('leaflet')).default) as unknown as { latLngBounds: ReturnType }
+ const dayPlaces = [
+ buildMapPlace({ id: 1, lat: 48.0, lng: 2.0 }),
+ buildMapPlace({ id: 2, lat: 48.1, lng: 2.1 }),
+ ]
+ // Day selected, route not computed yet → first fit is the two destinations.
+ const { rerender } = render()
+ const lastBounds = () => { const c = L.latLngBounds.mock.calls; return c[c.length - 1][0] }
+ expect(lastBounds()).toHaveLength(2)
+
+ // The day's route arrives → one-shot re-fit including the 3 route points.
+ L.latLngBounds.mockClear()
+ rerender()
+ expect(L.latLngBounds).toHaveBeenCalled()
+ expect(lastBounds()).toHaveLength(5) // 2 destinations + 3 route points
+ })
})
diff --git a/client/src/components/Map/MapView.tsx b/client/src/components/Map/MapView.tsx
index 57d59202c..e473fc4f8 100644
--- a/client/src/components/Map/MapView.tsx
+++ b/client/src/components/Map/MapView.tsx
@@ -212,24 +212,27 @@ function MapController({ center, zoom }: MapControllerProps) {
return null
}
-// Fit bounds when places change (fitKey triggers re-fit)
+// Fit bounds when places change (fitKey triggers re-fit). On a day selection we
+// fit to that day's destinations immediately, then — once the day's route has
+// finished computing asynchronously — re-fit once more to include the full route
+// polyline, so a route that bulges past its stops stays in view (#1128).
interface BoundsControllerProps {
hasDayDetail?: boolean
places: Place[]
+ routeCoords: [number, number][]
fitKey: number
paddingOpts: L.FitBoundsOptions
}
-function BoundsController({ places, fitKey, paddingOpts, hasDayDetail }: BoundsControllerProps) {
+function BoundsController({ places, routeCoords, fitKey, paddingOpts, hasDayDetail }: BoundsControllerProps) {
const map = useMap()
const prevFitKey = useRef(-1)
+ const awaitingRoute = useRef(false)
- useEffect(() => {
- if (fitKey === prevFitKey.current) return
- prevFitKey.current = fitKey
- if (places.length === 0) return
+ const fitTo = useCallback((coords: [number, number][]) => {
+ if (coords.length === 0) return
try {
- const bounds = L.latLngBounds(places.map(p => [p.lat, p.lng]))
+ const bounds = L.latLngBounds(coords)
if (bounds.isValid()) {
map.fitBounds(bounds, { ...paddingOpts, maxZoom: 16, animate: true })
if (hasDayDetail) {
@@ -237,8 +240,27 @@ function BoundsController({ places, fitKey, paddingOpts, hasDayDetail }: BoundsC
}
}
} catch {}
+ }, [map, paddingOpts, hasDayDetail])
+
+ // New fitKey (initial trip fit or a day selection): fit to the destinations now
+ // and arm a one-shot re-fit for when the route arrives.
+ useEffect(() => {
+ if (fitKey === prevFitKey.current) return
+ prevFitKey.current = fitKey
+ awaitingRoute.current = false
+ if (places.length === 0) return
+ fitTo(places.map(p => [p.lat, p.lng] as [number, number]))
+ awaitingRoute.current = true
}, [fitKey]) // eslint-disable-line react-hooks/exhaustive-deps
+ // Once the just-selected day's route is ready, expand the fit to include it.
+ // One-shot per day-fit, so later route-profile toggles don't re-zoom the map.
+ useEffect(() => {
+ if (!awaitingRoute.current || routeCoords.length === 0) return
+ awaitingRoute.current = false
+ fitTo([...places.map(p => [p.lat, p.lng] as [number, number]), ...routeCoords])
+ }, [routeCoords]) // eslint-disable-line react-hooks/exhaustive-deps
+
return null
}
@@ -463,6 +485,9 @@ export const MapView = memo(function MapView({
const thumbRafRef = useRef(null)
const placeIds = useMemo(() => places.map(p => p.id).join(','), [places])
+ // Flattened [lat,lng] points of the selected day's route, so the bounds fit can
+ // include the full polyline once it has been computed.
+ const routeCoords = useMemo<[number, number][]>(() => (route || []).flat() as [number, number][], [route])
useEffect(() => {
if (!places || places.length === 0 || !placesPhotosEnabled) return
const cleanups: (() => void)[] = []
@@ -597,7 +622,7 @@ export const MapView = memo(function MapView({
/>
- 0 ? dayPlaces : places} fitKey={fitKey} paddingOpts={paddingOpts} hasDayDetail={hasDayDetail} />
+ 0 ? dayPlaces : places} routeCoords={dayPlaces.length > 0 ? routeCoords : []} fitKey={fitKey} paddingOpts={paddingOpts} hasDayDetail={hasDayDetail} />