javascriptreactjstypescriptgoogle-mapsreact-google-maps-api

@react-google-map/api: auto adjust marker popup pos if near map container edge


I try to find a way to adjust markers popup window position based on following condition: if popup is near map containers edge.

Reason: right now popup is being hidden under container if marker (and popup, ofc) is near edge.

Working example: I am not sure do I have rights to link you to a different website except sandboxes, but Zillow popups does everything I've described - popup adjust position a bit to the left if initial position is going to be under the right edge of container (accept this for each edge of container)

Simple approaches: tried to add zIndex to popup component to place popup on top of container, but it doesn't work 'cause of GoogleMap's canvas

My Markers component clusterer render function:

  const renderClusterer = (clusterer, locations): any => {
    return (
      <OverlayViewF
        mapPaneName={'overlayMouseTarget'}
        position={{ lat: 0, lng: 0 }}
      >
        {locations?.map((location: IEstate, index) => (
          <OverlayViewF
            zIndex={999999}
            key={location.id}
            mapPaneName={'overlayMouseTarget'}
            position={{ lat: location.latitude, lng: location.longitude }}
          >
            <MarkerF
              position={{ lat: location.latitude, lng: location.longitude }}
              icon={{
                url: getMarkerIcon(location.isViewed, location.isFavorite),
                scaledSize: new window.google.maps.Size(17, 17),
              }}
              onClick={(e) => navigate(`/estate/${location.id}`)}
              onMouseOver={(e) => {
                setTooltip(location)
                globalStore.setHoveredEstateId(location.id)
              }}
              onMouseOut={(e) => {
                setTooltip(null)
                globalStore.setHoveredEstateId(null)
              }}
              clusterer={clusterer}
            >
            </MarkerF>
          </OverlayViewF>
        ))}
        {tooltip && tooltip.id && <StyledPopup marker={tooltip} />}
      </OverlayViewF>
    )
  }

Actual implementation in the component jsx body:

return (
  locations?.length > 0 && (
      <MarkerClustererF
        gridSize={11}
        minimumClusterSize={10}
        options={{
          enableRetinaIcons: true,
        }}
      >
        {(clusterer) => renderClusterer(clusterer, locations)}
      </MarkerClustererF>
    )
)

StyledPopup component jsx body:

<OverlayViewF
  mapPaneName={'overlayMouseTarget'}
  position={{ lat: marker.latitude, lng: marker.longitude }}
>
  //...regular jsx here, does nothing with @react-google-maps/api
</OverlayViewF>

UPD:

Tried to use following function to calculate adjustments:

  const adjustPopupPosition = useCallback(
    (position) => {
      const mapContainer = mapRef.current
      if (!mapContainer) return position
      console.log(!!mapContainer, mapContainer)

      const mapContainerRect = mapContainer.getBoundingClientRect()
      const popupWidth = 400

      const maxRightPosition = mapContainerRect.width - popupWidth
      const maxBottomPosition = mapContainerRect.height

      const adjustedPosition = {
        x: Math.min(position.x, maxRightPosition),
        y: Math.min(position.y, maxBottomPosition),
      }

      return adjustedPosition
    },
    [mapRef],
  )

And then used it in my StyledPopup like this:

  <OverlayViewF
    mapPaneName={'overlayMouseTarget'}
    position={{ lat: adjustedPosition.x, lng: adjustedPosition.y }}
  >
    //rest of regular jsx
  </OverlayViewF>

But still no results. Popup width is 400px.


Solution

  • In my StyledPopup.tsx:

    IPositionMarkerState:

    interface IPositionMarkerState {
      lng: number
      lat: number
    }
    

    FC:

    function adjustPosition(
        location: IPositionMarkerState,
        map: google.maps.Map,
      ): { x: number; y: number } {
        const overlay = new google.maps.OverlayView()
        // eslint-disable-next-line @typescript-eslint/no-empty-function
        overlay.draw = function () {} // empty function required
        overlay.setMap(map)
    
        let offsetX = 0
        let offsetY = 0
    
        const projection = overlay.getProjection()
        if (!projection) return { x: offsetX, y: offsetY } // make sure projection exists
    
        // Convert the LatLng location to pixel coordinates
        const point = projection.fromLatLngToContainerPixel(
          new google.maps.LatLng(location.lat, location.lng),
        )
    
        // Check if the location is within 350 pixels of the right edge
        if (point!.x + 350 > map.getDiv().offsetWidth) {
          offsetX = -350
        }
    
        // Check if the location is within 120 pixels of the bottom edge
        if (point!.y + 120 > map.getDiv().offsetHeight) {
          offsetY = -140
        }
    
        return { x: offsetX, y: offsetY } // Default no shift
      }
    
      const handlePosition = () => {
        if (!mapStore.mapRef.current) return { x: 0, y: 0 }
        return adjustPosition(
          { lat: marker.latitude, lng: marker.longitude },
          mapStore.mapRef.current,
        )
      }
    

    JSX:

      return (
        <OverlayViewF
          mapPaneName={'floatPane'}
          position={{ lat: marker.latitude, lng: marker.longitude }}
          getPixelPositionOffset={() => handlePosition()}
        >
        //rest of the code
        </OverlayViewF>
      )
    

    Basically I'm providing mapRef.current to adjustPosition(...) and returning the { x: number; y: number } that applies inside of OverlayViewF getPixelPositionOffset prop

    Should be tweaked based on the popup width and height (offsetX -350 turns popup to the right, my popup width is 300, and offsetY -140 turns popup up, popup height is 120)