diff --git a/docs/api-reference/components/advanced-marker.md b/docs/api-reference/components/advanced-marker.md index cffa682..a89aa10 100644 --- a/docs/api-reference/components/advanced-marker.md +++ b/docs/api-reference/components/advanced-marker.md @@ -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 - +import {AdvancedMarker, AdvancedMarkerAnchorPoint} from '@vis.gl/react-google-maps'; + + ... ``` @@ -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] @@ -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 @@ -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 @@ -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. @@ -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 diff --git a/examples/advanced-marker-interaction/README.md b/examples/advanced-marker-interaction/README.md new file mode 100644 index 0000000..96e7f4d --- /dev/null +++ b/examples/advanced-marker-interaction/README.md @@ -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="" +``` + +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 diff --git a/examples/advanced-marker-interaction/index.html b/examples/advanced-marker-interaction/index.html new file mode 100644 index 0000000..864069a --- /dev/null +++ b/examples/advanced-marker-interaction/index.html @@ -0,0 +1,31 @@ + + + + + + Advanced Marker interaction + + + + +
+ + + diff --git a/examples/advanced-marker-interaction/package.json b/examples/advanced-marker-interaction/package.json new file mode 100644 index 0000000..05765f9 --- /dev/null +++ b/examples/advanced-marker-interaction/package.json @@ -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" + } +} diff --git a/examples/advanced-marker-interaction/src/app.tsx b/examples/advanced-marker-interaction/src/app.tsx new file mode 100644 index 0000000..2cda246 --- /dev/null +++ b/examples/advanced-marker-interaction/src/app.tsx @@ -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(null); + const [selectedId, setSelectedId] = useState(null); + + const [anchorPoint, setAnchorPoint] = useState('BOTTOM' as AnchorPointName); + const [selectedMarker, setSelectedMarker] = + useState(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 ( + + + {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 ( + 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}> + + + ); + } + + if (type === 'html') { + return ( + + onMarkerClick(id, marker)} + onMouseEnter={() => onMouseEnter(id)} + onMouseLeave={onMouseLeave}> +
+
+ + {/* anchor point visualization marker */} + onMarkerClick(id, marker)} + zIndex={zIndex} + onMouseEnter={() => onMouseEnter(id)} + onMouseLeave={onMouseLeave} + anchorPoint={AdvancedMarkerAnchorPoint.CENTER} + position={position}> +
+
+
+ ); + } + })} + + {infoWindowShown && selectedMarker && ( + +

Marker {selectedId}

+

Some arbitrary html to be rendered into the InfoWindow.

+
+ )} +
+ + setAnchorPoint(newAnchorPoint) + } + /> +
+ ); +}; + +export const AdvancedMarkerWithRef = ( + props: AdvancedMarkerProps & { + onMarkerClick: (marker: google.maps.marker.AdvancedMarkerElement) => void; + } +) => { + const {children, onMarkerClick, ...advancedMarkerProps} = props; + const [markerRef, marker] = useAdvancedMarkerRef(); + + return ( + { + if (marker) { + onMarkerClick(marker); + } + }} + ref={markerRef} + {...advancedMarkerProps}> + {children} + + ); +}; + +export default App; + +export function renderToDom(container: HTMLElement) { + const root = createRoot(container); + + root.render( + + + + ); +} diff --git a/examples/advanced-marker-interaction/src/control-panel.tsx b/examples/advanced-marker-interaction/src/control-panel.tsx new file mode 100644 index 0000000..90c1212 --- /dev/null +++ b/examples/advanced-marker-interaction/src/control-panel.tsx @@ -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 ( +
+

Advanced Marker interaction

+

+ Markers scale on hover and change their 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). +

+

+ 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. +

+

+ +

+ +
+ ); +} + +export default React.memo(ControlPanel); diff --git a/examples/advanced-marker-interaction/src/data.ts b/examples/advanced-marker-interaction/src/data.ts new file mode 100644 index 0000000..5779027 --- /dev/null +++ b/examples/advanced-marker-interaction/src/data.ts @@ -0,0 +1,26 @@ +type MarkerData = Array<{ + id: string; + position: google.maps.LatLngLiteral; + type: 'pin' | 'html'; + zIndex: number; +}>; + +export function getData() { + const data: MarkerData = []; + + // create 50 random markers + for (let index = 0; index < 50; index++) { + data.push({ + id: String(index), + position: {lat: rnd(53.52, 53.63), lng: rnd(9.88, 10.12)}, + zIndex: index, + type: Math.random() < 0.5 ? 'pin' : 'html' + }); + } + + return data; +} + +function rnd(min: number, max: number) { + return Math.random() * (max - min) + min; +} diff --git a/examples/advanced-marker-interaction/src/style.css b/examples/advanced-marker-interaction/src/style.css new file mode 100644 index 0000000..b5eb82c --- /dev/null +++ b/examples/advanced-marker-interaction/src/style.css @@ -0,0 +1,24 @@ +.custom-marker { + transition: all 200ms ease-in-out; +} + +.custom-html-content { + width: 25px; + height: 25px; + transition: all 200ms ease-in-out; + border: 1px solid #ffa700; + border-radius: 4px; + background: #0057e7; +} + +.custom-html-content.selected { + background: #22ccff; +} + +.visualization-marker { + width: 8px; + height: 8px; + background: #ffa700; + border-radius: 50%; + border: 1px solid #0057e7; +} diff --git a/examples/advanced-marker-interaction/vite.config.js b/examples/advanced-marker-interaction/vite.config.js new file mode 100644 index 0000000..522c6cb --- /dev/null +++ b/examples/advanced-marker-interaction/vite.config.js @@ -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' + } + } + }; +}); diff --git a/src/components/__tests__/advanced-marker.test.tsx b/src/components/__tests__/advanced-marker.test.tsx index 42466cd..83b26b7 100644 --- a/src/components/__tests__/advanced-marker.test.tsx +++ b/src/components/__tests__/advanced-marker.test.tsx @@ -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(); diff --git a/src/components/advanced-marker.tsx b/src/components/advanced-marker.tsx index 673726e..c6f3b69 100644 --- a/src/components/advanced-marker.tsx +++ b/src/components/advanced-marker.tsx @@ -7,23 +7,30 @@ import React, { useEffect, useImperativeHandle, useMemo, - useRef, useState } from 'react'; import {createPortal} from 'react-dom'; import {useMap} from '../hooks/use-map'; import {useMapsLibrary} from '../hooks/use-maps-library'; -import {setValueForStyles} from '../libraries/set-value-for-styles'; import type {Ref, PropsWithChildren} from 'react'; import {useMapsEventListener} from '../hooks/use-maps-event-listener'; import {usePropBinding} from '../hooks/use-prop-binding'; +import {useDomEventListener} from '../hooks/use-dom-event-listener'; export interface AdvancedMarkerContextValue { marker: google.maps.marker.AdvancedMarkerElement; } +export function isAdvancedMarker( + marker: google.maps.Marker | google.maps.marker.AdvancedMarkerElement +): marker is google.maps.marker.AdvancedMarkerElement { + return ( + (marker as google.maps.marker.AdvancedMarkerElement).content !== undefined + ); +} + /** * 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. @@ -39,8 +46,34 @@ export type CollisionBehavior = export const AdvancedMarkerContext = React.createContext(null); +// [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'], + RIGHT: ['100%', '50%'], + RIGHT_CENTER: ['100%', '50%'], + RIGHT_BOTTOM: ['100%', '100%'], + BOTTOM_LEFT: ['0', '100%'], + BOTTOM_CENTER: ['50%', '100%'], + BOTTOM: ['50%', '100%'], + BOTTOM_RIGHT: ['100%', '100%'], + CENTER: ['50%', '50%'] +} as const; + +export type AdvancedMarkerAnchorPoint = + (typeof AdvancedMarkerAnchorPoint)[keyof typeof AdvancedMarkerAnchorPoint]; + type AdvancedMarkerEventProps = { onClick?: (e: google.maps.MapMouseEvent) => void; + onMouseEnter?: (e: google.maps.MapMouseEvent['domEvent']) => void; + onMouseLeave?: (e: google.maps.MapMouseEvent['domEvent']) => void; onDrag?: (e: google.maps.MapMouseEvent) => void; onDragStart?: (e: google.maps.MapMouseEvent) => void; onDragEnd?: (e: google.maps.MapMouseEvent) => void; @@ -55,6 +88,15 @@ export type AdvancedMarkerProps = PropsWithChildren< draggable?: boolean; clickable?: boolean; collisionBehavior?: CollisionBehavior; + /** + * The anchor point for the Advanced Marker. + * Either use one of the predefined anchor point 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 ("50%") or in pixels ("20px"). + */ + anchorPoint?: AdvancedMarkerAnchorPoint | [string, string]; /** * A className for the content element. * (can only be used with HTML Marker content) @@ -67,6 +109,43 @@ export type AdvancedMarkerProps = PropsWithChildren< } >; +type MarkerContentProps = PropsWithChildren & { + styles?: CSSProperties; + className?: string; + anchorPoint?: AdvancedMarkerAnchorPoint | [string, string]; +}; + +const MarkerContent = ({ + children, + styles, + className, + anchorPoint +}: MarkerContentProps) => { + const [xTranslation, yTranslation] = + anchorPoint ?? AdvancedMarkerAnchorPoint['BOTTOM']; + + const {transform: userTransform, ...restStyles} = styles ?? {}; + + let transformStyle = `translate(-${xTranslation}, -${yTranslation})`; + + // preserve extra transform styles that were set by the user + if (userTransform) { + transformStyle += ` ${userTransform}`; + } + return ( +
+ {children} +
+ ); +}; + export type AdvancedMarkerRef = google.maps.marker.AdvancedMarkerElement | null; function useAdvancedMarker(props: AdvancedMarkerProps) { const [marker, setMarker] = @@ -74,16 +153,15 @@ function useAdvancedMarker(props: AdvancedMarkerProps) { const [contentContainer, setContentContainer] = useState(null); - const prevStyleRef = useRef(null); - const map = useMap(); const markerLibrary = useMapsLibrary('marker'); const { children, - className, - style, onClick, + className, + onMouseEnter, + onMouseLeave, onDrag, onDragStart, onDragEnd, @@ -110,6 +188,8 @@ function useAdvancedMarker(props: AdvancedMarkerProps) { let contentElement: HTMLDivElement | null = null; if (numChildren > 0) { contentElement = document.createElement('div'); + contentElement.style.width = '0'; + contentElement.style.height = '0'; newMarker.content = contentElement; setContentContainer(contentElement); @@ -123,20 +203,15 @@ function useAdvancedMarker(props: AdvancedMarkerProps) { }; }, [map, markerLibrary, numChildren]); - // update className and styles of marker.content element + // When no children are present we don't have our own wrapper div + // which usually gets the user provided className. In this case + // we set the className directly on the marker.content element that comes + // with the AdvancedMarker. useEffect(() => { - if (!marker || !marker.content) return; + if (!marker || !marker.content || numChildren > 0) return; (marker.content as HTMLElement).className = className || ''; - }, [marker, className]); - - usePropBinding(contentContainer, 'className', className ?? ''); - useEffect(() => { - if (!contentContainer) return; - - setValueForStyles(contentContainer, style || null, prevStyleRef.current); - prevStyleRef.current = style || null; - }, [contentContainer, className, style]); + }, [marker, className, numChildren]); // copy other props usePropBinding(marker, 'position', position); @@ -173,12 +248,15 @@ function useAdvancedMarker(props: AdvancedMarkerProps) { useMapsEventListener(marker, 'dragstart', onDragStart); useMapsEventListener(marker, 'dragend', onDragEnd); + useDomEventListener(marker?.element, 'mouseenter', onMouseEnter); + useDomEventListener(marker?.element, 'mouseleave', onMouseLeave); + return [marker, contentContainer] as const; } export const AdvancedMarker = forwardRef( (props: AdvancedMarkerProps, ref: Ref) => { - const {children} = props; + const {children, style, className, anchorPoint} = props; const [marker, contentContainer] = useAdvancedMarker(props); const advancedMarkerContextValue: AdvancedMarkerContextValue | null = @@ -190,7 +268,15 @@ export const AdvancedMarker = forwardRef( return ( - {createPortal(children, contentContainer)} + {createPortal( + + {children} + , + contentContainer + )} ); } diff --git a/src/components/info-window.tsx b/src/components/info-window.tsx index 231876c..8cdfaad 100644 --- a/src/components/info-window.tsx +++ b/src/components/info-window.tsx @@ -14,6 +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'; export type InfoWindowProps = Omit< google.maps.InfoWindowOptions, @@ -176,6 +177,38 @@ export const InfoWindow = (props: PropsWithChildren) => { const openOptions: google.maps.InfoWindowOpenOptions = {map}; if (anchor) { openOptions.anchor = anchor; + + // 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; + + // 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) { + // 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 contentBcr = anchorDomContent?.getBoundingClientRect(); + + // center infowindow above marker + const anchorOffsetX = + contentBcr.x - wrapperBcr.x + contentBcr.width / 2; + const anchorOffsetY = contentBcr.y - wrapperBcr.y; + + const opts: google.maps.InfoWindowOptions = infoWindowOptions; + + opts.pixelOffset = new google.maps.Size( + pixelOffset ? pixelOffset[0] + anchorOffsetX : anchorOffsetX, + pixelOffset ? pixelOffset[1] + anchorOffsetY : anchorOffsetY + ); + + infoWindow.setOptions(opts); + } + } } if (shouldFocus !== undefined) { @@ -193,7 +226,7 @@ export const InfoWindow = (props: PropsWithChildren) => { infoWindow.close(); }; - }, [infoWindow, anchor, map, shouldFocus]); + }, [infoWindow, anchor, map, shouldFocus, infoWindowOptions, pixelOffset]); return ( <> diff --git a/src/components/pin.tsx b/src/components/pin.tsx index a94bf1a..f5549c1 100644 --- a/src/components/pin.tsx +++ b/src/components/pin.tsx @@ -58,7 +58,15 @@ export const Pin = (props: PropsWithChildren) => { } // Set content of Advanced Marker View to the Pin View element - advancedMarker.content = pinElement.element; + const markerContent = advancedMarker.content?.firstChild; + + while (markerContent?.firstChild) { + markerContent.removeChild(markerContent.firstChild); + } + + if (markerContent) { + markerContent.appendChild(pinElement.element); + } }, [advancedMarker, glyphContainer, props]); return createPortal(props.children, glyphContainer); diff --git a/src/hooks/use-dom-event-listener.ts b/src/hooks/use-dom-event-listener.ts new file mode 100644 index 0000000..76ad42b --- /dev/null +++ b/src/hooks/use-dom-event-listener.ts @@ -0,0 +1,20 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import {useEffect} from 'react'; + +/** + * Internally used to bind events to DOM nodes. + * @internal + */ +export function useDomEventListener void>( + target?: Node | null, + name?: string, + callback?: T | null +) { + useEffect(() => { + if (!target || !name || !callback) return; + + target.addEventListener(name, callback); + + return () => target.removeEventListener(name, callback); + }, [target, name, callback]); +} diff --git a/website/src/examples-sidebar.js b/website/src/examples-sidebar.js index e9c5fcb..bea0036 100644 --- a/website/src/examples-sidebar.js +++ b/website/src/examples-sidebar.js @@ -13,6 +13,7 @@ const sidebars = { 'basic-map', 'change-map-styles', 'markers-and-infowindows', + 'advanced-marker-interaction', 'map-control', 'multiple-maps', 'marker-clustering', diff --git a/website/src/examples/advanced-marker-interaction.mdx b/website/src/examples/advanced-marker-interaction.mdx new file mode 100644 index 0000000..850d419 --- /dev/null +++ b/website/src/examples/advanced-marker-interaction.mdx @@ -0,0 +1,5 @@ +# Advanced Marker interaction + +import App from 'website-examples/advanced-marker-interaction/src/app'; + + diff --git a/website/static/images/examples/advanced-marker-interaction.png b/website/static/images/examples/advanced-marker-interaction.png new file mode 100644 index 0000000..70b40b7 Binary files /dev/null and b/website/static/images/examples/advanced-marker-interaction.png differ