From edf610fb350a80ec0fc945e6f0c4b82cf1569302 Mon Sep 17 00:00:00 2001 From: David Stone Date: Thu, 9 Apr 2026 12:41:03 +0100 Subject: [PATCH 01/10] Geospatial list - render disabled action links --- src/client/javascripts/geospatial-map.js | 36 ++++++++++++++++++------ src/client/stylesheets/shared.scss | 5 ++++ 2 files changed, 32 insertions(+), 9 deletions(-) diff --git a/src/client/javascripts/geospatial-map.js b/src/client/javascripts/geospatial-map.js index ec8ec3215..f646ee12c 100644 --- a/src/client/javascripts/geospatial-map.js +++ b/src/client/javascripts/geospatial-map.js @@ -164,11 +164,17 @@ export function addFeatureToMap(feature, drawPlugin, map) { * Returns HTML summary list for the features * @param {FeatureCollection} features - the features * @param {string} mapId - the ID of the map + * @param {boolean} [disabled] - render the list with disabled links * @param {boolean} [readonly] - render the list in readonly mode */ -export function createFeaturesHTML(features, mapId, readonly = false) { +export function createFeaturesHTML( + features, + mapId, + disabled = false, + readonly = false +) { return `
- ${features.map((feature, index) => createFeatureHTML(feature, index, mapId, readonly)).join('\n')} + ${features.map((feature, index) => createFeatureHTML(feature, index, mapId, disabled, readonly)).join('\n')}
` } @@ -186,9 +192,16 @@ export function focusFeature(feature, mapProvider) { * @param {Feature} feature - the geo feature * @param {number} index - the feature index * @param {string} mapId - the ID of the map - * @param {boolean} readonly - render the list item in readonly mode + * @param {boolean} [disabled] - render the list with disabled links + * @param {boolean} [readonly] - render the list item in readonly mode */ -function createFeatureHTML(feature, index, mapId, readonly) { +function createFeatureHTML( + feature, + index, + mapId, + disabled = false, + readonly = false +) { const flattened = feature.geometry.coordinates.flat(2) const points = [] @@ -203,13 +216,13 @@ function createFeatureHTML(feature, index, mapId, readonly) { // Change action link const changeAction = () => `
  • - Update location
  • ` // Delete action link const deleteAction = () => `
  • - Delete location
  • ` @@ -415,8 +428,8 @@ function getFeaturesManager(geojson) { * @returns {RenderList} */ function getListRenderer(geojson, mapId, listEl, renderValue) { - return function renderList() { - const html = createFeaturesHTML(geojson.features, mapId) + return function renderList(disabled = false) { + const html = createFeaturesHTML(geojson.features, mapId, disabled) listEl.innerHTML = html @@ -523,7 +536,7 @@ function createContainers(geospatialInput, index) { function onMapReadyFactory(context) { const { map, activeFeatureManager, uiManager, interactPlugin, drawPlugin } = context - const { toggleActionButtons } = uiManager + const { toggleActionButtons, renderList } = uiManager const { resetActiveFeature } = activeFeatureManager /** @@ -542,6 +555,7 @@ function onMapReadyFactory(context) { onClick: () => { resetActiveFeature() toggleActionButtons(true) + renderList(true) interactPlugin.enable() }, mobile: { slot: 'actions' }, @@ -556,6 +570,7 @@ function onMapReadyFactory(context) { onClick: () => { resetActiveFeature() toggleActionButtons(true) + renderList(true) drawPlugin.newPolygon(generateID(), polygonFeatureProperties) }, mobile: { slot: 'actions' }, @@ -570,6 +585,7 @@ function onMapReadyFactory(context) { onClick: () => { resetActiveFeature() toggleActionButtons(true) + renderList(true) drawPlugin.newLine(generateID(), lineFeatureProperties) }, mobile: { slot: 'actions' }, @@ -815,6 +831,7 @@ function onListElClickFactory(context) { } toggleActionButtons(true) + renderList(true) } /** @@ -960,6 +977,7 @@ function onListElChangeFactory(context) { /** * Renders the features into the list * @callback RenderList + * @param {boolean} [disabled] - whether to render the list with disabled links * @returns {void} */ diff --git a/src/client/stylesheets/shared.scss b/src/client/stylesheets/shared.scss index 9597139aa..963c6156c 100644 --- a/src/client/stylesheets/shared.scss +++ b/src/client/stylesheets/shared.scss @@ -33,6 +33,11 @@ @include govuk-font($size: 19); } +// Used in geospatial field +.govuk-link--disabled { + opacity: 0.5; +} + // Hide urls for hyperlinks when in print mode @media print { .govuk-link[href]::after { From f6698eb0277a704881a8c80fe8664a066594b499 Mon Sep 17 00:00:00 2001 From: David Stone Date: Thu, 9 Apr 2026 13:29:32 +0100 Subject: [PATCH 02/10] Fixes issue where the enter/return keypress activates the map search --- src/client/javascripts/geospatial-map.js | 26 +++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/src/client/javascripts/geospatial-map.js b/src/client/javascripts/geospatial-map.js index f646ee12c..3393bcfa7 100644 --- a/src/client/javascripts/geospatial-map.js +++ b/src/client/javascripts/geospatial-map.js @@ -605,6 +605,7 @@ function onMapReadyFactory(context) { const { listEl } = uiManager listEl.addEventListener('click', onListElClickFactory(context), false) listEl.addEventListener('change', onListElChangeFactory(context), false) + listEl.addEventListener('keydown', onListElKeydownFactory(), false) } } @@ -876,7 +877,7 @@ function onListElClickFactory(context) { } /** - * Callback factory function that fires a 'change' event is fired on the list container + * Callback factory function that fires when a 'change' event is fired on the list container * @param {Context} context - the UI context */ function onListElChangeFactory(context) { @@ -908,6 +909,29 @@ function onListElChangeFactory(context) { } } +/** + * Callback factory function that fires when a 'keydown' event is fired on the list container + */ +function onListElKeydownFactory() { + /** + * List container delegated 'keydown' events handler + * Fixes the issue of pressing "Enter" key in the description input triggering the map search + * @param {KeyboardEvent} e + */ + return function (e) { + const target = e.target + + if (!(target instanceof HTMLInputElement)) { + return + } + + if (e.code === 'Enter' || e.code === 'NumpadEnter') { + e.preventDefault() + e.stopPropagation() + } + } +} + /** * @import { MapsEnvironmentConfig, InteractiveMap } from '~/src/client/javascripts/map.js' */ From 636268347452cafb185b8002c00ebc7257c94b87 Mon Sep 17 00:00:00 2001 From: David Stone Date: Thu, 9 Apr 2026 13:30:18 +0100 Subject: [PATCH 03/10] Revert change that treated an empty string as an empty array --- src/server/plugins/engine/components/GeospatialField.test.ts | 4 ++-- src/server/plugins/engine/components/helpers/geospatial.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/server/plugins/engine/components/GeospatialField.test.ts b/src/server/plugins/engine/components/GeospatialField.test.ts index 647695871..5950cab5f 100644 --- a/src/server/plugins/engine/components/GeospatialField.test.ts +++ b/src/server/plugins/engine/components/GeospatialField.test.ts @@ -110,7 +110,7 @@ describe('GeospatialField', () => { expect(result.errors).toEqual([ expect.objectContaining({ - text: 'Example geospatial must contain at least 1 items' + text: 'Select example geospatial' }) ]) }) @@ -128,7 +128,7 @@ describe('GeospatialField', () => { expect(result.errors).toEqual([ expect.objectContaining({ - text: 'Example geospatial title must contain at least 1 items' + text: 'Select example geospatial title' }) ]) }) diff --git a/src/server/plugins/engine/components/helpers/geospatial.ts b/src/server/plugins/engine/components/helpers/geospatial.ts index 5f043aef9..a9bbeeb57 100644 --- a/src/server/plugins/engine/components/helpers/geospatial.ts +++ b/src/server/plugins/engine/components/helpers/geospatial.ts @@ -20,7 +20,7 @@ const Joi = JoiBase.extend({ if (typeof value === 'string') { if (value.trim() === '') { return { - value: [] + value: undefined } } From 8316476f3b2453a1658d035c2d5c1a42e7f2d261 Mon Sep 17 00:00:00 2001 From: David Stone Date: Thu, 9 Apr 2026 13:36:20 +0100 Subject: [PATCH 04/10] Fix geospatial joi tests --- .../plugins/engine/components/helpers/geospatial.test.js | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/src/server/plugins/engine/components/helpers/geospatial.test.js b/src/server/plugins/engine/components/helpers/geospatial.test.js index 8b77fa648..3079217c3 100644 --- a/src/server/plugins/engine/components/helpers/geospatial.test.js +++ b/src/server/plugins/engine/components/helpers/geospatial.test.js @@ -49,14 +49,7 @@ describe('Geospatial validation helpers', () => { test('it should validate an empty string', () => { const result = geospatialSchema.validate('') - expect(result.error).toBeUndefined() - expect(result.value).toEqual([]) - }) - - test('it should validate an empty string with errors when required', () => { - const result = geospatialSchema.min(1).required().validate('') - expect(result.error).toBeDefined() - expect(result.value).toEqual([]) + expect(result.value).toBeUndefined() }) }) From 26af6dbc748600bac79bc94d19a24add3a6aec0c Mon Sep 17 00:00:00 2001 From: David Stone Date: Thu, 9 Apr 2026 15:58:22 +0100 Subject: [PATCH 05/10] Code coverage --- test/client/javascripts/map.test.js | 57 +++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/test/client/javascripts/map.test.js b/test/client/javascripts/map.test.js index 554528280..69f9cf480 100644 --- a/test/client/javascripts/map.test.js +++ b/test/client/javascripts/map.test.js @@ -1199,6 +1199,63 @@ describe('Maps Client JS', () => { ...features.slice(1) ]) }) + + test('description enter/return key', () => { + const { geospatialInput, listContainer } = initialiseGeospatialMaps() + + // Manually change the description + const buckinghamPalaceInputEl = getDescription(listContainer, 0) + + buckinghamPalaceInputEl.value = 'New description' + buckinghamPalaceInputEl.dispatchEvent( + new window.Event('change', { bubbles: true }) + ) + buckinghamPalaceInputEl.dispatchEvent( + new window.KeyboardEvent('keydown', { bubbles: true, code: 'Enter' }) + ) + + expect(JSON.parse(geospatialInput.value)).toEqual([ + { + ...features[0], + properties: { + description: 'New description', + coordinateGridReference: 'TQ 29031 79662', + centroidGridReference: 'TQ 29031 79662' + } + }, + ...features.slice(1) + ]) + }) + + test('description enter/return numpad key', () => { + const { geospatialInput, listContainer } = initialiseGeospatialMaps() + + // Manually change the description + const buckinghamPalaceInputEl = getDescription(listContainer, 0) + + buckinghamPalaceInputEl.value = 'New description' + buckinghamPalaceInputEl.dispatchEvent( + new window.Event('change', { bubbles: true }) + ) + buckinghamPalaceInputEl.dispatchEvent( + new window.KeyboardEvent('keydown', { + bubbles: true, + code: 'NumpadEnter' + }) + ) + + expect(JSON.parse(geospatialInput.value)).toEqual([ + { + ...features[0], + properties: { + description: 'New description', + coordinateGridReference: 'TQ 29031 79662', + centroidGridReference: 'TQ 29031 79662' + } + }, + ...features.slice(1) + ]) + }) }) }) From cc323fa83e65b9c18db5c37857de2a269b8fbf6e Mon Sep 17 00:00:00 2001 From: David Stone Date: Thu, 9 Apr 2026 17:02:03 +0100 Subject: [PATCH 06/10] Add test coverage for readonly/disabled list --- test/client/javascripts/map.test.js | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/test/client/javascripts/map.test.js b/test/client/javascripts/map.test.js index 69f9cf480..6a38d26af 100644 --- a/test/client/javascripts/map.test.js +++ b/test/client/javascripts/map.test.js @@ -1,3 +1,4 @@ +import { createFeaturesHTML } from '~/src/client/javascripts/geospatial-map.js' import { formSubmitFactory, getCentroidGridRef, @@ -1256,6 +1257,18 @@ describe('Maps Client JS', () => { ...features.slice(1) ]) }) + + test('createFeaturesHTML - readonly (for use in designer viewer)', () => { + const html = createFeaturesHTML(features.slice(1), 'test', false, true) + + expect(html).toContain('data-action="focus"') + }) + + test('createFeaturesHTML - exported for use in designer viewer - disabled', () => { + const html = createFeaturesHTML(features.slice(1), 'test', true, false) + + expect(html).toContain('govuk-link--disabled') + }) }) }) From b82d08bf5a505a370d91103e86cc8e973f9bc416 Mon Sep 17 00:00:00 2001 From: David Stone Date: Thu, 9 Apr 2026 17:19:24 +0100 Subject: [PATCH 07/10] createFeaturesHTML tests --- test/client/javascripts/map.test.js | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/test/client/javascripts/map.test.js b/test/client/javascripts/map.test.js index 6a38d26af..5da4d484d 100644 --- a/test/client/javascripts/map.test.js +++ b/test/client/javascripts/map.test.js @@ -1258,16 +1258,30 @@ describe('Maps Client JS', () => { ]) }) + test('createFeaturesHTML - normal', () => { + const html = createFeaturesHTML(features.slice(1), 'test', false, false) + + expect(html).toContain('data-action="delete"') + expect(html).toContain('data-action="edit"') + expect(html).not.toContain('govuk-link--disabled') + expect(html).not.toContain('data-action="focus"') + }) + test('createFeaturesHTML - readonly (for use in designer viewer)', () => { const html = createFeaturesHTML(features.slice(1), 'test', false, true) + expect(html).not.toContain('data-action="delete"') + expect(html).not.toContain('data-action="edit"') expect(html).toContain('data-action="focus"') }) - test('createFeaturesHTML - exported for use in designer viewer - disabled', () => { + test('createFeaturesHTML - disabled', () => { const html = createFeaturesHTML(features.slice(1), 'test', true, false) expect(html).toContain('govuk-link--disabled') + expect(html).toContain('data-action="delete"') + expect(html).toContain('data-action="edit"') + expect(html).not.toContain('data-action="focus"') }) }) }) From 672b62676d382c01a6b497ba56571dc671cf9130 Mon Sep 17 00:00:00 2001 From: David Stone Date: Thu, 9 Apr 2026 17:41:37 +0100 Subject: [PATCH 08/10] Re-render list on cancel --- src/client/javascripts/geospatial-map.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/client/javascripts/geospatial-map.js b/src/client/javascripts/geospatial-map.js index 3393bcfa7..d62f39985 100644 --- a/src/client/javascripts/geospatial-map.js +++ b/src/client/javascripts/geospatial-map.js @@ -696,7 +696,7 @@ function onDrawEditedFactory(context) { */ function onDrawCancelledFactory(context) { const { uiManager, activeFeatureManager } = context - const { toggleActionButtons } = uiManager + const { toggleActionButtons, renderList } = uiManager const { resetActiveFeature } = activeFeatureManager /** @@ -705,6 +705,7 @@ function onDrawCancelledFactory(context) { return function onDrawCancelled() { toggleActionButtons(false) resetActiveFeature() + renderList() } } From 3fcd8b1acc994da9ab774b857ffa3b1628888c62 Mon Sep 17 00:00:00 2001 From: David Stone Date: Thu, 9 Apr 2026 18:11:10 +0100 Subject: [PATCH 09/10] Code coverage --- test/client/javascripts/map.test.js | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/test/client/javascripts/map.test.js b/test/client/javascripts/map.test.js index 5da4d484d..bd91c409b 100644 --- a/test/client/javascripts/map.test.js +++ b/test/client/javascripts/map.test.js @@ -1267,6 +1267,15 @@ describe('Maps Client JS', () => { expect(html).not.toContain('data-action="focus"') }) + test('createFeaturesHTML - normal (defaults)', () => { + const html = createFeaturesHTML(features.slice(1), 'test') + + expect(html).toContain('data-action="delete"') + expect(html).toContain('data-action="edit"') + expect(html).not.toContain('govuk-link--disabled') + expect(html).not.toContain('data-action="focus"') + }) + test('createFeaturesHTML - readonly (for use in designer viewer)', () => { const html = createFeaturesHTML(features.slice(1), 'test', false, true) From 863e0c47145f9a6fbee7a8a84c207e949e90ee6f Mon Sep 17 00:00:00 2001 From: David Stone Date: Thu, 9 Apr 2026 19:08:40 +0100 Subject: [PATCH 10/10] Code coverage --- src/client/javascripts/geospatial-map.js | 2 +- test/client/javascripts/map.test.js | 14 +++++++++++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/src/client/javascripts/geospatial-map.js b/src/client/javascripts/geospatial-map.js index d62f39985..73318d618 100644 --- a/src/client/javascripts/geospatial-map.js +++ b/src/client/javascripts/geospatial-map.js @@ -195,7 +195,7 @@ export function focusFeature(feature, mapProvider) { * @param {boolean} [disabled] - render the list with disabled links * @param {boolean} [readonly] - render the list item in readonly mode */ -function createFeatureHTML( +export function createFeatureHTML( feature, index, mapId, diff --git a/test/client/javascripts/map.test.js b/test/client/javascripts/map.test.js index bd91c409b..7ca4399ca 100644 --- a/test/client/javascripts/map.test.js +++ b/test/client/javascripts/map.test.js @@ -1,4 +1,7 @@ -import { createFeaturesHTML } from '~/src/client/javascripts/geospatial-map.js' +import { + createFeatureHTML, + createFeaturesHTML +} from '~/src/client/javascripts/geospatial-map.js' import { formSubmitFactory, getCentroidGridRef, @@ -1292,6 +1295,15 @@ describe('Maps Client JS', () => { expect(html).toContain('data-action="edit"') expect(html).not.toContain('data-action="focus"') }) + + test('createFeatureHTML - normal (defaults)', () => { + const html = createFeatureHTML(features[1], 0, 'test') + + expect(html).toContain('data-action="delete"') + expect(html).toContain('data-action="edit"') + expect(html).not.toContain('govuk-link--disabled') + expect(html).not.toContain('data-action="focus"') + }) }) })