Skip to content

Commit

Permalink
feat: add hover events and anchor points to advanced markers (#472)
Browse files Browse the repository at this point in the history
This is a significant change to how the AdvancedMarker component works internally and adds two major new features to advanced markers:

 - `Event Handling`: in addition to the click and drag events, the Advanced marker now handles mouseenter and mouseleave events correctly, independent of being used with a Pin or with custom html content

- `Anchoring`: the AdvancedMarker component now supports configurable anchor-points, to make it easier to create different kinds of markers. This also new properly works together with the infowindows.

This change required us to change the DOM structure created by the AdvancedMarker by add an additional div around the content. If you’ve been using very specific selectors to style the content of an AdvancedMarker, this might require you to update them accordingly.
  • Loading branch information
mrMetalWood authored Sep 13, 2024
1 parent 9ad358a commit cc4a397
Show file tree
Hide file tree
Showing 17 changed files with 624 additions and 32 deletions.
39 changes: 30 additions & 9 deletions docs/api-reference/components/advanced-marker.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,13 +56,13 @@ element.

When custom html is specified, the marker will be positioned such that the
`position` on the map is at the bottom center of the content-element.
If you need it positioned differently, you can use css-transforms on
the content element. For example, to have the anchor point in the top-left
corner of the marker (the transform can also be applied via a css class and
specified as `className`):
If you need it positioned differently, you can use the [`anchorPoint`](#anchorpoint-advancedmarkeranchorpoint--string-string) property of the `AdvancedMarker`. For example, to have the anchor point in the top-left
corner of the marker:

```tsx
<AdvancedMarker position={...} style={{transform: 'translate(50%, 100%)'}}>
import {AdvancedMarker, AdvancedMarkerAnchorPoint} from '@vis.gl/react-google-maps';

<AdvancedMarker position={...} anchorPoint={AdvancedMarkerAnchorPoint.TOP_LEFT}>
...
</AdvancedMarker>
```
Expand All @@ -85,9 +85,9 @@ shown on the map.

#### `className`: string

A className to be added to the markers content-element. The content-element is
either an element that contains the custom HTML content or the DOM
representation of the `google.maps.marker.PinElement` when a Pin or an
A className to be added to the markers content-element. The content-element is
either an element that contains the custom HTML content or the DOM
representation of the `google.maps.marker.PinElement` when a Pin or an
empty AdvancedMarker component is rendered.

#### `style`: [CSSProperties][react-dev-styling]
Expand All @@ -107,7 +107,7 @@ provided value.
#### `position`: [google.maps.LatLngLiteral][gmp-ll] | [google.maps.LatLngAltitudeLiteral][gmp-lla]

The position of the marker. For maps with tilt enabled, an `AdvancedMarker`
can also be placed at an altitude using the `{lat: number, lng: number,
can also be placed at an altitude using the `{lat: number, lng: number,
altitude: number}` format.

#### `zIndex`: number
Expand Down Expand Up @@ -161,6 +161,18 @@ import {AdvancedMarker, CollisionBehavior} from '@vis.gl/react-google-maps';
See the documentation on [Marker Collision Management][gmp-collisions]
for more information.

#### `anchorPoint`: AdvancedMarkerAnchorPoint | [string, string]

Defines the point on the marker which should align with the geo position of the marker.
The default anchor point is `BOTTOM_CENTER`. That means for a standard map marker, the bottom of the pin is on the exact geo location of the marker

Either use one of the predefined anchor points from the `AdvancedMarkerAnchorPoint` export
or provide a string tuple in the form of `["xPosition", "yPosition"]`.

The position is measured from the top-left corner and
can be anything that can be consumed by a CSS translate() function.
For example in percent `[10%, 90%]` or in pixels `[10px, 20px]`.

### Other Props

#### `clickable`: boolean
Expand Down Expand Up @@ -192,6 +204,14 @@ specified in the position can't be dragged.

This event is fired when the marker is clicked.

#### `onMouseEnter`: (e: [google.maps.MapMouseEvent['domEvent']][gmp-map-mouse-ev-dom]) => void

This event is fired when the mouse enters the marker.

#### `onMouseLeave`: (e: [google.maps.MapMouseEvent['domEvent']][gmp-map-mouse-ev-dom]) => void

This event is fired when the mouse leaves the marker.

#### `onDragStart`: (e: [google.maps.MapMouseEvent][gmp-map-mouse-ev]) => void

This event is fired when the user starts dragging the marker.
Expand Down Expand Up @@ -246,6 +266,7 @@ const MarkerWithInfoWindow = props => {
[gmp-collisions]: https://developers.google.com/maps/documentation/javascript/examples/marker-collision-management
[gmp-adv-marker-click-ev]: https://developers.google.com/maps/documentation/javascript/reference/advanced-markers#AdvancedMarkerClickEvent
[gmp-map-mouse-ev]: https://developers.google.com/maps/documentation/javascript/reference/map#MapMouseEvent
[gmp-map-mouse-ev-dom]: https://developers.google.com/maps/documentation/javascript/reference/map#MapMouseEvent.domEvent
[adv-marker-src]: https://github.com/visgl/react-google-maps/tree/main/src/components/advanced-marker.tsx
[react-portal]: https://react.dev/reference/react-dom/createPortal
[react-dev-styling]: https://react.dev/reference/react-dom/components/common#applying-css-styles
36 changes: 36 additions & 0 deletions examples/advanced-marker-interaction/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# Advanced Marker interaction example

This example showcases a classic interaction pattern when dealing with map markers.
It covers hover-, click- and z-index handling as well as modifying the anchor point for an `AdvancedMarker`.

## Google Maps Platform API Key

This example does not come with an API key. Running the examples locally requires a valid API key for the Google Maps Platform.
See [the official documentation][get-api-key] on how to create and configure your own key.

The API key has to be provided via an environment variable `GOOGLE_MAPS_API_KEY`. This can be done by creating a
file named `.env` in the example directory with the following content:

```shell title=".env"
GOOGLE_MAPS_API_KEY="<YOUR API KEY HERE>"
```

If you are on the CodeSandbox playground you can also choose to [provide the API key like this](https://codesandbox.io/docs/learn/environment/secrets)

## Development

Go into the example-directory and run

```shell
npm install
```

To start the example with the local library run

```shell
npm run start-local
```

The regular `npm start` task is only used for the standalone versions of the example (CodeSandbox for example)

[get-api-key]: https://developers.google.com/maps/documentation/javascript/get-api-key
31 changes: 31 additions & 0 deletions examples/advanced-marker-interaction/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1.0, user-scalable=no" />
<title>Advanced Marker interaction</title>
<meta name="description" content="Advanced Marker interaction" />
<style>
body {
margin: 0;
font-family: sans-serif;
}
#app {
width: 100vw;
height: 100vh;
}
</style>
</head>
<body>
<div id="app"></div>
<script type="module">
import '@vis.gl/react-google-maps/examples.css';
import '@vis.gl/react-google-maps/examples.js';
import {renderToDom} from './src/app';

renderToDom(document.querySelector('#app'));
</script>
</body>
</html>
14 changes: 14 additions & 0 deletions examples/advanced-marker-interaction/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"type": "module",
"dependencies": {
"@vis.gl/react-google-maps": "latest",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"vite": "^5.0.4"
},
"scripts": {
"start": "vite",
"start-local": "vite --config ../vite.config.local.js",
"build": "vite build"
}
}
211 changes: 211 additions & 0 deletions examples/advanced-marker-interaction/src/app.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
import React, {useCallback, useState} from 'react';
import {createRoot} from 'react-dom/client';

import {
AdvancedMarker,
AdvancedMarkerAnchorPoint,
AdvancedMarkerProps,
APIProvider,
InfoWindow,
Map,
Pin,
useAdvancedMarkerRef
} from '@vis.gl/react-google-maps';

import {getData} from './data';

import ControlPanel from './control-panel';

import './style.css';

export type AnchorPointName = keyof typeof AdvancedMarkerAnchorPoint;

// A common pattern for applying z-indexes is to sort the markers
// by latitude and apply a default z-index according to the index position
// This usually is the most pleasing visually. Markers that are more "south"
// thus appear in front.
const data = getData()
.sort((a, b) => b.position.lat - a.position.lat)
.map((dataItem, index) => ({...dataItem, zIndex: index}));

const Z_INDEX_SELECTED = data.length;
const Z_INDEX_HOVER = data.length + 1;

const API_KEY =
globalThis.GOOGLE_MAPS_API_KEY ?? (process.env.GOOGLE_MAPS_API_KEY as string);

const App = () => {
const [markers] = useState(data);

const [hoverId, setHoverId] = useState<string | null>(null);
const [selectedId, setSelectedId] = useState<string | null>(null);

const [anchorPoint, setAnchorPoint] = useState('BOTTOM' as AnchorPointName);
const [selectedMarker, setSelectedMarker] =
useState<google.maps.marker.AdvancedMarkerElement | null>(null);
const [infoWindowShown, setInfoWindowShown] = useState(false);

const onMouseEnter = useCallback((id: string | null) => setHoverId(id), []);
const onMouseLeave = useCallback(() => setHoverId(null), []);
const onMarkerClick = useCallback(
(id: string | null, marker?: google.maps.marker.AdvancedMarkerElement) => {
setSelectedId(id);

if (marker) {
setSelectedMarker(marker);
}

if (id !== selectedId) {
setInfoWindowShown(true);
} else {
setInfoWindowShown(isShown => !isShown);
}
},
[selectedId]
);

const onMapClick = useCallback(() => {
setSelectedId(null);
setSelectedMarker(null);
setInfoWindowShown(false);
}, []);

const handleInfowindowCloseClick = useCallback(
() => setInfoWindowShown(false),
[]
);

return (
<APIProvider apiKey={API_KEY} libraries={['marker']}>
<Map
mapId={'bf51a910020fa25a'}
defaultZoom={12}
defaultCenter={{lat: 53.55909057947169, lng: 10.005767668054645}}
gestureHandling={'greedy'}
onClick={onMapClick}
clickableIcons={false}
disableDefaultUI>
{markers.map(({id, zIndex: zIndexDefault, position, type}) => {
let zIndex = zIndexDefault;

if (hoverId === id) {
zIndex = Z_INDEX_HOVER;
}

if (selectedId === id) {
zIndex = Z_INDEX_SELECTED;
}

if (type === 'pin') {
return (
<AdvancedMarkerWithRef
onMarkerClick={(
marker: google.maps.marker.AdvancedMarkerElement
) => onMarkerClick(id, marker)}
onMouseEnter={() => onMouseEnter(id)}
onMouseLeave={onMouseLeave}
key={id}
zIndex={zIndex}
className="custom-marker"
style={{
transform: `scale(${[hoverId, selectedId].includes(id) ? 1.4 : 1})`
}}
position={position}>
<Pin
background={selectedId === id ? '#22ccff' : null}
borderColor={selectedId === id ? '#1e89a1' : null}
glyphColor={selectedId === id ? '#0f677a' : null}
/>
</AdvancedMarkerWithRef>
);
}

if (type === 'html') {
return (
<React.Fragment key={id}>
<AdvancedMarkerWithRef
position={position}
zIndex={zIndex}
anchorPoint={AdvancedMarkerAnchorPoint[anchorPoint]}
className="custom-marker"
style={{
transform: `scale(${[hoverId, selectedId].includes(id) ? 1.4 : 1})`
}}
onMarkerClick={(
marker: google.maps.marker.AdvancedMarkerElement
) => onMarkerClick(id, marker)}
onMouseEnter={() => onMouseEnter(id)}
onMouseLeave={onMouseLeave}>
<div
className={`custom-html-content ${selectedId === id ? 'selected' : ''}`}></div>
</AdvancedMarkerWithRef>

{/* anchor point visualization marker */}
<AdvancedMarkerWithRef
onMarkerClick={(
marker: google.maps.marker.AdvancedMarkerElement
) => onMarkerClick(id, marker)}
zIndex={zIndex}
onMouseEnter={() => onMouseEnter(id)}
onMouseLeave={onMouseLeave}
anchorPoint={AdvancedMarkerAnchorPoint.CENTER}
position={position}>
<div className="visualization-marker"></div>
</AdvancedMarkerWithRef>
</React.Fragment>
);
}
})}

{infoWindowShown && selectedMarker && (
<InfoWindow
anchor={selectedMarker}
onCloseClick={handleInfowindowCloseClick}>
<h2>Marker {selectedId}</h2>
<p>Some arbitrary html to be rendered into the InfoWindow.</p>
</InfoWindow>
)}
</Map>
<ControlPanel
anchorPointName={anchorPoint}
onAnchorPointChange={(newAnchorPoint: AnchorPointName) =>
setAnchorPoint(newAnchorPoint)
}
/>
</APIProvider>
);
};

export const AdvancedMarkerWithRef = (
props: AdvancedMarkerProps & {
onMarkerClick: (marker: google.maps.marker.AdvancedMarkerElement) => void;
}
) => {
const {children, onMarkerClick, ...advancedMarkerProps} = props;
const [markerRef, marker] = useAdvancedMarkerRef();

return (
<AdvancedMarker
onClick={() => {
if (marker) {
onMarkerClick(marker);
}
}}
ref={markerRef}
{...advancedMarkerProps}>
{children}
</AdvancedMarker>
);
};

export default App;

export function renderToDom(container: HTMLElement) {
const root = createRoot(container);

root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);
}
Loading

0 comments on commit cc4a397

Please sign in to comment.