@defra/interactive-map 0.0.35-alpha → 0.0.37-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.
Files changed (55) hide show
  1. package/dist/css/index.css +1 -1
  2. package/dist/esm/im-core.js +1 -1
  3. package/dist/esm/im-shell.js +1 -1
  4. package/dist/umd/im-core.js +1 -1
  5. package/dist/umd/index.js +1 -1
  6. package/docs/api.md +15 -1
  7. package/package.json +1 -1
  8. package/plugins/beta/draw-es/dist/esm/im-draw-es-plugin.js +1 -1
  9. package/plugins/beta/draw-es/src/events.js +1 -1
  10. package/plugins/beta/map-styles/dist/esm/im-map-styles-plugin.js +1 -1
  11. package/plugins/beta/map-styles/dist/umd/im-map-styles-plugin.js +1 -1
  12. package/plugins/beta/map-styles/dist/umd/index.js +1 -1
  13. package/plugins/datasets/dist/css/index.css +1 -1
  14. package/plugins/datasets/dist/esm/esriLayerAdapter.js +1 -1
  15. package/plugins/datasets/dist/esm/im-datasets-plugin.js +1 -1
  16. package/plugins/datasets/dist/umd/im-datasets-esri-adapter.js +1 -1
  17. package/plugins/datasets/dist/umd/im-datasets-plugin.js +1 -1
  18. package/plugins/datasets/src/adapters/esri/esriLayerAdapter.js +20 -1
  19. package/plugins/datasets/src/adapters/esri/esriLayerAdapter.test.js +1 -0
  20. package/plugins/datasets/src/components/LayersMenu/Layers.module.scss +3 -0
  21. package/plugins/datasets/src/components/LayersMenu/LayersMenuGroupWrapper.jsx +3 -1
  22. package/plugins/datasets/src/components/LayersMenu/LayersRadioGroupWrapper.jsx +3 -1
  23. package/plugins/datasets/src/initialise/initialiseDatasets.js +2 -2
  24. package/plugins/datasets/src/reducers/pluginState.js +2 -0
  25. package/plugins/datasets/src/registry/datasetRegistry.js +1 -1
  26. package/plugins/interact/dist/esm/im-interact-plugin.js +1 -1
  27. package/plugins/interact/dist/umd/im-interact-plugin.js +1 -1
  28. package/plugins/interact/dist/umd/index.js +1 -1
  29. package/plugins/interact/src/hooks/useMapItemList.js +2 -2
  30. package/providers/beta/openlayers/src/utils/tileLayers.test.js +1 -1
  31. package/providers/maplibre/src/utils/queryFeatures.test.js +6 -6
  32. package/src/App/components/Actions/Actions.jsx +2 -2
  33. package/src/App/components/MoveControl/MoveControl.jsx +113 -0
  34. package/src/App/components/MoveControl/MoveControl.module.scss +121 -0
  35. package/src/App/components/MoveControl/MoveControl.test.jsx +196 -0
  36. package/src/App/components/Tabs/Tabs.jsx +1 -0
  37. package/src/App/components/Tabs/Tabs.test.jsx +11 -34
  38. package/src/App/controls/keyboardActions.js +4 -2
  39. package/src/App/hooks/useMarkersAPI.test.js +1 -1
  40. package/src/App/registry/pluginRegistry.test.js +2 -2
  41. package/src/App/renderer/mapButtons.test.js +8 -8
  42. package/src/App/renderer/mapControls.js +6 -1
  43. package/src/App/renderer/mapControls.test.js +24 -0
  44. package/src/App/renderer/slots.js +1 -0
  45. package/src/App/store/appActionsMap.js +8 -0
  46. package/src/App/store/appActionsMap.test.js +12 -0
  47. package/src/App/store/appReducer.js +1 -0
  48. package/src/App/store/appReducer.test.js +1 -0
  49. package/src/config/appConfig.js +41 -2
  50. package/src/config/appConfig.test.js +45 -4
  51. package/src/config/defaults.js +1 -0
  52. package/src/scss/main.scss +1 -0
  53. package/src/types.js +9 -1
  54. package/src/utils/resolveNudgeStep.js +2 -0
  55. package/src/utils/resolveNudgeStep.test.js +11 -0
@@ -152,6 +152,13 @@ const toggleHasExclusiveControl = (state, payload) => {
152
152
  }
153
153
  }
154
154
 
155
+ const toggleNudgeStep = (state) => {
156
+ return {
157
+ ...state,
158
+ nudgeStepSize: state.nudgeStepSize === 'small' ? 'large' : 'small'
159
+ }
160
+ }
161
+
155
162
  const setPluginsEvaluated = (state) =>
156
163
  state.arePluginsEvaluated ? state : { ...state, arePluginsEvaluated: true }
157
164
 
@@ -385,6 +392,7 @@ export const actionsMap = {
385
392
  RESTORE_PREVIOUS_PANELS: restorePreviousPanels,
386
393
  TOGGLE_APP_VISIBLE: toggleAppVisible,
387
394
  TOGGLE_HAS_EXCLUSIVE_CONTROL: toggleHasExclusiveControl,
395
+ TOGGLE_NUDGE_STEP: toggleNudgeStep,
388
396
  TOGGLE_BUTTON_DISABLED: toggleButtonDisabled,
389
397
  TOGGLE_BUTTON_HIDDEN: toggleButtonHidden,
390
398
  TOGGLE_BUTTON_PRESSED: toggleButtonPressed,
@@ -25,6 +25,7 @@ describe('actionsMap full coverage', () => {
25
25
  openPanels: { panel1: { props: {} } },
26
26
  previousOpenPanels: {},
27
27
  hasExclusiveControl: false,
28
+ nudgeStepSize: 'small',
28
29
  safeZoneInset: { top: 0, bottom: 0 },
29
30
  isLayoutReady: false,
30
31
  syncMapPadding: true,
@@ -138,6 +139,17 @@ describe('actionsMap full coverage', () => {
138
139
  expect(result.hasExclusiveControl).toBe(true)
139
140
  })
140
141
 
142
+ test('TOGGLE_NUDGE_STEP flips small to large', () => {
143
+ const result = actionsMap.TOGGLE_NUDGE_STEP(state)
144
+ expect(result.nudgeStepSize).toBe('large')
145
+ })
146
+
147
+ test('TOGGLE_NUDGE_STEP flips large to small', () => {
148
+ const s = { ...state, nudgeStepSize: 'large' }
149
+ const result = actionsMap.TOGGLE_NUDGE_STEP(s)
150
+ expect(result.nudgeStepSize).toBe('small')
151
+ })
152
+
141
153
  test('PLUGINS_EVALUATED is no-op when arePluginsEvaluated already true', () => {
142
154
  const s = { ...state, arePluginsEvaluated: true }
143
155
  expect(actionsMap.PLUGINS_EVALUATED(s)).toBe(s)
@@ -45,6 +45,7 @@ export const initialState = (config) => {
45
45
  pressedButtons: new Set(),
46
46
  expandedButtons: new Set(),
47
47
  hasExclusiveControl: false,
48
+ nudgeStepSize: 'large',
48
49
  openPanels,
49
50
  previousOpenPanels: {},
50
51
  listboxIsActive: false,
@@ -47,6 +47,7 @@ describe('initialState', () => {
47
47
  expect(result.preferredColorScheme).toBe('dark')
48
48
  expect(result.mode).toBe('edit')
49
49
  expect(result.panelConfig).toEqual({ panel1: {} })
50
+ expect(result.nudgeStepSize).toBe('large')
50
51
  })
51
52
 
52
53
  test('uses appColorScheme when autoColorScheme is false', () => {
@@ -1,4 +1,5 @@
1
1
  import { KeyboardHelp } from '../App/components/KeyboardHelp/KeyboardHelp.jsx'
2
+ import { MoveControl } from '../App/components/MoveControl/MoveControl.jsx'
2
3
 
3
4
  const keyboardBasePanelSlots = {
4
5
  slot: 'middle',
@@ -12,6 +13,10 @@ const buttonSlots = {
12
13
  showLabel: false
13
14
  }
14
15
 
16
+ const moveControlSlot = {
17
+ slot: 'right-bottom'
18
+ }
19
+
15
20
  const exitButtonSlots = {
16
21
  slot: 'top-left',
17
22
  showLabel: false
@@ -72,7 +77,7 @@ export const defaultAppConfig = {
72
77
  iconId: 'plus',
73
78
  keepFocus: true,
74
79
  onClick: (_e, { mapProvider, appConfig }) => mapProvider.zoomIn(appConfig.zoomDelta),
75
- excludeWhen: ({ appState, appConfig }) => !appConfig.enableZoomControls || appState.interfaceType === 'touch',
80
+ excludeWhen: ({ appState, appConfig }) => !appConfig.enableZoomControls || appConfig.enableMoveControl || appState.interfaceType === 'touch',
76
81
  enableWhen: ({ mapState }) => !mapState.isAtMaxZoom,
77
82
  mobile: buttonSlots,
78
83
  tablet: buttonSlots,
@@ -84,11 +89,26 @@ export const defaultAppConfig = {
84
89
  iconId: 'minus',
85
90
  keepFocus: true,
86
91
  onClick: (_e, { mapProvider, appConfig }) => mapProvider.zoomOut(appConfig.zoomDelta),
87
- excludeWhen: ({ appState, appConfig }) => !appConfig.enableZoomControls || appState.interfaceType === 'touch',
92
+ excludeWhen: ({ appState, appConfig }) => !appConfig.enableZoomControls || appConfig.enableMoveControl || appState.interfaceType === 'touch',
88
93
  enableWhen: ({ mapState }) => !mapState.isAtMinZoom,
89
94
  mobile: buttonSlots,
90
95
  tablet: buttonSlots,
91
96
  desktop: buttonSlots
97
+ }, {
98
+ id: 'moveControl',
99
+ label: 'Move and zoom controls',
100
+ iconId: 'move',
101
+ keepFocus: true,
102
+ isExpanded: false,
103
+ ariaControls: ({ appConfig }) => `${appConfig.id}-move-control-content`,
104
+ onClick: (_e, { appState }) => appState.dispatch({
105
+ type: 'TOGGLE_BUTTON_EXPANDED',
106
+ payload: { id: 'moveControl', isExpanded: !appState.expandedButtons.has('moveControl') }
107
+ }),
108
+ excludeWhen: ({ appConfig }) => !appConfig.enableMoveControl,
109
+ mobile: buttonSlots,
110
+ tablet: buttonSlots,
111
+ desktop: buttonSlots
92
112
  }],
93
113
 
94
114
  panels: [{
@@ -108,6 +128,16 @@ export const defaultAppConfig = {
108
128
  render: (props) => <KeyboardHelp context={props?.context} />
109
129
  }],
110
130
 
131
+ controls: [{
132
+ id: 'moveControl',
133
+ label: 'Move and zoom',
134
+ excludeWhen: ({ appConfig }) => !appConfig.enableMoveControl,
135
+ mobile: moveControlSlot,
136
+ tablet: moveControlSlot,
137
+ desktop: moveControlSlot,
138
+ render: MoveControl
139
+ }],
140
+
111
141
  icons: [{
112
142
  id: 'maximise',
113
143
  svgContent: '<path d="M15 3h6v6"/><path d="m21 3-7 7"/><path d="m3 21 7-7"/><path d="M9 21H3v-6"/>'
@@ -123,6 +153,15 @@ export const defaultAppConfig = {
123
153
  }, {
124
154
  id: 'chevron',
125
155
  svgContent: '<path d="m6 9 6 6 6-6"/>'
156
+ }, {
157
+ id: 'move',
158
+ svgContent: '<path d="M12 2v20"/><path d="m15 19-3 3-3-3"/><path d="m19 9 3 3-3 3"/><path d="M2 12h20"/><path d="m5 9-3 3 3 3"/><path d="m9 5 3-3 3 3"/>'
159
+ }, {
160
+ id: 'precision',
161
+ 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"/>'
162
+ }, {
163
+ id: 'precision-active',
164
+ svgContent: '<circle cx="12" cy="12" r="10"/><line x1="22" x2="16" y1="12" y2="12"/><line x1="8" x2="2" y1="12" y2="12"/><line x1="12" x2="12" y1="8" y2="2"/><line x1="12" x2="12" y1="22" y2="16"/><circle cx="12" cy="12" r="1.5" fill="currentColor" stroke="none"/>'
126
165
  }]
127
166
  }
128
167
 
@@ -19,6 +19,7 @@ describe('defaultAppConfig', () => {
19
19
  const journeyContinueBtn = buttons.find(b => b.id === 'journeyContinue')
20
20
  const zoomInBtn = buttons.find(b => b.id === 'zoomIn')
21
21
  const zoomOutBtn = buttons.find(b => b.id === 'zoomOut')
22
+ const moveControlBtn = buttons.find(b => b.id === 'moveControl')
22
23
 
23
24
  // --- UI RENDER TESTS ---
24
25
  it('renders KeyboardHelp panel', () => {
@@ -143,15 +144,21 @@ describe('defaultAppConfig', () => {
143
144
  appState: { interfaceType: 'mouse' }
144
145
  })).toBe(true)
145
146
 
146
- // Branch B: Second part of OR is true (interfaceType === 'touch')
147
+ // Branch B: Second part of OR is true (enableMoveControl)
147
148
  expect(btn.excludeWhen({
148
- appConfig: { enableZoomControls: true },
149
+ appConfig: { enableZoomControls: true, enableMoveControl: true },
150
+ appState: { interfaceType: 'mouse' }
151
+ })).toBe(true)
152
+
153
+ // Branch C: Third part of OR is true (interfaceType === 'touch')
154
+ expect(btn.excludeWhen({
155
+ appConfig: { enableZoomControls: true, enableMoveControl: false },
149
156
  appState: { interfaceType: 'touch' }
150
157
  })).toBe(true)
151
158
 
152
- // Branch C: Both parts are false (Result: false)
159
+ // Branch D: All parts are false (Result: false)
153
160
  expect(btn.excludeWhen({
154
- appConfig: { enableZoomControls: true },
161
+ appConfig: { enableZoomControls: true, enableMoveControl: false },
155
162
  appState: { interfaceType: 'mouse' }
156
163
  })).toBe(false)
157
164
  })
@@ -175,6 +182,40 @@ describe('defaultAppConfig', () => {
175
182
  expect(mapProviderMock.zoomOut).toHaveBeenCalledWith(2)
176
183
  })
177
184
 
185
+ // --- MOVE CONTROL TOGGLE BUTTON ---
186
+ it('covers all branches of moveControl excludeWhen', () => {
187
+ expect(moveControlBtn.excludeWhen({ appConfig: { enableMoveControl: false } })).toBe(true)
188
+ expect(moveControlBtn.excludeWhen({ appConfig: { enableMoveControl: true } })).toBe(false)
189
+ })
190
+
191
+ it('moveControl ariaControls resolves the control element id', () => {
192
+ expect(moveControlBtn.ariaControls({ appConfig: { id: 'im' } })).toBe('im-move-control-content')
193
+ })
194
+
195
+ it('moveControl onClick toggles TOGGLE_BUTTON_EXPANDED based on current state', () => {
196
+ const dispatch = jest.fn()
197
+
198
+ moveControlBtn.onClick({}, { appState: { dispatch, expandedButtons: new Set() } })
199
+ expect(dispatch).toHaveBeenCalledWith({
200
+ type: 'TOGGLE_BUTTON_EXPANDED',
201
+ payload: { id: 'moveControl', isExpanded: true }
202
+ })
203
+
204
+ dispatch.mockClear()
205
+ moveControlBtn.onClick({}, { appState: { dispatch, expandedButtons: new Set(['moveControl']) } })
206
+ expect(dispatch).toHaveBeenCalledWith({
207
+ type: 'TOGGLE_BUTTON_EXPANDED',
208
+ payload: { id: 'moveControl', isExpanded: false }
209
+ })
210
+ })
211
+
212
+ // --- MOVE CONTROL ---
213
+ it('covers all branches of moveControl control excludeWhen', () => {
214
+ const control = defaultAppConfig.controls.find(c => c.id === 'moveControl')
215
+ expect(control.excludeWhen({ appConfig: { enableMoveControl: false } })).toBe(true)
216
+ expect(control.excludeWhen({ appConfig: { enableMoveControl: true } })).toBe(false)
217
+ })
218
+
178
219
  // --- SUPPLEMENTARY CONFIGS ---
179
220
  it('exports supplementary configs and constants', () => {
180
221
  expect(defaultButtonConfig.label).toBe('Button')
@@ -16,6 +16,7 @@ const defaults = {
16
16
  containerHeight: '600px',
17
17
  deviceNotSupportedText: 'Your device is not supported. A map is available with a more up-to-date browser or device.',
18
18
  enableFullscreen: false,
19
+ enableMoveControl: true,
19
20
  enableZoomControls: true,
20
21
  genericErrorText: 'There was a problem loading the map. Please try again later.',
21
22
  hasExitButton: false,
@@ -31,6 +31,7 @@
31
31
  @use '../App/components/Markers/Markers.module';
32
32
  @use '../App/components/Viewport/Features.module';
33
33
  @use '../App/components/Actions/Actions.module';
34
+ @use '../App/components/MoveControl/MoveControl.module';
34
35
 
35
36
  // Components that dont have a corresponding React Component
36
37
  @use 'components';
package/src/types.js CHANGED
@@ -685,8 +685,16 @@
685
685
  * @property {boolean} [enableFullscreen=false]
686
686
  * Whether a toggle fullscreen button is displayed.
687
687
  *
688
+ * @property {boolean} [enableMoveControl=true]
689
+ * Whether the move control is displayed — a button that reveals directional pan,
690
+ * zoom, and step-size buttons, providing a non-dragging alternative to panning
691
+ * and zooming the map (WCAG 2.5.7). Unlike enableZoomControls, it remains visible
692
+ * when the interface type is 'touch'. When enabled, enableZoomControls' buttons
693
+ * are hidden to avoid duplicating zoom controls.
694
+ *
688
695
  * @property {boolean} [enableZoomControls=true]
689
- * Whether zoom control buttons are displayed.
696
+ * Whether zoom control buttons are displayed. Not displayed when the interface
697
+ * type is 'touch', or when enableMoveControl is enabled.
690
698
  *
691
699
  * @property {[number, number, number, number]} [extent]
692
700
  * Initial extent [minX, minY, maxX, maxY]. Equivalent to bounds; use whichever matches your map provider's terminology.
@@ -0,0 +1,2 @@
1
+ export const resolveStepAmount = (isLargeStep, smallValue, largeValue) =>
2
+ isLargeStep ? largeValue : smallValue
@@ -0,0 +1,11 @@
1
+ import { resolveStepAmount } from './resolveNudgeStep.js'
2
+
3
+ describe('resolveStepAmount', () => {
4
+ it('returns the small value when isLargeStep is false', () => {
5
+ expect(resolveStepAmount(false, 5, 100)).toBe(5)
6
+ })
7
+
8
+ it('returns the large value when isLargeStep is true', () => {
9
+ expect(resolveStepAmount(true, 5, 100)).toBe(100)
10
+ })
11
+ })