@defra/interactive-map 0.0.48-fmp-1 → 0.0.49-alpha

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. package/dist/css/index.css +1 -1
  2. package/dist/esm/im-core.js +1 -1
  3. package/dist/umd/im-core.js +1 -1
  4. package/package.json +1 -1
  5. package/plugins/beta/draw-es/dist/esm/im-draw-es-plugin.js +1 -1
  6. package/plugins/beta/draw-es/src/graphic.js +1 -0
  7. package/plugins/datasets/dist/esm/esriLayerAdapter.js +1 -1
  8. package/plugins/datasets/dist/umd/im-datasets-esri-adapter.js +1 -1
  9. package/plugins/datasets/src/adapters/esri/esriLayerAdapter.js +60 -9
  10. package/plugins/datasets/src/adapters/esri/esriLayerAdapter.test.js +90 -4
  11. package/plugins/datasets/src/adapters/maplibre/registry/mapLibreDataset.test.js +8 -8
  12. package/plugins/datasets/src/reducers/__data__/demoDatasets.js +3 -3
  13. package/plugins/map-key/dist/esm/im-map-key-plugin.js +1 -1
  14. package/plugins/map-key/dist/umd/im-map-key-plugin.js +1 -1
  15. package/plugins/map-key/dist/umd/index.js +1 -1
  16. package/plugins/map-key/src/api/addSymbol.js +3 -0
  17. package/plugins/map-key/src/api/addSymbol.test.js +14 -0
  18. package/plugins/map-key/src/api/index.js +4 -4
  19. package/plugins/map-key/src/api/index.test.js +6 -6
  20. package/plugins/map-key/src/api/removeSymbol.js +3 -0
  21. package/plugins/map-key/src/api/removeSymbol.test.js +14 -0
  22. package/plugins/map-key/src/initialise/MapKeyInit.jsx +1 -0
  23. package/plugins/map-key/src/initialise/MapKeyInit.test.jsx +1 -1
  24. package/plugins/map-key/src/reducers/pluginState.js +4 -4
  25. package/plugins/map-key/src/reducers/pluginState.test.js +5 -5
  26. package/src/App/hooks/useLayoutMeasurements.js +28 -14
  27. package/src/App/hooks/useLayoutMeasurements.test.js +57 -10
  28. package/src/App/layout/layout.module.scss +10 -2
  29. package/plugins/map-key/src/api/addKeyItem.js +0 -3
  30. package/plugins/map-key/src/api/addKeyItem.test.js +0 -14
  31. package/plugins/map-key/src/api/removeKeyItem.js +0 -3
  32. package/plugins/map-key/src/api/removeKeyItem.test.js +0 -14
@@ -1,2 +1,2 @@
1
1
  /*! For license information please see index.js.LICENSE.txt */
2
- !function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define("defra",[],t):"object"==typeof exports?exports.defra=t():(e.defra=e.defra||{},e.defra.mapKeyPlugin=t())}(this,()=>(()=>{"use strict";var e,t,r={738(e){e.exports=preactCompat},287(e){e.exports=preactJsxRuntime}},n={};function o(e){var t=n[e];if(void 0!==t)return t.exports;var i=n[e]={exports:{}};return r[e](i,i.exports,o),i.exports}o.m=r,o.d=(e,t)=>{for(var r in t)o.o(t,r)&&!o.o(e,r)&&Object.defineProperty(e,r,{enumerable:!0,get:t[r]})},o.f={},o.e=e=>Promise.all(Object.keys(o.f).reduce((t,r)=>(o.f[r](e,t),t),[])),o.u=e=>"im-map-key-plugin.js",o.miniCssF=e=>{},o.g=function(){if("object"==typeof globalThis)return globalThis;try{return this||new Function("return this")()}catch(e){if("object"==typeof window)return window}}(),o.o=(e,t)=>Object.prototype.hasOwnProperty.call(e,t),e={},t="defra.mapKeyPlugin:",o.l=(r,n,i,a)=>{if(e[r])e[r].push(n);else{var u,c;if(void 0!==i)for(var f=document.getElementsByTagName("script"),p=0;p<f.length;p++){var l=f[p];if(l.getAttribute("src")==r||l.getAttribute("data-webpack")==t+i){u=l;break}}u||(c=!0,(u=document.createElement("script")).charset="utf-8",o.nc&&u.setAttribute("nonce",o.nc),u.setAttribute("data-webpack",t+i),u.src=r),e[r]=[n];var s=(t,n)=>{u.onerror=u.onload=null,clearTimeout(d);var o=e[r];if(delete e[r],u.parentNode&&u.parentNode.removeChild(u),o&&o.forEach(e=>e(n)),t)return t(n)},d=setTimeout(s.bind(null,void 0,{type:"timeout",target:u}),12e4);u.onerror=s.bind(null,u.onerror),u.onload=s.bind(null,u.onload),c&&document.head.appendChild(u)}},o.r=e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},(()=>{var e;o.g.importScripts&&(e=o.g.location+"");var t=o.g.document;if(!e&&t&&(t.currentScript&&"SCRIPT"===t.currentScript.tagName.toUpperCase()&&(e=t.currentScript.src),!e)){var r=t.getElementsByTagName("script");if(r.length)for(var n=r.length-1;n>-1&&(!e||!/^http(s?):/.test(e));)e=r[n--].src}if(!e)throw new Error("Automatic publicPath is not supported in this browser");e=e.replace(/^blob:/,"").replace(/#.*$/,"").replace(/\?.*$/,"").replace(/\/[^\/]+$/,"/"),o.p=e})(),(()=>{var e={57:0};o.f.j=(t,r)=>{var n=o.o(e,t)?e[t]:void 0;if(0!==n)if(n)r.push(n[2]);else{var i=new Promise((r,o)=>n=e[t]=[r,o]);r.push(n[2]=i);var a=o.p+o.u(t),u=new Error;o.l(a,r=>{if(o.o(e,t)&&(0!==(n=e[t])&&(e[t]=void 0),n)){var i=r&&("load"===r.type?"missing":r.type),a=r&&r.target&&r.target.src;u.message="Loading chunk "+t+" failed.\n("+i+": "+a+")",u.name="ChunkLoadError",u.type=i,u.request=a,n[1](u)}},"chunk-"+t,t)}};var t=(t,r)=>{var n,i,[a,u,c]=r,f=0;if(a.some(t=>0!==e[t])){for(n in u)o.o(u,n)&&(o.m[n]=u[n]);c&&c(o)}for(t&&t(r);f<a.length;f++)i=a[f],o.o(e,i)&&e[i]&&e[i][0](),e[i]=0},r=this.webpackChunkdefra_DefraMap=this.webpackChunkdefra_DefraMap||[];r.forEach(t.bind(null,0)),r.push=t.bind(null,r.push.bind(r))})();var i={};function a(e){return a="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e},a(e)}function u(){var e,t,r="function"==typeof Symbol?Symbol:{},n=r.iterator||"@@iterator",o=r.toStringTag||"@@toStringTag";function i(r,n,o,i){var u=n&&n.prototype instanceof f?n:f,p=Object.create(u.prototype);return c(p,"_invoke",function(r,n,o){var i,u,c,f=0,p=o||[],l=!1,s={p:0,n:0,v:e,a:d,f:d.bind(e,4),d:function(t,r){return i=t,u=0,c=e,s.n=r,a}};function d(r,n){for(u=r,c=n,t=0;!l&&f&&!o&&t<p.length;t++){var o,i=p[t],d=s.p,b=i[2];r>3?(o=b===n)&&(c=i[(u=i[4])?5:(u=3,3)],i[4]=i[5]=e):i[0]<=d&&((o=r<2&&d<i[1])?(u=0,s.v=n,s.n=i[1]):d<b&&(o=r<3||i[0]>n||n>b)&&(i[4]=r,i[5]=n,s.n=b,u=0))}if(o||r>1)return a;throw l=!0,n}return function(o,p,b){if(f>1)throw TypeError("Generator is already running");for(l&&1===p&&d(p,b),u=p,c=b;(t=u<2?e:c)||!l;){i||(u?u<3?(u>1&&(s.n=-1),d(u,c)):s.n=c:s.v=c);try{if(f=2,i){if(u||(o="next"),t=i[o]){if(!(t=t.call(i,c)))throw TypeError("iterator result is not an object");if(!t.done)return t;c=t.value,u<2&&(u=0)}else 1===u&&(t=i.return)&&t.call(i),u<2&&(c=TypeError("The iterator does not provide a '"+o+"' method"),u=1);i=e}else if((t=(l=s.n<0)?c:r.call(n,s))!==a)break}catch(t){i=e,u=1,c=t}finally{f=1}}return{value:t,done:l}}}(r,o,i),!0),p}var a={};function f(){}function p(){}function l(){}t=Object.getPrototypeOf;var s=[][n]?t(t([][n]())):(c(t={},n,function(){return this}),t),d=l.prototype=f.prototype=Object.create(s);function b(e){return Object.setPrototypeOf?Object.setPrototypeOf(e,l):(e.__proto__=l,c(e,o,"GeneratorFunction")),e.prototype=Object.create(d),e}return p.prototype=l,c(d,"constructor",l),c(l,"constructor",p),p.displayName="GeneratorFunction",c(l,o,"GeneratorFunction"),c(d),c(d,o,"Generator"),c(d,n,function(){return this}),c(d,"toString",function(){return"[object Generator]"}),(u=function(){return{w:i,m:b}})()}function c(e,t,r,n){var o=Object.defineProperty;try{o({},"",{})}catch(e){o=0}c=function(e,t,r,n){function i(t,r){c(e,t,function(e){return this._invoke(t,r,e)})}t?o?o(e,t,{value:r,enumerable:!n,configurable:!n,writable:!n}):e[t]=r:(i("next",0),i("throw",1),i("return",2))},c(e,t,r,n)}function f(e,t,r,n,o,i,a){try{var u=e[i](a),c=u.value}catch(e){return void r(e)}u.done?t(c):Promise.resolve(c).then(n,o)}function p(e,t){var r=Object.keys(e);if(Object.getOwnPropertySymbols){var n=Object.getOwnPropertySymbols(e);t&&(n=n.filter(function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable})),r.push.apply(r,n)}return r}function l(e){for(var t=1;t<arguments.length;t++){var r=null!=arguments[t]?arguments[t]:{};t%2?p(Object(r),!0).forEach(function(t){s(e,t,r[t])}):Object.getOwnPropertyDescriptors?Object.defineProperties(e,Object.getOwnPropertyDescriptors(r)):p(Object(r)).forEach(function(t){Object.defineProperty(e,t,Object.getOwnPropertyDescriptor(r,t))})}return e}function s(e,t,r){return(t=function(e){var t=function(e){if("object"!=a(e)||!e)return e;var t=e[Symbol.toPrimitive];if(void 0!==t){var r=t.call(e,"string");if("object"!=a(r))return r;throw new TypeError("@@toPrimitive must return a primitive value.")}return String(e)}(e);return"symbol"==a(t)?t:t+""}(t))in e?Object.defineProperty(e,t,{value:r,enumerable:!0,configurable:!0,writable:!0}):e[t]=r,e}function d(){var e,t;return l(l({noKeyItemText:"No features displayed"},arguments.length>0&&void 0!==arguments[0]?arguments[0]:{}),{},{id:"mapKey",load:(e=u().m(function e(){var t;return u().w(function(e){for(;;)switch(e.n){case 0:return e.n=1,o.e(414).then(o.bind(o,935));case 1:return t=e.v.manifest,e.a(2,t)}},e)}),t=function(){var t=this,r=arguments;return new Promise(function(n,o){var i=e.apply(t,r);function a(e){f(i,n,o,a,u,"next",e)}function u(e){f(i,n,o,a,u,"throw",e)}a(void 0)})},function(){return t.apply(this,arguments)})})}return o.d(i,{default:()=>d}),i.default})());
2
+ !function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define("defra",[],t):"object"==typeof exports?exports.defra=t():(e.defra=e.defra||{},e.defra.mapKeyPlugin=t())}(this,()=>(()=>{"use strict";var e,t,r={738(e){e.exports=preactCompat},287(e){e.exports=preactJsxRuntime}},n={};function o(e){var t=n[e];if(void 0!==t)return t.exports;var i=n[e]={exports:{}};return r[e](i,i.exports,o),i.exports}o.m=r,o.d=(e,t)=>{for(var r in t)o.o(t,r)&&!o.o(e,r)&&Object.defineProperty(e,r,{enumerable:!0,get:t[r]})},o.f={},o.e=e=>Promise.all(Object.keys(o.f).reduce((t,r)=>(o.f[r](e,t),t),[])),o.u=e=>"im-map-key-plugin.js",o.miniCssF=e=>{},o.g=function(){if("object"==typeof globalThis)return globalThis;try{return this||new Function("return this")()}catch(e){if("object"==typeof window)return window}}(),o.o=(e,t)=>Object.prototype.hasOwnProperty.call(e,t),e={},t="defra.mapKeyPlugin:",o.l=(r,n,i,a)=>{if(e[r])e[r].push(n);else{var u,c;if(void 0!==i)for(var f=document.getElementsByTagName("script"),p=0;p<f.length;p++){var l=f[p];if(l.getAttribute("src")==r||l.getAttribute("data-webpack")==t+i){u=l;break}}u||(c=!0,(u=document.createElement("script")).charset="utf-8",o.nc&&u.setAttribute("nonce",o.nc),u.setAttribute("data-webpack",t+i),u.src=r),e[r]=[n];var s=(t,n)=>{u.onerror=u.onload=null,clearTimeout(d);var o=e[r];if(delete e[r],u.parentNode&&u.parentNode.removeChild(u),o&&o.forEach(e=>e(n)),t)return t(n)},d=setTimeout(s.bind(null,void 0,{type:"timeout",target:u}),12e4);u.onerror=s.bind(null,u.onerror),u.onload=s.bind(null,u.onload),c&&document.head.appendChild(u)}},o.r=e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},(()=>{var e;o.g.importScripts&&(e=o.g.location+"");var t=o.g.document;if(!e&&t&&(t.currentScript&&"SCRIPT"===t.currentScript.tagName.toUpperCase()&&(e=t.currentScript.src),!e)){var r=t.getElementsByTagName("script");if(r.length)for(var n=r.length-1;n>-1&&(!e||!/^http(s?):/.test(e));)e=r[n--].src}if(!e)throw new Error("Automatic publicPath is not supported in this browser");e=e.replace(/^blob:/,"").replace(/#.*$/,"").replace(/\?.*$/,"").replace(/\/[^\/]+$/,"/"),o.p=e})(),(()=>{var e={57:0};o.f.j=(t,r)=>{var n=o.o(e,t)?e[t]:void 0;if(0!==n)if(n)r.push(n[2]);else{var i=new Promise((r,o)=>n=e[t]=[r,o]);r.push(n[2]=i);var a=o.p+o.u(t),u=new Error;o.l(a,r=>{if(o.o(e,t)&&(0!==(n=e[t])&&(e[t]=void 0),n)){var i=r&&("load"===r.type?"missing":r.type),a=r&&r.target&&r.target.src;u.message="Loading chunk "+t+" failed.\n("+i+": "+a+")",u.name="ChunkLoadError",u.type=i,u.request=a,n[1](u)}},"chunk-"+t,t)}};var t=(t,r)=>{var n,i,[a,u,c]=r,f=0;if(a.some(t=>0!==e[t])){for(n in u)o.o(u,n)&&(o.m[n]=u[n]);c&&c(o)}for(t&&t(r);f<a.length;f++)i=a[f],o.o(e,i)&&e[i]&&e[i][0](),e[i]=0},r=this.webpackChunkdefra_DefraMap=this.webpackChunkdefra_DefraMap||[];r.forEach(t.bind(null,0)),r.push=t.bind(null,r.push.bind(r))})();var i={};function a(e){return a="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e},a(e)}function u(){var e,t,r="function"==typeof Symbol?Symbol:{},n=r.iterator||"@@iterator",o=r.toStringTag||"@@toStringTag";function i(r,n,o,i){var u=n&&n.prototype instanceof f?n:f,p=Object.create(u.prototype);return c(p,"_invoke",function(r,n,o){var i,u,c,f=0,p=o||[],l=!1,s={p:0,n:0,v:e,a:d,f:d.bind(e,4),d:function(t,r){return i=t,u=0,c=e,s.n=r,a}};function d(r,n){for(u=r,c=n,t=0;!l&&f&&!o&&t<p.length;t++){var o,i=p[t],d=s.p,b=i[2];r>3?(o=b===n)&&(c=i[(u=i[4])?5:(u=3,3)],i[4]=i[5]=e):i[0]<=d&&((o=r<2&&d<i[1])?(u=0,s.v=n,s.n=i[1]):d<b&&(o=r<3||i[0]>n||n>b)&&(i[4]=r,i[5]=n,s.n=b,u=0))}if(o||r>1)return a;throw l=!0,n}return function(o,p,b){if(f>1)throw TypeError("Generator is already running");for(l&&1===p&&d(p,b),u=p,c=b;(t=u<2?e:c)||!l;){i||(u?u<3?(u>1&&(s.n=-1),d(u,c)):s.n=c:s.v=c);try{if(f=2,i){if(u||(o="next"),t=i[o]){if(!(t=t.call(i,c)))throw TypeError("iterator result is not an object");if(!t.done)return t;c=t.value,u<2&&(u=0)}else 1===u&&(t=i.return)&&t.call(i),u<2&&(c=TypeError("The iterator does not provide a '"+o+"' method"),u=1);i=e}else if((t=(l=s.n<0)?c:r.call(n,s))!==a)break}catch(t){i=e,u=1,c=t}finally{f=1}}return{value:t,done:l}}}(r,o,i),!0),p}var a={};function f(){}function p(){}function l(){}t=Object.getPrototypeOf;var s=[][n]?t(t([][n]())):(c(t={},n,function(){return this}),t),d=l.prototype=f.prototype=Object.create(s);function b(e){return Object.setPrototypeOf?Object.setPrototypeOf(e,l):(e.__proto__=l,c(e,o,"GeneratorFunction")),e.prototype=Object.create(d),e}return p.prototype=l,c(d,"constructor",l),c(l,"constructor",p),p.displayName="GeneratorFunction",c(l,o,"GeneratorFunction"),c(d),c(d,o,"Generator"),c(d,n,function(){return this}),c(d,"toString",function(){return"[object Generator]"}),(u=function(){return{w:i,m:b}})()}function c(e,t,r,n){var o=Object.defineProperty;try{o({},"",{})}catch(e){o=0}c=function(e,t,r,n){function i(t,r){c(e,t,function(e){return this._invoke(t,r,e)})}t?o?o(e,t,{value:r,enumerable:!n,configurable:!n,writable:!n}):e[t]=r:(i("next",0),i("throw",1),i("return",2))},c(e,t,r,n)}function f(e,t,r,n,o,i,a){try{var u=e[i](a),c=u.value}catch(e){return void r(e)}u.done?t(c):Promise.resolve(c).then(n,o)}function p(e,t){var r=Object.keys(e);if(Object.getOwnPropertySymbols){var n=Object.getOwnPropertySymbols(e);t&&(n=n.filter(function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable})),r.push.apply(r,n)}return r}function l(e){for(var t=1;t<arguments.length;t++){var r=null!=arguments[t]?arguments[t]:{};t%2?p(Object(r),!0).forEach(function(t){s(e,t,r[t])}):Object.getOwnPropertyDescriptors?Object.defineProperties(e,Object.getOwnPropertyDescriptors(r)):p(Object(r)).forEach(function(t){Object.defineProperty(e,t,Object.getOwnPropertyDescriptor(r,t))})}return e}function s(e,t,r){return(t=function(e){var t=function(e){if("object"!=a(e)||!e)return e;var t=e[Symbol.toPrimitive];if(void 0!==t){var r=t.call(e,"string");if("object"!=a(r))return r;throw new TypeError("@@toPrimitive must return a primitive value.")}return String(e)}(e);return"symbol"==a(t)?t:t+""}(t))in e?Object.defineProperty(e,t,{value:r,enumerable:!0,configurable:!0,writable:!0}):e[t]=r,e}function d(){var e,t;return l(l({noKeyItemText:"No features displayed"},arguments.length>0&&void 0!==arguments[0]?arguments[0]:{}),{},{id:"mapKey",load:(e=u().m(function e(){var t;return u().w(function(e){for(;;)switch(e.n){case 0:return e.n=1,o.e(414).then(o.bind(o,64));case 1:return t=e.v.manifest,e.a(2,t)}},e)}),t=function(){var t=this,r=arguments;return new Promise(function(n,o){var i=e.apply(t,r);function a(e){f(i,n,o,a,u,"next",e)}function u(e){f(i,n,o,a,u,"throw",e)}a(void 0)})},function(){return t.apply(this,arguments)})})}return o.d(i,{default:()=>d}),i.default})());
@@ -0,0 +1,3 @@
1
+ export const addSymbol = ({ pluginState: { dispatch } }, keyDefinition) => {
2
+ dispatch({ type: 'ADD_KEY_SYMBOL', payload: keyDefinition })
3
+ }
@@ -0,0 +1,14 @@
1
+ import { addSymbol } from './addSymbol.js'
2
+
3
+ describe('addSymbol', () => {
4
+ it('dispatches an ADD_KEY_SYMBOL action with the key definition', () => {
5
+ const dispatch = jest.fn()
6
+
7
+ addSymbol({ pluginState: { dispatch } }, { id: 'map-key', groupLabel: 'Map key' })
8
+
9
+ expect(dispatch).toHaveBeenCalledWith({
10
+ type: 'ADD_KEY_SYMBOL',
11
+ payload: { id: 'map-key', groupLabel: 'Map key' }
12
+ })
13
+ })
14
+ })
@@ -1,7 +1,7 @@
1
- import { addKeyItem } from './addKeyItem.js'
2
- import { removeKeyItem } from './removeKeyItem.js'
1
+ import { addSymbol } from './addSymbol.js'
2
+ import { removeSymbol } from './removeSymbol.js'
3
3
 
4
4
  export const api = {
5
- addKeyItem,
6
- removeKeyItem
5
+ addSymbol,
6
+ removeSymbol
7
7
  }
@@ -1,13 +1,13 @@
1
1
  import { api } from './index.js'
2
- import { addKeyItem } from './addKeyItem.js'
3
- import { removeKeyItem } from './removeKeyItem.js'
2
+ import { addSymbol } from './addSymbol.js'
3
+ import { removeSymbol } from './removeSymbol.js'
4
4
 
5
5
  describe('api/index', () => {
6
- it('exports the addKeyItem API', () => {
7
- expect(api.addKeyItem).toBe(addKeyItem)
6
+ it('exports the addSymbol API', () => {
7
+ expect(api.addSymbol).toBe(addSymbol)
8
8
  })
9
9
 
10
- it('exports the removeKeyItem API', () => {
11
- expect(api.removeKeyItem).toBe(removeKeyItem)
10
+ it('exports the removeSymbol API', () => {
11
+ expect(api.removeSymbol).toBe(removeSymbol)
12
12
  })
13
13
  })
@@ -0,0 +1,3 @@
1
+ export const removeSymbol = ({ pluginState: { dispatch } }, keyDefinition) => {
2
+ dispatch({ type: 'REMOVE_KEY_SYMBOL', payload: keyDefinition })
3
+ }
@@ -0,0 +1,14 @@
1
+ import { removeSymbol } from './removeSymbol.js'
2
+
3
+ describe('removeSymbol', () => {
4
+ it('dispatches a REMOVE_KEY_SYMBOL action with the key definition', () => {
5
+ const dispatch = jest.fn()
6
+
7
+ removeSymbol({ pluginState: { dispatch } }, { id: 'map-key' })
8
+
9
+ expect(dispatch).toHaveBeenCalledWith({
10
+ type: 'REMOVE_KEY_SYMBOL',
11
+ payload: { id: 'map-key' }
12
+ })
13
+ })
14
+ })
@@ -16,6 +16,7 @@ export function MapKeyInit ({ pluginConfig, pluginState, mapState, services }) {
16
16
  attachPluginStateRef(pluginStateRef)
17
17
  // Request a handle on the datasetsRegistry singleton
18
18
  eventBus.requestOnce('datasets:registry', setDatasetRegistry)
19
+ eventBus.emit('map-key:ready')
19
20
  }, [mapState.isMapReady])
20
21
 
21
22
  useEffect(() => {
@@ -6,7 +6,7 @@ jest.mock('../registry/getDatasetRegistry.js', () => ({
6
6
  setDatasetRegistry: jest.fn()
7
7
  }))
8
8
 
9
- const eventBus = { requestOnce: jest.fn() }
9
+ const eventBus = { requestOnce: jest.fn(), emit: jest.fn() }
10
10
  const pluginState = { dispatch: jest.fn() }
11
11
  const pluginConfig = {
12
12
  groups: {
@@ -17,7 +17,7 @@ const findGroup = (state, id, groupId) => {
17
17
  })
18
18
  }
19
19
 
20
- const addKeyItem = (state, keyDefinition) => {
20
+ const addSymbol = (state, keyDefinition) => {
21
21
  const { id, groupLabel } = keyDefinition
22
22
  const groupId = keyDefinition.groupId || labelToId(groupLabel)
23
23
  // Check for an existingGroup
@@ -41,7 +41,7 @@ const addKeyItem = (state, keyDefinition) => {
41
41
  }
42
42
  }
43
43
 
44
- const removeKeyItem = (state, keyDefinition) => {
44
+ const removeSymbol = (state, keyDefinition) => {
45
45
  const { id } = keyDefinition
46
46
  return {
47
47
  ...state,
@@ -57,8 +57,8 @@ const addKeyGroups = (state, groups) => {
57
57
  }
58
58
 
59
59
  const actions = {
60
- ADD_KEY_ITEM: addKeyItem,
61
- REMOVE_KEY_ITEM: removeKeyItem,
60
+ ADD_KEY_SYMBOL: addSymbol,
61
+ REMOVE_KEY_SYMBOL: removeSymbol,
62
62
  ADD_KEY_GROUPS: addKeyGroups
63
63
  }
64
64
 
@@ -6,7 +6,7 @@ describe('pluginState reducer helpers', () => {
6
6
  })
7
7
 
8
8
  it('adds a flat key item and creates a flat group when needed', () => {
9
- const nextState = actions.ADD_KEY_ITEM(initialState, { id: 'flat-key', label: 'Flat key' })
9
+ const nextState = actions.ADD_KEY_SYMBOL(initialState, { id: 'flat-key', label: 'Flat key' })
10
10
 
11
11
  expect(nextState).toEqual({
12
12
  keyDefinitions: [{ id: 'flat-key', label: 'Flat key', type: 'manual' }],
@@ -15,7 +15,7 @@ describe('pluginState reducer helpers', () => {
15
15
  })
16
16
 
17
17
  it('adds a grouped key item when the label can be converted to a group id', () => {
18
- const nextState = actions.ADD_KEY_ITEM(initialState, { id: 'group-key', groupLabel: 'Group Label', label: 'Group key' })
18
+ const nextState = actions.ADD_KEY_SYMBOL(initialState, { id: 'group-key', groupLabel: 'Group Label', label: 'Group key' })
19
19
 
20
20
  expect(nextState).toEqual({
21
21
  keyDefinitions: [{
@@ -35,7 +35,7 @@ describe('pluginState reducer helpers', () => {
35
35
  groups: [{ id: 'existing-item', type: 'flat' }]
36
36
  }
37
37
 
38
- expect(actions.ADD_KEY_ITEM(state, { id: 'existing-item', label: 'Existing item' })).toEqual({
38
+ expect(actions.ADD_KEY_SYMBOL(state, { id: 'existing-item', label: 'Existing item' })).toEqual({
39
39
  keyDefinitions: [
40
40
  { id: 'existing-item' },
41
41
  { id: 'existing-item', label: 'Existing item', type: 'manual' }
@@ -50,7 +50,7 @@ describe('pluginState reducer helpers', () => {
50
50
  groups: [{ id: 'group-1', type: 'group', groupLabel: 'Group 1' }]
51
51
  }
52
52
 
53
- expect(actions.ADD_KEY_ITEM(state, { id: 'new-item', groupId: 'group-1', groupLabel: 'Group 1', label: 'New item' })).toEqual({
53
+ expect(actions.ADD_KEY_SYMBOL(state, { id: 'new-item', groupId: 'group-1', groupLabel: 'Group 1', label: 'New item' })).toEqual({
54
54
  keyDefinitions: [
55
55
  { id: 'existing-group-item', groupId: 'group-1' },
56
56
  { id: 'new-item', groupId: 'group-1', groupLabel: 'Group 1', label: 'New item', type: 'manual' }
@@ -65,7 +65,7 @@ describe('pluginState reducer helpers', () => {
65
65
  groups: [{ id: 'a', type: 'flat' }]
66
66
  }
67
67
 
68
- expect(actions.REMOVE_KEY_ITEM(state, { id: 'a' })).toEqual({
68
+ expect(actions.REMOVE_KEY_SYMBOL(state, { id: 'a' })).toEqual({
69
69
  keyDefinitions: [{ id: 'b' }],
70
70
  groups: [{ id: 'a', type: 'flat' }]
71
71
  })
@@ -17,8 +17,12 @@ const symmetricWidth = (left, right) => left || right ? Math.max(left, right) :
17
17
  const subSlotMaxHeight = (columnHeight, siblingButtons, gap) => columnHeight - (siblingButtons ? siblingButtons + gap : 0)
18
18
 
19
19
  // bottomRightHeight is 0 when empty, falling back to the attributions' own height for spacing.
20
- const rightOffsetBottom = (containerPad, bottomRightHeight, attributionsHeight, gap) =>
21
- containerPad + (bottomRightHeight > 0 ? bottomRightHeight + gap : attributionsHeight)
20
+ // rise is how far attributions has lifted .im-o-app__bottom-right above .im-o-app__bottom's
21
+ // own bottom edge (see attributionsRise below) — without adding it here, .im-o-app__right's
22
+ // computed bottom offset stays at the box's un-lifted position and the floating right column
23
+ // overlaps the now-raised bottom-right buttons.
24
+ const rightOffsetBottom = (containerPad, bottomRightHeight, attributionsHeight, rise, gap) =>
25
+ containerPad + (bottomRightHeight > 0 ? bottomRightHeight + rise + gap : attributionsHeight)
22
26
 
23
27
  // Clears the bottom row's own TOP edge (not just its trailing gap below), so the hint
24
28
  // never overlaps the logo/attribution row itself, plus a gap above it. That trivially
@@ -74,13 +78,22 @@ const attributionsNaturalWidth = (attributionsEl) => {
74
78
  // longer fits, at its natural width, in the space beside the logo column.
75
79
  const isAttributionsStacked = (naturalWidth, availableWidth) => naturalWidth > availableWidth
76
80
 
77
- // Docked, attributions bleeds primaryGap below the row's bottom edge then grows upward from
78
- // there by its own height — once that height exceeds primaryGap, it creeps back up into the
79
- // row itself, where .im-o-app__bottom-right shares the same horizontal space. This is how far
80
- // that needs pushing up to stay clear. Stacked already has its own clearance (the stacked
81
- // rule's margin-top), so none is needed there.
82
- const attributionsBottomRightClearance = (isStacked, attributionsHeight, dividerGap, primaryGap) =>
83
- isStacked ? 0 : Math.max(0, attributionsHeight + dividerGap - primaryGap)
81
+ // How far attributions lifts .im-o-app__bottom-right above .im-o-app__bottom's own bottom
82
+ // edge. The same figure covers both modes, because both bleed primaryGap past that edge and
83
+ // then take attributionsHeight (plus a dividerGap) back up from there:
84
+ // - docked: the box grows upward from the bleed into the row itself, where
85
+ // .im-o-app__bottom-right shares the same horizontal space, so the row needs an explicit
86
+ // margin-bottom (--bottom-right-clearance) to stay clear of it;
87
+ // - stacked: the box is an extra flex line *below* the row, so it lifts the row by exactly
88
+ // that much on its own — no margin needed, hence bottomRightClearance's isStacked branch.
89
+ // Either way .im-o-app__right must clear the lift, so the offsets below use this, not the
90
+ // clearance. Clamped at 0: a box shorter than its own bleed lifts nothing.
91
+ const attributionsRise = (attributionsHeight, dividerGap, primaryGap) =>
92
+ Math.max(0, attributionsHeight + dividerGap - primaryGap)
93
+
94
+ // Stacked gets its lift from the extra flex line itself (see attributionsRise above), so only
95
+ // docked needs the row pushed up by a margin.
96
+ const bottomRightClearance = (isStacked, rise) => isStacked ? 0 : rise
84
97
 
85
98
  // Docks centred between the side columns when there's room, otherwise stacks full-width
86
99
  // (mobile always stacks). Sets the banner's own CSS vars and returns what the side-column
@@ -125,8 +138,9 @@ function applyAttributionsLayout ({ appContainer, bottom, attributions, dividerG
125
138
  // space-between` gap between the two columns isn't a fixed value (unlike `gap`) to reconstruct.
126
139
  const left = attributionsCol ? Math.round(attributionsCol.getBoundingClientRect().left - bottom.getBoundingClientRect().left) : 0
127
140
  appContainer.style.setProperty('--attributions-left', `${left}px`)
128
- const clearance = attributionsBottomRightClearance(isStacked, attributions.offsetHeight, dividerGap, primaryGap)
129
- appContainer.style.setProperty('--bottom-right-clearance', `${clearance}px`)
141
+ const rise = attributionsRise(attributions.offsetHeight, dividerGap, primaryGap)
142
+ appContainer.style.setProperty('--bottom-right-clearance', `${bottomRightClearance(isStacked, rise)}px`)
143
+ return { rise }
130
144
  }
131
145
 
132
146
  /**
@@ -174,7 +188,7 @@ function calculateLayout (layoutRefs, breakpoint) {
174
188
  ? bannerTop + bannerHeight + dividerGap
175
189
  : colHeight + top.offsetTop
176
190
 
177
- applyAttributionsLayout({ appContainer, bottom, attributions, dividerGap, primaryGap })
191
+ const { rise: attributionsLift } = applyAttributionsLayout({ appContainer, bottom, attributions, dividerGap, primaryGap })
178
192
 
179
193
  // === Left container offsets ===
180
194
  const leftOffsetTop = sideOffsetTop(topLeftCol.offsetHeight)
@@ -187,10 +201,10 @@ function calculateLayout (layoutRefs, breakpoint) {
187
201
  const bottomRightHeight = buttonHeight(bottomRightRef)
188
202
  const bottomContainerPad = main.offsetHeight - bottom.offsetTop - bottom.offsetHeight
189
203
  const rightOffsetTop = sideOffsetTop(topRightCol.offsetHeight)
190
- const rightEffectiveBottom = bottom.offsetTop + bottom.offsetHeight - bottomRightHeight
204
+ const rightEffectiveBottom = bottom.offsetTop + bottom.offsetHeight - bottomRightHeight - attributionsLift
191
205
  const rightColumnHeight = rightEffectiveBottom - rightOffsetTop - dividerGap
192
206
  appContainer.style.setProperty('--right-offset-top', `${rightOffsetTop}px`)
193
- appContainer.style.setProperty('--right-offset-bottom', `${rightOffsetBottom(bottomContainerPad, bottomRightHeight, attributions.offsetHeight, dividerGap)}px`)
207
+ appContainer.style.setProperty('--right-offset-bottom', `${rightOffsetBottom(bottomContainerPad, bottomRightHeight, attributions.offsetHeight, attributionsLift, dividerGap)}px`)
194
208
  appContainer.style.setProperty('--right-top-max-height', `${rightColumnHeight}px`)
195
209
 
196
210
  // === Keyboard hint bottom offset ===
@@ -88,9 +88,11 @@ describe('useLayoutMeasurements', () => {
88
88
  test.each([
89
89
  ['right-offset-top', { topRightCol: { offsetHeight: 80 }, top: { offsetTop: 15 } }, '95px'],
90
90
  ['right-offset-bottom', { main: { offsetHeight: 600 }, bottom: { offsetTop: 500 } }, '116px'],
91
- // leftColumnHeight = 400 - (50+10) - 8 = 332; rightColumnHeight = 400 - (40+10) - 8 = 342
91
+ // leftColumnHeight = 400 - (50+10) - 8 = 332
92
+ // rightColumnHeight: rightEffectiveBottom = 400 - 0 (bottomRightHeight) - 16 (bottom-right-clearance,
93
+ // since default attributions offsetHeight 16 + dividerGap 8 - primaryGap 8 = 16) = 384; 384 - 50 - 8 = 326
92
94
  ['left-top-max-height', {}, '332px'],
93
- ['right-top-max-height', {}, '342px']
95
+ ['right-top-max-height', {}, '326px']
94
96
  ])('calculates %s correctly', (name, refOverrides, expected) => {
95
97
  const { layoutRefs } = setup({ refs: refOverrides })
96
98
  renderHook(() => useLayoutMeasurements())
@@ -103,10 +105,11 @@ describe('useLayoutMeasurements', () => {
103
105
  ['--left-top-panel-max-height', { leftBottom: { offsetHeight: 50 } }, '274px'], // 332 - 50 - 8
104
106
  ['--left-bottom-panel-max-height', {}, '332px'],
105
107
  ['--left-bottom-panel-max-height', { leftTop: { offsetHeight: 40 } }, '284px'], // 332 - 40 - 8
106
- ['--right-top-panel-max-height', {}, '342px'],
107
- ['--right-top-panel-max-height', { rightBottom: { offsetHeight: 60 } }, '274px'], // 342 - 60 - 8
108
- ['--right-bottom-panel-max-height', {}, '342px'],
109
- ['--right-bottom-panel-max-height', { rightTop: { offsetHeight: 30 } }, '304px'] // 342 - 30 - 8
108
+ // rightColumnHeight is 326 by default here (see the right-top-max-height case above)
109
+ ['--right-top-panel-max-height', {}, '326px'],
110
+ ['--right-top-panel-max-height', { rightBottom: { offsetHeight: 60 } }, '258px'], // 326 - 60 - 8
111
+ ['--right-bottom-panel-max-height', {}, '326px'],
112
+ ['--right-bottom-panel-max-height', { rightTop: { offsetHeight: 30 } }, '288px'] // 326 - 30 - 8
110
113
  ])('calculates %s with sibling buttons=%o correctly', (varName, refOverrides, expected) => {
111
114
  const { layoutRefs } = setup({ refs: refOverrides })
112
115
  renderHook(() => useLayoutMeasurements())
@@ -232,7 +235,7 @@ describe('useLayoutMeasurements', () => {
232
235
  appendChild(layoutRefs.attributionsRef.current, { scrollWidth: 250 }) // attribution text, too wide to fit
233
236
  renderHook(() => useLayoutMeasurements())
234
237
  expect(layoutRefs.attributionsRef.current.classList.contains('im-o-app__attributions--stacked')).toBe(true)
235
- // stacked already has its own clearance (the stacked rule's own margin-top), so none needed here
238
+ // stacked is lifted by its own extra flex line, not a margin, so no clearance is needed here
236
239
  expect(layoutRefs.appContainerRef.current.style.setProperty).toHaveBeenCalledWith('--bottom-right-clearance', '0px')
237
240
  })
238
241
 
@@ -260,9 +263,53 @@ describe('useLayoutMeasurements', () => {
260
263
  })
261
264
  renderHook(() => useLayoutMeasurements())
262
265
  // bottomContainerPad = 500 - 400 - 0 = 100
263
- // expected = 100 + (20 + 8) = 128
266
+ // attributions lift = 16 (default attributions offsetHeight 16 + dividerGap 8 - primaryGap 8)
267
+ // expected = 100 + (20 + 16 + 8) = 144
264
268
  expect(layoutRefs.appContainerRef.current.style.setProperty)
265
- .toHaveBeenCalledWith('--right-offset-bottom', '128px')
269
+ .toHaveBeenCalledWith('--right-offset-bottom', '144px')
270
+ })
271
+
272
+ test('adds the attributions lift into right-offset-bottom so .im-o-app__right does not overlap a bottom-right box pushed up by tall docked attributions', () => {
273
+ const { layoutRefs } = setup({
274
+ refs: {
275
+ bottomRight: { offsetHeight: 20 },
276
+ attributions: { offsetHeight: 40 } // lift = 40 + 8 (dividerGap) - 8 (primaryGap) = 40
277
+ }
278
+ })
279
+ renderHook(() => useLayoutMeasurements())
280
+ // bottomContainerPad = 500 - 400 - 0 = 100; expected = 100 + (20 + 40 + 8) = 168
281
+ // Without the lift term this regressed to 128px, letting .im-o-app__right's
282
+ // bottom offset sit below the (margin-bottom-raised) bottom-right box's real top edge.
283
+ expect(layoutRefs.appContainerRef.current.style.setProperty)
284
+ .toHaveBeenCalledWith('--right-offset-bottom', '168px')
285
+ })
286
+
287
+ // Stacked attributions takes a flex line of its own BELOW the bottom-right row, lifting the
288
+ // row by the same amount the docked box's margin-bottom does — but as part of
289
+ // .im-o-app__bottom's own grown height, not as a margin, so --bottom-right-clearance is 0.
290
+ // Reading the clearance here (rather than the mode-independent lift) left the right column's
291
+ // bottom offset at the un-lifted position, so a right-bottom scale bar overlapped a
292
+ // bottom-right draw menu on mobile — the breakpoint where attributions always stacks.
293
+ test('adds the attributions lift into right-offset-bottom when attributions is stacked, not just docked', () => {
294
+ const { layoutRefs } = setup({
295
+ refs: {
296
+ bottom: { rect: { left: 0 } },
297
+ bottomRight: { offsetHeight: 20 },
298
+ attributions: { offsetHeight: 40 } // lift = 40 + 8 (dividerGap) - 8 (primaryGap) = 40
299
+ }
300
+ })
301
+ appendChild(layoutRefs.bottomRef.current, {}) // logo column
302
+ appendChild(layoutRefs.bottomRef.current, { offsetWidth: 192, rect: { left: 115 } }) // bottom-right column
303
+ layoutRefs.bottomRef.current.appendChild(layoutRefs.attributionsRef.current) // sibling of both columns
304
+ appendChild(layoutRefs.attributionsRef.current, { scrollWidth: 250 }) // too wide to fit beside the logo
305
+ renderHook(() => useLayoutMeasurements())
306
+ const spy = layoutRefs.appContainerRef.current.style.setProperty
307
+ expect(layoutRefs.attributionsRef.current.classList.contains('im-o-app__attributions--stacked')).toBe(true)
308
+ expect(spy).toHaveBeenCalledWith('--bottom-right-clearance', '0px')
309
+ // Same 168px as the docked case above, despite the 0px clearance.
310
+ expect(spy).toHaveBeenCalledWith('--right-offset-bottom', '168px')
311
+ // rightEffectiveBottom = 400 + 0 - 20 - 40 = 340; 340 - 50 (rightOffsetTop) - 8 = 282
312
+ expect(spy).toHaveBeenCalledWith('--right-top-max-height', '282px')
266
313
  })
267
314
 
268
315
  test('uses 0 when sub-slot refs have null current', () => {
@@ -274,7 +321,7 @@ describe('useLayoutMeasurements', () => {
274
321
  renderHook(() => useLayoutMeasurements())
275
322
  // With all sub-slot refs null, buttons = 0 ?? 0 = 0, so max-heights equal full column height
276
323
  expect(layoutRefs.appContainerRef.current.style.setProperty).toHaveBeenCalledWith('--left-top-panel-max-height', '332px')
277
- expect(layoutRefs.appContainerRef.current.style.setProperty).toHaveBeenCalledWith('--right-bottom-panel-max-height', '342px')
324
+ expect(layoutRefs.appContainerRef.current.style.setProperty).toHaveBeenCalledWith('--right-bottom-panel-max-height', '326px')
278
325
  })
279
326
 
280
327
  test('dispatches safe zone inset on desktop (post-batch RAF read only)', () => {
@@ -266,6 +266,12 @@
266
266
  flex-wrap: wrap; // lets a stacked attributions row wrap onto its own line
267
267
  justify-content: space-between;
268
268
  align-items: flex-end;
269
+ // No effect on a single line (the common case), only between wrapped lines — covers both
270
+ // a too-wide .im-o-app__bottom-right dropping below the logo/bottom-left column, and the
271
+ // stacked attributions row below that (see its own comment further down: this same gap
272
+ // replaces what used to be its one-off margin-top, so a doubly-wrapped bottom stays evenly
273
+ // spaced rather than gapping only at the line the margin happened to be declared on).
274
+ row-gap: var(--divider-gap);
269
275
  z-index: -2; // Support masking the viewport
270
276
  }
271
277
 
@@ -323,7 +329,9 @@
323
329
  // (being bottom-pinned) pushes the logo and bottom-right buttons up rather than squeezing
324
330
  // the text into an ever-narrower column. The oversized flex-basis plus negative
325
331
  // right/bottom/left margin bleeds that row flush to the true edges the same way the docked
326
- // box's absolute offsets do above.
332
+ // box's absolute offsets do above; the gap above it comes from .im-o-app__bottom's own
333
+ // row-gap (not a margin-top here), so it stays exactly one gap even if .im-o-app__bottom-right
334
+ // has also wrapped onto a line of its own above it.
327
335
 
328
336
  .im-o-app__attributions:not(:empty) {
329
337
  position: absolute;
@@ -337,7 +345,7 @@
337
345
  .im-o-app__attributions--stacked:not(:empty) {
338
346
  position: static;
339
347
  flex-basis: calc(100% + var(--primary-gap) * 2);
340
- margin: var(--divider-gap) calc(var(--primary-gap) * -1) calc(var(--primary-gap) * -1) calc(var(--primary-gap) * -1);
348
+ margin: 0 calc(var(--primary-gap) * -1) calc(var(--primary-gap) * -1);
341
349
  }
342
350
 
343
351
  // ---------------------------------------------------
@@ -1,3 +0,0 @@
1
- export const addKeyItem = ({ pluginState: { dispatch } }, keyDefinition) => {
2
- dispatch({ type: 'ADD_KEY_ITEM', payload: keyDefinition })
3
- }
@@ -1,14 +0,0 @@
1
- import { addKeyItem } from './addKeyItem.js'
2
-
3
- describe('addKeyItem', () => {
4
- it('dispatches an ADD_KEY_ITEM action with the key definition', () => {
5
- const dispatch = jest.fn()
6
-
7
- addKeyItem({ pluginState: { dispatch } }, { id: 'map-key', groupLabel: 'Map key' })
8
-
9
- expect(dispatch).toHaveBeenCalledWith({
10
- type: 'ADD_KEY_ITEM',
11
- payload: { id: 'map-key', groupLabel: 'Map key' }
12
- })
13
- })
14
- })
@@ -1,3 +0,0 @@
1
- export const removeKeyItem = ({ pluginState: { dispatch } }, keyDefinition) => {
2
- dispatch({ type: 'REMOVE_KEY_ITEM', payload: keyDefinition })
3
- }
@@ -1,14 +0,0 @@
1
- import { removeKeyItem } from './removeKeyItem.js'
2
-
3
- describe('removeKeyItem', () => {
4
- it('dispatches a REMOVE_KEY_ITEM action with the key definition', () => {
5
- const dispatch = jest.fn()
6
-
7
- removeKeyItem({ pluginState: { dispatch } }, { id: 'map-key' })
8
-
9
- expect(dispatch).toHaveBeenCalledWith({
10
- type: 'REMOVE_KEY_ITEM',
11
- payload: { id: 'map-key' }
12
- })
13
- })
14
- })