Repository: symfony/ux
Pull request #2162 (2024-09-14 09:19:36)

[Map] Adding polygons to google and leaflet + info window
| Q             | A
| ------------- | ---
| Bug fix?      | no
| New feature?  | yes
| Issues        | 
| License       | MIT

Hello, 

In my job i use to work a lot with maps, so i would love to work with symfony ux in next projects.
But i have to work with polygons, so i tried to implement it to the ux-map.

I added polygons on the Map component, and made it work on both bridge : google and leaflet.
I added a window popup on click also, the current version of window popup worked only with markers.
I had to rename `createInfoWindow` to `createInfoWindowMarker`. With that i could add a `createInfoWindowPolygon`.
The only difference with the infoWindow from marker is that his anchor will be the event cursor position, because the area of the polygon could be very large and we want a window on the click position.

You can try by adding a simple polygon on your map :
```php
      ->addPolygon(new Polygon(
                points: [
                    new Point(45.7402741, 3.191185),
                    new Point(45.7314078, 3.1903267),
                    new Point(45.7307488, 3.208952),
                    new Point(45.7402741, 3.2112694)
                ],
                rawOptions:[
                    'fillColor' => 'green',
                    'color' => 'green',
                ],
                infoWindow: new InfoWindow(
                    headerContent: '<b>Polygon</b>',


                    content: 'I am a polygon'
                )
            ))
```
![smallgreenrectangle](https://github.com/user-attachments/assets/09282c03-dc7a-499c-b489-44bf56816cb4)

Here a full example that works with leaflet and google that show some of french regions via an api call

```php
$polygons = json_decode(file_get_contents('api_url'), true);
        $polygons = $polygons['features'];
        
        $googleOptions = (new GoogleOptions())
            ->mapId('mapId')
        ;
    
        $map = (new Map('default', $googleOptions))
            ->center(new Point(45.8079318, 3))
            ->zoom(10)
            ;
    
        foreach ($polygons as $polygon) {
            $points = [];
            $title = $polygon['properties']['nom'];
            foreach ($polygon['geometry']['coordinates'][0] as $point) {
                if (count($point) == 2) {
                    $points[] = new Point($point[1], $point[0]);
                }
            }
    
            $map->addPolygon(new Polygon(
                points: $points,
                rawOptions:[
                    'fillColor' => 'red',
                    'color' => 'red',
                ],
                infoWindow: new InfoWindow(
                    headerContent: '<b>'.$title.'</b>',
                    content: 'The French region of '.$title.'.'
                )
            ));
        }
```
![googleregions](https://github.com/user-attachments/assets/511ec6d5-5ab1-4feb-b58f-6b5542e28091)
![leafletregions](https://github.com/user-attachments/assets/f68d18b3-8399-40a9-979f-db039a5298ec)

I added tests for polygons and polygon info window
I hope I did it right, i listen for any suggestions or edits in the PR






smnandre: Thank you very much for this PR @rrr63 👍 

I think @Kocal will lead the review here :) 
rrr63: @Kocal Thank for your comments, i fixed it.
You are right ! It will not break any apps because of `info-window:before-create` & `info-window:after-create`.
And it seems more accessible for future additions
rrr63: However, I wonder because a test did not pass on the Autocomplete component by just changing the Map's changelog ...
rrr63: Weird, it works by adding the changelog : `Polygons added` but didnt work with `Adding polygons` ... :)
Kocal: I've just merged #2117, you will need to rebase your PR and add `polygons` support to `ux_map()` and `<twig:ux:map>` aswell, thanks 🙏🏻 

Feel free to ask help if needed
rrr63: Hello @Kocal 
Any tips on how could i resolved this conflicts ?
Do i have to do it  locally or on github ?

Thanks
smnandre: Locally after a rebase on 2.x 
smnandre: I'm adding a couple of comments :)
rrr63: Oh so sorry,

Do you think it will be easier for you if I create a new PR?
Kocal: I will see what I can do
Kocal: It should be fine, the PR is back to ~10 commits and ~20 files 
<img width="1031" alt="image" src="https://github.com/user-attachments/assets/83c148ab-2c99-4557-885c-882d3c6fd7da">

rrr63: > It should be fine, the PR is back to ~10 commits and ~20 files 
> <img width="1031" alt="image" src="https://github.com/user-attachments/assets/83c148ab-2c99-4557-885c-882d3c6fd7da">
> 

Thanks!!!
rrr63: Yes I could, with pleasure :)
Kocal: I'm taking care of the rest, rebasing and fixing conflicts, testing the feature on my side, and mergiing :)

Patch:
diff --git a/src/Map/CHANGELOG.md b/src/Map/CHANGELOG.md
index de37e53e..3883fe05 100644
--- a/src/Map/CHANGELOG.md
+++ b/src/Map/CHANGELOG.md
@@ -9,6 +9,7 @@
 -   Add `<twig:ux:map />` Twig component
 -   The importmap entry `@symfony/ux-map/abstract-map-controller` can be removed
     from your importmap, it is no longer needed. 
+-   Add `Polygon` support
 
 ## 2.19
 
diff --git a/src/Map/assets/dist/abstract_map_controller.d.ts b/src/Map/assets/dist/abstract_map_controller.d.ts
index 142c2f69..f7f9ffd8 100644
--- a/src/Map/assets/dist/abstract_map_controller.d.ts
+++ b/src/Map/assets/dist/abstract_map_controller.d.ts
@@ -3,11 +3,12 @@ export type Point = {
     lat: number;
     lng: number;
 };
-export type MapView<Options, MarkerOptions, InfoWindowOptions> = {
+export type MapView<Options, MarkerOptions, InfoWindowOptions, PolygonOptions> = {
     center: Point | null;
     zoom: number | null;
     fitBoundsToMarkers: boolean;
     markers: Array<MarkerDefinition<MarkerOptions, InfoWindowOptions>>;
+    polygons: Array<PolygonDefinition<PolygonOptions, InfoWindowOptions>>;
     options: Options;
 };
 export type MarkerDefinition<MarkerOptions, InfoWindowOptions> = {
@@ -17,6 +18,13 @@ export type MarkerDefinition<MarkerOptions, InfoWindowOptions> = {
     rawOptions?: MarkerOptions;
     extra: Record<string, unknown>;
 };
+export type PolygonDefinition<PolygonOptions, InfoWindowOptions> = {
+    infoWindow?: Omit<InfoWindowDefinition<InfoWindowOptions>, 'position'>;
+    points: Array<Point>;
+    title: string | null;
+    rawOptions?: PolygonOptions;
+    extra: Record<string, unknown>;
+};
 export type InfoWindowDefinition<InfoWindowOptions> = {
     headerContent: string | null;
     content: string | null;
@@ -26,15 +34,16 @@ export type InfoWindowDefinition<InfoWindowOptions> = {
     rawOptions?: InfoWindowOptions;
     extra: Record<string, unknown>;
 };
-export default abstract class<MapOptions, Map, MarkerOptions, Marker, InfoWindowOptions, InfoWindow> extends Controller<HTMLElement> {
+export default abstract class<MapOptions, Map, MarkerOptions, Marker, InfoWindowOptions, InfoWindow, PolygonOptions, Polygon> extends Controller<HTMLElement> {
     static values: {
         providerOptions: ObjectConstructor;
         view: ObjectConstructor;
     };
-    viewValue: MapView<MapOptions, MarkerOptions, InfoWindowOptions>;
+    viewValue: MapView<MapOptions, MarkerOptions, InfoWindowOptions, PolygonOptions>;
     protected map: Map;
     protected markers: Array<Marker>;
     protected infoWindows: Array<InfoWindow>;
+    protected polygons: Array<Polygon>;
     connect(): void;
     protected abstract doCreateMap({ center, zoom, options, }: {
         center: Point | null;
@@ -42,14 +51,19 @@ export default abstract class<MapOptions, Map, MarkerOptions, Marker, InfoWindow
         options: MapOptions;
     }): Map;
     createMarker(definition: MarkerDefinition<MarkerOptions, InfoWindowOptions>): Marker;
+    createPolygon(definition: PolygonDefinition<PolygonOptions, InfoWindowOptions>): Polygon;
     protected abstract doCreateMarker(definition: MarkerDefinition<MarkerOptions, InfoWindowOptions>): Marker;
-    protected createInfoWindow({ definition, marker, }: {
-        definition: MarkerDefinition<MarkerOptions, InfoWindowOptions>['infoWindow'];
-        marker: Marker;
+    protected abstract doCreatePolygon(definition: PolygonDefinition<PolygonOptions, InfoWindowOptions>): Polygon;
+    protected createInfoWindow({ definition, element, }: {
+        definition: MarkerDefinition<MarkerOptions, InfoWindowOptions>['infoWindow'] | PolygonDefinition<PolygonOptions, InfoWindowOptions>['infoWindow'];
+        element: Marker | Polygon;
     }): InfoWindow;
-    protected abstract doCreateInfoWindow({ definition, marker, }: {
+    protected abstract doCreateInfoWindow({ definition, element, }: {
         definition: MarkerDefinition<MarkerOptions, InfoWindowOptions>['infoWindow'];
-        marker: Marker;
+        element: Marker;
+    } | {
+        definition: PolygonDefinition<PolygonOptions, InfoWindowOptions>['infoWindow'];
+        element: Polygon;
     }): InfoWindow;
     protected abstract doFitBoundsToMarkers(): void;
     protected abstract dispatchEvent(name: string, payload: Record<string, unknown>): void;
diff --git a/src/Map/assets/dist/abstract_map_controller.js b/src/Map/assets/dist/abstract_map_controller.js
index 9d2e3024..83cc772e 100644
--- a/src/Map/assets/dist/abstract_map_controller.js
+++ b/src/Map/assets/dist/abstract_map_controller.js
@@ -5,18 +5,21 @@ class default_1 extends Controller {
         super(...arguments);
         this.markers = [];
         this.infoWindows = [];
+        this.polygons = [];
     }
     connect() {
-        const { center, zoom, options, markers, fitBoundsToMarkers } = this.viewValue;
+        const { center, zoom, options, markers, polygons, fitBoundsToMarkers } = this.viewValue;
         this.dispatchEvent('pre-connect', { options });
         this.map = this.doCreateMap({ center, zoom, options });
         markers.forEach((marker) => this.createMarker(marker));
+        polygons.forEach((polygon) => this.createPolygon(polygon));
         if (fitBoundsToMarkers) {
             this.doFitBoundsToMarkers();
         }
         this.dispatchEvent('connect', {
             map: this.map,
             markers: this.markers,
+            polygons: this.polygons,
             infoWindows: this.infoWindows,
         });
     }
@@ -27,10 +30,17 @@ class default_1 extends Controller {
         this.markers.push(marker);
         return marker;
     }
-    createInfoWindow({ definition, marker, }) {
-        this.dispatchEvent('info-window:before-create', { definition, marker });
-        const infoWindow = this.doCreateInfoWindow({ definition, marker });
-        this.dispatchEvent('info-window:after-create', { infoWindow, marker });
+    createPolygon(definition) {
+        this.dispatchEvent('polygon:before-create', { definition });
+        const polygon = this.doCreatePolygon(definition);
+        this.dispatchEvent('polygon:after-create', { polygon });
+        this.polygons.push(polygon);
+        return polygon;
+    }
+    createInfoWindow({ definition, element, }) {
+        this.dispatchEvent('info-window:before-create', { definition, element });
+        const infoWindow = this.doCreateInfoWindow({ definition, element });
+        this.dispatchEvent('info-window:after-create', { infoWindow, element });
         this.infoWindows.push(infoWindow);
         return infoWindow;
     }
diff --git a/src/Map/assets/src/abstract_map_controller.ts b/src/Map/assets/src/abstract_map_controller.ts
index 802b6477..bae763cc 100644
--- a/src/Map/assets/src/abstract_map_controller.ts
+++ b/src/Map/assets/src/abstract_map_controller.ts
@@ -2,11 +2,12 @@ import { Controller } from '@hotwired/stimulus';
 
 export type Point = { lat: number; lng: number };
 
-export type MapView<Options, MarkerOptions, InfoWindowOptions> = {
+export type MapView<Options, MarkerOptions, InfoWindowOptions, PolygonOptions> = {
     center: Point | null;
     zoom: number | null;
     fitBoundsToMarkers: boolean;
     markers: Array<MarkerDefinition<MarkerOptions, InfoWindowOptions>>;
+    polygons: Array<PolygonDefinition<PolygonOptions, InfoWindowOptions>>;
     options: Options;
 };
 
@@ -27,6 +28,14 @@ export type MarkerDefinition<MarkerOptions, InfoWindowOptions> = {
     extra: Record<string, unknown>;
 };
 
+export type PolygonDefinition<PolygonOptions, InfoWindowOptions> = {
+    infoWindow?: Omit<InfoWindowDefinition<InfoWindowOptions>, 'position'>;
+    points: Array<Point>;
+    title: string | null;
+    rawOptions?: PolygonOptions;
+    extra: Record<string, unknown>;
+};
+
 export type InfoWindowDefinition<InfoWindowOptions> = {
     headerContent: string | null;
     content: string | null;
@@ -54,20 +63,23 @@ export default abstract class<
     Marker,
     InfoWindowOptions,
     InfoWindow,
+    PolygonOptions,
+    Polygon,
 > extends Controller<HTMLElement> {
     static values = {
         providerOptions: Object,
         view: Object,
     };
 
-    declare viewValue: MapView<MapOptions, MarkerOptions, InfoWindowOptions>;
+    declare viewValue: MapView<MapOptions, MarkerOptions, InfoWindowOptions, PolygonOptions>;
 
     protected map: Map;
     protected markers: Array<Marker> = [];
     protected infoWindows: Array<InfoWindow> = [];
+    protected polygons: Array<Polygon> = [];
 
     connect() {
-        const { center, zoom, options, markers, fitBoundsToMarkers } = this.viewValue;
+        const { center, zoom, options, markers, polygons, fitBoundsToMarkers } = this.viewValue;
 
         this.dispatchEvent('pre-connect', { options });
 
@@ -75,6 +87,8 @@ export default abstract class<
 
         markers.forEach((marker) => this.createMarker(marker));
 
+        polygons.forEach((polygon) => this.createPolygon(polygon));
+
         if (fitBoundsToMarkers) {
             this.doFitBoundsToMarkers();
         }
@@ -82,6 +96,7 @@ export default abstract class<
         this.dispatchEvent('connect', {
             map: this.map,
             markers: this.markers,
+            polygons: this.polygons,
             infoWindows: this.infoWindows,
         });
     }
@@ -106,18 +121,29 @@ export default abstract class<
         return marker;
     }
 
+    createPolygon(definition: PolygonDefinition<PolygonOptions, InfoWindowOptions>): Polygon {
+        this.dispatchEvent('polygon:before-create', { definition });
+        const polygon = this.doCreatePolygon(definition);
+        this.dispatchEvent('polygon:after-create', { polygon });
+        this.polygons.push(polygon);
+        return polygon;
+    }
+
     protected abstract doCreateMarker(definition: MarkerDefinition<MarkerOptions, InfoWindowOptions>): Marker;
+    protected abstract doCreatePolygon(definition: PolygonDefinition<PolygonOptions, InfoWindowOptions>): Polygon;
 
     protected createInfoWindow({
         definition,
-        marker,
+        element,
     }: {
-        definition: MarkerDefinition<MarkerOptions, InfoWindowOptions>['infoWindow'];
-        marker: Marker;
+        definition:
+            | MarkerDefinition<MarkerOptions, InfoWindowOptions>['infoWindow']
+            | PolygonDefinition<PolygonOptions, InfoWindowOptions>['infoWindow'];
+        element: Marker | Polygon;
     }): InfoWindow {
-        this.dispatchEvent('info-window:before-create', { definition, marker });
-        const infoWindow = this.doCreateInfoWindow({ definition, marker });
-        this.dispatchEvent('info-window:after-create', { infoWindow, marker });
+        this.dispatchEvent('info-window:before-create', { definition, element });
+        const infoWindow = this.doCreateInfoWindow({ definition, element });
+        this.dispatchEvent('info-window:after-create', { infoWindow, element });
 
         this.infoWindows.push(infoWindow);
 
@@ -126,11 +152,16 @@ export default abstract class<
 
     protected abstract doCreateInfoWindow({
         definition,
-        marker,
-    }: {
-        definition: MarkerDefinition<MarkerOptions, InfoWindowOptions>['infoWindow'];
-        marker: Marker;
-    }): InfoWindow;
+        element,
+    }:
+        | {
+              definition: MarkerDefinition<MarkerOptions, InfoWindowOptions>['infoWindow'];
+              element: Marker;
+          }
+        | {
+              definition: PolygonDefinition<PolygonOptions, InfoWindowOptions>['infoWindow'];
+              element: Polygon;
+          }): InfoWindow;
 
     protected abstract doFitBoundsToMarkers(): void;
 
diff --git a/src/Map/doc/index.rst b/src/Map/doc/index.rst
index a6d70a80..be16614c 100644
--- a/src/Map/doc/index.rst
+++ b/src/Map/doc/index.rst
@@ -118,8 +118,21 @@ A map is created by calling ``new Map()``. You can configure the center, zoom, a
                     ),
                 )
             ;
-    
-            // 3. And inject the map in your template to render it
+
+            // 3. You can also add Polygons, which represents an area enclosed by a series of `Point` instances
+            $map->addPolygon(new Polygon(
+                points: [
+                    new Point(48.8566, 2.3522),
+                    new Point(45.7640, 4.8357),
+                    new Point(43.2965, 5.3698),
+                    new Point(44.8378, -0.5792),
+                ],
+                infoWindow: new InfoWindow(
+                    content: 'Paris, Lyon, Marseille, Bordeaux',
+                ),
+            ));
+
+            // 4. And inject the map in your template to render it
             return $this->render('contact/index.html.twig', [
                 'my_map' => $myMap,
             ]);
diff --git a/src/Map/src/Bridge/Google/assets/dist/map_controller.d.ts b/src/Map/src/Bridge/Google/assets/dist/map_controller.d.ts
index 8a2d2abc..5095762f 100644
--- a/src/Map/src/Bridge/Google/assets/dist/map_controller.d.ts
+++ b/src/Map/src/Bridge/Google/assets/dist/map_controller.d.ts
@@ -1,8 +1,8 @@
 import AbstractMapController from '@symfony/ux-map';
-import type { Point, MarkerDefinition } from '@symfony/ux-map';
+import type { Point, MarkerDefinition, PolygonDefinition } from '@symfony/ux-map';
 import type { LoaderOptions } from '@googlemaps/js-api-loader';
 type MapOptions = Pick<google.maps.MapOptions, 'mapId' | 'gestureHandling' | 'backgroundColor' | 'disableDoubleClickZoom' | 'zoomControl' | 'zoomControlOptions' | 'mapTypeControl' | 'mapTypeControlOptions' | 'streetViewControl' | 'streetViewControlOptions' | 'fullscreenControl' | 'fullscreenControlOptions'>;
-export default class extends AbstractMapController<MapOptions, google.maps.Map, google.maps.marker.AdvancedMarkerElement, google.maps.InfoWindow> {
+export default class extends AbstractMapController<MapOptions, google.maps.Map, google.maps.marker.AdvancedMarkerElementOptions, google.maps.marker.AdvancedMarkerElement, google.maps.InfoWindowOptions, google.maps.InfoWindow, google.maps.PolygonOptions, google.maps.Polygon> {
     static values: {
         providerOptions: ObjectConstructor;
     };
@@ -15,9 +15,10 @@ export default class extends AbstractMapController<MapOptions, google.maps.Map,
         options: MapOptions;
     }): google.maps.Map;
     protected doCreateMarker(definition: MarkerDefinition<google.maps.marker.AdvancedMarkerElementOptions, google.maps.InfoWindowOptions>): google.maps.marker.AdvancedMarkerElement;
-    protected doCreateInfoWindow({ definition, marker, }: {
-        definition: MarkerDefinition<google.maps.marker.AdvancedMarkerElementOptions, google.maps.InfoWindowOptions>['infoWindow'];
-        marker: google.maps.marker.AdvancedMarkerElement;
+    protected doCreatePolygon(definition: PolygonDefinition<google.maps.Polygon, google.maps.InfoWindowOptions>): google.maps.Polygon;
+    protected doCreateInfoWindow({ definition, element, }: {
+        definition: MarkerDefinition<google.maps.marker.AdvancedMarkerElementOptions, google.maps.InfoWindowOptions>['infoWindow'] | PolygonDefinition<google.maps.Polygon, google.maps.InfoWindowOptions>['infoWindow'];
+        element: google.maps.marker.AdvancedMarkerElement | google.maps.Polygon;
     }): google.maps.InfoWindow;
     private createTextOrElement;
     private closeInfoWindowsExcept;
diff --git a/src/Map/src/Bridge/Google/assets/dist/map_controller.js b/src/Map/src/Bridge/Google/assets/dist/map_controller.js
index d35e6512..30fbe283 100644
--- a/src/Map/src/Bridge/Google/assets/dist/map_controller.js
+++ b/src/Map/src/Bridge/Google/assets/dist/map_controller.js
@@ -6,18 +6,21 @@ let default_1$1 = class default_1 extends Controller {
         super(...arguments);
         this.markers = [];
         this.infoWindows = [];
+        this.polygons = [];
     }
     connect() {
-        const { center, zoom, options, markers, fitBoundsToMarkers } = this.viewValue;
+        const { center, zoom, options, markers, polygons, fitBoundsToMarkers } = this.viewValue;
         this.dispatchEvent('pre-connect', { options });
         this.map = this.doCreateMap({ center, zoom, options });
         markers.forEach((marker) => this.createMarker(marker));
+        polygons.forEach((polygon) => this.createPolygon(polygon));
         if (fitBoundsToMarkers) {
             this.doFitBoundsToMarkers();
         }
         this.dispatchEvent('connect', {
             map: this.map,
             markers: this.markers,
+            polygons: this.polygons,
             infoWindows: this.infoWindows,
         });
     }
@@ -28,10 +31,17 @@ let default_1$1 = class default_1 extends Controller {
         this.markers.push(marker);
         return marker;
     }
-    createInfoWindow({ definition, marker, }) {
-        this.dispatchEvent('info-window:before-create', { definition, marker });
-        const infoWindow = this.doCreateInfoWindow({ definition, marker });
-        this.dispatchEvent('info-window:after-create', { infoWindow, marker });
+    createPolygon(definition) {
+        this.dispatchEvent('polygon:before-create', { definition });
+        const polygon = this.doCreatePolygon(definition);
+        this.dispatchEvent('polygon:after-create', { polygon });
+        this.polygons.push(polygon);
+        return polygon;
+    }
+    createInfoWindow({ definition, element, }) {
+        this.dispatchEvent('info-window:before-create', { definition, element });
+        const infoWindow = this.doCreateInfoWindow({ definition, element });
+        this.dispatchEvent('info-window:after-create', { infoWindow, element });
         this.infoWindows.push(infoWindow);
         return infoWindow;
     }
@@ -92,11 +102,26 @@ class default_1 extends default_1$1 {
             map: this.map,
         });
         if (infoWindow) {
-            this.createInfoWindow({ definition: infoWindow, marker });
+            this.createInfoWindow({ definition: infoWindow, element: marker });
         }
         return marker;
     }
-    doCreateInfoWindow({ definition, marker, }) {
+    doCreatePolygon(definition) {
+        const { points, title, infoWindow, rawOptions = {} } = definition;
+        const polygon = new _google.maps.Polygon({
+            ...rawOptions,
+            paths: points,
+            map: this.map,
+        });
+        if (title) {
+            polygon.set('title', title);
+        }
+        if (infoWindow) {
+            this.createInfoWindow({ definition: infoWindow, element: polygon });
+        }
+        return polygon;
+    }
+    doCreateInfoWindow({ definition, element, }) {
         const { headerContent, content, extra, rawOptions = {}, ...otherOptions } = definition;
         const infoWindow = new _google.maps.InfoWindow({
             headerContent: this.createTextOrElement(headerContent),
@@ -104,22 +129,34 @@ class default_1 extends default_1$1 {
             ...otherOptions,
             ...rawOptions,
         });
-        if (definition.opened) {
-            infoWindow.open({
-                map: this.map,
-                shouldFocus: false,
-                anchor: marker,
+        if (element instanceof google.maps.marker.AdvancedMarkerElement) {
+            element.addListener('click', () => {
+                if (definition.autoClose) {
+                    this.closeInfoWindowsExcept(infoWindow);
+                }
+                infoWindow.open({ map: this.map, anchor: element });
             });
-        }
-        marker.addListener('click', () => {
-            if (definition.autoClose) {
-                this.closeInfoWindowsExcept(infoWindow);
+            if (definition.opened) {
+                infoWindow.open({ map: this.map, anchor: element });
             }
-            infoWindow.open({
-                map: this.map,
-                anchor: marker,
+        }
+        else if (element instanceof google.maps.Polygon) {
+            element.addListener('click', (event) => {
+                if (definition.autoClose) {
+                    this.closeInfoWindowsExcept(infoWindow);
+                }
+                infoWindow.setPosition(event.latLng);
+                infoWindow.open(this.map);
             });
-        });
+            if (definition.opened) {
+                const bounds = new google.maps.LatLngBounds();
+                element.getPath().forEach((point) => {
+                    bounds.extend(point);
+                });
+                infoWindow.setPosition(bounds.getCenter());
+                infoWindow.open({ map: this.map, anchor: element });
+            }
+        }
         return infoWindow;
     }
     createTextOrElement(content) {
diff --git a/src/Map/src/Bridge/Google/assets/src/map_controller.ts b/src/Map/src/Bridge/Google/assets/src/map_controller.ts
index 7eed733f..05116d80 100644
--- a/src/Map/src/Bridge/Google/assets/src/map_controller.ts
+++ b/src/Map/src/Bridge/Google/assets/src/map_controller.ts
@@ -8,7 +8,7 @@
  */
 
 import AbstractMapController from '@symfony/ux-map';
-import type { Point, MarkerDefinition } from '@symfony/ux-map';
+import type { Point, MarkerDefinition, PolygonDefinition } from '@symfony/ux-map';
 import type { LoaderOptions } from '@googlemaps/js-api-loader';
 import { Loader } from '@googlemaps/js-api-loader';
 
@@ -33,8 +33,12 @@ let _google: typeof google;
 export default class extends AbstractMapController<
     MapOptions,
     google.maps.Map,
+    google.maps.marker.AdvancedMarkerElementOptions,
     google.maps.marker.AdvancedMarkerElement,
-    google.maps.InfoWindow
+    google.maps.InfoWindowOptions,
+    google.maps.InfoWindow,
+    google.maps.PolygonOptions,
+    google.maps.Polygon
 > {
     static values = {
         providerOptions: Object,
@@ -121,21 +125,45 @@ export default class extends AbstractMapController<
         });
 
         if (infoWindow) {
-            this.createInfoWindow({ definition: infoWindow, marker });
+            this.createInfoWindow({ definition: infoWindow, element: marker });
         }
 
         return marker;
     }
 
+    protected doCreatePolygon(
+        definition: PolygonDefinition<google.maps.Polygon, google.maps.InfoWindowOptions>
+    ): google.maps.Polygon {
+        const { points, title, infoWindow, rawOptions = {} } = definition;
+
+        const polygon = new _google.maps.Polygon({
+            ...rawOptions,
+            paths: points,
+            map: this.map,
+        });
+
+        if (title) {
+            polygon.set('title', title);
+        }
+
+        if (infoWindow) {
+            this.createInfoWindow({ definition: infoWindow, element: polygon });
+        }
+
+        return polygon;
+    }
+
     protected doCreateInfoWindow({
         definition,
-        marker,
+        element,
     }: {
-        definition: MarkerDefinition<
-            google.maps.marker.AdvancedMarkerElementOptions,
-            google.maps.InfoWindowOptions
-        >['infoWindow'];
-        marker: google.maps.marker.AdvancedMarkerElement;
+        definition:
+            | MarkerDefinition<
+                  google.maps.marker.AdvancedMarkerElementOptions,
+                  google.maps.InfoWindowOptions
+              >['infoWindow']
+            | PolygonDefinition<google.maps.Polygon, google.maps.InfoWindowOptions>['infoWindow'];
+        element: google.maps.marker.AdvancedMarkerElement | google.maps.Polygon;
     }): google.maps.InfoWindow {
         const { headerContent, content, extra, rawOptions = {}, ...otherOptions } = definition;
 
@@ -146,24 +174,35 @@ export default class extends AbstractMapController<
             ...rawOptions,
         });
 
-        if (definition.opened) {
-            infoWindow.open({
-                map: this.map,
-                shouldFocus: false,
-                anchor: marker,
+        if (element instanceof google.maps.marker.AdvancedMarkerElement) {
+            element.addListener('click', () => {
+                if (definition.autoClose) {
+                    this.closeInfoWindowsExcept(infoWindow);
+                }
+                infoWindow.open({ map: this.map, anchor: element });
             });
-        }
 
-        marker.addListener('click', () => {
-            if (definition.autoClose) {
-                this.closeInfoWindowsExcept(infoWindow);
+            if (definition.opened) {
+                infoWindow.open({ map: this.map, anchor: element });
             }
-
-            infoWindow.open({
-                map: this.map,
-                anchor: marker,
+        } else if (element instanceof google.maps.Polygon) {
+            element.addListener('click', (event: any) => {
+                if (definition.autoClose) {
+                    this.closeInfoWindowsExcept(infoWindow);
+                }
+                infoWindow.setPosition(event.latLng);
+                infoWindow.open(this.map);
             });
-        });
+
+            if (definition.opened) {
+                const bounds = new google.maps.LatLngBounds();
+                element.getPath().forEach((point: google.maps.LatLng) => {
+                    bounds.extend(point);
+                });
+                infoWindow.setPosition(bounds.getCenter());
+                infoWindow.open({ map: this.map, anchor: element });
+            }
+        }
 
         return infoWindow;
     }
diff --git a/src/Map/src/Bridge/Leaflet/assets/dist/map_controller.d.ts b/src/Map/src/Bridge/Leaflet/assets/dist/map_controller.d.ts
index 04e533b4..6b32a8df 100644
--- a/src/Map/src/Bridge/Leaflet/assets/dist/map_controller.d.ts
+++ b/src/Map/src/Bridge/Leaflet/assets/dist/map_controller.d.ts
@@ -1,8 +1,8 @@
 import AbstractMapController from '@symfony/ux-map';
-import type { Point, MarkerDefinition } from '@symfony/ux-map';
+import type { Point, MarkerDefinition, PolygonDefinition } from '@symfony/ux-map';
 import 'leaflet/dist/leaflet.min.css';
 import * as L from 'leaflet';
-import type { MapOptions as LeafletMapOptions, MarkerOptions, PopupOptions } from 'leaflet';
+import type { MapOptions as LeafletMapOptions, MarkerOptions, PopupOptions, PolygonOptions } from 'leaflet';
 type MapOptions = Pick<LeafletMapOptions, 'center' | 'zoom'> & {
     tileLayer: {
         url: string;
@@ -10,7 +10,7 @@ type MapOptions = Pick<LeafletMapOptions, 'center' | 'zoom'> & {
         options: Record<string, unknown>;
     };
 };
-export default class extends AbstractMapController<MapOptions, typeof L.Map, MarkerOptions, typeof L.Marker, typeof L.Popup, PopupOptions> {
+export default class extends AbstractMapController<MapOptions, typeof L.Map, MarkerOptions, typeof L.Marker, PopupOptions, typeof L.Popup, PolygonOptions, typeof L.Polygon> {
     connect(): void;
     protected dispatchEvent(name: string, payload?: Record<string, unknown>): void;
     protected doCreateMap({ center, zoom, options, }: {
@@ -19,9 +19,10 @@ export default class extends AbstractMapController<MapOptions, typeof L.Map, Mar
         options: MapOptions;
     }): L.Map;
     protected doCreateMarker(definition: MarkerDefinition): L.Marker;
-    protected doCreateInfoWindow({ definition, marker, }: {
-        definition: MarkerDefinition['infoWindow'];
-        marker: L.Marker;
+    protected doCreatePolygon(definition: PolygonDefinition): L.Polygon;
+    protected doCreateInfoWindow({ definition, element, }: {
+        definition: MarkerDefinition['infoWindow'] | PolygonDefinition['infoWindow'];
+        element: L.Marker | L.Polygon;
     }): L.Popup;
     protected doFitBoundsToMarkers(): void;
 }
diff --git a/src/Map/src/Bridge/Leaflet/assets/dist/map_controller.js b/src/Map/src/Bridge/Leaflet/assets/dist/map_controller.js
index 36331506..6d4a1837 100644
--- a/src/Map/src/Bridge/Leaflet/assets/dist/map_controller.js
+++ b/src/Map/src/Bridge/Leaflet/assets/dist/map_controller.js
@@ -7,18 +7,21 @@ class default_1 extends Controller {
         super(...arguments);
         this.markers = [];
         this.infoWindows = [];
+        this.polygons = [];
     }
     connect() {
-        const { center, zoom, options, markers, fitBoundsToMarkers } = this.viewValue;
+        const { center, zoom, options, markers, polygons, fitBoundsToMarkers } = this.viewValue;
         this.dispatchEvent('pre-connect', { options });
         this.map = this.doCreateMap({ center, zoom, options });
         markers.forEach((marker) => this.createMarker(marker));
+        polygons.forEach((polygon) => this.createPolygon(polygon));
         if (fitBoundsToMarkers) {
             this.doFitBoundsToMarkers();
         }
         this.dispatchEvent('connect', {
             map: this.map,
             markers: this.markers,
+            polygons: this.polygons,
             infoWindows: this.infoWindows,
         });
     }
@@ -29,10 +32,17 @@ class default_1 extends Controller {
         this.markers.push(marker);
         return marker;
     }
-    createInfoWindow({ definition, marker, }) {
-        this.dispatchEvent('info-window:before-create', { definition, marker });
-        const infoWindow = this.doCreateInfoWindow({ definition, marker });
-        this.dispatchEvent('info-window:after-create', { infoWindow, marker });
+    createPolygon(definition) {
+        this.dispatchEvent('polygon:before-create', { definition });
+        const polygon = this.doCreatePolygon(definition);
+        this.dispatchEvent('polygon:after-create', { polygon });
+        this.polygons.push(polygon);
+        return polygon;
+    }
+    createInfoWindow({ definition, element, }) {
+        this.dispatchEvent('info-window:before-create', { definition, element });
+        const infoWindow = this.doCreateInfoWindow({ definition, element });
+        this.dispatchEvent('info-window:after-create', { infoWindow, element });
         this.infoWindows.push(infoWindow);
         return infoWindow;
     }
@@ -78,19 +88,30 @@ class map_controller extends default_1 {
         const { position, title, infoWindow, extra, rawOptions = {}, ...otherOptions } = definition;
         const marker = L.marker(position, { title, ...otherOptions, ...rawOptions }).addTo(this.map);
         if (infoWindow) {
-            this.createInfoWindow({ definition: infoWindow, marker });
+            this.createInfoWindow({ definition: infoWindow, element: marker });
         }
         return marker;
     }
-    doCreateInfoWindow({ definition, marker, }) {
-        const { headerContent, content, extra, rawOptions = {}, ...otherOptions } = definition;
-        marker.bindPopup([headerContent, content].filter((x) => x).join('<br>'), { ...otherOptions, ...rawOptions });
+    doCreatePolygon(definition) {
+        const { points, title, infoWindow, rawOptions = {} } = definition;
+        const polygon = L.polygon(points, { ...rawOptions }).addTo(this.map);
+        if (title) {
+            polygon.bindPopup(title);
+        }
+        if (infoWindow) {
+            this.createInfoWindow({ definition: infoWindow, element: polygon });
+        }
+        return polygon;
+    }
+    doCreateInfoWindow({ definition, element, }) {
+        const { headerContent, content, rawOptions = {}, ...otherOptions } = definition;
+        element.bindPopup([headerContent, content].filter((x) => x).join('<br>'), { ...otherOptions, ...rawOptions });
         if (definition.opened) {
-            marker.openPopup();
+            element.openPopup();
         }
-        const popup = marker.getPopup();
+        const popup = element.getPopup();
         if (!popup) {
-            throw new Error('Unable to get the Popup associated to the Marker, this should not happens.');
+            throw new Error('Unable to get the Popup associated with the element.');
         }
         return popup;
     }
diff --git a/src/Map/src/Bridge/Leaflet/assets/src/map_controller.ts b/src/Map/src/Bridge/Leaflet/assets/src/map_controller.ts
index 3e342b77..12ed1f29 100644
--- a/src/Map/src/Bridge/Leaflet/assets/src/map_controller.ts
+++ b/src/Map/src/Bridge/Leaflet/assets/src/map_controller.ts
@@ -1,8 +1,8 @@
 import AbstractMapController from '@symfony/ux-map';
-import type { Point, MarkerDefinition } from '@symfony/ux-map';
+import type { Point, MarkerDefinition, PolygonDefinition } from '@symfony/ux-map';
 import 'leaflet/dist/leaflet.min.css';
 import * as L from 'leaflet';
-import type { MapOptions as LeafletMapOptions, MarkerOptions, PopupOptions } from 'leaflet';
+import type { MapOptions as LeafletMapOptions, MarkerOptions, PopupOptions, PolygonOptions } from 'leaflet';
 
 type MapOptions = Pick<LeafletMapOptions, 'center' | 'zoom'> & {
     tileLayer: { url: string; attribution: string; options: Record<string, unknown> };
@@ -13,8 +13,10 @@ export default class extends AbstractMapController<
     typeof L.Map,
     MarkerOptions,
     typeof L.Marker,
+    PopupOptions,
     typeof L.Popup,
-    PopupOptions
+    PolygonOptions,
+    typeof L.Polygon
 > {
     connect(): void {
         L.Marker.prototype.options.icon = L.divIcon({
@@ -63,30 +65,48 @@ export default class extends AbstractMapController<
         const marker = L.marker(position, { title, ...otherOptions, ...rawOptions }).addTo(this.map);
 
         if (infoWindow) {
-            this.createInfoWindow({ definition: infoWindow, marker });
+            this.createInfoWindow({ definition: infoWindow, element: marker });
         }
 
         return marker;
     }
 
+    protected doCreatePolygon(definition: PolygonDefinition): L.Polygon {
+        const { points, title, infoWindow, rawOptions = {} } = definition;
+
+        const polygon = L.polygon(points, { ...rawOptions }).addTo(this.map);
+
+        if (title) {
+            polygon.bindPopup(title);
+        }
+
+        if (infoWindow) {
+            this.createInfoWindow({ definition: infoWindow, element: polygon });
+        }
+
+        return polygon;
+    }
+
     protected doCreateInfoWindow({
         definition,
-        marker,
+        element,
     }: {
-        definition: MarkerDefinition['infoWindow'];
-        marker: L.Marker;
+        definition: MarkerDefinition['infoWindow'] | PolygonDefinition['infoWindow'];
+        element: L.Marker | L.Polygon;
     }): L.Popup {
-        const { headerContent, content, extra, rawOptions = {}, ...otherOptions } = definition;
+        const { headerContent, content, rawOptions = {}, ...otherOptions } = definition;
+
+        element.bindPopup([headerContent, content].filter((x) => x).join('<br>'), { ...otherOptions, ...rawOptions });
 
-        marker.bindPopup([headerContent, content].filter((x) => x).join('<br>'), { ...otherOptions, ...rawOptions });
         if (definition.opened) {
-            marker.openPopup();
+            element.openPopup();
         }
 
-        const popup = marker.getPopup();
+        const popup = element.getPopup();
         if (!popup) {
-            throw new Error('Unable to get the Popup associated to the Marker, this should not happens.');
+            throw new Error('Unable to get the Popup associated with the element.');
         }
+
         return popup;
     }
 
diff --git a/src/Map/src/Map.php b/src/Map/src/Map.php
index d8fdf005..3ab240ae 100644
--- a/src/Map/src/Map.php
+++ b/src/Map/src/Map.php
@@ -30,6 +30,11 @@ final class Map
          * @var array<Marker>
          */
         private array $markers = [],
+
+        /**
+         * @var array<Polygon>
+         */
+        private array $polygons = [],
     ) {
     }
 
@@ -83,6 +88,13 @@ final class Map
         return $this;
     }
 
+    public function addPolygon(Polygon $polygon): self
+    {
+        $this->polygons[] = $polygon;
+
+        return $this;
+    }
+
     public function toArray(): array
     {
         if (!$this->fitBoundsToMarkers) {
@@ -101,6 +113,7 @@ final class Map
             'fitBoundsToMarkers' => $this->fitBoundsToMarkers,
             'options' => (object) ($this->options?->toArray() ?? []),
             'markers' => array_map(static fn (Marker $marker) => $marker->toArray(), $this->markers),
+            'polygons' => array_map(static fn (Polygon $polygon) => $polygon->toArray(), $this->polygons),
         ];
     }
 
@@ -109,6 +122,7 @@ final class Map
      *     center?: array{lat: float, lng: float},
      *     zoom?: float,
      *     markers?: list<array>,
+     *     polygons?: list<array>,
      *     fitBoundsToMarkers?: bool,
      *     options?: object,
      * } $map
@@ -133,6 +147,12 @@ final class Map
         }
         $map['markers'] = array_map(Marker::fromArray(...), $map['markers']);
 
+        $map['polygons'] ??= [];
+        if (!\is_array($map['polygons'])) {
+            throw new InvalidArgumentException('The "polygons" parameter must be an array.');
+        }
+        $map['polygons'] = array_map(Polygon::fromArray(...), $map['polygons']);
+
         return new self(...$map);
     }
 }
diff --git a/src/Map/src/Polygon.php b/src/Map/src/Polygon.php
new file mode 100644
index 00000000..5d474346
--- /dev/null
+++ b/src/Map/src/Polygon.php
@@ -0,0 +1,70 @@
+<?php
+
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\UX\Map;
+
+use Symfony\UX\Map\Exception\InvalidArgumentException;
+
+/**
+ * Represents a polygon on a map.
+ *
+ * @author [Pierre Svgnt]
+ */
+final readonly class Polygon
+{
+    /**
+     * @param array<string, mixed> $extra Extra data, can be used by the developer to store additional information and use them later JavaScript side
+     */
+    public function __construct(
+        private array $points,
+        private ?string $title = null,
+        private ?InfoWindow $infoWindow = null,
+        private array $extra = [],
+    ) {
+    }
+
+    /**
+     * Convert the polygon to an array representation.
+     */
+    public function toArray(): array
+    {
+        return [
+            'points' => array_map(fn (Point $point) => $point->toArray(), $this->points),
+            'title' => $this->title,
+            'infoWindow' => $this->infoWindow?->toArray(),
+            'extra' => (object) $this->extra,
+        ];
+    }
+
+    /**
+     * @param array{
+     *     points: array<array{lat: float, lng: float}>,
+     *     title: string|null,
+     *     infoWindow: array<string, mixed>|null,
+     *     extra: object,
+     * } $polygon
+     *
+     * @internal
+     */
+    public static function fromArray(array $polygon): self
+    {
+        if (!isset($polygon['points'])) {
+            throw new InvalidArgumentException('The "points" parameter is required.');
+        }
+        $polygon['points'] = array_map(Point::fromArray(...), $polygon['points']);
+
+        if (isset($polygon['infoWindow'])) {
+            $polygon['infoWindow'] = InfoWindow::fromArray($polygon['infoWindow']);
+        }
+
+        return new self(...$polygon);
+    }
+}
diff --git a/src/Map/src/Twig/MapRuntime.php b/src/Map/src/Twig/MapRuntime.php
index 62e50be8..cfb47560 100644
--- a/src/Map/src/Twig/MapRuntime.php
+++ b/src/Map/src/Twig/MapRuntime.php
@@ -14,6 +14,7 @@ namespace Symfony\UX\Map\Twig;
 use Symfony\UX\Map\Map;
 use Symfony\UX\Map\Marker;
 use Symfony\UX\Map\Point;
+use Symfony\UX\Map\Polygon;
 use Symfony\UX\Map\Renderer\RendererInterface;
 use Twig\Extension\RuntimeExtensionInterface;
 
@@ -32,11 +33,13 @@ final class MapRuntime implements RuntimeExtensionInterface
     /**
      * @param array<string, mixed> $attributes
      * @param array<string, mixed> $markers
+     * @param array<string, mixed> $polygons
      */
     public function renderMap(
         ?Map $map = null,
         array $attributes = [],
         ?array $markers = null,
+        ?array $polygons = null,
         ?array $center = null,
         ?float $zoom = null,
     ): string {
@@ -52,6 +55,9 @@ final class MapRuntime implements RuntimeExtensionInterface
         foreach ($markers ?? [] as $marker) {
             $map->addMarker(Marker::fromArray($marker));
         }
+        foreach ($polygons ?? [] as $polygons) {
+            $map->addPolygon(Polygon::fromArray($polygons));
+        }
         if (null !== $center) {
             $map->center(Point::fromArray($center));
         }
diff --git a/src/Map/src/Twig/UXMapComponent.php b/src/Map/src/Twig/UXMapComponent.php
index 94cb6407..39e362b3 100644
--- a/src/Map/src/Twig/UXMapComponent.php
+++ b/src/Map/src/Twig/UXMapComponent.php
@@ -13,6 +13,7 @@ namespace Symfony\UX\Map\Twig;
 
 use Symfony\UX\Map\Marker;
 use Symfony\UX\Map\Point;
+use Symfony\UX\Map\Polygon;
 
 /**
  * @author Simon André <smn.andre@gmail.com>
@@ -29,4 +30,9 @@ final class UXMapComponent
      * @var Marker[]
      */
     public array $markers;
+
+    /**
+     * @var Polygon[]
+     */
+    public array $polygons;
 }
diff --git a/src/Map/src/Twig/UXMapComponentListener.php b/src/Map/src/Twig/UXMapComponentListener.php
index 3a36e558..51034c53 100644
--- a/src/Map/src/Twig/UXMapComponentListener.php
+++ b/src/Map/src/Twig/UXMapComponentListener.php
@@ -32,7 +32,7 @@ final class UXMapComponentListener
         }
 
         $attributes = $event->getInputProps();
-        $map = array_intersect_key($attributes, ['markers' => 0, 'center' => 1, 'zoom' => 2]);
+        $map = array_intersect_key($attributes, ['markers' => 0, 'polygons' => 0, 'center' => 1, 'zoom' => 2]);
         $attributes = array_diff_key($attributes, $map);
 
         $html = $this->mapRuntime->renderMap(...$map, attributes: $attributes);


Test patch:
diff --git a/src/Map/assets/test/abstract_map_controller.test.ts b/src/Map/assets/test/abstract_map_controller.test.ts
index 0beadef2..c9e0e38a 100644
--- a/src/Map/assets/test/abstract_map_controller.test.ts
+++ b/src/Map/assets/test/abstract_map_controller.test.ts
@@ -20,14 +20,28 @@ class MyMapController extends AbstractMapController {
         const marker = { marker: 'marker', title: definition.title };
 
         if (definition.infoWindow) {
-            this.createInfoWindow({ definition: definition.infoWindow, marker });
+            this.createInfoWindow({ definition: definition.infoWindow, element: marker });
         }
 
         return marker;
     }
 
-    doCreateInfoWindow({ definition, marker }) {
-        return { infoWindow: 'infoWindow', headerContent: definition.headerContent, marker: marker.title };
+    doCreatePolygon(definition) {
+        const polygon = { polygon: 'polygon', title: definition.title };
+
+        if (definition.infoWindow) {
+            this.createInfoWindow({ definition: definition.infoWindow, element: polygon });
+        }
+        return polygon;
+    }
+
+    doCreateInfoWindow({ definition, element }) {
+        if (element.marker) {
+            return { infoWindow: 'infoWindow', headerContent: definition.headerContent, marker: element.title };
+        }
+        if (element.polygon) {
+            return { infoWindow: 'infoWindow', headerContent: definition.headerContent, polygon: element.title };
+        }
     }
 
     doFitBoundsToMarkers() {
@@ -47,12 +61,61 @@ describe('AbstractMapController', () => {
     beforeEach(() => {
         container = mountDOM(`
           <div 
-              data-testid="map" 
-              data-controller="map" 
-              style="height&#x3A;&#x20;700px&#x3B;&#x20;margin&#x3A;&#x20;10px" 
-              data-map-provider-options-value="&#x7B;&#x7D;" 
-              data-map-view-value="&#x7B;&quot;center&quot;&#x3A;&#x7B;&quot;lat&quot;&#x3A;48.8566,&quot;lng&quot;&#x3A;2.3522&#x7D;,&quot;zoom&quot;&#x3A;4,&quot;fitBoundsToMarkers&quot;&#x3A;true,&quot;options&quot;&#x3A;&#x7B;&#x7D;,&quot;markers&quot;&#x3A;&#x5B;&#x7B;&quot;position&quot;&#x3A;&#x7B;&quot;lat&quot;&#x3A;48.8566,&quot;lng&quot;&#x3A;2.3522&#x7D;,&quot;title&quot;&#x3A;&quot;Paris&quot;,&quot;infoWindow&quot;&#x3A;null&#x7D;,&#x7B;&quot;position&quot;&#x3A;&#x7B;&quot;lat&quot;&#x3A;45.764,&quot;lng&quot;&#x3A;4.8357&#x7D;,&quot;title&quot;&#x3A;&quot;Lyon&quot;,&quot;infoWindow&quot;&#x3A;&#x7B;&quot;headerContent&quot;&#x3A;&quot;&lt;b&gt;Lyon&lt;&#x5C;&#x2F;b&gt;&quot;,&quot;content&quot;&#x3A;&quot;The&#x20;French&#x20;town&#x20;in&#x20;the&#x20;historic&#x20;Rh&#x5C;u00f4ne-Alpes&#x20;region,&#x20;located&#x20;at&#x20;the&#x20;junction&#x20;of&#x20;the&#x20;Rh&#x5C;u00f4ne&#x20;and&#x20;Sa&#x5C;u00f4ne&#x20;rivers.&quot;,&quot;position&quot;&#x3A;null,&quot;opened&quot;&#x3A;false,&quot;autoClose&quot;&#x3A;true&#x7D;&#x7D;&#x5D;&#x7D;"
-          ></div>
+            data-testid="map" 
+            data-controller="map" 
+            style="height: 700px; margin: 10px;" 
+            data-map-provider-options-value="{}" 
+            data-map-view-value='{
+                "center": { "lat": 48.8566, "lng": 2.3522 },
+                "zoom": 4,
+                "fitBoundsToMarkers": true,
+                "options": {},
+                "markers": [
+                    {
+                        "position": { "lat": 48.8566, "lng": 2.3522 },
+                        "title": "Paris",
+                        "infoWindow": null
+                    },
+                    {
+                        "position": { "lat": 45.764, "lng": 4.8357 },
+                        "title": "Lyon",
+                        "infoWindow": {
+                            "headerContent": "<b>Lyon</b>",
+                            "content": "The French town in the historic Rhône-Alpes region, located at the junction of the Rhône and Saône rivers.",
+                            "position": null,
+                            "opened": false,
+                            "autoClose": true
+                        }
+                    }
+                ],
+                "polygons": [
+                    {
+                        "coordinates": [
+                            { "lat": 48.858844, "lng": 2.294351 },
+                            { "lat": 48.853, "lng": 2.3499 },
+                            { "lat": 48.8566, "lng": 2.3522 }
+                        ],
+                        "title": "Polygon 1",
+                        "infoWindow": null
+                    },
+                    {
+                        "coordinates": [
+                            { "lat": 45.764043, "lng": 4.835659 },
+                            { "lat": 45.750000, "lng": 4.850000 },
+                            { "lat": 45.770000, "lng": 4.820000 }
+                        ],
+                        "title": "Polygon 2",
+                        "infoWindow": {
+                            "headerContent": "<b>Polygon 2</b>",
+                            "content": "A polygon around Lyon with some additional info.",
+                            "position": null,
+                            "opened": false,
+                            "autoClose": true
+                        }
+                    }
+                ]
+            }'>
+        </div>
         `);
     });
 
@@ -60,7 +123,7 @@ describe('AbstractMapController', () => {
         clearDOM();
     });
 
-    it('connect and create map, marker and info window', async () => {
+    it('connect and create map, marker, polygon and info window', async () => {
         const div = getByTestId(container, 'map');
         expect(div).not.toHaveClass('connected');
 
@@ -73,12 +136,21 @@ describe('AbstractMapController', () => {
             { marker: 'marker', title: 'Paris' },
             { marker: 'marker', title: 'Lyon' },
         ]);
+        expect(controller.polygons).toEqual([
+            { polygon: 'polygon', title: 'Polygon 1' },
+            { polygon: 'polygon', title: 'Polygon 2' },
+        ]);
         expect(controller.infoWindows).toEqual([
             {
                 headerContent: '<b>Lyon</b>',
                 infoWindow: 'infoWindow',
                 marker: 'Lyon',
             },
+            {
+                headerContent: '<b>Polygon 2</b>',
+                infoWindow: 'infoWindow',
+                polygon: 'Polygon 2',
+            },
         ]);
     });
 });
diff --git a/src/Map/src/Bridge/Google/assets/test/map_controller.test.ts b/src/Map/src/Bridge/Google/assets/test/map_controller.test.ts
index 1db8edff..f1b08abb 100644
--- a/src/Map/src/Bridge/Google/assets/test/map_controller.test.ts
+++ b/src/Map/src/Bridge/Google/assets/test/map_controller.test.ts
@@ -41,7 +41,7 @@ describe('GoogleMapsController', () => {
               data-controller="check google" 
               style="height&#x3A;&#x20;700px&#x3B;&#x20;margin&#x3A;&#x20;10px" 
               data-google-provider-options-value="&#x7B;&quot;version&quot;&#x3A;&quot;weekly&quot;,&quot;libraries&quot;&#x3A;&#x5B;&quot;maps&quot;,&quot;marker&quot;&#x5D;,&quot;apiKey&quot;&#x3A;&quot;&quot;&#x7D;" 
-              data-google-view-value="&#x7B;&quot;center&quot;&#x3A;&#x7B;&quot;lat&quot;&#x3A;48.8566,&quot;lng&quot;&#x3A;2.3522&#x7D;,&quot;zoom&quot;&#x3A;4,&quot;fitBoundsToMarkers&quot;&#x3A;true,&quot;options&quot;&#x3A;&#x7B;&quot;mapId&quot;&#x3A;&quot;YOUR_MAP_ID&quot;,&quot;gestureHandling&quot;&#x3A;&quot;auto&quot;,&quot;backgroundColor&quot;&#x3A;null,&quot;disableDoubleClickZoom&quot;&#x3A;false,&quot;zoomControl&quot;&#x3A;true,&quot;zoomControlOptions&quot;&#x3A;&#x7B;&quot;position&quot;&#x3A;22&#x7D;,&quot;mapTypeControl&quot;&#x3A;true,&quot;mapTypeControlOptions&quot;&#x3A;&#x7B;&quot;mapTypeIds&quot;&#x3A;&#x5B;&#x5D;,&quot;position&quot;&#x3A;14,&quot;style&quot;&#x3A;0&#x7D;,&quot;streetViewControl&quot;&#x3A;true,&quot;streetViewControlOptions&quot;&#x3A;&#x7B;&quot;position&quot;&#x3A;22&#x7D;,&quot;fullscreenControl&quot;&#x3A;true,&quot;fullscreenControlOptions&quot;&#x3A;&#x7B;&quot;position&quot;&#x3A;20&#x7D;&#x7D;,&quot;markers&quot;&#x3A;&#x5B;&#x7B;&quot;position&quot;&#x3A;&#x7B;&quot;lat&quot;&#x3A;48.8566,&quot;lng&quot;&#x3A;2.3522&#x7D;,&quot;title&quot;&#x3A;&quot;Paris&quot;,&quot;infoWindow&quot;&#x3A;null&#x7D;,&#x7B;&quot;position&quot;&#x3A;&#x7B;&quot;lat&quot;&#x3A;45.764,&quot;lng&quot;&#x3A;4.8357&#x7D;,&quot;title&quot;&#x3A;&quot;Lyon&quot;,&quot;infoWindow&quot;&#x3A;&#x7B;&quot;headerContent&quot;&#x3A;&quot;&lt;b&gt;Lyon&lt;&#x5C;&#x2F;b&gt;&quot;,&quot;content&quot;&#x3A;&quot;The&#x20;French&#x20;town&#x20;in&#x20;the&#x20;historic&#x20;Rh&#x5C;u00f4ne-Alpes&#x20;region,&#x20;located&#x20;at&#x20;the&#x20;junction&#x20;of&#x20;the&#x20;Rh&#x5C;u00f4ne&#x20;and&#x20;Sa&#x5C;u00f4ne&#x20;rivers.&quot;,&quot;position&quot;&#x3A;null,&quot;opened&quot;&#x3A;false,&quot;autoClose&quot;&#x3A;true&#x7D;&#x7D;&#x5D;&#x7D;"
+              data-google-view-value="&#x7B;&quot;center&quot;&#x3A;&#x7B;&quot;lat&quot;&#x3A;48.8566,&quot;lng&quot;&#x3A;2.3522&#x7D;,&quot;zoom&quot;&#x3A;4,&quot;fitBoundsToMarkers&quot;&#x3A;true,&quot;options&quot;&#x3A;&#x7B;&quot;mapId&quot;&#x3A;&quot;YOUR_MAP_ID&quot;,&quot;gestureHandling&quot;&#x3A;&quot;auto&quot;,&quot;backgroundColor&quot;&#x3A;null,&quot;disableDoubleClickZoom&quot;&#x3A;false,&quot;zoomControl&quot;&#x3A;true,&quot;zoomControlOptions&quot;&#x3A;&#x7B;&quot;position&quot;&#x3A;22&#x7D;,&quot;mapTypeControl&quot;&#x3A;true,&quot;mapTypeControlOptions&quot;&#x3A;&#x7B;&quot;mapTypeIds&quot;&#x3A;&#x5B;&#x5D;,&quot;position&quot;&#x3A;14,&quot;style&quot;&#x3A;0&#x7D;,&quot;streetViewControl&quot;&#x3A;true,&quot;streetViewControlOptions&quot;&#x3A;&#x7B;&quot;position&quot;&#x3A;22&#x7D;,&quot;fullscreenControl&quot;&#x3A;true,&quot;fullscreenControlOptions&quot;&#x3A;&#x7B;&quot;position&quot;&#x3A;20&#x7D;&#x7D;,&quot;markers&quot;&#x3A;&#x5B;&#x7B;&quot;position&quot;&#x3A;&#x7B;&quot;lat&quot;&#x3A;48.8566,&quot;lng&quot;&#x3A;2.3522&#x7D;,&quot;title&quot;&#x3A;&quot;Paris&quot;,&quot;infoWindow&quot;&#x3A;null&#x7D;,&#x7B;&quot;position&quot;&#x3A;&#x7B;&quot;lat&quot;&#x3A;45.764,&quot;lng&quot;&#x3A;4.8357&#x7D;,&quot;title&quot;&#x3A;&quot;Lyon&quot;,&quot;infoWindow&quot;&#x3A;&#x7B;&quot;headerContent&quot;&#x3A;&quot;&lt;b&gt;Lyon&lt;&#x5C;&#x2F;b&gt;&quot;,&quot;content&quot;&#x3A;&quot;The&#x20;French&#x20;town&#x20;in&#x20;the&#x20;historic&#x20;Rh&#x5C;u00f4ne-Alpes&#x20;region,&#x20;located&#x20;at&#x20;the&#x20;junction&#x20;of&#x20;the&#x20;Rh&#x5C;u00f4ne&#x20;and&#x20;Sa&#x5C;u00f4ne&#x20;rivers.&quot;,&quot;position&quot;&#x3A;null,&quot;opened&quot;&#x3A;false,&quot;autoClose&quot;&#x3A;true&#x7D;&#x7D;&#x5D;,&quot;polygons&quot;&#x3A;[]&#x7D;"
           ></div>
         `);
     });
diff --git a/src/Map/src/Bridge/Google/tests/GoogleRendererTest.php b/src/Map/src/Bridge/Google/tests/GoogleRendererTest.php
index db011e30..32ca96df 100644
--- a/src/Map/src/Bridge/Google/tests/GoogleRendererTest.php
+++ b/src/Map/src/Bridge/Google/tests/GoogleRendererTest.php
@@ -29,26 +29,26 @@ class GoogleRendererTest extends RendererTestCase
             ->zoom(12);
 
         yield 'simple map, with minimum options' => [
-            'expected_render' => '<div data-controller="symfony--ux-google-map--map" data-symfony--ux-google-map--map-provider-options-value="&#x7B;&quot;apiKey&quot;&#x3A;&quot;api_key&quot;&#x7D;" data-symfony--ux-google-map--map-view-value="&#x7B;&quot;center&quot;&#x3A;&#x7B;&quot;lat&quot;&#x3A;48.8566,&quot;lng&quot;&#x3A;2.3522&#x7D;,&quot;zoom&quot;&#x3A;12,&quot;fitBoundsToMarkers&quot;&#x3A;false,&quot;options&quot;&#x3A;&#x7B;&quot;mapId&quot;&#x3A;null,&quot;gestureHandling&quot;&#x3A;&quot;auto&quot;,&quot;backgroundColor&quot;&#x3A;null,&quot;disableDoubleClickZoom&quot;&#x3A;false,&quot;zoomControlOptions&quot;&#x3A;&#x7B;&quot;position&quot;&#x3A;22&#x7D;,&quot;mapTypeControlOptions&quot;&#x3A;&#x7B;&quot;mapTypeIds&quot;&#x3A;&#x5B;&#x5D;,&quot;position&quot;&#x3A;14,&quot;style&quot;&#x3A;0&#x7D;,&quot;streetViewControlOptions&quot;&#x3A;&#x7B;&quot;position&quot;&#x3A;22&#x7D;,&quot;fullscreenControlOptions&quot;&#x3A;&#x7B;&quot;position&quot;&#x3A;20&#x7D;&#x7D;,&quot;markers&quot;&#x3A;&#x5B;&#x5D;&#x7D;"></div>',
+            'expected_render' => '<div data-controller="symfony--ux-google-map--map" data-symfony--ux-google-map--map-provider-options-value="&#x7B;&quot;apiKey&quot;&#x3A;&quot;api_key&quot;&#x7D;" data-symfony--ux-google-map--map-view-value="&#x7B;&quot;center&quot;&#x3A;&#x7B;&quot;lat&quot;&#x3A;48.8566,&quot;lng&quot;&#x3A;2.3522&#x7D;,&quot;zoom&quot;&#x3A;12,&quot;fitBoundsToMarkers&quot;&#x3A;false,&quot;options&quot;&#x3A;&#x7B;&quot;mapId&quot;&#x3A;null,&quot;gestureHandling&quot;&#x3A;&quot;auto&quot;,&quot;backgroundColor&quot;&#x3A;null,&quot;disableDoubleClickZoom&quot;&#x3A;false,&quot;zoomControlOptions&quot;&#x3A;&#x7B;&quot;position&quot;&#x3A;22&#x7D;,&quot;mapTypeControlOptions&quot;&#x3A;&#x7B;&quot;mapTypeIds&quot;&#x3A;&#x5B;&#x5D;,&quot;position&quot;&#x3A;14,&quot;style&quot;&#x3A;0&#x7D;,&quot;streetViewControlOptions&quot;&#x3A;&#x7B;&quot;position&quot;&#x3A;22&#x7D;,&quot;fullscreenControlOptions&quot;&#x3A;&#x7B;&quot;position&quot;&#x3A;20&#x7D;&#x7D;,&quot;markers&quot;&#x3A;&#x5B;&#x5D;,&quot;polygons&quot;&#x3A;&#x5B;&#x5D;&#x7D;"></div>',
             'renderer' => new GoogleRenderer(new StimulusHelper(null), apiKey: 'api_key'),
             'map' => $map,
         ];
 
         yield 'with every options' => [
-            'expected_render' => '<div data-controller="symfony--ux-google-map--map" data-symfony--ux-google-map--map-provider-options-value="&#x7B;&quot;id&quot;&#x3A;&quot;gmap&quot;,&quot;language&quot;&#x3A;&quot;fr&quot;,&quot;region&quot;&#x3A;&quot;FR&quot;,&quot;nonce&quot;&#x3A;&quot;abcd&quot;,&quot;retries&quot;&#x3A;10,&quot;url&quot;&#x3A;&quot;https&#x3A;&#x5C;&#x2F;&#x5C;&#x2F;maps.googleapis.com&#x5C;&#x2F;maps&#x5C;&#x2F;api&#x5C;&#x2F;js&quot;,&quot;version&quot;&#x3A;&quot;quarterly&quot;,&quot;apiKey&quot;&#x3A;&quot;api_key&quot;&#x7D;" data-symfony--ux-google-map--map-view-value="&#x7B;&quot;center&quot;&#x3A;&#x7B;&quot;lat&quot;&#x3A;48.8566,&quot;lng&quot;&#x3A;2.3522&#x7D;,&quot;zoom&quot;&#x3A;12,&quot;fitBoundsToMarkers&quot;&#x3A;false,&quot;options&quot;&#x3A;&#x7B;&quot;mapId&quot;&#x3A;null,&quot;gestureHandling&quot;&#x3A;&quot;auto&quot;,&quot;backgroundColor&quot;&#x3A;null,&quot;disableDoubleClickZoom&quot;&#x3A;false,&quot;zoomControlOptions&quot;&#x3A;&#x7B;&quot;position&quot;&#x3A;22&#x7D;,&quot;mapTypeControlOptions&quot;&#x3A;&#x7B;&quot;mapTypeIds&quot;&#x3A;&#x5B;&#x5D;,&quot;position&quot;&#x3A;14,&quot;style&quot;&#x3A;0&#x7D;,&quot;streetViewControlOptions&quot;&#x3A;&#x7B;&quot;position&quot;&#x3A;22&#x7D;,&quot;fullscreenControlOptions&quot;&#x3A;&#x7B;&quot;position&quot;&#x3A;20&#x7D;&#x7D;,&quot;markers&quot;&#x3A;&#x5B;&#x5D;&#x7D;"></div>',
+            'expected_render' => '<div data-controller="symfony--ux-google-map--map" data-symfony--ux-google-map--map-provider-options-value="&#x7B;&quot;id&quot;&#x3A;&quot;gmap&quot;,&quot;language&quot;&#x3A;&quot;fr&quot;,&quot;region&quot;&#x3A;&quot;FR&quot;,&quot;nonce&quot;&#x3A;&quot;abcd&quot;,&quot;retries&quot;&#x3A;10,&quot;url&quot;&#x3A;&quot;https&#x3A;&#x5C;&#x2F;&#x5C;&#x2F;maps.googleapis.com&#x5C;&#x2F;maps&#x5C;&#x2F;api&#x5C;&#x2F;js&quot;,&quot;version&quot;&#x3A;&quot;quarterly&quot;,&quot;apiKey&quot;&#x3A;&quot;api_key&quot;&#x7D;" data-symfony--ux-google-map--map-view-value="&#x7B;&quot;center&quot;&#x3A;&#x7B;&quot;lat&quot;&#x3A;48.8566,&quot;lng&quot;&#x3A;2.3522&#x7D;,&quot;zoom&quot;&#x3A;12,&quot;fitBoundsToMarkers&quot;&#x3A;false,&quot;options&quot;&#x3A;&#x7B;&quot;mapId&quot;&#x3A;null,&quot;gestureHandling&quot;&#x3A;&quot;auto&quot;,&quot;backgroundColor&quot;&#x3A;null,&quot;disableDoubleClickZoom&quot;&#x3A;false,&quot;zoomControlOptions&quot;&#x3A;&#x7B;&quot;position&quot;&#x3A;22&#x7D;,&quot;mapTypeControlOptions&quot;&#x3A;&#x7B;&quot;mapTypeIds&quot;&#x3A;&#x5B;&#x5D;,&quot;position&quot;&#x3A;14,&quot;style&quot;&#x3A;0&#x7D;,&quot;streetViewControlOptions&quot;&#x3A;&#x7B;&quot;position&quot;&#x3A;22&#x7D;,&quot;fullscreenControlOptions&quot;&#x3A;&#x7B;&quot;position&quot;&#x3A;20&#x7D;&#x7D;,&quot;markers&quot;&#x3A;&#x5B;&#x5D;,&quot;polygons&quot;&#x3A;&#x5B;&#x5D;&#x7D;"></div>',
             'renderer' => new GoogleRenderer(new StimulusHelper(null), apiKey: 'api_key', id: 'gmap', language: 'fr', region: 'FR', nonce: 'abcd', retries: 10, url: 'https://maps.googleapis.com/maps/api/js', version: 'quarterly'),
             'map' => $map,
         ];
 
         yield 'with custom attributes' => [
-            'expected_render' => '<div data-controller="my-custom-controller symfony--ux-google-map--map" data-symfony--ux-google-map--map-provider-options-value="&#x7B;&quot;apiKey&quot;&#x3A;&quot;api_key&quot;&#x7D;" data-symfony--ux-google-map--map-view-value="&#x7B;&quot;center&quot;&#x3A;&#x7B;&quot;lat&quot;&#x3A;48.8566,&quot;lng&quot;&#x3A;2.3522&#x7D;,&quot;zoom&quot;&#x3A;12,&quot;fitBoundsToMarkers&quot;&#x3A;false,&quot;options&quot;&#x3A;&#x7B;&quot;mapId&quot;&#x3A;null,&quot;gestureHandling&quot;&#x3A;&quot;auto&quot;,&quot;backgroundColor&quot;&#x3A;null,&quot;disableDoubleClickZoom&quot;&#x3A;false,&quot;zoomControlOptions&quot;&#x3A;&#x7B;&quot;position&quot;&#x3A;22&#x7D;,&quot;mapTypeControlOptions&quot;&#x3A;&#x7B;&quot;mapTypeIds&quot;&#x3A;&#x5B;&#x5D;,&quot;position&quot;&#x3A;14,&quot;style&quot;&#x3A;0&#x7D;,&quot;streetViewControlOptions&quot;&#x3A;&#x7B;&quot;position&quot;&#x3A;22&#x7D;,&quot;fullscreenControlOptions&quot;&#x3A;&#x7B;&quot;position&quot;&#x3A;20&#x7D;&#x7D;,&quot;markers&quot;&#x3A;&#x5B;&#x5D;&#x7D;" class="map"></div>',
+            'expected_render' => '<div data-controller="my-custom-controller symfony--ux-google-map--map" data-symfony--ux-google-map--map-provider-options-value="&#x7B;&quot;apiKey&quot;&#x3A;&quot;api_key&quot;&#x7D;" data-symfony--ux-google-map--map-view-value="&#x7B;&quot;center&quot;&#x3A;&#x7B;&quot;lat&quot;&#x3A;48.8566,&quot;lng&quot;&#x3A;2.3522&#x7D;,&quot;zoom&quot;&#x3A;12,&quot;fitBoundsToMarkers&quot;&#x3A;false,&quot;options&quot;&#x3A;&#x7B;&quot;mapId&quot;&#x3A;null,&quot;gestureHandling&quot;&#x3A;&quot;auto&quot;,&quot;backgroundColor&quot;&#x3A;null,&quot;disableDoubleClickZoom&quot;&#x3A;false,&quot;zoomControlOptions&quot;&#x3A;&#x7B;&quot;position&quot;&#x3A;22&#x7D;,&quot;mapTypeControlOptions&quot;&#x3A;&#x7B;&quot;mapTypeIds&quot;&#x3A;&#x5B;&#x5D;,&quot;position&quot;&#x3A;14,&quot;style&quot;&#x3A;0&#x7D;,&quot;streetViewControlOptions&quot;&#x3A;&#x7B;&quot;position&quot;&#x3A;22&#x7D;,&quot;fullscreenControlOptions&quot;&#x3A;&#x7B;&quot;position&quot;&#x3A;20&#x7D;&#x7D;,&quot;markers&quot;&#x3A;&#x5B;&#x5D;,&quot;polygons&quot;&#x3A;&#x5B;&#x5D;&#x7D;" class="map"></div>',
             'renderer' => new GoogleRenderer(new StimulusHelper(null), apiKey: 'api_key'),
             'map' => $map,
             'attributes' => ['data-controller' => 'my-custom-controller', 'class' => 'map'],
         ];
 
         yield 'with markers and infoWindows' => [
-            'expected_render' => '<div data-controller="symfony--ux-google-map--map" data-symfony--ux-google-map--map-provider-options-value="&#x7B;&quot;apiKey&quot;&#x3A;&quot;api_key&quot;&#x7D;" data-symfony--ux-google-map--map-view-value="&#x7B;&quot;center&quot;&#x3A;&#x7B;&quot;lat&quot;&#x3A;48.8566,&quot;lng&quot;&#x3A;2.3522&#x7D;,&quot;zoom&quot;&#x3A;12,&quot;fitBoundsToMarkers&quot;&#x3A;false,&quot;options&quot;&#x3A;&#x7B;&quot;mapId&quot;&#x3A;null,&quot;gestureHandling&quot;&#x3A;&quot;auto&quot;,&quot;backgroundColor&quot;&#x3A;null,&quot;disableDoubleClickZoom&quot;&#x3A;false,&quot;zoomControlOptions&quot;&#x3A;&#x7B;&quot;position&quot;&#x3A;22&#x7D;,&quot;mapTypeControlOptions&quot;&#x3A;&#x7B;&quot;mapTypeIds&quot;&#x3A;&#x5B;&#x5D;,&quot;position&quot;&#x3A;14,&quot;style&quot;&#x3A;0&#x7D;,&quot;streetViewControlOptions&quot;&#x3A;&#x7B;&quot;position&quot;&#x3A;22&#x7D;,&quot;fullscreenControlOptions&quot;&#x3A;&#x7B;&quot;position&quot;&#x3A;20&#x7D;&#x7D;,&quot;markers&quot;&#x3A;&#x5B;&#x7B;&quot;position&quot;&#x3A;&#x7B;&quot;lat&quot;&#x3A;48.8566,&quot;lng&quot;&#x3A;2.3522&#x7D;,&quot;title&quot;&#x3A;&quot;Paris&quot;,&quot;infoWindow&quot;&#x3A;null,&quot;extra&quot;&#x3A;&#x7B;&#x7D;&#x7D;,&#x7B;&quot;position&quot;&#x3A;&#x7B;&quot;lat&quot;&#x3A;48.8566,&quot;lng&quot;&#x3A;2.3522&#x7D;,&quot;title&quot;&#x3A;&quot;Lyon&quot;,&quot;infoWindow&quot;&#x3A;&#x7B;&quot;headerContent&quot;&#x3A;null,&quot;content&quot;&#x3A;&quot;Lyon&quot;,&quot;position&quot;&#x3A;null,&quot;opened&quot;&#x3A;false,&quot;autoClose&quot;&#x3A;true,&quot;extra&quot;&#x3A;&#x7B;&#x7D;&#x7D;,&quot;extra&quot;&#x3A;&#x7B;&#x7D;&#x7D;&#x5D;&#x7D;"></div>',
+            'expected_render' => '<div data-controller="symfony--ux-google-map--map" data-symfony--ux-google-map--map-provider-options-value="&#x7B;&quot;apiKey&quot;&#x3A;&quot;api_key&quot;&#x7D;" data-symfony--ux-google-map--map-view-value="&#x7B;&quot;center&quot;&#x3A;&#x7B;&quot;lat&quot;&#x3A;48.8566,&quot;lng&quot;&#x3A;2.3522&#x7D;,&quot;zoom&quot;&#x3A;12,&quot;fitBoundsToMarkers&quot;&#x3A;false,&quot;options&quot;&#x3A;&#x7B;&quot;mapId&quot;&#x3A;null,&quot;gestureHandling&quot;&#x3A;&quot;auto&quot;,&quot;backgroundColor&quot;&#x3A;null,&quot;disableDoubleClickZoom&quot;&#x3A;false,&quot;zoomControlOptions&quot;&#x3A;&#x7B;&quot;position&quot;&#x3A;22&#x7D;,&quot;mapTypeControlOptions&quot;&#x3A;&#x7B;&quot;mapTypeIds&quot;&#x3A;&#x5B;&#x5D;,&quot;position&quot;&#x3A;14,&quot;style&quot;&#x3A;0&#x7D;,&quot;streetViewControlOptions&quot;&#x3A;&#x7B;&quot;position&quot;&#x3A;22&#x7D;,&quot;fullscreenControlOptions&quot;&#x3A;&#x7B;&quot;position&quot;&#x3A;20&#x7D;&#x7D;,&quot;markers&quot;&#x3A;&#x5B;&#x7B;&quot;position&quot;&#x3A;&#x7B;&quot;lat&quot;&#x3A;48.8566,&quot;lng&quot;&#x3A;2.3522&#x7D;,&quot;title&quot;&#x3A;&quot;Paris&quot;,&quot;infoWindow&quot;&#x3A;null,&quot;extra&quot;&#x3A;&#x7B;&#x7D;&#x7D;,&#x7B;&quot;position&quot;&#x3A;&#x7B;&quot;lat&quot;&#x3A;48.8566,&quot;lng&quot;&#x3A;2.3522&#x7D;,&quot;title&quot;&#x3A;&quot;Lyon&quot;,&quot;infoWindow&quot;&#x3A;&#x7B;&quot;headerContent&quot;&#x3A;null,&quot;content&quot;&#x3A;&quot;Lyon&quot;,&quot;position&quot;&#x3A;null,&quot;opened&quot;&#x3A;false,&quot;autoClose&quot;&#x3A;true,&quot;extra&quot;&#x3A;&#x7B;&#x7D;&#x7D;,&quot;extra&quot;&#x3A;&#x7B;&#x7D;&#x7D;&#x5D;,&quot;polygons&quot;&#x3A;&#x5B;&#x5D;&#x7D;"></div>',
             'renderer' => new GoogleRenderer(new StimulusHelper(null), apiKey: 'api_key'),
             'map' => (clone $map)
                 ->addMarker(new Marker(new Point(48.8566, 2.3522), 'Paris'))
@@ -56,7 +56,7 @@ class GoogleRendererTest extends RendererTestCase
         ];
 
         yield 'with controls enabled' => [
-            'expected_render' => '<div data-controller="symfony--ux-google-map--map" data-symfony--ux-google-map--map-provider-options-value="&#x7B;&quot;apiKey&quot;&#x3A;&quot;api_key&quot;&#x7D;" data-symfony--ux-google-map--map-view-value="&#x7B;&quot;center&quot;&#x3A;&#x7B;&quot;lat&quot;&#x3A;48.8566,&quot;lng&quot;&#x3A;2.3522&#x7D;,&quot;zoom&quot;&#x3A;12,&quot;fitBoundsToMarkers&quot;&#x3A;false,&quot;options&quot;&#x3A;&#x7B;&quot;mapId&quot;&#x3A;null,&quot;gestureHandling&quot;&#x3A;&quot;auto&quot;,&quot;backgroundColor&quot;&#x3A;null,&quot;disableDoubleClickZoom&quot;&#x3A;false,&quot;zoomControlOptions&quot;&#x3A;&#x7B;&quot;position&quot;&#x3A;22&#x7D;,&quot;mapTypeControlOptions&quot;&#x3A;&#x7B;&quot;mapTypeIds&quot;&#x3A;&#x5B;&#x5D;,&quot;position&quot;&#x3A;14,&quot;style&quot;&#x3A;0&#x7D;,&quot;streetViewControlOptions&quot;&#x3A;&#x7B;&quot;position&quot;&#x3A;22&#x7D;,&quot;fullscreenControlOptions&quot;&#x3A;&#x7B;&quot;position&quot;&#x3A;20&#x7D;&#x7D;,&quot;markers&quot;&#x3A;&#x5B;&#x5D;&#x7D;"></div>',
+            'expected_render' => '<div data-controller="symfony--ux-google-map--map" data-symfony--ux-google-map--map-provider-options-value="&#x7B;&quot;apiKey&quot;&#x3A;&quot;api_key&quot;&#x7D;" data-symfony--ux-google-map--map-view-value="&#x7B;&quot;center&quot;&#x3A;&#x7B;&quot;lat&quot;&#x3A;48.8566,&quot;lng&quot;&#x3A;2.3522&#x7D;,&quot;zoom&quot;&#x3A;12,&quot;fitBoundsToMarkers&quot;&#x3A;false,&quot;options&quot;&#x3A;&#x7B;&quot;mapId&quot;&#x3A;null,&quot;gestureHandling&quot;&#x3A;&quot;auto&quot;,&quot;backgroundColor&quot;&#x3A;null,&quot;disableDoubleClickZoom&quot;&#x3A;false,&quot;zoomControlOptions&quot;&#x3A;&#x7B;&quot;position&quot;&#x3A;22&#x7D;,&quot;mapTypeControlOptions&quot;&#x3A;&#x7B;&quot;mapTypeIds&quot;&#x3A;&#x5B;&#x5D;,&quot;position&quot;&#x3A;14,&quot;style&quot;&#x3A;0&#x7D;,&quot;streetViewControlOptions&quot;&#x3A;&#x7B;&quot;position&quot;&#x3A;22&#x7D;,&quot;fullscreenControlOptions&quot;&#x3A;&#x7B;&quot;position&quot;&#x3A;20&#x7D;&#x7D;,&quot;markers&quot;&#x3A;&#x5B;&#x5D;,&quot;polygons&quot;&#x3A;&#x5B;&#x5D;&#x7D;"></div>',
             'renderer' => new GoogleRenderer(new StimulusHelper(null), apiKey: 'api_key'),
             'map' => (clone $map)
                 ->options(new GoogleOptions(
@@ -68,7 +68,7 @@ class GoogleRendererTest extends RendererTestCase
         ];
 
         yield 'without controls enabled' => [
-            'expected_render' => '<div data-controller="symfony--ux-google-map--map" data-symfony--ux-google-map--map-provider-options-value="&#x7B;&quot;apiKey&quot;&#x3A;&quot;api_key&quot;&#x7D;" data-symfony--ux-google-map--map-view-value="&#x7B;&quot;center&quot;&#x3A;&#x7B;&quot;lat&quot;&#x3A;48.8566,&quot;lng&quot;&#x3A;2.3522&#x7D;,&quot;zoom&quot;&#x3A;12,&quot;fitBoundsToMarkers&quot;&#x3A;false,&quot;options&quot;&#x3A;&#x7B;&quot;mapId&quot;&#x3A;null,&quot;gestureHandling&quot;&#x3A;&quot;auto&quot;,&quot;backgroundColor&quot;&#x3A;null,&quot;disableDoubleClickZoom&quot;&#x3A;false&#x7D;,&quot;markers&quot;&#x3A;&#x5B;&#x5D;&#x7D;"></div>',
+            'expected_render' => '<div data-controller="symfony--ux-google-map--map" data-symfony--ux-google-map--map-provider-options-value="&#x7B;&quot;apiKey&quot;&#x3A;&quot;api_key&quot;&#x7D;" data-symfony--ux-google-map--map-view-value="&#x7B;&quot;center&quot;&#x3A;&#x7B;&quot;lat&quot;&#x3A;48.8566,&quot;lng&quot;&#x3A;2.3522&#x7D;,&quot;zoom&quot;&#x3A;12,&quot;fitBoundsToMarkers&quot;&#x3A;false,&quot;options&quot;&#x3A;&#x7B;&quot;mapId&quot;&#x3A;null,&quot;gestureHandling&quot;&#x3A;&quot;auto&quot;,&quot;backgroundColor&quot;&#x3A;null,&quot;disableDoubleClickZoom&quot;&#x3A;false&#x7D;,&quot;markers&quot;&#x3A;&#x5B;&#x5D;,&quot;polygons&quot;&#x3A;&#x5B;&#x5D;&#x7D;"></div>',
             'renderer' => new GoogleRenderer(new StimulusHelper(null), apiKey: 'api_key'),
             'map' => (clone $map)
                 ->options(new GoogleOptions(
diff --git a/src/Map/src/Bridge/Leaflet/assets/test/map_controller.test.ts b/src/Map/src/Bridge/Leaflet/assets/test/map_controller.test.ts
index e6aa9276..5a51bf5f 100644
--- a/src/Map/src/Bridge/Leaflet/assets/test/map_controller.test.ts
+++ b/src/Map/src/Bridge/Leaflet/assets/test/map_controller.test.ts
@@ -36,12 +36,12 @@ describe('LeafletController', () => {
 
     beforeEach(() => {
         container = mountDOM(`
-          <div 
+           <div 
               data-testid="map" 
               data-controller="check leaflet" 
               style="height&#x3A;&#x20;700px&#x3B;&#x20;margin&#x3A;&#x20;10px" 
               data-leaflet-provider-options-value="&#x7B;&#x7D;" 
-              data-leaflet-view-value="&#x7B;&quot;center&quot;&#x3A;&#x7B;&quot;lat&quot;&#x3A;48.8566,&quot;lng&quot;&#x3A;2.3522&#x7D;,&quot;zoom&quot;&#x3A;4,&quot;fitBoundsToMarkers&quot;&#x3A;true,&quot;options&quot;&#x3A;&#x7B;&quot;tileLayer&quot;&#x3A;&#x7B;&quot;url&quot;&#x3A;&quot;https&#x3A;&#x5C;&#x2F;&#x5C;&#x2F;tile.openstreetmap.org&#x5C;&#x2F;&#x7B;z&#x7D;&#x5C;&#x2F;&#x7B;x&#x7D;&#x5C;&#x2F;&#x7B;y&#x7D;.png&quot;,&quot;attribution&quot;&#x3A;&quot;&#x5C;u00a9&#x20;&lt;a&#x20;href&#x3D;&#x5C;&quot;https&#x3A;&#x5C;&#x2F;&#x5C;&#x2F;www.openstreetmap.org&#x5C;&#x2F;copyright&#x5C;&quot;&gt;OpenStreetMap&lt;&#x5C;&#x2F;a&gt;&quot;,&quot;options&quot;&#x3A;&#x7B;&#x7D;&#x7D;&#x7D;,&quot;markers&quot;&#x3A;&#x5B;&#x7B;&quot;position&quot;&#x3A;&#x7B;&quot;lat&quot;&#x3A;48.8566,&quot;lng&quot;&#x3A;2.3522&#x7D;,&quot;title&quot;&#x3A;&quot;Paris&quot;,&quot;infoWindow&quot;&#x3A;null&#x7D;,&#x7B;&quot;position&quot;&#x3A;&#x7B;&quot;lat&quot;&#x3A;45.764,&quot;lng&quot;&#x3A;4.8357&#x7D;,&quot;title&quot;&#x3A;&quot;Lyon&quot;,&quot;infoWindow&quot;&#x3A;&#x7B;&quot;headerContent&quot;&#x3A;&quot;&lt;b&gt;Lyon&lt;&#x5C;&#x2F;b&gt;&quot;,&quot;content&quot;&#x3A;&quot;The&#x20;French&#x20;town&#x20;in&#x20;the&#x20;historic&#x20;Rh&#x5C;u00f4ne-Alpes&#x20;region,&#x20;located&#x20;at&#x20;the&#x20;junction&#x20;of&#x20;the&#x20;Rh&#x5C;u00f4ne&#x20;and&#x20;Sa&#x5C;u00f4ne&#x20;rivers.&quot;,&quot;position&quot;&#x3A;null,&quot;opened&quot;&#x3A;false,&quot;autoClose&quot;&#x3A;true&#x7D;&#x7D;&#x5D;&#x7D;"
+              data-leaflet-view-value="&#x7B;&quot;center&quot;&#x3A;&#x7B;&quot;lat&quot;&#x3A;48.8566,&quot;lng&quot;&#x3A;2.3522&#x7D;,&quot;zoom&quot;&#x3A;4,&quot;fitBoundsToMarkers&quot;&#x3A;true,&quot;options&quot;&#x3A;&#x7B;&quot;tileLayer&quot;&#x3A;&#x7B;&quot;url&quot;&#x3A;&quot;https&#x3A;&#x5C;&#x2F;&#x5C;&#x2F;tile.openstreetmap.org&#x5C;&#x2F;&#x7B;z&#x7D;&#x5C;&#x2F;&#x7B;x&#x7D;&#x5C;&#x2F;&#x7B;y&#x7D;.png&quot;,&quot;attribution&quot;&#x3A;&quot;&#x5C;u00a9&#x20;&lt;a&#x20;href&#x3D;&#x5C;&quot;https&#x3A;&#x5C;&#x2F;&#x5C;&#x2F;www.openstreetmap.org&#x5C;&#x2F;copyright&#x5C;&quot;&gt;OpenStreetMap&lt;&#x5C;&#x2F;a&gt;&quot;,&quot;options&quot;&#x3A;&#x7B;&#x7D;&#x7D;&#x7D;,&quot;markers&quot;&#x3A;&#x5B;&#x7B;&quot;position&quot;&#x3A;&#x7B;&quot;lat&quot;&#x3A;48.8566,&quot;lng&quot;&#x3A;2.3522&#x7D;,&quot;title&quot;&#x3A;&quot;Paris&quot;,&quot;infoWindow&quot;&#x3A;null&#x7D;,&#x7B;&quot;position&quot;&#x3A;&#x7B;&quot;lat&quot;&#x3A;45.764,&quot;lng&quot;&#x3A;4.8357&#x7D;,&quot;title&quot;&#x3A;&quot;Lyon&quot;,&quot;infoWindow&quot;&#x3A;&#x7B;&quot;headerContent&quot;&#x3A;&quot;&lt;b&gt;Lyon&lt;&#x5C;&#x2F;b&gt;&quot;,&quot;content&quot;&#x3A;&quot;The&#x20;French&#x20;town&#x20;in&#x20;the&#x20;historic&#x20;Rh&#x5C;u00f4ne-Alpes&#x20;region,&#x20;located&#x20;at&#x20;the&#x20;junction&#x20;of&#x20;the&#x20;Rh&#x5C;u00f4ne&#x20;and&#x20;Sa&#x5C;u00f4ne&#x20;rivers.&quot;,&quot;position&quot;&#x3A;null,&quot;opened&quot;&#x3A;false,&quot;autoClose&quot;&#x3A;true&#x7D;&#x7D;&#x5D;,&quot;polygons&quot;&#x3A;[]&#x7D;"
           ></div>
         `);
     });
diff --git a/src/Map/src/Bridge/Leaflet/tests/LeafletRendererTest.php b/src/Map/src/Bridge/Leaflet/tests/LeafletRendererTest.php
index 6931f53a..d9ad391c 100644
--- a/src/Map/src/Bridge/Leaflet/tests/LeafletRendererTest.php
+++ b/src/Map/src/Bridge/Leaflet/tests/LeafletRendererTest.php
@@ -28,20 +28,20 @@ class LeafletRendererTest extends RendererTestCase
             ->zoom(12);
 
         yield 'simple map' => [
-            'expected_render' => '<div data-controller="symfony--ux-leaflet-map--map" data-symfony--ux-leaflet-map--map-provider-options-value="&#x7B;&#x7D;" data-symfony--ux-leaflet-map--map-view-value="&#x7B;&quot;center&quot;&#x3A;&#x7B;&quot;lat&quot;&#x3A;48.8566,&quot;lng&quot;&#x3A;2.3522&#x7D;,&quot;zoom&quot;&#x3A;12,&quot;fitBoundsToMarkers&quot;&#x3A;false,&quot;options&quot;&#x3A;&#x7B;&quot;tileLayer&quot;&#x3A;&#x7B;&quot;url&quot;&#x3A;&quot;https&#x3A;&#x5C;&#x2F;&#x5C;&#x2F;tile.openstreetmap.org&#x5C;&#x2F;&#x7B;z&#x7D;&#x5C;&#x2F;&#x7B;x&#x7D;&#x5C;&#x2F;&#x7B;y&#x7D;.png&quot;,&quot;attribution&quot;&#x3A;&quot;&#x5C;u00a9&#x20;&lt;a&#x20;href&#x3D;&#x5C;&quot;https&#x3A;&#x5C;&#x2F;&#x5C;&#x2F;www.openstreetmap.org&#x5C;&#x2F;copyright&#x5C;&quot;&gt;OpenStreetMap&lt;&#x5C;&#x2F;a&gt;&quot;,&quot;options&quot;&#x3A;&#x7B;&#x7D;&#x7D;&#x7D;,&quot;markers&quot;&#x3A;&#x5B;&#x5D;&#x7D;"></div>',
+            'expected_render' => '<div data-controller="symfony--ux-leaflet-map--map" data-symfony--ux-leaflet-map--map-provider-options-value="&#x7B;&#x7D;" data-symfony--ux-leaflet-map--map-view-value="&#x7B;&quot;center&quot;&#x3A;&#x7B;&quot;lat&quot;&#x3A;48.8566,&quot;lng&quot;&#x3A;2.3522&#x7D;,&quot;zoom&quot;&#x3A;12,&quot;fitBoundsToMarkers&quot;&#x3A;false,&quot;options&quot;&#x3A;&#x7B;&quot;tileLayer&quot;&#x3A;&#x7B;&quot;url&quot;&#x3A;&quot;https&#x3A;&#x5C;&#x2F;&#x5C;&#x2F;tile.openstreetmap.org&#x5C;&#x2F;&#x7B;z&#x7D;&#x5C;&#x2F;&#x7B;x&#x7D;&#x5C;&#x2F;&#x7B;y&#x7D;.png&quot;,&quot;attribution&quot;&#x3A;&quot;&#x5C;u00a9&#x20;&lt;a&#x20;href&#x3D;&#x5C;&quot;https&#x3A;&#x5C;&#x2F;&#x5C;&#x2F;www.openstreetmap.org&#x5C;&#x2F;copyright&#x5C;&quot;&gt;OpenStreetMap&lt;&#x5C;&#x2F;a&gt;&quot;,&quot;options&quot;&#x3A;&#x7B;&#x7D;&#x7D;&#x7D;,&quot;markers&quot;&#x3A;&#x5B;&#x5D;,&quot;polygons&quot;&#x3A;&#x5B;&#x5D;&#x7D;"></div>',
             'renderer' => new LeafletRenderer(new StimulusHelper(null)),
             'map' => $map,
         ];
 
         yield 'with custom attributes' => [
-            'expected_render' => '<div data-controller="my-custom-controller symfony--ux-leaflet-map--map" data-symfony--ux-leaflet-map--map-provider-options-value="&#x7B;&#x7D;" data-symfony--ux-leaflet-map--map-view-value="&#x7B;&quot;center&quot;&#x3A;&#x7B;&quot;lat&quot;&#x3A;48.8566,&quot;lng&quot;&#x3A;2.3522&#x7D;,&quot;zoom&quot;&#x3A;12,&quot;fitBoundsToMarkers&quot;&#x3A;false,&quot;options&quot;&#x3A;&#x7B;&quot;tileLayer&quot;&#x3A;&#x7B;&quot;url&quot;&#x3A;&quot;https&#x3A;&#x5C;&#x2F;&#x5C;&#x2F;tile.openstreetmap.org&#x5C;&#x2F;&#x7B;z&#x7D;&#x5C;&#x2F;&#x7B;x&#x7D;&#x5C;&#x2F;&#x7B;y&#x7D;.png&quot;,&quot;attribution&quot;&#x3A;&quot;&#x5C;u00a9&#x20;&lt;a&#x20;href&#x3D;&#x5C;&quot;https&#x3A;&#x5C;&#x2F;&#x5C;&#x2F;www.openstreetmap.org&#x5C;&#x2F;copyright&#x5C;&quot;&gt;OpenStreetMap&lt;&#x5C;&#x2F;a&gt;&quot;,&quot;options&quot;&#x3A;&#x7B;&#x7D;&#x7D;&#x7D;,&quot;markers&quot;&#x3A;&#x5B;&#x5D;&#x7D;" class="map"></div>',
+            'expected_render' => '<div data-controller="my-custom-controller symfony--ux-leaflet-map--map" data-symfony--ux-leaflet-map--map-provider-options-value="&#x7B;&#x7D;" data-symfony--ux-leaflet-map--map-view-value="&#x7B;&quot;center&quot;&#x3A;&#x7B;&quot;lat&quot;&#x3A;48.8566,&quot;lng&quot;&#x3A;2.3522&#x7D;,&quot;zoom&quot;&#x3A;12,&quot;fitBoundsToMarkers&quot;&#x3A;false,&quot;options&quot;&#x3A;&#x7B;&quot;tileLayer&quot;&#x3A;&#x7B;&quot;url&quot;&#x3A;&quot;https&#x3A;&#x5C;&#x2F;&#x5C;&#x2F;tile.openstreetmap.org&#x5C;&#x2F;&#x7B;z&#x7D;&#x5C;&#x2F;&#x7B;x&#x7D;&#x5C;&#x2F;&#x7B;y&#x7D;.png&quot;,&quot;attribution&quot;&#x3A;&quot;&#x5C;u00a9&#x20;&lt;a&#x20;href&#x3D;&#x5C;&quot;https&#x3A;&#x5C;&#x2F;&#x5C;&#x2F;www.openstreetmap.org&#x5C;&#x2F;copyright&#x5C;&quot;&gt;OpenStreetMap&lt;&#x5C;&#x2F;a&gt;&quot;,&quot;options&quot;&#x3A;&#x7B;&#x7D;&#x7D;&#x7D;,&quot;markers&quot;&#x3A;&#x5B;&#x5D;,&quot;polygons&quot;&#x3A;&#x5B;&#x5D;&#x7D;" class="map"></div>',
             'renderer' => new LeafletRenderer(new StimulusHelper(null)),
             'map' => $map,
             'attributes' => ['data-controller' => 'my-custom-controller', 'class' => 'map'],
         ];
 
         yield 'with markers and infoWindows' => [
-            'expected_render' => '<div data-controller="symfony--ux-leaflet-map--map" data-symfony--ux-leaflet-map--map-provider-options-value="&#x7B;&#x7D;" data-symfony--ux-leaflet-map--map-view-value="&#x7B;&quot;center&quot;&#x3A;&#x7B;&quot;lat&quot;&#x3A;48.8566,&quot;lng&quot;&#x3A;2.3522&#x7D;,&quot;zoom&quot;&#x3A;12,&quot;fitBoundsToMarkers&quot;&#x3A;false,&quot;options&quot;&#x3A;&#x7B;&quot;tileLayer&quot;&#x3A;&#x7B;&quot;url&quot;&#x3A;&quot;https&#x3A;&#x5C;&#x2F;&#x5C;&#x2F;tile.openstreetmap.org&#x5C;&#x2F;&#x7B;z&#x7D;&#x5C;&#x2F;&#x7B;x&#x7D;&#x5C;&#x2F;&#x7B;y&#x7D;.png&quot;,&quot;attribution&quot;&#x3A;&quot;&#x5C;u00a9&#x20;&lt;a&#x20;href&#x3D;&#x5C;&quot;https&#x3A;&#x5C;&#x2F;&#x5C;&#x2F;www.openstreetmap.org&#x5C;&#x2F;copyright&#x5C;&quot;&gt;OpenStreetMap&lt;&#x5C;&#x2F;a&gt;&quot;,&quot;options&quot;&#x3A;&#x7B;&#x7D;&#x7D;&#x7D;,&quot;markers&quot;&#x3A;&#x5B;&#x7B;&quot;position&quot;&#x3A;&#x7B;&quot;lat&quot;&#x3A;48.8566,&quot;lng&quot;&#x3A;2.3522&#x7D;,&quot;title&quot;&#x3A;&quot;Paris&quot;,&quot;infoWindow&quot;&#x3A;null,&quot;extra&quot;&#x3A;&#x7B;&#x7D;&#x7D;,&#x7B;&quot;position&quot;&#x3A;&#x7B;&quot;lat&quot;&#x3A;48.8566,&quot;lng&quot;&#x3A;2.3522&#x7D;,&quot;title&quot;&#x3A;&quot;Lyon&quot;,&quot;infoWindow&quot;&#x3A;&#x7B;&quot;headerContent&quot;&#x3A;null,&quot;content&quot;&#x3A;&quot;Lyon&quot;,&quot;position&quot;&#x3A;null,&quot;opened&quot;&#x3A;false,&quot;autoClose&quot;&#x3A;true,&quot;extra&quot;&#x3A;&#x7B;&#x7D;&#x7D;,&quot;extra&quot;&#x3A;&#x7B;&#x7D;&#x7D;&#x5D;&#x7D;"></div>',
+            'expected_render' => '<div data-controller="symfony--ux-leaflet-map--map" data-symfony--ux-leaflet-map--map-provider-options-value="&#x7B;&#x7D;" data-symfony--ux-leaflet-map--map-view-value="&#x7B;&quot;center&quot;&#x3A;&#x7B;&quot;lat&quot;&#x3A;48.8566,&quot;lng&quot;&#x3A;2.3522&#x7D;,&quot;zoom&quot;&#x3A;12,&quot;fitBoundsToMarkers&quot;&#x3A;false,&quot;options&quot;&#x3A;&#x7B;&quot;tileLayer&quot;&#x3A;&#x7B;&quot;url&quot;&#x3A;&quot;https&#x3A;&#x5C;&#x2F;&#x5C;&#x2F;tile.openstreetmap.org&#x5C;&#x2F;&#x7B;z&#x7D;&#x5C;&#x2F;&#x7B;x&#x7D;&#x5C;&#x2F;&#x7B;y&#x7D;.png&quot;,&quot;attribution&quot;&#x3A;&quot;&#x5C;u00a9&#x20;&lt;a&#x20;href&#x3D;&#x5C;&quot;https&#x3A;&#x5C;&#x2F;&#x5C;&#x2F;www.openstreetmap.org&#x5C;&#x2F;copyright&#x5C;&quot;&gt;OpenStreetMap&lt;&#x5C;&#x2F;a&gt;&quot;,&quot;options&quot;&#x3A;&#x7B;&#x7D;&#x7D;&#x7D;,&quot;markers&quot;&#x3A;&#x5B;&#x7B;&quot;position&quot;&#x3A;&#x7B;&quot;lat&quot;&#x3A;48.8566,&quot;lng&quot;&#x3A;2.3522&#x7D;,&quot;title&quot;&#x3A;&quot;Paris&quot;,&quot;infoWindow&quot;&#x3A;null,&quot;extra&quot;&#x3A;&#x7B;&#x7D;&#x7D;,&#x7B;&quot;position&quot;&#x3A;&#x7B;&quot;lat&quot;&#x3A;48.8566,&quot;lng&quot;&#x3A;2.3522&#x7D;,&quot;title&quot;&#x3A;&quot;Lyon&quot;,&quot;infoWindow&quot;&#x3A;&#x7B;&quot;headerContent&quot;&#x3A;null,&quot;content&quot;&#x3A;&quot;Lyon&quot;,&quot;position&quot;&#x3A;null,&quot;opened&quot;&#x3A;false,&quot;autoClose&quot;&#x3A;true,&quot;extra&quot;&#x3A;&#x7B;&#x7D;&#x7D;,&quot;extra&quot;&#x3A;&#x7B;&#x7D;&#x7D;&#x5D;,&quot;polygons&quot;&#x3A;&#x5B;&#x5D;&#x7D;"></div>',
             'renderer' => new LeafletRenderer(new StimulusHelper(null)),
             'map' => (clone $map)
                 ->addMarker(new Marker(new Point(48.8566, 2.3522), 'Paris'))
diff --git a/src/Map/tests/MapFactoryTest.php b/src/Map/tests/MapFactoryTest.php
index a19e2d79..fcff3b05 100644
--- a/src/Map/tests/MapFactoryTest.php
+++ b/src/Map/tests/MapFactoryTest.php
@@ -32,6 +32,13 @@ class MapFactoryTest extends TestCase
         $this->assertSame($array['markers'][0]['title'], $markers[0]['title']);
         $this->assertSame($array['markers'][0]['infoWindow']['headerContent'], $markers[0]['infoWindow']['headerContent']);
         $this->assertSame($array['markers'][0]['infoWindow']['content'], $markers[0]['infoWindow']['content']);
+
+        $this->assertCount(1, $polygons = $map->toArray()['polygons']);
+        $this->assertEquals($array['polygons'][0]['points'], $polygons[0]['points']);
+        $this->assertEquals($array['polygons'][0]['points'], $polygons[0]['points']);
+        $this->assertSame($array['polygons'][0]['title'], $polygons[0]['title']);
+        $this->assertSame($array['polygons'][0]['infoWindow']['headerContent'], $polygons[0]['infoWindow']['headerContent']);
+        $this->assertSame($array['polygons'][0]['infoWindow']['content'], $polygons[0]['infoWindow']['content']);
     }
 
     public function testFromArrayWithInvalidCenter(): void
@@ -76,6 +83,30 @@ class MapFactoryTest extends TestCase
         Map::fromArray($array);
     }
 
+    public function testFromArrayWithInvalidPolygons(): void
+    {
+        $array = self::createMapArray();
+        $array['polygons'] = 'invalid';
+
+        $this->expectException(\InvalidArgumentException::class);
+        $this->expectExceptionMessage('The "polygons" parameter must be an array.');
+        Map::fromArray($array);
+    }
+
+    public function testFromArrayWithInvalidPolygon(): void
+    {
+        $array = self::createMapArray();
+        $array['polygons'] = [
+            [
+                'invalid',
+            ],
+        ];
+
+        $this->expectException(\InvalidArgumentException::class);
+        $this->expectExceptionMessage('The "points" parameter is required.');
+        Map::fromArray($array);
+    }
+
     private static function createMapArray(): array
     {
         return [
@@ -97,6 +128,29 @@ class MapFactoryTest extends TestCase
                     ],
                 ],
             ],
+            'polygons' => [
+                [
+                    'points' => [
+                        [
+                            'lat' => 48.858844,
+                            'lng' => 2.294351,
+                        ],
+                        [
+                            'lat' => 48.853,
+                            'lng' => 2.3499,
+                        ],
+                        [
+                            'lat' => 48.8566,
+                            'lng' => 2.3522,
+                        ],
+                    ],
+                    'title' => 'Polygon 1',
+                    'infoWindow' => [
+                        'headerContent' => 'Polygon 1',
+                        'content' => 'Polygon 1',
+                    ],
+                ],
+            ],
         ];
     }
 }
diff --git a/src/Map/tests/MapTest.php b/src/Map/tests/MapTest.php
index 99a4f1eb..95703724 100644
--- a/src/Map/tests/MapTest.php
+++ b/src/Map/tests/MapTest.php
@@ -18,6 +18,7 @@ use Symfony\UX\Map\Map;
 use Symfony\UX\Map\MapOptionsInterface;
 use Symfony\UX\Map\Marker;
 use Symfony\UX\Map\Point;
+use Symfony\UX\Map\Polygon;
 
 class MapTest extends TestCase
 {
@@ -55,6 +56,7 @@ class MapTest extends TestCase
             'fitBoundsToMarkers' => true,
             'options' => $array['options'],
             'markers' => [],
+            'polygons' => [],
         ], $array);
     }
 
@@ -73,6 +75,7 @@ class MapTest extends TestCase
             'fitBoundsToMarkers' => false,
             'options' => $array['options'],
             'markers' => [],
+            'polygons' => [],
         ], $array);
     }
 
@@ -105,11 +108,36 @@ class MapTest extends TestCase
                 position: new Point(43.2965, 5.3698),
                 title: 'Marseille',
                 infoWindow: new InfoWindow(headerContent: '<b>Marseille</b>', content: 'Marseille', position: new Point(43.2965, 5.3698), opened: true)
-            ));
+            ))
+            ->addPolygon(new Polygon(
+                points: [
+                    new Point(48.858844, 2.294351),
+                    new Point(48.853, 2.3499),
+                    new Point(48.8566, 2.3522),
+                ],
+                title: 'Polygon 1',
+                infoWindow: null,
+            ))
+            ->addPolygon(new Polygon(
+                points: [
+                    new Point(45.764043, 4.835659),
+                    new Point(45.75, 4.85),
+                    new Point(45.77, 4.82),
+                ],
+                title: 'Polygon 2',
+                infoWindow: new InfoWindow(
+                    headerContent: '<b>Polygon 2</b>',
+                    content: 'A polygon around Lyon with some additional info.',
+                    position: new Point(45.764, 4.8357),
+                    opened: true,
+                    autoClose: true,
+                ),
+            ))
+        ;
 
         $array = $map->toArray();
 
-        self::assertSame([
+        self::assertEquals([
             'center' => ['lat' => 48.8566, 'lng' => 2.3522],
             'zoom' => 6.0,
             'fitBoundsToMarkers' => true,
@@ -155,6 +183,35 @@ class MapTest extends TestCase
                     'extra' => $array['markers'][2]['extra'],
                 ],
             ],
+            'polygons' => [
+                [
+                    'points' => [
+                        ['lat' => 48.858844, 'lng' => 2.294351],
+                        ['lat' => 48.853, 'lng' => 2.3499],
+                        ['lat' => 48.8566, 'lng' => 2.3522],
+                    ],
+                    'title' => 'Polygon 1',
+                    'infoWindow' => null,
+                    'extra' => $array['polygons'][0]['extra'],
+                ],
+                [
+                    'points' => [
+                        ['lat' => 45.764043, 'lng' => 4.835659],
+                        ['lat' => 45.75, 'lng' => 4.85],
+                        ['lat' => 45.77, 'lng' => 4.82],
+                    ],
+                    'title' => 'Polygon 2',
+                    'infoWindow' => [
+                        'headerContent' => '<b>Polygon 2</b>',
+                        'content' => 'A polygon around Lyon with some additional info.',
+                        'position' => ['lat' => 45.764, 'lng' => 4.8357],
+                        'opened' => true,
+                        'autoClose' => true,
+                        'extra' => $array['polygons'][1]['infoWindow']['extra'],
+                    ],
+                    'extra' => $array['polygons'][1]['extra'],
+                ],
+            ],
         ], $array);
 
         self::assertSame('roadmap', $array['options']->mapTypeId);