From cb22828b4bef01877a5557dbda0ad83f997aa324 Mon Sep 17 00:00:00 2001 From: Dan Leech Date: Wed, 4 Mar 2026 20:08:53 +0000 Subject: [PATCH 1/8] ESRI view.click performance fix --- package-lock.json | 521 ++++++++++++++++------ package.json | 4 +- plugins/interact/src/InteractInit.jsx | 34 +- plugins/interact/src/InteractInit.test.js | 24 +- plugins/interact/src/events.js | 28 +- plugins/interact/src/events.test.js | 41 +- providers/beta/esri/src/esriProvider.js | 2 + providers/beta/esri/src/mapEvents.js | 37 +- src/App/hooks/useMarkersAPI.js | 8 +- src/App/layout/Layout.jsx | 2 +- src/App/renderer/HtmlElementHost.jsx | 1 + src/App/renderer/pluginWrapper.js | 5 +- src/App/renderer/slots.js | 2 +- src/App/store/AppProvider.jsx | 1 + 14 files changed, 514 insertions(+), 196 deletions(-) diff --git a/package-lock.json b/package-lock.json index 1a80ed2c..10b5b139 100644 --- a/package-lock.json +++ b/package-lock.json @@ -29,7 +29,7 @@ "tslib": "^2.8.1" }, "devDependencies": { - "@arcgis/core": "^4.34.8", + "@arcgis/core": "^5.0.9", "@babel/core": "^7.28.0", "@babel/plugin-transform-runtime": "^7.29.0", "@babel/preset-env": "^7.28.0", @@ -90,9 +90,6 @@ "webpack-merge": "^6.0.1", "webpack-remove-empty-scripts": "^1.1.1" }, - "peerDependencies": { - "@arcgis/core": "^4.34.8" - }, "peerDependenciesMeta": { "@arcgis/core": { "optional": true @@ -132,7 +129,9 @@ "license": "MIT" }, "node_modules/@amcharts/amcharts5": { - "version": "5.14.4", + "version": "5.15.6", + "resolved": "https://registry.npmjs.org/@amcharts/amcharts5/-/amcharts5-5.15.6.tgz", + "integrity": "sha512-6gVWngHgkMN+v6dJhZ4XJE3fcHjvD5hyjgyNnfvVE1H0ukmHpfFmhI7cCX0cgy0NRyZc4fwgxDMZsiNA7yYKjA==", "dev": true, "license": "SEE LICENSE IN LICENSE", "dependencies": { @@ -164,32 +163,36 @@ } }, "node_modules/@arcgis/core": { - "version": "4.34.8", + "version": "5.0.9", + "resolved": "https://registry.npmjs.org/@arcgis/core/-/core-5.0.9.tgz", + "integrity": "sha512-z9LNHKKH+denjz8Ko5csj4QbKBldBXLie3cx3bGC4py+b6NNGK1i5iNAiPPGQ3IyxcotrA76j6BnQG0zuwK8HQ==", "dev": true, - "license": "SEE LICENSE IN copyright.txt", + "license": "SEE LICENSE IN LICENSE.md", "dependencies": { - "@amcharts/amcharts5": "~5.14.1", - "@arcgis/toolkit": "^4.34.0", + "@amcharts/amcharts5": "~5.15.5", + "@arcgis/toolkit": "^5.0.0", "@esri/arcgis-html-sanitizer": "~4.1.0", - "@esri/calcite-components": "^3.3.2", - "@vaadin/grid": "~24.9.1", - "@zip.js/zip.js": "~2.8.7", + "@esri/calcite-components": "^5.0.2", + "@vaadin/grid": "~25.0.3", + "@zip.js/zip.js": "~2.8.16", "luxon": "~3.7.2", - "marked": "~16.3.0", + "marked": "~17.0.3", "tslib": "^2.8.1" } }, "node_modules/@arcgis/lumina": { - "version": "4.34.9", + "version": "5.0.9", + "resolved": "https://registry.npmjs.org/@arcgis/lumina/-/lumina-5.0.9.tgz", + "integrity": "sha512-6lvzfi7BKEUehRUdixEsm0r90pB9znE2AmdFkzKeKl3Eav2viFE/Eca4m8rYB4KWowYBxAZX1EyrRLUvDET+Tw==", "dev": true, "license": "SEE LICENSE IN LICENSE.md", "dependencies": { - "@arcgis/toolkit": "~4.34.9", + "@arcgis/toolkit": "~5.0.9", "csstype": "^3.1.3", "tslib": "^2.8.1" }, "peerDependencies": { - "@lit/context": "^1.1.5", + "@lit/context": "^1.1.6", "lit": "^3.3.0" }, "peerDependenciesMeta": { @@ -199,7 +202,9 @@ } }, "node_modules/@arcgis/toolkit": { - "version": "4.34.9", + "version": "5.0.9", + "resolved": "https://registry.npmjs.org/@arcgis/toolkit/-/toolkit-5.0.9.tgz", + "integrity": "sha512-D8M0k/SWZZme4FkQav773s+85deqnY3Cxm7u9Tryuvgq0E1t/SoFs/XepYzcO4H5YG18j6hyDHGlXrd+kdKBtA==", "dev": true, "license": "SEE LICENSE IN LICENSE.md", "dependencies": { @@ -4737,6 +4742,8 @@ }, "node_modules/@esri/arcgis-html-sanitizer": { "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@esri/arcgis-html-sanitizer/-/arcgis-html-sanitizer-4.1.0.tgz", + "integrity": "sha512-einEveDJ/k1180NOp78PB/4Hje9eBy3dyOGLLtLn6bSkizpUfCwuYBIXOA7Y3F/k/BsTQXgKqUVwQ0eiscWMdA==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -4777,17 +4784,19 @@ } }, "node_modules/@esri/calcite-components": { - "version": "3.3.3", + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@esri/calcite-components/-/calcite-components-5.0.2.tgz", + "integrity": "sha512-AE8AQBsdpWWAPJEBc/nabJbdN+H+iuVBJv215mm8xjlyqpHd6BLsD8nG/K8IywDFHeB9TyHLQdYCk/sNiyQo7w==", "dev": true, "license": "SEE LICENSE.md", "dependencies": { - "@arcgis/lumina": ">=4.34.0-next.158 <4.35.0", - "@arcgis/toolkit": ">=4.34.0-next.158 <4.35.0", - "@esri/calcite-ui-icons": "4.3.0", + "@arcgis/lumina": ">=5.0.0-next.144 <6.0.0", + "@arcgis/toolkit": ">=5.0.0-next.144 <6.0.0", + "@esri/calcite-ui-icons": "4.4.0", "@floating-ui/dom": "^1.6.12", "@floating-ui/utils": "^0.2.8", "@types/sortablejs": "^1.15.8", - "color": "^5.0.0", + "color": "^5.0.3", "composed-offset-position": "^0.0.6", "es-toolkit": "^1.39.8", "focus-trap": "^7.6.5", @@ -4799,7 +4808,9 @@ } }, "node_modules/@esri/calcite-ui-icons": { - "version": "4.3.0", + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@esri/calcite-ui-icons/-/calcite-ui-icons-4.4.0.tgz", + "integrity": "sha512-PcCGId6vKBysRuuoCIYrVsaC+YVv1lFMqVUQn3Qna0BHOIyf7Mursf3NStELNjYdmL0MRYfmLDEDYQwnAJ/tSA==", "dev": true, "license": "SEE LICENSE.md", "bin": { @@ -4807,29 +4818,37 @@ } }, "node_modules/@floating-ui/core": { - "version": "1.7.4", + "version": "1.7.5", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.5.tgz", + "integrity": "sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==", "dev": true, "license": "MIT", "dependencies": { - "@floating-ui/utils": "^0.2.10" + "@floating-ui/utils": "^0.2.11" } }, "node_modules/@floating-ui/dom": { - "version": "1.7.5", + "version": "1.7.6", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.6.tgz", + "integrity": "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==", "dev": true, "license": "MIT", "dependencies": { - "@floating-ui/core": "^1.7.4", - "@floating-ui/utils": "^0.2.10" + "@floating-ui/core": "^1.7.5", + "@floating-ui/utils": "^0.2.11" } }, "node_modules/@floating-ui/utils": { - "version": "0.2.10", + "version": "0.2.11", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.11.tgz", + "integrity": "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==", "dev": true, "license": "MIT" }, "node_modules/@foliojs-fork/fontkit": { "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@foliojs-fork/fontkit/-/fontkit-1.9.2.tgz", + "integrity": "sha512-IfB5EiIb+GZk+77TRB86AHroVaqfq8JRFlUbz0WEwsInyCG0epX2tCPOy+UfaWPju30DeVoUAXfzWXmhn753KA==", "dev": true, "license": "MIT", "dependencies": { @@ -4845,6 +4864,8 @@ }, "node_modules/@foliojs-fork/linebreak": { "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@foliojs-fork/linebreak/-/linebreak-1.1.2.tgz", + "integrity": "sha512-ZPohpxxbuKNE0l/5iBJnOAfUaMACwvUIKCvqtWGKIMv1lPYoNjYXRfhi9FeeV9McBkBLxsMFWTVVhHJA8cyzvg==", "dev": true, "license": "MIT", "dependencies": { @@ -4854,6 +4875,8 @@ }, "node_modules/@foliojs-fork/pdfkit": { "version": "0.15.3", + "resolved": "https://registry.npmjs.org/@foliojs-fork/pdfkit/-/pdfkit-0.15.3.tgz", + "integrity": "sha512-Obc0Wmy3bm7BINFVvPhcl2rnSSK61DQrlHU8aXnAqDk9LCjWdUOPwhgD8Ywz5VtuFjRxmVOM/kQ/XLIBjDvltw==", "dev": true, "license": "MIT", "dependencies": { @@ -4866,6 +4889,8 @@ }, "node_modules/@foliojs-fork/restructure": { "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@foliojs-fork/restructure/-/restructure-2.0.2.tgz", + "integrity": "sha512-59SgoZ3EXbkfSX7b63tsou/SDGzwUEK6MuB5sKqgVK1/XE0fxmpsOb9DQI8LXW3KfGnAjImCGhhEb7uPPAUVNA==", "dev": true, "license": "MIT" }, @@ -4919,6 +4944,8 @@ }, "node_modules/@interactjs/types": { "version": "1.10.27", + "resolved": "https://registry.npmjs.org/@interactjs/types/-/types-1.10.27.tgz", + "integrity": "sha512-BUdv0cvs4H5ODuwft2Xp4eL8Vmi3LcihK42z0Ft/FbVJZoRioBsxH+LlsBdK4tAie7PqlKGy+1oyOncu1nQ6eA==", "dev": true, "license": "MIT" }, @@ -5977,11 +6004,15 @@ }, "node_modules/@lit-labs/ssr-dom-shim": { "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@lit-labs/ssr-dom-shim/-/ssr-dom-shim-1.5.1.tgz", + "integrity": "sha512-Aou5UdlSpr5whQe8AA/bZG0jMj96CoJIWbGfZ91qieWu5AWUMKw8VR/pAkQkJYvBNhmCcWnZlyyk5oze8JIqYA==", "dev": true, "license": "BSD-3-Clause" }, "node_modules/@lit/reactive-element": { "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@lit/reactive-element/-/reactive-element-2.1.2.tgz", + "integrity": "sha512-pbCDiVMnne1lYUIaYNN5wrwQXDtHaYtg7YEFPeW+hws6U47WeFvISGUWekPGKWOP1ygrs0ef0o1VJMk1exos5A==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -7698,6 +7729,8 @@ }, "node_modules/@open-wc/dedupe-mixin": { "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@open-wc/dedupe-mixin/-/dedupe-mixin-1.4.0.tgz", + "integrity": "sha512-Sj7gKl1TLcDbF7B6KUhtvr+1UCxdhMbNY5KxdU5IfMFWqL8oy1ZeAcCANjoB1TL0AJTcPmcCFsCbHf8X2jGDUA==", "dev": true, "license": "MIT" }, @@ -8182,14 +8215,6 @@ "version": "1.0.0-next.29", "license": "MIT" }, - "node_modules/@polymer/polymer": { - "version": "3.5.2", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "@webcomponents/shadycss": "^1.9.1" - } - }, "node_modules/@react-foundry/anchor": { "version": "0.1.7", "resolved": "https://registry.npmjs.org/@react-foundry/anchor/-/anchor-0.1.7.tgz", @@ -9780,6 +9805,8 @@ }, "node_modules/@types/d3": { "version": "7.4.3", + "resolved": "https://registry.npmjs.org/@types/d3/-/d3-7.4.3.tgz", + "integrity": "sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww==", "dev": true, "license": "MIT", "dependencies": { @@ -9817,11 +9844,15 @@ }, "node_modules/@types/d3-array": { "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz", + "integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==", "dev": true, "license": "MIT" }, "node_modules/@types/d3-axis": { "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-axis/-/d3-axis-3.0.6.tgz", + "integrity": "sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw==", "dev": true, "license": "MIT", "dependencies": { @@ -9830,6 +9861,8 @@ }, "node_modules/@types/d3-brush": { "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-brush/-/d3-brush-3.0.6.tgz", + "integrity": "sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A==", "dev": true, "license": "MIT", "dependencies": { @@ -9838,16 +9871,22 @@ }, "node_modules/@types/d3-chord": { "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-chord/-/d3-chord-3.0.6.tgz", + "integrity": "sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg==", "dev": true, "license": "MIT" }, "node_modules/@types/d3-color": { "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", "dev": true, "license": "MIT" }, "node_modules/@types/d3-contour": { "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-contour/-/d3-contour-3.0.6.tgz", + "integrity": "sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg==", "dev": true, "license": "MIT", "dependencies": { @@ -9857,16 +9896,22 @@ }, "node_modules/@types/d3-delaunay": { "version": "6.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-delaunay/-/d3-delaunay-6.0.4.tgz", + "integrity": "sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw==", "dev": true, "license": "MIT" }, "node_modules/@types/d3-dispatch": { "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-dispatch/-/d3-dispatch-3.0.7.tgz", + "integrity": "sha512-5o9OIAdKkhN1QItV2oqaE5KMIiXAvDWBDPrD85e58Qlz1c1kI/J0NcqbEG88CoTwJrYe7ntUCVfeUl2UJKbWgA==", "dev": true, "license": "MIT" }, "node_modules/@types/d3-drag": { "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-3.0.7.tgz", + "integrity": "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==", "dev": true, "license": "MIT", "dependencies": { @@ -9875,16 +9920,22 @@ }, "node_modules/@types/d3-dsv": { "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-dsv/-/d3-dsv-3.0.7.tgz", + "integrity": "sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g==", "dev": true, "license": "MIT" }, "node_modules/@types/d3-ease": { "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", + "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==", "dev": true, "license": "MIT" }, "node_modules/@types/d3-fetch": { "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-fetch/-/d3-fetch-3.0.7.tgz", + "integrity": "sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA==", "dev": true, "license": "MIT", "dependencies": { @@ -9893,16 +9944,22 @@ }, "node_modules/@types/d3-force": { "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@types/d3-force/-/d3-force-3.0.10.tgz", + "integrity": "sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw==", "dev": true, "license": "MIT" }, "node_modules/@types/d3-format": { "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-format/-/d3-format-3.0.4.tgz", + "integrity": "sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g==", "dev": true, "license": "MIT" }, "node_modules/@types/d3-geo": { "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@types/d3-geo/-/d3-geo-3.1.0.tgz", + "integrity": "sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ==", "dev": true, "license": "MIT", "dependencies": { @@ -9911,11 +9968,15 @@ }, "node_modules/@types/d3-hierarchy": { "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@types/d3-hierarchy/-/d3-hierarchy-3.1.1.tgz", + "integrity": "sha512-QwjxA3+YCKH3N1Rs3uSiSy1bdxlLB1uUiENXeJudBoAFvtDuswUxLcanoOaR2JYn1melDTuIXR8VhnVyI3yG/A==", "dev": true, "license": "MIT" }, "node_modules/@types/d3-interpolate": { "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", "dev": true, "license": "MIT", "dependencies": { @@ -9924,26 +9985,36 @@ }, "node_modules/@types/d3-path": { "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz", + "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==", "dev": true, "license": "MIT" }, "node_modules/@types/d3-polygon": { "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-polygon/-/d3-polygon-3.0.2.tgz", + "integrity": "sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA==", "dev": true, "license": "MIT" }, "node_modules/@types/d3-quadtree": { "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-quadtree/-/d3-quadtree-3.0.6.tgz", + "integrity": "sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg==", "dev": true, "license": "MIT" }, "node_modules/@types/d3-random": { "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-random/-/d3-random-3.0.3.tgz", + "integrity": "sha512-Imagg1vJ3y76Y2ea0871wpabqp613+8/r0mCLEBfdtqC7xMSfj9idOnmBYyMoULfHePJyxMAw3nWhJxzc+LFwQ==", "dev": true, "license": "MIT" }, "node_modules/@types/d3-sankey": { "version": "0.11.2", + "resolved": "https://registry.npmjs.org/@types/d3-sankey/-/d3-sankey-0.11.2.tgz", + "integrity": "sha512-U6SrTWUERSlOhnpSrgvMX64WblX1AxX6nEjI2t3mLK2USpQrnbwYYK+AS9SwiE7wgYmOsSSKoSdr8aoKBH0HgQ==", "dev": true, "license": "MIT", "dependencies": { @@ -9952,11 +10023,15 @@ }, "node_modules/@types/d3-sankey/node_modules/@types/d3-path": { "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-1.0.11.tgz", + "integrity": "sha512-4pQMp8ldf7UaB/gR8Fvvy69psNHkTpD/pVw3vmEi8iZAB9EPMBruB1JvHO4BIq9QkUUd2lV1F5YXpMNj7JPBpw==", "dev": true, "license": "MIT" }, "node_modules/@types/d3-sankey/node_modules/@types/d3-shape": { "version": "1.3.12", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-1.3.12.tgz", + "integrity": "sha512-8oMzcd4+poSLGgV0R1Q1rOlx/xdmozS4Xab7np0eamFFUYq71AU9pOCJEFnkXW2aI/oXdVYJzw6pssbSut7Z9Q==", "dev": true, "license": "MIT", "dependencies": { @@ -9965,6 +10040,8 @@ }, "node_modules/@types/d3-scale": { "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz", + "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", "dev": true, "license": "MIT", "dependencies": { @@ -9973,16 +10050,22 @@ }, "node_modules/@types/d3-scale-chromatic": { "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", + "integrity": "sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ==", "dev": true, "license": "MIT" }, "node_modules/@types/d3-selection": { "version": "3.0.11", + "resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.11.tgz", + "integrity": "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==", "dev": true, "license": "MIT" }, "node_modules/@types/d3-shape": { "version": "3.1.8", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.8.tgz", + "integrity": "sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==", "dev": true, "license": "MIT", "dependencies": { @@ -9991,21 +10074,29 @@ }, "node_modules/@types/d3-time": { "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz", + "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==", "dev": true, "license": "MIT" }, "node_modules/@types/d3-time-format": { "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-time-format/-/d3-time-format-4.0.3.tgz", + "integrity": "sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg==", "dev": true, "license": "MIT" }, "node_modules/@types/d3-timer": { "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", + "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", "dev": true, "license": "MIT" }, "node_modules/@types/d3-transition": { "version": "3.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.9.tgz", + "integrity": "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==", "dev": true, "license": "MIT", "dependencies": { @@ -10014,6 +10105,8 @@ }, "node_modules/@types/d3-zoom": { "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.8.tgz", + "integrity": "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==", "dev": true, "license": "MIT", "dependencies": { @@ -10210,6 +10303,8 @@ }, "node_modules/@types/polylabel": { "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@types/polylabel/-/polylabel-1.1.3.tgz", + "integrity": "sha512-9Zw2KoDpi+T4PZz2G6pO2xArE0m/GSMTW1MIxF2s8ZY8x9XDO6fv9um0ydRGvcbkFLlaq8yNK6eZxnmMZtDgWQ==", "dev": true, "license": "MIT" }, @@ -10319,6 +10414,8 @@ }, "node_modules/@types/sortablejs": { "version": "1.15.9", + "resolved": "https://registry.npmjs.org/@types/sortablejs/-/sortablejs-1.15.9.tgz", + "integrity": "sha512-7HP+rZGE2p886PKV9c9OJzLBI6BBJu1O7lJGYnPyG3fS4/duUCcngkNCjsLwIMV+WMqANe3tt4irrXHSIe68OQ==", "dev": true, "license": "MIT" }, @@ -10336,6 +10433,8 @@ }, "node_modules/@types/svg-arc-to-cubic-bezier": { "version": "3.2.3", + "resolved": "https://registry.npmjs.org/@types/svg-arc-to-cubic-bezier/-/svg-arc-to-cubic-bezier-3.2.3.tgz", + "integrity": "sha512-UNOnbTtl0nVTm8hwKaz5R5VZRvSulFMGojO5+Q7yucKxBoCaTtS4ibSQVRHo5VW5AaRo145U8p1Vfg5KrYe9Bg==", "dev": true, "license": "MIT" }, @@ -10346,6 +10445,8 @@ }, "node_modules/@types/trusted-types": { "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", "dev": true, "license": "MIT" }, @@ -10388,102 +10489,91 @@ "license": "ISC" }, "node_modules/@vaadin/a11y-base": { - "version": "24.9.10", + "version": "25.0.5", + "resolved": "https://registry.npmjs.org/@vaadin/a11y-base/-/a11y-base-25.0.5.tgz", + "integrity": "sha512-yE7p9pk1PrQUfur/0rR/jKb5uodYmpn9BXPUb8rfH92CSzOo92QHTO3tC3j7XEIY2UqCMUQo4qI7PQ5EURmSYg==", "dev": true, "license": "Apache-2.0", "dependencies": { "@open-wc/dedupe-mixin": "^1.3.0", - "@polymer/polymer": "^3.0.0", - "@vaadin/component-base": "~24.9.10", + "@vaadin/component-base": "~25.0.5", "lit": "^3.0.0" } }, "node_modules/@vaadin/checkbox": { - "version": "24.9.10", + "version": "25.0.5", + "resolved": "https://registry.npmjs.org/@vaadin/checkbox/-/checkbox-25.0.5.tgz", + "integrity": "sha512-DcHbkxZdUzw9/kDwJ4aJw7hFK61d16kCWyKfyqhcr9klL2AjPlUMt7IcKGugw83gvAtS4MaSD/KvtHappYIr5Q==", "dev": true, "license": "Apache-2.0", "dependencies": { "@open-wc/dedupe-mixin": "^1.3.0", - "@polymer/polymer": "^3.0.0", - "@vaadin/a11y-base": "~24.9.10", - "@vaadin/component-base": "~24.9.10", - "@vaadin/field-base": "~24.9.10", - "@vaadin/vaadin-lumo-styles": "~24.9.10", - "@vaadin/vaadin-material-styles": "~24.9.10", - "@vaadin/vaadin-themable-mixin": "~24.9.10", + "@vaadin/a11y-base": "~25.0.5", + "@vaadin/component-base": "~25.0.5", + "@vaadin/field-base": "~25.0.5", + "@vaadin/vaadin-themable-mixin": "~25.0.5", "lit": "^3.0.0" } }, "node_modules/@vaadin/component-base": { - "version": "24.9.10", + "version": "25.0.5", + "resolved": "https://registry.npmjs.org/@vaadin/component-base/-/component-base-25.0.5.tgz", + "integrity": "sha512-iKQ+haAZkAz3HGFN8pxk03DF15XxYRksqkQlvOHUCWDBTSp/pvtJmL1oaXNkjzoZCJdeIYZTWsrqQB9GXIRWeg==", "dev": true, "license": "Apache-2.0", "dependencies": { "@open-wc/dedupe-mixin": "^1.3.0", - "@polymer/polymer": "^3.0.0", "@vaadin/vaadin-development-mode-detector": "^2.0.0", "@vaadin/vaadin-usage-statistics": "^2.1.0", "lit": "^3.0.0" } }, "node_modules/@vaadin/field-base": { - "version": "24.9.10", + "version": "25.0.5", + "resolved": "https://registry.npmjs.org/@vaadin/field-base/-/field-base-25.0.5.tgz", + "integrity": "sha512-2oBmGgDB7DEhMpBnhW408MsRwetrMN0oDHFOzpI1iHvu9YOGBQUznZNCWKIgBPbGfIJv35epoS4wW/g6/ChHRQ==", "dev": true, "license": "Apache-2.0", "dependencies": { "@open-wc/dedupe-mixin": "^1.3.0", - "@polymer/polymer": "^3.0.0", - "@vaadin/a11y-base": "~24.9.10", - "@vaadin/component-base": "~24.9.10", + "@vaadin/a11y-base": "~25.0.5", + "@vaadin/component-base": "~25.0.5", "lit": "^3.0.0" } }, "node_modules/@vaadin/grid": { - "version": "24.9.10", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@open-wc/dedupe-mixin": "^1.3.0", - "@polymer/polymer": "^3.0.0", - "@vaadin/a11y-base": "~24.9.10", - "@vaadin/checkbox": "~24.9.10", - "@vaadin/component-base": "~24.9.10", - "@vaadin/lit-renderer": "~24.9.10", - "@vaadin/text-field": "~24.9.10", - "@vaadin/vaadin-lumo-styles": "~24.9.10", - "@vaadin/vaadin-material-styles": "~24.9.10", - "@vaadin/vaadin-themable-mixin": "~24.9.10", - "lit": "^3.0.0" - } - }, - "node_modules/@vaadin/icon": { - "version": "24.9.10", + "version": "25.0.5", + "resolved": "https://registry.npmjs.org/@vaadin/grid/-/grid-25.0.5.tgz", + "integrity": "sha512-HIzRSTbtdRu/f9keZ++vPtHa49zHbXPvmWKFNp0Tyle05zprhcde5je3+rQukr+YIZfSJyB3gnPOolPRt51uDw==", "dev": true, "license": "Apache-2.0", "dependencies": { "@open-wc/dedupe-mixin": "^1.3.0", - "@polymer/polymer": "^3.0.0", - "@vaadin/component-base": "~24.9.10", - "@vaadin/vaadin-lumo-styles": "~24.9.10", - "@vaadin/vaadin-themable-mixin": "~24.9.10", + "@vaadin/a11y-base": "~25.0.5", + "@vaadin/checkbox": "~25.0.5", + "@vaadin/component-base": "~25.0.5", + "@vaadin/lit-renderer": "~25.0.5", + "@vaadin/text-field": "~25.0.5", + "@vaadin/vaadin-themable-mixin": "~25.0.5", "lit": "^3.0.0" } }, "node_modules/@vaadin/input-container": { - "version": "24.9.10", + "version": "25.0.5", + "resolved": "https://registry.npmjs.org/@vaadin/input-container/-/input-container-25.0.5.tgz", + "integrity": "sha512-mozENjFDmIehVLjgA59njaLgxjo9uSFN2vwxK8wOavxlDBhR2PzxBCQXS0WAZq2rwxUyDIFP6cCavi1WMb2UFg==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@polymer/polymer": "^3.0.0", - "@vaadin/component-base": "~24.9.10", - "@vaadin/vaadin-lumo-styles": "~24.9.10", - "@vaadin/vaadin-material-styles": "~24.9.10", - "@vaadin/vaadin-themable-mixin": "~24.9.10", + "@vaadin/component-base": "~25.0.5", + "@vaadin/vaadin-themable-mixin": "~25.0.5", "lit": "^3.0.0" } }, "node_modules/@vaadin/lit-renderer": { - "version": "24.9.10", + "version": "25.0.5", + "resolved": "https://registry.npmjs.org/@vaadin/lit-renderer/-/lit-renderer-25.0.5.tgz", + "integrity": "sha512-j/7fsXUSUif57G/bDvn7wNhtBtK8Th6IeCwYfbd17KEDc7asVSdEJO1Y6rwQ+sKJijiGdkNRe12FODB3lwc7Aw==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -10491,60 +10581,44 @@ } }, "node_modules/@vaadin/text-field": { - "version": "24.9.10", + "version": "25.0.5", + "resolved": "https://registry.npmjs.org/@vaadin/text-field/-/text-field-25.0.5.tgz", + "integrity": "sha512-EzwXpXgBOd8EcLJUIszyIP4VAKKR9+3sL2fwQpVpezqsDb3aN+WiOKT9+LF+N51evbXmFs2uenmyBRpngU3Qyw==", "dev": true, "license": "Apache-2.0", "dependencies": { "@open-wc/dedupe-mixin": "^1.3.0", - "@polymer/polymer": "^3.0.0", - "@vaadin/a11y-base": "~24.9.10", - "@vaadin/component-base": "~24.9.10", - "@vaadin/field-base": "~24.9.10", - "@vaadin/input-container": "~24.9.10", - "@vaadin/vaadin-lumo-styles": "~24.9.10", - "@vaadin/vaadin-material-styles": "~24.9.10", - "@vaadin/vaadin-themable-mixin": "~24.9.10", + "@vaadin/a11y-base": "~25.0.5", + "@vaadin/component-base": "~25.0.5", + "@vaadin/field-base": "~25.0.5", + "@vaadin/input-container": "~25.0.5", + "@vaadin/vaadin-themable-mixin": "~25.0.5", "lit": "^3.0.0" } }, "node_modules/@vaadin/vaadin-development-mode-detector": { "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@vaadin/vaadin-development-mode-detector/-/vaadin-development-mode-detector-2.0.7.tgz", + "integrity": "sha512-9FhVhr0ynSR3X2ao+vaIEttcNU5XfzCbxtmYOV8uIRnUCtNgbvMOIcyGBvntsX9I5kvIP2dV3cFAOG9SILJzEA==", "dev": true, "license": "Apache-2.0" }, - "node_modules/@vaadin/vaadin-lumo-styles": { - "version": "24.9.10", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@polymer/polymer": "^3.0.0", - "@vaadin/component-base": "~24.9.10", - "@vaadin/icon": "~24.9.10", - "@vaadin/vaadin-themable-mixin": "~24.9.10" - } - }, - "node_modules/@vaadin/vaadin-material-styles": { - "version": "24.9.10", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@polymer/polymer": "^3.0.0", - "@vaadin/component-base": "~24.9.10", - "@vaadin/vaadin-themable-mixin": "~24.9.10" - } - }, "node_modules/@vaadin/vaadin-themable-mixin": { - "version": "24.9.10", + "version": "25.0.5", + "resolved": "https://registry.npmjs.org/@vaadin/vaadin-themable-mixin/-/vaadin-themable-mixin-25.0.5.tgz", + "integrity": "sha512-U9pGUa8n+BgmJsEoCtFYTnUPLLFE/mty8aplue8FtCQ6NQGGWE0rmlEGQwMCO/H8//OBEFI1VqGGa5ceFKtvaw==", "dev": true, "license": "Apache-2.0", "dependencies": { "@open-wc/dedupe-mixin": "^1.3.0", - "lit": "^3.0.0", - "style-observer": "^0.0.8" + "@vaadin/component-base": "~25.0.5", + "lit": "^3.0.0" } }, "node_modules/@vaadin/vaadin-usage-statistics": { "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@vaadin/vaadin-usage-statistics/-/vaadin-usage-statistics-2.1.3.tgz", + "integrity": "sha512-8r4TNknD7OJQADe3VygeofFR7UNAXZ2/jjBFP5dgI8+2uMfnuGYgbuHivasKr9WSQ64sPej6m8rDoM1uSllXjQ==", "dev": true, "hasInstallScript": true, "license": "Apache-2.0", @@ -10692,11 +10766,6 @@ "@xtuc/long": "4.2.2" } }, - "node_modules/@webcomponents/shadycss": { - "version": "1.11.2", - "dev": true, - "license": "BSD-3-Clause" - }, "node_modules/@webpack-cli/configtest": { "version": "3.0.1", "dev": true, @@ -10747,7 +10816,9 @@ "license": "Apache-2.0" }, "node_modules/@zip.js/zip.js": { - "version": "2.8.16", + "version": "2.8.22", + "resolved": "https://registry.npmjs.org/@zip.js/zip.js/-/zip.js-2.8.22.tgz", + "integrity": "sha512-0KlzbVR6r8irIX2o3zvUlosBDef62VDl47oUfa1U/qgEs67h4/eGBrX/6HWa1RQbt+J6sAeVmtyFKbTHNdF8qQ==", "dev": true, "license": "BSD-3-Clause", "engines": { @@ -11442,6 +11513,8 @@ }, "node_modules/base64-js": { "version": "1.3.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.1.tgz", + "integrity": "sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g==", "dev": true, "license": "MIT" }, @@ -11655,6 +11728,8 @@ }, "node_modules/brotli": { "version": "1.3.3", + "resolved": "https://registry.npmjs.org/brotli/-/brotli-1.3.3.tgz", + "integrity": "sha512-oTKjJdShmDuGW94SyyaoQvAjf30dZaHnjJ8uAF+u2/vGJkJbJPJAT1gDiOJP5v1Zb6f9KEyW/1HpuaWIXtGHPg==", "dev": true, "license": "MIT", "dependencies": { @@ -12272,6 +12347,8 @@ }, "node_modules/clone": { "version": "1.0.4", + "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", + "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==", "dev": true, "license": "MIT", "engines": { @@ -12335,6 +12412,8 @@ }, "node_modules/color": { "version": "5.0.3", + "resolved": "https://registry.npmjs.org/color/-/color-5.0.3.tgz", + "integrity": "sha512-ezmVcLR3xAVp8kYOm4GS45ZLLgIE6SPAFoduLr6hTDajwb3KZ2F46gulK3XpcwRFb5KKGCSezCBAY4Dw4HsyXA==", "dev": true, "license": "MIT", "dependencies": { @@ -12347,6 +12426,8 @@ }, "node_modules/color-convert": { "version": "3.1.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-3.1.3.tgz", + "integrity": "sha512-fasDH2ont2GqF5HpyO4w0+BcewlhHEZOFn9c1ckZdHpJ56Qb7MHhH/IcJZbBGgvdtwdwNbLvxiBEdg336iA9Sg==", "dev": true, "license": "MIT", "dependencies": { @@ -12358,6 +12439,8 @@ }, "node_modules/color-name": { "version": "2.1.0", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-2.1.0.tgz", + "integrity": "sha512-1bPaDNFm0axzE4MEAzKPuqKWeRaT43U/hyxKPBdqTfmPF+d6n7FSoTFxLVULUJOmiLp01KjhIPPH+HrXZJN4Rg==", "dev": true, "license": "MIT", "engines": { @@ -12366,6 +12449,8 @@ }, "node_modules/color-string": { "version": "2.1.4", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-2.1.4.tgz", + "integrity": "sha512-Bb6Cq8oq0IjDOe8wJmi4JeNn763Xs9cfrBcaylK1tPypWzyoy2G3l90v9k64kjphl/ZJjPIShFztenRomi8WTg==", "dev": true, "license": "MIT", "dependencies": { @@ -12428,6 +12513,8 @@ }, "node_modules/composed-offset-position": { "version": "0.0.6", + "resolved": "https://registry.npmjs.org/composed-offset-position/-/composed-offset-position-0.0.6.tgz", + "integrity": "sha512-Q7dLompI6lUwd7LWyIcP66r4WcS9u7AL2h8HaeipiRfCRPLMWqRx8fYsjb4OHi6UQFifO7XtNC2IlEJ1ozIFxw==", "dev": true, "license": "MIT", "peerDependencies": { @@ -12729,6 +12816,8 @@ }, "node_modules/crypto-js": { "version": "4.2.0", + "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz", + "integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==", "dev": true, "license": "MIT" }, @@ -13012,6 +13101,8 @@ }, "node_modules/cssfilter": { "version": "0.0.10", + "resolved": "https://registry.npmjs.org/cssfilter/-/cssfilter-0.0.10.tgz", + "integrity": "sha512-FAaLDaplstoRsDR8XGYH51znUN0UY7nMc6Z9/fvE8EXGwvJE9hu7W2vHwx1+bd6gCYnln9nLbzxFTrcO9YQDZw==", "dev": true, "license": "MIT" }, @@ -13163,6 +13254,8 @@ }, "node_modules/d3": { "version": "7.9.0", + "resolved": "https://registry.npmjs.org/d3/-/d3-7.9.0.tgz", + "integrity": "sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA==", "dev": true, "license": "ISC", "dependencies": { @@ -13203,6 +13296,8 @@ }, "node_modules/d3-array": { "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", "dev": true, "license": "ISC", "dependencies": { @@ -13214,6 +13309,8 @@ }, "node_modules/d3-axis": { "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-axis/-/d3-axis-3.0.0.tgz", + "integrity": "sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==", "dev": true, "license": "ISC", "engines": { @@ -13222,6 +13319,8 @@ }, "node_modules/d3-brush": { "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-brush/-/d3-brush-3.0.0.tgz", + "integrity": "sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==", "dev": true, "license": "ISC", "dependencies": { @@ -13237,6 +13336,8 @@ }, "node_modules/d3-chord": { "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-chord/-/d3-chord-3.0.1.tgz", + "integrity": "sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==", "dev": true, "license": "ISC", "dependencies": { @@ -13248,6 +13349,8 @@ }, "node_modules/d3-color": { "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", "dev": true, "license": "ISC", "engines": { @@ -13256,6 +13359,8 @@ }, "node_modules/d3-contour": { "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-contour/-/d3-contour-4.0.2.tgz", + "integrity": "sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==", "dev": true, "license": "ISC", "dependencies": { @@ -13267,6 +13372,8 @@ }, "node_modules/d3-delaunay": { "version": "6.0.4", + "resolved": "https://registry.npmjs.org/d3-delaunay/-/d3-delaunay-6.0.4.tgz", + "integrity": "sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==", "dev": true, "license": "ISC", "dependencies": { @@ -13278,6 +13385,8 @@ }, "node_modules/d3-dispatch": { "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz", + "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==", "dev": true, "license": "ISC", "engines": { @@ -13286,6 +13395,8 @@ }, "node_modules/d3-drag": { "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz", + "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==", "dev": true, "license": "ISC", "dependencies": { @@ -13298,6 +13409,8 @@ }, "node_modules/d3-dsv": { "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dsv/-/d3-dsv-3.0.1.tgz", + "integrity": "sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==", "dev": true, "license": "ISC", "dependencies": { @@ -13322,6 +13435,8 @@ }, "node_modules/d3-ease": { "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", "dev": true, "license": "BSD-3-Clause", "engines": { @@ -13330,6 +13445,8 @@ }, "node_modules/d3-fetch": { "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-fetch/-/d3-fetch-3.0.1.tgz", + "integrity": "sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==", "dev": true, "license": "ISC", "dependencies": { @@ -13341,6 +13458,8 @@ }, "node_modules/d3-force": { "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-force/-/d3-force-3.0.0.tgz", + "integrity": "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==", "dev": true, "license": "ISC", "dependencies": { @@ -13354,6 +13473,8 @@ }, "node_modules/d3-format": { "version": "3.1.2", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.2.tgz", + "integrity": "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==", "dev": true, "license": "ISC", "engines": { @@ -13362,6 +13483,8 @@ }, "node_modules/d3-geo": { "version": "3.1.1", + "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-3.1.1.tgz", + "integrity": "sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==", "dev": true, "license": "ISC", "dependencies": { @@ -13373,6 +13496,8 @@ }, "node_modules/d3-hierarchy": { "version": "3.1.2", + "resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz", + "integrity": "sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==", "dev": true, "license": "ISC", "engines": { @@ -13381,6 +13506,8 @@ }, "node_modules/d3-interpolate": { "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", "dev": true, "license": "ISC", "dependencies": { @@ -13392,6 +13519,8 @@ }, "node_modules/d3-path": { "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", "dev": true, "license": "ISC", "engines": { @@ -13400,6 +13529,8 @@ }, "node_modules/d3-polygon": { "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-polygon/-/d3-polygon-3.0.1.tgz", + "integrity": "sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==", "dev": true, "license": "ISC", "engines": { @@ -13408,6 +13539,8 @@ }, "node_modules/d3-quadtree": { "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-3.0.1.tgz", + "integrity": "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==", "dev": true, "license": "ISC", "engines": { @@ -13416,6 +13549,8 @@ }, "node_modules/d3-random": { "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-random/-/d3-random-3.0.1.tgz", + "integrity": "sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==", "dev": true, "license": "ISC", "engines": { @@ -13424,6 +13559,8 @@ }, "node_modules/d3-sankey": { "version": "0.12.3", + "resolved": "https://registry.npmjs.org/d3-sankey/-/d3-sankey-0.12.3.tgz", + "integrity": "sha512-nQhsBRmM19Ax5xEIPLMY9ZmJ/cDvd1BG3UVvt5h3WRxKg5zGRbvnteTyWAbzeSvlh3tW7ZEmq4VwR5mB3tutmQ==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -13433,6 +13570,8 @@ }, "node_modules/d3-sankey/node_modules/d3-array": { "version": "2.12.1", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-2.12.1.tgz", + "integrity": "sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -13441,11 +13580,15 @@ }, "node_modules/d3-sankey/node_modules/d3-path": { "version": "1.0.9", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-1.0.9.tgz", + "integrity": "sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg==", "dev": true, "license": "BSD-3-Clause" }, "node_modules/d3-sankey/node_modules/d3-shape": { "version": "1.3.7", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-1.3.7.tgz", + "integrity": "sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -13454,11 +13597,15 @@ }, "node_modules/d3-sankey/node_modules/internmap": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-1.0.1.tgz", + "integrity": "sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw==", "dev": true, "license": "ISC" }, "node_modules/d3-scale": { "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", "dev": true, "license": "ISC", "dependencies": { @@ -13474,6 +13621,8 @@ }, "node_modules/d3-scale-chromatic": { "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", + "integrity": "sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==", "dev": true, "license": "ISC", "dependencies": { @@ -13486,6 +13635,8 @@ }, "node_modules/d3-selection": { "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", + "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", "dev": true, "license": "ISC", "engines": { @@ -13494,6 +13645,8 @@ }, "node_modules/d3-shape": { "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", "dev": true, "license": "ISC", "dependencies": { @@ -13505,6 +13658,8 @@ }, "node_modules/d3-time": { "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", "dev": true, "license": "ISC", "dependencies": { @@ -13516,6 +13671,8 @@ }, "node_modules/d3-time-format": { "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", "dev": true, "license": "ISC", "dependencies": { @@ -13527,6 +13684,8 @@ }, "node_modules/d3-timer": { "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", "dev": true, "license": "ISC", "engines": { @@ -13535,6 +13694,8 @@ }, "node_modules/d3-transition": { "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz", + "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==", "dev": true, "license": "ISC", "dependencies": { @@ -13553,6 +13714,8 @@ }, "node_modules/d3-voronoi-map": { "version": "2.1.1", + "resolved": "https://registry.npmjs.org/d3-voronoi-map/-/d3-voronoi-map-2.1.1.tgz", + "integrity": "sha512-mCXfz/kD9IQxjHaU2IMjkO8fSo4J6oysPR2iL+omDsCy1i1Qn6BQ/e4hEAW8C6ms2kfuHwqtbNom80Hih94YsA==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -13564,21 +13727,29 @@ }, "node_modules/d3-voronoi-map/node_modules/d3-dispatch": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-2.0.0.tgz", + "integrity": "sha512-S/m2VsXI7gAti2pBoLClFFTMOO1HTtT0j99AuXLoGFKO6deHDdnv6ZGTxSTTUTgO1zVcv82fCOtDjYK4EECmWA==", "dev": true, "license": "BSD-3-Clause" }, "node_modules/d3-voronoi-map/node_modules/d3-polygon": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/d3-polygon/-/d3-polygon-2.0.0.tgz", + "integrity": "sha512-MsexrCK38cTGermELs0cO1d79DcTsQRN7IWMJKczD/2kBjzNXxLUWP33qRF6VDpiLV/4EI4r6Gs0DAWQkE8pSQ==", "dev": true, "license": "BSD-3-Clause" }, "node_modules/d3-voronoi-map/node_modules/d3-timer": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-2.0.0.tgz", + "integrity": "sha512-TO4VLh0/420Y/9dO3+f9abDEFYeCUr2WZRlxJvbp4HPTQcSylXNiL6yZa9FIUvV1yRiFufl1bszTCLDqv9PWNA==", "dev": true, "license": "BSD-3-Clause" }, "node_modules/d3-voronoi-treemap": { "version": "1.1.2", + "resolved": "https://registry.npmjs.org/d3-voronoi-treemap/-/d3-voronoi-treemap-1.1.2.tgz", + "integrity": "sha512-7odu9HdG/yLPWwzDteJq4yd9Q/NwgQV7IE/u36VQtcCK7k1sZwDqbkHCeMKNTBsq5mQjDwolTsrXcU0j8ZEMCA==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -13587,6 +13758,8 @@ }, "node_modules/d3-weighted-voronoi": { "version": "1.1.3", + "resolved": "https://registry.npmjs.org/d3-weighted-voronoi/-/d3-weighted-voronoi-1.1.3.tgz", + "integrity": "sha512-C3WdvSKl9aqhAy+f3QT3PPsQG6V+ajDfYO3BSclQDSD+araW2xDBFIH67aKzsSuuuKaX8K2y2dGq1fq/dWTVig==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -13596,6 +13769,8 @@ }, "node_modules/d3-weighted-voronoi/node_modules/d3-array": { "version": "2.12.1", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-2.12.1.tgz", + "integrity": "sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -13604,16 +13779,22 @@ }, "node_modules/d3-weighted-voronoi/node_modules/d3-polygon": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/d3-polygon/-/d3-polygon-2.0.0.tgz", + "integrity": "sha512-MsexrCK38cTGermELs0cO1d79DcTsQRN7IWMJKczD/2kBjzNXxLUWP33qRF6VDpiLV/4EI4r6Gs0DAWQkE8pSQ==", "dev": true, "license": "BSD-3-Clause" }, "node_modules/d3-weighted-voronoi/node_modules/internmap": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-1.0.1.tgz", + "integrity": "sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw==", "dev": true, "license": "ISC" }, "node_modules/d3-zoom": { "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz", + "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==", "dev": true, "license": "ISC", "dependencies": { @@ -13770,6 +13951,8 @@ }, "node_modules/deep-equal": { "version": "1.1.2", + "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.1.2.tgz", + "integrity": "sha512-5tdhKF6DbU7iIzrIOa1AOUt39ZRm13cmL1cGEh//aqR8x9+tNfbywRf0n5FD/18OKMdo7DNEtrX2t22ZAkI+eg==", "dev": true, "license": "MIT", "dependencies": { @@ -13880,6 +14063,8 @@ }, "node_modules/delaunator": { "version": "5.0.1", + "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.0.1.tgz", + "integrity": "sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw==", "dev": true, "license": "ISC", "dependencies": { @@ -13959,6 +14144,8 @@ }, "node_modules/dfa": { "version": "1.2.0", + "resolved": "https://registry.npmjs.org/dfa/-/dfa-1.2.0.tgz", + "integrity": "sha512-ED3jP8saaweFTjeGX8HQPjeC1YYyZs98jGNZx6IiBvxW7JG5v492kamAQB3m2wop07CvU/RQmzcKr6bgcC5D/Q==", "dev": true, "license": "MIT" }, @@ -14423,7 +14610,9 @@ } }, "node_modules/es-toolkit": { - "version": "1.44.0", + "version": "1.45.1", + "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.45.1.tgz", + "integrity": "sha512-/jhoOj/Fx+A+IIyDNOvO3TItGmlMKhtX8ISAHKE90c4b/k1tqaqEZ+uUqfpU8DMnW5cgNJv606zS55jGvza0Xw==", "dev": true, "license": "MIT", "workspaces": [ @@ -15795,6 +15984,8 @@ }, "node_modules/flatpickr": { "version": "4.6.13", + "resolved": "https://registry.npmjs.org/flatpickr/-/flatpickr-4.6.13.tgz", + "integrity": "sha512-97PMG/aywoYpB4IvbvUJi0RQi8vearvU0oov1WW3k0WZPBMrTQVqekSX5CjSG/M4Q3i6A/0FKXC7RyAoAUUSPw==", "dev": true, "license": "MIT" }, @@ -15805,6 +15996,8 @@ }, "node_modules/focus-trap": { "version": "7.8.0", + "resolved": "https://registry.npmjs.org/focus-trap/-/focus-trap-7.8.0.tgz", + "integrity": "sha512-/yNdlIkpWbM0ptxno3ONTuf+2g318kh2ez3KSeZN5dZ8YC6AAmgeWz+GasYYiBJPFaYcSAPeu4GfhUaChzIJXA==", "dev": true, "license": "MIT", "dependencies": { @@ -17191,6 +17384,8 @@ }, "node_modules/interactjs": { "version": "1.10.27", + "resolved": "https://registry.npmjs.org/interactjs/-/interactjs-1.10.27.tgz", + "integrity": "sha512-y/8RcCftGAF24gSp76X2JS3XpHiUvDQyhF8i7ujemBz77hwiHDuJzftHx7thY8cxGogwGiPJ+o97kWB6eAXnsA==", "dev": true, "license": "MIT", "dependencies": { @@ -17212,6 +17407,8 @@ }, "node_modules/internmap": { "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", "dev": true, "license": "ISC", "engines": { @@ -17266,6 +17463,8 @@ }, "node_modules/is-arguments": { "version": "1.2.0", + "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.2.0.tgz", + "integrity": "sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==", "dev": true, "license": "MIT", "dependencies": { @@ -18974,6 +19173,9 @@ }, "node_modules/jpeg-exif": { "version": "1.1.4", + "resolved": "https://registry.npmjs.org/jpeg-exif/-/jpeg-exif-1.1.4.tgz", + "integrity": "sha512-a+bKEcCjtuW5WTdgeXFzswSrdqi0jk4XlEtZlx5A94wCoBpFjfFTbo/Tra5SpNCl/YFZPvcV1dJc+TAYeg6ROQ==", + "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", "dev": true, "license": "MIT" }, @@ -19428,6 +19630,8 @@ }, "node_modules/lit": { "version": "3.3.2", + "resolved": "https://registry.npmjs.org/lit/-/lit-3.3.2.tgz", + "integrity": "sha512-NF9zbsP79l4ao2SNrH3NkfmFgN/hBYSQo90saIVI1o5GpjAdCPVstVzO1MrLOakHoEhYkrtRjPK6Ob521aoYWQ==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -19438,6 +19642,8 @@ }, "node_modules/lit-element": { "version": "4.2.2", + "resolved": "https://registry.npmjs.org/lit-element/-/lit-element-4.2.2.tgz", + "integrity": "sha512-aFKhNToWxoyhkNDmWZwEva2SlQia+jfG0fjIWV//YeTaWrVnOxD89dPKfigCUspXFmjzOEUQpOkejH5Ly6sG0w==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -19448,6 +19654,8 @@ }, "node_modules/lit-html": { "version": "3.3.2", + "resolved": "https://registry.npmjs.org/lit-html/-/lit-html-3.3.2.tgz", + "integrity": "sha512-Qy9hU88zcmaxBXcc10ZpdK7cOLXvXpRoBxERdtqV9QOrfpMZZ6pSYP91LhpPtap3sFMUiL7Tw2RImbe0Al2/kw==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -19626,6 +19834,8 @@ }, "node_modules/luxon": { "version": "3.7.2", + "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.7.2.tgz", + "integrity": "sha512-vtEhXh/gNjI9Yg1u4jX/0YVPMvxzHuGgCm6tC5kZyb08yjGWGnqAjGJvcXbqQR2P3MyMEFnRbpcdFS6PBcLqew==", "dev": true, "license": "MIT", "engines": { @@ -19917,7 +20127,9 @@ } }, "node_modules/marked": { - "version": "16.3.0", + "version": "17.0.4", + "resolved": "https://registry.npmjs.org/marked/-/marked-17.0.4.tgz", + "integrity": "sha512-NOmVMM+KAokHMvjWmC5N/ZOvgmSWuqJB8FoYI019j4ogb/PeRMKoKIjReZ2w3376kkA8dSJIP8uD993Kxc0iRQ==", "dev": true, "license": "MIT", "bin": { @@ -19929,6 +20141,8 @@ }, "node_modules/markerjs2": { "version": "2.32.7", + "resolved": "https://registry.npmjs.org/markerjs2/-/markerjs2-2.32.7.tgz", + "integrity": "sha512-HeFRZjmc43DOG3lSQp92z49cq2oCYpYn2pX++SkJAW1Dij4xJtRquVRf+cXeSZQWDX3ufns1Ry/bGk+zveP7rA==", "dev": true, "license": "SEE LICENSE IN LICENSE" }, @@ -24929,6 +25143,8 @@ }, "node_modules/object-is": { "version": "1.1.6", + "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.6.tgz", + "integrity": "sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q==", "dev": true, "license": "MIT", "dependencies": { @@ -25281,6 +25497,8 @@ }, "node_modules/pako": { "version": "0.2.9", + "resolved": "https://registry.npmjs.org/pako/-/pako-0.2.9.tgz", + "integrity": "sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==", "dev": true, "license": "MIT" }, @@ -25495,6 +25713,8 @@ }, "node_modules/pdfmake": { "version": "0.2.23", + "resolved": "https://registry.npmjs.org/pdfmake/-/pdfmake-0.2.23.tgz", + "integrity": "sha512-A/IksoKb/ikOZH1edSDJ/2zBbqJKDghD4+fXn3rT7quvCJDlsZMs3NmIB3eajLMMFU9Bd3bZPVvlUMXhvFI+bQ==", "dev": true, "license": "MIT", "dependencies": { @@ -25509,6 +25729,8 @@ }, "node_modules/pdfmake/node_modules/iconv-lite": { "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", "dev": true, "license": "MIT", "dependencies": { @@ -25824,6 +26046,8 @@ }, "node_modules/png-js": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/png-js/-/png-js-1.0.0.tgz", + "integrity": "sha512-k+YsbhpA9e+EFfKjTCH3VW6aoKlyNYI6NYdTfDL4CIvFnvsuO84ttonmZE7rc+v23SLTH8XX+5w/Ak9v0xGY4g==", "dev": true }, "node_modules/point-in-polygon-hao": { @@ -25883,6 +26107,8 @@ }, "node_modules/polylabel": { "version": "1.1.0", + "resolved": "https://registry.npmjs.org/polylabel/-/polylabel-1.1.0.tgz", + "integrity": "sha512-bxaGcA40sL3d6M4hH72Z4NdLqxpXRsCFk8AITYg6x1rn1Ei3izf00UMLklerBZTO49aPA3CYrIwVulx2Bce2pA==", "dev": true, "license": "ISC", "dependencies": { @@ -25891,6 +26117,8 @@ }, "node_modules/polylabel/node_modules/tinyqueue": { "version": "2.0.3", + "resolved": "https://registry.npmjs.org/tinyqueue/-/tinyqueue-2.0.3.tgz", + "integrity": "sha512-ppJZNDuKGgxzkHihX8v9v9G5f+18gzaTfrukGrq6ueg0lmH4nqVnA2IPG0AEH3jKEk2GRJCUhDoqpoiw3PHLBA==", "dev": true, "license": "ISC" }, @@ -29689,7 +29917,9 @@ } }, "node_modules/sax": { - "version": "1.4.4", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.5.0.tgz", + "integrity": "sha512-21IYA3Q5cQf089Z6tgaUTr7lDAyzoTPx5HRtbhsME8Udispad8dC/+sziTNugOEx54ilvatQ9YCzl4KQLPcRHA==", "dev": true, "license": "BlueOak-1.0.0", "engines": { @@ -29748,6 +29978,8 @@ }, "node_modules/seedrandom": { "version": "3.0.5", + "resolved": "https://registry.npmjs.org/seedrandom/-/seedrandom-3.0.5.tgz", + "integrity": "sha512-8OwmbklUNzwezjGInmZ+2clQmExQPvomqjL7LFqOYqtmuxRgQYqOD3mHaU+MvZn5FLUeVxVfQjwLZW/n/JFuqg==", "dev": true, "license": "MIT" }, @@ -30212,7 +30444,9 @@ } }, "node_modules/sortablejs": { - "version": "1.15.6", + "version": "1.15.7", + "resolved": "https://registry.npmjs.org/sortablejs/-/sortablejs-1.15.7.tgz", + "integrity": "sha512-Kk8wLQPlS+yi1ZEf48a4+fzHa4yxjC30M/Sr2AnQu+f/MPwvvX9XjZ6OWejiz8crBsLwSq8GHqaxaET7u6ux0A==", "dev": true, "license": "MIT" }, @@ -30667,21 +30901,6 @@ "webpack": "^5.27.0" } }, - "node_modules/style-observer": { - "version": "0.0.8", - "dev": true, - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/LeaVerou" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/leaverou" - } - ], - "license": "MIT" - }, "node_modules/style-to-js": { "version": "1.1.21", "resolved": "https://registry.npmjs.org/style-to-js/-/style-to-js-1.1.21.tgz", @@ -31015,6 +31234,8 @@ }, "node_modules/svg-arc-to-cubic-bezier": { "version": "3.2.0", + "resolved": "https://registry.npmjs.org/svg-arc-to-cubic-bezier/-/svg-arc-to-cubic-bezier-3.2.0.tgz", + "integrity": "sha512-djbJ/vZKZO+gPoSDThGNpKDO+o+bAeA4XQKovvkNCqnIS2t+S4qnLAGQhyyrulhCFRl1WWzAp0wUDV8PpTVU3g==", "dev": true, "license": "ISC" }, @@ -31155,6 +31376,8 @@ }, "node_modules/tabbable": { "version": "6.4.0", + "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.4.0.tgz", + "integrity": "sha512-05PUHKSNE8ou2dwIxTngl4EzcnsCDZGJ/iCLtDflR/SHB/ny14rXc+qU5P4mG9JkusiV7EivzY9Mhm55AzAvCg==", "dev": true, "license": "MIT" }, @@ -31307,6 +31530,8 @@ }, "node_modules/timezone-groups": { "version": "0.10.4", + "resolved": "https://registry.npmjs.org/timezone-groups/-/timezone-groups-0.10.4.tgz", + "integrity": "sha512-AnkJYrbb7uPkDCEqGeVJiawZNiwVlSkkeX4jZg1gTEguClhyX+/Ezn07KB6DT29tG3UN418ldmS/W6KqGOTDjg==", "dev": true, "license": "MIT", "engines": { @@ -31315,6 +31540,8 @@ }, "node_modules/tiny-inflate": { "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tiny-inflate/-/tiny-inflate-1.0.3.tgz", + "integrity": "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==", "dev": true, "license": "MIT" }, @@ -31815,6 +32042,8 @@ }, "node_modules/unicode-properties": { "version": "1.4.1", + "resolved": "https://registry.npmjs.org/unicode-properties/-/unicode-properties-1.4.1.tgz", + "integrity": "sha512-CLjCCLQ6UuMxWnbIylkisbRj31qxHPAurvena/0iwSVbQ2G1VY5/HjV0IRabOEbDHlzZlRdCrD4NhB0JtU40Pg==", "dev": true, "license": "MIT", "dependencies": { @@ -31831,6 +32060,8 @@ }, "node_modules/unicode-trie": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-trie/-/unicode-trie-2.0.0.tgz", + "integrity": "sha512-x7bc76x0bm4prf1VLg79uhAzKw8DVboClSN5VxJuQ+LKDOVEW9CdH+VY7SP+vX7xCYQqzzgQpFqz15zeLvAtZQ==", "dev": true, "license": "MIT", "dependencies": { @@ -33250,6 +33481,8 @@ }, "node_modules/xmldoc": { "version": "2.0.3", + "resolved": "https://registry.npmjs.org/xmldoc/-/xmldoc-2.0.3.tgz", + "integrity": "sha512-6gRk4NY/Jvg67xn7OzJuxLRsGgiXBaPUQplVJ/9l99uIugxh4FTOewYz5ic8WScj7Xx/2WvhENiQKwkK9RpE4w==", "dev": true, "license": "MIT", "dependencies": { @@ -33261,6 +33494,8 @@ }, "node_modules/xss": { "version": "1.0.13", + "resolved": "https://registry.npmjs.org/xss/-/xss-1.0.13.tgz", + "integrity": "sha512-clu7dxTm1e8Mo5fz3n/oW3UCXBfV89xZ72jM8yzo1vR/pIS0w3sgB3XV2H8Vm6zfGnHL0FzvLJPJEBhd86/z4Q==", "dev": true, "license": "MIT", "dependencies": { @@ -33276,6 +33511,8 @@ }, "node_modules/xss/node_modules/commander": { "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", "dev": true, "license": "MIT" }, diff --git a/package.json b/package.json index e99a92d9..409a941b 100755 --- a/package.json +++ b/package.json @@ -110,7 +110,7 @@ ] }, "peerDependencies": { - "@arcgis/core": "^4.34.8" + "@arcgis/core": "^5.0.9" }, "peerDependenciesMeta": { "@arcgis/core": { @@ -118,7 +118,7 @@ } }, "devDependencies": { - "@arcgis/core": "^4.34.8", + "@arcgis/core": "^5.0.9", "@babel/core": "^7.28.0", "@babel/plugin-transform-runtime": "^7.29.0", "@babel/preset-env": "^7.28.0", diff --git a/plugins/interact/src/InteractInit.jsx b/plugins/interact/src/InteractInit.jsx index 66aba4c7..e17988cb 100755 --- a/plugins/interact/src/InteractInit.jsx +++ b/plugins/interact/src/InteractInit.jsx @@ -1,4 +1,4 @@ -import { useEffect } from 'react' +import { useEffect, useRef } from 'react' import { useInteractionHandlers } from './hooks/useInteractionHandlers.js' import { useHighlightSync } from './hooks/useHighlightSync.js' import { attachEvents } from './events.js' @@ -27,6 +27,27 @@ export const InteractInit = ({ mapProvider, }) + // Refs updated synchronously each render — keeps callbacks fresh without re-attaching events + const handleInteractionRef = useRef(handleInteraction) + handleInteractionRef.current = handleInteraction + + const pluginStateRef = useRef(pluginState) + pluginStateRef.current = pluginState + + const appStateRef = useRef(appState) + appStateRef.current = appState + + // Defer click handling by one macrotask so any click that triggered the enable + // (e.g. finishing a draw gesture) fires before this handler is live. + // Managed separately from attachEvents so re-runs of that effect don't reset it — + // only resets when enabled actually changes. + const clickReadyRef = useRef(false) + useEffect(() => { + clickReadyRef.current = false + const timer = setTimeout(() => { clickReadyRef.current = true }, 0) + return () => clearTimeout(timer) + }, [pluginState.enabled]) + // Highlight features and sync state selectedBounds from mapProvider useHighlightSync({ mapProvider, @@ -54,18 +75,19 @@ export const InteractInit = ({ } const cleanupEvents = attachEvents({ - appState, - pluginState, + getAppState: () => appStateRef.current, mapState, + getPluginState: () => pluginStateRef.current, buttonConfig, events, eventBus, - handleInteraction, + handleInteraction: (e) => handleInteractionRef.current(e), + clickReadyRef, closeApp }) - + return cleanupEvents - }, [appState, mapState, pluginState, buttonConfig, eventBus, handleInteraction]) + }, [pluginState.enabled, buttonConfig, events, eventBus, closeApp]) return null diff --git a/plugins/interact/src/InteractInit.test.js b/plugins/interact/src/InteractInit.test.js index aea5d68c..5c57c608 100644 --- a/plugins/interact/src/InteractInit.test.js +++ b/plugins/interact/src/InteractInit.test.js @@ -1,4 +1,4 @@ -import { render } from '@testing-library/react' +import { act, render } from '@testing-library/react' import { InteractInit } from './InteractInit.jsx' import { useInteractionHandlers } from './hooks/useInteractionHandlers.js' import { useHighlightSync } from './hooks/useHighlightSync.js' @@ -70,9 +70,9 @@ describe('InteractInit', () => { it('attaches events and returns cleanup', () => { const { unmount } = render() expect(attachEvents).toHaveBeenCalledWith(expect.objectContaining({ - handleInteraction: handleInteractionMock, - appState: props.appState, - pluginState: props.pluginState, + getAppState: expect.any(Function), + getPluginState: expect.any(Function), + handleInteraction: expect.any(Function), mapState: props.mapState, buttonConfig: props.buttonConfig, events: props.services.events, @@ -80,11 +80,25 @@ describe('InteractInit', () => { closeApp: props.services.closeApp })) - // simulate unmount + const { getAppState, getPluginState, handleInteraction } = attachEvents.mock.calls.at(-1)[0] + expect(getAppState()).toMatchObject(props.appState) + expect(getPluginState()).toMatchObject({ enabled: props.pluginState.enabled }) + + const event = { point: {}, coords: [] } + handleInteraction(event) + expect(handleInteractionMock).toHaveBeenCalledWith(event) + unmount() expect(cleanupMock).toHaveBeenCalled() }) + it('enables click handling after a macrotask', () => { + jest.useFakeTimers() + render() + act(() => jest.runAllTimers()) + jest.useRealTimers() + }) + it('does not attach events if plugin not enabled', () => { const disabledProps = { ...props, diff --git a/plugins/interact/src/events.js b/plugins/interact/src/events.js index 599d18c1..eb14edab 100755 --- a/plugins/interact/src/events.js +++ b/plugins/interact/src/events.js @@ -1,5 +1,6 @@ // Helper for feature toggling logic -const createFeatureHandler = (mapState, pluginState) => (args, addToExisting) => { +const createFeatureHandler = (mapState, getPluginState) => (args, addToExisting) => { + const pluginState = getPluginState() mapState.markers.remove('location') pluginState.dispatch({ type: 'TOGGLE_SELECTED_FEATURES', @@ -12,17 +13,18 @@ const createFeatureHandler = (mapState, pluginState) => (args, addToExisting) => } export function attachEvents ({ - appState, + getAppState, mapState, - pluginState, + getPluginState, buttonConfig, events, eventBus, handleInteraction, + clickReadyRef, closeApp }) { const { selectDone, selectAtTarget, selectCancel } = buttonConfig - const { viewportRef } = appState.layoutRefs + const { viewportRef } = getAppState().layoutRefs // Keyboard Logic let enterOnViewport = false @@ -35,23 +37,20 @@ export function attachEvents ({ } // Interaction Handlers - // Defer click handling by one macrotask so any click that triggered the enable - // (e.g. finishing a draw gesture) fires before this handler is live. - let clickReady = false - const clickReadyTimer = setTimeout(() => { clickReady = true }, 0) const handleMapClick = (e) => { - if (clickReady) { + if (clickReadyRef.current) { handleInteraction(e) } } const handleSelectAtTarget = () => handleInteraction(mapState.crossHair.getDetail()) const handleSelectDone = () => { + const pluginState = getPluginState() const marker = mapState.markers.getMarker('location') const { coords } = marker || {} const { selectionBounds, selectedFeatures } = pluginState - - if (appState.disabledButtons.has('selectDone')) { + + if (getAppState().disabledButtons.has('selectDone')) { return } @@ -68,12 +67,12 @@ export function attachEvents ({ const handleSelectCancel = () => { eventBus.emit('interact:cancel') - if (pluginState.closeOnAction ?? true) { + if (getPluginState().closeOnAction ?? true) { closeApp() } } - const toggleFeature = createFeatureHandler(mapState, pluginState) + const toggleFeature = createFeatureHandler(mapState, getPluginState) const handleSelect = (args) => toggleFeature(args, true) const handleUnselect = (args) => toggleFeature(args, false) @@ -88,7 +87,6 @@ export function attachEvents ({ selectCancel.onClick = handleSelectCancel return () => { - clearTimeout(clickReadyTimer) selectDone.onClick = null selectAtTarget.onClick = null selectCancel.onClick = null @@ -98,4 +96,4 @@ export function attachEvents ({ eventBus.off('interact:selectFeature', handleSelect) eventBus.off('interact:unselectFeature', handleUnselect) } -} \ No newline at end of file +} diff --git a/plugins/interact/src/events.test.js b/plugins/interact/src/events.test.js index 1ebcadf5..9b317478 100644 --- a/plugins/interact/src/events.test.js +++ b/plugins/interact/src/events.test.js @@ -6,19 +6,27 @@ describe('attachEvents', () => { beforeEach(() => { jest.useFakeTimers() // factory function to create fresh params for each test - createParams = () => ({ - appState: { layoutRefs: { viewportRef: { current: document.body } }, disabledButtons: new Set() }, - mapState: { - markers: { remove: jest.fn(), getMarker: jest.fn(() => null) }, - crossHair: { getDetail: jest.fn(() => ({ point: { x: 0, y: 0 }, coords: [0,0] })) } - }, - pluginState: { dispatch: jest.fn(), selectionBounds: null, selectedFeatures: [], closeOnAction: true, multiSelect: false }, - buttonConfig: { selectDone: {}, selectAtTarget: {}, selectCancel: {} }, - events: { MAP_CLICK: 'map:click' }, - eventBus: { on: jest.fn(), off: jest.fn(), emit: jest.fn() }, - handleInteraction: jest.fn(), - closeApp: jest.fn() - }) + createParams = () => { + const appState = { layoutRefs: { viewportRef: { current: document.body } }, disabledButtons: new Set() } + const pluginState = { dispatch: jest.fn(), selectionBounds: null, selectedFeatures: [], closeOnAction: true, multiSelect: false } + const clickReadyRef = { current: false } + return { + appState, + pluginState, + clickReadyRef, + getAppState: () => appState, + getPluginState: () => pluginState, + mapState: { + markers: { remove: jest.fn(), getMarker: jest.fn(() => null) }, + crossHair: { getDetail: jest.fn(() => ({ point: { x: 0, y: 0 }, coords: [0,0] })) } + }, + buttonConfig: { selectDone: {}, selectAtTarget: {}, selectCancel: {} }, + events: { MAP_CLICK: 'map:click' }, + eventBus: { on: jest.fn(), off: jest.fn(), emit: jest.fn() }, + handleInteraction: jest.fn(), + closeApp: jest.fn() + } + } }) afterEach(() => { @@ -65,10 +73,10 @@ describe('attachEvents', () => { expect(params.handleInteraction).not.toHaveBeenCalled() }) - it('map click triggers interaction after timer fires', () => { + it('map click triggers interaction when clickReadyRef is true', () => { const params = createParams() + params.clickReadyRef.current = true cleanup = attachEvents(params) - jest.runAllTimers() const handler = params.eventBus.on.mock.calls.find(c => c[0]==='map:click')[1] handler({ point:{x:1,y:2}, coords:[3,4] }) @@ -76,8 +84,9 @@ describe('attachEvents', () => { expect(params.handleInteraction).toHaveBeenCalledWith({ point:{x:1,y:2}, coords:[3,4] }) }) - it('map click is suppressed immediately after enable before timer fires', () => { + it('map click is suppressed when clickReadyRef is false', () => { const params = createParams() + params.clickReadyRef.current = false cleanup = attachEvents(params) const handler = params.eventBus.on.mock.calls.find(c => c[0]==='map:click')[1] diff --git a/providers/beta/esri/src/esriProvider.js b/providers/beta/esri/src/esriProvider.js index 2e6a8803..09712b53 100644 --- a/providers/beta/esri/src/esriProvider.js +++ b/providers/beta/esri/src/esriProvider.js @@ -1,6 +1,7 @@ // src/plugins/mapStyles/EsriProvider.jsx import './esriProvider.scss' import esriConfig from '@arcgis/core/config.js' +import TileInfo from '@arcgis/core/layers/support/TileInfo.js' import EsriMap from '@arcgis/core/Map.js' import MapView from '@arcgis/core/views/MapView.js' import VectorTileLayer from '@arcgis/core/layers/VectorTileLayer.js' @@ -50,6 +51,7 @@ export default class EsriProvider { center: getPointFromFlatCoords(config.center), maxExtent: maxExtent, constraints: { + lods: TileInfo.create({ spatialReference: { wkid: 27700 } }).lods, snapToZoom: false, minZoom: config.minZoom, maxZoom: config.maxZoom, diff --git a/providers/beta/esri/src/mapEvents.js b/providers/beta/esri/src/mapEvents.js index 6bfe8399..a790113d 100644 --- a/providers/beta/esri/src/mapEvents.js +++ b/providers/beta/esri/src/mapEvents.js @@ -105,10 +105,41 @@ export function attachMapEvents ({ updating => !updating && emitDataChange() )) - // click - handlers.push(view.on('click', e => { - const mapPoint = e.mapPoint + // Click + // handlers.push(view.on('click', e => { + // const mapPoint = e.mapPoint + // const screenPoint = { x: e.x, y: e.y } + // eventBus.emit(events.MAP_CLICK, { point: screenPoint, coords: [mapPoint.x, mapPoint.y] }) + // })) + + // Using pointer-up instead of click/immediate-click — significantly more responsive as it + // bypasses ArcGIS's internal hit-testing pipeline. Track pointer-down position to suppress + // pointer-up after a drag/pan. Also suppress when SketchViewModel is active so draw/edit + // vertex clicks don't leak through to the map click handler. + const DRAG_TOLERANCE = 6 + let pointerDownX = null + let pointerDownY = null + + handlers.push(view.on('pointer-down', e => { + pointerDownX = e.x + pointerDownY = e.y + })) + + handlers.push(view.on('pointer-up', e => { + if (e.button !== 0) { + return + } + if (Math.hypot(e.x - pointerDownX, e.y - pointerDownY) > DRAG_TOLERANCE) { + return + } + if (mapProvider.sketchViewModel?.state === 'active') { + return + } const screenPoint = { x: e.x, y: e.y } + const mapPoint = view.toMap(screenPoint) + if (!mapPoint) { + return + } eventBus.emit(events.MAP_CLICK, { point: screenPoint, coords: [mapPoint.x, mapPoint.y] }) })) diff --git a/src/App/hooks/useMarkersAPI.js b/src/App/hooks/useMarkersAPI.js index 71dff898..4fb5e705 100755 --- a/src/App/hooks/useMarkersAPI.js +++ b/src/App/hooks/useMarkersAPI.js @@ -1,4 +1,4 @@ -import { useCallback, useEffect, useRef } from 'react' +import { useCallback, useEffect, useLayoutEffect, useRef } from 'react' import { useConfig } from '../store/configContext.js' import { useMap } from '../store/mapContext.js' import { useService } from '../store/serviceContext.js' @@ -99,8 +99,10 @@ export const useMarkers = () => { const { markers, dispatch, mapSize, isMapReady } = useMap() const markerRefs = useRef(new Map()) - // Attach add, remove, and getMarker methods to the markers store object - useEffect(() => { + // Attach add, remove, and getMarker methods to the markers store object. + // useLayoutEffect ensures these are assigned before paint so rapid clicks can't + // arrive between a render (new markers object) and the async useEffect assignment. + useLayoutEffect(() => { if (!mapProvider) { return } diff --git a/src/App/layout/Layout.jsx b/src/App/layout/Layout.jsx index eb4e39c2..85919ef3 100755 --- a/src/App/layout/Layout.jsx +++ b/src/App/layout/Layout.jsx @@ -70,7 +70,7 @@ export const Layout = () => {
-
+
diff --git a/src/App/renderer/HtmlElementHost.jsx b/src/App/renderer/HtmlElementHost.jsx index 9b92b52b..52820756 100644 --- a/src/App/renderer/HtmlElementHost.jsx +++ b/src/App/renderer/HtmlElementHost.jsx @@ -18,6 +18,7 @@ export const getSlotRef = (slot, layoutRefs) => { inset: layoutRefs.insetRef, middle: layoutRefs.middleRef, bottom: layoutRefs.bottomRef, + 'right-bottom': layoutRefs.rightBottomRef, actions: layoutRefs.actionsRef, modal: layoutRefs.modalRef } diff --git a/src/App/renderer/pluginWrapper.js b/src/App/renderer/pluginWrapper.js index 47cccc3e..4a290fd7 100755 --- a/src/App/renderer/pluginWrapper.js +++ b/src/App/renderer/pluginWrapper.js @@ -1,4 +1,5 @@ // src/core/renderers/pluginWrapper.js +import { useMemo } from 'react' import { useConfig } from '../store/configContext.js' import { useApp } from '../store/appContext.js' import { useMap } from '../store/mapContext.js' @@ -59,11 +60,11 @@ export function withPluginContexts (Component, { pluginId, pluginConfig }) { services={services} mapProvider={appConfig.mapProvider} iconRegistry={getIconRegistry()} - buttonConfig={Object.fromEntries( + buttonConfig={useMemo(() => Object.fromEntries( Object.entries(appState.buttonConfig).filter( ([_, btn]) => btn.pluginId === pluginId ) - )} + ), [appState.buttonConfig])} /> ) }) diff --git a/src/App/renderer/slots.js b/src/App/renderer/slots.js index 0df0a29d..8d302ea5 100755 --- a/src/App/renderer/slots.js +++ b/src/App/renderer/slots.js @@ -30,10 +30,10 @@ export const allowedSlots = Object.freeze({ layoutSlots.SIDE, layoutSlots.BANNER, layoutSlots.INSET, + layoutSlots.RIGHT_BOTTOM, layoutSlots.MIDDLE, layoutSlots.BOTTOM, layoutSlots.ACTIONS, - layoutSlots.DRAWER, layoutSlots.MODAL ], button: [ diff --git a/src/App/store/AppProvider.jsx b/src/App/store/AppProvider.jsx index 390108ca..10345ab8 100755 --- a/src/App/store/AppProvider.jsx +++ b/src/App/store/AppProvider.jsx @@ -21,6 +21,7 @@ export const AppProvider = ({ options, children }) => { topRightColRef: useRef(null), insetRef: useRef(null), rightRef: useRef(null), + rightBottomRef: useRef(null), middleRef: useRef(null), bottomRef: useRef(null), footerRef: useRef(null), From 1bb92ffc15fa0089fe30c7b574c9a62f8d8f40d7 Mon Sep 17 00:00:00 2001 From: Dan Leech Date: Thu, 5 Mar 2026 09:45:54 +0000 Subject: [PATCH 2/8] draw.created firing twice fix --- plugins/beta/draw-ml/src/events.js | 18 ++++-------------- .../beta/draw-ml/src/modes/createDrawMode.js | 4 +--- 2 files changed, 5 insertions(+), 17 deletions(-) diff --git a/plugins/beta/draw-ml/src/events.js b/plugins/beta/draw-ml/src/events.js index 78027f8e..ecd59247 100755 --- a/plugins/beta/draw-ml/src/events.js +++ b/plugins/beta/draw-ml/src/events.js @@ -33,28 +33,18 @@ export function attachEvents({ pluginState, mapProvider, buttonConfig, eventBus disableSnap() mapProvider.undoStack?.clear() - const features = draw.getAll().features - if (mode === 'edit_vertex') { map.fire('draw.editfinish', { features: [draw.get(tempFeature.id)] }) return } - if (!['draw_polygon', 'draw_line'].includes(mode) || features.length === 0) { + if (!['draw_polygon', 'draw_line'].includes(mode)) { return } - const feature = features?.[0] - const geom = feature.geometry - - if (geom.type === 'Polygon') { - const ring = geom.coordinates[0] - geom.coordinates[0] = [...ring.slice(0, -2), ring[0]] - } else { - geom.coordinates = geom.coordinates.slice(0, -1) - } - - map.fire('draw.create', { features: [feature] }) + // Trigger onStop → ParentMode.onStop, which strips the rubber-band vertex + // and fires draw.create once with the correct geometry. + draw.changeMode('disabled') } const handleCancel = () => { diff --git a/plugins/beta/draw-ml/src/modes/createDrawMode.js b/plugins/beta/draw-ml/src/modes/createDrawMode.js index 44ebfb62..52372359 100644 --- a/plugins/beta/draw-ml/src/modes/createDrawMode.js +++ b/plugins/beta/draw-ml/src/modes/createDrawMode.js @@ -137,9 +137,7 @@ export const createDrawMode = (ParentMode, config) => { // For lines: clicking same spot (like double-click) should finish the line if (finishOnInvalidClick && coords.length > 1) { coords.pop() - this.map.fire('draw.create', { - features: [feature.toGeoJSON()] - }) + this.map.fire('draw.create', { features: [feature.toGeoJSON()] }) this.changeMode('simple_select', { featureIds: [feature.id] }) } return From 0dc5adfaa15765d63c3802c7a5e5ebcb5db7ee88 Mon Sep 17 00:00:00 2001 From: Dan Leech Date: Thu, 5 Mar 2026 11:25:17 +0000 Subject: [PATCH 3/8] Button slot fix --- demo/js/planning.js | 6 +- docs/plugins/plugin-descriptor.md | 37 +++++ src/App/hooks/useModalPanelBehaviour.js | 21 ++- src/App/hooks/useModalPanelBehaviour.test.js | 146 +++++++++++-------- src/App/layout/layout.module.scss | 5 +- 5 files changed, 147 insertions(+), 68 deletions(-) diff --git a/demo/js/planning.js b/demo/js/planning.js index b58c3373..bd57e4e8 100755 --- a/demo/js/planning.js +++ b/demo/js/planning.js @@ -69,7 +69,11 @@ const interactiveMap = new InteractiveMap('map', { hasExitButton: true, plugins: [ mapStylesPlugin({ - mapStyles: vtsMapStyles27700 + mapStyles: vtsMapStyles27700, + manifest: { + buttons: [{ id: 'mapStyles', desktop: { slot: 'right-top', showLabel: false }}], + panels: [{ id: 'mapStyles', desktop: { slot: 'map-styles-button', width: '400px', modal: true }}] + } }), scaleBarPlugin({ units: 'metric' diff --git a/docs/plugins/plugin-descriptor.md b/docs/plugins/plugin-descriptor.md index 03fb7495..91ff2e0a 100644 --- a/docs/plugins/plugin-descriptor.md +++ b/docs/plugins/plugin-descriptor.md @@ -69,3 +69,40 @@ Async function that loads and returns a [PluginManifest](./plugin-manifest.md). **Type:** `Partial` Optional manifest overrides. Allows overriding properties of the loaded [PluginManifest](./plugin-manifest.md). + +Use when you need to customise the UI of a plugin at registration time — particularly for predefined or third-party plugins where you cannot modify the original manifest directly. + +Only the properties you provide are merged into the loaded manifest. Overrides are matched by `id`. + +#### Example + +This example: + +- Moves the `mapStyles` button to the `right-top` slot on desktop. +- Hides the button label. +- Positions the related panel so it opens from the `map-styles-button` slot. +- Sets the panel width to `400px`. +- Makes the panel modal. + +```js +manifest: { + buttons: [ + { + id: 'mapStyles', + desktop: { + slot: 'right-top', + showLabel: false + } + } + ], + panels: [ + { + id: 'mapStyles', + desktop: { + slot: 'map-styles-button', + width: '400px', + modal: true + } + } + ] +} diff --git a/src/App/hooks/useModalPanelBehaviour.js b/src/App/hooks/useModalPanelBehaviour.js index 49d217af..4b7408a5 100755 --- a/src/App/hooks/useModalPanelBehaviour.js +++ b/src/App/hooks/useModalPanelBehaviour.js @@ -75,11 +75,28 @@ export function useModalPanelBehaviour ({ const dividerGap = Number.parseInt(getComputedStyle(root).getPropertyValue('--divider-gap'), 10) useResizeObserver([mainRef], () => { - if (!isModal || !buttonContainerEl || !mainRef.current) { + if (!isModal || !mainRef.current) { return } + + // buttonContainerEl is only defined for button-slot panels (bpConfig.slot ends with '-button'). + // Skip positioning for all other modal types. + if (buttonContainerEl === undefined) { + return + } + + // Dynamically query the current controlling button via aria-controls to handle the case + // where the button has remounted after a breakpoint change (stale triggeringElement). + const panelElId = panelRef.current?.id + const currentButtonEl = panelElId ? document.querySelector(`[aria-controls="${panelElId}"]`) : null + const effectiveContainer = currentButtonEl?.parentElement ?? (buttonContainerEl?.isConnected ? buttonContainerEl : null) + + if (!effectiveContainer) { + return + } + const mainRect = mainRef.current.getBoundingClientRect() - const buttonRect = buttonContainerEl.getBoundingClientRect() + const buttonRect = effectiveContainer.getBoundingClientRect() const offsetTop = buttonRect.top - mainRect.top const offsetRight = Math.round(mainRect.right - buttonRect.right + buttonRect.width + dividerGap) root.style.setProperty('--modal-inset', `${offsetTop}px ${offsetRight}px auto auto`) diff --git a/src/App/hooks/useModalPanelBehaviour.test.js b/src/App/hooks/useModalPanelBehaviour.test.js index 8117382d..3c5b7de1 100644 --- a/src/App/hooks/useModalPanelBehaviour.test.js +++ b/src/App/hooks/useModalPanelBehaviour.test.js @@ -17,12 +17,17 @@ describe('useModalPanelBehaviour', () => { main: { current: document.createElement('div') }, panel: { current: document.createElement('div') } } + // Give panel an ID for aria-controls tests + refs.panel.current.id = 'modal-panel-id' + elements = { buttonContainer: document.createElement('div'), root: document.createElement('div') } + elements.root.appendChild(refs.panel.current) document.body.appendChild(elements.root) + handleClose = jest.fn() jest.clearAllMocks() document.documentElement.style.setProperty('--modal-inset', '') @@ -32,13 +37,17 @@ describe('useModalPanelBehaviour', () => { document.body.innerHTML = '' }) - const TestComponent = ({ isModal = true }) => { + const TestComponent = ({ + isModal = true, + buttonContainerEl, + rootEl = elements.root + }) => { useModalPanelBehaviour({ mainRef: refs.main, panelRef: refs.panel, isModal, - rootEl: elements.root, - buttonContainerEl: elements.buttonContainer, + rootEl, + buttonContainerEl, handleClose }) return null @@ -63,66 +72,51 @@ describe('useModalPanelBehaviour', () => { ) }) - it('updates --modal-inset on resize', () => { - useResizeObserverModule.useResizeObserver.mockImplementation((_, cb) => cb()) - Object.defineProperty(refs.main.current, 'getBoundingClientRect', { - value: () => ({ top: 0, right: 100, bottom: 50, left: 0, width: 100, height: 50 }) + describe('positioning (--modal-inset)', () => { + beforeEach(() => { + // Force ResizeObserver to run the callback immediately + useResizeObserverModule.useResizeObserver.mockImplementation((_, cb) => cb()) + + Object.defineProperty(refs.main.current, 'getBoundingClientRect', { + value: () => ({ top: 0, right: 100, bottom: 50, left: 0, width: 100, height: 50 }), + configurable: true + }) + Object.defineProperty(elements.buttonContainer, 'getBoundingClientRect', { + value: () => ({ top: 10, right: 80, bottom: 40, left: 20, width: 60, height: 30 }), + configurable: true + }) }) - Object.defineProperty(elements.buttonContainer, 'getBoundingClientRect', { - value: () => ({ top: 10, right: 80, bottom: 40, left: 20, width: 60, height: 30 }) - }) - - render() - - const inset = getComputedStyle(document.documentElement).getPropertyValue('--modal-inset') - expect(inset).toContain('10px') - }) - describe('backdrop clicks', () => { - const createBackdrop = (appendTo) => { - const backdrop = document.createElement('div') - backdrop.className = 'im-o-app__modal-backdrop' - appendTo.appendChild(backdrop) - return backdrop - } - - it('calls handleClose when backdrop inside rootEl is clicked', () => { - const backdrop = createBackdrop(elements.root) + it('hits the buttonContainerEl === undefined branch', () => { + refs.main.current = document.createElement('div') // mainRef must exist render() - fireEvent.click(backdrop) - expect(handleClose).toHaveBeenCalled() - }) - it('does not call handleClose when backdrop outside rootEl is clicked', () => { - const backdrop = createBackdrop(document.body) - render() - fireEvent.click(backdrop) - expect(handleClose).not.toHaveBeenCalled() - }) + // Manually trigger ResizeObserver callback (if mocked) + const callback = useResizeObserverModule.useResizeObserver.mock.calls[0][1] + callback() - it('does not call handleClose when non-backdrop element is clicked', () => { - elements.root.appendChild(document.createElement('div')) - render() - fireEvent.click(elements.root.firstChild) - expect(handleClose).not.toHaveBeenCalled() + // Expect CSS variable not set, just to assert callback ran + const inset = document.documentElement.style.getPropertyValue('--modal-inset') + expect(inset).toBe('') }) - }) - it('toggles inert elements on mount and cleanup', () => { - const { unmount } = render() + it('updates --modal-inset via aria-controls when buttonContainerEl is stale', () => { + const button = document.createElement('button') + button.setAttribute('aria-controls', 'modal-panel-id') + elements.buttonContainer.appendChild(button) + document.body.appendChild(elements.buttonContainer) - expect(toggleInertModule.toggleInertElements).toHaveBeenCalledWith({ - containerEl: refs.panel.current, - isFullscreen: true, - boundaryEl: elements.root - }) + const staleEl = document.createElement('div') // detached + render() - unmount() + const inset = document.documentElement.style.getPropertyValue('--modal-inset') + expect(inset).toContain('10px') + }) - expect(toggleInertModule.toggleInertElements).toHaveBeenCalledWith({ - containerEl: refs.panel.current, - isFullscreen: false, - boundaryEl: elements.root + it('skips update when effectiveContainer cannot be resolved', () => { + render() + const inset = document.documentElement.style.getPropertyValue('--modal-inset') + expect(inset).toBe('') }) }) @@ -138,6 +132,20 @@ describe('useModalPanelBehaviour', () => { expect(refs.panel.current.focus).toHaveBeenCalled() }) + // COVERS LINE 44 (The early return branch) + it('does not redirect focus when focus moves completely outside the app root', () => { + refs.panel.current.focus = jest.fn() + render() + + const externalEl = document.createElement('button') + document.body.appendChild(externalEl) // Outside elements.root + + dispatchFocusIn(externalEl) + + // Since isInsideApp is false, it should hit the "return" and not call focus() + expect(refs.panel.current.focus).not.toHaveBeenCalled() + }) + it('does not redirect focus when focus is already inside panel', () => { refs.panel.current.focus = jest.fn() render() @@ -149,23 +157,39 @@ describe('useModalPanelBehaviour', () => { expect(refs.panel.current.focus).not.toHaveBeenCalled() }) - it('handles edge cases gracefully', () => { + it('handles null focus targets gracefully', () => { render() - dispatchFocusIn(null) + expect(true).toBe(true) + }) + }) - refs.panel.current = null - dispatchFocusIn(document.body) + describe('backdrop and inert', () => { + it('calls handleClose when backdrop inside rootEl is clicked', () => { + const backdrop = document.createElement('div') + backdrop.className = 'im-o-app__modal-backdrop' + elements.root.appendChild(backdrop) + + render() + fireEvent.click(backdrop) + expect(handleClose).toHaveBeenCalled() + }) - expect(true).toBe(true) // No errors thrown + it('toggles inert elements on mount and cleanup', () => { + const { unmount } = render() + expect(toggleInertModule.toggleInertElements).toHaveBeenCalledWith( + expect.objectContaining({ isFullscreen: true }) + ) + unmount() + expect(toggleInertModule.toggleInertElements).toHaveBeenCalledWith( + expect.objectContaining({ isFullscreen: false }) + ) }) }) it('does nothing when isModal is false', () => { render() - fireEvent.keyDown(refs.panel.current, { key: 'Escape' }) expect(handleClose).not.toHaveBeenCalled() - expect(toggleInertModule.toggleInertElements).not.toHaveBeenCalled() }) -}) +}) \ No newline at end of file diff --git a/src/App/layout/layout.module.scss b/src/App/layout/layout.module.scss index deddb1b5..70c35ae8 100755 --- a/src/App/layout/layout.module.scss +++ b/src/App/layout/layout.module.scss @@ -322,10 +322,7 @@ } [class*="im-c-panel--"][class*="-button"] { // Adjacent to button - top: var(--modal-inset); - right: var(--modal-inset); - bottom: var(--modal-inset); - left: var(--modal-inset); + inset: var(--modal-inset); } } From 6ee8bf1eb8fbd73346c1683e0c7d4e6ac37fb901 Mon Sep 17 00:00:00 2001 From: Dan Leech Date: Thu, 5 Mar 2026 12:40:29 +0000 Subject: [PATCH 4/8] Modal unit test fixes --- demo/js/planning.js | 14 ++++++++++--- src/App/hooks/useModalPanelBehaviour.test.js | 22 ++++++++++---------- 2 files changed, 22 insertions(+), 14 deletions(-) diff --git a/demo/js/planning.js b/demo/js/planning.js index bd57e4e8..6dec37a9 100755 --- a/demo/js/planning.js +++ b/demo/js/planning.js @@ -57,8 +57,8 @@ const interactiveMap = new InteractiveMap('map', { // minDesktopWidth: 960, mapLabel: 'Ambleside', // zoom: 14, - minZoom: 2, - maxZoom: 15, + minZoom: 6, + maxZoom: 20, autoColorScheme: true, // center: [337672, 504580], extent: [337047, 503795, 338120, 505281], @@ -72,7 +72,7 @@ const interactiveMap = new InteractiveMap('map', { mapStyles: vtsMapStyles27700, manifest: { buttons: [{ id: 'mapStyles', desktop: { slot: 'right-top', showLabel: false }}], - panels: [{ id: 'mapStyles', desktop: { slot: 'map-styles-button', width: '400px', modal: true }}] + panels: [{ id: 'mapStyles', desktop: { slot: 'map-styles-button', width: '400px', modal: false }}] } }), scaleBarPlugin({ @@ -96,6 +96,14 @@ const interactiveMap = new InteractiveMap('map', { }) interactiveMap.on('app:ready', function (e) { + interactiveMap.addButton('help', { + label: 'Help', + href: 'https://google.co.uk', + iconSvgContent: '', + mobile: { slot: 'right-top', showLabel: false }, + tablet: { slot: 'right-top', showLabel: false, order: 1 }, + desktop: { slot: 'right-top', showLabel: false, order: 1 } + }) interactiveMap.addButton('menu', { label: 'Menu', panelId: 'menu', diff --git a/src/App/hooks/useModalPanelBehaviour.test.js b/src/App/hooks/useModalPanelBehaviour.test.js index 3c5b7de1..31dc643b 100644 --- a/src/App/hooks/useModalPanelBehaviour.test.js +++ b/src/App/hooks/useModalPanelBehaviour.test.js @@ -19,15 +19,15 @@ describe('useModalPanelBehaviour', () => { } // Give panel an ID for aria-controls tests refs.panel.current.id = 'modal-panel-id' - + elements = { buttonContainer: document.createElement('div'), root: document.createElement('div') } - + elements.root.appendChild(refs.panel.current) document.body.appendChild(elements.root) - + handleClose = jest.fn() jest.clearAllMocks() document.documentElement.style.setProperty('--modal-inset', '') @@ -37,10 +37,10 @@ describe('useModalPanelBehaviour', () => { document.body.innerHTML = '' }) - const TestComponent = ({ - isModal = true, + const TestComponent = ({ + isModal = true, buttonContainerEl, - rootEl = elements.root + rootEl = elements.root }) => { useModalPanelBehaviour({ mainRef: refs.main, @@ -76,7 +76,7 @@ describe('useModalPanelBehaviour', () => { beforeEach(() => { // Force ResizeObserver to run the callback immediately useResizeObserverModule.useResizeObserver.mockImplementation((_, cb) => cb()) - + Object.defineProperty(refs.main.current, 'getBoundingClientRect', { value: () => ({ top: 0, right: 100, bottom: 50, left: 0, width: 100, height: 50 }), configurable: true @@ -139,7 +139,7 @@ describe('useModalPanelBehaviour', () => { const externalEl = document.createElement('button') document.body.appendChild(externalEl) // Outside elements.root - + dispatchFocusIn(externalEl) // Since isInsideApp is false, it should hit the "return" and not call focus() @@ -160,7 +160,7 @@ describe('useModalPanelBehaviour', () => { it('handles null focus targets gracefully', () => { render() dispatchFocusIn(null) - expect(true).toBe(true) + expect(true).toBe(true) }) }) @@ -169,7 +169,7 @@ describe('useModalPanelBehaviour', () => { const backdrop = document.createElement('div') backdrop.className = 'im-o-app__modal-backdrop' elements.root.appendChild(backdrop) - + render() fireEvent.click(backdrop) expect(handleClose).toHaveBeenCalled() @@ -192,4 +192,4 @@ describe('useModalPanelBehaviour', () => { fireEvent.keyDown(refs.panel.current, { key: 'Escape' }) expect(handleClose).not.toHaveBeenCalled() }) -}) \ No newline at end of file +}) From 2a05d286e8a8dd12dd0d9f51e9dde2d693fdf345 Mon Sep 17 00:00:00 2001 From: Dan Leech Date: Thu, 5 Mar 2026 16:11:52 +0000 Subject: [PATCH 5/8] Slot additions --- demo/js/planning.js | 7 + providers/beta/esri/src/esriProvider.scss | 5 + src/App/hooks/useLayoutMeasurements.js | 26 ++-- src/App/layout/Layout.jsx | 16 ++- src/App/layout/layout.module.scss | 27 +++- src/App/renderer/HtmlElementHost.jsx | 5 +- src/App/renderer/slots.js | 11 +- src/App/store/AppProvider.jsx | 6 +- src/utils/getSafeZoneInset.js | 21 +-- src/utils/getSafeZoneInset.test.js | 162 ++++++++++++++-------- 10 files changed, 199 insertions(+), 87 deletions(-) diff --git a/demo/js/planning.js b/demo/js/planning.js index 6dec37a9..8b8e1805 100755 --- a/demo/js/planning.js +++ b/demo/js/planning.js @@ -134,6 +134,13 @@ interactiveMap.on('app:ready', function (e) { tablet: { slot: 'inset', width: '260px', open: false, exclusive: true }, desktop: { slot: 'inset', width: '280px', open: false, exclusive: true } }) + interactiveMap.addPanel('test', { + label: 'test', + html: '

Test

', + mobile: { slot: 'left-top', open: true }, + tablet: { slot: 'inset', open: true }, + desktop: { slot: 'left-top', open: true } + }) }) interactiveMap.on('map:ready', function (e) { diff --git a/providers/beta/esri/src/esriProvider.scss b/providers/beta/esri/src/esriProvider.scss index ec8ead3a..a36189ee 100644 --- a/providers/beta/esri/src/esriProvider.scss +++ b/providers/beta/esri/src/esriProvider.scss @@ -42,4 +42,9 @@ body, html { letter-spacing: inherit !important; line-height: inherit !important; } +} + +// Hide the ESRI Attribution, manage this elsewhere +.esri-view-attribution { + display: none; } \ No newline at end of file diff --git a/src/App/hooks/useLayoutMeasurements.js b/src/App/hooks/useLayoutMeasurements.js index 707247ca..2b938adf 100755 --- a/src/App/hooks/useLayoutMeasurements.js +++ b/src/App/hooks/useLayoutMeasurements.js @@ -40,17 +40,23 @@ export function useLayoutMeasurements () { const root = document.documentElement const dividerGap = Number.parseInt(getComputedStyle(root).getPropertyValue('--divider-gap'), 10) + // === Top column width === + const leftWidth = topLeftCol.offsetWidth || 0 + const rightWidth = topRightCol.offsetWidth || 0 + const finalWidth = leftWidth || rightWidth ? Math.max(leftWidth, rightWidth) : 0 + appContainer.style.setProperty('--top-col-width', `${finalWidth}px`) + // === Inset offsets === const insetOffsetTop = topLeftCol.offsetHeight + top.offsetTop const insetMaxHeight = main.offsetHeight - insetOffsetTop - top.offsetTop appContainer.style.setProperty('--inset-offset-top', `${insetOffsetTop}px`) appContainer.style.setProperty('--inset-max-height', `${insetMaxHeight}px`) - // === Bottom left offset === - const insetBottom = inset.offsetHeight + insetOffsetTop - const bottomOffsetTop = Math.min(bottom.offsetTop, actions.offsetTop) - const bottomOffsetLeft = bottomOffsetTop - dividerGap > insetBottom ? 0 : inset.offsetLeft + inset.offsetWidth - appContainer.style.setProperty('--offset-left', `${bottomOffsetLeft}px`) + // === Left container offsets === + const leftOffsetTop = topLeftCol.offsetHeight + top.offsetTop + const leftOffsetBottom = main.offsetHeight - bottom.offsetTop + dividerGap + appContainer.style.setProperty('--left-offset-top', `${leftOffsetTop}px`) + appContainer.style.setProperty('--left-offset-bottom', `${leftOffsetBottom}px`) // === Right container offsets === const rightOffsetTop = topRightCol.offsetHeight + top.offsetTop @@ -58,11 +64,11 @@ export function useLayoutMeasurements () { appContainer.style.setProperty('--right-offset-top', `${rightOffsetTop}px`) appContainer.style.setProperty('--right-offset-bottom', `${rightOffsetBottom}px`) - // === Top column width === - const leftWidth = topLeftCol.offsetWidth || 0 - const rightWidth = topRightCol.offsetWidth || 0 - const finalWidth = leftWidth || rightWidth ? Math.max(leftWidth, rightWidth) : 0 - appContainer.style.setProperty('--top-col-width', `${finalWidth}px`) + // === Bottom left offset === + const insetBottom = inset.offsetHeight + insetOffsetTop + const bottomOffsetTop = Math.min(bottom.offsetTop, actions.offsetTop) + const bottomOffsetLeft = bottomOffsetTop - dividerGap > insetBottom ? 0 : inset.offsetLeft + inset.offsetWidth + appContainer.style.setProperty('--offset-left', `${bottomOffsetLeft}px`) } // -------------------------------- diff --git a/src/App/layout/Layout.jsx b/src/App/layout/Layout.jsx index 85919ef3..a701fbe2 100755 --- a/src/App/layout/Layout.jsx +++ b/src/App/layout/Layout.jsx @@ -66,17 +66,25 @@ export const Layout = () => {
+
+
+ +
+
+ +
+
+
+ +
-
+
-
- -
diff --git a/src/App/layout/layout.module.scss b/src/App/layout/layout.module.scss index 70c35ae8..ee4692e7 100755 --- a/src/App/layout/layout.module.scss +++ b/src/App/layout/layout.module.scss @@ -147,6 +147,31 @@ max-height: var(--inset-max-height); } +// --------------------------------------------------- +// Right: Buttons and panels +// --------------------------------------------------- + +.im-o-app__left { + position: absolute; + display: flex; + flex-direction: column; + left: var(--primary-gap); + top: var(--left-offset-top); + bottom: var(--left-offset-bottom); +} + +.im-o-app__left-top { + display: flex; + flex-direction: column; + flex-shrink: 0; + align-items: flex-end; + gap: var(--divider-gap); +} + +.im-o-app__left-bottom { + margin-top: auto; +} + // --------------------------------------------------- // Middle: // --------------------------------------------------- @@ -164,7 +189,7 @@ } // --------------------------------------------------- -// Right: Buttons and inset panel +// Right: Buttons and panels // --------------------------------------------------- .im-o-app__right { diff --git a/src/App/renderer/HtmlElementHost.jsx b/src/App/renderer/HtmlElementHost.jsx index 52820756..7e120b52 100644 --- a/src/App/renderer/HtmlElementHost.jsx +++ b/src/App/renderer/HtmlElementHost.jsx @@ -16,9 +16,12 @@ export const getSlotRef = (slot, layoutRefs) => { 'top-left': layoutRefs.topLeftColRef, 'top-right': layoutRefs.topRightColRef, inset: layoutRefs.insetRef, + 'left-top': layoutRefs.leftTopRef, + 'left-bottom': layoutRefs.leftBottomRef, middle: layoutRefs.middleRef, - bottom: layoutRefs.bottomRef, + 'right-top': layoutRefs.rightTopRef, 'right-bottom': layoutRefs.rightBottomRef, + bottom: layoutRefs.bottomRef, actions: layoutRefs.actionsRef, modal: layoutRefs.modalRef } diff --git a/src/App/renderer/slots.js b/src/App/renderer/slots.js index 8d302ea5..dd94d840 100755 --- a/src/App/renderer/slots.js +++ b/src/App/renderer/slots.js @@ -6,9 +6,11 @@ export const layoutSlots = Object.freeze({ TOP_MIDDLE: 'top-middle', TOP_RIGHT: 'top-right', INSET: 'inset', + LEFT_TOP: 'left-top', + LEFT_BOTTOM: 'left-bottom', + MIDDLE: 'middle', RIGHT_TOP: 'right-top', RIGHT_BOTTOM: 'right-bottom', - MIDDLE: 'middle', FOOTER_RIGHT: 'footer-right', BOTTOM: 'bottom', ACTIONS: 'actions', @@ -30,8 +32,11 @@ export const allowedSlots = Object.freeze({ layoutSlots.SIDE, layoutSlots.BANNER, layoutSlots.INSET, - layoutSlots.RIGHT_BOTTOM, + layoutSlots.LEFT_TOP, + layoutSlots.LEFT_BOTTOM, layoutSlots.MIDDLE, + layoutSlots.RIGHT_TOP, + layoutSlots.RIGHT_BOTTOM, layoutSlots.BOTTOM, layoutSlots.ACTIONS, layoutSlots.MODAL @@ -40,6 +45,8 @@ export const allowedSlots = Object.freeze({ layoutSlots.TOP_LEFT, layoutSlots.TOP_MIDDLE, layoutSlots.TOP_RIGHT, + layoutSlots.LEFT_TOP, + layoutSlots.LEFT_BOTTOM, layoutSlots.RIGHT_TOP, layoutSlots.RIGHT_BOTTOM, layoutSlots.ACTIONS diff --git a/src/App/store/AppProvider.jsx b/src/App/store/AppProvider.jsx index 10345ab8..cf483359 100755 --- a/src/App/store/AppProvider.jsx +++ b/src/App/store/AppProvider.jsx @@ -20,9 +20,13 @@ export const AppProvider = ({ options, children }) => { topLeftColRef: useRef(null), topRightColRef: useRef(null), insetRef: useRef(null), + leftRef: useRef(null), + leftTopRef: useRef(null), + leftBottomRef: useRef(null), + middleRef: useRef(null), rightRef: useRef(null), + rightTopRef: useRef(null), rightBottomRef: useRef(null), - middleRef: useRef(null), bottomRef: useRef(null), footerRef: useRef(null), actionsRef: useRef(null), diff --git a/src/utils/getSafeZoneInset.js b/src/utils/getSafeZoneInset.js index 98748200..aa84c58a 100755 --- a/src/utils/getSafeZoneInset.js +++ b/src/utils/getSafeZoneInset.js @@ -10,6 +10,7 @@ * @param {Object} refs - React refs for the key layout elements. * @param {React.RefObject} refs.mainRef - The main content area. * @param {React.RefObject} refs.insetRef - The inset panel (e.g. search results). + * @param {React.RefObject} refs.leftRef - The left-hand button column. * @param {React.RefObject} refs.rightRef - The right-hand button column. * @param {React.RefObject} refs.actionsRef - The bottom action bar. * @param {React.RefObject} refs.footerRef - The footer (logo, copyright etc). @@ -19,29 +20,31 @@ export const getSafeZoneInset = ({ mainRef, insetRef, + leftRef, rightRef, actionsRef, footerRef }) => { - const refs = [mainRef, insetRef, rightRef, actionsRef, footerRef] + const refs = [mainRef, insetRef, leftRef, rightRef, actionsRef, footerRef] if (refs.some(ref => !ref.current)) { return undefined } - const [main, inset, right, actions, footer] = refs.map(ref => ref.current) + const [main, inset, left, right, actions, footer] = refs.map(ref => ref.current) const root = document.documentElement const dividerGap = Number.parseInt(getComputedStyle(root).getPropertyValue('--divider-gap'), 10) // === Safe area logic === const availableHeight = actions.offsetTop - inset.offsetTop - dividerGap - const rightOffset = inset.offsetLeft + right.offsetWidth + dividerGap - const availableWidth = main.offsetWidth - rightOffset * 2 - const insetOverlapWidth = inset.offsetWidth - rightOffset + inset.offsetLeft + const leftOffset = left.offsetLeft + left.offsetWidth + dividerGap + const rightOffset = left.offsetLeft + right.offsetWidth + dividerGap + const availableWidth = main.offsetWidth - (leftOffset + rightOffset) + const insetOverlapWidth = inset.offsetWidth - leftOffset + left.offsetLeft const isLandscape = availableWidth - insetOverlapWidth > availableHeight - inset.offsetHeight - const topOffset = inset.offsetTop + (!isLandscape && inset.offsetHeight > 0 ? inset.offsetHeight + dividerGap : 0) - const leftOffset = isLandscape ? inset.offsetWidth + inset.offsetLeft + dividerGap : rightOffset + const topOffset = left.offsetTop + (!isLandscape && inset.offsetHeight > 0 ? inset.offsetHeight + dividerGap : 0) + const combinedLeftOffset = isLandscape ? Math.max(inset.offsetWidth, left.offsetWidth) + left.offsetLeft + dividerGap : rightOffset const actionsOffset = main.offsetHeight - actions.offsetTop const footerOffset = main.offsetHeight - footer.offsetTop @@ -49,8 +52,8 @@ export const getSafeZoneInset = ({ const hasRoom = insetOverlapWidth < availableWidth / RATIO && inset.offsetHeight < availableHeight / RATIO const top = hasRoom ? inset.offsetTop : topOffset - const left = main.offsetLeft + (hasRoom ? rightOffset : Math.max(leftOffset, rightOffset)) + const combinedLeft = main.offsetLeft + (hasRoom ? rightOffset : combinedLeftOffset) const bottom = Math.max(actionsOffset, footerOffset) + dividerGap - return { top, right: rightOffset, left, bottom } + return { top, right: rightOffset, left: combinedLeft, bottom } } diff --git a/src/utils/getSafeZoneInset.test.js b/src/utils/getSafeZoneInset.test.js index 3705554e..876ad84b 100644 --- a/src/utils/getSafeZoneInset.test.js +++ b/src/utils/getSafeZoneInset.test.js @@ -1,7 +1,7 @@ import { getSafeZoneInset } from './getSafeZoneInset' describe('getSafeZoneInset', () => { - let mainRef, insetRef, rightRef, footerRef, actionsRef + let mainRef, insetRef, leftRef, rightRef, actionsRef, footerRef let originalGetComputedStyle beforeAll(() => { originalGetComputedStyle = window.getComputedStyle }) @@ -10,80 +10,124 @@ describe('getSafeZoneInset', () => { beforeEach(() => { mainRef = { current: { offsetWidth: 800, offsetHeight: 600, offsetLeft: 0 } } insetRef = { current: { offsetWidth: 100, offsetHeight: 50, offsetTop: 50, offsetLeft: 20 } } - rightRef = { current: { offsetWidth: 50, offsetLeft: 0 } } - footerRef = { current: { offsetTop: 550 } } + leftRef = { current: { offsetWidth: 50, offsetLeft: 20, offsetTop: 10 } } + rightRef = { current: { offsetWidth: 50, offsetLeft: 730 } } actionsRef = { current: { offsetTop: 520 } } + footerRef = { current: { offsetTop: 550 } } - // Mock CSS var --divider-gap = 10 + // CSS var mock window.getComputedStyle = jest.fn().mockReturnValue({ getPropertyValue: () => '10' }) }) - const runScenario = ({ isLandscape, insetHeight }) => { - insetRef.current.offsetHeight = insetHeight + it('returns undefined if any ref.current is null', () => { + const result = getSafeZoneInset({ + mainRef: { current: null }, + insetRef, + leftRef, + rightRef, + actionsRef, + footerRef + }) + expect(result).toBeUndefined() + }) - // Manipulate dimensions to influence landscape heuristic - if (isLandscape) { - mainRef.current.offsetWidth = 1000 - insetRef.current.offsetWidth = 400 - } else { - mainRef.current.offsetWidth = 600 - insetRef.current.offsetWidth = 100 - } + it('portrait mode shifts inset below itself when it does NOT have enough vertical room', () => { + // Mock layout + mainRef.current.offsetWidth = 200 + mainRef.current.offsetHeight = 200 + insetRef.current.offsetWidth = 100 + insetRef.current.offsetHeight = 50 + insetRef.current.offsetTop = 50 + insetRef.current.offsetLeft = 20 - return getSafeZoneInset({ mainRef, insetRef, rightRef, footerRef, actionsRef }) - } + leftRef.current.offsetWidth = 50 + leftRef.current.offsetLeft = 10 + leftRef.current.offsetTop = 10 - it('topOffset adds 0 when portrait and height = 0', () => { - const result = runScenario({ isLandscape: false, insetHeight: 0 }) - expect(result.top).toBe(insetRef.current.offsetTop) - }) + rightRef.current.offsetWidth = 50 + rightRef.current.offsetLeft = 140 - it('landscape returns left = rightOffset when there is enough room', () => { - const result = runScenario({ isLandscape: true, insetHeight: 50 }) - expect(result.top).toBe(insetRef.current.offsetTop) - expect(result.left).toBe(80) // rightOffset = 20 + 50 + 10 - expect(result.left).toBe(result.right) // left equals returned rightOffset - }) + actionsRef.current.offsetTop = 150 + footerRef.current.offsetTop = 180 - it('landscape returns left = rightOffset even when inset height = 0', () => { - const result = runScenario({ isLandscape: true, insetHeight: 0 }) - expect(result.top).toBe(insetRef.current.offsetTop) - expect(result.left).toBe(80) - expect(result.left).toBe(result.right) + const dividerGap = 10 + + const result = getSafeZoneInset({ mainRef, insetRef, leftRef, rightRef, actionsRef, footerRef }) + + // Compute expected values exactly as function would + const availableHeight = actionsRef.current.offsetTop - insetRef.current.offsetTop - dividerGap // 150 - 50 - 10 = 90 + const leftOffset = leftRef.current.offsetLeft + leftRef.current.offsetWidth + dividerGap // 10 + 50 + 10 = 70 + const rightOffset = leftRef.current.offsetLeft + rightRef.current.offsetWidth + dividerGap // 10 + 50 + 10 = 70 + const availableWidth = mainRef.current.offsetWidth - (leftOffset + rightOffset) // 200 - (70+70) = 60 + const insetOverlapWidth = insetRef.current.offsetWidth - leftOffset + leftRef.current.offsetLeft // 100 - 70 + 10 = 40 + const isLandscape = availableWidth - insetOverlapWidth > availableHeight - insetRef.current.offsetHeight // 60-40 > 90-50 => 20>40 false + + const topOffset = leftRef.current.offsetTop + (!isLandscape && insetRef.current.offsetHeight > 0 ? insetRef.current.offsetHeight + dividerGap : 0) // 10 + 50 +10 = 70 + const combinedLeftOffset = isLandscape ? Math.max(insetRef.current.offsetWidth, leftRef.current.offsetWidth) + leftRef.current.offsetLeft + dividerGap : rightOffset // isLandscape=false -> 70 + const actionsOffset = mainRef.current.offsetHeight - actionsRef.current.offsetTop // 200-150=50 + const footerOffset = mainRef.current.offsetHeight - footerRef.current.offsetTop // 200-180=20 + const hasRoom = insetOverlapWidth < availableWidth / 2 && insetRef.current.offsetHeight < availableHeight / 2 // 40 < 60/2? 40<30 false + + const expectedTop = hasRoom ? insetRef.current.offsetTop : topOffset // false -> topOffset = 70 + const expectedLeft = mainRef.current.offsetLeft + (hasRoom ? rightOffset : combinedLeftOffset) // 0+70=70 + const expectedRight = rightOffset // 70 + const expectedBottom = Math.max(actionsOffset, footerOffset) + dividerGap // max(50,20)+10 = 60 + + expect(result.top).toBe(expectedTop) + expect(result.left).toBe(expectedLeft) + expect(result.right).toBe(expectedRight) + expect(result.bottom).toBe(expectedBottom) }) - it('portrait shifts inset below itself when it does NOT have enough vertical room', () => { - // Force a portrait overflow case - mainRef.current.offsetWidth = 200 - insetRef.current.offsetWidth = 100 + it('landscape mode places inset beside panel when enough room', () => { + mainRef.current.offsetWidth = 1000 + insetRef.current.offsetWidth = 200 insetRef.current.offsetHeight = 50 - insetRef.current.offsetTop = 50 - window.getComputedStyle = jest.fn().mockReturnValue({ getPropertyValue: () => '10' }) - const result = getSafeZoneInset({ mainRef, insetRef, rightRef, footerRef, actionsRef }) + const result = getSafeZoneInset({ mainRef, insetRef, leftRef, rightRef, actionsRef, footerRef }) + + const dividerGap = 10 + const leftOffset = leftRef.current.offsetLeft + leftRef.current.offsetWidth + dividerGap + const rightOffset = leftRef.current.offsetLeft + rightRef.current.offsetWidth + dividerGap + const availableWidth = mainRef.current.offsetWidth - (leftOffset + rightOffset) + const insetOverlapWidth = insetRef.current.offsetWidth - leftOffset + leftRef.current.offsetLeft + const availableHeight = actionsRef.current.offsetTop - insetRef.current.offsetTop - dividerGap + const isLandscape = availableWidth - insetOverlapWidth > availableHeight - insetRef.current.offsetHeight + + const topOffset = leftRef.current.offsetTop + (!isLandscape && insetRef.current.offsetHeight > 0 ? insetRef.current.offsetHeight + dividerGap : 0) + const combinedLeftOffset = isLandscape ? Math.max(insetRef.current.offsetWidth, leftRef.current.offsetWidth) + leftRef.current.offsetLeft + dividerGap : rightOffset + const actionsOffset = mainRef.current.offsetHeight - actionsRef.current.offsetTop + const footerOffset = mainRef.current.offsetHeight - footerRef.current.offsetTop + const hasRoom = insetOverlapWidth < availableWidth / 2 && insetRef.current.offsetHeight < availableHeight / 2 + const top = hasRoom ? insetRef.current.offsetTop : topOffset + const combinedLeft = mainRef.current.offsetLeft + (hasRoom ? rightOffset : combinedLeftOffset) + const bottom = Math.max(actionsOffset, footerOffset) + dividerGap - // topOffset = 50 + 50 + 10 = 110 - expect(result.top).toBe(110) - // left = rightOffset = 20 + 50 + 10 = 80 - expect(result.left).toBe(80) - expect(result.right).toBe(80) + expect(result.top).toBe(top) + expect(result.left).toBe(combinedLeft) + expect(result.right).toBe(rightOffset) + expect(result.bottom).toBe(bottom) }) - /** - * Test to ensure coverage for the safety guardrail (Line 29). - * Validates that the function returns undefined if React refs are - * not yet attached to DOM elements. - */ - it('returns undefined if any ref.current is null (unattached)', () => { - const unattachedRefs = { - mainRef: { current: null }, - insetRef: { current: null }, - rightRef: { current: null }, - actionsRef: { current: null }, - footerRef: { current: null } - } + it('portrait mode with zero inset height leaves top unchanged', () => { + insetRef.current.offsetHeight = 0 + mainRef.current.offsetWidth = 500 + insetRef.current.offsetWidth = 100 - const result = getSafeZoneInset(unattachedRefs) - expect(result).toBeUndefined() + const result = getSafeZoneInset({ mainRef, insetRef, leftRef, rightRef, actionsRef, footerRef }) + expect(result.top).toBe(insetRef.current.offsetTop) + }) + + it('calculates correct bottom using max of actions and footer offsets', () => { + mainRef.current.offsetHeight = 600 + actionsRef.current.offsetTop = 500 + footerRef.current.offsetTop = 550 + + const result = getSafeZoneInset({ mainRef, insetRef, leftRef, rightRef, actionsRef, footerRef }) + + const dividerGap = 10 + const expectedBottom = Math.max(mainRef.current.offsetHeight - actionsRef.current.offsetTop, + mainRef.current.offsetHeight - footerRef.current.offsetTop) + dividerGap + expect(result.bottom).toBe(expectedBottom) }) -}) +}) \ No newline at end of file From dbc50f5305de54e294871a4374ba720f3c3f24f0 Mon Sep 17 00:00:00 2001 From: Dan Leech Date: Thu, 5 Mar 2026 16:15:32 +0000 Subject: [PATCH 6/8] Lint fixes --- src/App/renderer/HtmlElementHost.jsx | 1 - src/utils/getSafeZoneInset.test.js | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/App/renderer/HtmlElementHost.jsx b/src/App/renderer/HtmlElementHost.jsx index 91f4658a..7e120b52 100644 --- a/src/App/renderer/HtmlElementHost.jsx +++ b/src/App/renderer/HtmlElementHost.jsx @@ -22,7 +22,6 @@ export const getSlotRef = (slot, layoutRefs) => { 'right-top': layoutRefs.rightTopRef, 'right-bottom': layoutRefs.rightBottomRef, bottom: layoutRefs.bottomRef, - 'right-bottom': layoutRefs.rightBottomRef, actions: layoutRefs.actionsRef, modal: layoutRefs.modalRef } diff --git a/src/utils/getSafeZoneInset.test.js b/src/utils/getSafeZoneInset.test.js index 876ad84b..1c72ac7b 100644 --- a/src/utils/getSafeZoneInset.test.js +++ b/src/utils/getSafeZoneInset.test.js @@ -127,7 +127,7 @@ describe('getSafeZoneInset', () => { const dividerGap = 10 const expectedBottom = Math.max(mainRef.current.offsetHeight - actionsRef.current.offsetTop, - mainRef.current.offsetHeight - footerRef.current.offsetTop) + dividerGap + mainRef.current.offsetHeight - footerRef.current.offsetTop) + dividerGap expect(result.bottom).toBe(expectedBottom) }) -}) \ No newline at end of file +}) From fc7beeac4b3784603a4a27bda9939428feb29c17 Mon Sep 17 00:00:00 2001 From: Dan Leech Date: Fri, 6 Mar 2026 17:15:19 +0000 Subject: [PATCH 7/8] Slot refactoring and search transform async fix --- demo/js/index.js | 2 +- demo/js/planning.js | 17 ++- plugins/search/src/events/fetchSuggestions.js | 15 +- src/App/components/MapButton/MapButton.jsx | 1 + src/App/components/Panel/Panel.jsx | 27 ++-- src/App/hooks/useLayoutMeasurements.js | 35 +++-- src/App/hooks/useLayoutMeasurements.test.js | 28 +++- src/App/hooks/useModalPanelBehaviour.js | 106 +++++++++++--- src/App/hooks/useModalPanelBehaviour.test.js | 66 +++++++-- src/App/layout/layout.module.scss | 138 ++++++++++++++++-- src/App/renderer/HtmlElementHost.jsx | 8 +- src/App/renderer/HtmlElementHost.test.jsx | 12 ++ src/App/renderer/SlotRenderer.jsx | 2 +- src/App/renderer/mapPanels.js | 3 +- src/App/renderer/slots.js | 7 +- 15 files changed, 370 insertions(+), 97 deletions(-) diff --git a/demo/js/index.js b/demo/js/index.js index 22431cf1..481a2ba4 100755 --- a/demo/js/index.js +++ b/demo/js/index.js @@ -126,7 +126,7 @@ const interactiveMap = new InteractiveMap('map', { transformRequest: transformTileRequest, enableZoomControls: true, readMapText: true, - enableFullscreen: true, + // enableFullscreen: true, // hasExitButton: true, // markers: [{ // id: 'location', diff --git a/demo/js/planning.js b/demo/js/planning.js index 8b8e1805..7f587089 100755 --- a/demo/js/planning.js +++ b/demo/js/planning.js @@ -71,8 +71,8 @@ const interactiveMap = new InteractiveMap('map', { mapStylesPlugin({ mapStyles: vtsMapStyles27700, manifest: { - buttons: [{ id: 'mapStyles', desktop: { slot: 'right-top', showLabel: false }}], - panels: [{ id: 'mapStyles', desktop: { slot: 'map-styles-button', width: '400px', modal: false }}] + buttons: [{ id: 'mapStyles', desktop: { slot: 'right-top', showLabel: false, modal: true }}], + panels: [{ id: 'mapStyles', desktop: { slot: 'map-styles-button', width: '400px' }}] } }), scaleBarPlugin({ @@ -136,11 +136,18 @@ interactiveMap.on('app:ready', function (e) { }) interactiveMap.addPanel('test', { label: 'test', - html: '

Test

', - mobile: { slot: 'left-top', open: true }, - tablet: { slot: 'inset', open: true }, + html: '

Test

Test

Test

Test

Test

Test

Test

Test

Test

Test

Test

Test

Test

Test

Test

Test

Test

Test

Test

Test

Test

Test

Test

Test

Test

Test

Test

Test

Test

Test

Test

Test

Test

Test

Test

Test

Test

Test

Test

Test

Test

Test

Test

Test

', + mobile: { slot: 'inset', open: true, modal: true }, + tablet: { slot: 'left-top', open: true }, desktop: { slot: 'left-top', open: true } }) + interactiveMap.addPanel('test2', { + label: 'test2', + html: '

Test

Test

Test

Test

Test

Test

Test

Test

Test

Test

Test

Test

Test

Test

Test

Test

Test

Test

Test

Test

Test

Test

Test

Test

Test

Test

Test

Test

Test

Test

Test

Test

Test

Test

Test

Test

Test

Test

Test

Test

Test

Test

Test

Test

', + mobile: { slot: 'inset', open: true, modal: true }, + tablet: { slot: 'left-bottom', open: true }, + desktop: { slot: 'left-bottom', open: true } + }) }) interactiveMap.on('map:ready', function (e) { diff --git a/plugins/search/src/events/fetchSuggestions.js b/plugins/search/src/events/fetchSuggestions.js index 9fc24d6f..76f6ad06 100755 --- a/plugins/search/src/events/fetchSuggestions.js +++ b/plugins/search/src/events/fetchSuggestions.js @@ -2,19 +2,22 @@ export const sanitiseQuery = (value) => value.replace(/[^a-zA-Z0-9\s\-.,]/g, '').trim() -const getRequestConfig = (ds, query, transformRequest) => { +const getRequestConfig = async (ds, query, transformRequest) => { const defaultRequest = { url: ds.urlTemplate?.replace('{query}', encodeURIComponent(query)), options: { method: 'GET' } } - if (typeof ds.buildRequest === 'function') { + if (ds.buildRequest) { return ds.buildRequest(query, () => defaultRequest) } - return typeof transformRequest === 'function' - ? transformRequest(defaultRequest, query) - : defaultRequest + if (transformRequest) { + const transformedRequest = await transformRequest(defaultRequest, query) + return transformedRequest + } + + return defaultRequest } /** @@ -50,7 +53,7 @@ export const fetchSuggestions = async (value, datasets, dispatch, transformReque let finalResults = [] for (const ds of activeDatasets) { - const request = getRequestConfig(ds, sanitisedValue, transformRequest) + const request = await getRequestConfig(ds, sanitisedValue, transformRequest) const results = await fetchDatasetResults(ds, request, sanitisedValue) // Check if we have results to add diff --git a/src/App/components/MapButton/MapButton.jsx b/src/App/components/MapButton/MapButton.jsx index bd33fc6d..16d8659c 100755 --- a/src/App/components/MapButton/MapButton.jsx +++ b/src/App/components/MapButton/MapButton.jsx @@ -244,6 +244,7 @@ export const MapButton = ({ return (
{showLabel ? buttonEl : {buttonEl}} diff --git a/src/App/components/Panel/Panel.jsx b/src/App/components/Panel/Panel.jsx index 1548fa7b..db8c4f01 100755 --- a/src/App/components/Panel/Panel.jsx +++ b/src/App/components/Panel/Panel.jsx @@ -10,17 +10,17 @@ const computePanelState = (bpConfig, triggeringElement) => { const isAside = bpConfig.slot === 'side' && bpConfig.open && !bpConfig.modal const isDialog = !isAside && bpConfig.dismissible const isModal = bpConfig.modal === true - const isDismissable = bpConfig.dismissible !== false + const isDismissible = bpConfig.dismissible !== false const shouldFocus = Boolean(isModal || triggeringElement) const buttonContainerEl = bpConfig.slot.endsWith('button') ? triggeringElement?.parentNode : undefined - return { isAside, isDialog, isModal, isDismissable, shouldFocus, buttonContainerEl } + return { isAside, isDialog, isModal, isDismissible, shouldFocus, buttonContainerEl } } -const getPanelRole = (isDialog, isDismissable) => { +const getPanelRole = (isDialog, isDismissible) => { if (isDialog) { return 'dialog' } - if (isDismissable) { + if (isDismissible) { return 'complementary' } return 'region' @@ -32,19 +32,20 @@ const buildPanelClassNames = (slot, showLabel) => [ !showLabel && 'im-c-panel--no-heading' ].filter(Boolean).join(' ') -const buildPanelBodyClassNames = (showLabel, isDismissable) => [ +const buildPanelBodyClassNames = (showLabel, isDismissible) => [ 'im-c-panel__body', - !showLabel && isDismissable && 'im-c-panel__body--offset' + !showLabel && isDismissible && 'im-c-panel__body--offset' ].filter(Boolean).join(' ') -const buildPanelProps = ({ elementId, shouldFocus, isDialog, isDismissable, isModal, width, panelClass }) => ({ +const buildPanelProps = ({ elementId, shouldFocus, isDialog, isDismissible, isModal, width, panelClass, slot }) => ({ id: elementId, 'aria-labelledby': `${elementId}-label`, tabIndex: shouldFocus ? -1 : undefined, // nosonar - role: getPanelRole(isDialog, isDismissable), + role: getPanelRole(isDialog, isDismissible), 'aria-modal': isDialog && isModal ? 'true' : undefined, style: width ? { width } : undefined, - className: panelClass + className: panelClass, + 'data-slot': slot }) const buildBodyProps = ({ bodyRef, panelBodyClass, isBodyScrollable, elementId }) => ({ @@ -65,7 +66,7 @@ export const Panel = ({ panelId, panelConfig, props, WrappedChild, label, html, const bpConfig = panelConfig[breakpoint] const elementId = `${id}-panel-${stringToKebab(panelId)}` - const { isAside, isDialog, isModal, isDismissable, shouldFocus, buttonContainerEl } = computePanelState(bpConfig, props?.triggeringElement) + const { isAside, isDialog, isModal, isDismissible, shouldFocus, buttonContainerEl } = computePanelState(bpConfig, props?.triggeringElement) // For persistent panels, gate modal behaviour on open state const isModalActive = isModal && isOpen @@ -97,10 +98,10 @@ export const Panel = ({ panelId, panelConfig, props, WrappedChild, label, html, }, [isOpen]) const panelClass = buildPanelClassNames(bpConfig.slot, bpConfig.showLabel ?? true) - const panelBodyClass = buildPanelBodyClassNames(bpConfig.showLabel ?? true, isDismissable) + const panelBodyClass = buildPanelBodyClassNames(bpConfig.showLabel ?? true, isDismissible) const innerHtmlProp = useMemo(() => html ? { __html: html } : null, [html]) - const panelProps = buildPanelProps({ elementId, shouldFocus, isDialog, isDismissable, isModal, width: bpConfig.width, panelClass }) + const panelProps = buildPanelProps({ elementId, shouldFocus, isDialog, isDismissible, isModal, width: bpConfig.width, panelClass, slot: bpConfig.slot }) const bodyProps = buildBodyProps({ bodyRef, panelBodyClass, isBodyScrollable, elementId }) return ( @@ -115,7 +116,7 @@ export const Panel = ({ panelId, panelConfig, props, WrappedChild, label, html, {label} - {isDismissable && ( + {isDismissible && (