Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add hover events and anchor points to advanced markers #472

Merged
merged 12 commits into from
Sep 13, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 is example showcases a classic interaction pattern when dealing with map markers.
It covers hover-, click- and z-index handling as well as setting the correct anchor point for a marker.

usefulthink marked this conversation as resolved.
Show resolved Hide resolved
## 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"
}
}
187 changes: 187 additions & 0 deletions examples/advanced-marker-interaction/src/app.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
import React, {useCallback, useState} from 'react';
import {createRoot} from 'react-dom/client';

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

import ControlPanel from './control-panel';

function randomLat() {
usefulthink marked this conversation as resolved.
Show resolved Hide resolved
const min = 53.52;
const max = 53.63;

return Math.random() * (max - min) + min;
}
function randomLng() {
const min = 9.88;
const max = 10.12;

return Math.random() * (max - min) + min;
}
usefulthink marked this conversation as resolved.
Show resolved Hide resolved

type MarkerData = Array<{
id: string;
position: google.maps.LatLngLiteral;
type: 'pin' | 'html';
zIndex: number;
}>;

export type AnchorPointName = keyof typeof AdvancedMarkerAnchorPoint;

const initialData: MarkerData = [];

// create 50 random markers
for (let index = 0; index < 50; index++) {
const r = Math.random();

initialData.push({
id: String(index),
position: {lat: randomLat(), lng: randomLng()},
usefulthink marked this conversation as resolved.
Show resolved Hide resolved
zIndex: index,
type: r < 0.5 ? 'pin' : 'html'
});
}

// 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 = initialData
.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 onMouseEnter = useCallback((id: string | null) => setHoverId(id), []);
const onMouseLeave = useCallback(() => setHoverId(null), []);
const onClick = useCallback((id: string | null) => setSelectedId(id), []);

return (
<APIProvider apiKey={API_KEY} libraries={['marker']}>
<Map
mapId={'bf51a910020fa25a'}
defaultZoom={12}
defaultCenter={{lat: 53.55909057947169, lng: 10.005767668054645}}
gestureHandling={'greedy'}
onClick={() => onClick(null)}
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 (
<AdvancedMarker
onClick={() => onClick(id)}
onMouseEnter={() => onMouseEnter(id)}
onMouseLeave={() => onMouseLeave()}
key={id}
zIndex={zIndex}
className="test-class"
style={{
transition: 'all 200ms ease-in-out',
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}
/>
</AdvancedMarker>
);
}

if (type === 'html') {
return (
<React.Fragment key={id}>
<AdvancedMarker
onClick={() => onClick(id)}
zIndex={zIndex}
onMouseEnter={() => onMouseEnter(id)}
onMouseLeave={() => onMouseLeave()}
className="test-class"
style={{
transition: 'all 200ms ease-in-out',
transform: `scale(${[hoverId, selectedId].includes(id) ? 1.4 : 1})`
}}
anchorPoint={AdvancedMarkerAnchorPoint[anchorPoint]}
position={position}>
<div
style={{
usefulthink marked this conversation as resolved.
Show resolved Hide resolved
width: '25px',
height: '25px',
background: selectedId === id ? '#22ccff' : '#0057e7',
transition: 'all 200ms ease-in-out',
border: '1px solid #ffa700',
borderRadius: '4px'
}}></div>
</AdvancedMarker>

{/* anchor point visualization marker */}
<AdvancedMarker
onClick={() => onClick(id)}
zIndex={zIndex}
onMouseEnter={() => onMouseEnter(id)}
onMouseLeave={() => onMouseLeave()}
anchorPoint={AdvancedMarkerAnchorPoint.CENTER}
position={position}>
<div
style={{
width: '8px',
height: '8px',
background: '#ffa700',
borderRadius: '50%',
border: '1px solid #0057e7'
}}></div>
</AdvancedMarker>
</React.Fragment>
);
}
})}
</Map>
<ControlPanel
anchorPointName={anchorPoint}
onAnchorPointChange={(newAnchorPoint: AnchorPointName) =>
setAnchorPoint(newAnchorPoint)
}
/>
</APIProvider>
);
};

export default App;

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

root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);
}
59 changes: 59 additions & 0 deletions examples/advanced-marker-interaction/src/control-panel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import * as React from 'react';
import {AdvancedMarkerAnchorPoint} from '@vis.gl/react-google-maps';
import {AnchorPointName} from './app';

interface Props {
anchorPointName: AnchorPointName;
onAnchorPointChange: (anchorPointName: AnchorPointName) => void;
}

function ControlPanel(props: Props) {
return (
<div className="control-panel">
<h3>Advanced Marker interaction</h3>
<p>
Markers scale on hover plus they change color when they are selected by
clicking on them. The default z-index is sorted by latitude. The z-index
hierachy is "hover" on top, then "selected" and then the default
(latitude).
usefulthink marked this conversation as resolved.
Show resolved Hide resolved
</p>
<p>
The orange dot on the blue markers represents the current anchor point
of the marker. Use the dropdown to change the anchor point and see its
impact.
</p>
<p>
<select
value={props.anchorPointName}
onChange={event =>
props.onAnchorPointChange(
event.currentTarget.value as AnchorPointName
)
}>
{Object.keys(AdvancedMarkerAnchorPoint).map(anchorPoint => {
return (
<option key={anchorPoint} value={anchorPoint}>
{anchorPoint}
</option>
);
})}
</select>
</p>
<div className="links">
<a
href="https://codesandbox.io/s/github/visgl/react-google-maps/tree/main/examples/advanced-marker-interaction"
target="_new">
Try on CodeSandbox ↗
</a>

<a
href="https://github.com/visgl/react-google-maps/tree/main/examples/advanced-marker-interaction"
target="_new">
View Code ↗
</a>
</div>
</div>
);
}

export default React.memo(ControlPanel);
17 changes: 17 additions & 0 deletions examples/advanced-marker-interaction/vite.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import {defineConfig, loadEnv} from 'vite';

export default defineConfig(({mode}) => {
const {GOOGLE_MAPS_API_KEY = ''} = loadEnv(mode, process.cwd(), '');

return {
define: {
'process.env.GOOGLE_MAPS_API_KEY': JSON.stringify(GOOGLE_MAPS_API_KEY)
},
resolve: {
alias: {
'@vis.gl/react-google-maps/examples.js':
'https://visgl.github.io/react-google-maps/scripts/examples.js'
}
}
};
});
4 changes: 2 additions & 2 deletions src/components/__tests__/advanced-marker.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -148,8 +148,8 @@ describe('map and marker-library loaded', () => {
.get(google.maps.marker.AdvancedMarkerElement)
.at(0) as google.maps.marker.AdvancedMarkerElement;

expect(marker.content).toHaveClass('classname-test');
expect(marker.content).toHaveStyle('width: 200px');
expect(marker.content?.firstChild).toHaveClass('classname-test');
expect(marker.content?.firstChild).toHaveStyle('width: 200px');
expect(
queryByTestId(marker.content as HTMLElement, 'marker-content')
).toBeTruthy();
Expand Down
Loading
Loading