@defra/interactive-map 0.0.41-alpha → 0.0.42-fmp-2
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/umd/im-core.js +1 -1
- package/dist/umd/index.js +1 -1
- package/docs/api/button-definition.md +7 -8
- package/docs/api/control-definition.md +8 -0
- package/docs/api/panel-definition.md +5 -0
- package/docs/api/slots.md +24 -1
- package/docs/examples/add-polygons.mdx +8 -2
- package/docs/examples/add-symbols.mdx +8 -2
- package/docs/plugins/datasets.md +23 -15
- package/docs/plugins/map-key.md +147 -0
- package/docs/plugins.md +5 -1
- package/package.json +4 -2
- package/plugins/beta/draw-ol/dist/css/index.css +1 -13
- package/plugins/beta/frame/dist/css/index.css +1 -11
- package/plugins/beta/map-styles/dist/css/index.css +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/scale-bar/dist/css/index.css +1 -31
- package/plugins/beta/use-location/dist/esm/im-use-location-plugin.js +1 -1
- package/plugins/beta/use-location/dist/umd/im-use-location-plugin.js +1 -1
- package/plugins/beta/use-location/src/manifest.js +1 -1
- package/plugins/datasets/dist/css/5648.index.css +1 -5
- package/plugins/datasets/dist/css/index.css +1 -1
- package/plugins/draw/dist/esm/im-draw-plugin.js +1 -1
- package/plugins/draw/dist/umd/im-draw-plugin.js +1 -1
- package/plugins/draw/dist/umd/index.js +1 -1
- package/plugins/draw/src/api/createNewShape.js +41 -0
- package/plugins/draw/src/api/createNewShape.test.js +63 -0
- package/plugins/draw/src/api/newLine.js +2 -40
- package/plugins/draw/src/api/newPolygon.js +2 -40
- 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/manifest.js +1 -1
- package/plugins/map-key/dist/css/index.css +1 -62
- 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/src/components/Key/Key.jsx +5 -3
- package/plugins/map-key/src/components/Key/Key.module.scss +6 -0
- package/plugins/map-key/src/components/Key/KeyItem.jsx +5 -8
- package/plugins/map-key/src/components/Key/KeySvg.jsx +21 -22
- package/plugins/map-key/src/components/Key/KeySvgLine.jsx +3 -9
- package/plugins/map-key/src/components/Key/KeySvgPattern.jsx +2 -7
- package/plugins/map-key/src/components/Key/KeySvgPattern.test.jsx +6 -0
- package/plugins/map-key/src/components/Key/KeySvgRect.jsx +4 -11
- package/plugins/map-key/src/components/Key/KeySvgSymbol.jsx +5 -11
- package/plugins/map-key/src/components/Key/KeySvgSymbol.test.jsx +12 -0
- package/plugins/map-key/src/components/Key/MapKey.jsx +5 -2
- package/plugins/map-key/src/components/Key/MapKey.test.jsx +28 -11
- package/plugins/menu/dist/css/index.css +1 -80
- package/plugins/search/dist/css/index.css +1 -1
- package/providers/beta/esri/dist/css/index.css +1 -34
- package/rollup.esm.mjs +2 -1
- package/scripts/publish-package.sh +8 -0
- package/src/App/components/KeyboardHelp/KeyboardHelp.jsx +25 -23
- package/src/App/components/KeyboardHelp/KeyboardHelp.test.jsx +22 -5
- package/src/App/components/Panel/Panel.jsx +50 -8
- package/src/App/components/Panel/Panel.module.scss +31 -7
- package/src/App/components/Panel/Panel.test.jsx +81 -2
- package/src/App/components/Tabs/Tabs.jsx +43 -17
- package/src/App/components/Tabs/Tabs.module.scss +32 -5
- package/src/App/components/Tabs/Tabs.test.jsx +40 -10
- package/src/App/renderer/groupByKey.js +28 -0
- package/src/App/renderer/groupByKey.test.js +39 -0
- package/src/App/renderer/groupIntoTabs.js +42 -0
- package/src/App/renderer/groupIntoTabs.test.js +95 -0
- package/src/App/renderer/mapButtons.js +74 -122
- package/src/App/renderer/mapButtons.test.js +51 -80
- package/src/App/renderer/mapControls.js +1 -0
- package/src/App/renderer/mapControls.test.js +16 -0
- package/src/App/renderer/mapPanels.js +26 -19
- package/src/App/renderer/mapPanels.test.js +44 -0
- package/src/config/appConfig.js +2 -2
- package/src/types.js +20 -2
- package/webpack.umd.mjs +6 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React from 'react'
|
|
2
|
-
import { mapButtons, getMatchingButtons, applySlotExclusivity, SlotButton
|
|
2
|
+
import { mapButtons, getMatchingButtons, applySlotExclusivity, SlotButton } from './mapButtons.js'
|
|
3
3
|
import { logger } from '../../services/logger.js'
|
|
4
4
|
import { getPanelConfig } from '../registry/panelRegistry.js'
|
|
5
5
|
|
|
@@ -47,52 +47,6 @@ describe('mapButtons module', () => {
|
|
|
47
47
|
getPanelConfig.mockReturnValue({})
|
|
48
48
|
})
|
|
49
49
|
|
|
50
|
-
// -------------------------
|
|
51
|
-
// resolveGroup* helper tests
|
|
52
|
-
// -------------------------
|
|
53
|
-
describe('resolveGroupName', () => {
|
|
54
|
-
it('returns null when group is null or undefined', () => {
|
|
55
|
-
expect(resolveGroupName(null)).toBeNull()
|
|
56
|
-
expect(resolveGroupName(undefined)).toBeNull()
|
|
57
|
-
})
|
|
58
|
-
it('returns the string when group is a string', () => {
|
|
59
|
-
expect(resolveGroupName('g1')).toBe('g1')
|
|
60
|
-
})
|
|
61
|
-
it('returns group.name when group is an object', () => {
|
|
62
|
-
expect(resolveGroupName({ name: 'g1' })).toBe('g1')
|
|
63
|
-
expect(resolveGroupName({ name: undefined })).toBeNull()
|
|
64
|
-
})
|
|
65
|
-
})
|
|
66
|
-
|
|
67
|
-
describe('resolveGroupLabel', () => {
|
|
68
|
-
it('returns empty string when group is falsy', () => {
|
|
69
|
-
expect(resolveGroupLabel(null)).toBe('')
|
|
70
|
-
expect(resolveGroupLabel(undefined)).toBe('')
|
|
71
|
-
})
|
|
72
|
-
it('returns the string itself when group is a string', () => {
|
|
73
|
-
expect(resolveGroupLabel('My Group')).toBe('My Group')
|
|
74
|
-
})
|
|
75
|
-
it('returns group.label when provided, else group.name, else empty string', () => {
|
|
76
|
-
expect(resolveGroupLabel({ name: 'g1', label: 'Group One' })).toBe('Group One')
|
|
77
|
-
expect(resolveGroupLabel({ name: 'g1' })).toBe('g1')
|
|
78
|
-
expect(resolveGroupLabel({ order: 5 })).toBe('')
|
|
79
|
-
})
|
|
80
|
-
})
|
|
81
|
-
|
|
82
|
-
describe('resolveGroupOrder', () => {
|
|
83
|
-
it('returns 0 when group is falsy', () => {
|
|
84
|
-
expect(resolveGroupOrder(null)).toBe(0)
|
|
85
|
-
expect(resolveGroupOrder(undefined)).toBe(0)
|
|
86
|
-
})
|
|
87
|
-
it('returns 0 when group is a string', () => {
|
|
88
|
-
expect(resolveGroupOrder('g1')).toBe(0)
|
|
89
|
-
})
|
|
90
|
-
it('returns group.order when provided, else 0', () => {
|
|
91
|
-
expect(resolveGroupOrder({ name: 'g1', order: 5 })).toBe(5)
|
|
92
|
-
expect(resolveGroupOrder({ name: 'g1' })).toBe(0)
|
|
93
|
-
})
|
|
94
|
-
})
|
|
95
|
-
|
|
96
50
|
// -------------------------
|
|
97
51
|
// getMatchingButtons tests
|
|
98
52
|
// -------------------------
|
|
@@ -283,68 +237,85 @@ describe('mapButtons module', () => {
|
|
|
283
237
|
expect(result[0]).toMatchObject({ id: 'b1', type: 'button', order: 1 })
|
|
284
238
|
})
|
|
285
239
|
|
|
286
|
-
it('renders grouped buttons as a single group item with role=group', () => {
|
|
240
|
+
it('renders grouped buttons as a single group item with role=group, keyed by kebab-cased label', () => {
|
|
287
241
|
appState.buttonConfig = ({
|
|
288
|
-
b1: { ...baseBtn, group: {
|
|
289
|
-
b2: { ...baseBtn, desktop: { slot: 'header', order: 2 }, group: {
|
|
242
|
+
b1: { ...baseBtn, group: { label: 'Group 1', slotOrder: 2 } },
|
|
243
|
+
b2: { ...baseBtn, desktop: { slot: 'header', order: 2 }, group: { label: 'Group 1', slotOrder: 2 } }
|
|
290
244
|
})
|
|
291
245
|
const result = map()
|
|
292
246
|
expect(result).toHaveLength(1)
|
|
293
|
-
|
|
247
|
+
// stringToKebab only hyphenates camelCase boundaries, not spaces — this id is an internal
|
|
248
|
+
// React key/identifier only, never rendered as a literal DOM id, so that's harmless here.
|
|
249
|
+
expect(result[0]).toMatchObject({ id: 'group-group 1', type: 'group', order: 2 })
|
|
294
250
|
expect(result[0].element.props.role).toBe('group')
|
|
295
251
|
expect(result[0].element.props['aria-label']).toBe('Group 1')
|
|
296
252
|
})
|
|
297
253
|
|
|
298
|
-
it('
|
|
254
|
+
it('merges group labels that differ only by case/whitespace into one group', () => {
|
|
299
255
|
appState.buttonConfig = ({
|
|
300
|
-
b1: { ...baseBtn, group: {
|
|
301
|
-
b2: { ...baseBtn, group: {
|
|
256
|
+
b1: { ...baseBtn, group: { label: 'Zoom Controls' } },
|
|
257
|
+
b2: { ...baseBtn, group: { label: 'zoom controls' } }
|
|
302
258
|
})
|
|
303
259
|
const result = map()
|
|
304
|
-
expect(result
|
|
260
|
+
expect(result).toHaveLength(1)
|
|
261
|
+
// First-encountered member's raw label wins, same convention as panel tabs (groupIntoTabs.js)
|
|
262
|
+
expect(result[0].element.props['aria-label']).toBe('Zoom Controls')
|
|
305
263
|
})
|
|
306
264
|
|
|
307
|
-
it('
|
|
265
|
+
it('orders group members via orderItems — unordered members keep natural sequence, an ordered one splices in by position', () => {
|
|
308
266
|
appState.buttonConfig = ({
|
|
309
|
-
b1: { ...baseBtn, group: {
|
|
310
|
-
b2: { ...baseBtn, group: {
|
|
311
|
-
b3: { ...baseBtn, group: {
|
|
267
|
+
b1: { ...baseBtn, group: { label: 'g1' }, desktop: { slot: 'header' } },
|
|
268
|
+
b2: { ...baseBtn, group: { label: 'g1' }, desktop: { slot: 'header', order: 1 } },
|
|
269
|
+
b3: { ...baseBtn, group: { label: 'g1' }, desktop: { slot: 'header' } }
|
|
312
270
|
})
|
|
313
|
-
const
|
|
314
|
-
expect(
|
|
315
|
-
const children = result[0].element.props.children
|
|
316
|
-
expect(children[0].props.buttonId).toBe('b2')
|
|
317
|
-
expect(children[1].props.buttonId).toBe('b3')
|
|
318
|
-
expect(children[2].props.buttonId).toBe('b1')
|
|
271
|
+
const children = map()[0].element.props.children
|
|
272
|
+
expect(children.map(c => c.props.buttonId)).toEqual(['b2', 'b1', 'b3'])
|
|
319
273
|
})
|
|
320
274
|
|
|
321
|
-
it('renders singleton groups as regular buttons using group slot order', () => {
|
|
322
|
-
appState.buttonConfig = ({ b1: { ...baseBtn, group: {
|
|
275
|
+
it('renders singleton groups as regular buttons using the group\'s slot order', () => {
|
|
276
|
+
appState.buttonConfig = ({ b1: { ...baseBtn, group: { label: 'Group 1', slotOrder: 3 } } })
|
|
323
277
|
const result = map()
|
|
324
278
|
expect(result).toHaveLength(1)
|
|
325
279
|
expect(result[0]).toMatchObject({ id: 'b1', type: 'button', order: 3 })
|
|
326
280
|
})
|
|
327
281
|
|
|
328
|
-
it('
|
|
329
|
-
appState.buttonConfig = ({ b1: { ...baseBtn, desktop: { slot: 'header', order: 4 }, group: {
|
|
330
|
-
expect(map()[0].order).toBe(
|
|
282
|
+
it('does not fall back to the button\'s own breakpoint order when group slotOrder is explicitly 0', () => {
|
|
283
|
+
appState.buttonConfig = ({ b1: { ...baseBtn, desktop: { slot: 'header', order: 4 }, group: { label: 'g1', slotOrder: 0 } } })
|
|
284
|
+
expect(map()[0].order).toBe(0)
|
|
331
285
|
})
|
|
332
286
|
|
|
333
|
-
it('falls back to 0 for singleton group when
|
|
334
|
-
appState.buttonConfig = ({ b1: { ...baseBtn, desktop: { slot: 'header' }, group: {
|
|
287
|
+
it('falls back to 0 for singleton group when group slotOrder is absent', () => {
|
|
288
|
+
appState.buttonConfig = ({ b1: { ...baseBtn, desktop: { slot: 'header' }, group: { label: 'g1' } } })
|
|
335
289
|
expect(map()[0].order).toBe(0)
|
|
336
290
|
})
|
|
337
291
|
|
|
338
|
-
it('
|
|
292
|
+
it('warns in dev mode when group members declare inconsistent slotOrder values, using the first one encountered', () => {
|
|
339
293
|
appState.buttonConfig = ({
|
|
340
|
-
b1: { ...baseBtn, group: {
|
|
341
|
-
b2: { ...baseBtn,
|
|
342
|
-
b3: { ...baseBtn, group: { name: 'g1', order: 0 }, desktop: { slot: 'header', order: 1 } }
|
|
294
|
+
b1: { ...baseBtn, group: { label: 'g1', slotOrder: 2 } },
|
|
295
|
+
b2: { ...baseBtn, desktop: { slot: 'header', order: 1 }, group: { label: 'g1', slotOrder: 5 } }
|
|
343
296
|
})
|
|
344
|
-
const
|
|
345
|
-
expect(
|
|
346
|
-
expect(
|
|
347
|
-
|
|
297
|
+
const result = map()
|
|
298
|
+
expect(result[0].order).toBe(2)
|
|
299
|
+
expect(logger.warn).toHaveBeenCalledWith(expect.stringContaining('inconsistent slotOrder values'))
|
|
300
|
+
})
|
|
301
|
+
|
|
302
|
+
it('does not warn when group members agree on slotOrder', () => {
|
|
303
|
+
appState.buttonConfig = ({
|
|
304
|
+
b1: { ...baseBtn, group: { label: 'g1', slotOrder: 2 } },
|
|
305
|
+
b2: { ...baseBtn, desktop: { slot: 'header', order: 1 }, group: { label: 'g1', slotOrder: 2 } }
|
|
306
|
+
})
|
|
307
|
+
map()
|
|
308
|
+
expect(logger.warn).not.toHaveBeenCalled()
|
|
309
|
+
})
|
|
310
|
+
|
|
311
|
+
it('mixes grouped and ungrouped buttons in the same slot', () => {
|
|
312
|
+
appState.buttonConfig = ({
|
|
313
|
+
solo: { ...baseBtn, desktop: { slot: 'header', order: 1 } },
|
|
314
|
+
b1: { ...baseBtn, group: { label: 'g1' }, desktop: { slot: 'header' } },
|
|
315
|
+
b2: { ...baseBtn, group: { label: 'g1' }, desktop: { slot: 'header' } }
|
|
316
|
+
})
|
|
317
|
+
const result = map()
|
|
318
|
+
expect(result.map(r => r.id).sort()).toEqual(['group-g1', 'solo'])
|
|
348
319
|
})
|
|
349
320
|
|
|
350
321
|
it('falls back to order 0 when order is not specified in breakpoint config', () => {
|
|
@@ -180,4 +180,20 @@ describe('mapControls', () => {
|
|
|
180
180
|
const result = mapControls({ slot: 'header', appState: defaultAppState, evaluateProp: (p) => p })
|
|
181
181
|
expect(result).toEqual([])
|
|
182
182
|
})
|
|
183
|
+
|
|
184
|
+
it('passes through the tab field from the breakpoint config', () => {
|
|
185
|
+
defaultAppState.controlConfig = ({
|
|
186
|
+
ctrl1: { id: 'ctrl1', desktop: { slot: 'map-styles-panel', tab: 'Styles' }, includeModes: ['view'] }
|
|
187
|
+
})
|
|
188
|
+
const result = mapControls({ slot: 'map-styles-panel', appState: defaultAppState, evaluateProp: (p) => p })
|
|
189
|
+
expect(result[0].tab).toBe('Styles')
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
it('leaves tab undefined when not set on the breakpoint config', () => {
|
|
193
|
+
defaultAppState.controlConfig = ({
|
|
194
|
+
ctrl1: { id: 'ctrl1', desktop: { slot: 'header', order: 1 }, includeModes: ['view'] }
|
|
195
|
+
})
|
|
196
|
+
const result = mapControls({ slot: 'header', appState: defaultAppState, evaluateProp: (p) => p })
|
|
197
|
+
expect(result[0].tab).toBeUndefined()
|
|
198
|
+
})
|
|
183
199
|
})
|
|
@@ -6,6 +6,7 @@ import { allowedSlots } from './slots.js'
|
|
|
6
6
|
import { resolveTargetSlot, isModeAllowed, isConsumerHtml } from './slotHelpers.js'
|
|
7
7
|
import { mapControls } from './mapControls.js'
|
|
8
8
|
import { orderItems } from './orderItems.js'
|
|
9
|
+
import { groupIntoTabs } from './groupIntoTabs.js'
|
|
9
10
|
import { stringToKebab } from '../../utils/stringToKebab.js'
|
|
10
11
|
import { logger } from '../../services/logger.js'
|
|
11
12
|
|
|
@@ -48,7 +49,7 @@ export function mapPanels ({ slot, appState, evaluateProp }) {
|
|
|
48
49
|
const cfg = panelConfig[panelId]?.[breakpoint]
|
|
49
50
|
return cfg?.modal
|
|
50
51
|
})
|
|
51
|
-
const allowedModalPanelId = modalPanels.length > 0 ? modalPanels[modalPanels.length - 1][0] : null
|
|
52
|
+
const allowedModalPanelId = modalPanels.length > 0 ? modalPanels[modalPanels.length - 1][0] : null // NOSONAR, .at() is only Chrome 90+
|
|
52
53
|
|
|
53
54
|
return openPanelEntries.map(([panelId, { props, focusOnOpen }]) => {
|
|
54
55
|
const config = panelConfig[panelId]
|
|
@@ -78,6 +79,7 @@ export function mapPanels ({ slot, appState, evaluateProp }) {
|
|
|
78
79
|
const pluginId = plugin?.id
|
|
79
80
|
|
|
80
81
|
const html = pluginId ? evaluateProp(config.html, pluginId) : config.html
|
|
82
|
+
const label = evaluateProp(config.label, pluginId)
|
|
81
83
|
|
|
82
84
|
return {
|
|
83
85
|
id: panelId,
|
|
@@ -90,8 +92,8 @@ export function mapPanels ({ slot, appState, evaluateProp }) {
|
|
|
90
92
|
panelConfig={config}
|
|
91
93
|
props={props}
|
|
92
94
|
focusOnOpen={focusOnOpen}
|
|
93
|
-
|
|
94
|
-
label={
|
|
95
|
+
{...buildPanelBody({ panelId, config, bpConfig, props, plugin, pluginId, html, label, appState, evaluateProp })}
|
|
96
|
+
label={label}
|
|
95
97
|
html={html}
|
|
96
98
|
/>
|
|
97
99
|
)
|
|
@@ -101,12 +103,16 @@ export function mapPanels ({ slot, appState, evaluateProp }) {
|
|
|
101
103
|
}
|
|
102
104
|
|
|
103
105
|
/**
|
|
104
|
-
* Builds
|
|
105
|
-
*
|
|
106
|
-
*
|
|
106
|
+
* Builds a panel's body: its own render content (if any) plus any controls registered against
|
|
107
|
+
* its `<panelId>-panel` slot by other plugins, merged with `orderItems` — or, when two or more
|
|
108
|
+
* distinct `tab`s are present among them, grouped into tabs instead (see `groupIntoTabs`).
|
|
109
|
+
* Static-html panels don't build a body — dangerouslySetInnerHTML can't host injected content,
|
|
107
110
|
* so any controls targeting one are silently skipped (with a dev warning).
|
|
111
|
+
*
|
|
112
|
+
* @returns {{ items?: object[], tabs?: object[] }} spread directly onto `<Panel>` — exactly one
|
|
113
|
+
* of `items`/`tabs` is set (or neither, for a static-html panel).
|
|
108
114
|
*/
|
|
109
|
-
function
|
|
115
|
+
function buildPanelBody ({ panelId, config, bpConfig, props, plugin, pluginId, html, label, appState, evaluateProp }) {
|
|
110
116
|
const injectedItems = mapControls({
|
|
111
117
|
slot: `${stringToKebab(panelId)}-panel`,
|
|
112
118
|
appState,
|
|
@@ -118,21 +124,22 @@ function buildPanelBodyItems ({ panelId, config, props, plugin, pluginId, html,
|
|
|
118
124
|
if (process.env.NODE_ENV !== 'production' && injectedItems.length > 0) {
|
|
119
125
|
logger.warn(`Panel "${panelId}" uses static html — controls targeting its slot are not rendered.`)
|
|
120
126
|
}
|
|
121
|
-
return
|
|
127
|
+
return {}
|
|
122
128
|
}
|
|
123
129
|
|
|
124
|
-
|
|
125
|
-
|
|
130
|
+
let ownItem = null
|
|
131
|
+
if (config.render) {
|
|
132
|
+
const WrappedChild = withPluginContexts(config.render, {
|
|
133
|
+
...props,
|
|
134
|
+
pluginId,
|
|
135
|
+
pluginConfig: plugin?.config
|
|
136
|
+
})
|
|
137
|
+
ownItem = { id: panelId, order: 0, tab: bpConfig.tab, element: <WrappedChild {...props} /> }
|
|
126
138
|
}
|
|
127
139
|
|
|
128
|
-
const
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
pluginConfig: plugin?.config
|
|
132
|
-
})
|
|
140
|
+
const allItems = ownItem ? [ownItem, ...injectedItems] : injectedItems
|
|
141
|
+
|
|
142
|
+
const tabs = groupIntoTabs({ items: allItems, fallbackLabel: label })
|
|
133
143
|
|
|
134
|
-
return orderItems(
|
|
135
|
-
{ id: panelId, order: 0, element: <WrappedChild {...props} /> },
|
|
136
|
-
...injectedItems
|
|
137
|
-
])
|
|
144
|
+
return tabs ? { tabs } : { items: orderItems(allItems) }
|
|
138
145
|
}
|
|
@@ -170,6 +170,50 @@ describe('mapPanels', () => {
|
|
|
170
170
|
expect(logger.warn).toHaveBeenCalledWith(expect.stringContaining('p1'))
|
|
171
171
|
})
|
|
172
172
|
|
|
173
|
+
it('orders multiple injected controls even when the panel has no render (regression: previously returned mapControls output unordered)', () => {
|
|
174
|
+
mapControls.mockReturnValue([
|
|
175
|
+
{ id: 'first', order: 2, element: <span>first</span> },
|
|
176
|
+
{ id: 'second', order: 1, element: <span>second</span> }
|
|
177
|
+
])
|
|
178
|
+
defaultAppState.panelConfig = ({ p1: { desktop: { slot: 'header' }, includeModes: ['view'] } })
|
|
179
|
+
const result = map()
|
|
180
|
+
expect(result[0].element.props.items.map(i => i.id)).toEqual(['second', 'first'])
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
describe('tabs', () => {
|
|
184
|
+
it('does not build a tabs list when everything shares one (or no) tab', () => {
|
|
185
|
+
const renderFn = () => <div>child</div>
|
|
186
|
+
mapControls.mockReturnValue([{ id: 'injected1', order: 0, element: <span>injected</span> }])
|
|
187
|
+
defaultAppState.panelConfig = ({ p1: { ...baseConfig, render: renderFn } })
|
|
188
|
+
const result = map()
|
|
189
|
+
expect(result[0].element.props.tabs).toBeUndefined()
|
|
190
|
+
expect(result[0].element.props.items).toBeDefined()
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
it('groups the panel\'s own content and an injected control into separate tabs', () => {
|
|
194
|
+
const renderFn = () => <div>child</div>
|
|
195
|
+
mapControls.mockReturnValue([{ id: 'injected1', order: 0, tab: 'Injected', element: <span>injected</span> }])
|
|
196
|
+
defaultAppState.panelConfig = ({
|
|
197
|
+
p1: { ...baseConfig, render: renderFn, desktop: { slot: 'header', order: 1, tab: 'Own' } }
|
|
198
|
+
})
|
|
199
|
+
const result = map()
|
|
200
|
+
expect(result[0].element.props.items).toBeUndefined()
|
|
201
|
+
const tabNames = result[0].element.props.tabs.map(t => t.name)
|
|
202
|
+
expect(tabNames.sort()).toEqual(['Injected', 'Own'])
|
|
203
|
+
})
|
|
204
|
+
|
|
205
|
+
it('falls back to the panel\'s own label for its content\'s implicit tab', () => {
|
|
206
|
+
const renderFn = () => <div>child</div>
|
|
207
|
+
mapControls.mockReturnValue([{ id: 'injected1', order: 0, tab: 'Injected', element: <span>injected</span> }])
|
|
208
|
+
defaultAppState.panelConfig = ({
|
|
209
|
+
p1: { ...baseConfig, render: renderFn, label: 'Map styles' }
|
|
210
|
+
})
|
|
211
|
+
const result = map()
|
|
212
|
+
const tabNames = result[0].element.props.tabs.map(t => t.name)
|
|
213
|
+
expect(tabNames).toContain('Map styles')
|
|
214
|
+
})
|
|
215
|
+
})
|
|
216
|
+
|
|
173
217
|
it('returns correct structure and defaults', () => {
|
|
174
218
|
defaultAppState.panelConfig = ({ p1: { desktop: { slot: 'header' }, includeModes: ['view'] } })
|
|
175
219
|
const result = map()
|
package/src/config/appConfig.js
CHANGED
|
@@ -74,7 +74,7 @@ export const defaultAppConfig = {
|
|
|
74
74
|
desktop: buttonSlots
|
|
75
75
|
}, {
|
|
76
76
|
id: 'zoomIn',
|
|
77
|
-
group: {
|
|
77
|
+
group: { label: 'Zoom controls', slotOrder: 0 },
|
|
78
78
|
label: 'Zoom in',
|
|
79
79
|
iconId: 'plus',
|
|
80
80
|
keepFocus: true,
|
|
@@ -86,7 +86,7 @@ export const defaultAppConfig = {
|
|
|
86
86
|
desktop: buttonSlots
|
|
87
87
|
}, {
|
|
88
88
|
id: 'zoomOut',
|
|
89
|
-
group: {
|
|
89
|
+
group: { label: 'Zoom controls', slotOrder: 0 },
|
|
90
90
|
label: 'Zoom out',
|
|
91
91
|
iconId: 'minus',
|
|
92
92
|
keepFocus: true,
|
package/src/types.js
CHANGED
|
@@ -40,6 +40,15 @@
|
|
|
40
40
|
* different plugin. The panel's own content and every control targeting it are ordered together
|
|
41
41
|
* via `order`. Only supported by panels with a `render` component — a panel using static `html`
|
|
42
42
|
* can't host injected controls, since `dangerouslySetInnerHTML` owns its body's children.
|
|
43
|
+
*
|
|
44
|
+
* @property {string} [tab]
|
|
45
|
+
* Groups this item into a tab within its target panel. Two items share a tab by giving it the
|
|
46
|
+
* same `tab` string (case/whitespace-insensitive — compared kebab-cased); items with no `tab`
|
|
47
|
+
* share one implicit tab, labelled with the panel's own `label`. Tabs only render once this
|
|
48
|
+
* produces more than one distinct group — a panel with zero or one tab renders flat, with no
|
|
49
|
+
* tab UI at all. A tab's position and displayed label both come from whichever member ends up
|
|
50
|
+
* first once its own members are ordered by `order`, so there's no separate tab-order property
|
|
51
|
+
* to set. Like `order`, only respected for controls with a `render` component.
|
|
43
52
|
*/
|
|
44
53
|
|
|
45
54
|
/**
|
|
@@ -67,6 +76,11 @@
|
|
|
67
76
|
* @property {string} slot
|
|
68
77
|
* Slot identifier.
|
|
69
78
|
*
|
|
79
|
+
* @property {string} [tab]
|
|
80
|
+
* Groups the panel's own content into a tab alongside any controls injected into it — see
|
|
81
|
+
* `ControlBreakpointConfig.tab`. Optional even once tabs are active: the panel's own content
|
|
82
|
+
* falls back to an implicit tab labelled with the panel's `label` when omitted.
|
|
83
|
+
*
|
|
70
84
|
* @property {string} [width]
|
|
71
85
|
* Panel width.
|
|
72
86
|
*/
|
|
@@ -156,8 +170,12 @@
|
|
|
156
170
|
* @property {(context: PluginContext) => boolean} [excludeWhen]
|
|
157
171
|
* Callback to determine if the button should be excluded from rendering.
|
|
158
172
|
*
|
|
159
|
-
* @property {string} [group]
|
|
160
|
-
*
|
|
173
|
+
* @property {{ label: string, slotOrder?: number }} [group]
|
|
174
|
+
* Groups this button with any other button sharing the same `label` (kebab-cased) into one
|
|
175
|
+
* `role="group"` container, rendered as a single item in the slot. `label` is also the
|
|
176
|
+
* group's accessible name (aria-label) — buttons whose labels differ only by case/whitespace
|
|
177
|
+
* still merge into one group. `slotOrder` positions the group itself within the slot (defaults
|
|
178
|
+
* to 0); each button's own breakpoint `order` controls its position *within* the group.
|
|
161
179
|
*
|
|
162
180
|
* @property {(context: PluginContext) => boolean} [hiddenWhen]
|
|
163
181
|
* Callback to determine if the button should be hidden. Sets display: none if true.
|
package/webpack.umd.mjs
CHANGED
|
@@ -3,6 +3,7 @@ import { fileURLToPath } from 'url'
|
|
|
3
3
|
import fs from 'fs'
|
|
4
4
|
|
|
5
5
|
import MiniCssExtractPlugin from 'mini-css-extract-plugin'
|
|
6
|
+
import CssMinimizerPlugin from 'css-minimizer-webpack-plugin'
|
|
6
7
|
import RemoveEmptyScriptsPlugin from 'webpack-remove-empty-scripts'
|
|
7
8
|
import RemoveFilesPlugin from 'remove-files-webpack-plugin'
|
|
8
9
|
|
|
@@ -124,7 +125,11 @@ const createUMDConfig = (entryName, entryPath, libraryPath, outDir, isCore = fal
|
|
|
124
125
|
|
|
125
126
|
optimization: {
|
|
126
127
|
splitChunks: { chunks: () => false },
|
|
127
|
-
removeEmptyChunks: true
|
|
128
|
+
removeEmptyChunks: true,
|
|
129
|
+
// '...' keeps webpack's default JS minimizer (Terser); CssMinimizerPlugin
|
|
130
|
+
// is added alongside it since setting `minimizer` explicitly replaces the
|
|
131
|
+
// defaults otherwise.
|
|
132
|
+
minimizer: ['...', new CssMinimizerPlugin()]
|
|
128
133
|
}
|
|
129
134
|
}
|
|
130
135
|
}
|