@defra/interactive-map 0.0.42-fmp-4 → 0.0.43-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/im-shell.js +1 -1
- package/dist/umd/im-core.js +1 -1
- package/dist/umd/index.js +1 -1
- package/docs/api/keyboard-shortcut-definition.md +102 -0
- package/docs/api.md +2 -0
- package/docs/building-a-plugin.md +1 -0
- package/docs/plugins/draw.md +1 -1
- package/docs/plugins/plugin-manifest.md +9 -0
- package/package.json +1 -1
- 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 +28 -2
- 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/datasets/dist/esm/im-datasets-plugin.js +1 -1
- package/plugins/datasets/dist/umd/im-datasets-plugin.js +1 -1
- package/plugins/datasets/src/registry/dataset.js +8 -12
- package/plugins/datasets/src/registry/dataset.test.js +17 -0
- package/plugins/datasets/src/registry/datasetRegistry.js +4 -5
- package/plugins/draw/dist/esm/im-draw-ml-adapter.js +1 -1
- package/plugins/draw/dist/esm/im-draw-ol-adapter.js +1 -1
- package/plugins/draw/dist/esm/im-draw-plugin.js +1 -1
- package/plugins/draw/dist/umd/im-draw-ml-adapter.js +1 -1
- package/plugins/draw/dist/umd/im-draw-ol-adapter.js +1 -1
- package/plugins/draw/dist/umd/im-draw-plugin.js +1 -1
- package/plugins/draw/src/DrawInit.jsx +23 -13
- package/plugins/draw/src/adapters/maplibre/modes/drawMode/lifecycle.js +15 -0
- package/plugins/draw/src/adapters/maplibre/modes/drawMode/lifecycle.test.js +18 -0
- package/plugins/draw/src/adapters/openlayers/draw/drawInput.js +9 -0
- package/plugins/draw/src/adapters/openlayers/point/drawPointMode.js +12 -1
- package/plugins/draw/src/adapters/openlayers/point/drawPointMode.test.js +18 -0
- package/plugins/draw/src/manifest.js +4 -1
- package/plugins/draw/src/manifest.test.js +19 -7
- 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/events.js +6 -2
- package/plugins/interact/src/events.test.js +32 -0
- package/plugins/interact/src/hooks/useCrossHairVisibility.js +8 -3
- package/plugins/interact/src/hooks/useMapItemList.js +41 -12
- package/plugins/interact/src/hooks/useMapItemList.test.js +68 -3
- package/plugins/interact/src/utils/spatial.js +24 -1
- package/plugins/interact/src/utils/spatial.test.js +26 -1
- package/plugins/map-key/dist/css/index.css +1 -1
- package/plugins/map-key/dist/esm/im-map-key-plugin.js +1 -1
- package/plugins/map-key/dist/umd/im-map-key-plugin.js +1 -1
- package/plugins/map-key/dist/umd/index.js +1 -1
- package/plugins/map-key/src/components/Key/Key.jsx +3 -4
- package/plugins/map-key/src/components/Key/Key.module.scss +10 -8
- package/plugins/map-key/src/components/Key/KeyGroupItem.test.jsx +2 -2
- package/plugins/map-key/src/components/Key/KeyItem.jsx +2 -2
- package/plugins/map-key/src/components/Key/KeySvg.jsx +9 -7
- package/plugins/map-key/src/components/Key/KeySvg.test.jsx +4 -4
- package/plugins/map-key/src/components/Key/MapKey.jsx +5 -3
- package/plugins/map-key/src/manifest.js +1 -1
- package/plugins/map-key/src/utils/groupStyles.js +7 -0
- package/plugins/map-key/src/utils/mergeKeyGroupItems.js +12 -0
- package/plugins/map-key/src/utils/mergeKeyGroupItems.test.js +53 -0
- package/plugins/menu/dist/esm/im-menu-plugin.js +1 -1
- package/plugins/menu/dist/umd/im-menu-plugin.js +1 -1
- package/plugins/menu/dist/umd/index.js +1 -1
- package/plugins/menu/src/initialise/MenuInit.jsx +15 -3
- package/plugins/menu/src/initialise/MenuInit.test.jsx +25 -0
- package/plugins/menu/src/initialise/createMenu.js +3 -1
- package/plugins/menu/src/initialise/createMenu.test.js +20 -5
- package/plugins/menu/src/initialise/updateUrl.js +44 -0
- package/plugins/menu/src/initialise/updateUrl.test.js +91 -0
- package/plugins/menu/src/reducers/menuStateReducer.js +19 -1
- package/plugins/menu/src/reducers/menuStateReducer.test.js +53 -20
- package/plugins/menu/src/reducers/pluginState.js +1 -6
- package/plugins/menu/src/reducers/pluginState.test.js +0 -25
- 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/src/manifest.js +5 -9
- package/plugins/search/src/manifest.test.js +5 -0
- package/providers/beta/openlayers/dist/esm/im-openlayers-provider.js +1 -1
- package/providers/beta/openlayers/dist/umd/im-openlayers-provider.js +1 -1
- package/providers/beta/openlayers/src/openlayersProvider.js +3 -3
- package/providers/beta/openlayers/src/openlayersProvider.test.js +6 -3
- package/providers/beta/openlayers/src/utils/queryFeatures.js +72 -0
- package/providers/beta/openlayers/src/utils/queryFeatures.test.js +122 -1
- package/providers/maplibre/src/defaults.test.js +26 -0
- package/src/App/components/CrossHair/CrossHair.jsx +19 -11
- package/src/App/components/CrossHair/CrossHair.module.scss +33 -12
- package/src/App/components/CrossHair/CrossHair.test.jsx +35 -11
- package/src/App/components/KeyboardHelp/KeyboardHelp.jsx +4 -1
- package/src/App/components/KeyboardHelp/KeyboardHelp.test.jsx +17 -0
- package/src/App/components/MapButton/MapButton.jsx +19 -7
- package/src/App/components/MapButton/MapButton.test.jsx +42 -4
- package/src/App/components/Markers/SymbolMarker.jsx +2 -3
- package/src/App/components/Markers/SymbolMarker.test.jsx +8 -9
- package/src/App/components/Panel/Panel.jsx +19 -20
- package/src/App/components/Panel/Panel.module.scss +5 -0
- package/src/App/components/Panel/Panel.test.jsx +69 -4
- package/src/App/components/PopupMenu/PopupMenu.jsx +7 -3
- package/src/App/components/PopupMenu/PopupMenu.test.jsx +71 -0
- package/src/App/components/PopupMenu/usePopupMenu.js +10 -9
- package/src/App/components/Viewport/Features.jsx +18 -4
- package/src/App/components/Viewport/Features.module.scss +4 -4
- package/src/App/components/Viewport/Features.test.jsx +155 -11
- package/src/App/components/Viewport/Viewport.jsx +3 -3
- package/src/App/controls/keyboardMappings.js +7 -2
- package/src/App/controls/keyboardMappings.test.js +21 -0
- package/src/App/controls/keyboardShortcuts.js +16 -2
- package/src/App/controls/keyboardShortcuts.test.js +21 -0
- package/src/App/hooks/useCrossHairAPI.js +29 -1
- package/src/App/hooks/useCrossHairAPI.test.js +17 -1
- package/src/App/hooks/useFeatureFocus.js +86 -28
- package/src/App/hooks/useFeatureFocus.test.js +167 -0
- package/src/App/hooks/useFeatureItems.js +1 -1
- package/src/App/hooks/useInterfaceAPI.js +2 -1
- package/src/App/hooks/useInterfaceAPI.test.js +9 -0
- package/src/App/hooks/useKeyboardShortcuts.js +66 -35
- package/src/App/hooks/useKeyboardShortcuts.test.js +52 -0
- package/src/App/hooks/useModalPanelBehaviour.js +48 -34
- package/src/App/hooks/useModalPanelBehaviour.test.js +59 -0
- package/src/App/layout/Layout.jsx +4 -2
- package/src/App/layout/Layout.test.jsx +21 -0
- package/src/App/layout/layout.module.scss +7 -1
- package/src/App/registry/keyboardShortcutRegistry.js +2 -2
- package/src/App/registry/keyboardShortcutRegistry.test.js +16 -0
- package/src/App/renderer/HtmlElementHost.jsx +12 -32
- package/src/App/renderer/mapButtons.js +4 -0
- package/src/App/renderer/mapButtons.test.js +20 -0
- package/src/App/renderer/mapPanels.js +33 -53
- package/src/App/renderer/mapPanels.test.js +29 -4
- package/src/App/renderer/slotHelpers.js +33 -0
- package/src/App/renderer/slotHelpers.test.js +79 -2
- package/src/InteractiveMap/InteractiveMap.js +13 -2
- package/src/InteractiveMap/InteractiveMap.test.js +33 -0
- package/src/config/appConfig.js +2 -2
- package/src/types.js +40 -0
- package/src/utils/getPanelElementId.js +4 -0
- package/src/utils/getPanelElementId.test.js +11 -0
- package/src/utils/getPanelRole.js +22 -0
- package/src/utils/getPanelRole.test.js +56 -0
|
@@ -93,6 +93,22 @@ describe('createKeyboardShortcutRegistry', () => {
|
|
|
93
93
|
])
|
|
94
94
|
})
|
|
95
95
|
|
|
96
|
+
test('a mapProviderAgnostic core shortcut is included even when no provider declares support for it', () => {
|
|
97
|
+
jest.resetModules()
|
|
98
|
+
jest.doMock('../controls/keyboardShortcuts.js', () => ({
|
|
99
|
+
coreShortcuts: [
|
|
100
|
+
{ id: 'ordinary', description: 'Ordinary' },
|
|
101
|
+
{ id: 'agnostic', description: 'Agnostic', mapProviderAgnostic: true }
|
|
102
|
+
]
|
|
103
|
+
}))
|
|
104
|
+
const registry = require('./keyboardShortcutRegistry.js').createKeyboardShortcutRegistry()
|
|
105
|
+
registry.setProviderSupportedShortcuts([]) // no provider support declared at all
|
|
106
|
+
|
|
107
|
+
expect(registry.getKeyboardShortcuts()).toEqual([
|
|
108
|
+
{ id: 'agnostic', description: 'Agnostic', mapProviderAgnostic: true }
|
|
109
|
+
])
|
|
110
|
+
})
|
|
111
|
+
|
|
96
112
|
test('two registry instances are fully isolated from each other', () => {
|
|
97
113
|
// This is the multi-map-on-a-page scenario: a shortcut registered by one map
|
|
98
114
|
// (e.g. a plugin) must never leak into another map's help panel, and each map's
|
|
@@ -2,8 +2,7 @@
|
|
|
2
2
|
import React, { useRef, useLayoutEffect, useMemo } from 'react'
|
|
3
3
|
import { useApp } from '../store/appContext.js'
|
|
4
4
|
import { Panel } from '../components/Panel/Panel.jsx'
|
|
5
|
-
import { resolveTargetSlot,
|
|
6
|
-
import { allowedSlots } from './slots.js'
|
|
5
|
+
import { resolveTargetSlot, isControlVisible, isConsumerHtml, isPanelSlotEligible, getAllowedModalPanelId } from './slotHelpers.js'
|
|
7
6
|
|
|
8
7
|
/**
|
|
9
8
|
* Maps slot names to their corresponding layout refs.
|
|
@@ -90,28 +89,13 @@ const PersistentPanel = ({ panelId, config, isOpen, openPanelProps, focusOnOpen,
|
|
|
90
89
|
const bpConfig = config[breakpoint]
|
|
91
90
|
const targetSlot = bpConfig ? resolveTargetSlot(bpConfig, breakpoint) : null
|
|
92
91
|
|
|
93
|
-
//
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
// 2. Slot Validation
|
|
101
|
-
const isNextToButton = targetSlot.endsWith('-button')
|
|
102
|
-
const isSlotAllowed = allowedSlots.panel.includes(targetSlot) || isNextToButton
|
|
103
|
-
|
|
104
|
-
if (!isSlotAllowed) {
|
|
105
|
-
return false
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
// 3. Business Logic: Combine remaining conditions into a single "fail" check
|
|
109
|
-
const isForbiddenModal = bpConfig.modal && panelId !== allowedModalPanelId
|
|
110
|
-
const isForbiddenInline = config.inline === false && !isFullscreen
|
|
111
|
-
const isInvalidMode = !isModeAllowed(config, mode)
|
|
112
|
-
|
|
113
|
-
return !(isForbiddenModal || isForbiddenInline || isInvalidMode)
|
|
114
|
-
})()
|
|
92
|
+
// Same eligibility/modal-exclusivity rules as mapPanels.js (see slotHelpers.js), combined into
|
|
93
|
+
// one boolean since isVisible alone decides whether useDomProjection shows it.
|
|
94
|
+
const isVisible = Boolean(
|
|
95
|
+
isOpen && bpConfig && targetSlot &&
|
|
96
|
+
isPanelSlotEligible(config, { targetSlot, mode, isFullscreen }) &&
|
|
97
|
+
(!bpConfig.modal || panelId === allowedModalPanelId)
|
|
98
|
+
)
|
|
115
99
|
|
|
116
100
|
useDomProjection(panelRootRef, targetSlot, isVisible, layoutRefs, breakpoint)
|
|
117
101
|
|
|
@@ -181,14 +165,10 @@ export const HtmlElementHost = () => {
|
|
|
181
165
|
)
|
|
182
166
|
|
|
183
167
|
// Determine which modal panel is allowed (topmost open modal)
|
|
184
|
-
const allowedModalPanelId = useMemo(
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
return cfg?.modal
|
|
189
|
-
})
|
|
190
|
-
return modalPanels.length > 0 ? modalPanels[modalPanels.length - 1][0] : null // NOSONAR - .at() unsupported on Chrome < 92
|
|
191
|
-
}, [openPanels, panelConfig, breakpoint])
|
|
168
|
+
const allowedModalPanelId = useMemo(
|
|
169
|
+
() => getAllowedModalPanelId(openPanels, panelConfig, breakpoint),
|
|
170
|
+
[openPanels, panelConfig, breakpoint]
|
|
171
|
+
)
|
|
192
172
|
|
|
193
173
|
if (!htmlPanels.length && !htmlControls.length) {
|
|
194
174
|
return null
|
|
@@ -3,6 +3,7 @@ import { MapButton } from '../components/MapButton/MapButton.jsx'
|
|
|
3
3
|
import { allowedSlots } from './slots.js'
|
|
4
4
|
import { groupByKey } from './groupByKey.js'
|
|
5
5
|
import { orderItems } from './orderItems.js'
|
|
6
|
+
import { classifyPanel, getPanelRole } from '../../utils/getPanelRole.js'
|
|
6
7
|
import { logger } from '../../services/logger.js'
|
|
7
8
|
|
|
8
9
|
function getMatchingButtons ({ appState, buttonConfig, slot, evaluateProp }) {
|
|
@@ -118,6 +119,8 @@ function SlotButton ({ buttonId, config, appState, appConfig, evaluateProp }) {
|
|
|
118
119
|
const bpConfig = config[appState.breakpoint] ?? {}
|
|
119
120
|
const handleClick = createButtonClickHandler(config, appState, evaluateProp)
|
|
120
121
|
const isPanelOpen = !!(config.panelId && appState.openPanels[config.panelId])
|
|
122
|
+
const panelBpConfig = config.panelId ? appState.panelConfig?.[config.panelId]?.[appState.breakpoint] : undefined
|
|
123
|
+
const panelRole = panelBpConfig ? getPanelRole(classifyPanel(panelBpConfig)) : undefined
|
|
121
124
|
|
|
122
125
|
return (
|
|
123
126
|
<MapButton
|
|
@@ -133,6 +136,7 @@ function SlotButton ({ buttonId, config, appState, appConfig, evaluateProp }) {
|
|
|
133
136
|
isPressed={(config.isPressed !== undefined || config.pressedWhen) ? appState.pressedButtons.has(buttonId) : undefined}
|
|
134
137
|
isExpanded={(config.isExpanded !== undefined || config.expandedWhen) ? appState.expandedButtons.has(buttonId) : undefined}
|
|
135
138
|
isPanelOpen={isPanelOpen}
|
|
139
|
+
panelRole={panelRole}
|
|
136
140
|
onClick={handleClick}
|
|
137
141
|
panelId={config.panelId}
|
|
138
142
|
menuItems={config.menuItems}
|
|
@@ -170,6 +170,26 @@ describe('mapButtons module', () => {
|
|
|
170
170
|
expect(result.props).toMatchObject({ isDisabled: true, isHidden: true, isPressed: true, isExpanded: true })
|
|
171
171
|
})
|
|
172
172
|
|
|
173
|
+
it.each([
|
|
174
|
+
[{ slot: 'right-top', open: false }, 'dialog'],
|
|
175
|
+
[{ slot: 'right-top', open: false, dismissible: false }, 'region'],
|
|
176
|
+
[{ slot: 'side', open: true }, 'complementary']
|
|
177
|
+
])('derives panelRole %j -> %s from the target panel\'s breakpoint config', (panelBpConfig, expectedRole) => {
|
|
178
|
+
const state = { ...appState, panelConfig: { p1: { desktop: panelBpConfig } } }
|
|
179
|
+
const result = render({ ...baseBtn, panelId: 'p1' }, state)
|
|
180
|
+
expect(result.props.panelRole).toBe(expectedRole)
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
it('leaves panelRole undefined when the button has no panelId', () => {
|
|
184
|
+
const result = render(baseBtn)
|
|
185
|
+
expect(result.props.panelRole).toBeUndefined()
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
it('leaves panelRole undefined when the target panel has no config at this breakpoint', () => {
|
|
189
|
+
const result = render({ ...baseBtn, panelId: 'missing' })
|
|
190
|
+
expect(result.props.panelRole).toBeUndefined()
|
|
191
|
+
})
|
|
192
|
+
|
|
173
193
|
it('uses empty object fallback for missing breakpoint config', () => {
|
|
174
194
|
const result = render(baseBtn, { ...appState, breakpoint: 'mobile' })
|
|
175
195
|
expect(result.props.showLabel).toBe(true)
|
|
@@ -2,8 +2,7 @@
|
|
|
2
2
|
import React from 'react'
|
|
3
3
|
import { withPluginContexts } from './pluginWrapper.js'
|
|
4
4
|
import { Panel } from '../components/Panel/Panel.jsx'
|
|
5
|
-
import {
|
|
6
|
-
import { resolveTargetSlot, isModeAllowed, isConsumerHtml } from './slotHelpers.js'
|
|
5
|
+
import { resolveTargetSlot, isConsumerHtml, isPanelSlotEligible, getAllowedModalPanelId } from './slotHelpers.js'
|
|
7
6
|
import { mapControls } from './mapControls.js'
|
|
8
7
|
import { orderItems } from './orderItems.js'
|
|
9
8
|
import { groupIntoTabs } from './groupIntoTabs.js'
|
|
@@ -11,69 +10,49 @@ import { stringToKebab } from '../../utils/stringToKebab.js'
|
|
|
11
10
|
import { logger } from '../../services/logger.js'
|
|
12
11
|
|
|
13
12
|
/**
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
13
|
+
* Maps every configured panel eligible for the given layout slot to a renderable entry — open or
|
|
14
|
+
* closed alike, so a button's aria-controls id always resolves to a stable, permanently-mounted
|
|
15
|
+
* <Panel> (hidden when closed). buildPanelBody only runs while open, so nothing expensive mounts
|
|
16
|
+
* before then.
|
|
17
17
|
*/
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
18
|
+
// Consumer HTML panels are managed by HtmlElementHost; the rest need a breakpoint config and to
|
|
19
|
+
// be eligible for this slot. Returns null (skip) or the pieces the caller needs.
|
|
20
|
+
const getEligiblePanelConfig = (panelId, panelConfig, breakpoint, { slot, mode, isFullscreen }) => {
|
|
21
|
+
const config = panelConfig[panelId]
|
|
22
|
+
if (!config || isConsumerHtml(config)) {
|
|
23
|
+
return null
|
|
22
24
|
}
|
|
23
|
-
|
|
24
|
-
|
|
25
|
+
const bpConfig = config[breakpoint]
|
|
26
|
+
if (!bpConfig) {
|
|
27
|
+
return null
|
|
25
28
|
}
|
|
26
|
-
|
|
27
|
-
|
|
29
|
+
const targetSlot = resolveTargetSlot(bpConfig, breakpoint)
|
|
30
|
+
if (!isPanelSlotEligible(config, { targetSlot, slot, mode, isFullscreen })) {
|
|
31
|
+
return null
|
|
28
32
|
}
|
|
29
|
-
|
|
30
|
-
return false
|
|
31
|
-
}
|
|
32
|
-
if (bpConfig.modal && panelId !== allowedModalPanelId) {
|
|
33
|
-
return false
|
|
34
|
-
}
|
|
35
|
-
return true
|
|
33
|
+
return { config, bpConfig }
|
|
36
34
|
}
|
|
37
35
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
*/
|
|
36
|
+
// A losing modal panel (see getAllowedModalPanelId) still gets a shell — it's just not open.
|
|
37
|
+
const resolveIsOpen = (openEntry, bpConfig, panelId, allowedModalPanelId) =>
|
|
38
|
+
Boolean(openEntry) && (!bpConfig.modal || panelId === allowedModalPanelId)
|
|
39
|
+
|
|
43
40
|
export function mapPanels ({ slot, appState, evaluateProp }) {
|
|
44
41
|
const { breakpoint, pluginRegistry, panelConfig, mode, openPanels } = appState
|
|
45
42
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
const modalPanels = openPanelEntries.filter(([panelId]) => {
|
|
49
|
-
const cfg = panelConfig[panelId]?.[breakpoint]
|
|
50
|
-
return cfg?.modal
|
|
51
|
-
})
|
|
52
|
-
const allowedModalPanelId = modalPanels.length > 0 ? modalPanels[modalPanels.length - 1][0] : null // NOSONAR, .at() is only Chrome 90+
|
|
53
|
-
|
|
54
|
-
return openPanelEntries.map(([panelId, { props, focusOnOpen }]) => {
|
|
55
|
-
const config = panelConfig[panelId]
|
|
56
|
-
if (!config) {
|
|
57
|
-
return null
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
// Consumer HTML panels are managed by HtmlElementHost
|
|
61
|
-
if (isConsumerHtml(config)) {
|
|
62
|
-
return null
|
|
63
|
-
}
|
|
43
|
+
// Only the most-recently-opened modal panel is ever actually shown — see isOpen below.
|
|
44
|
+
const allowedModalPanelId = getAllowedModalPanelId(openPanels, panelConfig, breakpoint)
|
|
64
45
|
|
|
65
|
-
|
|
66
|
-
|
|
46
|
+
return Object.keys(panelConfig).map((panelId) => {
|
|
47
|
+
const eligible = getEligiblePanelConfig(panelId, panelConfig, breakpoint, { slot, mode, isFullscreen: appState.isFullscreen })
|
|
48
|
+
if (!eligible) {
|
|
67
49
|
return null
|
|
68
50
|
}
|
|
51
|
+
const { config, bpConfig } = eligible
|
|
69
52
|
|
|
70
|
-
const
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
targetSlot, slot, mode, isFullscreen: appState.isFullscreen, allowedModalPanelId
|
|
74
|
-
})) {
|
|
75
|
-
return null
|
|
76
|
-
}
|
|
53
|
+
const openEntry = openPanels[panelId]
|
|
54
|
+
const isOpen = resolveIsOpen(openEntry, bpConfig, panelId, allowedModalPanelId)
|
|
55
|
+
const { props = {}, focusOnOpen } = openEntry ?? {}
|
|
77
56
|
|
|
78
57
|
const plugin = pluginRegistry.registeredPlugins.find(p => p.id === config.pluginId)
|
|
79
58
|
const pluginId = plugin?.id
|
|
@@ -92,7 +71,8 @@ export function mapPanels ({ slot, appState, evaluateProp }) {
|
|
|
92
71
|
panelConfig={config}
|
|
93
72
|
props={props}
|
|
94
73
|
focusOnOpen={focusOnOpen}
|
|
95
|
-
{
|
|
74
|
+
isOpen={isOpen}
|
|
75
|
+
{...(isOpen ? buildPanelBody({ panelId, config, bpConfig, props, plugin, pluginId, html, label, appState, evaluateProp }) : {})}
|
|
96
76
|
label={label}
|
|
97
77
|
html={html}
|
|
98
78
|
/>
|
|
@@ -40,8 +40,25 @@ describe('mapPanels', () => {
|
|
|
40
40
|
defaultAppState.panelConfig = ({ p1: baseConfig })
|
|
41
41
|
})
|
|
42
42
|
|
|
43
|
-
it('
|
|
44
|
-
|
|
43
|
+
it('renders a closed shell (not nothing) for an eligible panel that is not open', () => {
|
|
44
|
+
const result = map({ ...defaultAppState, openPanels: {} })
|
|
45
|
+
expect(result).toHaveLength(1)
|
|
46
|
+
expect(result[0]).toMatchObject({ id: 'p1', type: 'panel', order: 1 })
|
|
47
|
+
expect(result[0].element.props.isOpen).toBe(false)
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
it('does not build a closed panel\'s body before it opens', () => {
|
|
51
|
+
const renderFn = jest.fn(() => <div>child</div>)
|
|
52
|
+
defaultAppState.panelConfig = ({ p1: { ...baseConfig, render: renderFn } })
|
|
53
|
+
const result = map({ ...defaultAppState, openPanels: {} })
|
|
54
|
+
expect(mapControls).not.toHaveBeenCalled()
|
|
55
|
+
expect(withPluginContexts).not.toHaveBeenCalled()
|
|
56
|
+
expect(result[0].element.props.items).toBeUndefined()
|
|
57
|
+
expect(result[0].element.props.tabs).toBeUndefined()
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
it('returns an empty array only when there is nothing eligible for the slot at all', () => {
|
|
61
|
+
expect(map({ ...defaultAppState, panelConfig: {} })).toEqual([])
|
|
45
62
|
})
|
|
46
63
|
|
|
47
64
|
it('skips panel if config is missing', () => {
|
|
@@ -49,6 +66,11 @@ describe('mapPanels', () => {
|
|
|
49
66
|
expect(map()).toEqual([])
|
|
50
67
|
})
|
|
51
68
|
|
|
69
|
+
it('skips a registered panelId whose config is falsy', () => {
|
|
70
|
+
defaultAppState.panelConfig = ({ p1: null })
|
|
71
|
+
expect(map()).toEqual([])
|
|
72
|
+
})
|
|
73
|
+
|
|
52
74
|
it('skips panel if breakpoint config is missing', () => {
|
|
53
75
|
defaultAppState.panelConfig = ({ p1: {} })
|
|
54
76
|
expect(map()).toEqual([])
|
|
@@ -92,7 +114,7 @@ describe('mapPanels', () => {
|
|
|
92
114
|
expect(result).toEqual([])
|
|
93
115
|
})
|
|
94
116
|
|
|
95
|
-
it('only
|
|
117
|
+
it('renders both modal panels\' shells but only marks the last-opened one as open', () => {
|
|
96
118
|
defaultAppState.panelConfig = ({
|
|
97
119
|
p1: { desktop: { modal: true }, includeModes: ['view'] },
|
|
98
120
|
p2: { desktop: { modal: true }, includeModes: ['view'] }
|
|
@@ -101,7 +123,10 @@ describe('mapPanels', () => {
|
|
|
101
123
|
...defaultAppState,
|
|
102
124
|
openPanels: { p1: { props: {} }, p2: { props: {} } }
|
|
103
125
|
}
|
|
104
|
-
|
|
126
|
+
const result = map(state, 'modal')
|
|
127
|
+
expect(result.map(r => r.id).sort()).toEqual(['p1', 'p2'])
|
|
128
|
+
expect(result.find(r => r.id === 'p1').element.props.isOpen).toBe(false)
|
|
129
|
+
expect(result.find(r => r.id === 'p2').element.props.isOpen).toBe(true)
|
|
105
130
|
})
|
|
106
131
|
|
|
107
132
|
it('wraps render function with plugin context', () => {
|
|
@@ -60,3 +60,36 @@ export const isControlVisible = (control, { breakpoint, mode, isFullscreen }) =>
|
|
|
60
60
|
export const isConsumerHtml = (config) => {
|
|
61
61
|
return typeof config.html === 'string' && !config.pluginId
|
|
62
62
|
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Whether a panel is eligible for a target slot (slot type, mode, inline/fullscreen) —
|
|
66
|
+
* independent of open state and modal exclusivity (see getAllowedModalPanelId). `slot`, if
|
|
67
|
+
* given, also requires an exact match to the panel's resolved targetSlot.
|
|
68
|
+
*/
|
|
69
|
+
export const isPanelSlotEligible = (config, { targetSlot, slot, mode, isFullscreen }) => {
|
|
70
|
+
const isNextToButton = targetSlot.endsWith('-button')
|
|
71
|
+
if (!allowedSlots.panel.includes(targetSlot) && !isNextToButton) {
|
|
72
|
+
return false
|
|
73
|
+
}
|
|
74
|
+
if (!isModeAllowed(config, mode)) {
|
|
75
|
+
return false
|
|
76
|
+
}
|
|
77
|
+
if (config.inline === false && !isFullscreen) {
|
|
78
|
+
return false
|
|
79
|
+
}
|
|
80
|
+
if (slot !== undefined && targetSlot !== slot) {
|
|
81
|
+
return false
|
|
82
|
+
}
|
|
83
|
+
return true
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// The id of the most-recently-opened modal panel — the only one actually allowed to show.
|
|
87
|
+
export const getAllowedModalPanelId = (openPanels, panelConfig, breakpoint) => {
|
|
88
|
+
const openModalPanelIds = Object.keys(openPanels).filter(panelId => panelConfig[panelId]?.[breakpoint]?.modal)
|
|
89
|
+
return openModalPanelIds.length > 0 ? openModalPanelIds[openModalPanelIds.length - 1] : null // NOSONAR, .at() is only Chrome 90+
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Whether any modal-configured panel is open — drives the modal backdrop's visibility.
|
|
93
|
+
export const hasOpenModalPanel = (openPanels, panelConfig, breakpoint) => {
|
|
94
|
+
return getAllowedModalPanelId(openPanels, panelConfig, breakpoint) !== null
|
|
95
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { resolveTargetSlot, isModeAllowed, isControlVisible, isConsumerHtml } from './slotHelpers.js'
|
|
1
|
+
import { resolveTargetSlot, isModeAllowed, isControlVisible, isConsumerHtml, isPanelSlotEligible, getAllowedModalPanelId, hasOpenModalPanel } from './slotHelpers.js'
|
|
2
2
|
|
|
3
|
-
jest.mock('./slots.js', () => ({ allowedSlots: { control: ['inset', 'banner', 'actions'] } }))
|
|
3
|
+
jest.mock('./slots.js', () => ({ allowedSlots: { control: ['inset', 'banner', 'actions'], panel: ['header', 'modal', 'left-top'] } }))
|
|
4
4
|
|
|
5
5
|
describe('resolveTargetSlot', () => {
|
|
6
6
|
it('returns modal for modal panels', () => {
|
|
@@ -85,3 +85,80 @@ describe('isConsumerHtml', () => {
|
|
|
85
85
|
expect(isConsumerHtml({ render: () => {} })).toBe(false)
|
|
86
86
|
})
|
|
87
87
|
})
|
|
88
|
+
|
|
89
|
+
describe('isPanelSlotEligible', () => {
|
|
90
|
+
const base = { includeModes: ['view'] }
|
|
91
|
+
const ctx = { targetSlot: 'header', mode: 'view', isFullscreen: false }
|
|
92
|
+
|
|
93
|
+
it('returns true for a panel in an allowed slot', () => {
|
|
94
|
+
expect(isPanelSlotEligible(base, ctx)).toBe(true)
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
it('returns false when the target slot is not an allowed panel slot', () => {
|
|
98
|
+
expect(isPanelSlotEligible(base, { ...ctx, targetSlot: 'invalid' })).toBe(false)
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
it('allows a target slot next to a button even though it is not a named panel slot', () => {
|
|
102
|
+
expect(isPanelSlotEligible(base, { ...ctx, targetSlot: 'my-button-button' })).toBe(true)
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
it('returns false when mode is not allowed', () => {
|
|
106
|
+
expect(isPanelSlotEligible({ includeModes: ['edit'] }, ctx)).toBe(false)
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
it('returns false when inline:false and not fullscreen', () => {
|
|
110
|
+
expect(isPanelSlotEligible({ ...base, inline: false }, ctx)).toBe(false)
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
it('returns true when inline:false and fullscreen', () => {
|
|
114
|
+
expect(isPanelSlotEligible({ ...base, inline: false }, { ...ctx, isFullscreen: true })).toBe(true)
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
it('returns false when a specific requested slot is given and the target slot does not match it', () => {
|
|
118
|
+
expect(isPanelSlotEligible(base, { ...ctx, slot: 'left-top' })).toBe(false)
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
it('returns true when a specific requested slot is given and matches the target slot', () => {
|
|
122
|
+
expect(isPanelSlotEligible(base, { ...ctx, slot: 'header' })).toBe(true)
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
it('skips the requested-slot check entirely when slot is omitted (HtmlElementHost usage)', () => {
|
|
126
|
+
expect(isPanelSlotEligible(base, { targetSlot: 'header', mode: 'view', isFullscreen: false })).toBe(true)
|
|
127
|
+
})
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
describe('getAllowedModalPanelId', () => {
|
|
131
|
+
const panelConfig = {
|
|
132
|
+
p1: { desktop: { modal: true } },
|
|
133
|
+
p2: { desktop: { modal: true } },
|
|
134
|
+
p3: { desktop: {} }
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
it('returns null when no modal panel is open', () => {
|
|
138
|
+
expect(getAllowedModalPanelId({ p3: { props: {} } }, panelConfig, 'desktop')).toBeNull()
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
it('returns the only open modal panel', () => {
|
|
142
|
+
expect(getAllowedModalPanelId({ p1: { props: {} } }, panelConfig, 'desktop')).toBe('p1')
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
it('returns the most recently opened modal panel when more than one is open', () => {
|
|
146
|
+
expect(getAllowedModalPanelId({ p1: { props: {} }, p2: { props: {} } }, panelConfig, 'desktop')).toBe('p2')
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
it('returns null when there are no open panels at all', () => {
|
|
150
|
+
expect(getAllowedModalPanelId({}, panelConfig, 'desktop')).toBeNull()
|
|
151
|
+
})
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
describe('hasOpenModalPanel', () => {
|
|
155
|
+
const panelConfig = { p1: { desktop: { modal: true } }, p2: { desktop: {} } }
|
|
156
|
+
|
|
157
|
+
it('returns false when no modal panel is open', () => {
|
|
158
|
+
expect(hasOpenModalPanel({ p2: { props: {} } }, panelConfig, 'desktop')).toBe(false)
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
it('returns true when a modal panel is open', () => {
|
|
162
|
+
expect(hasOpenModalPanel({ p1: { props: {} } }, panelConfig, 'desktop')).toBe(true)
|
|
163
|
+
})
|
|
164
|
+
})
|
|
@@ -461,12 +461,23 @@ export default class InteractiveMap {
|
|
|
461
461
|
* Focus is moved to the panel by default. Set `focus: false` in options to
|
|
462
462
|
* suppress this — useful when showing a panel and you want focus to remain on the button.
|
|
463
463
|
*
|
|
464
|
+
* When the panel is later closed, focus returns to `triggeringElement` if one is given or
|
|
465
|
+
* can be inferred, falling back to the map viewport otherwise. If `triggeringElement` isn't
|
|
466
|
+
* passed explicitly, whatever currently has real DOM focus is captured automatically (e.g. a
|
|
467
|
+
* selected item in the map features list) — this only needs to be passed explicitly when the
|
|
468
|
+
* panel is opened from something that doesn't hold focus at the time (e.g a timer, or a
|
|
469
|
+
* genuine mouse click, which doesn't reliably focus its target in every browser).
|
|
470
|
+
*
|
|
464
471
|
* @param {string} id - Panel identifier to show.
|
|
465
472
|
* @param {object} [options]
|
|
466
473
|
* @param {boolean} [options.focus=true] - Whether to move focus to the panel.
|
|
474
|
+
* @param {HTMLElement} [options.triggeringElement] - Element to return focus to on close.
|
|
475
|
+
* Defaults to the currently focused element, if any.
|
|
467
476
|
*/
|
|
468
|
-
showPanel (id, { focus = true } = {}) {
|
|
469
|
-
|
|
477
|
+
showPanel (id, { focus = true, triggeringElement } = {}) {
|
|
478
|
+
const resolvedTriggeringElement = triggeringElement ??
|
|
479
|
+
(document.activeElement && document.activeElement !== document.body ? document.activeElement : undefined)
|
|
480
|
+
this.eventBus.emit(events.APP_SHOW_PANEL, { id, focus, triggeringElement: resolvedTriggeringElement })
|
|
470
481
|
}
|
|
471
482
|
|
|
472
483
|
/**
|
|
@@ -580,6 +580,39 @@ describe('InteractiveMap — Public API Methods', () => {
|
|
|
580
580
|
expect(map.eventBus.emit).toHaveBeenCalledWith('app:togglebuttonstate', { id: 'btn-1', prop: 'disabled', value: true })
|
|
581
581
|
})
|
|
582
582
|
|
|
583
|
+
describe('showPanel — triggeringElement', () => {
|
|
584
|
+
afterEach(() => { document.body.innerHTML = '' })
|
|
585
|
+
|
|
586
|
+
it('passes an explicit triggeringElement through unchanged', () => {
|
|
587
|
+
const el = document.createElement('button')
|
|
588
|
+
map.showPanel('panel1', { triggeringElement: el })
|
|
589
|
+
expect(map.eventBus.emit).toHaveBeenCalledWith('app:showpanel', { id: 'panel1', focus: true, triggeringElement: el })
|
|
590
|
+
})
|
|
591
|
+
|
|
592
|
+
it('auto-captures the currently focused element when none is given', () => {
|
|
593
|
+
const el = document.createElement('button')
|
|
594
|
+
document.body.appendChild(el)
|
|
595
|
+
el.focus()
|
|
596
|
+
map.showPanel('panel1')
|
|
597
|
+
expect(map.eventBus.emit).toHaveBeenCalledWith('app:showpanel', { id: 'panel1', focus: true, triggeringElement: el })
|
|
598
|
+
})
|
|
599
|
+
|
|
600
|
+
it('does not fall back to document.body as a triggeringElement', () => {
|
|
601
|
+
document.body.focus?.() // no-op in jsdom, but document.activeElement defaults to body regardless
|
|
602
|
+
map.showPanel('panel1')
|
|
603
|
+
expect(map.eventBus.emit).toHaveBeenCalledWith('app:showpanel', { id: 'panel1', focus: true, triggeringElement: undefined })
|
|
604
|
+
})
|
|
605
|
+
|
|
606
|
+
it('an explicit triggeringElement takes priority over the currently focused element', () => {
|
|
607
|
+
const focused = document.createElement('button')
|
|
608
|
+
document.body.appendChild(focused)
|
|
609
|
+
focused.focus()
|
|
610
|
+
const explicit = document.createElement('button')
|
|
611
|
+
map.showPanel('panel1', { triggeringElement: explicit })
|
|
612
|
+
expect(map.eventBus.emit).toHaveBeenCalledWith('app:showpanel', { id: 'panel1', focus: true, triggeringElement: explicit })
|
|
613
|
+
})
|
|
614
|
+
})
|
|
615
|
+
|
|
583
616
|
it('setContinueEnabled enables and disables the journey continue button', () => {
|
|
584
617
|
map.setContinueEnabled(true)
|
|
585
618
|
expect(map.eventBus.emit).toHaveBeenCalledWith('app:togglebuttonstate', { id: 'journeyContinue', prop: 'disabled', value: false })
|
package/src/config/appConfig.js
CHANGED
|
@@ -98,7 +98,7 @@ export const defaultAppConfig = {
|
|
|
98
98
|
desktop: buttonSlots
|
|
99
99
|
}, {
|
|
100
100
|
id: 'moveControls',
|
|
101
|
-
label: '
|
|
101
|
+
label: 'Map controls',
|
|
102
102
|
iconId: 'move',
|
|
103
103
|
keepFocus: true,
|
|
104
104
|
isExpanded: false,
|
|
@@ -157,7 +157,7 @@ export const defaultAppConfig = {
|
|
|
157
157
|
svgContent: '<path d="m6 9 6 6 6-6"/>'
|
|
158
158
|
}, {
|
|
159
159
|
id: 'move',
|
|
160
|
-
svgContent: '<path d="
|
|
160
|
+
svgContent: '<path d="M11.146 15.854a1.207 1.207 0 0 1 1.708 0l1.56 1.56A2 2 0 0 1 15 18.828V21a1 1 0 0 1-1 1h-4a1 1 0 0 1-1-1v-2.172a2 2 0 0 1 .586-1.414z"/><path d="M18.828 15a2 2 0 0 1-1.414-.586l-1.56-1.56a1.207 1.207 0 0 1 0-1.708l1.56-1.56A2 2 0 0 1 18.828 9H21a1 1 0 0 1 1 1v4a1 1 0 0 1-1 1z"/><path d="M6.586 14.414A2 2 0 0 1 5.172 15H3a1 1 0 0 1-1-1v-4a1 1 0 0 1 1-1h2.172a2 2 0 0 1 1.414.586l1.56 1.56a1.207 1.207 0 0 1 0 1.708z"/><path d="M9 3a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v2.172a2 2 0 0 1-.586 1.414l-1.56 1.56a1.207 1.207 0 0 1-1.708 0l-1.56-1.56A2 2 0 0 1 9 5.172z"/>'
|
|
161
161
|
}, {
|
|
162
162
|
id: 'precision',
|
|
163
163
|
svgContent: '<circle cx="12" cy="12" r="10"/><line x1="22" x2="18" y1="12" y2="12"/><line x1="6" x2="2" y1="12" y2="12"/><line x1="12" x2="12" y1="6" y2="2"/><line x1="12" x2="12" y1="22" y2="18"/>'
|
package/src/types.js
CHANGED
|
@@ -629,6 +629,43 @@
|
|
|
629
629
|
* Raw SVG content (without the outer SVG tag).
|
|
630
630
|
*/
|
|
631
631
|
|
|
632
|
+
/**
|
|
633
|
+
* Describes a single row in the keyboard shortcuts help panel (opened via Shift+?).
|
|
634
|
+
* Plugins register these via `PluginManifest.keyboardShortcuts`; core (built-in)
|
|
635
|
+
* shortcuts share this same shape internally.
|
|
636
|
+
*
|
|
637
|
+
* @typedef {Object} KeyboardShortcutDefinition
|
|
638
|
+
*
|
|
639
|
+
* @property {string} command
|
|
640
|
+
* HTML string describing the key combination, rendered as markup — wrap keys in
|
|
641
|
+
* `<kbd>` tags, e.g. `'<kbd>Shift</kbd> + <kbd>K</kbd>'`. If it uses the Alt key,
|
|
642
|
+
* label it per platform (macOS calls it Option, not Alt) — see the shared `isMac()`
|
|
643
|
+
* helper and its use in the app's own core shortcuts.
|
|
644
|
+
*
|
|
645
|
+
* @property {'viewport' | 'listbox' | 'global'} [context='viewport']
|
|
646
|
+
* Which help-panel context this shortcut applies to. Used only to choose the panel's
|
|
647
|
+
* default open tab — it doesn't affect whether the row is shown.
|
|
648
|
+
*
|
|
649
|
+
* @property {string} [group='Navigate']
|
|
650
|
+
* Tab label the shortcut is grouped under in the help panel. Shortcuts sharing a group
|
|
651
|
+
* (case/whitespace-insensitive) appear together under one tab; if every visible shortcut
|
|
652
|
+
* shares one group, the panel renders as a flat list with no tabs.
|
|
653
|
+
*
|
|
654
|
+
* @property {string} id
|
|
655
|
+
* Unique shortcut identifier.
|
|
656
|
+
*
|
|
657
|
+
* @property {string[]} [requiredConfig]
|
|
658
|
+
* App config keys that must all be truthy for the shortcut to appear in the help panel.
|
|
659
|
+
*
|
|
660
|
+
* @property {string} title
|
|
661
|
+
* Accessible title shown in the help panel.
|
|
662
|
+
*
|
|
663
|
+
* @property {boolean} [visuallyHidden=false]
|
|
664
|
+
* When true, the row is hidden from sighted users but stays in the DOM and remains
|
|
665
|
+
* discoverable by assistive technology. Use for a shortcut with no visual affordance to
|
|
666
|
+
* discover it by otherwise — e.g. one whose only effect is a screen reader announcement.
|
|
667
|
+
*/
|
|
668
|
+
|
|
632
669
|
/**
|
|
633
670
|
* Manifest defining a plugin's buttons, panels, controls, API methods, and state.
|
|
634
671
|
*
|
|
@@ -649,6 +686,9 @@
|
|
|
649
686
|
* @property {ComponentType} [InitComponent]
|
|
650
687
|
* Initialization component.
|
|
651
688
|
*
|
|
689
|
+
* @property {KeyboardShortcutDefinition[]} [keyboardShortcuts]
|
|
690
|
+
* Keyboard shortcut definitions shown in the keyboard shortcuts help panel.
|
|
691
|
+
*
|
|
652
692
|
* @property {PanelDefinition[]} [panels]
|
|
653
693
|
* Panel definitions.
|
|
654
694
|
*
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { getPanelElementId } from './getPanelElementId.js'
|
|
2
|
+
|
|
3
|
+
describe('getPanelElementId', () => {
|
|
4
|
+
it('builds the id from the idPrefix and kebab-cased panelId', () => {
|
|
5
|
+
expect(getPanelElementId('prefix', 'Settings')).toBe('prefix-panel-settings')
|
|
6
|
+
})
|
|
7
|
+
|
|
8
|
+
it('kebab-cases a camelCase panelId', () => {
|
|
9
|
+
expect(getPanelElementId('map1', 'layerOptions')).toBe('map1-panel-layer-options')
|
|
10
|
+
})
|
|
11
|
+
})
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// Shared panel dialog/modality classification, used by Panel.jsx (to render the panel's
|
|
2
|
+
// own role/aria-modal) and MapButton.jsx (to decide a trigger button's aria-haspopup) so
|
|
3
|
+
// the two can't drift apart.
|
|
4
|
+
|
|
5
|
+
// WCAG requires a modal to always be dismissible, so modal:true forces it regardless of config.
|
|
6
|
+
export const classifyPanel = (bpConfig) => {
|
|
7
|
+
const isModal = bpConfig.modal === true
|
|
8
|
+
const isAside = bpConfig.slot === 'side' && bpConfig.open && !isModal
|
|
9
|
+
const isDismissible = isModal || bpConfig.dismissible !== false
|
|
10
|
+
const isDialog = !isAside && isDismissible
|
|
11
|
+
return { isModal, isAside, isDismissible, isDialog }
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const getPanelRole = ({ isDialog, isDismissible }) => {
|
|
15
|
+
if (isDialog) {
|
|
16
|
+
return 'dialog'
|
|
17
|
+
}
|
|
18
|
+
if (isDismissible) {
|
|
19
|
+
return 'complementary'
|
|
20
|
+
}
|
|
21
|
+
return 'region'
|
|
22
|
+
}
|