diff --git a/docs/api-reference/components/advanced-marker.md b/docs/api-reference/components/advanced-marker.md index bd629d33..f9d13332 100644 --- a/docs/api-reference/components/advanced-marker.md +++ b/docs/api-reference/components/advanced-marker.md @@ -1,117 +1,250 @@ # `` Component -React component to display -a [Google Maps Advanced Marker Element](https://developers.google.com/maps/documentation/javascript/reference/advanced-markers#AdvancedMarkerElement) -instance. +A component to add an [`AdvancedMarkerElement`][gmp-adv-marker] +to a map. By default, an AdvancedMarker will appear as a balloon-shaped, +red maps-pin at the specified position on the map, but the appearance of the +markers can be fully customized. -## Setup +:::info -To use the Advanced Marker View component, it is necessary to add a custom `mapId` -to the map options. To see how this works, check out the following tutorial: -[Use Map IDs](https://developers.google.com/maps/documentation/get-map-id). +The `AdvancedMarker` can only be used on maps using cloud-based map styling +(i.e. the `Map`-component has a [`mapId`][gmp-mapid] specified). -### APIProvider and Map setup to implement the Advanced Marker View component - -```tsx - - '}> - - - - -``` +::: ## Usage -Advanced Marker Element can either be as a standalone component or be customized with -the [Pin Element component](./pin.md) or be displayed with custom HTML. - -### Simple Advanced Marker Element implementation +By default, the marker will be rendered as the default red balloon pin. +This can be customized in two ways: by specifying custom colors, an icon and +such via a [`Pin`](./pin.md) component, or by creating the complete marker with +html/css (images, svg, animations are all supported). -See also: https://developers.google.com/maps/documentation/javascript/adding-a-google-map +For this, the `AdvancedMarker` component optionally accepts child components that +will be rendered instead of the default pin-element on the map, making it +possible to create simple labels and infowindows with it. ```tsx -const App = () => ( - - '}> - - - -); -export default App; +import {AdvancedMarker} from './advanced-marker'; + + + {/* red default marker */} + + + {/* customized green marker */} + + + + + {/* fully customized marker */} + + + +; ``` -### Advanced Marker Element with Pin Element component implementation +When anything other than a `Pin` component is specified for the marker, a +div element (the "content element") will be created and the children will be +rendered into that content element via a [portal][react-portal]. The `style` +and `className` props can be used to configure the styling of this content +element. -See also: https://developers.google.com/maps/documentation/javascript/advanced-markers/basic-customization +:::tip + +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`): ```tsx -const App = () => ( - - '}> - - - - - -); -export default App; + + ... + ``` -### Advanced Marker Element component with custom HTML implementation +::: + +## Props + +The `AdvancedMarker` component supports most of the options in +[`google.maps.marker.AdvancedMarkerElementOptions`][gmp-adv-marker-opts] +as props, as well as a couple of others that are specific to React. + +### Required + +There are no strictly required props for the AdvancedMarker component, +but – for obvious reasons – the position has to be set for the marker to be +shown on the map. + +### Content Props + +#### `className`: string + +A className to be added to the content-element. Since the content-element +isn't created when using the default-pin, this option is only available when +using custom HTML markers. + +#### `style`: [CSSProperties][react-dev-styling] + +Additional style-rules to apply to the content-element. Since the +content-element isn't created when using the default-pin, this option is +only available when using custom HTML markers. + +#### `title`: string + +The title of the marker. If provided, an accessibility text (e.g. for use +with screen readers) will be added to the AdvancedMarkerElement with the +provided value. + +### Positioning Props + +#### `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, +altitude: number}` format. + +#### `zIndex`: number + +All markers are displayed on the map in order of their zIndex, with higher +values in front of lower values. + +By default, `AdvancedMarker`s are displayed according to their vertical +position on screen, with lower AdvancedMarkerElements appearing in front of +AdvancedMarkerElements farther up the screen. + +:::note + +The `zIndex` is also used to help determine relative +priority between multiple markers when using collision +behavior `CollisionBehavior.OPTIONAL_AND_HIDES_LOWER_PRIORITY`. +A higher `zIndex` value indicates higher priority. + +::: + +#### `collisionBehavior`: CollisionBehavior + +Defines how the marker behaves when it collides with another marker or with +the basemap labels on a vector map. Specified as one of the +`CollisionBehaviour` constants. -See also: https://developers.google.com/maps/documentation/javascript/advanced-markers/html-markers +Collision between multiple markers works on both raster and vector +maps; however, hiding labels and default-markers of the base map to make +room for the markers will only work on vector maps. + +:::note + +You should always import the `CollisionBehavior` enum from the +`@vis.gl/react-google-maps` package instead of using the +`google.maps.CollisionBehavior` constants. This will help avoid problems +with using the constants before the maps API has finished loading. ```tsx -const App = () => ( - - '}> - -

I am so customized

-

That is pretty awesome!

-
-
-
-); -export default App; +import {AdvancedMarker, CollisionBehavior} from '@vis.gl/react-google-maps'; + +// ... + + + ... +; ``` -To apply style to the custom HTML marker, it is possible to add a class via the className property which will add -styling to the Advanced Marker Element container. +::: + +See the documentation on [Marker Collision Management][gmp-collisions] +for more information. + +### Other Props + +#### `clickable`: boolean + +Controls if the marker should be clickable. If true, the +marker will be clickable and will be interactive for accessibility purposes +(e.g., allowing keyboard navigation via arrow keys). + +By default, this will automatically be set to true when the `onClick` prop +is specified. + +#### `draggable`: boolean + +Controls if the marker can be repositioned by dragging. + +By default, this will be set to true if any of the corresponding +event-handlers (`onDragStart`, `onDrag`, `onDragEnd`) are specified. + +:::note + +Dragging is only available in 2D. Markers that have an altitude +specified in the position can't be dragged. + +::: + +### Events + +#### `onClick`: (e: [google.maps.marker.AdvancedMarkerClickEvent][gmp-adv-marker-click-ev]) => void + +This event is fired when the marker is clicked. + +#### `onDragStart`: (e: [google.maps.MapMouseEvent][gmp-map-mouse-ev]) => void + +This event is fired when the user starts dragging the marker. + +#### `onDrag`: (e: [google.maps.MapMouseEvent][gmp-map-mouse-ev]) => void + +This event is repeatedly fired while the user drags the marker. + +#### `onDragEnd`: (e: [google.maps.MapMouseEvent][gmp-map-mouse-ev]) => void + +This event is fired when the user stops dragging the marker. + +## Context + +## Hooks -### Draggable Advanced Marker Element component implementation +### `useAdvancedMarkerRef()` -see -also: https://developers.google.com/maps/documentation/javascript/advanced-markers/accessible-markers#make_a_marker_draggable +A hook that can be used to simplify the connection between a marker and an +infowindow. Returns an array containing both a `RefCallback` that can be passed +to the `ref`-prop of the `AdvancedMarker` and the value of the ref as state +variable to be passed to the anchor prop of the `InfoWindow`. ```tsx -const App = () => ( - - '}> - - - -); -export default App; +import { + AdvancedMarker, + InfoWindow, + useAdvancedMarkerRef +} from '@vis.gl/react-google-maps'; + +const MarkerWithInfoWindow = props => { + const [markerRef, marker] = useAdvancedMarkerRef(); + + return ( + <> + + Infowindow Content + + ); +}; ``` -To see an Advanced Marker Element on the map, the `position` property needs to be set. +## Source + +[`./src/components/advanced-marker.tsx`][adv-marker-src] + +[gmp-adv-marker]: https://developers.google.com/maps/documentation/javascript/reference/advanced-markers#AdvancedMarkerElement +[gmp-adv-marker-opts]: https://developers.google.com/maps/documentation/javascript/reference/advanced-markers#AdvancedMarkerElementOptions +[gmp-mapid]: https://developers.google.com/maps/documentation/get-map-id +[gmp-ll]: https://developers.google.com/maps/documentation/javascript/reference/coordinates#LatLngLiteral +[gmp-lla]: https://developers.google.com/maps/documentation/javascript/reference/coordinates#LatLngAltitudeLiteral +[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 +[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/docs/api-reference/components/map.md b/docs/api-reference/components/map.md index f2926638..c5cbca26 100644 --- a/docs/api-reference/components/map.md +++ b/docs/api-reference/components/map.md @@ -186,7 +186,7 @@ const MapWithEventHandler = props => { ``` See [the table below](#mapping-of-google-maps-event-names-to-react-props) -for the full list of props and corresponding prop names. +for the full list of events and corresponding prop names. All event callbacks receive a single argument of type `MapEvent` with the following properties and methods: diff --git a/docs/api-reference/components/pin.md b/docs/api-reference/components/pin.md index cbf77704..b3d1812e 100644 --- a/docs/api-reference/components/pin.md +++ b/docs/api-reference/components/pin.md @@ -1,40 +1,22 @@ # `` Component -React component to display -a [Pin Element](https://developers.google.com/maps/documentation/javascript/reference/advanced-markers#PinElement) -instance. -The Pin Element Component can only be used together with the Advanced Marker. To see how to implement an Advanced -Marker, please check: [Advanced Marker](advanced-marker.md). +The `Pin` component can be used to customize the appearance of an +[`AdvancedMarker`](./advanced-marker.md) component. ## Usage -The Pin Element component needs to be wrapped inside an Advanced Marker Element component. - ```tsx -const App = () => ( - - '}> - - - - - +const CustomizedMarker = () => ( + + + ); -export default App; ``` ## Props -The Pin Props type mirrors -the [google.maps.PinElementOptions interface](https://developers.google.com/maps/documentation/javascript/reference/advanced-markers#PinElementOptions) +The `PinProps` type mirrors the [`google.maps.PinElementOptions` interface][gmp-pin-element-options] and includes all possible options available for a Pin Element instance. -```tsx -type PinProps = google.maps.marker.PinElementOptions; -``` - -To see a Pin on the Map, it has to be wrapped inside an Advanced Marker Element -and the `position` of the marker needs to be set. +[gmp-pin-element]: https://developers.google.com/maps/documentation/javascript/reference/advanced-markers#PinElement +[gmp-pin-element-options]: https://developers.google.com/maps/documentation/javascript/reference/advanced-markers#PinElementOptions diff --git a/src/components/advanced-marker.tsx b/src/components/advanced-marker.tsx index 2b8c846e..92fdfa80 100644 --- a/src/components/advanced-marker.tsx +++ b/src/components/advanced-marker.tsx @@ -17,11 +17,25 @@ 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'; export interface AdvancedMarkerContextValue { marker: google.maps.marker.AdvancedMarkerElement; } +/** + * 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. + */ +export const CollisionBehavior = { + REQUIRED: 'REQUIRED', + REQUIRED_AND_HIDES_OPTIONAL: 'REQUIRED_AND_HIDES_OPTIONAL', + OPTIONAL_AND_HIDES_LOWER_PRIORITY: 'OPTIONAL_AND_HIDES_LOWER_PRIORITY' +} as const; +export type CollisionBehavior = + (typeof CollisionBehavior)[keyof typeof CollisionBehavior]; + export const AdvancedMarkerContext = React.createContext(null); @@ -35,9 +49,12 @@ type AdvancedMarkerEventProps = { export type AdvancedMarkerProps = PropsWithChildren< Omit< google.maps.marker.AdvancedMarkerElementOptions, - 'gmpDraggable' | 'map' + 'gmpDraggable' | 'gmpClickable' | 'content' | 'map' | 'collisionBehavior' > & AdvancedMarkerEventProps & { + draggable?: boolean; + clickable?: boolean; + collisionBehavior?: CollisionBehavior; /** * A className for the content element. * (can only be used with HTML Marker content) @@ -47,7 +64,6 @@ export type AdvancedMarkerProps = PropsWithChildren< * Additional styles to apply to the content element. */ style?: CSSProperties; - draggable?: boolean; } >; @@ -72,6 +88,7 @@ function useAdvancedMarker(props: AdvancedMarkerProps) { onDragStart, onDragEnd, collisionBehavior, + clickable, draggable, position, title, @@ -105,50 +122,48 @@ function useAdvancedMarker(props: AdvancedMarkerProps) { }, [map, markerLibrary, numChildren]); // update className and styles of marker.content element + usePropBinding(contentContainer, 'className', className ?? ''); useEffect(() => { if (!contentContainer) return; setValueForStyles(contentContainer, style || null, prevStyleRef.current); prevStyleRef.current = style || null; - - if (className !== contentContainer.className) - contentContainer.className = className ?? ''; }, [contentContainer, className, style]); - // bind all marker events + // copy other props + usePropBinding(marker, 'position', position); + usePropBinding(marker, 'title', title ?? ''); + usePropBinding(marker, 'zIndex', zIndex); + usePropBinding( + marker, + 'collisionBehavior', + collisionBehavior as google.maps.CollisionBehavior + ); + + // set gmpDraggable from props (when unspecified, it's true if any drag-event + // callbacks are specified) useEffect(() => { if (!marker) return; - const gme = google.maps.event; - - if (onClick) gme.addListener(marker, 'click', onClick); - if (onDrag) gme.addListener(marker, 'drag', onDrag); - if (onDragStart) gme.addListener(marker, 'dragstart', onDragStart); - if (onDragEnd) gme.addListener(marker, 'dragend', onDragEnd); - - if ((onDrag || onDragStart || onDragEnd) && !draggable) { - console.warn( - 'You need to set the marker to draggable to listen to drag-events.' - ); - } - - const m = marker; - return () => { - gme.clearInstanceListeners(m); - }; - }, [marker, draggable, onClick, onDragStart, onDrag, onDragEnd]); + if (draggable !== undefined) marker.gmpDraggable = draggable; + else if (onDrag || onDragStart || onDragEnd) marker.gmpDraggable = true; + else marker.gmpDraggable = false; + }, [marker, draggable, onDrag, onDragEnd, onDragStart]); - // update other marker props when changed + // set gmpClickable from props (when unspecified, it's true if the onClick event + // callback is specified) useEffect(() => { if (!marker) return; - if (position !== undefined) marker.position = position; - if (draggable !== undefined) marker.gmpDraggable = draggable; - if (collisionBehavior !== undefined) - marker.collisionBehavior = collisionBehavior; - if (zIndex !== undefined) marker.zIndex = zIndex; - if (typeof title === 'string') marker.title = title; - }, [marker, position, draggable, collisionBehavior, zIndex, title]); + if (clickable !== undefined) marker.gmpClickable = clickable; + else if (onClick) marker.gmpClickable = true; + else marker.gmpClickable = false; + }, [marker, clickable, onClick]); + + useMapsEventListener(marker, 'click', onClick); + useMapsEventListener(marker, 'drag', onDrag); + useMapsEventListener(marker, 'dragstart', onDragStart); + useMapsEventListener(marker, 'dragend', onDragEnd); return [marker, contentContainer] as const; } @@ -163,11 +178,11 @@ export const AdvancedMarker = forwardRef( useImperativeHandle(ref, () => marker, [marker]); - if (!marker) return null; + if (!contentContainer) return null; return ( - {contentContainer !== null && createPortal(children, contentContainer)} + {createPortal(children, contentContainer)} ); } diff --git a/src/hooks/use-maps-event-listener.ts b/src/hooks/use-maps-event-listener.ts index 1525b214..a527ea2c 100644 --- a/src/hooks/use-maps-event-listener.ts +++ b/src/hooks/use-maps-event-listener.ts @@ -1,13 +1,14 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ import {useEffect} from 'react'; /** * Internally used to bind events to google maps API objects. * @internal */ -export function useMapsEventListener( - target?: google.maps.MVCObject | null, +export function useMapsEventListener void>( + target?: object | null, name?: string, - callback?: ((arg?: unknown) => void) | null + callback?: T | null ) { useEffect(() => { if (!target || !name || !callback) return; diff --git a/src/hooks/use-prop-binding.ts b/src/hooks/use-prop-binding.ts new file mode 100644 index 00000000..f881dc53 --- /dev/null +++ b/src/hooks/use-prop-binding.ts @@ -0,0 +1,22 @@ +import {useEffect} from 'react'; + +/** + * Internally used to copy values from props into API-Objects + * whenever they change. + * + * @example + * usePropBinding(marker, 'position', position); + * + * @internal + */ +export function usePropBinding( + object: T | null, + prop: K, + value: T[K] +) { + useEffect(() => { + if (!object) return; + + object[prop] = value; + }, [object, prop, value]); +}