@defra/interactive-map 0.0.46-alpha → 0.0.46-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.
Files changed (39) hide show
  1. package/dist/css/index.css +1 -1
  2. package/dist/esm/im-core.js +1 -1
  3. package/dist/umd/im-core.js +1 -1
  4. package/docs/api/map-style-config.md +7 -0
  5. package/docs/assets/images/select-feature.jpg +0 -0
  6. package/govuk-prototype-kit.config.json +1 -0
  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/api/editFeature.js +12 -5
  10. package/plugins/beta/draw-es/src/api/newPolygon.js +7 -2
  11. package/plugins/beta/draw-es/src/api/removeSafeZone.js +30 -0
  12. package/plugins/draw/dist/esm/im-draw-ol-adapter.js +1 -1
  13. package/plugins/draw/dist/umd/im-draw-ol-adapter.js +1 -1
  14. package/plugins/draw/dist/umd/im-draw-plugin.js +1 -1
  15. package/plugins/draw/src/adapters/openlayers/core/OLDrawManager.js +5 -0
  16. package/plugins/draw/src/adapters/openlayers/snap/snapEngine.js +10 -5
  17. package/plugins/draw/src/adapters/openlayers/snap/snapEngine.test.js +1 -0
  18. package/providers/beta/openlayers/dist/esm/im-openlayers-provider.js +1 -1
  19. package/providers/beta/openlayers/dist/umd/im-openlayers-provider.js +1 -1
  20. package/providers/beta/openlayers/src/openlayersProvider.js +1 -1
  21. package/providers/beta/openlayers/src/openlayersProvider.test.js +22 -0
  22. package/providers/beta/openlayers/src/utils/highlightFeatures.js +23 -11
  23. package/providers/beta/openlayers/src/utils/highlightFeatures.test.js +2 -0
  24. package/providers/beta/openlayers/src/utils/hoverCursor.js +6 -5
  25. package/providers/beta/openlayers/src/utils/hoverCursor.test.js +2 -1
  26. package/providers/beta/openlayers/src/utils/queryFeatures.js +9 -6
  27. package/providers/beta/openlayers/src/utils/queryFeatures.test.js +6 -3
  28. package/providers/beta/openlayers/src/utils/spatial.js +6 -15
  29. package/providers/beta/openlayers/src/utils/spatial.test.js +3 -9
  30. package/providers/beta/openlayers/src/utils/tileLayers.js +5 -0
  31. package/providers/beta/openlayers/src/utils/tileLayers.test.js +1 -1
  32. package/src/App/components/Attributions/Attributions.jsx +3 -1
  33. package/src/App/components/Attributions/Attributions.module.scss +7 -3
  34. package/src/App/components/Attributions/Attributions.test.jsx +8 -0
  35. package/src/App/hooks/useLayoutMeasurements.js +94 -29
  36. package/src/App/hooks/useLayoutMeasurements.test.js +55 -10
  37. package/src/App/layout/Layout.jsx +7 -3
  38. package/src/App/layout/layout.module.scss +30 -1
  39. package/src/types.js +5 -0
@@ -10,13 +10,23 @@ jest.mock('../store/appContext.js')
10
10
  jest.mock('../store/mapContext.js')
11
11
  jest.mock('../../utils/getSafeZoneInset.js')
12
12
 
13
- const el = (props = {}) => {
13
+ const el = ({ rect, ...props } = {}) => {
14
14
  const e = document.createElement('div')
15
15
  e.style.setProperty = jest.fn()
16
16
  Object.entries(props).forEach(([k, v]) => Object.defineProperty(e, k, { value: v, configurable: true }))
17
+ if (rect) {
18
+ e.getBoundingClientRect = () => ({ top: 0, left: 0, right: 0, bottom: 0, width: 0, height: 0, ...rect })
19
+ }
17
20
  return e
18
21
  }
19
22
 
23
+ // For firstElementChild-based reads (logo column width, attribution text natural width).
24
+ const appendChild = (parent, props = {}) => {
25
+ const child = el(props)
26
+ parent.appendChild(child)
27
+ return child
28
+ }
29
+
20
30
  const refs = (o = {}) => ({
21
31
  appContainerRef: { current: o.appContainer || el() },
22
32
  mainRef: { current: o.main === null ? null : el({ offsetHeight: 500, ...o.main }) },
@@ -105,19 +115,19 @@ describe('useLayoutMeasurements', () => {
105
115
 
106
116
  test.each([
107
117
  [
108
- 'no actions (height 0) — uses base gap',
118
+ 'no actions (height 0) — clears the bottom row',
109
119
  { main: { offsetHeight: 500 }, bottom: { offsetTop: 400 }, actions: { offsetTop: 450, offsetHeight: 0 } },
110
- '100px' // baseBottom = 500 - 400 - 0 = 100
120
+ '108px' // clearsBottomRow = 500 - 400 + dividerGap (8) = 108
111
121
  ],
112
122
  [
113
- 'actions floating above base gap (tablet/desktop) — uses actionsOffset + dividerGap',
123
+ 'actions floating above the row (tablet/desktop) — uses actionsOffset + dividerGap',
114
124
  { main: { offsetHeight: 500 }, bottom: { offsetTop: 440, offsetHeight: 20 }, actions: { offsetTop: 408, offsetHeight: 60 } },
115
- '100px' // actionsOffset = 500 - 408 = 92, 92 + 8 = 100 > baseBottom (40)
125
+ '100px' // actionsOffset = 500 - 408 = 92, 92 + 8 = 100 > clearsBottomRow (68)
116
126
  ],
117
127
  [
118
- 'base gap already larger than actionsOffset (mobile in-flow) — base gap wins',
128
+ 'row clearance already larger than actionsOffset (mobile in-flow) — row clearance wins',
119
129
  { main: { offsetHeight: 500 }, bottom: { offsetTop: 350 }, actions: { offsetTop: 430, offsetHeight: 40 } },
120
- '150px' // baseBottom = 150 > actionsOffset (70) + dividerGap (8) = 78
130
+ '158px' // clearsBottomRow = 500 - 350 + 8 = 158 > actionsOffset (70) + dividerGap (8) = 78
121
131
  ]
122
132
  ])('calculates --hint-bottom for %s', (_, refOverrides, expected) => {
123
133
  const { layoutRefs } = setup({ refs: refOverrides })
@@ -129,8 +139,8 @@ describe('useLayoutMeasurements', () => {
129
139
  const { layoutRefs } = setup()
130
140
  layoutRefs.actionsRef.current = null
131
141
  renderHook(() => useLayoutMeasurements())
132
- // actionsHeight = 0, falls back to baseBottom = 500 - 400 - 0 = 100
133
- expect(layoutRefs.appContainerRef.current.style.setProperty).toHaveBeenCalledWith('--hint-bottom', '100px')
142
+ // actionsHeight = 0, falls back to clearsBottomRow = 500 - 400 + dividerGap (8) = 108
143
+ expect(layoutRefs.appContainerRef.current.style.setProperty).toHaveBeenCalledWith('--hint-bottom', '108px')
134
144
  })
135
145
 
136
146
  test.each([
@@ -200,6 +210,41 @@ describe('useLayoutMeasurements', () => {
200
210
  expect(layoutRefs.appContainerRef.current.style.setProperty).toHaveBeenCalledWith('--left-offset-top', '48px')
201
211
  })
202
212
 
213
+ test('keeps attributions docked beside the logo when the natural text width fits', () => {
214
+ const { layoutRefs } = setup({ refs: { bottom: { rect: { left: 0 } } } })
215
+ appendChild(layoutRefs.bottomRef.current, {}) // logo column
216
+ appendChild(layoutRefs.bottomRef.current, { offsetWidth: 192, rect: { left: 115 } }) // bottom-right column
217
+ layoutRefs.bottomRef.current.appendChild(layoutRefs.attributionsRef.current) // sibling of both columns
218
+ appendChild(layoutRefs.attributionsRef.current, { scrollWidth: 150 }) // attribution text, fits
219
+ renderHook(() => useLayoutMeasurements())
220
+ expect(layoutRefs.attributionsRef.current.classList.contains('im-o-app__attributions--stacked')).toBe(false)
221
+ expect(layoutRefs.appContainerRef.current.style.setProperty).toHaveBeenCalledWith('--attributions-left', '115px')
222
+ // docked: attributions.offsetHeight (16) + dividerGap (8) - primaryGap (8) = 16 — the
223
+ // bled attribution creeps 16px back up into the row, so bottom-right needs pushing clear
224
+ expect(layoutRefs.appContainerRef.current.style.setProperty).toHaveBeenCalledWith('--bottom-right-clearance', '16px')
225
+ })
226
+
227
+ test('stacks attributions onto their own row when the text is wider than the space beside the logo', () => {
228
+ const { layoutRefs } = setup({ refs: { bottom: { rect: { left: 0 } } } })
229
+ appendChild(layoutRefs.bottomRef.current, {}) // logo column
230
+ appendChild(layoutRefs.bottomRef.current, { offsetWidth: 192, rect: { left: 115 } }) // bottom-right column
231
+ layoutRefs.bottomRef.current.appendChild(layoutRefs.attributionsRef.current) // sibling of both columns
232
+ appendChild(layoutRefs.attributionsRef.current, { scrollWidth: 250 }) // attribution text, too wide to fit
233
+ renderHook(() => useLayoutMeasurements())
234
+ expect(layoutRefs.attributionsRef.current.classList.contains('im-o-app__attributions--stacked')).toBe(true)
235
+ // stacked already has its own clearance (the stacked rule's own margin-top), so none needed here
236
+ expect(layoutRefs.appContainerRef.current.style.setProperty).toHaveBeenCalledWith('--bottom-right-clearance', '0px')
237
+ })
238
+
239
+ test('never gives bottom-right negative clearance when attributions is shorter than the bleed', () => {
240
+ const { layoutRefs } = setup({ refs: { bottom: { rect: { left: 0 } }, attributions: { offsetHeight: 0 } } })
241
+ appendChild(layoutRefs.bottomRef.current, {}) // logo column
242
+ appendChild(layoutRefs.bottomRef.current, { offsetWidth: 192, rect: { left: 115 } }) // bottom-right column
243
+ layoutRefs.bottomRef.current.appendChild(layoutRefs.attributionsRef.current) // sibling of both columns
244
+ renderHook(() => useLayoutMeasurements())
245
+ expect(layoutRefs.appContainerRef.current.style.setProperty).toHaveBeenCalledWith('--bottom-right-clearance', '0px')
246
+ })
247
+
203
248
  test('uses 0 when bottomRightRef current is null', () => {
204
249
  const { layoutRefs } = setup()
205
250
  layoutRefs.bottomRightRef.current = null
@@ -324,7 +369,7 @@ describe('useLayoutMeasurements', () => {
324
369
  const { layoutRefs } = setup()
325
370
  renderHook(() => useLayoutMeasurements())
326
371
  expect(useResizeObserver).toHaveBeenCalledWith(
327
- [layoutRefs.bannerRef, layoutRefs.mainRef, layoutRefs.headerRef, layoutRefs.topRef, layoutRefs.topLeftColRef, layoutRefs.topRightColRef, layoutRefs.actionsRef, layoutRefs.bottomRef, layoutRefs.bottomRightRef, layoutRefs.leftTopRef, layoutRefs.leftBottomRef, layoutRefs.rightTopRef, layoutRefs.rightBottomRef, layoutRefs.drawerRef, layoutRefs.leftRef, layoutRefs.rightRef],
372
+ [layoutRefs.bannerRef, layoutRefs.mainRef, layoutRefs.headerRef, layoutRefs.topRef, layoutRefs.topLeftColRef, layoutRefs.topRightColRef, layoutRefs.actionsRef, layoutRefs.bottomRef, layoutRefs.bottomRightRef, layoutRefs.attributionsRef, layoutRefs.leftTopRef, layoutRefs.leftBottomRef, layoutRefs.rightTopRef, layoutRefs.rightBottomRef, layoutRefs.drawerRef, layoutRefs.leftRef, layoutRefs.rightRef],
328
373
  expect.any(Function)
329
374
  )
330
375
  layoutRefs.appContainerRef.current.style.setProperty.mockClear()
@@ -97,9 +97,13 @@ export const Layout = () => {
97
97
  <div className='im-o-app__bottom-right' ref={layoutRefs.bottomRightRef}>
98
98
  <SlotRenderer slot={layoutSlots.BOTTOM_RIGHT} />
99
99
  </div>
100
- <div className='im-o-app__attributions' ref={layoutRefs.attributionsRef}>
101
- <Attributions />
102
- </div>
100
+ </div>
101
+ {/* A sibling of both bottom-cols (not nested in one) so a stacked attribution
102
+ (useLayoutMeasurements) can wrap onto its own full row and genuinely grow
103
+ .im-o-app__bottom, pushing the logo up — nesting it in a column can't do that,
104
+ since only .im-o-app__bottom's own flex-wrap can force a new line. */}
105
+ <div className='im-o-app__attributions' ref={layoutRefs.attributionsRef}>
106
+ <Attributions />
103
107
  </div>
104
108
  </div>
105
109
  <div className='im-o-app__drawer' ref={layoutRefs.drawerRef}>
@@ -261,7 +261,9 @@
261
261
  // ---------------------------------------------------
262
262
 
263
263
  .im-o-app__bottom {
264
+ position: relative; // anchor for the docked (absolutely-positioned) attributions
264
265
  display: flex;
266
+ flex-wrap: wrap; // lets a stacked attributions row wrap onto its own line
265
267
  justify-content: space-between;
266
268
  align-items: flex-end;
267
269
  z-index: -2; // Support masking the viewport
@@ -279,7 +281,9 @@
279
281
  gap: var(--divider-gap);
280
282
  }
281
283
 
282
- .im-o-app__bottom-col:last-child {
284
+ // :nth-child, not :last-child — .im-o-app__attributions (Layout.jsx) is a third sibling
285
+ // after both bottom-cols, so this is no longer literally the last child.
286
+ .im-o-app__bottom-col:nth-child(2) {
283
287
  position: relative; // anchor for absolutely-positioned attributions
284
288
  flex: 1 1 auto;
285
289
  flex-direction: column;
@@ -297,20 +301,45 @@
297
301
 
298
302
  .im-o-app__bottom-right {
299
303
  justify-content: flex-end;
304
+ // Docked attributions (below) bleeds past the row's own bottom edge and can grow back up
305
+ // into it — --bottom-right-clearance (useLayoutMeasurements) keeps this content clear.
306
+ margin-bottom: var(--bottom-right-clearance, 0);
300
307
  }
301
308
 
302
309
  // ---------------------------------------------------
303
310
  // Attributions:
304
311
  // ---------------------------------------------------
312
+ //
313
+ // Docked (default): absolutely positioned against .im-o-app__bottom (not the bottom-right
314
+ // column it's a DOM sibling of — see Layout.jsx), so it can bleed flush into the corner
315
+ // past .im-o-app__main's padding on bottom/right. `left` (--attributions-left, computed in
316
+ // useLayoutMeasurements from the bottom-right column's own measured position) caps it to
317
+ // that column's width, so it never overlaps the logo — long text wraps within that width.
318
+ //
319
+ // Stacked (im-o-app__attributions--stacked, toggled in useLayoutMeasurements when the
320
+ // copyright text no longer fits beside the logo at its natural width): back in normal flex
321
+ // flow as .im-o-app__bottom's third (wrapped) item, `flex-basis` forces it onto its own
322
+ // full-width row below both columns — genuinely growing .im-o-app__bottom's height, which
323
+ // (being bottom-pinned) pushes the logo and bottom-right buttons up rather than squeezing
324
+ // the text into an ever-narrower column. The oversized flex-basis plus negative
325
+ // right/bottom/left margin bleeds that row flush to the true edges the same way the docked
326
+ // box's absolute offsets do above.
305
327
 
306
328
  .im-o-app__attributions:not(:empty) {
307
329
  position: absolute;
308
330
  bottom: calc(var(--primary-gap) * -1);
309
331
  right: calc(var(--primary-gap) * -1);
332
+ left: var(--attributions-left, 0);
310
333
  display: flex;
311
334
  justify-content: flex-end;
312
335
  }
313
336
 
337
+ .im-o-app__attributions--stacked:not(:empty) {
338
+ position: static;
339
+ flex-basis: calc(100% + var(--primary-gap) * 2);
340
+ margin: var(--divider-gap) calc(var(--primary-gap) * -1) calc(var(--primary-gap) * -1) calc(var(--primary-gap) * -1);
341
+ }
342
+
314
343
  // ---------------------------------------------------
315
344
  // Drawer:
316
345
  // ---------------------------------------------------
package/src/types.js CHANGED
@@ -394,6 +394,11 @@
394
394
  * @property {string} [attribution]
395
395
  * Attribution text.
396
396
  *
397
+ * @property {boolean} [showAttributionOnMobile]
398
+ * Some basemap providers' terms require attribution to remain visible on all devices, while
399
+ * others are content for it to be hidden on mobile to save space. Attribution is hidden on
400
+ * mobile by default; set to `true` to keep it visible on mobile for this style.
401
+ *
397
402
  * @property {string} [backgroundColor]
398
403
  * CSS background color. Allows the viewport background to matche the background layer of the style.
399
404
  *