@tpzdsp/next-toolkit 1.12.1 → 1.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,40 +1,36 @@
1
1
  /* eslint-disable no-restricted-syntax */
2
- import { createFocusTrap } from 'focus-trap';
3
- import type { FocusTrap } from 'focus-trap';
2
+ import { createElement } from 'react';
3
+
4
4
  import { Map } from 'ol';
5
5
  import { Control } from 'ol/control';
6
6
  import type { Options as ControlOptions } from 'ol/control/Control';
7
7
  import BaseLayer from 'ol/layer/Base';
8
+ import { createRoot } from 'react-dom/client';
9
+ import type { Root } from 'react-dom/client';
8
10
 
9
- const TIMEOUT = 300; // Match CSS transition duration
10
- const ARIA_LABEL = 'aria-label';
11
+ import { createControlButton } from './createControlButton';
12
+ import { LayerSwitcherPanel } from './LayerSwitcherPanel';
11
13
 
12
14
  const MAP_LOGO =
13
15
  '<path d="M20.5 3l-5.7 2.1-6-2L3.5 5c-.3.1-.5.5-.5.8v15.2c0 .3.2.6.5.7l5.7-2.1 6 2 5.3-1.9c.3-.1.5-.4.5-.7V3.8c0-.3-.2-.6-.5-.8zM10 5.2l4 1.3v12.3l-4-1.3V5.2zm-6 1.1l4-1.4v12.3l-4 1.4V6.3zm16 12.4l-4 1.4V7.8l4-1.4v12.3z"/>';
14
16
 
15
17
  export class LayerSwitcherControl extends Control {
16
18
  map!: Map;
17
- panel!: HTMLElement;
18
19
  liveRegion!: HTMLElement;
19
20
  isOpen = false;
20
- private focusTrap: FocusTrap | null = null;
21
+ private readonly layers: BaseLayer[];
22
+ private reactRoot: Root | null = null;
23
+ private reactContainer: HTMLDivElement | null = null;
24
+ private readonly button!: HTMLButtonElement;
25
+ private isClosingFromPanel = false;
21
26
 
22
27
  constructor(layers: BaseLayer[], options?: ControlOptions) {
23
- const button = document.createElement('button');
24
-
25
- button.setAttribute(ARIA_LABEL, 'Button to toggle layer switcher');
26
- button.setAttribute('title', 'Basemap switcher');
27
- button.setAttribute('aria-expanded', 'false');
28
- button.className = 'ol-layer-switcher-toggle ol-btn';
29
-
30
- const switcherIcon = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
31
-
32
- switcherIcon.setAttribute('width', '20');
33
- switcherIcon.setAttribute('height', '20');
34
- switcherIcon.setAttribute('viewBox', '0 0 24 24');
35
- switcherIcon.setAttribute('fill', 'currentColor');
36
- switcherIcon.innerHTML = MAP_LOGO;
37
- button.appendChild(switcherIcon);
28
+ const button = createControlButton({
29
+ ariaLabel: 'Toggle basemap selector',
30
+ title: 'Basemap selector',
31
+ iconSvg: MAP_LOGO,
32
+ hasPopup: true,
33
+ });
38
34
 
39
35
  const element = document.createElement('div');
40
36
 
@@ -62,206 +58,116 @@ export class LayerSwitcherControl extends Control {
62
58
  target: options?.target,
63
59
  });
64
60
 
65
- // Assign the live region to the class property so it can be used in other functions.
61
+ this.layers = layers;
62
+ this.button = button;
66
63
  this.liveRegion = liveRegion;
67
64
 
68
- this.panel = document.createElement('div');
69
- this.panel.className = 'ol-layer-switcher-panel';
70
- this.panel.setAttribute('role', 'dialog');
71
- this.panel.setAttribute('aria-modal', 'true');
72
- this.panel.setAttribute(ARIA_LABEL, 'Basemap switcher');
73
-
74
- // Create the header for the close button
75
- const header = document.createElement('div');
76
-
77
- header.className = 'ol-layer-switcher-header';
78
-
79
- // Create the close button
80
- const closeBtn = document.createElement('button');
81
-
82
- closeBtn.className = 'ol-layer-switcher-close ol-btn';
83
- closeBtn.setAttribute(ARIA_LABEL, 'Close basemap switcher');
84
- closeBtn.innerHTML = '&times;';
85
- closeBtn.type = 'button';
86
- closeBtn.addEventListener('click', () => {
87
- this.toggleLayerSwitcher();
88
- });
89
- header.appendChild(closeBtn);
90
-
91
- // Create the content area for basemap buttons
92
- const content = document.createElement('div');
93
-
94
- content.className = 'ol-layer-switcher-content';
95
-
96
- // Add a button for each basemap layer
97
- layers.forEach((layer) => {
98
- const img = document.createElement('img');
99
-
100
- img.src = layer.get('image') as string;
101
- img.setAttribute('title', layer.get('name'));
102
- img.setAttribute('alt', `Preview of ${layer.get('name')}`);
103
-
104
- const switcherText = document.createElement('div');
105
-
106
- switcherText.textContent = layer.get('name') as string;
107
-
108
- const btn = document.createElement('button');
109
-
110
- btn.setAttribute('type', 'button');
111
- btn.setAttribute('aria-pressed', btn.classList.contains('active') ? 'true' : 'false');
112
- btn.appendChild(img);
113
- btn.appendChild(switcherText);
114
-
115
- btn.addEventListener('click', () => this.selectBasemap(layer.get('name') as string), false);
116
-
117
- content.appendChild(btn);
118
- });
119
-
120
- // Assemble the panel
121
- this.panel.appendChild(header);
122
- this.panel.appendChild(content);
123
-
124
- // Create focus trap once - will be activated/deactivated as needed
125
- this.focusTrap = createFocusTrap(this.panel, {
126
- clickOutsideDeactivates: (event) => {
127
- // if the user happens to click on the open button again we let the button handle the click event, not the focus trap
128
- if (button.contains(event.target as Node)) {
129
- return true;
130
- }
131
-
132
- this.toggleLayerSwitcher();
133
-
134
- return false;
135
- },
136
- escapeDeactivates: () => {
137
- this.toggleLayerSwitcher();
138
-
139
- return false;
140
- },
141
- returnFocusOnDeactivate: true,
142
- fallbackFocus: () => {
143
- return this.panel;
144
- },
145
- });
65
+ // Create React container
66
+ this.reactContainer = document.createElement('div');
67
+ this.reactRoot = createRoot(this.reactContainer);
146
68
 
147
69
  button.addEventListener('click', this.toggleLayerSwitcher, false);
148
70
  }
149
71
 
150
72
  private getCurrentBasemap(): BaseLayer | undefined {
151
- return this.getMap()
152
- ?.getLayers()
153
- .getArray()
154
- .filter((layer: BaseLayer) => layer.get('basemap') === true)
155
- .find((layer: BaseLayer) => layer.getVisible() === true);
73
+ return this.layers.find((layer: BaseLayer) => layer.getVisible() === true);
156
74
  }
157
75
 
158
76
  private getBasemapByName(layerName: string): BaseLayer | undefined {
159
- return this.getMap()
160
- ?.getLayers()
161
- .getArray()
162
- .find((layer: BaseLayer) => layer.get('name') === layerName);
77
+ return this.layers.find((layer: BaseLayer) => layer.get('name') === layerName);
163
78
  }
164
79
 
165
- private announceBasemapChange(layerName: string) {
166
- if (this.liveRegion) {
167
- this.liveRegion.textContent = `Basemap changed to ${layerName}`;
168
- // Optionally clear the message after a short delay to allow repeated announcements
169
- setTimeout(() => {
170
- if (this.liveRegion) {
171
- this.liveRegion.textContent = '';
172
- }
173
- }, 1000);
174
- }
175
- }
176
-
177
- setMap(map: Map) {
80
+ setMap(map: Map | null) {
178
81
  super.setMap(map);
179
82
 
180
83
  if (map) {
181
- // Ensure we only set the initial active layer when the map is assigned
182
- map.once('rendercomplete', this.setInitialActiveLayer);
84
+ this.map = map;
183
85
  }
184
86
  }
185
87
 
186
- // Arrow function: 'this' is always bound to the class instance
187
- toggleLayerSwitcher = () => {
188
- if (!this.isOpen) {
189
- this.element.appendChild(this.panel);
190
-
191
- requestAnimationFrame(() => {
192
- this.panel.classList.add('open'); // Ensure animation works after adding to DOM
193
-
194
- // Activate the existing focus trap
195
- this.focusTrap?.activate();
196
- });
197
- } else {
198
- this.panel.classList.remove('open');
199
-
200
- // Deactivate focus trap but keep the instance
201
- this.focusTrap?.deactivate();
88
+ toggleLayerSwitcher = (): void => {
89
+ // Prevent double-toggle when panel closes via focus trap
90
+ if (this.isClosingFromPanel) {
91
+ this.isClosingFromPanel = false;
202
92
 
203
- setTimeout(() => {
204
- this.element.removeChild(this.panel);
205
- }, TIMEOUT); // Matches CSS transition time to prevent flickering
93
+ return;
206
94
  }
207
95
 
208
96
  this.isOpen = !this.isOpen;
97
+ this.button.setAttribute('aria-expanded', this.isOpen.toString());
209
98
 
210
- // Update aria-expanded on the toggle button
211
- const toggleBtn = this.element.querySelector('button.ol-layer-switcher-toggle');
212
-
213
- if (toggleBtn) {
214
- toggleBtn.setAttribute('aria-expanded', this.isOpen.toString());
99
+ if (this.isOpen) {
100
+ this.liveRegion.textContent = 'Basemap switcher opened';
101
+ this.renderPanel();
102
+ } else {
103
+ this.liveRegion.textContent = 'Basemap switcher closed';
104
+ this.closePanel();
215
105
  }
216
106
  };
217
107
 
218
- selectBasemap = (layerName: string) => {
219
- const currentBasemap = this.getCurrentBasemap();
220
-
221
- const newBasemap = this.getBasemapByName(layerName);
222
-
223
- currentBasemap?.setVisible(false);
224
- newBasemap?.setVisible(true);
225
-
226
- this.updateActiveButton(layerName); // Update the active button style
227
-
228
- this.announceBasemapChange(layerName);
108
+ private readonly handlePanelClose = (): void => {
109
+ if (this.isOpen) {
110
+ this.isClosingFromPanel = true;
111
+ this.isOpen = false;
112
+ this.button.setAttribute('aria-expanded', 'false');
113
+ this.liveRegion.textContent = 'Basemap switcher closed';
114
+ this.closePanel();
115
+ }
229
116
  };
230
117
 
231
- setInitialActiveLayer = () => {
232
- const map = this.getMap();
233
-
234
- if (!map) {
118
+ private readonly renderPanel = (): void => {
119
+ if (!this.reactRoot || !this.reactContainer) {
235
120
  return;
236
121
  }
237
122
 
238
- const currentBasemap = this.getCurrentBasemap();
239
-
240
- if (currentBasemap) {
241
- const activeLayerName = currentBasemap.get('name');
123
+ const buttonRect = this.button.getBoundingClientRect();
124
+ const activeLayer = this.getCurrentBasemap();
125
+ const activeLayerName = activeLayer ? (activeLayer.get('name') as string) : '';
126
+
127
+ this.reactRoot.render(
128
+ createElement(LayerSwitcherPanel, {
129
+ isOpen: this.isOpen,
130
+ onClose: this.handlePanelClose,
131
+ layers: this.layers,
132
+ activeLayerName,
133
+ onSelectLayer: this.selectBasemap,
134
+ buttonRect,
135
+ }),
136
+ );
137
+ };
242
138
 
243
- this.updateActiveButton(activeLayerName);
139
+ private readonly closePanel = (): void => {
140
+ if (!this.reactRoot) {
141
+ return;
244
142
  }
143
+
144
+ this.reactRoot.render(null);
245
145
  };
246
146
 
247
- updateActiveButton = (layerName: string) => {
248
- const buttons = this.panel.querySelectorAll('button');
147
+ selectBasemap = (layerName: string): void => {
148
+ const currentBasemap = this.getCurrentBasemap();
149
+ const newBasemap = this.getBasemapByName(layerName);
150
+
151
+ currentBasemap?.setVisible(false);
152
+ newBasemap?.setVisible(true);
249
153
 
250
- buttons.forEach((btn: HTMLButtonElement) => {
251
- const isActive = btn.textContent?.trim() === layerName;
154
+ this.liveRegion.textContent = `${layerName} basemap selected`;
252
155
 
253
- btn.classList.toggle('active', isActive);
254
- btn.setAttribute('aria-pressed', isActive ? 'true' : 'false');
255
- });
156
+ // Re-render to update active state
157
+ if (this.isOpen) {
158
+ this.renderPanel();
159
+ }
256
160
  };
257
161
 
258
- // Cleanup method - call this when removing the control
259
- destroy() {
260
- // Deactivate and destroy focus trap
261
- if (this.focusTrap) {
262
- this.focusTrap.deactivate();
162
+ dispose(): void {
163
+ // Clean up React root
164
+ if (this.reactRoot) {
165
+ this.reactRoot.unmount();
166
+ this.reactRoot = null;
263
167
  }
264
168
 
265
- this.focusTrap = null;
169
+ this.reactContainer = null;
170
+
171
+ super.dispose?.();
266
172
  }
267
173
  }
@@ -0,0 +1,173 @@
1
+ 'use client';
2
+
3
+ import { useEffect, useRef, useState } from 'react';
4
+
5
+ import { FocusTrap } from 'focus-trap-react';
6
+ import BaseLayer from 'ol/layer/Base';
7
+ import { createPortal } from 'react-dom';
8
+
9
+ import { cn } from '../utils';
10
+
11
+ type LayerSwitcherPanelProps = {
12
+ isOpen: boolean;
13
+ onClose: () => void;
14
+ layers: BaseLayer[];
15
+ activeLayerName: string | null;
16
+ onSelectLayer: (layerName: string) => void;
17
+ buttonRect: DOMRect | null;
18
+ };
19
+
20
+ export const LayerSwitcherPanel = ({
21
+ isOpen,
22
+ onClose,
23
+ layers,
24
+ activeLayerName,
25
+ onSelectLayer,
26
+ buttonRect,
27
+ }: LayerSwitcherPanelProps) => {
28
+ const [isMounted, setIsMounted] = useState(false);
29
+ const [isTrapActive, setIsTrapActive] = useState(false);
30
+ const panelRef = useRef<HTMLDivElement>(null);
31
+ const closeButtonRef = useRef<HTMLButtonElement>(null);
32
+
33
+ const handleClose = () => {
34
+ setIsTrapActive(false);
35
+ onClose();
36
+ };
37
+
38
+ useEffect(() => {
39
+ setIsMounted(true);
40
+ }, []);
41
+
42
+ useEffect(() => {
43
+ if (isOpen) {
44
+ setIsTrapActive(true);
45
+ } else {
46
+ setIsTrapActive(false);
47
+ }
48
+ }, [isOpen]);
49
+
50
+ if (!isMounted || !isOpen) {
51
+ return null;
52
+ }
53
+
54
+ // Calculate position based on button position
55
+ const top = buttonRect ? buttonRect.bottom + 8 : 118;
56
+ const right = 48; // 3rem = 48px
57
+
58
+ // Calculate max-height to prevent panel from going off-screen
59
+ // Leave 16px padding at the bottom
60
+ const maxHeight = buttonRect
61
+ ? `${window.innerHeight - buttonRect.bottom - 24}px`
62
+ : 'calc(100vh - 150px)';
63
+
64
+ const panelClasses = cn(
65
+ 'fixed bg-white rounded-lg shadow-lg border border-gray-200',
66
+ 'flex flex-col',
67
+ 'p-2 gap-2',
68
+ 'w-[180px] max-w-[250px]',
69
+ 'transition-all duration-300 ease-in-out',
70
+ 'z-[9999]',
71
+ isOpen ? 'opacity-100 translate-x-0' : 'opacity-0 translate-x-full pointer-events-none',
72
+ );
73
+
74
+ const contentClasses = cn(
75
+ 'grid grid-cols-1 gap-2',
76
+ 'flex-1 min-h-0',
77
+ 'overflow-y-auto overflow-x-hidden',
78
+ );
79
+
80
+ return createPortal(
81
+ <FocusTrap
82
+ active={isTrapActive}
83
+ focusTrapOptions={{
84
+ clickOutsideDeactivates: true,
85
+ escapeDeactivates: true,
86
+ onDeactivate: handleClose,
87
+ returnFocusOnDeactivate: true,
88
+ initialFocus: false, // Don't auto-focus close button
89
+ fallbackFocus: () => panelRef.current ?? document.body,
90
+ }}
91
+ >
92
+ {/* We use role="dialog" instead of <dialog> tag for better portal positioning control */}
93
+ <div
94
+ ref={panelRef}
95
+ role="dialog"
96
+ aria-modal="true"
97
+ aria-label="Basemap switcher"
98
+ aria-describedby="layer-switcher-description"
99
+ style={{ top, right, maxHeight }}
100
+ className={panelClasses}
101
+ >
102
+ {/* Screen reader description */}
103
+ <div id="layer-switcher-description" className="sr-only">
104
+ Select a basemap layer. Use arrow keys to navigate between options, Enter or Space to
105
+ select. Press Escape to close.
106
+ </div>
107
+ {/* Header with close button */}
108
+ <div className="flex justify-end flex-shrink-0">
109
+ <button
110
+ ref={closeButtonRef}
111
+ type="button"
112
+ onClick={handleClose}
113
+ aria-label="Close basemap switcher"
114
+ tabIndex={0}
115
+ className="w-8 h-8 flex items-center justify-center rounded text-gray-600 hover:bg-focus
116
+ focus:outline focus:outline-2 focus:outline-focus focus:bg-focus transition-colors"
117
+ >
118
+ <span className="text-xl leading-none" aria-hidden="true">
119
+ &times;
120
+ </span>
121
+ </button>
122
+ </div>
123
+
124
+ {/* Content with basemap buttons */}
125
+ <div className={contentClasses} role="radiogroup" aria-label="Basemap options">
126
+ {layers.map((layer) => {
127
+ const name = layer.get('name') as string;
128
+ const image = layer.get('image') as string;
129
+ const isActive = name === activeLayerName;
130
+
131
+ return (
132
+ <button
133
+ key={name}
134
+ type="button"
135
+ role="radio"
136
+ aria-checked={isActive}
137
+ aria-label={`${name} basemap${isActive ? ', currently selected' : ''}`}
138
+ onClick={() => onSelectLayer(name)}
139
+ className={cn(
140
+ 'flex flex-col items-center justify-start gap-2',
141
+ 'w-full p-2',
142
+ 'border-2 rounded',
143
+ 'transition-all duration-200',
144
+ 'focus:outline focus:outline-2 focus:outline-focus',
145
+ isActive
146
+ ? 'bg-focus text-black border-focus'
147
+ : 'bg-white text-black border-gray-300 hover:bg-focus',
148
+ )}
149
+ >
150
+ <div className="flex items-center justify-center w-full flex-shrink-0">
151
+ <img
152
+ src={image}
153
+ alt={`Preview of ${name}`}
154
+ title={name}
155
+ className={cn(
156
+ 'max-w-[60px] max-h-[60px] w-auto h-auto object-contain border',
157
+ isActive ? 'border-black' : 'grayscale border-black',
158
+ )}
159
+ />
160
+ </div>
161
+
162
+ <div className="text-xs leading-tight text-center w-full flex-shrink-0 px-1">
163
+ {name}
164
+ </div>
165
+ </button>
166
+ );
167
+ })}
168
+ </div>
169
+ </div>
170
+ </FocusTrap>,
171
+ document.body,
172
+ );
173
+ };
@@ -1,13 +1,13 @@
1
1
  'use client';
2
2
 
3
- import { useEffect, useRef, useState } from 'react';
3
+ import { memo, useEffect, useRef, useState } from 'react';
4
4
 
5
5
  import { Map, Overlay, View } from 'ol';
6
- import { Attribution, Control, ScaleLine, Zoom } from 'ol/control';
6
+ import { Attribution, ScaleLine, Zoom } from 'ol/control';
7
7
  import { fromLonLat } from 'ol/proj';
8
8
 
9
9
  import { initializeBasemapLayers } from './basemaps';
10
- import { initializeGeocoder } from './geocoder';
10
+ import { FullScreenControl } from './FullScreenControl';
11
11
  import { LayerSwitcherControl } from './LayerSwitcherControl';
12
12
  import { useMap } from './MapContext';
13
13
  import { Popup } from './Popup';
@@ -16,9 +16,7 @@ import type { PopupDirection } from '../types/map';
16
16
 
17
17
  export type MapComponentProps = {
18
18
  osMapsApiKey?: string;
19
- geocoderUrl: string;
20
19
  basePath: string;
21
- isLoading?: boolean;
22
20
  };
23
21
 
24
22
  const positionTransforms: Record<PopupDirection, string> = {
@@ -40,14 +38,12 @@ const arrowStyles: Record<PopupDirection, string> = {
40
38
  * and basic controls are added to the map. The map component encapsulates
41
39
  * a number of child components, used to interact with the map itself.
42
40
  *
41
+ * Memoized to prevent unnecessary re-renders that could conflict with
42
+ * OpenLayers' imperative DOM manipulation.
43
+ *
43
44
  * @return {*}
44
45
  */
45
- export const MapComponent = ({
46
- osMapsApiKey,
47
- geocoderUrl,
48
- basePath,
49
- isLoading,
50
- }: MapComponentProps) => {
46
+ const MapComponentBase = ({ osMapsApiKey, basePath }: MapComponentProps) => {
51
47
  const [popupFeatures, setPopupFeatures] = useState([]);
52
48
  const [popupCoordinate, setPopupCoordinate] = useState<number[] | null>(null);
53
49
  const [popupPositionClass, setPopupPositionClass] = useState<PopupDirection>('bottom-right');
@@ -88,9 +84,10 @@ export const MapComponent = ({
88
84
  const scaleLine = new ScaleLine({ units: 'metric' });
89
85
  const attribution = new Attribution();
90
86
  const layerSwitcher = new LayerSwitcherControl(layers);
87
+ const fullScreenControl = new FullScreenControl();
91
88
 
92
89
  // Add controls in the desired order
93
- const controls = [mapZoom, layerSwitcher, scaleLine, attribution];
90
+ const controls = [mapZoom, layerSwitcher, fullScreenControl, scaleLine, attribution];
94
91
 
95
92
  const newMap = new Map({
96
93
  target,
@@ -98,36 +95,14 @@ export const MapComponent = ({
98
95
  layers,
99
96
  view: new View({
100
97
  projection: 'EPSG:3857',
101
- // extent: transformExtent(
102
- // [-8.371582, 49.852152, 2.021484, 59.445075],
103
- // 'EPSG:4326',
104
- // 'EPSG:3857',
105
- // ),
106
98
  center: fromLonLat(center),
107
99
  zoom,
108
100
  }),
109
101
  });
110
102
 
111
103
  // Mark the map as initialized to prevent re-initialization
112
- // This is a workaround to avoid re-initializing the map when the component
113
- // re-renders. The map is only initialized once when the component mounts.
114
- // This is important because the map is a singleton and should not be
115
- // re-initialized.
116
104
  mapInitializedRef.current = true;
117
105
 
118
- // Create an instance of the custom provider, passing any options that are
119
- // required
120
- // Only initialize geocoder if API key is available
121
- if (osMapsApiKey) {
122
- try {
123
- const geocoder = initializeGeocoder(osMapsApiKey, geocoderUrl, newMap);
124
-
125
- newMap.addControl(geocoder as Control);
126
- } catch (error) {
127
- console.error('Failed to initialize geocoder:', error);
128
- }
129
- }
130
-
131
106
  // Setup popup overlay
132
107
  const overlay = new Overlay({
133
108
  element: document.getElementById('popup-container') ?? undefined,
@@ -151,7 +126,7 @@ export const MapComponent = ({
151
126
 
152
127
  setPopupFeatures(features);
153
128
  setPopupCoordinate(coordinate);
154
- setPopupPositionClass(direction); // new state we add below
129
+ setPopupPositionClass(direction);
155
130
  overlay.setPosition(event.coordinate);
156
131
  }
157
132
  });
@@ -182,17 +157,6 @@ export const MapComponent = ({
182
157
  // eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex
183
158
  tabIndex={0}
184
159
  >
185
- {isLoading ? (
186
- <div className="absolute inset-0 flex items-center justify-center bg-white/50 z-10">
187
- <div
188
- className="w-9 h-9 border-4 border-gray-200 border-t-blue-500 rounded-full
189
- animate-spin"
190
- />
191
- </div>
192
- ) : null}
193
-
194
- <div className="absolute top-36 z-20" id="geocoder" />
195
-
196
160
  <div
197
161
  className={`absolute z-20 ${positionTransforms[popupPositionClass]}`}
198
162
  id="popup-container"
@@ -211,3 +175,5 @@ export const MapComponent = ({
211
175
  </div>
212
176
  );
213
177
  };
178
+
179
+ export const MapComponent = memo(MapComponentBase);