From bba35d646d771cdd419fb512a051585d57f015c7 Mon Sep 17 00:00:00 2001
From: Aliyan Haq <55751566+AliyanH@users.noreply.github.com>
Date: Fri, 3 Jul 2026 15:46:56 -0400
Subject: [PATCH 1/6] Add Legend in layer control settings
---
src/mapml.css | 15 ++-
.../layers/createLayerControlForLayer.js | 31 ++++++-
test/e2e/layers/layerLegend.html | 60 ++++++++++++
test/e2e/layers/layerLegend.test.js | 93 +++++++++++++++++++
4 files changed, 195 insertions(+), 4 deletions(-)
create mode 100644 test/e2e/layers/layerLegend.html
create mode 100644 test/e2e/layers/layerLegend.test.js
diff --git a/src/mapml.css b/src/mapml.css
index 77a7ac794..5fd14a752 100644
--- a/src/mapml.css
+++ b/src/mapml.css
@@ -95,6 +95,19 @@ the browser)' */
color: revert;
}
+.mapml-layer-item-legend-link {
+ display: inline-block;
+ margin-block-start: .25rem;
+}
+
+.mapml-layer-item-legend-image {
+ display: block;
+ max-width: min(100%, 16rem);
+ height: auto;
+ border: 1px solid #e3e3e3;
+ border-radius: 2px;
+}
+
.leaflet-top .leaflet-control {
margin-top: 5px;
}
@@ -804,7 +817,7 @@ label.mapml-layer-item-toggle {
padding-block-start: .25rem;
padding-block-end: .25rem;
padding-inline-start: .25rem;
- padding-inline-end: 1rem;
+ display: inline-block;
}
.mapml-layer-item-settings > * {
diff --git a/src/mapml/elementSupport/layers/createLayerControlForLayer.js b/src/mapml/elementSupport/layers/createLayerControlForLayer.js
index 3a3a0a14d..61a0f5289 100644
--- a/src/mapml/elementSupport/layers/createLayerControlForLayer.js
+++ b/src/mapml/elementSupport/layers/createLayerControlForLayer.js
@@ -145,13 +145,38 @@ export var createLayerControlHTML = async function () {
// to `.href` here. `rel="noopener noreferrer"` closes the
// reverse-tabnabbing and referrer-leak side channels for the
// author-supplied cross-origin destination.
- var legendLink = document.createElement('a');
- legendLink.text = ' ' + this._layer._title;
+ layerItemName.innerText = this._layer._title;
+
+ let legendControl = DomUtil.create(
+ 'details',
+ 'mapml-layer-item-legend mapml-control-layers',
+ layerItemSettings
+ ),
+ legendSummary = DomUtil.create('summary'),
+ legendLink = document.createElement('a'),
+ legendImage = document.createElement('img');
+
+ legendSummary.innerText = mapEl.locale.lmLegend;
+ legendControl.appendChild(legendSummary);
+
legendLink.href = this._layer._legendUrl;
legendLink.target = '_blank';
legendLink.rel = 'noopener noreferrer';
legendLink.draggable = false;
- layerItemName.appendChild(legendLink);
+ legendLink.className = 'mapml-layer-item-legend-link';
+
+ legendImage.src = this._layer._legendUrl;
+ legendImage.alt = `${this._layer._title} legend`;
+ legendImage.loading = 'lazy';
+ legendImage.decoding = 'async';
+ legendImage.className = 'mapml-layer-item-legend-image';
+ legendImage.addEventListener('error', () => {
+ legendLink.textContent = mapEl.locale.lmOpenInNewTab;
+ legendImage.remove();
+ });
+
+ legendLink.appendChild(legendImage);
+ legendControl.appendChild(legendLink);
} else {
// textContent (not innerHTML) so that a malicious layer title
// cannot inject markup into the layer control.
diff --git a/test/e2e/layers/layerLegend.html b/test/e2e/layers/layerLegend.html
new file mode 100644
index 000000000..ea3425f1d
--- /dev/null
+++ b/test/e2e/layers/layerLegend.html
@@ -0,0 +1,60 @@
+
+
+
+
+ Layer Legend Control Tests
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ -75.697193 45.421530
+
+
+
+
+
+
+
diff --git a/test/e2e/layers/layerLegend.test.js b/test/e2e/layers/layerLegend.test.js
new file mode 100644
index 000000000..6339da207
--- /dev/null
+++ b/test/e2e/layers/layerLegend.test.js
@@ -0,0 +1,93 @@
+import { test, expect, chromium } from '@playwright/test';
+
+test.describe('Layer legend tests', () => {
+ let page;
+ let context;
+
+ test.beforeAll(async () => {
+ context = await chromium.launchPersistentContext('', { slowMo: 250 });
+ page = await context.newPage();
+ await page.goto('layerLegend.html');
+ await page.locator('mapml-viewer').hover();
+ });
+
+ test.afterAll(async () => {
+ await context.close();
+ });
+
+ test('Legend layer has legend details in layer settings', async () => {
+ // Get the first layer in the overlay list (the one with an img legend)
+ const layer = page
+ .locator('.leaflet-control-layers-overlays > fieldset')
+ .first();
+
+ // Open the settings for that layer and check that the legend details are present
+ const settings = layer.locator('.mapml-layer-item-settings');
+
+ // Check that the legend details, name, and link are present and correct
+ const legendDetails = settings.locator('details.mapml-layer-item-legend');
+ await expect(legendDetails).toHaveCount(1);
+ await expect(legendDetails.locator('summary')).toHaveText('Legend');
+ await expect(
+ legendDetails.locator('a.mapml-layer-item-legend-link')
+ ).toHaveAttribute(
+ 'href',
+ 'http://maps.geogratis.gc.ca/wms/toporama_en?SERVICE=WMS&REQUEST=GetLegendGraphic&LAYER=WMS-Toporama&VERSION=1.1&FORMAT=image/png'
+ );
+ await expect(
+ legendDetails.locator('img.mapml-layer-item-legend-image')
+ ).toHaveAttribute(
+ 'src',
+ 'http://maps.geogratis.gc.ca/wms/toporama_en?SERVICE=WMS&REQUEST=GetLegendGraphic&LAYER=WMS-Toporama&VERSION=1.1&FORMAT=image/png'
+ );
+
+ // check that the legend details are the second details element in the settings
+ const secondDetails = settings.locator('> details').nth(1);
+ await expect(secondDetails).toHaveClass(
+ 'mapml-layer-item-legend mapml-control-layers'
+ );
+ });
+
+ test('Layer without legend does not render legend details in settings', async () => {
+ // Get the second layer in the overlay list (the one without a legend)
+ const layer = page
+ .locator('.leaflet-control-layers-overlays > fieldset')
+ .nth(1);
+
+ // check that the settings for that layer do not contain any legend details
+ const settings = layer.locator('.mapml-layer-item-settings');
+ await expect(
+ settings.locator('details.mapml-layer-item-legend')
+ ).toHaveCount(0);
+ });
+
+ test('Layer with a non img legend renders a legend link', async () => {
+ // Get the third layer in the overlay list (the one with a non img legend)
+ const layer = page
+ .locator('.leaflet-control-layers-overlays > fieldset')
+ .nth(2);
+
+ // check that the settings for that layer contain a legend link
+ const settings = layer.locator('.mapml-layer-item-settings');
+
+ // Check that the legend details, name, and link are present and correct
+ const legendDetails = settings.locator('details.mapml-layer-item-legend');
+ await expect(legendDetails).toHaveCount(1);
+ await expect(legendDetails.locator('summary')).toHaveText('Legend');
+ await expect(
+ legendDetails.locator('a.mapml-layer-item-legend-link')
+ ).toHaveAttribute('href', 'https://maps4html.org/web-map-doc/');
+
+ // not working, need to manually open the legend for it to update.
+ /*await expect(
+ legendDetails.locator('a.mapml-layer-item-legend-link')
+ ).toHaveText("Open Legend");
+ */
+
+ // check that the legend details are the second details element in the settings
+ const secondDetails = settings.locator('> details').nth(1);
+ await expect(secondDetails).toHaveClass(
+ 'mapml-layer-item-legend mapml-control-layers'
+ );
+ });
+});
From 7173bdd7844d6abfbdc67eea0d41d71f11cdb434 Mon Sep 17 00:00:00 2001
From: Peter Rushforth
Date: Tue, 7 Jul 2026 11:17:35 -0400
Subject: [PATCH 2/6] Refresh package-lock, pin mapml-extension to current main
---
package-lock.json | 4 ++--
package.json | 2 +-
2 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/package-lock.json b/package-lock.json
index 4997940a2..beb36507c 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -28,7 +28,7 @@
"grunt-prettier": "^2.2.0",
"leaflet": "^1.9.4",
"leaflet.locatecontrol": "^0.81.1",
- "mapml-extension": "git+https://github.com/Maps4HTML/mapml-extension",
+ "mapml-extension": "github:Maps4HTML/mapml-extension#main",
"media-query-parser": "^3.0.2",
"media-query-solver": "^0.1.3",
"path": "^0.12.7",
@@ -1904,7 +1904,7 @@
},
"node_modules/mapml-extension": {
"version": "1.0.0",
- "resolved": "git+ssh://git@github.com/Maps4HTML/mapml-extension.git#534fd64c01b736d4ea0383dcf7a0cfefce68026b",
+ "resolved": "git+ssh://git@github.com/Maps4HTML/mapml-extension.git#7a713db8eba1230edd9b08aad1f0936962343ee2",
"dev": true,
"license": "W3C"
},
diff --git a/package.json b/package.json
index 20e695d84..d22682a2f 100644
--- a/package.json
+++ b/package.json
@@ -55,7 +55,7 @@
"grunt-prettier": "^2.2.0",
"leaflet": "^1.9.4",
"leaflet.locatecontrol": "^0.81.1",
- "mapml-extension": "git+https://github.com/Maps4HTML/mapml-extension",
+ "mapml-extension": "github:Maps4HTML/mapml-extension#main",
"media-query-parser": "^3.0.2",
"media-query-solver": "^0.1.3",
"path": "^0.12.7",
From 1020fff5ec90e1558773483653a4eb0e540ce6ed Mon Sep 17 00:00:00 2001
From: Peter Rushforth
Date: Tue, 25 Aug 2026 11:25:20 -0400
Subject: [PATCH 3/6] Bump mapml-extension to latest main
---
package-lock.json | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/package-lock.json b/package-lock.json
index beb36507c..08d75cd78 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1904,7 +1904,7 @@
},
"node_modules/mapml-extension": {
"version": "1.0.0",
- "resolved": "git+ssh://git@github.com/Maps4HTML/mapml-extension.git#7a713db8eba1230edd9b08aad1f0936962343ee2",
+ "resolved": "git+ssh://git@github.com/Maps4HTML/mapml-extension.git#24cede26c81f1f095dd7e24fba4304e6c9752f88",
"dev": true,
"license": "W3C"
},
From 868fc17902f0719acbf76307a324d22209442282 Mon Sep 17 00:00:00 2001
From: Peter Rushforth
Date: Tue, 25 Aug 2026 13:14:28 -0400
Subject: [PATCH 4/6] Update localization test so map-options are generated,
not hard-coded
---
test/e2e/mapml-viewer/localization.html | 12 ++++---
test/e2e/mapml-viewer/localization.test.js | 40 ++++++++++++++++++----
2 files changed, 42 insertions(+), 10 deletions(-)
diff --git a/test/e2e/mapml-viewer/localization.html b/test/e2e/mapml-viewer/localization.html
index 3f70c5ef1..d8d96ae0e 100644
--- a/test/e2e/mapml-viewer/localization.html
+++ b/test/e2e/mapml-viewer/localization.html
@@ -4,11 +4,15 @@
localization.html
- {"locale":{"cmBack":"Повернутися назад","cmForward":"Перейти вперед","cmReload":"Перезавантажити","cmToggleControls":"Увімкнути/вимкнути елементи керування","cmCopyCoords":"Копіювати","cmToggleDebug":"Перемкнути на режим діагностики","cmCopyMapML":"Мапа","cmCopyExtent":"Межа екрану","cmCopyLocation":"Місцеположення","cmPasteLayer":"Вставити","cmViewSource":"Переглянути джерело мапи","lmZoomToLayer":"Збільшити до шару","lmCopyLayer":"Копіювати шар","lmLayerSettings":"Налаштування шару","lmRemoveLayer":"Видалити шар","lmZoomToExtent":"Збільшити до підтипу шару","lmCopyExtent":"Копіювати підтип шару","lmExtentSettings":"Налаштування підшару","lmRemoveExtent":"Видалити підшар","lmStyle":"Стиль","lmLegend":"Легенда","lmOpenInNewTab":"Відкрити легенду","lcOpacity":"Прозорість","btnZoomIn":"Збільшити","btnZoomOut":"Зменшити","btnAttribution":"Авторські права","btnFullScreen":"Повноекранний режим","btnExitFullScreen":"Вихід з повноекранного режиму","btnLocTrackOn":"Показувати моє місцезнаходження - відстеження місцезнаходження увімкнено","btnMyLocTrackOn":"Моє поточне місцезнаходження на карті","btnLocTrackOff":"Показувати моє місцезнаходження - відстеження місцезнаходження вимкнено","btnMyLastKnownLocTrackOn":"Моє останнє відоме місцезнаходження на карті","btnLocTrackLastKnown":"Показати моє місцезнаходження - показано останнє відоме місцезнаходження","btnFocusMap":"Карта Фокусування","btnFocusControls":"Елементи Керування Фокусуванням","btnPrevFeature":"Попередня Функція","btnNextFeature":"Наступна Функція","amZoom":"рівень масштабування","amColumn":"стовпчик","amRow":"рядок","amMaxZoom":"На максимальному рівні масштабування наближення вимкнено","amMinZoom":"На мінімальному рівні масштабування зменшення масштабу вимкнено","amZoomedOut":"Збільшення за межами, повернутись до","amDraggedOut":"Розширення за межами, повернутись до","amEastBound":"Досягнуто східного напрямку, панорамування на схід вимкнено","amWestBound":"Досягнуто західного напрямку, панорамування на захід вимкнено","amNorthBound":"Досягнуто північного напрямку, панорамування на північ вимкнено","amSouthBound":"Досягнуто південного напрямку, панорамування на південь вимкнено","kbdShortcuts":"Комбінації клавіш","kbdMovement":"Клавіші переміщення","kbdFeature":"Функціональні клавіші навігації","kbdPanUp":"Переміщення вгору","kbdPanDown":"Переміщення вниз","kbdPanLeft":"Переміщення ліворуч","kbdPanRight":"Переміщення праворуч","kbdPanIncrement":"крок переміщення зображення","kbdZoom":"Збільшення/зменшення на 3 рівні","kbdFocusMap":"Сфокусувати карту","kbdFocusControls":"Сфокусувати елементи керування","kbdPrevFeature":"Попередній об'єкт","kbdNextFeature":"Наступний об'єкт","dfLayer":"Шар","dfExtent":"Підтип шару","dfPastedLayer":"Вставлений шар","dfFeatureCaption":"Об'єкт","popupZoom":"Наблизити сюди","popupPropName":"Назва властивості","popupPropValue":"Значення властивості","fIndexNoFeatures":"Об'єктів не знайдено","btnSearch":"Пошук","btnSearchClose":"Закрити пошук","searchPlaceholder":"Пошук\u2026","searchResultWithNoName":"Без назви"}}