@defra/interactive-map 0.0.4-alpha → 0.0.5-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/dist/css/index.css +1 -1
- package/dist/esm/im-core.js +1 -1
- package/dist/esm/index.js +1 -1
- package/dist/umd/im-core.js +1 -1
- package/dist/umd/index.js +1 -1
- package/docs/api.md +8 -0
- package/package.json +3 -2
- package/plugins/beta/datasets/dist/esm/im-datasets-plugin.js +2 -1
- package/plugins/beta/datasets/dist/esm/im-datasets-plugin.js.LICENSE.txt +1 -0
- package/plugins/beta/datasets/dist/umd/im-datasets-plugin.js +2 -1
- package/plugins/beta/datasets/dist/umd/im-datasets-plugin.js.LICENSE.txt +1 -0
- package/plugins/beta/datasets/dist/umd/index.js +1 -1
- package/plugins/beta/datasets/src/datasets.js +55 -2
- package/plugins/beta/datasets/src/fetch/createDynamicSource.js +206 -0
- package/plugins/beta/datasets/src/fetch/fetchGeoJSON.js +38 -0
- package/plugins/beta/datasets/src/handleSetMapStyle.js +8 -1
- package/plugins/beta/datasets/src/mapLayers.js +41 -3
- package/plugins/beta/datasets/src/utils/bbox.js +113 -2
- 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/dist/umd/index.js +1 -1
- package/plugins/beta/draw-ml/src/api/addFeature.js +15 -3
- package/plugins/beta/draw-ml/src/api/editFeature.js +16 -5
- package/plugins/beta/draw-ml/src/api/newLine.js +32 -7
- package/plugins/beta/draw-ml/src/api/newPolygon.js +33 -8
- package/plugins/beta/draw-ml/src/defaults.js +1 -0
- package/plugins/beta/draw-ml/src/draw.scss +23 -0
- package/plugins/beta/draw-ml/src/events.js +55 -113
- package/plugins/beta/draw-ml/src/manifest.js +64 -38
- package/plugins/beta/draw-ml/src/mapboxSnap.js +16 -12
- package/plugins/beta/draw-ml/src/modes/createDrawMode.js +4 -3
- package/plugins/beta/draw-ml/src/modes/editVertex/geometryHelpers.js +135 -0
- package/plugins/beta/draw-ml/src/modes/editVertex/helpers.js +2 -0
- package/plugins/beta/draw-ml/src/modes/editVertex/touchHandlers.js +134 -0
- package/plugins/beta/draw-ml/src/modes/editVertex/undoHandlers.js +133 -0
- package/plugins/beta/draw-ml/src/modes/editVertex/vertexOperations.js +141 -0
- package/plugins/beta/draw-ml/src/modes/editVertex/vertexQueries.js +121 -0
- package/plugins/beta/draw-ml/src/modes/editVertexMode.js +51 -406
- package/plugins/beta/draw-ml/src/utils/flattenStyleProperties.js +49 -0
- 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 +1 -1
- package/plugins/beta/map-styles/dist/css/index.css +1 -1
- package/plugins/beta/map-styles/dist/esm/index.js +1 -1
- package/plugins/beta/map-styles/dist/umd/im-map-styles-plugin.js +1 -1
- package/plugins/beta/map-styles/dist/umd/index.js +1 -1
- package/plugins/beta/map-styles/src/mapStyles.scss +4 -1
- 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/dist/umd/index.js +1 -1
- package/plugins/interact/src/InteractInit.jsx +1 -1
- package/plugins/interact/src/defaults.js +1 -0
- package/plugins/interact/src/events.js +37 -52
- package/plugins/interact/src/events.test.js +35 -0
- package/plugins/interact/src/hooks/useHighlightSync.js +1 -1
- package/plugins/interact/src/hooks/useInteractionHandlers.js +80 -48
- package/plugins/interact/src/hooks/useInteractionHandlers.test.js +64 -8
- package/plugins/interact/src/interact.scss +6 -0
- package/plugins/interact/src/reducer.js +25 -27
- package/plugins/interact/src/reducer.test.js +19 -6
- package/plugins/interact/src/utils/featureQueries.js +2 -2
- package/plugins/interact/src/utils/spatial.js +95 -0
- package/plugins/interact/src/utils/spatial.test.js +93 -0
- package/plugins/search/dist/css/index.css +1 -1
- package/plugins/search/dist/esm/im-search-plugin.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 +2 -2
- package/plugins/search/src/components/Form/Form.jsx +1 -1
- package/plugins/search/src/components/Suggestions/Suggestions.jsx +5 -5
- package/plugins/search/src/datasets.js +1 -1
- package/plugins/search/src/defaults.js +3 -0
- package/plugins/search/src/events/fetchSuggestions.js +48 -53
- package/plugins/search/src/events/formHandlers.js +3 -2
- package/plugins/search/src/events/index.js +1 -1
- package/plugins/search/src/events/inputHandlers.js +3 -1
- package/plugins/search/src/events/suggestionHandlers.js +12 -3
- package/plugins/search/src/search.scss +4 -1
- package/plugins/search/src/utils/parseOsNamesResults.js +12 -12
- package/providers/beta/esri/src/esriProvider.js +1 -1
- package/providers/beta/esri/src/index.js +1 -1
- package/providers/maplibre/dist/esm/im-maplibre-provider.js +1 -1
- package/providers/maplibre/dist/esm/index.js +1 -1
- package/providers/maplibre/dist/umd/im-maplibre-framework.js +1 -1
- package/providers/maplibre/dist/umd/im-maplibre-provider.js +1 -1
- package/providers/maplibre/dist/umd/index.js +1 -1
- package/providers/maplibre/src/defaults.js +3 -2
- package/providers/maplibre/src/index.js +22 -19
- package/providers/maplibre/src/maplibreProvider.js +13 -11
- package/providers/maplibre/src/utils/detectWebgl.js +1 -2
- package/providers/maplibre/src/utils/highlightFeatures.js +78 -67
- package/providers/maplibre/src/utils/labels.js +174 -120
- package/providers/maplibre/src/utils/maplibreFixes.js +2 -2
- package/providers/maplibre/src/utils/queryFeatures.js +146 -0
- package/providers/maplibre/src/utils/spatial.js +14 -17
- package/sonar-project.properties +39 -2
- package/src/App/components/Actions/Actions.jsx +2 -2
- package/src/App/components/Actions/Actions.module.scss +2 -2
- package/src/App/components/KeyboardHelp/KeyboardHelp.jsx +3 -1
- package/src/App/components/KeyboardHelp/KeyboardHelp.test.jsx +9 -0
- package/src/App/components/Logo/Logo.jsx +1 -1
- package/src/App/components/MapButton/MapButton.jsx +217 -41
- package/src/App/components/MapButton/MapButton.module.scss +32 -21
- package/src/App/components/MapButton/MapButton.test.jsx +128 -84
- package/src/App/components/Markers/Markers.jsx +1 -1
- package/src/App/components/Markers/Markers.module.scss +0 -5
- package/src/App/components/Panel/Panel.jsx +75 -45
- package/src/App/components/Panel/Panel.module.scss +18 -24
- package/src/App/components/Panel/Panel.test.jsx +18 -0
- package/src/App/components/PopupMenu/PopupMenu.jsx +246 -0
- package/src/App/components/PopupMenu/PopupMenu.module.scss +70 -0
- package/src/App/components/PopupMenu/PopupMenu.test.jsx +304 -0
- package/src/App/components/Tooltip/Tooltip.jsx +4 -2
- package/src/App/components/Tooltip/getTooltipPosition.js +1 -1
- package/src/App/components/Viewport/MapController.jsx +1 -1
- package/src/App/components/Viewport/MapStatus.jsx +1 -1
- package/src/App/components/Viewport/Viewport.jsx +2 -2
- package/src/App/components/Viewport/Viewport.module.scss +2 -2
- package/src/App/controls/keyboardActions.js +3 -2
- package/src/App/controls/keyboardShortcuts.js +4 -2
- package/src/App/hooks/useButtonStateEvaluator.js +58 -45
- package/src/App/hooks/useButtonStateEvaluator.test.js +36 -3
- package/src/App/hooks/useCrossHairAPI.js +49 -47
- package/src/App/hooks/useFocusVisible.js +2 -2
- package/src/App/hooks/useInterfaceAPI.js +16 -4
- package/src/App/hooks/useKeyboardHint.js +1 -1
- package/src/App/hooks/useKeyboardShortcuts.js +4 -2
- package/src/App/hooks/useMapAnnouncements.js +34 -32
- package/src/App/hooks/useMapEvents.js +12 -1
- package/src/App/hooks/useMapProviderOverrides.js +3 -3
- package/src/App/hooks/useMapStateSync.js +7 -1
- package/src/App/hooks/useMapURLSync.js +6 -1
- package/src/App/hooks/useMarkersAPI.js +88 -50
- package/src/App/hooks/useMediaQueryDispatch.test.js +48 -0
- package/src/App/hooks/useModalPanelBehaviour.js +46 -40
- package/src/App/hooks/useResizeObserver.js +2 -2
- package/src/App/initialiseApp.js +45 -38
- package/src/App/layout/Layout.jsx +6 -4
- package/src/App/layout/layout.module.scss +13 -17
- package/src/App/registry/keyboardShortcutRegistry.js +12 -2
- package/src/App/registry/keyboardShortcutRegistry.test.js +28 -0
- package/src/App/registry/mergeManifests.js +1 -1
- package/src/App/registry/panelRegistry.js +1 -1
- package/src/App/registry/panelRegistry.test.js +30 -1
- package/src/App/registry/pluginRegistry.js +14 -51
- package/src/App/registry/pluginRegistry.test.js +54 -6
- package/src/App/renderer/HtmlElementHost.jsx +186 -0
- package/src/App/renderer/HtmlElementHost.test.jsx +231 -0
- package/src/App/renderer/mapButtons.js +11 -8
- package/src/App/renderer/mapButtons.test.js +6 -4
- package/src/App/renderer/mapControls.js +6 -0
- package/src/App/renderer/mapControls.test.js +10 -2
- package/src/App/renderer/mapPanels.js +39 -26
- package/src/App/renderer/mapPanels.test.js +29 -0
- package/src/App/renderer/pluginWrapper.test.js +28 -0
- package/src/App/renderer/slotHelpers.js +60 -0
- package/src/App/renderer/slotHelpers.test.js +82 -0
- package/src/App/store/AppProvider.jsx +3 -0
- package/src/App/store/AppProvider.test.jsx +40 -0
- package/src/App/store/PluginProvider.jsx +7 -5
- package/src/App/store/appActionsMap.js +25 -1
- package/src/App/store/appActionsMap.test.js +21 -0
- package/src/App/store/appReducer.js +1 -0
- package/src/App/store/mapActionsMap.js +12 -24
- package/src/App/store/mapReducer.js +1 -1
- package/src/InteractiveMap/InteractiveMap.js +79 -6
- package/src/InteractiveMap/InteractiveMap.test.js +164 -2
- package/src/InteractiveMap/behaviourController.js +12 -2
- package/src/InteractiveMap/behaviourController.test.js +82 -1
- package/src/InteractiveMap/deviceChecker.js +1 -1
- package/src/InteractiveMap/deviceChecker.test.js +2 -2
- package/src/InteractiveMap/historyManager.js +41 -5
- package/src/InteractiveMap/historyManager.test.js +37 -6
- package/src/InteractiveMap/polyfills.js +39 -0
- package/src/InteractiveMap/polyfills.test.js +127 -0
- package/src/config/appConfig.js +5 -5
- package/src/config/appConfig.test.js +107 -42
- package/src/config/defaults.js +1 -0
- package/src/index.umd.js +1 -1
- package/src/scss/main.scss +1 -0
- package/src/scss/settings/_dimensions.scss +6 -2
- package/src/services/eventBus.test.js +24 -1
- package/src/test-utils.js +1 -0
- package/src/types.js +5 -0
- package/src/utils/detectBreakpoint.js +78 -77
- package/src/utils/detectBreakpoint.test.js +50 -4
- package/src/utils/detectInterfaceType.js +4 -4
- package/src/utils/getIsFullscreen.js +3 -1
- package/src/utils/getSafeZoneInset.js +23 -7
- package/src/utils/getSafeZoneInset.test.js +18 -0
- package/src/utils/mapStateSync.js +3 -3
- package/src/utils/queryString.js +1 -1
- package/src/utils/stringToKebab.js +1 -1
- package/src/utils/toggleInertElements.js +37 -12
- package/webpack.dev.mjs +3 -4
- package/plugins/interact/src/utils/turfHelpers.js +0 -30
- package/plugins/interact/src/utils/turfHelpers.test.js +0 -30
- package/plugins/search/src/utils/fetchDataset.js +0 -23
- package/providers/maplibre/dist/esm/im-maplibre-legacy-framework.js +0 -1
- package/providers/maplibre/dist/umd/im-maplibre-legacy-framework.js +0 -1
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
// src/components/KeyboardHelp.jsx
|
|
2
2
|
import React from 'react'
|
|
3
|
+
import { useConfig } from '../../store/configContext'
|
|
3
4
|
import { getKeyboardShortcuts } from '../../registry/keyboardShortcutRegistry.js'
|
|
4
5
|
|
|
5
6
|
// eslint-disable-next-line camelcase, react/jsx-pascal-case
|
|
6
7
|
// sonarjs/disable-next-line function-name
|
|
7
8
|
export const KeyboardHelp = () => {
|
|
8
|
-
const
|
|
9
|
+
const appConfig = useConfig()
|
|
10
|
+
const groups = getKeyboardShortcuts(appConfig).reduce((acc, shortcut) => {
|
|
9
11
|
acc[shortcut.group] = acc[shortcut.group] || []
|
|
10
12
|
acc[shortcut.group].push(shortcut)
|
|
11
13
|
return acc
|
|
@@ -3,12 +3,21 @@ import React from 'react'
|
|
|
3
3
|
import { render, screen, within } from '@testing-library/react'
|
|
4
4
|
import { KeyboardHelp } from './KeyboardHelp'
|
|
5
5
|
import { getKeyboardShortcuts } from '../../registry/keyboardShortcutRegistry.js'
|
|
6
|
+
import { useConfig } from '../../store/configContext'
|
|
6
7
|
|
|
7
8
|
jest.mock('../../registry/keyboardShortcutRegistry.js', () => ({
|
|
8
9
|
getKeyboardShortcuts: jest.fn()
|
|
9
10
|
}))
|
|
10
11
|
|
|
12
|
+
jest.mock('../../store/configContext', () => ({
|
|
13
|
+
useConfig: jest.fn()
|
|
14
|
+
}))
|
|
15
|
+
|
|
11
16
|
describe('KeyboardHelp', () => {
|
|
17
|
+
beforeEach(() => {
|
|
18
|
+
useConfig.mockReturnValue({})
|
|
19
|
+
})
|
|
20
|
+
|
|
12
21
|
afterEach(() => {
|
|
13
22
|
jest.clearAllMocks()
|
|
14
23
|
})
|
|
@@ -1,13 +1,169 @@
|
|
|
1
1
|
// components/MapButton.jsx
|
|
2
|
+
import React, { useState, useRef } from 'react'
|
|
2
3
|
import { stringToKebab } from '../../../utils/stringToKebab'
|
|
3
4
|
import { Tooltip } from '../Tooltip/Tooltip'
|
|
4
5
|
import { Icon } from '../Icon/Icon'
|
|
5
6
|
import { SlotRenderer } from '../../renderer/SlotRenderer'
|
|
7
|
+
import { PopupMenu } from '../PopupMenu/PopupMenu'
|
|
6
8
|
import { useConfig } from '../../store/configContext'
|
|
7
9
|
import { useApp } from '../../store/appContext'
|
|
8
10
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
+
/**
|
|
12
|
+
* Builds CSS class names for the map button element.
|
|
13
|
+
* @param {string} buttonId - Unique identifier for the button (kebab-cased)
|
|
14
|
+
* @param {string} variant - Visual variant of the button (e.g., 'primary', 'secondary')
|
|
15
|
+
* @param {boolean} showLabel - Whether the button label is displayed
|
|
16
|
+
* @returns {string} Space-separated CSS class names
|
|
17
|
+
*/
|
|
18
|
+
const buildButtonClassNames = (buttonId, variant, showLabel) => [
|
|
19
|
+
'im-c-map-button',
|
|
20
|
+
buttonId && `im-c-map-button--${stringToKebab(buttonId)}`,
|
|
21
|
+
variant && `im-c-map-button--${variant}`,
|
|
22
|
+
showLabel && 'im-c-map-button--with-label'
|
|
23
|
+
].filter(Boolean).join(' ')
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Builds CSS class names for the wrapper div that contains the button.
|
|
27
|
+
* @param {string} buttonId - Unique identifier for the button
|
|
28
|
+
* @param {boolean} showLabel - Whether the button label is displayed
|
|
29
|
+
* @param {boolean} groupStart - Whether this button is at the start of a button group
|
|
30
|
+
* @param {boolean} groupMiddle - Whether this button is in the middle of a button group
|
|
31
|
+
* @param {boolean} groupEnd - Whether this button is at the end of a button group
|
|
32
|
+
* @returns {string} Space-separated CSS class names for the wrapper
|
|
33
|
+
*/
|
|
34
|
+
const buildWrapperClassNames = (buttonId, showLabel, groupStart, groupMiddle, groupEnd) => [
|
|
35
|
+
'im-c-button-wrapper',
|
|
36
|
+
buttonId && `im-c-button-wrapper--${stringToKebab(buttonId)}`,
|
|
37
|
+
showLabel && 'im-c-button-wrapper--wide',
|
|
38
|
+
groupStart && 'im-c-button-wrapper--group-start',
|
|
39
|
+
groupMiddle && 'im-c-button-wrapper--group-middle',
|
|
40
|
+
groupEnd && 'im-c-button-wrapper--group-end'
|
|
41
|
+
].filter(Boolean).join(' ')
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Handles spacebar key presses on anchor links by triggering a click event.
|
|
45
|
+
* This ensures anchor buttons behave like standard buttons when spacebar is pressed.
|
|
46
|
+
* @param {KeyboardEvent} e - The keyboard event
|
|
47
|
+
*/
|
|
48
|
+
const handleKeyUp = (e) => {
|
|
49
|
+
if (e.key === ' ' || e.key === 'Spacebar') {
|
|
50
|
+
e.preventDefault()
|
|
51
|
+
e.currentTarget.click()
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Determines the controlled element (panel or popup menu) for ARIA attributes.
|
|
57
|
+
* @param {Object} options - Configuration options
|
|
58
|
+
* @param {string} options.idPrefix - Prefix for generated IDs
|
|
59
|
+
* @param {string} options.panelId - ID of the controlled panel (if applicable)
|
|
60
|
+
* @param {string} options.buttonId - Unique button identifier
|
|
61
|
+
* @param {boolean} options.hasMenu - Whether the button has a popup menu
|
|
62
|
+
* @returns {Object|null} Object with id and type ('panel' or 'popup'), or null if no controlled element
|
|
63
|
+
*/
|
|
64
|
+
const getControlledElement = ({ idPrefix, panelId, buttonId, hasMenu }) => {
|
|
65
|
+
if (panelId) {
|
|
66
|
+
return { id: `${idPrefix}-panel-${stringToKebab(panelId)}`, type: 'panel' }
|
|
67
|
+
}
|
|
68
|
+
if (hasMenu) {
|
|
69
|
+
return { id: `${idPrefix}-popup-${stringToKebab(buttonId)}`, type: 'popup' }
|
|
70
|
+
}
|
|
71
|
+
return null
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Builds the complete props object for a button or anchor element.
|
|
76
|
+
* Handles ARIA attributes, event handlers, refs, and conditional styling.
|
|
77
|
+
* @param {Object} options - Configuration options
|
|
78
|
+
* @param {string} options.appId - Application identifier
|
|
79
|
+
* @param {string} options.buttonId - Unique button identifier
|
|
80
|
+
* @param {string} options.className - CSS classes for the button
|
|
81
|
+
* @param {Function} options.onClick - Click event handler
|
|
82
|
+
* @param {Function} options.onKeyUp - Key up event handler
|
|
83
|
+
* @param {Object} options.buttonRefs - React ref object for storing button references
|
|
84
|
+
* @param {boolean} options.isDisabled - Whether the button is disabled
|
|
85
|
+
* @param {boolean} options.isPressed - Whether the button is in pressed state
|
|
86
|
+
* @param {boolean} options.isExpanded - Whether content controlled by button is expanded
|
|
87
|
+
* @param {boolean} options.isPanelOpen - Whether the controlled panel is open
|
|
88
|
+
* @param {boolean} options.isPopupOpen - Whether the popup menu is open
|
|
89
|
+
* @param {Object|null} options.controlledElement - The element controlled by this button
|
|
90
|
+
* @param {string} options.href - URL for anchor element (if provided, renders as <a> instead of <button>)
|
|
91
|
+
* @returns {Object} Props object suitable for button or anchor element
|
|
92
|
+
*/
|
|
93
|
+
const buildButtonProps = ({
|
|
94
|
+
appId,
|
|
95
|
+
buttonId,
|
|
96
|
+
className,
|
|
97
|
+
onClick,
|
|
98
|
+
onKeyUp,
|
|
99
|
+
buttonRefs,
|
|
100
|
+
isDisabled,
|
|
101
|
+
isPressed,
|
|
102
|
+
isExpanded,
|
|
103
|
+
isPanelOpen,
|
|
104
|
+
isPopupOpen,
|
|
105
|
+
controlledElement,
|
|
106
|
+
href
|
|
107
|
+
}) => {
|
|
108
|
+
let ariaExpanded
|
|
109
|
+
if (controlledElement?.type === 'panel') {
|
|
110
|
+
ariaExpanded = String(isPanelOpen)
|
|
111
|
+
} else if (controlledElement?.type === 'popup') {
|
|
112
|
+
ariaExpanded = isPopupOpen
|
|
113
|
+
} else if (typeof isExpanded === 'boolean') {
|
|
114
|
+
ariaExpanded = isExpanded
|
|
115
|
+
} else {
|
|
116
|
+
// No action
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return {
|
|
120
|
+
id: `${appId}-${stringToKebab(buttonId)}`,
|
|
121
|
+
className,
|
|
122
|
+
onClick,
|
|
123
|
+
onKeyUp,
|
|
124
|
+
ref: (el) => {
|
|
125
|
+
if (buttonRefs.current && buttonId) {
|
|
126
|
+
buttonRefs.current[buttonId] = el
|
|
127
|
+
}
|
|
128
|
+
},
|
|
129
|
+
'aria-disabled': isDisabled || undefined,
|
|
130
|
+
'aria-expanded': ariaExpanded,
|
|
131
|
+
'aria-pressed': typeof isPressed === 'boolean' ? isPressed : undefined,
|
|
132
|
+
'aria-controls': controlledElement?.id,
|
|
133
|
+
'aria-haspopup': controlledElement?.type === 'popup' || undefined,
|
|
134
|
+
...(href
|
|
135
|
+
? { href, target: '_blank', onKeyUp: handleKeyUp, role: 'button' }
|
|
136
|
+
: { type: 'button' })
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* MapButton component - A versatile button for map applications.
|
|
142
|
+
* Supports icons, labels, popups, panels, tooltips, and button grouping.
|
|
143
|
+
* Renders as either a <button> or <a> element depending on props.
|
|
144
|
+
* @component
|
|
145
|
+
* @param {Object} props - Component props
|
|
146
|
+
* @param {string} props.buttonId - Unique identifier for the button
|
|
147
|
+
* @param {string} [props.iconId] - Icon identifier to display
|
|
148
|
+
* @param {string} [props.iconSvgContent] - SVG content as an alternative to iconId
|
|
149
|
+
* @param {string} props.label - Button label/tooltip text
|
|
150
|
+
* @param {boolean} [props.showLabel=false] - Whether to display the label visually
|
|
151
|
+
* @param {boolean} [props.isDisabled=false] - Whether the button is disabled
|
|
152
|
+
* @param {boolean} [props.isPressed] - Whether the button is in pressed state (aria-pressed)
|
|
153
|
+
* @param {boolean} [props.isExpanded] - Whether content controlled by the button is expanded
|
|
154
|
+
* @param {boolean} [props.isHidden=false] - Whether to hide the button (CSS display: none)
|
|
155
|
+
* @param {boolean} [props.isPanelOpen=false] - Whether the controlled panel is open
|
|
156
|
+
* @param {string} [props.variant] - CSS variant class for styling (e.g., 'primary')
|
|
157
|
+
* @param {Function} [props.onClick] - Custom click handler
|
|
158
|
+
* @param {string} [props.panelId] - ID of the panel controlled by this button
|
|
159
|
+
* @param {Array<Object>} [props.menuItems] - Array of items for popup menu
|
|
160
|
+
* @param {string} [props.idPrefix=''] - Prefix for generated panel/popup IDs
|
|
161
|
+
* @param {string} [props.href] - URL for anchor element; if provided, renders as <a> instead of <button>
|
|
162
|
+
* @param {boolean} [props.groupStart=false] - Whether button is at start of button group
|
|
163
|
+
* @param {boolean} [props.groupMiddle=false] - Whether button is in middle of button group
|
|
164
|
+
* @param {boolean} [props.groupEnd=false] - Whether button is at end of button group
|
|
165
|
+
* @returns {JSX.Element} The rendered button component
|
|
166
|
+
*/
|
|
11
167
|
export const MapButton = ({
|
|
12
168
|
buttonId,
|
|
13
169
|
iconId,
|
|
@@ -15,13 +171,14 @@ export const MapButton = ({
|
|
|
15
171
|
label,
|
|
16
172
|
showLabel,
|
|
17
173
|
isDisabled,
|
|
18
|
-
isExpanded,
|
|
19
174
|
isPressed,
|
|
175
|
+
isExpanded,
|
|
20
176
|
isHidden,
|
|
21
|
-
|
|
177
|
+
isPanelOpen,
|
|
22
178
|
variant,
|
|
23
179
|
onClick,
|
|
24
180
|
panelId,
|
|
181
|
+
menuItems,
|
|
25
182
|
idPrefix,
|
|
26
183
|
href,
|
|
27
184
|
groupMiddle,
|
|
@@ -30,42 +187,65 @@ export const MapButton = ({
|
|
|
30
187
|
}) => {
|
|
31
188
|
const { id: appId } = useConfig()
|
|
32
189
|
const { buttonRefs } = useApp()
|
|
33
|
-
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
buttonId && `im-c-map-button--${stringToKebab(buttonId)}`,
|
|
37
|
-
variant && `im-c-map-button--${variant}`,
|
|
38
|
-
showLabel && 'im-c-map-button--with-label'
|
|
39
|
-
].filter(Boolean).join(' ')
|
|
190
|
+
const [isPopupOpen, setIsPopupOpen] = useState(false)
|
|
191
|
+
const [menuStartPos, setMenuStartPos] = useState(null)
|
|
192
|
+
const menuRef = useRef(null)
|
|
40
193
|
|
|
41
194
|
const Element = href ? 'a' : 'button'
|
|
195
|
+
const hasMenu = menuItems?.length >= 1
|
|
196
|
+
const controlledElement = getControlledElement({ idPrefix, panelId, buttonId, hasMenu })
|
|
42
197
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
198
|
+
/**
|
|
199
|
+
* Handles button click events.
|
|
200
|
+
* Toggles popup menu visibility if the button controls a popup.
|
|
201
|
+
* Calls the custom onClick handler if provided.
|
|
202
|
+
* @param {React.MouseEvent} e - The click event
|
|
203
|
+
*/
|
|
204
|
+
const handleButtonClick = (e) => {
|
|
205
|
+
if (isDisabled) {
|
|
206
|
+
return
|
|
207
|
+
}
|
|
208
|
+
if (controlledElement?.type === 'popup') {
|
|
209
|
+
const isKeyboard = e.nativeEvent.pointerType === ''
|
|
210
|
+
/* istanbul ignore next as pointerType can't be tested in jest */
|
|
211
|
+
setMenuStartPos(isKeyboard ? 'first' : null)
|
|
212
|
+
setIsPopupOpen((prev) => !prev)
|
|
213
|
+
}
|
|
214
|
+
if (onClick) {
|
|
215
|
+
onClick(e)
|
|
47
216
|
}
|
|
48
217
|
}
|
|
49
218
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
'aria-controls': panelId ? `${idPrefix}-panel-${stringToKebab(panelId)}` : undefined,
|
|
63
|
-
...(href
|
|
64
|
-
? { href, target: '_blank', onKeyUp: handleKeyUp, role: 'button' } // only <a>
|
|
65
|
-
: { type: 'button' } // only <button>
|
|
66
|
-
)
|
|
219
|
+
/**
|
|
220
|
+
* Handles key up events on buttons that control popup menus.
|
|
221
|
+
* ArrowDown opens the menu at the first item.
|
|
222
|
+
* ArrowUp opens the menu at the last item.
|
|
223
|
+
* @param {React.KeyboardEvent} e - The keyboard event
|
|
224
|
+
*/
|
|
225
|
+
const handleButtonKeyUp = e => {
|
|
226
|
+
if (hasMenu && ['ArrowDown', 'ArrowUp'].includes(e.key)) {
|
|
227
|
+
e.preventDefault()
|
|
228
|
+
setMenuStartPos(e.key === 'ArrowUp' ? 'last' : 'first')
|
|
229
|
+
setIsPopupOpen(true)
|
|
230
|
+
}
|
|
67
231
|
}
|
|
68
232
|
|
|
233
|
+
const buttonProps = buildButtonProps({
|
|
234
|
+
appId,
|
|
235
|
+
buttonId,
|
|
236
|
+
className: buildButtonClassNames(buttonId, variant, showLabel),
|
|
237
|
+
onClick: handleButtonClick,
|
|
238
|
+
onKeyUp: handleButtonKeyUp,
|
|
239
|
+
buttonRefs,
|
|
240
|
+
isDisabled,
|
|
241
|
+
isPressed,
|
|
242
|
+
isExpanded,
|
|
243
|
+
isPanelOpen,
|
|
244
|
+
isPopupOpen,
|
|
245
|
+
controlledElement,
|
|
246
|
+
href
|
|
247
|
+
})
|
|
248
|
+
|
|
69
249
|
const buttonEl = (
|
|
70
250
|
<Element {...buttonProps}>
|
|
71
251
|
{(iconId || iconSvgContent) && <Icon id={iconId} svgContent={iconSvgContent} />}
|
|
@@ -73,18 +253,14 @@ export const MapButton = ({
|
|
|
73
253
|
</Element>
|
|
74
254
|
)
|
|
75
255
|
|
|
76
|
-
const wrapperClassNames = [
|
|
77
|
-
'im-c-button-wrapper',
|
|
78
|
-
showLabel && ' im-c-button-wrapper--wide',
|
|
79
|
-
groupStart && 'im-c-button-wrapper--group-start',
|
|
80
|
-
groupMiddle && 'im-c-button-wrapper--group-middle',
|
|
81
|
-
groupEnd && 'im-c-button-wrapper--group-end'
|
|
82
|
-
].filter(Boolean).join(' ')
|
|
83
|
-
|
|
84
256
|
return (
|
|
85
|
-
<div
|
|
257
|
+
<div
|
|
258
|
+
className={buildWrapperClassNames(buttonId, showLabel, groupStart, groupMiddle, groupEnd)}
|
|
259
|
+
style={isHidden ? { display: 'none' } : undefined}
|
|
260
|
+
>
|
|
86
261
|
{showLabel ? buttonEl : <Tooltip content={label}>{buttonEl}</Tooltip>}
|
|
87
262
|
{panelId && <SlotRenderer slot={`${stringToKebab(buttonId)}-button`} />}
|
|
263
|
+
{isPopupOpen && <PopupMenu popupMenuId={controlledElement.id} buttonId={buttonId} startPos={menuStartPos} menuRef={menuRef} items={menuItems} setIsOpen={setIsPopupOpen} />}
|
|
88
264
|
</div>
|
|
89
265
|
)
|
|
90
266
|
}
|
|
@@ -26,23 +26,25 @@
|
|
|
26
26
|
$type: button
|
|
27
27
|
);
|
|
28
28
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
align-items: center;
|
|
29
|
+
display: flex;
|
|
30
|
+
justify-content: center;
|
|
31
|
+
align-items: center;
|
|
33
32
|
|
|
34
|
-
|
|
35
|
-
|
|
33
|
+
padding-left: var(--button-padding);
|
|
34
|
+
padding-right: var(--button-padding);
|
|
36
35
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
36
|
+
pointer-events: auto;
|
|
37
|
+
color: var(--foreground-color);
|
|
38
|
+
background-color: var(--button-background-color);
|
|
40
39
|
|
|
41
|
-
|
|
42
|
-
|
|
40
|
+
min-height: var(--button-size);
|
|
41
|
+
min-width: var(--button-size);
|
|
43
42
|
|
|
44
|
-
|
|
45
|
-
|
|
43
|
+
user-select: none;
|
|
44
|
+
cursor: pointer;
|
|
45
|
+
|
|
46
|
+
&--primary[aria-disabled="true"], &--tertiary[aria-disabled="true"] {
|
|
47
|
+
opacity: var(--disabled-button-opacity);
|
|
46
48
|
}
|
|
47
49
|
}
|
|
48
50
|
|
|
@@ -66,9 +68,7 @@
|
|
|
66
68
|
transition: background-color var(--duration) ease;
|
|
67
69
|
}
|
|
68
70
|
|
|
69
|
-
|
|
70
|
-
background-color: var(--button-hover-color);
|
|
71
|
-
}
|
|
71
|
+
background-color: var(--button-hover-color);
|
|
72
72
|
}
|
|
73
73
|
}
|
|
74
74
|
|
|
@@ -134,6 +134,14 @@
|
|
|
134
134
|
@include tools.border-focus-corner-override($corners: 'top');
|
|
135
135
|
}
|
|
136
136
|
|
|
137
|
+
.im-c-button-wrapper--group-middle .im-c-map-button {
|
|
138
|
+
@include tools.border-focus-corner-override($corners: 'none');
|
|
139
|
+
|
|
140
|
+
&::before {
|
|
141
|
+
clip-path: inset(0 -20px 0 -20px);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
137
145
|
.im-c-button-wrapper--group-end .im-c-map-button {
|
|
138
146
|
margin-top: 0;
|
|
139
147
|
@include tools.border-focus-corner-override($corners: 'bottom');
|
|
@@ -150,6 +158,14 @@
|
|
|
150
158
|
@include tools.border-focus-corner-override($corners: 'left');
|
|
151
159
|
}
|
|
152
160
|
|
|
161
|
+
.im-c-button-wrapper--group-middle .im-c-map-button {
|
|
162
|
+
@include tools.border-focus-corner-override($corners: 'none');
|
|
163
|
+
|
|
164
|
+
&::before {
|
|
165
|
+
clip-path: inset(-20px 0 -20px 0);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
153
169
|
.im-c-button-wrapper--group-end .im-c-map-button {
|
|
154
170
|
@include tools.border-focus-corner-override($corners: 'right');
|
|
155
171
|
}
|
|
@@ -162,11 +178,6 @@
|
|
|
162
178
|
}
|
|
163
179
|
}
|
|
164
180
|
|
|
165
|
-
.im-c-map-button {
|
|
166
|
-
&--primary[aria-disabled="true"], &--tertiary[aria-disabled="true"] {
|
|
167
|
-
opacity: var(--disabled-button-opacity);
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
181
|
|
|
171
182
|
.im-o-app__actions .im-c-map-button[aria-pressed="true"] {
|
|
172
183
|
color: var(--pressed-button-foreground-color);
|