@@ -2,7 +2,18 @@ import React, { useRef, useEffect, useState } from 'react';
22import { createPortal } from 'react-dom' ;
33import useMap from '../../hooks/useMap' ;
44import maplibregl from 'maplibre-gl' ;
5- import { Box } from '@mui/material' ;
5+ import { Box , Paper , IconButton } from '@mui/material' ;
6+ import CloseIcon from '@mui/icons-material/Close' ;
7+
8+ // Constants for popup styling
9+ const POPUP_PADDING_VERTICAL = 12 ;
10+ const POPUP_PADDING_HORIZONTAL = 16 ;
11+ const CLOSE_BUTTON_SPACING = 4 ;
12+ const SCROLLBAR_WIDTH = 16 ; // Typical scrollbar width
13+ const CLOSE_BUTTON_OFFSET = SCROLLBAR_WIDTH + CLOSE_BUTTON_SPACING ;
14+ const POPUP_MIN_WIDTH = 200 ;
15+ const POPUP_MAX_WIDTH = 750 ;
16+ const POPUP_MAX_HEIGHT = 500 ;
617
718export interface MlMarkerProps {
819 /** ID of the map to add the marker to */
@@ -27,6 +38,10 @@ export interface MlMarkerProps {
2738 contentOffset ?: number ;
2839 /** Whether mouse events pass through the marker content */
2940 passEventsThrough ?: boolean ;
41+ /** Whether to show a close button to remove the marker */
42+ showCloseButton ?: boolean ;
43+ /** Callback function when the close button is clicked */
44+ onClose ?: ( ) => void ;
3045 /** Anchor position of the marker relative to its coordinates */
3146 anchor ?:
3247 | 'top'
@@ -103,16 +118,33 @@ function getBoxMargins(
103118 return m ;
104119}
105120
106- const MlMarker = ( { passEventsThrough = true , contentOffset = 5 , ...props } : MlMarkerProps ) => {
121+ const MlMarker = ( {
122+ passEventsThrough = true ,
123+ contentOffset = 5 ,
124+ showCloseButton = true ,
125+ ...props
126+ } : MlMarkerProps ) => {
107127 const mapHook = useMap ( {
108128 mapId : props . mapId ,
109129 waitForLayer : props . insertBeforeLayer ,
110130 } ) ;
111131
112132 const [ marker , setMarker ] = useState < maplibregl . Marker | null > ( null ) ;
133+ const [ contentWidth , setContentWidth ] = useState < number > ( 300 ) ;
113134 const container = useRef < HTMLDivElement | null > ( null ) ;
114135 const iframeRef = useRef < HTMLIFrameElement | null > ( null ) ;
115136
137+ const handleClose = ( event : React . MouseEvent ) => {
138+ event . stopPropagation ( ) ;
139+ if ( props . onClose ) {
140+ props . onClose ( ) ;
141+ } else {
142+ // Default behavior: remove the marker
143+ marker ?. remove ( ) ;
144+ container . current ?. remove ( ) ;
145+ }
146+ } ;
147+
116148 useEffect ( ( ) => {
117149 if ( ! mapHook . map ) return ;
118150
@@ -161,9 +193,17 @@ const MlMarker = ({ passEventsThrough = true, contentOffset = 5, ...props }: MlM
161193
162194 function handleIframeLoad ( ) {
163195 const iframeDoc = iframeRef . current ?. contentWindow ?. document ;
164- if ( iframeDoc && iframeRef . current ?. parentElement ) {
196+ if ( iframeDoc && iframeRef . current ) {
165197 const scrollHeight = iframeDoc . documentElement . scrollHeight ;
166- iframeRef . current . parentElement . style . height = `${ scrollHeight } px` ;
198+ const scrollWidth = iframeDoc . documentElement . scrollWidth ;
199+ iframeRef . current . style . height = `${ scrollHeight } px` ;
200+
201+ // Set width based on content, with min and max constraints
202+ const calculatedWidth = Math . max (
203+ POPUP_MIN_WIDTH ,
204+ Math . min ( scrollWidth + POPUP_PADDING_HORIZONTAL * 2 , POPUP_MAX_WIDTH )
205+ ) ;
206+ setContentWidth ( calculatedWidth ) ;
167207 }
168208 }
169209
@@ -173,41 +213,117 @@ const MlMarker = ({ passEventsThrough = true, contentOffset = 5, ...props }: MlM
173213 < Box
174214 sx = { {
175215 position : 'absolute' ,
176- display : 'flex' ,
177- width : '300px' ,
178- maxHeight : '500px' ,
179- opacity : passEventsThrough ? 1 : 0.7 ,
180- zIndex : - 1 ,
181216 transform : getBoxTransform ( props . anchor ) ,
182217 ...getBoxMargins ( props . anchor , contentOffset , props . markerStyle ) ,
183- pointerEvents : passEventsThrough ? 'none' : 'auto' ,
184- '&:hover' : {
185- opacity : 1 ,
186- } ,
218+ zIndex : - 1 ,
187219 ...props . containerStyle ,
188220 } }
189221 >
190- < iframe
191- ref = { iframeRef }
192- onLoad = { handleIframeLoad }
193- style = { {
194- width : '100%' ,
195- borderStyle : 'none' ,
196- ...props . iframeStyle ,
222+ < Paper
223+ elevation = { 8 }
224+ sx = { {
225+ width : `${ contentWidth } px` ,
226+ maxWidth : '90vw' ,
227+ opacity : passEventsThrough ? 1 : 0.85 ,
228+ pointerEvents : 'auto' ,
229+ overflow : 'hidden' ,
230+ position : 'relative' ,
231+ transition : 'opacity 0.2s ease-in-out, width 0.2s ease-in-out' ,
232+ '&:hover' : {
233+ opacity : 1 ,
234+ } ,
197235 } }
198- srcDoc = { `<div>
236+ >
237+ { showCloseButton && (
238+ < IconButton
239+ onClick = { handleClose }
240+ sx = { {
241+ position : 'absolute' ,
242+ top : CLOSE_BUTTON_SPACING ,
243+ right : CLOSE_BUTTON_OFFSET ,
244+ zIndex : 1 ,
245+ padding : '4px' ,
246+ backgroundColor : 'rgba(255, 255, 255, 0.9)' ,
247+ '&:hover' : {
248+ backgroundColor : 'rgba(255, 255, 255, 1)' ,
249+ } ,
250+ } }
251+ size = "small"
252+ >
253+ < CloseIcon fontSize = "small" />
254+ </ IconButton >
255+ ) }
256+ < Box
257+ sx = { {
258+ maxHeight : `${ POPUP_MAX_HEIGHT } px` ,
259+ overflowY : 'auto' ,
260+ overflowX : 'hidden' ,
261+ } }
262+ >
263+ < iframe
264+ ref = { iframeRef }
265+ onLoad = { handleIframeLoad }
266+ style = { {
267+ width : '100%' ,
268+ border : 'none' ,
269+ display : 'block' ,
270+ ...props . iframeStyle ,
271+ } }
272+ srcDoc = { `<div>
199273 <style>
274+ * {
275+ box-sizing: border-box;
276+ }
200277 body {
278+ margin: 0;
279+ padding: ${ POPUP_PADDING_VERTICAL } px ${ POPUP_PADDING_HORIZONTAL } px;
280+ ${ showCloseButton ? 'padding-top: 40px;' : '' }
281+ background: transparent;
282+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif;
283+ font-size: 14px;
284+ line-height: 1.6;
285+ color: rgba(0, 0, 0, 0.87);
286+ -webkit-font-smoothing: antialiased;
287+ -moz-osx-font-smoothing: grayscale;
288+ overflow-x: hidden;
201289 ${ Object . entries ( props . iframeBodyStyle || { } )
202290 . map ( ( [ key , val ] ) => `${ key . replace ( / ( [ A - Z ] ) / g, '-$1' ) . toLowerCase ( ) } : ${ val } ;` )
203291 . join ( ' ' ) }
204292 }
293+ h1, h2, h3, h4, h5, h6 {
294+ margin: 0 0 8px 0;
295+ font-weight: 500;
296+ }
297+ p {
298+ margin: 0 0 8px 0;
299+ }
300+ table {
301+ border-collapse: collapse;
302+ width: 100%;
303+ max-width: 100%;
304+ }
305+ th, td {
306+ padding: 4px 8px;
307+ text-align: left;
308+ border-bottom: 1px solid rgba(0, 0, 0, 0.12);
309+ word-wrap: break-word;
310+ }
311+ th {
312+ font-weight: 500;
313+ color: rgba(0, 0, 0, 0.6);
314+ }
315+ img {
316+ max-width: 100%;
317+ height: auto;
318+ }
205319 </style>
206320 ${ props . content || '' }
207321</div>` }
208- sandbox = "allow-same-origin allow-popups-to-escape-sandbox allow-scripts"
209- title = { mapHook . componentId }
210- />
322+ sandbox = "allow-same-origin allow-popups-to-escape-sandbox allow-scripts"
323+ title = { mapHook . componentId }
324+ />
325+ </ Box >
326+ </ Paper >
211327 </ Box > ,
212328 container . current
213329 )
0 commit comments