@defra/interactive-map 0.0.31-alpha → 0.0.33-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.
- package/README.md +2 -2
- package/assets/images/bright-ofm-thumb.jpg +0 -0
- package/assets/images/dark-ofm-thumb.jpg +0 -0
- package/assets/images/road-ofm-thumb.jpg +0 -0
- package/assets/templates/add-polygons.njk +143 -0
- package/assets/templates/add-symbols.njk +119 -0
- package/assets/templates/{map.njk → basic-map.njk} +9 -17
- package/assets/templates/button-map.njk +56 -0
- package/assets/templates/draw-tools.njk +161 -0
- package/assets/templates/marker-panel.njk +91 -0
- package/assets/templates/place-marker.njk +104 -0
- package/assets/templates/search-control.njk +98 -0
- package/assets/templates/select-feature.njk +101 -0
- package/assets/templates/style-switcher.njk +87 -0
- package/assets/templates/toggle-marker-label.njk +74 -0
- package/dist/css/index.css +1 -1
- package/dist/esm/im-core.js +1 -1
- package/dist/esm/im-shell.js +1 -1
- package/dist/umd/im-core.js +1 -1
- package/dist/umd/index.js +1 -1
- package/docs/api/map-style-config.md +27 -3
- package/docs/api.md +55 -0
- package/docs/assets/css/docusaurus.scss +22 -6
- package/docs/assets/images/add-polygons.jpg +0 -0
- package/docs/assets/images/add-symbols.jpg +0 -0
- package/docs/assets/images/hero.png +0 -0
- package/docs/assets/images/place-marker.jpg +0 -0
- package/docs/assets/images/screenshot.jpg +0 -0
- package/docs/examples/add-marker-with-panel.mdx +1 -1
- package/docs/examples/add-polygons.mdx +225 -0
- package/docs/examples/add-symbols.mdx +177 -0
- package/docs/examples/{draw.mdx → draw-tools.mdx} +2 -2
- package/docs/examples/index.mdx +38 -2
- package/docs/examples/place-marker.mdx +151 -0
- package/docs/plugins/datasets.md +144 -96
- package/docs/plugins/interact.md +1 -4
- package/docusaurus.config.cjs +5 -2
- package/govuk-prototype-kit.config.json +52 -2
- package/package.json +1 -1
- package/plugins/beta/datasets/dist/css/index.css +1 -2
- package/plugins/beta/datasets/dist/esm/im-datasets-ml-adapter.js +1 -1
- package/plugins/beta/datasets/dist/esm/im-datasets-plugin.js +1 -1
- package/plugins/beta/datasets/dist/umd/im-datasets-ml-adapter.js +1 -1
- package/plugins/beta/datasets/dist/umd/im-datasets-plugin.js +1 -1
- package/plugins/beta/datasets/dist/umd/index.js +1 -1
- package/plugins/beta/datasets/src/adapters/maplibre/registry/mapLibreDataset.js +27 -9
- package/plugins/beta/datasets/src/adapters/maplibre/registry/mapLibreDataset.test.js +46 -4
- package/plugins/beta/datasets/src/api/setFeatureVisibility.js +10 -1
- package/plugins/beta/datasets/src/api/setFeatureVisibility.test.js +48 -6
- package/plugins/beta/datasets/src/components/LayersMenu/Layers.module.scss +12 -12
- package/plugins/beta/datasets/src/reducers/datasetsToMenu.js +11 -6
- package/plugins/beta/datasets/src/reducers/datasetsToMenu.test.js +49 -0
- package/plugins/beta/datasets/src/reducers/mappedDatasetsReducer.js +8 -1
- package/plugins/beta/datasets/src/reducers/pluginState.js +1 -1
- package/plugins/beta/datasets/src/registry/dataset.js +7 -3
- package/plugins/beta/datasets/src/registry/dataset.test.js +14 -0
- package/plugins/beta/datasets/src/registry/dynamicGeoJson.js +1 -1
- package/plugins/beta/draw-es/dist/esm/im-draw-es-plugin.js +1 -1
- package/plugins/beta/draw-es/src/api/addFeature.js +12 -3
- package/plugins/beta/draw-es/src/events.js +8 -3
- package/plugins/beta/draw-es/src/events.test.js +2 -0
- package/plugins/beta/draw-es/src/graphic.js +26 -2
- package/plugins/beta/draw-ml/dist/esm/im-draw-ml-plugin.js +1 -1
- package/plugins/beta/draw-ml/dist/umd/im-draw-ml-plugin.js +1 -1
- package/plugins/beta/draw-ml/src/DrawInit.jsx +19 -0
- package/plugins/beta/draw-ml/src/api/newLine.js +1 -0
- package/plugins/beta/draw-ml/src/api/newPolygon.js +1 -0
- package/plugins/beta/draw-ml/src/modes/createDrawMode.js +52 -37
- package/plugins/beta/draw-ml/src/modes/editVertex/touchHandlers.js +1 -1
- package/plugins/beta/draw-ml/src/modes/editVertex/vertexOperations.js +5 -4
- package/plugins/beta/draw-ml/src/modes/editVertexMode.js +53 -21
- package/plugins/beta/draw-ol/dist/esm/DrawMode.js +1 -1
- package/plugins/beta/draw-ol/dist/esm/EditMode.js +1 -1
- package/plugins/beta/draw-ol/dist/esm/im-draw-ol-plugin.js +1 -1
- package/plugins/beta/draw-ol/src/core/OLDrawManager.js +3 -0
- package/plugins/beta/draw-ol/src/draw/DrawMode.js +57 -49
- package/plugins/beta/draw-ol/src/draw/drawInput.js +76 -32
- package/plugins/beta/draw-ol/src/edit/EditMode.js +4 -4
- package/plugins/beta/draw-ol/src/edit/keyboardHandler.js +4 -2
- package/plugins/beta/draw-ol/src/manifest.js +5 -1
- package/plugins/beta/draw-ol/src/reducer.js +1 -1
- package/plugins/beta/draw-ol/src/snap/snapEngine.js +75 -31
- package/plugins/beta/draw-ol/src/snap/snapGeometry.js +23 -46
- package/plugins/beta/draw-ol/src/snap/snapInteraction.js +30 -29
- package/plugins/beta/draw-ol/src/snap/snapManager.js +10 -1
- package/plugins/beta/draw-ol/src/utils/geometryHelpers.js +9 -3
- package/plugins/beta/draw-ol/src/utils/olCoords.js +2 -2
- package/plugins/beta/draw-ol/src/utils/spatial.js +4 -8
- package/plugins/beta/map-styles/src/mapStyles.scss +41 -41
- package/plugins/interact/dist/esm/im-interact-plugin.js +1 -1
- package/plugins/interact/dist/umd/im-interact-plugin.js +1 -1
- package/plugins/interact/dist/umd/index.js +1 -1
- package/plugins/interact/src/InteractInit.jsx +1 -3
- package/plugins/interact/src/InteractInit.test.js +0 -2
- package/plugins/interact/src/events.test.js +1 -1
- package/plugins/interact/src/hooks/useCrossHairVisibility.js +4 -10
- package/plugins/interact/src/hooks/useCrossHairVisibility.test.js +5 -10
- package/plugins/interact/src/hooks/useHighlightSync.js +1 -10
- package/plugins/interact/src/hooks/useHighlightSync.test.js +2 -21
- package/plugins/interact/src/hooks/useInteractionHandlers.js +5 -11
- package/plugins/interact/src/hooks/useInteractionHandlers.test.js +5 -25
- package/plugins/interact/src/reducer.js +4 -22
- package/plugins/interact/src/reducer.test.js +11 -39
- package/plugins/search/dist/css/index.css +5 -2
- package/plugins/search/src/components/Form/Form.module.scss +5 -2
- package/plugins/search/src/search.scss +4 -4
- package/providers/beta/openlayers/dist/esm/im-openlayers-provider.js +1 -1
- package/providers/beta/openlayers/dist/umd/im-openlayers-provider.js +1 -1
- package/providers/beta/openlayers/dist/umd/index.js +1 -1
- package/providers/beta/openlayers/src/utils/tileLayers.js +29 -2
- package/providers/beta/openlayers/src/utils/tileLayers.test.js +53 -1
- package/src/App/components/CrossHair/CrossHair.module.scss +8 -8
- package/src/App/components/MapButton/MapButton.jsx +14 -10
- package/src/App/components/MapButton/MapButton.module.scss +1 -1
- package/src/App/components/MapButton/MapButton.test.jsx +1 -1
- package/src/App/components/PopupMenu/PopupMenu.test.jsx +79 -0
- package/src/App/components/PopupMenu/usePopupMenu.js +61 -12
- package/src/App/hooks/useLayoutMeasurements.js +10 -2
- package/src/App/hooks/useResizeObserver.js +4 -2
- package/src/InteractiveMap/InteractiveMap.js +4 -4
- package/src/InteractiveMap/InteractiveMap.test.js +3 -3
- package/src/InteractiveMap/behaviourController.js +3 -1
- package/src/InteractiveMap/behaviourController.test.js +6 -3
- package/src/InteractiveMap/domStateManager.js +2 -0
- package/src/config/events.js +21 -3
- package/src/types.js +12 -3
- package/src/utils/findNextTabStop.js +3 -3
- package/src/utils/findNextTabStop.test.js +25 -1
- package/webpack.dev.mjs +1 -0
- package/docs/assets/images/screens-blue.jpg +0 -0
- package/docs/assets/images/screens-white.jpg +0 -0
|
@@ -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.openLayersProvider=t())}(this,()=>(()=>{"use strict";var e,t,r={},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-openlayers-provider.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.openLayersProvider:",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 s=f[p];if(s.getAttribute("src")==r||s.getAttribute("data-webpack")==t+i){u=s;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 l=(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(l.bind(null,void 0,{type:"timeout",target:u}),12e4);u.onerror=l.bind(null,u.onerror),u.onload=l.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(){var e,t,r="function"==typeof Symbol?Symbol:{},n=r.iterator||"@@iterator",o=r.toStringTag||"@@toStringTag";function i(r,n,o,i){var a=n&&n.prototype instanceof f?n:f,p=Object.create(a.prototype);return u(p,"_invoke",function(r,n,o){var i,a,u,f=0,p=o||[],s=!1,l={p:0,n:0,v:e,a:d,f:d.bind(e,4),d:function(t,r){return i=t,a=0,u=e,l.n=r,c}};function d(r,n){for(a=r,u=n,t=0;!s&&f&&!o&&t<p.length;t++){var o,i=p[t],d=l.p,v=i[2];r>3?(o=v===n)&&(u=i[(a=i[4])?5:(a=3,3)],i[4]=i[5]=e):i[0]<=d&&((o=r<2&&d<i[1])?(a=0,l.v=n,l.n=i[1]):d<v&&(o=r<3||i[0]>n||n>v)&&(i[4]=r,i[5]=n,l.n=v,a=0))}if(o||r>1)return c;throw s=!0,n}return function(o,p,v){if(f>1)throw TypeError("Generator is already running");for(s&&1===p&&d(p,v),a=p,u=v;(t=a<2?e:u)||!s;){i||(a?a<3?(a>1&&(l.n=-1),d(a,u)):l.n=u:l.v=u);try{if(f=2,i){if(a||(o="next"),t=i[o]){if(!(t=t.call(i,u)))throw TypeError("iterator result is not an object");if(!t.done)return t;u=t.value,a<2&&(a=0)}else 1===a&&(t=i.return)&&t.call(i),a<2&&(u=TypeError("The iterator does not provide a '"+o+"' method"),a=1);i=e}else if((t=(s=l.n<0)?u:r.call(n,l))!==c)break}catch(t){i=e,a=1,u=t}finally{f=1}}return{value:t,done:s}}}(r,o,i),!0),p}var c={};function f(){}function p(){}function s(){}t=Object.getPrototypeOf;var l=[][n]?t(t([][n]())):(u(t={},n,function(){return this}),t),d=s.prototype=f.prototype=Object.create(l);function v(e){return Object.setPrototypeOf?Object.setPrototypeOf(e,s):(e.__proto__=s,u(e,o,"GeneratorFunction")),e.prototype=Object.create(d),e}return p.prototype=s,u(d,"constructor",s),u(s,"constructor",p),p.displayName="GeneratorFunction",u(s,o,"GeneratorFunction"),u(d),u(d,o,"Generator"),u(d,n,function(){return this}),u(d,"toString",function(){return"[object Generator]"}),(a=function(){return{w:i,m:v}})()}function u(e,t,r,n){var o=Object.defineProperty;try{o({},"",{})}catch(e){o=0}u=function(e,t,r,n){function i(t,r){u(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))},u(e,t,r,n)}function c(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 f(){var e,t,r=(arguments.length>0&&void 0!==arguments[0]?arguments[0]:{}).zoomAlignment;return{checkDeviceCapabilities:function(){return{isSupported:!!document.createElement("canvas").getContext,error:"Canvas is not supported in this browser"}},load:(e=a().m(function e(){var t,n;return a().w(function(e){for(;;)switch(e.n){case 0:return e.n=1,o.e(142).then(o.bind(o,
|
|
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.openLayersProvider=t())}(this,()=>(()=>{"use strict";var e,t,r={},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-openlayers-provider.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.openLayersProvider:",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 s=f[p];if(s.getAttribute("src")==r||s.getAttribute("data-webpack")==t+i){u=s;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 l=(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(l.bind(null,void 0,{type:"timeout",target:u}),12e4);u.onerror=l.bind(null,u.onerror),u.onload=l.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(){var e,t,r="function"==typeof Symbol?Symbol:{},n=r.iterator||"@@iterator",o=r.toStringTag||"@@toStringTag";function i(r,n,o,i){var a=n&&n.prototype instanceof f?n:f,p=Object.create(a.prototype);return u(p,"_invoke",function(r,n,o){var i,a,u,f=0,p=o||[],s=!1,l={p:0,n:0,v:e,a:d,f:d.bind(e,4),d:function(t,r){return i=t,a=0,u=e,l.n=r,c}};function d(r,n){for(a=r,u=n,t=0;!s&&f&&!o&&t<p.length;t++){var o,i=p[t],d=l.p,v=i[2];r>3?(o=v===n)&&(u=i[(a=i[4])?5:(a=3,3)],i[4]=i[5]=e):i[0]<=d&&((o=r<2&&d<i[1])?(a=0,l.v=n,l.n=i[1]):d<v&&(o=r<3||i[0]>n||n>v)&&(i[4]=r,i[5]=n,l.n=v,a=0))}if(o||r>1)return c;throw s=!0,n}return function(o,p,v){if(f>1)throw TypeError("Generator is already running");for(s&&1===p&&d(p,v),a=p,u=v;(t=a<2?e:u)||!s;){i||(a?a<3?(a>1&&(l.n=-1),d(a,u)):l.n=u:l.v=u);try{if(f=2,i){if(a||(o="next"),t=i[o]){if(!(t=t.call(i,u)))throw TypeError("iterator result is not an object");if(!t.done)return t;u=t.value,a<2&&(a=0)}else 1===a&&(t=i.return)&&t.call(i),a<2&&(u=TypeError("The iterator does not provide a '"+o+"' method"),a=1);i=e}else if((t=(s=l.n<0)?u:r.call(n,l))!==c)break}catch(t){i=e,a=1,u=t}finally{f=1}}return{value:t,done:s}}}(r,o,i),!0),p}var c={};function f(){}function p(){}function s(){}t=Object.getPrototypeOf;var l=[][n]?t(t([][n]())):(u(t={},n,function(){return this}),t),d=s.prototype=f.prototype=Object.create(l);function v(e){return Object.setPrototypeOf?Object.setPrototypeOf(e,s):(e.__proto__=s,u(e,o,"GeneratorFunction")),e.prototype=Object.create(d),e}return p.prototype=s,u(d,"constructor",s),u(s,"constructor",p),p.displayName="GeneratorFunction",u(s,o,"GeneratorFunction"),u(d),u(d,o,"Generator"),u(d,n,function(){return this}),u(d,"toString",function(){return"[object Generator]"}),(a=function(){return{w:i,m:v}})()}function u(e,t,r,n){var o=Object.defineProperty;try{o({},"",{})}catch(e){o=0}u=function(e,t,r,n){function i(t,r){u(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))},u(e,t,r,n)}function c(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 f(){var e,t,r=(arguments.length>0&&void 0!==arguments[0]?arguments[0]:{}).zoomAlignment;return{checkDeviceCapabilities:function(){return{isSupported:!!document.createElement("canvas").getContext,error:"Canvas is not supported in this browser"}},load:(e=a().m(function e(){var t,n;return a().w(function(e){for(;;)switch(e.n){case 0:return e.n=1,o.e(142).then(o.bind(o,93));case 1:return t=e.v.default,n={zoomAlignment:r,crs:"EPSG:27700"},e.a(2,{MapProvider:t,mapProviderConfig:n})}},e)}),t=function(){var t=this,r=arguments;return new Promise(function(n,o){var i=e.apply(t,r);function a(e){c(i,n,o,a,u,"next",e)}function u(e){c(i,n,o,a,u,"throw",e)}a(void 0)})},function(){return t.apply(this,arguments)})}}return o.d(i,{default:()=>f}),i.default})());
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import XYZ from 'ol/source/XYZ.js'
|
|
2
|
+
import TileWMS from 'ol/source/TileWMS.js'
|
|
2
3
|
import VectorTileSource from 'ol/source/VectorTile.js'
|
|
3
4
|
import TileLayer from 'ol/layer/Tile.js'
|
|
4
5
|
import VectorTileLayer from 'ol/layer/VectorTile.js'
|
|
@@ -14,6 +15,7 @@ import { TILE_GRID_RESOLUTIONS, TILE_GRID_ORIGIN, TILE_SIZE } from '../defaults.
|
|
|
14
15
|
recordStyleLayer(true)
|
|
15
16
|
|
|
16
17
|
const CRS = 'EPSG:27700'
|
|
18
|
+
const SUPPORTED_MAP_STYLE_TYPES = ['vector', 'raster', 'wms', 'ogc-vt']
|
|
17
19
|
|
|
18
20
|
export function fetchWithTransform (url, resourceType, transformRequest) {
|
|
19
21
|
const result = transformRequest ? (transformRequest(url, resourceType) || {}) : {}
|
|
@@ -30,12 +32,28 @@ const createTileLoadFunction = (transformRequest) => (tile, src) => {
|
|
|
30
32
|
.catch(() => tile.setState(TileState.ERROR))
|
|
31
33
|
}
|
|
32
34
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
+
function createTileGrid () {
|
|
36
|
+
return new TileGrid({
|
|
35
37
|
resolutions: TILE_GRID_RESOLUTIONS,
|
|
36
38
|
origin: TILE_GRID_ORIGIN,
|
|
37
39
|
tileSize: TILE_SIZE
|
|
38
40
|
})
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function createWMSTileSource (url, params, transformRequest) {
|
|
44
|
+
const tileGrid = createTileGrid()
|
|
45
|
+
|
|
46
|
+
return new TileWMS({
|
|
47
|
+
url,
|
|
48
|
+
params,
|
|
49
|
+
projection: CRS,
|
|
50
|
+
tileGrid,
|
|
51
|
+
tileLoadFunction: transformRequest ? createTileLoadFunction(transformRequest) : undefined
|
|
52
|
+
})
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function createTileSource (url, transformRequest) {
|
|
56
|
+
const tileGrid = createTileGrid()
|
|
39
57
|
|
|
40
58
|
const tileUrlFunction = ([z, x, y]) => url
|
|
41
59
|
.replace('{z}', z)
|
|
@@ -51,6 +69,15 @@ export function createTileSource (url, transformRequest) {
|
|
|
51
69
|
}
|
|
52
70
|
|
|
53
71
|
export async function createMapStyleLayer (mapStyle, transformRequest) {
|
|
72
|
+
if (mapStyle.type && !SUPPORTED_MAP_STYLE_TYPES.includes(mapStyle.type)) {
|
|
73
|
+
throw new Error(`Unsupported map style type: '${mapStyle.type}'`)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (mapStyle.type === 'wms') {
|
|
77
|
+
const source = createWMSTileSource(mapStyle.url, mapStyle.params || {}, transformRequest)
|
|
78
|
+
return { layer: new TileLayer({ source }), source }
|
|
79
|
+
}
|
|
80
|
+
|
|
54
81
|
if (mapStyle.type === 'raster') {
|
|
55
82
|
const source = createTileSource(mapStyle.url, transformRequest)
|
|
56
83
|
return { layer: new TileLayer({ source }), source }
|
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
import XYZ from 'ol/source/XYZ.js'
|
|
2
|
+
import TileWMS from 'ol/source/TileWMS.js'
|
|
2
3
|
import TileGrid from 'ol/tilegrid/TileGrid.js'
|
|
3
4
|
import TileLayer from 'ol/layer/Tile.js'
|
|
4
5
|
import VectorTileSource from 'ol/source/VectorTile.js'
|
|
5
6
|
import VectorTileLayer from 'ol/layer/VectorTile.js'
|
|
6
7
|
import { stylefunction } from 'ol-mapbox-style'
|
|
7
|
-
import { createTileSource, createMapStyleLayer, createVectorTileLayer } from './tileLayers.js'
|
|
8
|
+
import { createTileSource, createWMSTileSource, createMapStyleLayer, createVectorTileLayer } from './tileLayers.js'
|
|
8
9
|
import { TILE_GRID_RESOLUTIONS, TILE_GRID_ORIGIN, TILE_SIZE } from '../defaults.js'
|
|
9
10
|
|
|
10
11
|
const mockTileGridInstance = {}
|
|
11
12
|
const mockSourceInstance = {}
|
|
13
|
+
const mockWMSSourceInstance = {}
|
|
12
14
|
const mockTileLayerInstance = {}
|
|
13
15
|
const mockVectorTileSourceInstance = {}
|
|
14
16
|
const mockOGCVectorTileSourceInstance = { supportedMediaTypes: [] }
|
|
@@ -16,6 +18,7 @@ const mockVectorTileLayerInstance = {}
|
|
|
16
18
|
const mockMVTInstance = { supportedMediaTypes: [] }
|
|
17
19
|
|
|
18
20
|
jest.mock('ol/source/XYZ.js', () => ({ __esModule: true, default: jest.fn(() => mockSourceInstance) }))
|
|
21
|
+
jest.mock('ol/source/TileWMS.js', () => ({ __esModule: true, default: jest.fn(() => mockWMSSourceInstance) }))
|
|
19
22
|
jest.mock('ol/layer/Tile.js', () => ({ __esModule: true, default: jest.fn(() => mockTileLayerInstance) }))
|
|
20
23
|
jest.mock('ol/tilegrid/TileGrid.js', () => ({ __esModule: true, default: jest.fn(() => mockTileGridInstance) }))
|
|
21
24
|
jest.mock('ol/TileState.js', () => ({ __esModule: true, default: { ERROR: 'error' } }))
|
|
@@ -126,6 +129,43 @@ describe('createTileSource', () => {
|
|
|
126
129
|
})
|
|
127
130
|
})
|
|
128
131
|
|
|
132
|
+
describe('createWMSTileSource', () => {
|
|
133
|
+
const wmsParams = { LAYERS: 'MyLayer', FORMAT: 'image/jpeg' }
|
|
134
|
+
|
|
135
|
+
beforeEach(() => jest.clearAllMocks())
|
|
136
|
+
|
|
137
|
+
it('creates TileGrid with correct OS tile grid config', () => {
|
|
138
|
+
createWMSTileSource('https://wms.example.com/wms', wmsParams, null)
|
|
139
|
+
expect(TileGrid).toHaveBeenCalledWith({
|
|
140
|
+
resolutions: TILE_GRID_RESOLUTIONS,
|
|
141
|
+
origin: TILE_GRID_ORIGIN,
|
|
142
|
+
tileSize: TILE_SIZE
|
|
143
|
+
})
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
it('creates TileWMS source with EPSG:27700 projection and params', () => {
|
|
147
|
+
createWMSTileSource('https://wms.example.com/wms', wmsParams, null)
|
|
148
|
+
expect(TileWMS).toHaveBeenCalledWith(expect.objectContaining({
|
|
149
|
+
url: 'https://wms.example.com/wms',
|
|
150
|
+
params: wmsParams,
|
|
151
|
+
projection: 'EPSG:27700',
|
|
152
|
+
tileGrid: mockTileGridInstance
|
|
153
|
+
}))
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
it('does not set tileLoadFunction when transformRequest is null', () => {
|
|
157
|
+
createWMSTileSource('https://wms.example.com/wms', wmsParams, null)
|
|
158
|
+
const { tileLoadFunction } = TileWMS.mock.calls[0][0]
|
|
159
|
+
expect(tileLoadFunction).toBeUndefined()
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
it('sets tileLoadFunction when transformRequest is provided', () => {
|
|
163
|
+
createWMSTileSource('https://wms.example.com/wms', wmsParams, jest.fn())
|
|
164
|
+
const { tileLoadFunction } = TileWMS.mock.calls[0][0]
|
|
165
|
+
expect(typeof tileLoadFunction).toBe('function')
|
|
166
|
+
})
|
|
167
|
+
})
|
|
168
|
+
|
|
129
169
|
describe('tileLoadFunction (via transformRequest)', () => {
|
|
130
170
|
const url = 'https://tiles.example.com/7/3/5'
|
|
131
171
|
const mockImg = { src: null }
|
|
@@ -191,6 +231,13 @@ describe('createMapStyleLayer', () => {
|
|
|
191
231
|
global.fetch = makeVectorFetchMock()
|
|
192
232
|
})
|
|
193
233
|
|
|
234
|
+
it('creates a WMS tile layer and source when mapStyle.type is wms', async () => {
|
|
235
|
+
const params = { LAYERS: 'MyLayer' }
|
|
236
|
+
const result = await createMapStyleLayer({ url: 'https://wms.example.com/wms', type: 'wms', params }, null)
|
|
237
|
+
expect(TileLayer).toHaveBeenCalledWith({ source: mockWMSSourceInstance })
|
|
238
|
+
expect(result).toEqual({ layer: mockTileLayerInstance, source: mockWMSSourceInstance })
|
|
239
|
+
})
|
|
240
|
+
|
|
194
241
|
it('creates a raster tile layer and source when mapStyle.type is raster', async () => {
|
|
195
242
|
const result = await createMapStyleLayer({ url: 'https://tiles.example.com/{z}/{x}/{y}', type: 'raster' }, null)
|
|
196
243
|
expect(TileLayer).toHaveBeenCalledWith({ source: mockSourceInstance })
|
|
@@ -210,6 +257,11 @@ describe('createMapStyleLayer', () => {
|
|
|
210
257
|
expect(result).toEqual({ layer: mockVectorTileLayerInstance, source: mockVectorTileSourceInstance })
|
|
211
258
|
})
|
|
212
259
|
|
|
260
|
+
it('throws when mapStyle.type is unsupported', async () => {
|
|
261
|
+
await expect(createMapStyleLayer({ url: styleUrl, type: 'unsupported' }, null))
|
|
262
|
+
.rejects.toThrow("Unsupported map style type: 'unsupported'")
|
|
263
|
+
})
|
|
264
|
+
|
|
213
265
|
it('creates a vector tile layer and source when mapStyle.type is missing', async () => {
|
|
214
266
|
const result = await createMapStyleLayer({ url: styleUrl }, null)
|
|
215
267
|
expect(VectorTileLayer).toHaveBeenCalledWith({ source: mockVectorTileSourceInstance, declutter: true })
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
.im-c-cross-hair {
|
|
2
|
-
|
|
2
|
+
position: absolute;
|
|
3
3
|
top: 0;
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
left: 0;
|
|
5
|
+
pointer-events: none;
|
|
6
|
+
margin: -19px 0 0 -19px;
|
|
7
7
|
}
|
|
8
8
|
|
|
9
9
|
.im-c-cross-hair path {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
10
|
+
stroke-width: 3;
|
|
11
|
+
stroke: var(--map-overlay-halo-color);
|
|
12
|
+
fill: var(--map-overlay-foreground-color);
|
|
13
|
+
paint-order: stroke fill;
|
|
14
14
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// components/MapButton.jsx
|
|
2
|
-
import React, { useState, useRef } from 'react'
|
|
2
|
+
import React, { useState, useRef, useCallback } from 'react'
|
|
3
3
|
import { stringToKebab } from '../../../utils/stringToKebab'
|
|
4
4
|
import { Tooltip } from '../Tooltip/Tooltip'
|
|
5
5
|
import { Icon } from '../Icon/Icon'
|
|
@@ -116,7 +116,7 @@ const getControlledElement = ({ idPrefix, panelId, buttonId, hasMenu }) => {
|
|
|
116
116
|
* @param {string} options.className - CSS classes for the button
|
|
117
117
|
* @param {Function} options.onClick - Click event handler
|
|
118
118
|
* @param {Function} options.onKeyUp - Key up event handler
|
|
119
|
-
* @param {
|
|
119
|
+
* @param {Function} options.setButtonRef - Stable ref callback storing the button DOM node in buttonRefs
|
|
120
120
|
* @param {boolean} options.isDisabled - Whether the button is disabled
|
|
121
121
|
* @param {boolean} options.isPressed - Whether the button is in pressed state
|
|
122
122
|
* @param {boolean} options.isExpanded - Whether content controlled by button is expanded
|
|
@@ -133,7 +133,7 @@ const buildButtonProps = ({
|
|
|
133
133
|
onClick,
|
|
134
134
|
onKeyUp,
|
|
135
135
|
onKeyDown,
|
|
136
|
-
|
|
136
|
+
setButtonRef,
|
|
137
137
|
isDisabled,
|
|
138
138
|
isPressed,
|
|
139
139
|
isExpanded,
|
|
@@ -159,16 +159,12 @@ const buildButtonProps = ({
|
|
|
159
159
|
onClick,
|
|
160
160
|
onKeyUp,
|
|
161
161
|
onKeyDown,
|
|
162
|
-
ref:
|
|
163
|
-
if (buttonRefs.current && buttonId) {
|
|
164
|
-
buttonRefs.current[buttonId] = el
|
|
165
|
-
}
|
|
166
|
-
},
|
|
162
|
+
ref: setButtonRef,
|
|
167
163
|
'aria-disabled': isDisabled || undefined,
|
|
168
164
|
'aria-expanded': ariaExpanded,
|
|
169
165
|
'aria-pressed': typeof isPressed === 'boolean' ? isPressed : undefined,
|
|
170
166
|
'aria-controls': controlledElement?.id,
|
|
171
|
-
'aria-haspopup': controlledElement?.type === 'popup'
|
|
167
|
+
'aria-haspopup': controlledElement?.type === 'popup' ? 'menu' : undefined,
|
|
172
168
|
...(href
|
|
173
169
|
? { href, target: '_blank', onKeyUp: handleKeyUp, role: 'button' }
|
|
174
170
|
: { type: 'button' })
|
|
@@ -224,6 +220,14 @@ export const MapButton = ({
|
|
|
224
220
|
const [menuRect, setMenuRect] = useState(null)
|
|
225
221
|
const menuRef = useRef(null)
|
|
226
222
|
|
|
223
|
+
// Stable across renders so Preact doesn't detach/reattach it (and transiently
|
|
224
|
+
// null out buttonRefs.current[buttonId]) on every unrelated re-render of this button.
|
|
225
|
+
const setButtonRef = useCallback((el) => {
|
|
226
|
+
if (buttonRefs.current && buttonId) {
|
|
227
|
+
buttonRefs.current[buttonId] = el
|
|
228
|
+
}
|
|
229
|
+
}, [buttonRefs, buttonId])
|
|
230
|
+
|
|
227
231
|
const Element = href ? 'a' : 'button'
|
|
228
232
|
const hasMenu = menuItems?.length >= 1
|
|
229
233
|
const showIcon = iconId || iconSvgContent || hasMenu
|
|
@@ -264,7 +268,7 @@ export const MapButton = ({
|
|
|
264
268
|
onClick: handleButtonClick,
|
|
265
269
|
onKeyUp: handleButtonKeyUp,
|
|
266
270
|
onKeyDown: handleButtonKeyDown,
|
|
267
|
-
|
|
271
|
+
setButtonRef,
|
|
268
272
|
isDisabled,
|
|
269
273
|
isPressed,
|
|
270
274
|
isExpanded,
|
|
@@ -125,7 +125,7 @@ describe('MapButton', () => {
|
|
|
125
125
|
renderButton({ idPrefix: 'prefix', menuItems: [{ label: 'Item' }] })
|
|
126
126
|
const button = getButton()
|
|
127
127
|
expect(button).toHaveAttribute('aria-controls', 'prefix-popup-test')
|
|
128
|
-
expect(button).toHaveAttribute('aria-haspopup', '
|
|
128
|
+
expect(button).toHaveAttribute('aria-haspopup', 'menu')
|
|
129
129
|
})
|
|
130
130
|
|
|
131
131
|
it('toggles popup aria-expanded and startPos', () => {
|
|
@@ -145,6 +145,75 @@ describe('PopupMenu', () => {
|
|
|
145
145
|
expect(mockSetIsOpen).toHaveBeenCalled()
|
|
146
146
|
})
|
|
147
147
|
|
|
148
|
+
describe('Tab key focus handoff', () => {
|
|
149
|
+
let before, instigatorEl, after1, after2
|
|
150
|
+
|
|
151
|
+
beforeEach(() => {
|
|
152
|
+
before = document.createElement('button')
|
|
153
|
+
instigatorEl = document.createElement('button')
|
|
154
|
+
after1 = document.createElement('button')
|
|
155
|
+
after2 = document.createElement('button')
|
|
156
|
+
document.body.append(before, instigatorEl, after1, after2)
|
|
157
|
+
// jsdom never computes layout, so offsetParent is always null — findTabStop
|
|
158
|
+
// treats that as "hidden" and skips it, so it must be stubbed for these to look focusable.
|
|
159
|
+
;[before, instigatorEl, after1, after2].forEach(el => Object.defineProperty(el, 'offsetParent', { value: true, configurable: true }))
|
|
160
|
+
mockUseApp.buttonRefs.current.instigator = instigatorEl
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
afterEach(() => {
|
|
164
|
+
[before, instigatorEl, after1, after2].forEach(el => el.remove())
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
it('moves focus to the tab stop after the instigator, not the first tab stop on the page', () => {
|
|
168
|
+
renderMenu()
|
|
169
|
+
fireEvent.keyDown(screen.getByRole('menu'), { key: 'Tab' })
|
|
170
|
+
expect(document.activeElement).toBe(after1)
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
it('Shift+Tab moves focus to the tab stop before the instigator', () => {
|
|
174
|
+
renderMenu()
|
|
175
|
+
fireEvent.keyDown(screen.getByRole('menu'), { key: 'Tab', shiftKey: true })
|
|
176
|
+
expect(document.activeElement).toBe(before)
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
it('falls back to computing the tab stop from the menu itself when the instigator is no longer available', () => {
|
|
180
|
+
mockUseApp.buttonRefs.current.instigator = null
|
|
181
|
+
renderMenu()
|
|
182
|
+
expect(() => fireEvent.keyDown(screen.getByRole('menu'), { key: 'Tab' })).not.toThrow()
|
|
183
|
+
expect(document.activeElement).toBe(before)
|
|
184
|
+
expect(mockUseApp.layoutRefs.viewportRef.current.focus).not.toHaveBeenCalled()
|
|
185
|
+
})
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
describe('Tab key inside a modal dialog', () => {
|
|
189
|
+
let dialogEl, insideBefore, instigatorEl, outsideAfter
|
|
190
|
+
|
|
191
|
+
beforeEach(() => {
|
|
192
|
+
dialogEl = document.createElement('div')
|
|
193
|
+
dialogEl.setAttribute('role', 'dialog')
|
|
194
|
+
dialogEl.setAttribute('aria-modal', 'true')
|
|
195
|
+
insideBefore = document.createElement('button')
|
|
196
|
+
instigatorEl = document.createElement('button')
|
|
197
|
+
dialogEl.append(insideBefore, instigatorEl)
|
|
198
|
+
outsideAfter = document.createElement('button')
|
|
199
|
+
document.body.append(dialogEl, outsideAfter)
|
|
200
|
+
;[insideBefore, instigatorEl, outsideAfter].forEach(el => Object.defineProperty(el, 'offsetParent', { value: true, configurable: true }))
|
|
201
|
+
mockUseApp.buttonRefs.current.instigator = instigatorEl
|
|
202
|
+
})
|
|
203
|
+
|
|
204
|
+
afterEach(() => {
|
|
205
|
+
dialogEl.remove()
|
|
206
|
+
outsideAfter.remove()
|
|
207
|
+
})
|
|
208
|
+
|
|
209
|
+
it('wraps within the dialog instead of escaping to a tab stop outside it', () => {
|
|
210
|
+
renderMenu()
|
|
211
|
+
fireEvent.keyDown(screen.getByRole('menu'), { key: 'Tab' })
|
|
212
|
+
expect(document.activeElement).toBe(insideBefore)
|
|
213
|
+
expect(document.activeElement).not.toBe(outsideAfter)
|
|
214
|
+
})
|
|
215
|
+
})
|
|
216
|
+
|
|
148
217
|
it('calls item onClick directly if no buttonConfig', () => {
|
|
149
218
|
renderMenu()
|
|
150
219
|
fireEvent.click(screen.getByText('Item 1'))
|
|
@@ -362,6 +431,16 @@ describe('PopupMenu', () => {
|
|
|
362
431
|
expect(focusSpy).toHaveBeenCalled()
|
|
363
432
|
})
|
|
364
433
|
|
|
434
|
+
it('Escape falls back to focusing viewport when instigator is no longer available', () => {
|
|
435
|
+
mockUseApp.buttonRefs.current.instigator = null
|
|
436
|
+
const focusSpy = jest.spyOn(mockUseApp.layoutRefs.viewportRef.current, 'focus')
|
|
437
|
+
renderMenu()
|
|
438
|
+
const ul = screen.getByRole('menu')
|
|
439
|
+
expect(() => fireEvent.keyDown(ul, { key: 'Escape' })).not.toThrow()
|
|
440
|
+
expect(focusSpy).toHaveBeenCalled()
|
|
441
|
+
focusSpy.mockRestore()
|
|
442
|
+
})
|
|
443
|
+
|
|
365
444
|
it('direct render without startIndex respects startPos="first" (covers initializer and effect)', () => {
|
|
366
445
|
renderDirect({ startPos: 'first' })
|
|
367
446
|
expectSelected('Item 1')
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { useState, useMemo, useEffect } from 'react'
|
|
2
2
|
import { stringToKebab } from '../../../utils/stringToKebab.js'
|
|
3
|
+
import { findTabStop } from '../../../utils/findNextTabStop.js'
|
|
3
4
|
import { useApp } from '../../store/appContext.js'
|
|
4
5
|
|
|
5
6
|
/**
|
|
@@ -86,6 +87,54 @@ const resolveItemFocus = (item, buttonConfig) => ({
|
|
|
86
87
|
keepFocus: buttonConfig[item.id]?.keepFocus ?? item.keepFocus
|
|
87
88
|
})
|
|
88
89
|
|
|
90
|
+
/**
|
|
91
|
+
* Focuses the instigator button if it's still in the DOM, otherwise falls back
|
|
92
|
+
* to the map viewport. The instigator can disappear while the menu is open
|
|
93
|
+
* (e.g. app state changes elsewhere remove/disable the triggering button),
|
|
94
|
+
* so callers must not assume it's still focusable.
|
|
95
|
+
*
|
|
96
|
+
* @param {HTMLElement|null} instigator - DOM node of the trigger button, may no longer exist.
|
|
97
|
+
* @param {object} viewportRef - Ref to the map viewport element, used as fallback.
|
|
98
|
+
*/
|
|
99
|
+
const focusInstigatorOrViewport = (instigator, viewportRef) => {
|
|
100
|
+
if (instigator) {
|
|
101
|
+
instigator.focus()
|
|
102
|
+
} else {
|
|
103
|
+
viewportRef.current?.focus()
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// A Panel rendered as a modal dialog (see Panel.jsx's computePanelState/getPanelRole)
|
|
108
|
+
// carries both of these attributes together and traps focus within itself via
|
|
109
|
+
// useModalPanelBehaviour's focusin redirect — tab-stop search must stay inside it.
|
|
110
|
+
const MODAL_DIALOG_SELECTOR = '[role="dialog"][aria-modal="true"]'
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Moves focus to the tab stop immediately after (or before, for Shift+Tab)
|
|
114
|
+
* the instigator button, continuing the page's natural tab sequence rather
|
|
115
|
+
* than returning focus into the popup itself. The popup is rendered via a
|
|
116
|
+
* portal elsewhere in the DOM, so the browser's own Tab default action can't
|
|
117
|
+
* be relied on to continue from the instigator — it must be computed and
|
|
118
|
+
* applied manually. The search is scoped to the nearest modal dialog ancestor
|
|
119
|
+
* (if any): searching the whole document would let it pick a tab stop outside
|
|
120
|
+
* an open modal panel, which the app's own modal focus-trap then immediately
|
|
121
|
+
* yanks focus back from — landing on the trap's container div, which has no
|
|
122
|
+
* visible focus styling, making focus appear to vanish. Falls back to
|
|
123
|
+
* computing the tab stop from the menu element itself if the instigator is
|
|
124
|
+
* gone, so focus still lands on a real neighbouring control rather than
|
|
125
|
+
* jumping to an unrelated part of the app.
|
|
126
|
+
*
|
|
127
|
+
* @param {HTMLElement|null} instigator - DOM node of the trigger button, may no longer exist.
|
|
128
|
+
* @param {boolean} isShiftTab - Whether Shift+Tab (move backwards) was pressed.
|
|
129
|
+
* @param {HTMLElement} fallbackEl - Menu element to anchor from if the instigator is gone.
|
|
130
|
+
*/
|
|
131
|
+
const focusAdjacentTabStop = (instigator, isShiftTab, fallbackEl) => {
|
|
132
|
+
const anchor = instigator ?? fallbackEl
|
|
133
|
+
const root = anchor.closest?.(MODAL_DIALOG_SELECTOR) ?? document
|
|
134
|
+
const nextStop = findTabStop({ el: anchor, direction: isShiftTab ? 'prev' : 'next', root })
|
|
135
|
+
nextStop?.focus()
|
|
136
|
+
}
|
|
137
|
+
|
|
89
138
|
const handleMenuEnter = (event, { items, index, disabledButtons, activateCtx, instigator, dispatch, viewportRef, setIsOpen }) => {
|
|
90
139
|
event.preventDefault()
|
|
91
140
|
const item = items[index]
|
|
@@ -98,7 +147,7 @@ const handleMenuEnter = (event, { items, index, disabledButtons, activateCtx, in
|
|
|
98
147
|
}
|
|
99
148
|
activateItem(event, item, activateCtx)
|
|
100
149
|
if (keepFocus) {
|
|
101
|
-
instigator
|
|
150
|
+
focusInstigatorOrViewport(instigator, viewportRef)
|
|
102
151
|
setIsOpen(false)
|
|
103
152
|
return
|
|
104
153
|
}
|
|
@@ -122,7 +171,7 @@ const handleMenuSpace = (event, { items, index, disabledButtons, activateCtx, in
|
|
|
122
171
|
activateItem(event, item, activateCtx)
|
|
123
172
|
if (!(item.isPressed !== undefined || item.pressedWhen)) {
|
|
124
173
|
if (keepFocus) {
|
|
125
|
-
instigator
|
|
174
|
+
focusInstigatorOrViewport(instigator, viewportRef)
|
|
126
175
|
} else {
|
|
127
176
|
requestAnimationFrame(() => viewportRef.current?.focus())
|
|
128
177
|
}
|
|
@@ -149,11 +198,9 @@ const handleMenuSpace = (event, { items, index, disabledButtons, activateCtx, in
|
|
|
149
198
|
* @returns {Function} onKeyDown handler for the menu element.
|
|
150
199
|
*/
|
|
151
200
|
const createMenuKeyDownHandler = ({ items, visibleIndices, index, setIndex, disabledButtons, instigator, setIsOpen, activateCtx, dispatch, viewportRef }) => {
|
|
152
|
-
const closeAndFocus = (event
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
}
|
|
156
|
-
instigator.focus()
|
|
201
|
+
const closeAndFocus = (event) => {
|
|
202
|
+
event.preventDefault()
|
|
203
|
+
focusInstigatorOrViewport(instigator, viewportRef)
|
|
157
204
|
setIsOpen(false)
|
|
158
205
|
}
|
|
159
206
|
|
|
@@ -179,11 +226,13 @@ const createMenuKeyDownHandler = ({ items, visibleIndices, index, setIndex, disa
|
|
|
179
226
|
|
|
180
227
|
return (event) => {
|
|
181
228
|
if (['Escape', 'Esc'].includes(event.key)) {
|
|
182
|
-
closeAndFocus(event
|
|
229
|
+
closeAndFocus(event)
|
|
183
230
|
return
|
|
184
231
|
}
|
|
185
232
|
if (event.key === 'Tab') {
|
|
186
|
-
|
|
233
|
+
event.preventDefault()
|
|
234
|
+
focusAdjacentTabStop(instigator, event.shiftKey, event.currentTarget)
|
|
235
|
+
setIsOpen(false)
|
|
187
236
|
return
|
|
188
237
|
}
|
|
189
238
|
if (['ArrowDown', 'ArrowUp'].includes(event.key)) {
|
|
@@ -195,7 +244,7 @@ const createMenuKeyDownHandler = ({ items, visibleIndices, index, setIndex, disa
|
|
|
195
244
|
return
|
|
196
245
|
}
|
|
197
246
|
if (event.key === 'End' && visibleIndices.length) {
|
|
198
|
-
setIndex(visibleIndices[visibleIndices.length - 1])
|
|
247
|
+
setIndex(visibleIndices[visibleIndices.length - 1]) // NOSONAR .length - 1 used instead of .at(-1) for wider browser support
|
|
199
248
|
return
|
|
200
249
|
}
|
|
201
250
|
if (event.key === 'Enter') {
|
|
@@ -274,7 +323,7 @@ export const usePopupMenu = ({
|
|
|
274
323
|
setIsOpen(false)
|
|
275
324
|
activateItem(event, item, activateCtx)
|
|
276
325
|
if (keepFocus) {
|
|
277
|
-
instigator
|
|
326
|
+
focusInstigatorOrViewport(instigator, viewportRef)
|
|
278
327
|
} else {
|
|
279
328
|
viewportRef.current?.focus()
|
|
280
329
|
}
|
|
@@ -285,7 +334,7 @@ export const usePopupMenu = ({
|
|
|
285
334
|
if (startPos === 'first') {
|
|
286
335
|
setIndex(visibleIndices[0] ?? -1)
|
|
287
336
|
} else if (startPos === 'last') {
|
|
288
|
-
setIndex(visibleIndices[visibleIndices.length - 1] ?? -1)
|
|
337
|
+
setIndex(visibleIndices[visibleIndices.length - 1] ?? -1) // NOSONAR .length - 1 used instead of .at(-1) for wider browser support
|
|
289
338
|
} else {
|
|
290
339
|
// No action
|
|
291
340
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useLayoutEffect } from 'react'
|
|
1
|
+
import { useLayoutEffect, useMemo } from 'react'
|
|
2
2
|
import { useResizeObserver } from './useResizeObserver.js'
|
|
3
3
|
import { useApp } from '../store/appContext.js'
|
|
4
4
|
import { useMap } from '../store/mapContext.js'
|
|
@@ -153,7 +153,15 @@ export function useLayoutMeasurements () {
|
|
|
153
153
|
// actions buttons, etc.). Safe zone is intentionally not dispatched here —
|
|
154
154
|
// that is Effect 2's responsibility.
|
|
155
155
|
// --------------------------------
|
|
156
|
-
|
|
156
|
+
// Stable reference — all entries are ref objects from useApp that never change.
|
|
157
|
+
// Prevents useResizeObserver's effect from re-running (and cancelling its RAF)
|
|
158
|
+
// on every render due to a new array literal being passed each time.
|
|
159
|
+
const observedRefs = useMemo(
|
|
160
|
+
() => [bannerRef, mainRef, topRef, topLeftColRef, topRightColRef, actionsRef, bottomRef, bottomRightRef, leftTopRef, leftBottomRef, rightTopRef, rightBottomRef, drawerRef],
|
|
161
|
+
[]
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
useResizeObserver(observedRefs, () => {
|
|
157
165
|
calculateLayout(layoutRefs)
|
|
158
166
|
})
|
|
159
167
|
}
|
|
@@ -3,6 +3,8 @@ import { useEffect, useRef } from 'react'
|
|
|
3
3
|
export function useResizeObserver (targetRefs, callback) {
|
|
4
4
|
const frameRef = useRef()
|
|
5
5
|
const prevSizes = useRef(new WeakMap()) // track sizes per element
|
|
6
|
+
const callbackRef = useRef(callback)
|
|
7
|
+
callbackRef.current = callback
|
|
6
8
|
|
|
7
9
|
useEffect(() => {
|
|
8
10
|
const refs = Array.isArray(targetRefs) ? targetRefs : [targetRefs]
|
|
@@ -33,7 +35,7 @@ export function useResizeObserver (targetRefs, callback) {
|
|
|
33
35
|
// causing a ResizeObserver loop under synchronous renderers like preact.
|
|
34
36
|
cancelAnimationFrame(frameRef.current)
|
|
35
37
|
frameRef.current = requestAnimationFrame(() => {
|
|
36
|
-
for (const entry of changedEntries) {
|
|
38
|
+
for (const entry of changedEntries) { callbackRef.current(entry) }
|
|
37
39
|
})
|
|
38
40
|
}
|
|
39
41
|
})
|
|
@@ -46,7 +48,7 @@ export function useResizeObserver (targetRefs, callback) {
|
|
|
46
48
|
cancelAnimationFrame(frameRef.current)
|
|
47
49
|
}
|
|
48
50
|
}
|
|
49
|
-
}, [targetRefs
|
|
51
|
+
}, [targetRefs]) // callback intentionally omitted — always read from callbackRef
|
|
50
52
|
|
|
51
53
|
return { frameRef }
|
|
52
54
|
}
|
|
@@ -225,8 +225,8 @@ export default class InteractiveMap {
|
|
|
225
225
|
}
|
|
226
226
|
})
|
|
227
227
|
|
|
228
|
-
updateDOMState(this)
|
|
229
|
-
this.eventBus.emit(events.APP_OPENED, { statePreserved: false })
|
|
228
|
+
const { isFullscreen } = updateDOMState(this)
|
|
229
|
+
this.eventBus.emit(events.APP_OPENED, { statePreserved: false, isFullscreen })
|
|
230
230
|
} catch (err) {
|
|
231
231
|
this._openButton?.removeAttribute('style')
|
|
232
232
|
renderError(this.rootEl, this.config.genericErrorText)
|
|
@@ -304,9 +304,9 @@ export default class InteractiveMap {
|
|
|
304
304
|
this._openButton.style.display = 'none'
|
|
305
305
|
}
|
|
306
306
|
|
|
307
|
-
updateDOMState(this)
|
|
307
|
+
const { isFullscreen } = updateDOMState(this)
|
|
308
308
|
|
|
309
|
-
this.eventBus.emit(events.APP_OPENED, { statePreserved: true })
|
|
309
|
+
this.eventBus.emit(events.APP_OPENED, { statePreserved: true, isFullscreen })
|
|
310
310
|
}
|
|
311
311
|
|
|
312
312
|
/**
|
|
@@ -23,7 +23,7 @@ jest.mock('./behaviourController.js', () => ({
|
|
|
23
23
|
setupBehavior: jest.fn(),
|
|
24
24
|
shouldLoadComponent: jest.fn(() => true)
|
|
25
25
|
}))
|
|
26
|
-
jest.mock('./domStateManager.js', () => ({ updateDOMState: jest.fn(), removeLoadingState: jest.fn() }))
|
|
26
|
+
jest.mock('./domStateManager.js', () => ({ updateDOMState: jest.fn().mockReturnValue({ isFullscreen: false }), removeLoadingState: jest.fn() }))
|
|
27
27
|
jest.mock('./renderError.js', () => ({ renderError: jest.fn() }))
|
|
28
28
|
jest.mock('../config/mergeConfig.js', () => ({ mergeConfig: jest.fn(cfg => cfg) }))
|
|
29
29
|
const mockBreakpointDetector = {
|
|
@@ -183,7 +183,7 @@ describe('InteractiveMap — loadApp', () => {
|
|
|
183
183
|
await map.loadApp()
|
|
184
184
|
expect(map.on).toBe(originalOn)
|
|
185
185
|
expect(typeof map.someMethod).toBe('function')
|
|
186
|
-
expect(map.eventBus.emit).toHaveBeenCalledWith('app:opened', { statePreserved: false })
|
|
186
|
+
expect(map.eventBus.emit).toHaveBeenCalledWith('app:opened', { statePreserved: false, isFullscreen: false })
|
|
187
187
|
})
|
|
188
188
|
})
|
|
189
189
|
|
|
@@ -436,7 +436,7 @@ describe('InteractiveMap — hideApp and showApp', () => {
|
|
|
436
436
|
expect(map.rootEl.style.display).toBe('')
|
|
437
437
|
expect(mockButtonInstance.style.display).toBe('none')
|
|
438
438
|
expect(updateDOMState).toHaveBeenCalledWith(map)
|
|
439
|
-
expect(map.eventBus.emit).toHaveBeenCalledWith('app:opened', { statePreserved: true })
|
|
439
|
+
expect(map.eventBus.emit).toHaveBeenCalledWith('app:opened', { statePreserved: true, isFullscreen: false })
|
|
440
440
|
})
|
|
441
441
|
|
|
442
442
|
it('showApp: works without _openButton', () => {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { getQueryParam } from '../utils/queryString.js'
|
|
2
2
|
import { isHybridFullscreen } from '../utils/getIsFullscreen.js'
|
|
3
3
|
import { updateDOMState } from './domStateManager.js'
|
|
4
|
+
import { EVENTS } from '../config/events.js'
|
|
4
5
|
|
|
5
6
|
// -----------------------------------------------------------------------------
|
|
6
7
|
// Public API
|
|
@@ -62,7 +63,8 @@ function setupBehavior (mapInstance) {
|
|
|
62
63
|
mapInstance.loadApp()
|
|
63
64
|
} else {
|
|
64
65
|
// Map is showing - update DOM state for fullscreen/inline transition
|
|
65
|
-
updateDOMState(mapInstance)
|
|
66
|
+
const { isFullscreen } = updateDOMState(mapInstance)
|
|
67
|
+
mapInstance.eventBus.emit(EVENTS.APP_FULLSCREEN_CHANGE, { isFullscreen })
|
|
66
68
|
}
|
|
67
69
|
} else if (mapInstance._root) {
|
|
68
70
|
mapInstance.hideApp()
|