Skip to content

Commit 97a98b2

Browse files
authored
fix: adjust advanced marker markup to fix anchoring & collision behavior (#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.
1 parent c6b7947 commit 97a98b2

File tree

10 files changed

+113
-54
lines changed

10 files changed

+113
-54
lines changed

examples/advanced-marker-interaction/src/app.tsx

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@ import {
99
InfoWindow,
1010
Map,
1111
Pin,
12-
useAdvancedMarkerRef
12+
useAdvancedMarkerRef,
13+
CollisionBehavior
1314
} from '@vis.gl/react-google-maps';
1415

1516
import {getData} from './data';
@@ -108,7 +109,8 @@ const App = () => {
108109
zIndex={zIndex}
109110
className="custom-marker"
110111
style={{
111-
transform: `scale(${[hoverId, selectedId].includes(id) ? 1.4 : 1})`
112+
transform: `scale(${[hoverId, selectedId].includes(id) ? 1.3 : 1})`,
113+
transformOrigin: AdvancedMarkerAnchorPoint['BOTTOM'].join(' ')
112114
}}
113115
position={position}>
114116
<Pin
@@ -129,12 +131,17 @@ const App = () => {
129131
anchorPoint={AdvancedMarkerAnchorPoint[anchorPoint]}
130132
className="custom-marker"
131133
style={{
132-
transform: `scale(${[hoverId, selectedId].includes(id) ? 1.4 : 1})`
134+
transform: `scale(${[hoverId, selectedId].includes(id) ? 1.3 : 1})`,
135+
transformOrigin:
136+
AdvancedMarkerAnchorPoint[anchorPoint].join(' ')
133137
}}
134138
onMarkerClick={(
135139
marker: google.maps.marker.AdvancedMarkerElement
136140
) => onMarkerClick(id, marker)}
137141
onMouseEnter={() => onMouseEnter(id)}
142+
collisionBehavior={
143+
CollisionBehavior.OPTIONAL_AND_HIDES_LOWER_PRIORITY
144+
}
138145
onMouseLeave={onMouseLeave}>
139146
<div
140147
className={`custom-html-content ${selectedId === id ? 'selected' : ''}`}></div>
@@ -145,7 +152,7 @@ const App = () => {
145152
onMarkerClick={(
146153
marker: google.maps.marker.AdvancedMarkerElement
147154
) => onMarkerClick(id, marker)}
148-
zIndex={zIndex}
155+
zIndex={zIndex + 1}
149156
onMouseEnter={() => onMouseEnter(id)}
150157
onMouseLeave={onMouseLeave}
151158
anchorPoint={AdvancedMarkerAnchorPoint.CENTER}
@@ -160,6 +167,7 @@ const App = () => {
160167
{infoWindowShown && selectedMarker && (
161168
<InfoWindow
162169
anchor={selectedMarker}
170+
pixelOffset={[0, -2]}
163171
onCloseClick={handleInfowindowCloseClick}>
164172
<h2>Marker {selectedId}</h2>
165173
<p>Some arbitrary html to be rendered into the InfoWindow.</p>

examples/advanced-marker-interaction/src/control-panel.tsx

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,16 @@ function ControlPanel(props: Props) {
3939
})}
4040
</select>
4141
</p>
42+
<p>
43+
The blue markers also have the{' '}
44+
<a
45+
href="https://developers.google.com/maps/documentation/javascript/reference/advanced-markers#AdvancedMarkerElement.collisionBehavior"
46+
target="_blank">
47+
collision detection
48+
</a>{' '}
49+
feature turned on for demonstration purposes.
50+
</p>
51+
4252
<div className="links">
4353
<a
4454
href="https://codesandbox.io/s/github/visgl/react-google-maps/tree/main/examples/advanced-marker-interaction"

examples/custom-marker-clustering/src/app.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ const App = () => {
2727
features: Feature<Point>[];
2828
} | null>(null);
2929

30-
const hamdleInfoWindowClose = useCallback(
30+
const handleInfoWindowClose = useCallback(
3131
() => setInfowindowData(null),
3232
[setInfowindowData]
3333
);
@@ -40,6 +40,7 @@ const App = () => {
4040
defaultZoom={3}
4141
gestureHandling={'greedy'}
4242
disableDefaultUI
43+
onClick={() => setInfowindowData(null)}
4344
className={'custom-marker-clustering-map'}>
4445
{geojson && (
4546
<ClusteredMarkers
@@ -51,7 +52,7 @@ const App = () => {
5152

5253
{infowindowData && (
5354
<InfoWindow
54-
onClose={hamdleInfoWindowClose}
55+
onCloseClick={handleInfoWindowClose}
5556
anchor={infowindowData.anchor}>
5657
<InfoWindowContent features={infowindowData.features} />
5758
</InfoWindow>

examples/custom-marker-clustering/src/components/feature-marker.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
import React, {useCallback} from 'react';
2-
import {AdvancedMarker, useAdvancedMarkerRef} from '@vis.gl/react-google-maps';
2+
import {
3+
AdvancedMarker,
4+
AdvancedMarkerAnchorPoint,
5+
useAdvancedMarkerRef
6+
} from '@vis.gl/react-google-maps';
37
import {CastleSvg} from './castle-svg';
48

59
type TreeMarkerProps = {
@@ -27,6 +31,7 @@ export const FeatureMarker = ({
2731
ref={markerRef}
2832
position={position}
2933
onClick={handleClick}
34+
anchorPoint={AdvancedMarkerAnchorPoint.CENTER}
3035
className={'marker feature'}>
3136
<CastleSvg />
3237
</AdvancedMarker>

examples/custom-marker-clustering/src/components/features-cluster-marker.tsx

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
import React, {useCallback} from 'react';
2-
import {AdvancedMarker, useAdvancedMarkerRef} from '@vis.gl/react-google-maps';
2+
import {
3+
AdvancedMarker,
4+
AdvancedMarkerAnchorPoint,
5+
useAdvancedMarkerRef
6+
} from '@vis.gl/react-google-maps';
37
import {CastleSvg} from './castle-svg';
48

59
type TreeClusterMarkerProps = {
@@ -33,7 +37,8 @@ export const FeaturesClusterMarker = ({
3337
zIndex={size}
3438
onClick={handleClick}
3539
className={'marker cluster'}
36-
style={{width: markerSize, height: markerSize}}>
40+
style={{width: markerSize, height: markerSize}}
41+
anchorPoint={AdvancedMarkerAnchorPoint.CENTER}>
3742
<CastleSvg />
3843
<span>{sizeAsText}</span>
3944
</AdvancedMarker>

examples/custom-marker-clustering/src/style.css

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@
2525
box-sizing: border-box;
2626
border-radius: 50%;
2727
padding: 8px;
28-
translate: 0 50%;
2928
border: 1px solid white;
3029
color: white;
3130

src/components/__tests__/advanced-marker.test.tsx

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -148,8 +148,13 @@ describe('map and marker-library loaded', () => {
148148
.get(google.maps.marker.AdvancedMarkerElement)
149149
.at(0) as google.maps.marker.AdvancedMarkerElement;
150150

151-
expect(marker.content?.firstChild).toHaveClass('classname-test');
152-
expect(marker.content?.firstChild).toHaveStyle('width: 200px');
151+
const advancedMarkerWithClass = (
152+
marker.content as HTMLElement
153+
).querySelector('.classname-test');
154+
155+
expect(advancedMarkerWithClass).toBeTruthy();
156+
expect(advancedMarkerWithClass).toHaveStyle('width: 200px');
157+
153158
expect(
154159
queryByTestId(marker.content as HTMLElement, 'marker-content')
155160
).toBeTruthy();

src/components/advanced-marker.tsx

Lines changed: 55 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,10 @@ export function isAdvancedMarker(
3131
);
3232
}
3333

34+
function isElementNode(node: Node): node is HTMLElement {
35+
return node.nodeType === Node.ELEMENT_NODE;
36+
}
37+
3438
/**
3539
* Copy of the `google.maps.CollisionBehavior` constants.
3640
* They have to be duplicated here since we can't wait for the maps API to load to be able to use them.
@@ -48,19 +52,19 @@ export const AdvancedMarkerContext =
4852

4953
// [xPosition, yPosition] when the top left corner is [0, 0]
5054
export const AdvancedMarkerAnchorPoint = {
51-
TOP_LEFT: ['0', '0'],
52-
TOP_CENTER: ['50%', '0'],
53-
TOP: ['50%', '0'],
54-
TOP_RIGHT: ['100%', '0'],
55-
LEFT_CENTER: ['0', '50%'],
56-
LEFT_TOP: ['0', '0'],
57-
LEFT: ['0', '50%'],
58-
LEFT_BOTTOM: ['0', '100%'],
59-
RIGHT_TOP: ['100%', '0'],
55+
TOP_LEFT: ['0%', '0%'],
56+
TOP_CENTER: ['50%', '0%'],
57+
TOP: ['50%', '0%'],
58+
TOP_RIGHT: ['100%', '0%'],
59+
LEFT_CENTER: ['0%', '50%'],
60+
LEFT_TOP: ['0%', '0%'],
61+
LEFT: ['0%', '50%'],
62+
LEFT_BOTTOM: ['0%', '100%'],
63+
RIGHT_TOP: ['100%', '0%'],
6064
RIGHT: ['100%', '50%'],
6165
RIGHT_CENTER: ['100%', '50%'],
6266
RIGHT_BOTTOM: ['100%', '100%'],
63-
BOTTOM_LEFT: ['0', '100%'],
67+
BOTTOM_LEFT: ['0%', '100%'],
6468
BOTTOM_CENTER: ['50%', '100%'],
6569
BOTTOM: ['50%', '100%'],
6670
BOTTOM_RIGHT: ['100%', '100%'],
@@ -124,28 +128,25 @@ const MarkerContent = ({
124128
const [xTranslation, yTranslation] =
125129
anchorPoint ?? AdvancedMarkerAnchorPoint['BOTTOM'];
126130

127-
const {transform: userTransform, ...restStyles} = styles ?? {};
128-
129-
let transformStyle = `translate(-${xTranslation}, -${yTranslation})`;
131+
// The "translate(50%, 100%)" is here to counter and reset the default anchoring of the advanced marker element
132+
// that comes from the api
133+
const transformStyle = `translate(50%, 100%) translate(-${xTranslation}, -${yTranslation})`;
130134

131-
// preserve extra transform styles that were set by the user
132-
if (userTransform) {
133-
transformStyle += ` ${userTransform}`;
134-
}
135135
return (
136-
<div
137-
className={className}
138-
style={{
139-
width: 'fit-content',
140-
transformOrigin: `${xTranslation} ${yTranslation}`,
141-
transform: transformStyle,
142-
...restStyles
143-
}}>
144-
{children}
136+
// anchoring container
137+
<div style={{transform: transformStyle}}>
138+
{/* AdvancedMarker div that user can give styles and classes */}
139+
<div className={className} style={styles}>
140+
{children}
141+
</div>
145142
</div>
146143
);
147144
};
148145

146+
export type CustomMarkerContent =
147+
| (HTMLDivElement & {isCustomMarker?: boolean})
148+
| null;
149+
149150
export type AdvancedMarkerRef = google.maps.marker.AdvancedMarkerElement | null;
150151
function useAdvancedMarker(props: AdvancedMarkerProps) {
151152
const [marker, setMarker] =
@@ -185,11 +186,14 @@ function useAdvancedMarker(props: AdvancedMarkerProps) {
185186
setMarker(newMarker);
186187

187188
// create the container for marker content if there are children
188-
let contentElement: HTMLDivElement | null = null;
189+
let contentElement: CustomMarkerContent = null;
189190
if (numChildren > 0) {
190191
contentElement = document.createElement('div');
191-
contentElement.style.width = '0';
192-
contentElement.style.height = '0';
192+
193+
// We need some kind of flag to identify the custom marker content
194+
// in the infowindow component. Choosing a custom property instead of a className
195+
// to not encourage users to style the marker content directly.
196+
contentElement.isCustomMarker = true;
193197

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

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

241-
if (clickable !== undefined) marker.gmpClickable = clickable;
242-
else if (onClick) marker.gmpClickable = true;
243-
else marker.gmpClickable = false;
244-
}, [marker, clickable, onClick]);
245+
const gmpClickable =
246+
clickable !== undefined ||
247+
Boolean(onClick) ||
248+
Boolean(onMouseEnter) ||
249+
Boolean(onMouseLeave);
250+
251+
// gmpClickable is only available in beta version of the
252+
// maps api (as of 2024-10-10)
253+
marker.gmpClickable = gmpClickable;
254+
255+
// enable pointer events for the markers with custom content
256+
if (gmpClickable && marker?.content && isElementNode(marker.content)) {
257+
marker.content.style.pointerEvents = 'none';
258+
259+
if (marker.content.firstElementChild) {
260+
(marker.content.firstElementChild as HTMLElement).style.pointerEvents =
261+
'all';
262+
}
263+
}
264+
}, [marker, clickable, onClick, onMouseEnter, onMouseLeave]);
245265

246266
useMapsEventListener(marker, 'click', onClick);
247267
useMapsEventListener(marker, 'drag', onDrag);

src/components/info-window.tsx

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import {useMapsEventListener} from '../hooks/use-maps-event-listener';
1414
import {setValueForStyles} from '../libraries/set-value-for-styles';
1515
import {useMapsLibrary} from '../hooks/use-maps-library';
1616
import {useDeepCompareEffect} from '../libraries/use-deep-compare-effect';
17-
import {isAdvancedMarker} from './advanced-marker';
17+
import {CustomMarkerContent, isAdvancedMarker} from './advanced-marker';
1818

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

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

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

195196
const contentBcr = anchorDomContent?.getBoundingClientRect();
196197

197198
// center infowindow above marker
198199
const anchorOffsetX =
199-
contentBcr.x - wrapperBcr.x + contentBcr.width / 2;
200+
contentBcr.x -
201+
wrapperBcr.x +
202+
(contentBcr.width - wrapperBcr.width) / 2;
200203
const anchorOffsetY = contentBcr.y - wrapperBcr.y;
201204

202205
const opts: google.maps.InfoWindowOptions = infoWindowOptions;

src/components/pin.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,10 @@ export const Pin = (props: PropsWithChildren<PinProps>) => {
5858
}
5959

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

6366
while (markerContent?.firstChild) {
6467
markerContent.removeChild(markerContent.firstChild);

0 commit comments

Comments
 (0)