Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 39 additions & 9 deletions examples/advanced-marker-interaction/src/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,10 @@ import {
CollisionBehavior
} from '@vis.gl/react-google-maps';

import {getData} from './data';

import ControlPanel from './control-panel';

import {getData, MarkerType, textSnippets} from './data';

import './style.css';

export type AnchorPointName = keyof typeof AdvancedMarkerAnchorPoint;
Expand All @@ -27,7 +27,10 @@ export type AnchorPointName = keyof typeof AdvancedMarkerAnchorPoint;
// thus appear in front.
const data = getData()
.sort((a, b) => b.position.lat - a.position.lat)
.map((dataItem, index) => ({...dataItem, zIndex: index}));
.map((dataItem, index) => ({
...dataItem,
zIndex: index
}));

const Z_INDEX_SELECTED = data.length;
const Z_INDEX_HOVER = data.length + 1;
Expand All @@ -40,22 +43,33 @@ const App = () => {

const [hoverId, setHoverId] = useState<string | null>(null);
const [selectedId, setSelectedId] = useState<string | null>(null);
const [infowindowContent, setInfowindowContent] = useState<string | null>(
null
);

const [anchorPoint, setAnchorPoint] = useState('BOTTOM' as AnchorPointName);
const [anchorPoint, setAnchorPoint] = useState(
'LEFT_CENTER' as AnchorPointName
);
const [selectedMarker, setSelectedMarker] =
useState<google.maps.marker.AdvancedMarkerElement | null>(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) => {
(
id: string | null,
marker?: google.maps.marker.AdvancedMarkerElement,
type?: MarkerType
) => {
setSelectedId(id);

if (marker) {
setSelectedMarker(marker);
}

setInfowindowContent(type ? textSnippets[type] : null);

if (id !== selectedId) {
setInfoWindowShown(true);
} else {
Expand Down Expand Up @@ -97,12 +111,25 @@ const App = () => {
zIndex = Z_INDEX_SELECTED;
}

if (type === 'default') {
return (
<AdvancedMarkerWithRef
key={id}
zIndex={zIndex}
position={position}
onMarkerClick={marker => onMarkerClick(id, marker, type)}
onMouseEnter={() => onMouseEnter(id)}
onMouseLeave={onMouseLeave}
/>
);
}

if (type === 'pin') {
return (
<AdvancedMarkerWithRef
onMarkerClick={(
marker: google.maps.marker.AdvancedMarkerElement
) => onMarkerClick(id, marker)}
) => onMarkerClick(id, marker, type)}
onMouseEnter={() => onMouseEnter(id)}
onMouseLeave={onMouseLeave}
key={id}
Expand All @@ -114,7 +141,7 @@ const App = () => {
}}
position={position}>
<Pin
background={selectedId === id ? '#22ccff' : null}
background={selectedId === id ? '#22ccff' : 'orange'}
borderColor={selectedId === id ? '#1e89a1' : null}
glyphColor={selectedId === id ? '#0f677a' : null}
/>
Expand All @@ -137,7 +164,9 @@ const App = () => {
}}
onMarkerClick={(
marker: google.maps.marker.AdvancedMarkerElement
) => onMarkerClick(id, marker)}
) => {
onMarkerClick(id, marker, type);
}}
onMouseEnter={() => onMouseEnter(id)}
collisionBehavior={
CollisionBehavior.OPTIONAL_AND_HIDES_LOWER_PRIORITY
Expand Down Expand Up @@ -166,11 +195,12 @@ const App = () => {

{infoWindowShown && selectedMarker && (
<InfoWindow
headerDisabled={true}
anchor={selectedMarker}
pixelOffset={[0, -2]}
onCloseClick={handleInfowindowCloseClick}>
<h2>Marker {selectedId}</h2>
<p>Some arbitrary html to be rendered into the InfoWindow.</p>
<p>{infowindowContent}</p>
</InfoWindow>
)}
</Map>
Expand Down
16 changes: 14 additions & 2 deletions examples/advanced-marker-interaction/src/data.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,32 @@
export type MarkerType = 'default' | 'pin' | 'html';

type MarkerData = Array<{
id: string;
position: google.maps.LatLngLiteral;
type: 'pin' | 'html';
type: MarkerType;
zIndex: number;
infowindowContent?: string;
}>;

export const textSnippets = {
default: 'This is a default AdvancedMarkerElement without custom content',
pin: 'This is a AdvancedMarkerElement with custom pin-style marker',
html: 'This is a AdvancedMarkerElement with custom HTML content'
} as const;

export function getData() {
const data: MarkerData = [];

// create 50 random markers
for (let index = 0; index < 50; index++) {
const type =
Math.random() < 0.1 ? 'default' : Math.random() < 0.5 ? 'pin' : 'html';

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'
type
});
}

Expand Down
11 changes: 11 additions & 0 deletions examples/advanced-marker-interaction/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@vis.gl/react-google-maps": ["../../src"]
}
},
"include": ["src/**/*", "../../src/**/*", "./types/**/*"],
"exclude": ["node_modules", "dist"]
}
7 changes: 7 additions & 0 deletions examples/advanced-marker-interaction/types/global.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export declare global {
// const or let does not work in this case, it has to be var
// eslint-disable-next-line no-var
var GOOGLE_MAPS_API_KEY: string | undefined;
// eslint-disable-next-line no-var, @typescript-eslint/no-explicit-any
var process: any;
}
74 changes: 34 additions & 40 deletions src/components/advanced-marker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ 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';
import {globalStyleManager} from '../libraries/global-style-manager';

export interface AdvancedMarkerContextValue {
marker: google.maps.marker.AdvancedMarkerElement;
Expand Down Expand Up @@ -118,42 +119,16 @@ type MarkerContentProps = PropsWithChildren & {
anchorPoint?: AdvancedMarkerAnchorPoint | [string, string];
};

const MarkerContent = ({
children,
styles,
className,
anchorPoint
}: MarkerContentProps) => {
const [xTranslation, yTranslation] =
anchorPoint ?? AdvancedMarkerAnchorPoint['BOTTOM'];

let xTranslationFlipped = `-${xTranslation}`;
let yTranslationFlipped = `-${yTranslation}`;
if (xTranslation.trimStart().startsWith('-')) {
xTranslationFlipped = xTranslation.substring(1);
}
if (yTranslation.trimStart().startsWith('-')) {
yTranslationFlipped = yTranslation.substring(1);
}

// 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(${xTranslationFlipped}, ${yTranslationFlipped})`;

const MarkerContent = ({children, styles, className}: MarkerContentProps) => {
/* AdvancedMarker div that user can give styles and classes */
return (
// anchoring container
<div style={{transform: transformStyle}}>
{/* AdvancedMarker div that user can give styles and classes */}
<div className={className} style={styles}>
{children}
</div>
<div className={className} style={styles}>
{children}
</div>
);
};

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

export type AdvancedMarkerRef = google.maps.marker.AdvancedMarkerElement | null;
function useAdvancedMarker(props: AdvancedMarkerProps) {
Expand All @@ -179,11 +154,15 @@ function useAdvancedMarker(props: AdvancedMarkerProps) {
draggable,
position,
title,
zIndex
zIndex,
anchorPoint
} = props;

const numChildren = Children.count(children);

const [xTranslation, yTranslation] =
anchorPoint ?? AdvancedMarkerAnchorPoint['BOTTOM'];

// create an AdvancedMarkerElement instance and add it to the map once available
useEffect(() => {
if (!map || !markerLibrary) return;
Expand All @@ -199,9 +178,25 @@ function useAdvancedMarker(props: AdvancedMarkerProps) {
contentElement = document.createElement('div');

// 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;
// in the infowindow component. Choosing a data attribute to also be able
// to target it via CSS to disable pointer event when using custom anchor point
newMarker.dataset.origin = 'rgm';

let xTranslationFlipped = `-${xTranslation}`;
let yTranslationFlipped = `-${yTranslation}`;

if (xTranslation.trimStart().startsWith('-')) {
xTranslationFlipped = xTranslation.substring(1);
}
if (yTranslation.trimStart().startsWith('-')) {
yTranslationFlipped = yTranslation.substring(1);
}

// 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(${xTranslationFlipped}, ${yTranslationFlipped})`;
contentElement.style.transform = transformStyle;
globalStyleManager.addAdvancedMarkerPointerEventsOverwrite();

newMarker.content = contentElement;
setContentContainer(contentElement);
Expand All @@ -213,7 +208,7 @@ function useAdvancedMarker(props: AdvancedMarkerProps) {
setMarker(null);
setContentContainer(null);
};
}, [map, markerLibrary, numChildren]);
}, [xTranslation, yTranslation, map, markerLibrary, numChildren]);

// When no children are present we don't have our own wrapper div
// which usually gets the user provided className. In this case
Expand Down Expand Up @@ -263,11 +258,10 @@ function useAdvancedMarker(props: AdvancedMarkerProps) {

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

if (marker.content.firstElementChild) {
(marker.content.firstElementChild as HTMLElement).style.pointerEvents =
'all';
if (onClick) {
marker.content.style.cursor = 'pointer';
}
}
}, [marker, clickable, onClick, onMouseEnter, onMouseLeave]);
Expand Down
14 changes: 7 additions & 7 deletions src/components/info-window.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import {useMapsEventListener} from '../hooks/use-maps-event-listener';
import {useMapsLibrary} from '../hooks/use-maps-library';
import {useMemoized} from '../hooks/use-memoized';
import {setValueForStyles} from '../libraries/set-value-for-styles';
import {CustomMarkerContent, isAdvancedMarker} from './advanced-marker';
import {isAdvancedMarker} from './advanced-marker';

export type InfoWindowProps = Omit<
google.maps.InfoWindowOptions,
Expand Down Expand Up @@ -186,14 +186,13 @@ export const InfoWindow: FunctionComponent<

// Only do the infowindow adjusting when dealing with an AdvancedMarker
if (isAdvancedMarker(anchor) && anchor.content instanceof Element) {
const wrapper = anchor.content as CustomMarkerContent;
const wrapperBcr = wrapper?.getBoundingClientRect();
const anchorBcr = anchor?.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 (wrapperBcr && wrapper?.isCustomMarker) {
if (anchorBcr && anchor.dataset.origin === 'rgm') {
// We can safely typecast here since we control that element and we know that
// it is a div
const anchorDomContent = anchor.content.firstElementChild
Expand All @@ -204,9 +203,10 @@ export const InfoWindow: FunctionComponent<
// center infowindow above marker
const anchorOffsetX =
contentBcr.x -
wrapperBcr.x +
(contentBcr.width - wrapperBcr.width) / 2;
const anchorOffsetY = contentBcr.y - wrapperBcr.y;
anchorBcr.x +
(contentBcr.width - anchorBcr.width) / 2;

const anchorOffsetY = contentBcr.y - anchorBcr.y;

const opts: google.maps.InfoWindowOptions = infoWindowOptions;

Expand Down
4 changes: 2 additions & 2 deletions src/components/pin.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,8 @@ export const Pin: FunctionComponent<PinProps> = props => {
// Set content of Advanced Marker View to the Pin View element
// 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;
// "advancedMarker.content" (from google) -> "anchor container"
const markerContent = advancedMarker.content?.firstChild;

while (markerContent?.firstChild) {
markerContent.removeChild(markerContent.firstChild);
Expand Down
38 changes: 38 additions & 0 deletions src/libraries/global-style-manager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// Global style manager to track rendered styles and avoid duplicates
class GlobalStyleManager {
private renderedStyles = new Set<string>();
private styleElement: HTMLStyleElement | null = null;

private getStyleElement(): HTMLStyleElement {
if (!this.styleElement) {
this.styleElement = document.createElement('style');
this.styleElement.setAttribute('data-rgm-anchor-styles', '');
document.head.appendChild(this.styleElement);
}
return this.styleElement;
}

addAdvancedMarkerPointerEventsOverwrite(): void {
if (this.renderedStyles.has('marker-pointer-events')) {
return;
}

const styleElement = this.getStyleElement();
styleElement.textContent += `
gmp-advanced-marker[data-origin='rgm'] {
pointer-events: none !important;
}
`;
this.renderedStyles.add('marker-pointer-events');
}

cleanup(): void {
if (this.styleElement) {
this.styleElement.remove();
this.styleElement = null;
this.renderedStyles.clear();
}
}
}

export const globalStyleManager = new GlobalStyleManager();