Skip to content

Commit

Permalink
Allow MarkerCluster popups to contain interactive ReactNode (#169)
Browse files Browse the repository at this point in the history
* Allow MarkerCluster popup to be interactive

* Enable keypress to open popup

* Replace previous MarkerCluster implementation with new

* Add TileLayer to typescript exports
  • Loading branch information
halocline authored Sep 6, 2024
1 parent 7e58871 commit 310cf61
Show file tree
Hide file tree
Showing 6 changed files with 102 additions and 53 deletions.
3 changes: 3 additions & 0 deletions grommet-leaflet/src/grommet-leaflet-reset.css
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@
color: initial;
font-size: initial;
}
.leaflet-container a:hover {
color: initial;
}
.leaflet-popup-content-wrapper {
background-color: transparent;
padding: 0px;
Expand Down
1 change: 1 addition & 0 deletions grommet-leaflet/src/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,6 @@ export * from './layers/Marker';
export * from './layers/MarkerCluster';
export * from './layers/Pin';
export * from './layers/Popup';
export * from './layers/TileLayer';
export * from './utils';
export * from './themes';
148 changes: 95 additions & 53 deletions grommet-leaflet/src/layers/MarkerCluster/MarkerCluster.jsx
Original file line number Diff line number Diff line change
@@ -1,79 +1,121 @@
import React, { useContext } from 'react';
import React from 'react';
import ReactDOMServer from 'react-dom/server';
import { createRoot } from 'react-dom/client';
import PropTypes from 'prop-types';
import { ThemeContext } from 'styled-components';
import {
// createElementObject,
createPathComponent,
// extendContext,
} from '@react-leaflet/core';
import { v4 as uuidv4 } from 'uuid';
import L from 'leaflet';
import 'leaflet.markercluster';
import ReactDOMServer from 'react-dom/server';
import {
createElementObject,
createLayerComponent,
extendContext,
} from '@react-leaflet/core';
import { Cluster, Popup } from '..';

const createElementObject = (instance, context, container) =>
Object.freeze({ instance, context, container });
function createMarkerClusterGroup(props, context) {
const clusterProps = {};
const clusterEvents = {};

const extendContext = (source, extra) => Object.freeze({ ...source, ...extra });
Object.entries(props).forEach(([key, value]) => {
if (key.startsWith('on')) {
clusterEvents[key] = value;
} else {
clusterProps[key] = value;
}
});

const createMarkerClusterGroup = ({ ...rest }, context) => {
const markerClusterGroup = new L.MarkerClusterGroup({
const markers = new L.MarkerClusterGroup({
showCoverageOnHover: false,
zoomToBoundsOnClick: false,
...rest,
...clusterProps,
});

// Bind events to markers
Object.entries(clusterEvents).forEach(([key, value]) => {
const event = `cluster${key.substring(2).toLowerCase()}`;
markers.on(event, value);
});

markers.on('clusterclick clusterkeypress', e => {
const { propagatedFrom } = e;
const popupContent = clusterProps.popup({ markers });
const popupId = `grommet-leaflet-popup-${uuidv4()}`;
const popup = new L.Popup();
// In order to take advantage of Leaflet's automatic popup placement
// and panning we first need to render the popup's contents statically.
// This establishes the popup's dimensions so that the map will pan if the
// marker is close to the map's bounds.
popup
.setLatLng(propagatedFrom.getLatLng())
.setContent(
`<div id="${popupId}">${ReactDOMServer.renderToString(
popupContent,
)}</div>`,
)
.openOn(context.map);

// We then render the popup's contents dynamically and replace the static
// content with an interactive ReactNode.
const domNode = document.getElementById(popupId);
const root = createRoot(domNode);
root.render(popupContent);
});

return createElementObject(
markerClusterGroup,
extendContext(context, { layerContainer: markerClusterGroup }),
markers,
extendContext(context, {
layerContainer: markers,
}),
);
};
}

const LeafletMarkerCluster = createPathComponent(createMarkerClusterGroup);
const LeafletMarkerCluster = createLayerComponent(createMarkerClusterGroup);

const MarkerCluster = ({ icon: iconProp, popup: popupProp, ...rest }) => {
const theme = useContext(ThemeContext);
const MarkerCluster = props => {
const { children, icon: iconProp, popup: popupProp, ...rest } = props;
const theme = React.useContext(ThemeContext);
const popupRef = React.useRef(null);

return (
<LeafletMarkerCluster
iconCreateFunction={cluster => {
// only bind popup if popupProp is defined
if (popupProp && popupProp({ cluster })) {
const popup = cluster.bindPopup(
ReactDOMServer.renderToString(
<ThemeContext.Provider value={theme}>
<Popup>{popupProp({ cluster })}</Popup>
</ThemeContext.Provider>,
),
);
const createIcon = cluster => {
return L.divIcon({
// 'grommet-cluster-group' class prevents
// leaflet default divIcon styles
className: 'grommet-cluster-group',
html: ReactDOMServer.renderToString(
<ThemeContext.Provider value={theme}>
{iconProp ? (
React.cloneElement(iconProp({ cluster }), {
cluster,
})
) : (
<Cluster cluster={cluster} />
)}
</ThemeContext.Provider>,
),
});
};

cluster.on('click', () => {
popup.openPopup();
});
}
const popup = ({ cluster }) =>
popupProp && typeof popupProp === 'function' ? (
<ThemeContext.Provider ref={popupRef} value={theme}>
<Popup>{popupProp({ cluster })}</Popup>
</ThemeContext.Provider>
) : undefined;

return L.divIcon({
// 'grommet-cluster-group' class prevents
// leaflet default divIcon styles
className: 'grommet-cluster-group',
html: ReactDOMServer.renderToString(
<ThemeContext.Provider value={theme}>
{iconProp ? (
React.cloneElement(iconProp({ cluster }), {
cluster,
})
) : (
<Cluster cluster={cluster} />
)}
</ThemeContext.Provider>,
),
});
}}
return (
<LeafletMarkerCluster
iconCreateFunction={createIcon}
popup={popup}
{...rest}
/>
>
{children}
</LeafletMarkerCluster>
);
};

MarkerCluster.propTypes = {
children: PropTypes.node,
icon: PropTypes.func,
popup: PropTypes.func,
};
Expand Down
1 change: 1 addition & 0 deletions grommet-leaflet/src/layers/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ export * from './Marker';
export * from './MarkerCluster';
export * from './Pin';
export * from './Popup';
export * from './TileLayer';
1 change: 1 addition & 0 deletions grommet-leaflet/src/layers/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ export * from './Marker';
export * from './MarkerCluster';
export * from './Pin';
export * from './Popup';
export * from './TileLayer';
1 change: 1 addition & 0 deletions grommet-leaflet/vite.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export default defineConfig({
fileName: 'grommet-leaflet',
formats: ['es', 'cjs'],
},
sourcemap: true,
rollupOptions: {
// externalize deps that shouldn't be bundled with library
external: [
Expand Down

0 comments on commit 310cf61

Please sign in to comment.