fix(journey): keep GL photo markers absolutely positioned so thumbnails sit on their coordinates

The journey map's photo thumbnails set position:relative inline on the marker
element. That overrides the engine's .mapboxgl-marker/.maplibregl-marker
position:absolute, so every thumbnail entered the normal flow below the ones
drawn before it and the engine's translate was added on top: the further down
the list, the further the photo drifted from its spot. The location dot had the
same inline position. Both now leave position to the engine's class.

Closes #2910
This commit is contained in:
Maurice
2026-10-30 21:15:37 +01:00
parent 5e00282309
commit 8eebcea9f5
4 changed files with 19 additions and 3 deletions
@@ -1,4 +1,4 @@
// FE-COMP-JMAPGL-001 to FE-COMP-JMAPGL-048, FE-COMP-JMAPGL-2453
// FE-COMP-JMAPGL-001 to FE-COMP-JMAPGL-048, FE-COMP-JMAPGL-2453, FE-COMP-JMAPGL-2910
type Spy = ReturnType<typeof vi.fn>
@@ -644,6 +644,15 @@ describe('JourneyMapGL', () => {
expect(gl.map.on).toHaveBeenCalledWith('moveend', expect.any(Function))
})
it('FE-COMP-JMAPGL-2910: no marker element sets its own position, so the engine keeps it absolute', () => {
withToken()
const photos = [{ id: 'a', lat: 45, lng: 5, thumbUrl: '/api/photos/1/thumbnail' }]
const marks = [{ kind: 'stay', key: 's1', lat: 46, lng: 6, color: '#14b8a6', stay: {} }]
render(<JourneyMapGL checkins={[]} entries={entries} photos={photos} marks={marks} />)
expect(gl.markers.some(m => m.element.innerHTML.includes('/api/photos/'))).toBe(true)
for (const marker of gl.markers) expect(marker.element.style.position).toBe('')
})
it('FE-COMP-JMAPGL-2889: panning keeps the photo and mark groups, only a zoom regroups them', () => {
withToken()
const photos = [
@@ -557,7 +557,10 @@ function JourneyMapGL(
if (!photos?.length) return
for (const cluster of clusterPhotos(photos, glWorldPx(map))) {
const el = document.createElement('div')
el.style.cssText = 'position:relative;width:48px;height:48px;cursor:pointer'
// No inline `position` (#2910): the engine's marker class positions the element
// absolutely, and `position:relative` would drop it into the normal flow, where
// every thumbnail sits below the ones drawn before it.
el.style.cssText = 'width:48px;height:48px;cursor:pointer'
el.innerHTML = photoMarkerHtml(cluster.members[0].thumbUrl, cluster.members.length)
el.addEventListener('click', (ev) => {
ev.stopPropagation()
@@ -97,6 +97,8 @@ describe('attachLocationMarker', () => {
expect(root.children).toHaveLength(3)
// The cone starts hidden — there is no heading until the first fix.
expect((root.children[1] as HTMLElement).style.display).toBe('none')
// The GL marker class owns `position`; an inline one would drop it into the flow.
expect(root.style.position).toBe('')
})
it('FE-COMP-LOCMARKERGL-003: adds the accuracy source and fill layer when the map is already loaded', () => {
@@ -13,7 +13,9 @@ type MarkerConstructor = new (options?: { element?: HTMLElement; anchor?: string
// and mapbox doesn't get confused about which element to position.
function buildLocationEl(): { root: HTMLDivElement; cone: HTMLDivElement } {
const root = document.createElement('div')
root.style.cssText = 'width:28px;height:28px;position:relative;pointer-events:none;'
// No inline `position`: the GL marker class keeps the root absolute, which the
// children's insets resolve against (see the note on markers in MapViewGL).
root.style.cssText = 'width:28px;height:28px;pointer-events:none;'
// Accuracy pulse behind the dot
const pulse = document.createElement('div')
pulse.style.cssText = `