Skip to content

Commit

Permalink
fix: adjust advanced marker markup to fix anchoring & collision behav…
Browse files Browse the repository at this point in the history
…ior (#577)

This addresses a few issues that arose after we updated the structure of the advanced marker.

- Moving away from the the wrapper div that had 0,0 width and height. Now we reset the default transform of the marker to be able to apply our own anchoring.

- Less (no) interference with the html that user provide as custom HTML content of the marker.

- Better handling of when a marker should receive pointer events and when not.
  • Loading branch information
mrMetalWood authored Oct 24, 2024
1 parent c6b7947 commit 97a98b2
Show file tree
Hide file tree
Showing 10 changed files with 113 additions and 54 deletions.
16 changes: 12 additions & 4 deletions examples/advanced-marker-interaction/src/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ import {
InfoWindow,
Map,
Pin,
useAdvancedMarkerRef
useAdvancedMarkerRef,
CollisionBehavior
} from '@vis.gl/react-google-maps';

import {getData} from './data';
Expand Down Expand Up @@ -108,7 +109,8 @@ const App = () => {
zIndex={zIndex}
className="custom-marker"
style={{
transform: `scale(${[hoverId, selectedId].includes(id) ? 1.4 : 1})`
transform: `scale(${[hoverId, selectedId].includes(id) ? 1.3 : 1})`,
transformOrigin: AdvancedMarkerAnchorPoint['BOTTOM'].join(' ')
}}
position={position}>
<Pin
Expand All @@ -129,12 +131,17 @@ const App = () => {
anchorPoint={AdvancedMarkerAnchorPoint[anchorPoint]}
className="custom-marker"
style={{
transform: `scale(${[hoverId, selectedId].includes(id) ? 1.4 : 1})`
transform: `scale(${[hoverId, selectedId].includes(id) ? 1.3 : 1})`,
transformOrigin:
AdvancedMarkerAnchorPoint[anchorPoint].join(' ')
}}
onMarkerClick={(
marker: google.maps.marker.AdvancedMarkerElement
) => onMarkerClick(id, marker)}
onMouseEnter={() => onMouseEnter(id)}
collisionBehavior={
CollisionBehavior.OPTIONAL_AND_HIDES_LOWER_PRIORITY
}
onMouseLeave={onMouseLeave}>
<div
className={`custom-html-content ${selectedId === id ? 'selected' : ''}`}></div>
Expand All @@ -145,7 +152,7 @@ const App = () => {
onMarkerClick={(
marker: google.maps.marker.AdvancedMarkerElement
) => onMarkerClick(id, marker)}
zIndex={zIndex}
zIndex={zIndex + 1}
onMouseEnter={() => onMouseEnter(id)}
onMouseLeave={onMouseLeave}
anchorPoint={AdvancedMarkerAnchorPoint.CENTER}
Expand All @@ -160,6 +167,7 @@ const App = () => {
{infoWindowShown && selectedMarker && (
<InfoWindow
anchor={selectedMarker}
pixelOffset={[0, -2]}
onCloseClick={handleInfowindowCloseClick}>
<h2>Marker {selectedId}</h2>
<p>Some arbitrary html to be rendered into the InfoWindow.</p>
Expand Down
10 changes: 10 additions & 0 deletions examples/advanced-marker-interaction/src/control-panel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,16 @@ function ControlPanel(props: Props) {
})}
</select>
</p>
<p>
The blue markers also have the{' '}
<a
href="https://developers.google.com/maps/documentation/javascript/reference/advanced-markers#AdvancedMarkerElement.collisionBehavior"
target="_blank">
collision detection
</a>{' '}
feature turned on for demonstration purposes.
</p>

<div className="links">
<a
href="https://codesandbox.io/s/github/visgl/react-google-maps/tree/main/examples/advanced-marker-interaction"
Expand Down
5 changes: 3 additions & 2 deletions examples/custom-marker-clustering/src/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ const App = () => {
features: Feature<Point>[];
} | null>(null);

const hamdleInfoWindowClose = useCallback(
const handleInfoWindowClose = useCallback(
() => setInfowindowData(null),
[setInfowindowData]
);
Expand All @@ -40,6 +40,7 @@ const App = () => {
defaultZoom={3}
gestureHandling={'greedy'}
disableDefaultUI
onClick={() => setInfowindowData(null)}
className={'custom-marker-clustering-map'}>
{geojson && (
<ClusteredMarkers
Expand All @@ -51,7 +52,7 @@ const App = () => {

{infowindowData && (
<InfoWindow
onClose={hamdleInfoWindowClose}
onCloseClick={handleInfoWindowClose}
anchor={infowindowData.anchor}>
<InfoWindowContent features={infowindowData.features} />
</InfoWindow>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import React, {useCallback} from 'react';
import {AdvancedMarker, useAdvancedMarkerRef} from '@vis.gl/react-google-maps';
import {
AdvancedMarker,
AdvancedMarkerAnchorPoint,
useAdvancedMarkerRef
} from '@vis.gl/react-google-maps';
import {CastleSvg} from './castle-svg';

type TreeMarkerProps = {
Expand Down Expand Up @@ -27,6 +31,7 @@ export const FeatureMarker = ({
ref={markerRef}
position={position}
onClick={handleClick}
anchorPoint={AdvancedMarkerAnchorPoint.CENTER}
className={'marker feature'}>
<CastleSvg />
</AdvancedMarker>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import React, {useCallback} from 'react';
import {AdvancedMarker, useAdvancedMarkerRef} from '@vis.gl/react-google-maps';
import {
AdvancedMarker,
AdvancedMarkerAnchorPoint,
useAdvancedMarkerRef
} from '@vis.gl/react-google-maps';
import {CastleSvg} from './castle-svg';

type TreeClusterMarkerProps = {
Expand Down Expand Up @@ -33,7 +37,8 @@ export const FeaturesClusterMarker = ({
zIndex={size}
onClick={handleClick}
className={'marker cluster'}
style={{width: markerSize, height: markerSize}}>
style={{width: markerSize, height: markerSize}}
anchorPoint={AdvancedMarkerAnchorPoint.CENTER}>
<CastleSvg />
<span>{sizeAsText}</span>
</AdvancedMarker>
Expand Down
1 change: 0 additions & 1 deletion examples/custom-marker-clustering/src/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@
box-sizing: border-box;
border-radius: 50%;
padding: 8px;
translate: 0 50%;
border: 1px solid white;
color: white;

Expand Down
9 changes: 7 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,13 @@ describe('map and marker-library loaded', () => {
.get(google.maps.marker.AdvancedMarkerElement)
.at(0) as google.maps.marker.AdvancedMarkerElement;

expect(marker.content?.firstChild).toHaveClass('classname-test');
expect(marker.content?.firstChild).toHaveStyle('width: 200px');
const advancedMarkerWithClass = (
marker.content as HTMLElement
).querySelector('.classname-test');

expect(advancedMarkerWithClass).toBeTruthy();
expect(advancedMarkerWithClass).toHaveStyle('width: 200px');

expect(
queryByTestId(marker.content as HTMLElement, 'marker-content')
).toBeTruthy();
Expand Down
90 changes: 55 additions & 35 deletions src/components/advanced-marker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@ export function isAdvancedMarker(
);
}

function isElementNode(node: Node): node is HTMLElement {
return node.nodeType === Node.ELEMENT_NODE;
}

/**
* Copy of the `google.maps.CollisionBehavior` constants.
* They have to be duplicated here since we can't wait for the maps API to load to be able to use them.
Expand All @@ -48,19 +52,19 @@ export const AdvancedMarkerContext =

// [xPosition, yPosition] when the top left corner is [0, 0]
export const AdvancedMarkerAnchorPoint = {
TOP_LEFT: ['0', '0'],
TOP_CENTER: ['50%', '0'],
TOP: ['50%', '0'],
TOP_RIGHT: ['100%', '0'],
LEFT_CENTER: ['0', '50%'],
LEFT_TOP: ['0', '0'],
LEFT: ['0', '50%'],
LEFT_BOTTOM: ['0', '100%'],
RIGHT_TOP: ['100%', '0'],
TOP_LEFT: ['0%', '0%'],
TOP_CENTER: ['50%', '0%'],
TOP: ['50%', '0%'],
TOP_RIGHT: ['100%', '0%'],
LEFT_CENTER: ['0%', '50%'],
LEFT_TOP: ['0%', '0%'],
LEFT: ['0%', '50%'],
LEFT_BOTTOM: ['0%', '100%'],
RIGHT_TOP: ['100%', '0%'],
RIGHT: ['100%', '50%'],
RIGHT_CENTER: ['100%', '50%'],
RIGHT_BOTTOM: ['100%', '100%'],
BOTTOM_LEFT: ['0', '100%'],
BOTTOM_LEFT: ['0%', '100%'],
BOTTOM_CENTER: ['50%', '100%'],
BOTTOM: ['50%', '100%'],
BOTTOM_RIGHT: ['100%', '100%'],
Expand Down Expand Up @@ -124,28 +128,25 @@ const MarkerContent = ({
const [xTranslation, yTranslation] =
anchorPoint ?? AdvancedMarkerAnchorPoint['BOTTOM'];

const {transform: userTransform, ...restStyles} = styles ?? {};

let transformStyle = `translate(-${xTranslation}, -${yTranslation})`;
// The "translate(50%, 100%)" is here to counter and reset the default anchoring of the advanced marker element
// that comes from the api
const transformStyle = `translate(50%, 100%) translate(-${xTranslation}, -${yTranslation})`;

// preserve extra transform styles that were set by the user
if (userTransform) {
transformStyle += ` ${userTransform}`;
}
return (
<div
className={className}
style={{
width: 'fit-content',
transformOrigin: `${xTranslation} ${yTranslation}`,
transform: transformStyle,
...restStyles
}}>
{children}
// anchoring container
<div style={{transform: transformStyle}}>
{/* AdvancedMarker div that user can give styles and classes */}
<div className={className} style={styles}>
{children}
</div>
</div>
);
};

export type CustomMarkerContent =
| (HTMLDivElement & {isCustomMarker?: boolean})
| null;

export type AdvancedMarkerRef = google.maps.marker.AdvancedMarkerElement | null;
function useAdvancedMarker(props: AdvancedMarkerProps) {
const [marker, setMarker] =
Expand Down Expand Up @@ -185,11 +186,14 @@ function useAdvancedMarker(props: AdvancedMarkerProps) {
setMarker(newMarker);

// create the container for marker content if there are children
let contentElement: HTMLDivElement | null = null;
let contentElement: CustomMarkerContent = null;
if (numChildren > 0) {
contentElement = document.createElement('div');
contentElement.style.width = '0';
contentElement.style.height = '0';

// We need some kind of flag to identify the custom marker content
// in the infowindow component. Choosing a custom property instead of a className
// to not encourage users to style the marker content directly.
contentElement.isCustomMarker = true;

newMarker.content = contentElement;
setContentContainer(contentElement);
Expand Down Expand Up @@ -233,15 +237,31 @@ function useAdvancedMarker(props: AdvancedMarkerProps) {
else marker.gmpDraggable = false;
}, [marker, draggable, onDrag, onDragEnd, onDragStart]);

// set gmpClickable from props (when unspecified, it's true if the onClick event
// callback is specified)
// set gmpClickable from props (when unspecified, it's true if the onClick or one of
// the hover events callbacks are specified)
useEffect(() => {
if (!marker) return;

if (clickable !== undefined) marker.gmpClickable = clickable;
else if (onClick) marker.gmpClickable = true;
else marker.gmpClickable = false;
}, [marker, clickable, onClick]);
const gmpClickable =
clickable !== undefined ||
Boolean(onClick) ||
Boolean(onMouseEnter) ||
Boolean(onMouseLeave);

// gmpClickable is only available in beta version of the
// maps api (as of 2024-10-10)
marker.gmpClickable = gmpClickable;

// enable pointer events for the markers with custom content
if (gmpClickable && marker?.content && isElementNode(marker.content)) {
marker.content.style.pointerEvents = 'none';

if (marker.content.firstElementChild) {
(marker.content.firstElementChild as HTMLElement).style.pointerEvents =
'all';
}
}
}, [marker, clickable, onClick, onMouseEnter, onMouseLeave]);

useMapsEventListener(marker, 'click', onClick);
useMapsEventListener(marker, 'drag', onDrag);
Expand Down
15 changes: 9 additions & 6 deletions src/components/info-window.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import {useMapsEventListener} from '../hooks/use-maps-event-listener';
import {setValueForStyles} from '../libraries/set-value-for-styles';
import {useMapsLibrary} from '../hooks/use-maps-library';
import {useDeepCompareEffect} from '../libraries/use-deep-compare-effect';
import {isAdvancedMarker} from './advanced-marker';
import {CustomMarkerContent, isAdvancedMarker} from './advanced-marker';

export type InfoWindowProps = Omit<
google.maps.InfoWindowOptions,
Expand Down Expand Up @@ -180,23 +180,26 @@ export const InfoWindow = (props: PropsWithChildren<InfoWindowProps>) => {

// Only do the infowindow adjusting when dealing with an AdvancedMarker
if (isAdvancedMarker(anchor) && anchor.content instanceof Element) {
const wrapperBcr = anchor.content.getBoundingClientRect() ?? {};
const {width: anchorWidth, height: anchorHeight} = wrapperBcr;
const wrapper = anchor.content as CustomMarkerContent;
const wrapperBcr = wrapper?.getBoundingClientRect();

// This checks whether or not the anchor has custom content with our own
// div wrapper. If not, that means we have a regular AdvancedMarker without any children.
// In that case we do not want to adjust the infowindow since it is all handled correctly
// by the Google Maps API.
if (anchorWidth === 0 && anchorHeight === 0) {
if (wrapperBcr && wrapper?.isCustomMarker) {
// We can safely typecast here since we control that element and we know that
// it is a div
const anchorDomContent = anchor.content.firstElementChild as Element;
const anchorDomContent = anchor.content.firstElementChild
?.firstElementChild as Element;

const contentBcr = anchorDomContent?.getBoundingClientRect();

// center infowindow above marker
const anchorOffsetX =
contentBcr.x - wrapperBcr.x + contentBcr.width / 2;
contentBcr.x -
wrapperBcr.x +
(contentBcr.width - wrapperBcr.width) / 2;
const anchorOffsetY = contentBcr.y - wrapperBcr.y;

const opts: google.maps.InfoWindowOptions = infoWindowOptions;
Expand Down
5 changes: 4 additions & 1 deletion src/components/pin.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,10 @@ export const Pin = (props: PropsWithChildren<PinProps>) => {
}

// Set content of Advanced Marker View to the Pin View element
const markerContent = advancedMarker.content?.firstChild;
// Here we are selecting the anchor container.
// The hierarchy is as follows:
// "advancedMarker.content" (from google) -> "pointer events reset div" -> "anchor container"
const markerContent = advancedMarker.content?.firstChild?.firstChild;

while (markerContent?.firstChild) {
markerContent.removeChild(markerContent.firstChild);
Expand Down

0 comments on commit 97a98b2

Please sign in to comment.