fix(map): stop the day fit from framing everything and then drifting off it

Selecting a day compensated for the day panel twice: the fit reserves the
panel's height in its bottom padding, and then 300ms later the camera was
nudged again with panBy([0, 150]). Both came from the same change, and
together they moved the view 150px further than intended.

Wide days never showed it. A day that runs north to south does: the fit is
height-bound there, so the northernmost stop sits exactly on the top padding
line, and the nudge then pushes it off the canvas. What the user sees is the
map framing the whole day correctly and then sliding upwards, leaving the
first marker outside the view.

The padding alone is right, and it is what MapViewGL has always done with the
same numbers, so this also brings the two renderers back into agreement.

Closes #1982
This commit is contained in:
Maurice
2026-08-20 17:37:26 +02:00
committed by Maurice
parent d0cc4e2f00
commit 7aead0b713
2 changed files with 46 additions and 9 deletions
+29 -2
View File
@@ -916,7 +916,17 @@ describe('MapView live location', () => {
})
describe('MapView bounds fitting', () => {
it('FE-COMP-MAPVIEW-065: a day-detail fit nudges the map down once the animation ran (#1348)', async () => {
/*
* The day fit used to compensate for the day panel twice: once through the
* padding, and again 300ms later with panBy([0, 150]) (#1348). On a route
* running north to south the fit is height-bound, so the northernmost stop
* lands exactly on the top padding line and the nudge then pushed it off the
* canvas — the map framed everything and then drifted upwards (#1982).
*
* So this case is the inverse of what it used to assert: the padding does the
* work, and nothing moves the camera afterwards.
*/
it('FE-COMP-MAPVIEW-065: a day-detail fit reserves the panel in its padding and then leaves the camera alone (#1348, #1982)', async () => {
const places = [
buildMapPlace({ id: 1, lat: 48, lng: 2 }),
buildMapPlace({ id: 2, lat: 48.2, lng: 2.2 }),
@@ -925,7 +935,24 @@ describe('MapView bounds fitting', () => {
rerender(<MapView places={places} dayPlaces={places} fitKey={2} hasDayDetail />)
expect(mapMock.fitBounds).toHaveBeenCalled()
await waitFor(() => expect(mapMock.panBy).toHaveBeenCalledWith([0, 150], { animate: true }), { timeout: 2000 })
const opts = mapMock.fitBounds.mock.calls.at(-1)?.[1] as { paddingBottomRight?: [number, number] }
expect(opts?.paddingBottomRight?.[1]).toBe(280)
// Long enough that the old 300ms nudge would have fired by now.
await new Promise(r => setTimeout(r, 600))
expect(mapMock.panBy).not.toHaveBeenCalled()
})
it('FE-COMP-MAPVIEW-065b: reserves only the small margin when no day panel is open (#1982)', () => {
const places = [
buildMapPlace({ id: 1, lat: 48, lng: 2 }),
buildMapPlace({ id: 2, lat: 48.2, lng: 2.2 }),
]
const { rerender } = render(<MapView places={places} dayPlaces={places} fitKey={1} />)
rerender(<MapView places={places} dayPlaces={places} fitKey={2} />)
const opts = mapMock.fitBounds.mock.calls.at(-1)?.[1] as { paddingBottomRight?: [number, number] }
expect(opts?.paddingBottomRight?.[1]).toBe(60)
})
it('FE-COMP-MAPVIEW-066: a fitKey that never changed is not re-fitted', async () => {
+17 -7
View File
@@ -269,7 +269,6 @@ function MapController({ center, zoom }: MapControllerProps) {
// 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
@@ -278,7 +277,7 @@ interface BoundsControllerProps {
framedOnMount?: boolean
}
function BoundsController({ places, routeCoords, fitKey, paddingOpts, hasDayDetail, framedOnMount = false }: BoundsControllerProps) {
function BoundsController({ places, routeCoords, fitKey, paddingOpts, framedOnMount = false }: BoundsControllerProps) {
const map = useMap()
const prevFitKey = useRef(-1)
const awaitingRoute = useRef(false)
@@ -289,13 +288,24 @@ function BoundsController({ places, routeCoords, fitKey, paddingOpts, hasDayDeta
try {
const bounds = L.latLngBounds(coords)
if (bounds.isValid()) {
/*
* The padding already reserves the day panel's height at the bottom
* (paddingBox), so the fit puts the day's stops above it. There used to
* be a second, manual nudge 300ms later — panBy([0, 150]) — from the
* same change that introduced the padding, and the two compensated for
* the same panel twice.
*
* On a route that runs north to south the fit is height-bound, which
* puts the northernmost stop exactly on the top padding line; the nudge
* then pushed it off the canvas. The map appeared to frame everything
* correctly and then drift upwards, which is what the report describes
* (#1982). MapViewGL never had the nudge, so this also brings the two
* renderers back into agreement.
*/
map.fitBounds(bounds, { ...paddingOpts, maxZoom: 16, animate: true })
if (hasDayDetail) {
setTimeout(() => map.panBy([0, 150], { animate: true }), 300)
}
}
} catch {}
}, [map, paddingOpts, hasDayDetail])
}, [map, paddingOpts])
// 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.
@@ -827,7 +837,7 @@ export const MapView = memo(function MapView({
/>
<MapController center={center} zoom={zoom} />
<BoundsController places={dayPlaces.length > 0 ? dayPlaces : places} routeCoords={dayPlaces.length > 0 ? routeCoords : []} fitKey={fitKey} paddingOpts={paddingOpts} hasDayDetail={hasDayDetail} framedOnMount={initialView.framed} />
<BoundsController places={dayPlaces.length > 0 ? dayPlaces : places} routeCoords={dayPlaces.length > 0 ? routeCoords : []} fitKey={fitKey} paddingOpts={paddingOpts} framedOnMount={initialView.framed} />
<SelectionController places={places} selectedPlaceId={selectedPlaceId} dayPlaces={dayPlaces} paddingOpts={paddingOpts} />
<MapClickHandler onClick={onMapClick} />
<MapContextMenuHandler onContextMenu={onMapContextMenu} />