@defra/interactive-map 0.0.15-alpha → 0.0.17-alpha
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/assets/css/docusaurus.css +104 -0
- package/assets/images/favicon.svg +1 -0
- package/assets/images/hero.png +0 -0
- package/assets/images/slot-map.svg +264 -0
- package/dist/css/index.css +1 -1
- package/dist/esm/im-core.js +1 -1
- package/dist/esm/im-shell.js +1 -1
- package/dist/umd/im-core.js +1 -1
- package/dist/umd/index.js +1 -1
- package/docs/api/slots.md +90 -6
- package/docs/api.md +4 -4
- package/docs/architecture.md +3 -1
- package/docs/{demo.mdx → examples.mdx} +1 -1
- package/docs/getting-started.md +5 -4
- package/docs/index.mdx +42 -0
- package/docs/plugins/datasets.md +561 -0
- package/docs/plugins/interact.md +176 -55
- package/docs/plugins/map-styles.md +64 -7
- package/docs/plugins/search.md +207 -63
- package/docs/plugins.md +8 -16
- package/docusaurus.config.cjs +34 -34
- package/jest.setup.js +1 -1
- package/package.json +6 -5
- package/plugins/beta/datasets/dist/css/index.css +85 -15
- package/plugins/beta/datasets/dist/esm/im-datasets-plugin.js +1 -1
- package/plugins/beta/datasets/dist/umd/im-datasets-plugin.js +1 -1
- package/plugins/beta/datasets/dist/umd/index.js +1 -1
- package/plugins/beta/datasets/src/DatasetsInit.jsx +24 -9
- package/plugins/beta/datasets/src/adapters/maplibre/index.js +18 -0
- package/plugins/beta/datasets/src/adapters/maplibre/layerBuilders.js +113 -0
- package/plugins/beta/datasets/src/adapters/maplibre/layerIds.js +69 -0
- package/plugins/beta/datasets/src/adapters/maplibre/maplibreLayerAdapter.js +338 -0
- package/plugins/beta/datasets/src/adapters/maplibre/patternRegistry.js +48 -0
- package/plugins/beta/datasets/src/api/addDataset.js +3 -9
- package/plugins/beta/datasets/src/api/getOpacity.js +17 -0
- package/plugins/beta/datasets/src/api/getStyle.js +13 -0
- package/plugins/beta/datasets/src/api/removeDataset.js +3 -45
- package/plugins/beta/datasets/src/api/setData.js +8 -0
- package/plugins/beta/datasets/src/api/setDatasetVisibility.js +37 -0
- package/plugins/beta/datasets/src/api/setFeatureVisibility.js +22 -0
- package/plugins/beta/datasets/src/api/setOpacity.js +29 -0
- package/plugins/beta/datasets/src/api/setStyle.js +22 -0
- package/plugins/beta/datasets/src/datasets.js +33 -59
- package/plugins/beta/datasets/src/defaults.js +43 -9
- package/plugins/beta/datasets/src/fetch/createDynamicSource.js +39 -30
- package/plugins/beta/datasets/src/fetch/fetchGeoJSON.js +2 -2
- package/plugins/beta/datasets/src/manifest.js +27 -19
- package/plugins/beta/datasets/src/panels/Key.jsx +129 -49
- package/plugins/beta/datasets/src/panels/Key.module.scss +48 -9
- package/plugins/beta/datasets/src/panels/Layers.jsx +131 -29
- package/plugins/beta/datasets/src/panels/Layers.module.scss +50 -8
- package/plugins/beta/datasets/src/reducer.js +128 -9
- package/plugins/beta/datasets/src/styles/patterns.js +157 -0
- package/plugins/beta/datasets/src/utils/bbox.js +8 -6
- package/plugins/beta/datasets/src/utils/filters.js +5 -2
- package/plugins/beta/datasets/src/utils/mergeSublayer.js +78 -0
- package/plugins/beta/draw-es/dist/esm/im-draw-es-plugin.js +1 -1
- package/plugins/beta/draw-es/src/DrawInit.jsx +16 -16
- package/plugins/beta/draw-es/src/api/addFeature.js +3 -3
- package/plugins/beta/draw-es/src/api/deleteFeature.js +3 -3
- package/plugins/beta/draw-es/src/api/editFeature.js +3 -3
- package/plugins/beta/draw-es/src/api/newPolygon.js +3 -3
- package/plugins/beta/draw-es/src/events.js +52 -20
- package/plugins/beta/draw-es/src/events.test.js +301 -0
- package/plugins/beta/draw-es/src/graphic.js +1 -1
- package/plugins/beta/draw-es/src/manifest.js +4 -4
- package/plugins/beta/draw-es/src/reducer.js +1 -1
- package/plugins/beta/draw-es/src/sketchViewModel.js +1 -1
- package/plugins/beta/draw-ml/dist/css/index.css +1 -1
- package/plugins/beta/draw-ml/dist/esm/im-draw-ml-plugin.js +1 -1
- package/plugins/beta/draw-ml/dist/umd/im-draw-ml-plugin.js +1 -1
- package/plugins/beta/draw-ml/src/DrawInit.jsx +49 -52
- package/plugins/beta/draw-ml/src/api/deleteFeature.js +1 -1
- package/plugins/beta/draw-ml/src/api/editFeature.js +8 -5
- package/plugins/beta/draw-ml/src/api/newLine.js +0 -1
- package/plugins/beta/draw-ml/src/api/newPolygon.js +0 -1
- package/plugins/beta/draw-ml/src/api/split.js +4 -4
- package/plugins/beta/draw-ml/src/defaults.js +1 -1
- package/plugins/beta/draw-ml/src/draw.scss +0 -7
- package/plugins/beta/draw-ml/src/events.js +8 -6
- package/plugins/beta/draw-ml/src/manifest.js +29 -29
- package/plugins/beta/draw-ml/src/mapboxDraw.js +1 -1
- package/plugins/beta/draw-ml/src/mapboxSnap.js +17 -18
- package/plugins/beta/draw-ml/src/modes/createDrawMode.js +31 -31
- package/plugins/beta/draw-ml/src/modes/disabledMode.js +1 -1
- package/plugins/beta/draw-ml/src/modes/editVertex/touchHandlers.js +11 -11
- package/plugins/beta/draw-ml/src/modes/editVertex/undoHandlers.js +7 -7
- package/plugins/beta/draw-ml/src/modes/editVertex/vertexOperations.js +8 -8
- package/plugins/beta/draw-ml/src/modes/editVertex/vertexQueries.js +7 -7
- package/plugins/beta/draw-ml/src/modes/editVertexMode.js +32 -24
- package/plugins/beta/draw-ml/src/reducer.js +1 -1
- package/plugins/beta/draw-ml/src/undoStack.js +4 -4
- package/plugins/beta/draw-ml/src/utils/snapHelpers.js +12 -12
- package/plugins/beta/draw-ml/src/utils/spatial.js +11 -11
- package/plugins/beta/frame/dist/esm/im-frame-plugin.js +1 -1
- package/plugins/beta/frame/dist/umd/im-frame-plugin.js +1 -1
- package/plugins/beta/frame/src/Frame.jsx +9 -9
- package/plugins/beta/frame/src/FrameInit.jsx +4 -4
- package/plugins/beta/frame/src/api/addFrame.js +1 -1
- package/plugins/beta/frame/src/api/editFeature.js +1 -1
- package/plugins/beta/frame/src/config.js +1 -1
- package/plugins/beta/frame/src/manifest.js +3 -3
- package/plugins/beta/frame/src/reducer.js +1 -1
- package/plugins/beta/frame/src/utils.js +1 -1
- package/plugins/beta/map-styles/dist/esm/im-map-styles-plugin.js +1 -1
- package/plugins/beta/map-styles/dist/umd/im-map-styles-plugin.js +1 -1
- package/plugins/beta/map-styles/src/MapStyles.jsx +18 -18
- package/plugins/beta/map-styles/src/manifest.js +1 -1
- package/plugins/beta/scale-bar/dist/css/index.css +1 -1
- package/plugins/beta/scale-bar/dist/esm/im-scale-bar-plugin.js +1 -1
- package/plugins/beta/scale-bar/dist/umd/im-scale-bar-plugin.js +1 -1
- package/plugins/beta/scale-bar/src/ScaleBar.jsx +5 -5
- package/plugins/beta/scale-bar/src/index.test.js +3 -3
- package/plugins/beta/scale-bar/src/manifest.js +3 -3
- package/plugins/beta/scale-bar/src/scaleBar.scss +2 -1
- package/plugins/beta/use-location/src/UseLocation.jsx +1 -1
- package/plugins/beta/use-location/src/defaults.js +1 -1
- package/plugins/beta/use-location/src/events.js +3 -3
- package/plugins/interact/dist/css/index.css +1 -1
- package/plugins/interact/dist/esm/im-interact-plugin.js +1 -1
- package/plugins/interact/dist/umd/im-interact-plugin.js +1 -1
- package/plugins/interact/src/InteractInit.jsx +1 -2
- package/plugins/interact/src/api/enable.js +8 -5
- package/plugins/interact/src/api/enable.test.js +2 -2
- package/plugins/interact/src/api/selectFeature.js +4 -4
- package/plugins/interact/src/api/unselectFeature.js +5 -5
- package/plugins/interact/src/defaults.js +0 -1
- package/plugins/interact/src/events.test.js +15 -15
- package/plugins/interact/src/hooks/useHighlightSync.js +1 -1
- package/plugins/interact/src/hooks/useInteractionHandlers.js +2 -2
- package/plugins/interact/src/hooks/useInteractionHandlers.test.js +5 -5
- package/plugins/interact/src/interact.scss +0 -7
- package/plugins/interact/src/manifest.js +15 -19
- package/plugins/interact/src/manifest.test.js +6 -5
- package/plugins/interact/src/reducer.js +3 -3
- package/plugins/interact/src/reducer.test.js +0 -1
- package/plugins/interact/src/utils/spatial.js +10 -10
- package/plugins/interact/src/utils/spatial.test.js +14 -14
- package/plugins/search/dist/css/index.css +1 -1
- package/plugins/search/dist/esm/im-search-plugin.js +1 -1
- package/plugins/search/dist/esm/index.js +1 -1
- package/plugins/search/dist/umd/im-search-plugin.js +1 -1
- package/plugins/search/dist/umd/index.js +1 -1
- package/plugins/search/src/Search.jsx +7 -6
- package/plugins/search/src/Search.test.jsx +23 -23
- package/plugins/search/src/components/CloseButton/CloseButton.jsx +15 -15
- package/plugins/search/src/components/CloseButton/CloseButton.test.jsx +2 -2
- package/plugins/search/src/components/Form/Form.jsx +14 -14
- package/plugins/search/src/components/Form/Form.module.scss +2 -1
- package/plugins/search/src/components/Form/Form.test.jsx +11 -11
- package/plugins/search/src/components/OpenButton/OpenButton.jsx +16 -15
- package/plugins/search/src/components/OpenButton/OpenButton.test.jsx +6 -2
- package/plugins/search/src/components/SubmitButton/SubmitButton.jsx +15 -15
- package/plugins/search/src/components/Suggestions/Suggestions.jsx +6 -6
- package/plugins/search/src/components/Suggestions/Suggestions.test.jsx +4 -4
- package/plugins/search/src/datasets.js +12 -13
- package/plugins/search/src/datasets.test.js +1 -1
- package/plugins/search/src/defaults.js +1 -1
- package/plugins/search/src/events/fetchSuggestions.js +3 -3
- package/plugins/search/src/events/fetchSuggestions.test.js +1 -1
- package/plugins/search/src/events/formHandlers.js +3 -3
- package/plugins/search/src/events/formHandlers.test.js +1 -1
- package/plugins/search/src/events/index.js +2 -2
- package/plugins/search/src/events/index.test.js +2 -2
- package/plugins/search/src/events/inputHandlers.js +4 -4
- package/plugins/search/src/events/inputHandlers.test.js +1 -1
- package/plugins/search/src/events/suggestionHandlers.js +2 -2
- package/plugins/search/src/events/suggestionHandlers.test.js +1 -1
- package/plugins/search/src/index.js +2 -1
- package/plugins/search/src/index.test.js +3 -3
- package/plugins/search/src/manifest.js +6 -4
- package/plugins/search/src/reducer.js +1 -2
- package/plugins/search/src/reducer.test.js +2 -2
- package/plugins/search/src/search.scss +10 -3
- package/plugins/search/src/utils/parseOsNamesResults.js +1 -2
- package/plugins/search/src/utils/parseOsNamesResults.test.js +2 -2
- package/plugins/search/src/utils/updateMap.js +1 -1
- package/plugins/search/src/utils/updateMap.test.js +5 -5
- package/providers/beta/esri/dist/esm/im-esri-provider.js +1 -1
- package/providers/beta/esri/src/esriProvider.js +5 -5
- package/providers/beta/esri/src/utils/coords.js +1 -1
- package/providers/beta/esri/src/utils/esriFixes.js +1 -1
- package/providers/beta/esri/src/utils/query.js +4 -4
- package/providers/beta/esri/src/utils/spatial.js +1 -2
- package/providers/beta/esri/src/utils/spatial.test.js +4 -1
- package/providers/beta/open-names/src/utils/mapToLocationModel.test.js +1 -1
- package/providers/maplibre/dist/esm/im-maplibre-provider.js +1 -1
- package/providers/maplibre/dist/umd/im-maplibre-framework.js +1 -1
- package/providers/maplibre/dist/umd/im-maplibre-framework.js.LICENSE.txt +1 -1
- package/providers/maplibre/dist/umd/im-maplibre-provider.js +1 -1
- package/providers/maplibre/src/appEvents.test.js +1 -1
- package/providers/maplibre/src/index.js +1 -1
- package/providers/maplibre/src/index.test.js +3 -5
- package/providers/maplibre/src/mapEvents.test.js +15 -5
- package/providers/maplibre/src/maplibreProvider.test.js +6 -2
- package/providers/maplibre/src/utils/calculateLinearTextSize.js +4 -4
- package/providers/maplibre/src/utils/calculateLinearTextSize.test.js +3 -3
- package/providers/maplibre/src/utils/detectWebgl.test.js +1 -1
- package/providers/maplibre/src/utils/highlightFeatures.js +3 -2
- package/providers/maplibre/src/utils/highlightFeatures.test.js +13 -6
- package/providers/maplibre/src/utils/labels.js +19 -20
- package/providers/maplibre/src/utils/labels.test.js +15 -13
- package/providers/maplibre/src/utils/maplibreFixes.test.js +1 -1
- package/providers/maplibre/src/utils/queryFeatures.js +6 -6
- package/providers/maplibre/src/utils/queryFeatures.test.js +13 -13
- package/providers/maplibre/src/utils/spatial.js +0 -1
- package/providers/maplibre/src/utils/spatial.test.js +26 -27
- package/src/App/components/Actions/Actions.jsx +2 -2
- package/src/App/components/Actions/Actions.module.scss +0 -7
- package/src/App/components/Actions/Actions.test.jsx +1 -1
- package/src/App/components/Icon/Icon.jsx +3 -2
- package/src/App/components/Icon/Icon.module.scss +4 -0
- package/src/App/components/Icon/Icon.test.jsx +43 -4
- package/src/App/components/MapButton/MapButton.jsx +42 -17
- package/src/App/components/MapButton/MapButton.module.scss +4 -13
- package/src/App/components/MapButton/MapButton.test.jsx +27 -3
- package/src/App/components/PopupMenu/PopupMenu.jsx +51 -274
- package/src/App/components/PopupMenu/PopupMenu.module.scss +14 -7
- package/src/App/components/PopupMenu/PopupMenu.test.jsx +70 -1
- package/src/App/components/PopupMenu/usePopupMenu.js +258 -0
- package/src/App/hooks/useButtonStateEvaluator.js +12 -2
- package/src/App/hooks/useButtonStateEvaluator.test.js +38 -4
- package/src/App/hooks/useInterfaceAPI.js +6 -0
- package/src/App/hooks/useLayoutMeasurements.js +84 -18
- package/src/App/hooks/useLayoutMeasurements.test.js +124 -17
- package/src/App/layout/Layout.jsx +12 -7
- package/src/App/layout/Layout.test.jsx +2 -2
- package/src/App/layout/layout.module.scss +67 -29
- package/src/App/registry/pluginRegistry.js +17 -0
- package/src/App/registry/pluginRegistry.test.js +33 -0
- package/src/App/renderer/HtmlElementHost.jsx +2 -1
- package/src/App/renderer/HtmlElementHost.test.jsx +7 -7
- package/src/App/renderer/mapButtons.js +3 -2
- package/src/App/renderer/mapPanels.test.js +2 -2
- package/src/App/renderer/slotHelpers.js +2 -2
- package/src/App/renderer/slotHelpers.test.js +5 -5
- package/src/App/renderer/slots.js +9 -5
- package/src/App/store/AppProvider.jsx +3 -1
- package/src/App/store/AppProvider.test.jsx +1 -1
- package/src/App/store/ServiceProvider.jsx +3 -1
- package/src/App/store/appActionsMap.js +16 -0
- package/src/App/store/appActionsMap.test.js +27 -0
- package/src/App/store/appDispatchMiddleware.js +33 -1
- package/src/App/store/appDispatchMiddleware.test.js +250 -222
- package/src/App/store/appReducer.js +2 -0
- package/src/InteractiveMap/InteractiveMap.js +4 -0
- package/src/config/appConfig.js +7 -4
- package/src/config/events.js +28 -0
- package/src/scss/main.scss +1 -0
- package/src/scss/settings/_dimensions.scss +0 -1
- package/src/services/logger.js +6 -0
- package/src/services/logger.test.js +32 -0
- package/src/utils/getSafeZoneInset.js +9 -7
- package/src/utils/getSafeZoneInset.test.js +10 -10
- package/webpack.dev.mjs +23 -19
- package/docs/govuk-prototype.md +0 -23
- package/docs/index.md +0 -19
- package/plugins/beta/datasets/src/api/hideDataset.js +0 -14
- package/plugins/beta/datasets/src/api/hideFeatures.js +0 -41
- package/plugins/beta/datasets/src/api/showDataset.js +0 -14
- package/plugins/beta/datasets/src/api/showFeatures.js +0 -44
- package/plugins/beta/datasets/src/handleSetMapStyle.js +0 -54
- package/plugins/beta/datasets/src/mapLayers.js +0 -165
|
@@ -5,14 +5,13 @@ import {
|
|
|
5
5
|
navigateToNextLabel, createMapLabelNavigator
|
|
6
6
|
} from './labels.js'
|
|
7
7
|
|
|
8
|
-
jest.mock('./spatial.js', () => ({ spatialNavigate: jest.fn() }))
|
|
9
|
-
jest.mock('./calculateLinearTextSize.js', () => ({ calculateLinearTextSize: jest.fn(() => 12) }))
|
|
10
|
-
|
|
11
8
|
import { spatialNavigate } from './spatial.js'
|
|
12
9
|
import { calculateLinearTextSize } from './calculateLinearTextSize.js'
|
|
13
10
|
|
|
14
|
-
|
|
11
|
+
jest.mock('./spatial.js', () => ({ spatialNavigate: jest.fn() }))
|
|
12
|
+
jest.mock('./calculateLinearTextSize.js', () => ({ calculateLinearTextSize: jest.fn(() => 12) }))
|
|
15
13
|
|
|
14
|
+
describe('labels utils', () => {
|
|
16
15
|
test('getGeometryCenter all geometry types', () => {
|
|
17
16
|
expect(getGeometryCenter({ type: 'Point', coordinates: [1, 2] })).toEqual([1, 2])
|
|
18
17
|
expect(getGeometryCenter({ type: 'MultiPoint', coordinates: [[3, 4]] })).toEqual([3, 4])
|
|
@@ -30,9 +29,9 @@ describe('labels utils', () => {
|
|
|
30
29
|
expect(evalInterpolate(['literal', 'x'], 10)).toBe(12)
|
|
31
30
|
expect(() => evalInterpolate(['interpolate', ['linear'], ['get', 'p'], 5, 10], 10)).toThrow()
|
|
32
31
|
const expr = ['interpolate', ['linear'], ['zoom'], 5, 10, 10, 20]
|
|
33
|
-
expect(evalInterpolate(expr, 3)).toBe(10)
|
|
34
|
-
expect(evalInterpolate(expr, 7.5)).toBe(15)
|
|
35
|
-
expect(evalInterpolate(expr, 15)).toBe(20)
|
|
32
|
+
expect(evalInterpolate(expr, 3)).toBe(10) // zoom <= z0
|
|
33
|
+
expect(evalInterpolate(expr, 7.5)).toBe(15) // interpolated
|
|
34
|
+
expect(evalInterpolate(expr, 15)).toBe(20) // beyond last stop
|
|
36
35
|
})
|
|
37
36
|
|
|
38
37
|
test('getHighlightColors', () => {
|
|
@@ -77,7 +76,7 @@ describe('labels utils', () => {
|
|
|
77
76
|
|
|
78
77
|
test('findClosestLabel: empty → undefined; returns closest; skips farther', () => {
|
|
79
78
|
expect(findClosestLabel([], { x: 0, y: 0 })).toBeUndefined()
|
|
80
|
-
const labels = [{ x: 2, y: 0 }, { x: 10, y: 0 }]
|
|
79
|
+
const labels = [{ x: 2, y: 0 }, { x: 10, y: 0 }] // closer first → second hits false branch
|
|
81
80
|
expect(findClosestLabel(labels, { x: 0, y: 0 })).toBe(labels[0])
|
|
82
81
|
})
|
|
83
82
|
|
|
@@ -109,9 +108,12 @@ describe('labels utils', () => {
|
|
|
109
108
|
|
|
110
109
|
test('applyHighlight: early returns without feature.layer; applies otherwise', () => {
|
|
111
110
|
const map = {
|
|
112
|
-
getLayer: jest.fn(),
|
|
111
|
+
getLayer: jest.fn(),
|
|
112
|
+
removeLayer: jest.fn(),
|
|
113
113
|
getSource: jest.fn(() => ({ setData: jest.fn() })),
|
|
114
|
-
getZoom: jest.fn(() => 10),
|
|
114
|
+
getZoom: jest.fn(() => 10),
|
|
115
|
+
addLayer: jest.fn(),
|
|
116
|
+
moveLayer: jest.fn()
|
|
115
117
|
}
|
|
116
118
|
const state = { highlightLayerId: null, highlightedExpr: null, isDarkStyle: false }
|
|
117
119
|
applyHighlight(map, null, state)
|
|
@@ -132,9 +134,9 @@ describe('labels utils', () => {
|
|
|
132
134
|
currentPixel: { x: 1, y: 1 }, labels: [{ x: 1, y: 1 }]
|
|
133
135
|
})).toBeNull()
|
|
134
136
|
const state = { currentPixel: { x: 0, y: 0 }, labels: [{ x: 0, y: 0 }, { x: 5, y: 5 }] }
|
|
135
|
-
spatialNavigate.mockReturnValue(-1)
|
|
137
|
+
spatialNavigate.mockReturnValue(-1) // out of range → use 0
|
|
136
138
|
expect(navigateToNextLabel('ArrowRight', state)).toBe(state.labels[1])
|
|
137
|
-
spatialNavigate.mockReturnValue(0)
|
|
139
|
+
spatialNavigate.mockReturnValue(0) // valid index
|
|
138
140
|
expect(navigateToNextLabel('ArrowRight', state)).toBe(state.labels[1])
|
|
139
141
|
})
|
|
140
142
|
|
|
@@ -212,7 +214,7 @@ describe('labels utils', () => {
|
|
|
212
214
|
})
|
|
213
215
|
|
|
214
216
|
test('initLabelSource skips addSource when source exists; MAP_SET_STYLE triggers re-init', () => {
|
|
215
|
-
map.getSource.mockReset().mockReturnValue({ setData: jest.fn() })
|
|
217
|
+
map.getSource.mockReset().mockReturnValue({ setData: jest.fn() }) // source always exists
|
|
216
218
|
const eventBus = { on: jest.fn() }
|
|
217
219
|
createMapLabelNavigator(map, 'light', { MAP_SET_STYLE: 'set-style' }, eventBus)
|
|
218
220
|
expect(map.addSource).not.toHaveBeenCalled()
|
|
@@ -30,7 +30,7 @@ const isPointInPolygon = (point, ring) => {
|
|
|
30
30
|
const intersectX = ((xj - xi) * (py - yi)) / (yj - yi) + xi
|
|
31
31
|
|
|
32
32
|
if (px < intersectX) {
|
|
33
|
-
inside = !inside
|
|
33
|
+
inside = !inside
|
|
34
34
|
}
|
|
35
35
|
}
|
|
36
36
|
return inside
|
|
@@ -43,7 +43,7 @@ const getMinDistToGeometry = (map, point, geometry) => {
|
|
|
43
43
|
const { coordinates: coords, type } = geometry
|
|
44
44
|
let minSqDist = Infinity
|
|
45
45
|
const getScreenPt = (lngLat) => map.project(lngLat)
|
|
46
|
-
|
|
46
|
+
|
|
47
47
|
const processLine = (lineCoords) => {
|
|
48
48
|
for (let i = 0; i < lineCoords.length - 1; i++) {
|
|
49
49
|
const d2 = distToSegmentSquared(point, getScreenPt(lineCoords[i]), getScreenPt(lineCoords[i + 1]))
|
|
@@ -52,7 +52,7 @@ const getMinDistToGeometry = (map, point, geometry) => {
|
|
|
52
52
|
}
|
|
53
53
|
}
|
|
54
54
|
}
|
|
55
|
-
|
|
55
|
+
|
|
56
56
|
if (type === 'Point') {
|
|
57
57
|
const p = getScreenPt(coords)
|
|
58
58
|
minSqDist = (point.x - p.x) ** 2 + (point.y - p.y) ** 2
|
|
@@ -117,7 +117,7 @@ export const queryFeatures = (map, point, options = {}) => {
|
|
|
117
117
|
let score = 0
|
|
118
118
|
const type = f.geometry.type
|
|
119
119
|
const pixelDistSq = getMinDistToGeometry(map, point, f.geometry)
|
|
120
|
-
|
|
120
|
+
|
|
121
121
|
// PRIORITY 1: LAYER ORDER
|
|
122
122
|
const layerRank = layerStack.indexOf(f.layer.id)
|
|
123
123
|
score += (layerRank * 1000000)
|
|
@@ -126,7 +126,7 @@ export const queryFeatures = (map, point, options = {}) => {
|
|
|
126
126
|
if (type.includes('Polygon')) {
|
|
127
127
|
const polys = type === 'Polygon' ? [f.geometry.coordinates] : f.geometry.coordinates
|
|
128
128
|
const isInside = polys.some((ring) => isPointInPolygon(clickPt, ring[0]))
|
|
129
|
-
|
|
129
|
+
|
|
130
130
|
if (isInside === true) {
|
|
131
131
|
// Massive boost for polygons if we are actually inside them
|
|
132
132
|
score -= 500000 // NOSONAR - tolerance used only here
|
|
@@ -143,4 +143,4 @@ export const queryFeatures = (map, point, options = {}) => {
|
|
|
143
143
|
})
|
|
144
144
|
.sort((a, b) => a.score - b.score)
|
|
145
145
|
.map(({ f }) => f)
|
|
146
|
-
}
|
|
146
|
+
}
|
|
@@ -15,12 +15,12 @@ describe('queryFeatures coverage', () => {
|
|
|
15
15
|
const cases = [
|
|
16
16
|
{ type: 'Point', coords: [0, 0], p: { x: 3, y: 4 } },
|
|
17
17
|
{ type: 'LineString', coords: [[0, 0], [10, 0]], p: { x: 5, y: 5 } }, // t=0.5
|
|
18
|
-
{ type: 'LineString', coords: [[0, 0], [0, 0]], p: { x: 1, y: 1 } },
|
|
18
|
+
{ type: 'LineString', coords: [[0, 0], [0, 0]], p: { x: 1, y: 1 } }, // l2=0
|
|
19
19
|
{ type: 'LineString', coords: [[0, 0], [10, 0]], p: { x: -5, y: 0 } }, // t<0
|
|
20
20
|
{ type: 'MultiPoint', coords: [[0, 0], [10, 10]], p: { x: 1, y: 0 } },
|
|
21
21
|
{ type: 'MultiLineString', coords: [[[0, 0], [10, 0]]], p: { x: 5, y: 1 } },
|
|
22
22
|
{ type: 'Polygon', coords: [[[0, 0], [10, 0], [10, 10], [0, 10], [0, 0]]], p: { x: 5, y: 5 } }, // Inside
|
|
23
|
-
{ type: 'MultiPolygon', coords: [[[[0, 0], [10, 0], [10, 10], [0, 0]]]], p: { x: 20, y: 20 } },
|
|
23
|
+
{ type: 'MultiPolygon', coords: [[[[0, 0], [10, 0], [10, 10], [0, 0]]]], p: { x: 20, y: 20 } }, // Outside
|
|
24
24
|
{ type: 'Unknown', coords: [], p: { x: 0, y: 0 } }
|
|
25
25
|
]
|
|
26
26
|
|
|
@@ -31,21 +31,21 @@ describe('queryFeatures coverage', () => {
|
|
|
31
31
|
})
|
|
32
32
|
|
|
33
33
|
// 3. Hits Line 144 (.sort) and property-based ID fallback
|
|
34
|
-
const f1 = {
|
|
35
|
-
properties: { key: 'a' },
|
|
36
|
-
layer: { id: 'layer-A' },
|
|
37
|
-
geometry: { type: 'Point', coordinates: [10, 10] }
|
|
34
|
+
const f1 = {
|
|
35
|
+
properties: { key: 'a' },
|
|
36
|
+
layer: { id: 'layer-A' },
|
|
37
|
+
geometry: { type: 'Point', coordinates: [10, 10] }
|
|
38
38
|
}
|
|
39
|
-
const f2 = {
|
|
40
|
-
id: 'b',
|
|
41
|
-
layer: { id: 'layer-B' },
|
|
42
|
-
geometry: { type: 'Point', coordinates: [0, 0] }
|
|
39
|
+
const f2 = {
|
|
40
|
+
id: 'b',
|
|
41
|
+
layer: { id: 'layer-B' },
|
|
42
|
+
geometry: { type: 'Point', coordinates: [0, 0] }
|
|
43
43
|
}
|
|
44
|
-
|
|
44
|
+
|
|
45
45
|
// map.queryRenderedFeatures returns multiple items to trigger .sort()
|
|
46
46
|
const sortMap = { ...mockMap, queryRenderedFeatures: () => [f1, f2] }
|
|
47
47
|
const result = queryFeatures(sortMap, { x: 0, y: 0 })
|
|
48
|
-
|
|
48
|
+
|
|
49
49
|
expect(result.length).toBe(2)
|
|
50
50
|
expect(result[0].layer.id).toBe('layer-A') // Sorted by layerStack index
|
|
51
51
|
|
|
@@ -57,4 +57,4 @@ describe('queryFeatures coverage', () => {
|
|
|
57
57
|
const rayMap = { ...mockMap, queryRenderedFeatures: () => [polyFeat] }
|
|
58
58
|
expect(queryFeatures(rayMap, { x: -1, y: 5 }).length).toBe(1)
|
|
59
59
|
})
|
|
60
|
-
})
|
|
60
|
+
})
|
|
@@ -10,7 +10,6 @@ jest.mock('geodesy/latlon-spherical.js', () =>
|
|
|
10
10
|
jest.mock('@turf/bbox', () => jest.fn(() => [-1, 50, 1, 52]))
|
|
11
11
|
|
|
12
12
|
describe('spatial utils', () => {
|
|
13
|
-
|
|
14
13
|
test('formatDimension hits all branches', () => {
|
|
15
14
|
// < 0.5 miles
|
|
16
15
|
expect(spatial.formatDimension(500)).toMatch(/m$/)
|
|
@@ -43,55 +42,55 @@ describe('spatial utils', () => {
|
|
|
43
42
|
})
|
|
44
43
|
|
|
45
44
|
test('north/south/east/west moves', () => {
|
|
46
|
-
expect(spatial.getCardinalMove([0,0],[0,0.5])).toMatch(/north/)
|
|
47
|
-
expect(spatial.getCardinalMove([0,0],[0
|
|
48
|
-
expect(spatial.getCardinalMove([0,0],[0.5,0])).toMatch(/east/)
|
|
49
|
-
expect(spatial.getCardinalMove([0,0],[-0.5,0])).toMatch(/west/)
|
|
50
|
-
expect(spatial.getCardinalMove([0,0],[0.5,0.5])).toMatch(/north.*east|east.*north/)
|
|
51
|
-
expect(spatial.getCardinalMove([0,0],[0.00001,0.00001])).toBe('')
|
|
45
|
+
expect(spatial.getCardinalMove([0, 0], [0, 0.5])).toMatch(/north/)
|
|
46
|
+
expect(spatial.getCardinalMove([0, 0], [0, -0.5])).toMatch(/south/)
|
|
47
|
+
expect(spatial.getCardinalMove([0, 0], [0.5, 0])).toMatch(/east/)
|
|
48
|
+
expect(spatial.getCardinalMove([0, 0], [-0.5, 0])).toMatch(/west/)
|
|
49
|
+
expect(spatial.getCardinalMove([0, 0], [0.5, 0.5])).toMatch(/north.*east|east.*north/)
|
|
50
|
+
expect(spatial.getCardinalMove([0, 0], [0.00001, 0.00001])).toBe('')
|
|
52
51
|
})
|
|
53
52
|
|
|
54
53
|
test('spatialNavigate all directions and fallback', () => {
|
|
55
|
-
const pixels = [[0,0],[0
|
|
56
|
-
expect(spatial.spatialNavigate('ArrowUp',[0,0],pixels)).toBe(1)
|
|
57
|
-
expect(spatial.spatialNavigate('ArrowDown',[0,0],pixels)).toBe(3)
|
|
58
|
-
expect(spatial.spatialNavigate('ArrowLeft',[0,0],pixels)).toBe(4)
|
|
59
|
-
expect(spatial.spatialNavigate('ArrowRight',[0,0],pixels)).toBe(2)
|
|
60
|
-
expect(spatial.spatialNavigate('InvalidDir',[0,0],pixels)).toBe(0)
|
|
54
|
+
const pixels = [[0, 0], [0, -1], [1, 0], [0, 1], [-1, 0]]
|
|
55
|
+
expect(spatial.spatialNavigate('ArrowUp', [0, 0], pixels)).toBe(1)
|
|
56
|
+
expect(spatial.spatialNavigate('ArrowDown', [0, 0], pixels)).toBe(3)
|
|
57
|
+
expect(spatial.spatialNavigate('ArrowLeft', [0, 0], pixels)).toBe(4)
|
|
58
|
+
expect(spatial.spatialNavigate('ArrowRight', [0, 0], pixels)).toBe(2)
|
|
59
|
+
expect(spatial.spatialNavigate('InvalidDir', [0, 0], pixels)).toBe(0)
|
|
61
60
|
})
|
|
62
61
|
|
|
63
62
|
test('spatialNavigate finds closer candidates (hits dist < minDist)', () => {
|
|
64
|
-
const start = [0,0]
|
|
65
|
-
const pixels = [[0,0],[10,0],[2,0]]
|
|
63
|
+
const start = [0, 0]
|
|
64
|
+
const pixels = [[0, 0], [10, 0], [2, 0]]
|
|
66
65
|
expect(spatial.spatialNavigate('ArrowRight', start, pixels)).toBe(2)
|
|
67
66
|
})
|
|
68
67
|
|
|
69
68
|
test('spatialNavigate skips farther candidate (dist >= minDist false branch)', () => {
|
|
70
69
|
// Closer candidate first → second candidate fails dist < minDist
|
|
71
|
-
const pixels = [[0,0],[2,0],[10,0]]
|
|
72
|
-
expect(spatial.spatialNavigate('ArrowRight', [0,0], pixels)).toBe(1)
|
|
70
|
+
const pixels = [[0, 0], [2, 0], [10, 0]]
|
|
71
|
+
expect(spatial.spatialNavigate('ArrowRight', [0, 0], pixels)).toBe(1)
|
|
73
72
|
})
|
|
74
73
|
|
|
75
74
|
test('spatialNavigate diagonal with dx>dy', () => {
|
|
76
|
-
const start = [0,0]
|
|
77
|
-
const pixels = [[0,0],[3,1],[1,0]] // dx>dy
|
|
75
|
+
const start = [0, 0]
|
|
76
|
+
const pixels = [[0, 0], [3, 1], [1, 0]] // dx>dy
|
|
78
77
|
expect(spatial.spatialNavigate('ArrowRight', start, pixels)).toBe(2)
|
|
79
78
|
})
|
|
80
79
|
|
|
81
80
|
test('getResolution returns positive value', () => {
|
|
82
|
-
expect(spatial.getResolution({lat:0},1)).toBeGreaterThan(0)
|
|
81
|
+
expect(spatial.getResolution({ lat: 0 }, 1)).toBeGreaterThan(0)
|
|
83
82
|
})
|
|
84
83
|
|
|
85
84
|
test('getPaddedBounds returns bounds', () => {
|
|
86
85
|
const map = {
|
|
87
|
-
getContainer: () => ({ getBoundingClientRect: () => ({ width:100,height:200 }) }),
|
|
88
|
-
getPadding: () => ({ top:1,right:2,bottom:3,left:4 }),
|
|
89
|
-
unproject: p => ({ x:p[0], y:p[1] })
|
|
86
|
+
getContainer: () => ({ getBoundingClientRect: () => ({ width: 100, height: 200 }) }),
|
|
87
|
+
getPadding: () => ({ top: 1, right: 2, bottom: 3, left: 4 }),
|
|
88
|
+
unproject: p => ({ x: p[0], y: p[1] })
|
|
90
89
|
}
|
|
91
|
-
const LngLatBounds = function(sw,ne){
|
|
92
|
-
return {sw,ne}
|
|
90
|
+
const LngLatBounds = function (sw, ne) {
|
|
91
|
+
return { sw, ne }
|
|
93
92
|
}
|
|
94
|
-
const bounds = spatial.getPaddedBounds(LngLatBounds,map)
|
|
93
|
+
const bounds = spatial.getPaddedBounds(LngLatBounds, map)
|
|
95
94
|
expect(bounds.sw).toBeDefined()
|
|
96
95
|
expect(bounds.ne).toBeDefined()
|
|
97
96
|
})
|
|
@@ -140,4 +139,4 @@ describe('spatial utils', () => {
|
|
|
140
139
|
expect(map.project).toHaveBeenCalledTimes(4)
|
|
141
140
|
})
|
|
142
141
|
})
|
|
143
|
-
})
|
|
142
|
+
})
|
|
@@ -7,11 +7,11 @@ export const Actions = ({ children }) => {
|
|
|
7
7
|
const { openPanels, panelConfig, breakpoint } = useApp()
|
|
8
8
|
|
|
9
9
|
const childArray = React.Children.toArray(children)
|
|
10
|
-
const visibleChild = childArray.find(c => c.props?.isHidden === false)
|
|
10
|
+
const visibleChild = childArray.find(c => c.props?.isHidden === false && c.props?.variant !== 'touch')
|
|
11
11
|
|
|
12
12
|
// If a panel exists above we need so css adjustment
|
|
13
13
|
const isBottomSlotUsed = Object.keys(openPanels).some(panelId => {
|
|
14
|
-
return breakpoint === 'mobile' && panelConfig[panelId]?.[breakpoint]?.slot === '
|
|
14
|
+
return breakpoint === 'mobile' && panelConfig[panelId]?.[breakpoint]?.slot === 'drawer'
|
|
15
15
|
})
|
|
16
16
|
|
|
17
17
|
const className = [
|
|
@@ -6,19 +6,12 @@
|
|
|
6
6
|
padding: var(--panel-margin);
|
|
7
7
|
max-height: 200px;
|
|
8
8
|
|
|
9
|
-
@media (prefers-reduced-motion: no-preference) {
|
|
10
|
-
transition: max-height var(--duration) ease, opacity var(--duration) ease,
|
|
11
|
-
padding-top var(--duration) ease, padding-bottom var(--duration) ease;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
9
|
&--border-top {
|
|
15
10
|
border-top: 1px solid var(--app-border-color);
|
|
16
11
|
}
|
|
17
12
|
|
|
18
13
|
&--hidden {
|
|
19
14
|
max-height: 0;
|
|
20
|
-
overflow: hidden;
|
|
21
|
-
opacity: 0;
|
|
22
15
|
padding-top: 0;
|
|
23
16
|
padding-bottom: 0;
|
|
24
17
|
border: 0;
|
|
@@ -29,7 +29,7 @@ describe('Actions component', () => {
|
|
|
29
29
|
|
|
30
30
|
it('adds the border class if a bottom slot panel is open', () => {
|
|
31
31
|
mockUseApp.openPanels = { key: {} }
|
|
32
|
-
mockUseApp.panelConfig = { key: { mobile: { slot: '
|
|
32
|
+
mockUseApp.panelConfig = { key: { mobile: { slot: 'drawer' } } }
|
|
33
33
|
|
|
34
34
|
render(<Actions slot='actions'>Content</Actions>)
|
|
35
35
|
const container = screen.getByText('Content').closest('div')
|
|
@@ -3,8 +3,8 @@ import { getIconRegistry } from '../../registry/iconRegistry.js'
|
|
|
3
3
|
|
|
4
4
|
// eslint-disable-next-line camelcase, react/jsx-pascal-case
|
|
5
5
|
// sonarjs/disable-next-line function-name
|
|
6
|
-
export const Icon = ({ id, svgContent }) => {
|
|
7
|
-
const icon = getIconRegistry()[id] || svgContent
|
|
6
|
+
export const Icon = ({ id, svgContent, isMenu }) => {
|
|
7
|
+
const icon = isMenu ? getIconRegistry().chevron : (getIconRegistry()[id] || svgContent)
|
|
8
8
|
|
|
9
9
|
return (
|
|
10
10
|
<svg
|
|
@@ -20,6 +20,7 @@ export const Icon = ({ id, svgContent }) => {
|
|
|
20
20
|
aria-hidden='true'
|
|
21
21
|
focusable='false'
|
|
22
22
|
dangerouslySetInnerHTML={{ __html: icon }}
|
|
23
|
+
className={`im-c-icon${isMenu ? ' im-c-icon--narrow' : ''}`}
|
|
23
24
|
/>
|
|
24
25
|
)
|
|
25
26
|
}
|
|
@@ -33,27 +33,66 @@ describe('Icon component', () => {
|
|
|
33
33
|
it('renders the SVG from the registry when id is provided', () => {
|
|
34
34
|
getIconRegistry.mockReturnValue({ close: '<path d="M0 0 L10 10"/>' })
|
|
35
35
|
const { container } = render(<Icon id='close' />)
|
|
36
|
-
expect(container.querySelector('svg').innerHTML)
|
|
36
|
+
expect(container.querySelector('svg').innerHTML)
|
|
37
|
+
.toContain('<path d="M0 0 L10 10"')
|
|
37
38
|
})
|
|
38
39
|
|
|
39
40
|
it('falls back to svgContent if id not found in registry', () => {
|
|
40
41
|
getIconRegistry.mockReturnValue({})
|
|
41
42
|
const fallbackSVG = '<circle cx="5" cy="5" r="5"/>'
|
|
42
43
|
const { container } = render(<Icon id='unknown' svgContent={fallbackSVG} />)
|
|
43
|
-
expect(container.querySelector('svg').innerHTML)
|
|
44
|
+
expect(container.querySelector('svg').innerHTML)
|
|
45
|
+
.toContain('<circle cx="5" cy="5" r="5"')
|
|
44
46
|
})
|
|
45
47
|
|
|
46
48
|
it('renders svgContent directly if no id provided', () => {
|
|
47
49
|
const fallbackSVG = '<rect x="0" y="0" width="10" height="10"/>'
|
|
48
50
|
getIconRegistry.mockReturnValue({})
|
|
49
51
|
const { container } = render(<Icon svgContent={fallbackSVG} />)
|
|
50
|
-
expect(container.querySelector('svg').innerHTML)
|
|
52
|
+
expect(container.querySelector('svg').innerHTML)
|
|
53
|
+
.toContain('<rect x="0" y="0" width="10" height="10"')
|
|
51
54
|
})
|
|
52
55
|
|
|
53
56
|
it('uses registry icon if both id and svgContent provided', () => {
|
|
54
57
|
getIconRegistry.mockReturnValue({ check: '<path d="M1 1 L5 5"/>' })
|
|
55
58
|
const fallbackSVG = '<circle cx="5" cy="5" r="5"/>'
|
|
56
59
|
const { container } = render(<Icon id='check' svgContent={fallbackSVG} />)
|
|
57
|
-
expect(container.querySelector('svg').innerHTML)
|
|
60
|
+
expect(container.querySelector('svg').innerHTML)
|
|
61
|
+
.toContain('<path d="M1 1 L5 5"')
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
it('uses chevron icon when isMenu is true', () => {
|
|
65
|
+
getIconRegistry.mockReturnValue({
|
|
66
|
+
chevron: '<path d="M2 2 L8 8"/>',
|
|
67
|
+
close: '<path d="M0 0 L10 10"/>'
|
|
68
|
+
})
|
|
69
|
+
const { container } = render(<Icon id='close' isMenu />)
|
|
70
|
+
expect(container.querySelector('svg').innerHTML).toContain('<path d="M2 2 L8 8"')
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
it('ignores id and svgContent when isMenu is true', () => {
|
|
74
|
+
getIconRegistry.mockReturnValue({
|
|
75
|
+
chevron: '<path d="M2 2 L8 8"/>'
|
|
76
|
+
})
|
|
77
|
+
const fallbackSVG = '<circle cx="5" cy="5" r="5"/>'
|
|
78
|
+
const { container } = render(
|
|
79
|
+
<Icon id='close' svgContent={fallbackSVG} isMenu />
|
|
80
|
+
)
|
|
81
|
+
expect(container.querySelector('svg').innerHTML).toContain('<path d="M2 2 L8 8"')
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
it('adds narrow class when isMenu is true', () => {
|
|
85
|
+
getIconRegistry.mockReturnValue({
|
|
86
|
+
chevron: '<path d="M2 2 L8 8"/>'
|
|
87
|
+
})
|
|
88
|
+
const { container } = render(<Icon isMenu />)
|
|
89
|
+
const svg = container.querySelector('svg')
|
|
90
|
+
expect(svg).toHaveClass('im-c-icon--narrow')
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
it('renders nothing if isMenu is true and chevron is missing', () => {
|
|
94
|
+
getIconRegistry.mockReturnValue({})
|
|
95
|
+
const { container } = render(<Icon isMenu />)
|
|
96
|
+
expect(container.querySelector('svg').innerHTML).toBe('')
|
|
58
97
|
})
|
|
59
98
|
})
|
|
@@ -46,6 +46,37 @@ const handleKeyUp = (e) => {
|
|
|
46
46
|
}
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
+
const captureMenuRect = (buttonRefs, buttonId, setMenuRect) => {
|
|
50
|
+
const btn = buttonRefs.current[buttonId]
|
|
51
|
+
if (!btn) {
|
|
52
|
+
return
|
|
53
|
+
}
|
|
54
|
+
setMenuRect(btn.getBoundingClientRect().toJSON())
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Returns a keyup handler for buttons that control a popup menu.
|
|
59
|
+
* ArrowDown opens the menu at the first item; ArrowUp opens at the last.
|
|
60
|
+
* @param {boolean} hasMenu - Whether the button has a popup menu
|
|
61
|
+
* @param {Object} buttonRefs - React ref map of button elements
|
|
62
|
+
* @param {string} buttonId - Unique button identifier
|
|
63
|
+
* @param {Function} setMenuStartPos - State setter for menu start position
|
|
64
|
+
* @param {Function} setMenuRect - State setter for button bounding rect
|
|
65
|
+
* @param {Function} setIsPopupOpen - State setter for popup open state
|
|
66
|
+
* @returns {Function} Keyboard event handler
|
|
67
|
+
*/
|
|
68
|
+
const makePopupKeyUpHandler = (hasMenu, buttonRefs, buttonId, setMenuStartPos, setMenuRect, setIsPopupOpen) => (e) => {
|
|
69
|
+
if (hasMenu && ['ArrowDown', 'ArrowUp'].includes(e.key)) {
|
|
70
|
+
e.preventDefault()
|
|
71
|
+
setMenuStartPos(e.key === 'ArrowUp' ? 'last' : 'first')
|
|
72
|
+
captureMenuRect(buttonRefs, buttonId, setMenuRect)
|
|
73
|
+
setIsPopupOpen(true)
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const getButtonSlot = (panelId, buttonId) =>
|
|
78
|
+
panelId ? `${stringToKebab(buttonId)}-button` : undefined
|
|
79
|
+
|
|
49
80
|
/**
|
|
50
81
|
* Determines the controlled element (panel or popup menu) for ARIA attributes.
|
|
51
82
|
* @param {Object} options - Configuration options
|
|
@@ -177,10 +208,13 @@ export const MapButton = ({
|
|
|
177
208
|
const { buttonRefs } = useApp()
|
|
178
209
|
const [isPopupOpen, setIsPopupOpen] = useState(false)
|
|
179
210
|
const [menuStartPos, setMenuStartPos] = useState(null)
|
|
211
|
+
const [menuRect, setMenuRect] = useState(null)
|
|
180
212
|
const menuRef = useRef(null)
|
|
181
213
|
|
|
182
214
|
const Element = href ? 'a' : 'button'
|
|
183
215
|
const hasMenu = menuItems?.length >= 1
|
|
216
|
+
const showIcon = iconId || iconSvgContent || hasMenu
|
|
217
|
+
const buttonSlot = getButtonSlot(panelId, buttonId)
|
|
184
218
|
const controlledElement = getControlledElement({ idPrefix, panelId, buttonId, hasMenu })
|
|
185
219
|
|
|
186
220
|
/**
|
|
@@ -197,6 +231,9 @@ export const MapButton = ({
|
|
|
197
231
|
const isKeyboard = e.nativeEvent.pointerType === ''
|
|
198
232
|
/* istanbul ignore next as pointerType can't be tested in jest */
|
|
199
233
|
setMenuStartPos(isKeyboard ? 'first' : null)
|
|
234
|
+
if (!isPopupOpen) {
|
|
235
|
+
captureMenuRect(buttonRefs, buttonId, setMenuRect)
|
|
236
|
+
}
|
|
200
237
|
setIsPopupOpen((prev) => !prev)
|
|
201
238
|
}
|
|
202
239
|
if (onClick) {
|
|
@@ -204,19 +241,7 @@ export const MapButton = ({
|
|
|
204
241
|
}
|
|
205
242
|
}
|
|
206
243
|
|
|
207
|
-
|
|
208
|
-
* Handles key up events on buttons that control popup menus.
|
|
209
|
-
* ArrowDown opens the menu at the first item.
|
|
210
|
-
* ArrowUp opens the menu at the last item.
|
|
211
|
-
* @param {React.KeyboardEvent} e - The keyboard event
|
|
212
|
-
*/
|
|
213
|
-
const handleButtonKeyUp = e => {
|
|
214
|
-
if (hasMenu && ['ArrowDown', 'ArrowUp'].includes(e.key)) {
|
|
215
|
-
e.preventDefault()
|
|
216
|
-
setMenuStartPos(e.key === 'ArrowUp' ? 'last' : 'first')
|
|
217
|
-
setIsPopupOpen(true)
|
|
218
|
-
}
|
|
219
|
-
}
|
|
244
|
+
const handleButtonKeyUp = makePopupKeyUpHandler(hasMenu, buttonRefs, buttonId, setMenuStartPos, setMenuRect, setIsPopupOpen)
|
|
220
245
|
|
|
221
246
|
const buttonProps = buildButtonProps({
|
|
222
247
|
appId,
|
|
@@ -236,7 +261,7 @@ export const MapButton = ({
|
|
|
236
261
|
|
|
237
262
|
const buttonEl = (
|
|
238
263
|
<Element {...buttonProps}>
|
|
239
|
-
{
|
|
264
|
+
{showIcon && <Icon id={iconId} svgContent={iconSvgContent} isMenu={hasMenu} />}
|
|
240
265
|
{showLabel && <span>{label}</span>}
|
|
241
266
|
</Element>
|
|
242
267
|
)
|
|
@@ -244,12 +269,12 @@ export const MapButton = ({
|
|
|
244
269
|
return (
|
|
245
270
|
<div
|
|
246
271
|
className={buildWrapperClassNames(buttonId, showLabel)}
|
|
247
|
-
data-button-slot={
|
|
272
|
+
data-button-slot={buttonSlot}
|
|
248
273
|
style={isHidden ? { display: 'none' } : undefined}
|
|
249
274
|
>
|
|
250
275
|
{showLabel ? buttonEl : <Tooltip content={label}>{buttonEl}</Tooltip>}
|
|
251
|
-
{
|
|
252
|
-
{isPopupOpen && <PopupMenu popupMenuId={controlledElement.id} buttonId={buttonId} startPos={menuStartPos} menuRef={menuRef} items={menuItems} setIsOpen={setIsPopupOpen} />}
|
|
276
|
+
{buttonSlot && <SlotRenderer slot={buttonSlot} />}
|
|
277
|
+
{isPopupOpen && <PopupMenu popupMenuId={controlledElement.id} buttonId={buttonId} startPos={menuStartPos} menuRef={menuRef} items={menuItems} setIsOpen={setIsPopupOpen} buttonRect={menuRect} />}
|
|
253
278
|
</div>
|
|
254
279
|
)
|
|
255
280
|
}
|
|
@@ -124,19 +124,6 @@
|
|
|
124
124
|
}
|
|
125
125
|
}
|
|
126
126
|
|
|
127
|
-
.im-c-map-button--touch {
|
|
128
|
-
background-color: var(--map-overlay-foreground-color);
|
|
129
|
-
color: var(--map-overlay-halo-color);
|
|
130
|
-
|
|
131
|
-
width: var(--touch-button-size);
|
|
132
|
-
height: var(--touch-button-size);
|
|
133
|
-
border-radius: 100%;
|
|
134
|
-
|
|
135
|
-
&::before {
|
|
136
|
-
box-shadow: none;
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
|
|
140
127
|
.im-o-app__right {
|
|
141
128
|
.im-c-button-group {
|
|
142
129
|
display: flex;
|
|
@@ -185,6 +172,10 @@
|
|
|
185
172
|
}
|
|
186
173
|
|
|
187
174
|
// 4. State styles
|
|
175
|
+
.im-c-map-button[aria-haspopup="true"][aria-expanded="true"] svg {
|
|
176
|
+
transform: rotate(180deg);
|
|
177
|
+
}
|
|
178
|
+
|
|
188
179
|
.im-c-map-button[aria-disabled="true"]:not(.im-c-map-button--primary):not(.im-c-map-button--tertiary) {
|
|
189
180
|
svg, span {
|
|
190
181
|
opacity: var(--disabled-button-opacity);
|
|
@@ -23,7 +23,7 @@ jest.mock('../../renderer/SlotRenderer', () => ({
|
|
|
23
23
|
}))
|
|
24
24
|
|
|
25
25
|
jest.mock('../PopupMenu/PopupMenu', () => ({
|
|
26
|
-
PopupMenu: ({ startPos, items }) => {
|
|
26
|
+
PopupMenu: ({ startPos, items, buttonRect }) => {
|
|
27
27
|
let selectedIndex = -1
|
|
28
28
|
if (startPos === 'first' && items?.length > 0) {
|
|
29
29
|
selectedIndex = 0
|
|
@@ -31,7 +31,7 @@ jest.mock('../PopupMenu/PopupMenu', () => ({
|
|
|
31
31
|
if (startPos === 'last' && items?.length > 0) {
|
|
32
32
|
selectedIndex = items.length - 1
|
|
33
33
|
}
|
|
34
|
-
return <div data-testid='popup-menu' data-start-pos={String(startPos)} data-selected-index={String(selectedIndex)}>{items?.map((item, i) => <div key={i} data-testid={`menu-item-${i}`}>{item.label}</div>)}</div>
|
|
34
|
+
return <div data-testid='popup-menu' data-start-pos={String(startPos)} data-selected-index={String(selectedIndex)} data-has-rect={String(!!buttonRect)}>{items?.map((item, i) => <div key={i} data-testid={`menu-item-${i}`}>{item.label}</div>)}</div>
|
|
35
35
|
}
|
|
36
36
|
}))
|
|
37
37
|
|
|
@@ -41,7 +41,12 @@ const mockButtonRefs = { current: {} }
|
|
|
41
41
|
jest.mock('../../store/appContext', () => ({ useApp: () => ({ buttonRefs: mockButtonRefs }) }))
|
|
42
42
|
|
|
43
43
|
describe('MapButton', () => {
|
|
44
|
-
beforeEach(() => {
|
|
44
|
+
beforeEach(() => {
|
|
45
|
+
mockButtonRefs.current = {}
|
|
46
|
+
Element.prototype.getBoundingClientRect = jest.fn(() => ({
|
|
47
|
+
toJSON: () => ({ top: 0, left: 0, bottom: 0, right: 0, width: 0, height: 0 })
|
|
48
|
+
}))
|
|
49
|
+
})
|
|
45
50
|
|
|
46
51
|
const renderButton = (props = {}) => render(<MapButton buttonId='Test' iconId='icon' label='Label' {...props} />)
|
|
47
52
|
const getButton = () => screen.getByRole('button')
|
|
@@ -148,6 +153,19 @@ describe('MapButton', () => {
|
|
|
148
153
|
expect(menu).toHaveAttribute('data-selected-index', String(expectedIndex))
|
|
149
154
|
})
|
|
150
155
|
|
|
156
|
+
it('passes buttonRect to popup menu on open', () => {
|
|
157
|
+
renderButton({ menuItems: [{ label: 'Item' }] })
|
|
158
|
+
fireEvent.click(getButton())
|
|
159
|
+
expect(screen.getByTestId('popup-menu')).toHaveAttribute('data-has-rect', 'true')
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
it('captureMenuRect returns early when buttonRef is not stored', () => {
|
|
163
|
+
renderButton({ menuItems: [{ label: 'Item' }] })
|
|
164
|
+
mockButtonRefs.current = {}
|
|
165
|
+
fireEvent.click(getButton())
|
|
166
|
+
expect(screen.getByTestId('popup-menu')).toHaveAttribute('data-has-rect', 'false')
|
|
167
|
+
})
|
|
168
|
+
|
|
151
169
|
it('does nothing for arrow keys when no menu', () => {
|
|
152
170
|
renderButton()
|
|
153
171
|
fireEvent.keyUp(getButton(), { key: 'ArrowDown' })
|
|
@@ -188,4 +206,10 @@ describe('MapButton', () => {
|
|
|
188
206
|
fireEvent.keyUp(el, { key: 'Enter' })
|
|
189
207
|
expect(spy).not.toHaveBeenCalled()
|
|
190
208
|
})
|
|
209
|
+
|
|
210
|
+
it('renders no Icon when iconId, iconSvgContent and menuItems are all absent', () => {
|
|
211
|
+
render(<MapButton buttonId='Test' label='Label' />)
|
|
212
|
+
expect(screen.queryByRole('img', { hidden: true })).toBeNull()
|
|
213
|
+
expect(screen.queryByTestId('icon')).toBeNull()
|
|
214
|
+
})
|
|
191
215
|
})
|