diff --git a/examples/advanced-marker-interaction/src/app.tsx b/examples/advanced-marker-interaction/src/app.tsx index 2cda2466..ccb2f1bc 100644 --- a/examples/advanced-marker-interaction/src/app.tsx +++ b/examples/advanced-marker-interaction/src/app.tsx @@ -9,7 +9,8 @@ import { InfoWindow, Map, Pin, - useAdvancedMarkerRef + useAdvancedMarkerRef, + CollisionBehavior } from '@vis.gl/react-google-maps'; import {getData} from './data'; @@ -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}> { 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}>
@@ -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} @@ -160,6 +167,7 @@ const App = () => { {infoWindowShown && selectedMarker && (

Marker {selectedId}

Some arbitrary html to be rendered into the InfoWindow.

diff --git a/examples/advanced-marker-interaction/src/control-panel.tsx b/examples/advanced-marker-interaction/src/control-panel.tsx index 90c12126..e759c3b4 100644 --- a/examples/advanced-marker-interaction/src/control-panel.tsx +++ b/examples/advanced-marker-interaction/src/control-panel.tsx @@ -39,6 +39,16 @@ function ControlPanel(props: Props) { })}

+

+ The blue markers also have the{' '} + + collision detection + {' '} + feature turned on for demonstration purposes. +

+
{ features: Feature[]; } | null>(null); - const hamdleInfoWindowClose = useCallback( + const handleInfoWindowClose = useCallback( () => setInfowindowData(null), [setInfowindowData] ); @@ -40,6 +40,7 @@ const App = () => { defaultZoom={3} gestureHandling={'greedy'} disableDefaultUI + onClick={() => setInfowindowData(null)} className={'custom-marker-clustering-map'}> {geojson && ( { {infowindowData && ( diff --git a/examples/custom-marker-clustering/src/components/feature-marker.tsx b/examples/custom-marker-clustering/src/components/feature-marker.tsx index 6befae37..813ed83e 100644 --- a/examples/custom-marker-clustering/src/components/feature-marker.tsx +++ b/examples/custom-marker-clustering/src/components/feature-marker.tsx @@ -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 = { @@ -27,6 +31,7 @@ export const FeatureMarker = ({ ref={markerRef} position={position} onClick={handleClick} + anchorPoint={AdvancedMarkerAnchorPoint.CENTER} className={'marker feature'}> diff --git a/examples/custom-marker-clustering/src/components/features-cluster-marker.tsx b/examples/custom-marker-clustering/src/components/features-cluster-marker.tsx index f9038f9d..8ff440d3 100644 --- a/examples/custom-marker-clustering/src/components/features-cluster-marker.tsx +++ b/examples/custom-marker-clustering/src/components/features-cluster-marker.tsx @@ -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 = { @@ -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}> {sizeAsText} diff --git a/examples/custom-marker-clustering/src/style.css b/examples/custom-marker-clustering/src/style.css index e6fd4b26..ffa10e1f 100644 --- a/examples/custom-marker-clustering/src/style.css +++ b/examples/custom-marker-clustering/src/style.css @@ -25,7 +25,6 @@ box-sizing: border-box; border-radius: 50%; padding: 8px; - translate: 0 50%; border: 1px solid white; color: white; diff --git a/src/components/__tests__/advanced-marker.test.tsx b/src/components/__tests__/advanced-marker.test.tsx index 83b26b7a..84013cc2 100644 --- a/src/components/__tests__/advanced-marker.test.tsx +++ b/src/components/__tests__/advanced-marker.test.tsx @@ -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(); diff --git a/src/components/advanced-marker.tsx b/src/components/advanced-marker.tsx index c6f3b690..777b78c1 100644 --- a/src/components/advanced-marker.tsx +++ b/src/components/advanced-marker.tsx @@ -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. @@ -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%'], @@ -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 ( -
- {children} + // anchoring container +
+ {/* AdvancedMarker div that user can give styles and classes */} +
+ {children} +
); }; +export type CustomMarkerContent = + | (HTMLDivElement & {isCustomMarker?: boolean}) + | null; + export type AdvancedMarkerRef = google.maps.marker.AdvancedMarkerElement | null; function useAdvancedMarker(props: AdvancedMarkerProps) { const [marker, setMarker] = @@ -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); @@ -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); diff --git a/src/components/info-window.tsx b/src/components/info-window.tsx index 8cdfaad2..b4ed26c1 100644 --- a/src/components/info-window.tsx +++ b/src/components/info-window.tsx @@ -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, @@ -180,23 +180,26 @@ export const InfoWindow = (props: PropsWithChildren) => { // 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; diff --git a/src/components/pin.tsx b/src/components/pin.tsx index f5549c1f..c1e725e4 100644 --- a/src/components/pin.tsx +++ b/src/components/pin.tsx @@ -58,7 +58,10 @@ export const Pin = (props: PropsWithChildren) => { } // 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);