fix(roadtrip): a freshly placed via lost its first drag

Placing one triggers a re-route, the re-route lands about a second later, and
the render it causes rebuilt every via handle — which is exactly when somebody
reaches for the dot they just made. On the GL renderer that rebuild replaces
the DOM element outright, and a pointerdown belongs to the element it happened
on, so the drag went to an element that no longer existed. The second attempt
worked because by then nothing was pending, and an older via never showed it
because nothing was re-routing.

Two causes, both identity: the vias arrive as a fresh object on every reload
even when the rows came back identical, and `onMoveVia` is a fresh closure on
every render of the planner. Both were in the effect's dependencies.

The list is now keyed by what a handle is built from — id and position — so it
only changes when a via does, and the callbacks are read through a ref. A via
that genuinely moved still gets a new list, which is what redraws it where it
now is; the test pins both halves, because holding the element still must not
mean holding it in the wrong place.

Leaflet's markers had the same churn without the same symptom: a fresh
`position` array called `setLatLng` on every marker on every render, and a
fresh `eventHandlers` object unbound and rebound every listener. Same fix.
This commit is contained in:
Maurice
2026-09-05 01:10:33 +02:00
committed by Maurice
parent 676c82a308
commit c1f7587704
5 changed files with 226 additions and 24 deletions
+22 -5
View File
@@ -1,6 +1,7 @@
import { useEffect, useRef, useMemo, useState, createElement, useCallback } from 'react'
import { makeMarkerDraggable, makePoiDraggable, draggedPoiId } from './markerDrag'
import type { RoadtripVia } from '@trek/shared'
import { useStableVias } from './viaMarkerState'
import { ALT_CASING, ALT_LABEL_TEXT } from '../Roadtrip/alternativeColors'
import type { AlternativeOverlay } from '../Roadtrip/alternativeOverlays'
import { serviceMarkerHtml, serviceMarkerOuter } from '../Roadtrip/serviceMarker'
@@ -678,6 +679,19 @@ export function MapViewGL({
* drag is therefore hand-rolled — pointer events on the element, unproject on move.
*/
const viaCleanupRef = useRef<(() => void)[]>([])
/**
* The list only changes when a via does, and the callbacks are read through a ref.
*
* Both matter for the same reason: this effect destroys every handle and builds new DOM
* elements. It used to re-run on every render of the planner — the vias arrived as a
* fresh object each time and `onMoveVia` as a fresh closure — so the render that landed
* when a freshly placed via finished re-routing pulled the element out from under the
* pointer, and the first drag went nowhere.
*/
const vias = useStableVias(roadtripVias)
const viaHandlersRef = useRef({ onMoveVia, onRemoveVia })
viaHandlersRef.current = { onMoveVia, onRemoveVia }
const viasDraggable = !!onMoveVia
useEffect(() => {
const map = mapRef.current
if (!map || !mapReady) return
@@ -685,12 +699,12 @@ export function MapViewGL({
viaCleanupRef.current = []
viaPinsRef.current.forEach(p => p.remove())
viaPinsRef.current = []
for (const via of Object.values(roadtripVias ?? {}).flat()) {
for (const via of vias) {
const el = document.createElement('span')
el.style.cssText = 'display:block;width:12px;height:12px;border-radius:9999px;background:#0a84ff;border:2.5px solid #fff;box-shadow:0 1px 4px rgba(0,0,0,.45);cursor:grab;touch-action:none'
const pin = attachPin(map, gl, pinLayerRef.current, el, via.lng, via.lat)
viaPinsRef.current.push(pin)
if (!onMoveVia) continue
if (!viasDraggable) continue
let dragging = false
// The map starts panning on mousedown/touchstart anywhere on its surface, and a pin
@@ -716,9 +730,12 @@ export function MapViewGL({
el.style.cursor = 'grab'
const rect = map.getContainer().getBoundingClientRect()
const at = map.unproject([e.clientX - rect.left, e.clientY - rect.top])
onMoveVia(via.day_id, via.id, at.lat, at.lng)
viaHandlersRef.current.onMoveVia?.(via.day_id, via.id, at.lat, at.lng)
}
const onContext = (e: MouseEvent) => {
e.preventDefault()
viaHandlersRef.current.onRemoveVia?.(via.day_id, via.id)
}
const onContext = (e: MouseEvent) => { e.preventDefault(); onRemoveVia?.(via.day_id, via.id) }
el.addEventListener('pointerdown', onDown)
el.addEventListener('pointermove', onMove)
el.addEventListener('pointerup', onUp)
@@ -738,7 +755,7 @@ export function MapViewGL({
el.removeEventListener('dblclick', swallow)
})
}
}, [roadtripVias, mapReady, glProvider, onMoveVia, onRemoveVia])
}, [vias, viasDraggable, mapReady, glProvider])
/**
* The offered routes, drawn under the current one.