Search code examples
javascripthtml5-canvasmapboxmapbox-gl-js

Mapbox add popup on hover (layer), close on mouseleave, but keep open on popup hover


I implemented the example just fine:

https://docs.mapbox.com/mapbox-gl-js/example/popup-on-hover/

but I'm struggling to keep the popup open, when hovering the popup (which means leaving the symbol layer) BUT closing the popup when leaving it.

I tried several combinations of event handlers (even a mousemove handler), but since the layer is drawn on canvas, but popup is an domNode I didnt find a solution.

Anyone know where is is implemented, or how to do it?

My Goal is similar behaviour like googla maps on web:

enter image description here

Sample Code from Docs:

// Add a layer showing the places.
map.addLayer({
'id': 'places',
'type': 'circle',
'source': 'places',
'paint': {
'circle-color': '#4264fb',
'circle-radius': 6,
'circle-stroke-width': 2,
'circle-stroke-color': '#ffffff'
}
});
 
// Create a popup, but don't add it to the map yet.
const popup = new mapboxgl.Popup({
closeButton: false,
closeOnClick: false
});
 
map.on('mouseenter', 'places', (e) => {
// Change the cursor style as a UI indicator.
map.getCanvas().style.cursor = 'pointer';
 
// Copy coordinates array.
const coordinates = e.features[0].geometry.coordinates.slice();
const description = e.features[0].properties.description;
 
// Ensure that if the map is zoomed out such that multiple
// copies of the feature are visible, the popup appears
// over the copy being pointed to.
while (Math.abs(e.lngLat.lng - coordinates[0]) > 180) {
coordinates[0] += e.lngLat.lng > coordinates[0] ? 360 : -360;
}
 
// Populate the popup and set its coordinates
// based on the feature found.
popup.setLngLat(coordinates).setHTML(description).addTo(map);
});
 
map.on('mouseleave', 'places', () => {
map.getCanvas().style.cursor = '';
popup.remove();
});

Solution

  • Recently I have the same problem.

    My workaround is to have a function that will close the popup when the userleaves the icon layer or the popup.

    If before the timeout is fullfilled the user enters in the popup content, clear the timeout function. (The timeout function will also be called when the user leave popup content).

    // TEST modal variables
    // popupTEST is the popup where the content will be insert
    popupTEST = new mapboxgl.Popup({
        closeButton: false,
        closeOnClick: false,
        className: "popupTEST"
    });
    // The close of the modal will be via timeout in order to stop removing in when hover the popUp content
    popupTESTCloseTimeout = null;
    
    ....
    
    this.map.on("mouseenter", "test", (e) => {
        this.map.getCanvas().style.cursor = "pointer";
        // Clear close timeout and delete posible popup
        this.clearPopupTESTCloseTimeout();
        this.popupTEST.remove();
        if (e && e.features && e.features.length > 0 && e.features[0].properties) {
          const popupcontent = document.createElement("div");
          // Create a content using the properties ...
          // Is a react component, but you can use plain javascript
          const Contents = () => (
            <TestModalContent
              properties={properties}
              onMouseEnter={this.clearPopupOACCloseTimeout}
              onMouseLeave={this.closePopupOACWithTimeout} 
            />
          );
          ReactDOM.render(Contents(), popupcontent);
          this.popupTEST.setLngLat([long, lat]).setMaxWidth("none").setDOMContent(popupcontent).addTo(_this.map);
        }        
    });
    
    this.map.on("mouseleave", "sit-TEST", () => {
        this.map.getCanvas().style.cursor = "";
        this.closePopupTESTWithTimeout();      
    });
    
    closePopupTESTWithTimeout = () => {
        this.popupTESTCloseTimeout = setTimeout(() => this.popupTEST.remove(), 500);
    }
    
    clearPopupTESTCloseTimeout = () => {
        if (this.popupTESTCloseTimeout) {
          clearTimeout(this.popupTESTCloseTimeout);
          this.popupTESTCloseTimeout = null;
        }
    }