From 37ef3a9d185d9d017267a3ce94d0fc525c7f757c Mon Sep 17 00:00:00 2001 From: Maurice Date: Tue, 30 Jun 2026 19:26:41 +0200 Subject: [PATCH] fix(map): match the GL place hover tooltip to the Leaflet map (#1385) The MapLibre/Mapbox hover showed an anchored popup with a large photo thumbnail, completely unlike the Leaflet map's slim, cursor-following name/category/address card. Drop the anchored photo popup for places and render the same cursor-following overlay the Leaflet map uses (no photo, matching fonts/padding/shadow), so the two maps hover identically. --- client/src/components/Map/MapViewGL.tsx | 62 ++++++++++++++++++++++--- 1 file changed, 56 insertions(+), 6 deletions(-) diff --git a/client/src/components/Map/MapViewGL.tsx b/client/src/components/Map/MapViewGL.tsx index c33e3c719..9eaf41a92 100644 --- a/client/src/components/Map/MapViewGL.tsx +++ b/client/src/components/Map/MapViewGL.tsx @@ -16,7 +16,7 @@ import LocationButton from './LocationButton' import { useGeolocation } from '../../hooks/useGeolocation' import type { Place, Reservation } from '../../types' import { POI_CATEGORY_BY_KEY, type Poi } from './poiCategories' -import { buildPlacePopupHtml, buildPoiPopupHtml } from './placePopup' +import { buildPoiPopupHtml } from './placePopup' function categoryIconSvg(iconName: string | null | undefined, size: number): string { const IconComponent = (iconName && CATEGORY_ICON_MAP[iconName]) || CATEGORY_ICON_MAP['MapPin'] @@ -219,6 +219,11 @@ export function MapViewGL({ const placesPhotosEnabled = useAuthStore(s => s.placesPhotosEnabled) const [photoUrls, setPhotoUrls] = useState>(getAllThumbs) const [mapReady, setMapReady] = useState(false) + // Hover tooltip — a cursor-following name/category/address card, matching the + // Leaflet map's overlay exactly (no anchored popup, no photo thumbnail). + const [hoverPlace, setHoverPlace] = useState<(Place & { category_color?: string | null; category_icon?: string | null; category_name?: string | null }) | null>(null) + const [hoverPos, setHoverPos] = useState<{ x: number; y: number } | null>(null) + const hoverIdRef = useRef(null) const containerRef = useRef(null) // eslint-disable-next-line @typescript-eslint/no-explicit-any const mapRef = useRef(null) @@ -615,6 +620,9 @@ export function MapViewGL({ if (!ids.has(id)) { marker.remove() markersRef.current.delete(id) + // Removing a marker under the cursor (e.g. it just got clustered) never + // fires mouseleave, so drop its tooltip here to avoid orphaning it. + if (hoverIdRef.current === id) { hoverIdRef.current = null; setHoverPlace(null); setHoverPos(null) } } }) @@ -628,12 +636,19 @@ export function MapViewGL({ ev.stopPropagation() onClickRefs.current.marker?.(place.id) }) - el.addEventListener('mouseenter', () => { - popupRef.current?.setLngLat([place.lng, place.lat]) - .setHTML(buildPlacePopupHtml(place as Place & { category_color?: string; category_icon?: string; category_name?: string }, photoUrl)) - .addTo(map) + el.addEventListener('mouseenter', (ev) => { + hoverIdRef.current = place.id + setHoverPlace(place as Place & { category_color?: string; category_icon?: string; category_name?: string }) + setHoverPos({ x: (ev as MouseEvent).clientX, y: (ev as MouseEvent).clientY }) + }) + el.addEventListener('mousemove', (ev) => { + setHoverPos({ x: (ev as MouseEvent).clientX, y: (ev as MouseEvent).clientY }) + }) + el.addEventListener('mouseleave', () => { + hoverIdRef.current = null + setHoverPlace(null) + setHoverPos(null) }) - el.addEventListener('mouseleave', () => { popupRef.current?.remove() }) // Recreate marker each time rather than patching internal state — // mapbox-gl's internal _element bookkeeping breaks under DOM swaps. const existing = markersRef.current.get(place.id) @@ -904,6 +919,8 @@ export function MapViewGL({ ? 'calc(var(--bottom-nav-h, 84px) + 20px + var(--day-panel-h, 0px) + 12px)' : 'calc(var(--bottom-nav-h, 84px) + 12px)' + const HoverIcon = (hoverPlace?.category_icon && CATEGORY_ICON_MAP[hoverPlace.category_icon]) || CATEGORY_ICON_MAP['MapPin'] + return (
@@ -915,6 +932,39 @@ export function MapViewGL({ bottomOffset={buttonBottom as unknown as number} /> )} + {/* Hover tooltip — cursor-following name/category/address card, identical to + the Leaflet map's overlay (no anchored popup, no photo). */} + {hoverPlace && hoverPos && !isMobile && ( +
+
+ {hoverPlace.name} +
+ {hoverPlace.category_name && ( +
+ + {hoverPlace.category_name} +
+ )} + {hoverPlace.address && ( +
+ {hoverPlace.address} +
+ )} +
+ )}
) }