@defra/interactive-map 0.0.48-fmp-2 → 0.0.49-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/umd/im-core.js +1 -1
- package/package.json +1 -1
- package/plugins/beta/draw-es/dist/esm/im-draw-es-plugin.js +1 -1
- package/plugins/beta/draw-es/src/graphic.js +1 -0
- package/plugins/datasets/dist/esm/esriLayerAdapter.js +1 -1
- package/plugins/datasets/dist/umd/im-datasets-esri-adapter.js +1 -1
- package/plugins/datasets/src/adapters/esri/esriLayerAdapter.js +19 -1
- package/plugins/datasets/src/adapters/esri/esriLayerAdapter.test.js +48 -0
- package/plugins/datasets/src/adapters/maplibre/registry/mapLibreDataset.test.js +8 -8
- package/plugins/datasets/src/reducers/__data__/demoDatasets.js +3 -3
- package/src/App/hooks/useLayoutMeasurements.js +28 -19
- package/src/App/hooks/useLayoutMeasurements.test.js +33 -5
- package/src/App/layout/layout.module.scss +10 -2
|
@@ -65,6 +65,8 @@ export default class EsriLayerAdapter extends LayerAdapter {
|
|
|
65
65
|
await _add(registryDataset)
|
|
66
66
|
}
|
|
67
67
|
}
|
|
68
|
+
// Reorder layers after adding the initially visible datasets
|
|
69
|
+
this._reorderLayers()
|
|
68
70
|
// Add the non-visible datasets next
|
|
69
71
|
for (const registryDataset of topLevelDatasets) {
|
|
70
72
|
if (registryDataset.visibility !== 'visible') {
|
|
@@ -87,7 +89,6 @@ export default class EsriLayerAdapter extends LayerAdapter {
|
|
|
87
89
|
|
|
88
90
|
_reorderLayers () {
|
|
89
91
|
// Order: baseLayer first, then datasets by their registry order, then sketch layers last
|
|
90
|
-
// Layers within a group layer are not reordered individually - they keep their existing relative order
|
|
91
92
|
const allLayers = this._map?.allLayers?.items
|
|
92
93
|
if (!allLayers?.length) {
|
|
93
94
|
return
|
|
@@ -96,6 +97,20 @@ export default class EsriLayerAdapter extends LayerAdapter {
|
|
|
96
97
|
const topLevelLayers = allLayers.filter(layer => layer.parent?.type !== 'group')
|
|
97
98
|
const orderedLayers = [...topLevelLayers].sort((a, b) => getLayerSortOrder(a) - getLayerSortOrder(b))
|
|
98
99
|
orderedLayers.forEach((layer, index) => this._map.reorder(layer, index))
|
|
100
|
+
// Finally reorder the items within the group layers
|
|
101
|
+
this._reorderGroupLayers()
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
_reorderGroupLayers () {
|
|
105
|
+
const groupLayers = Object.values(this._groupLayers)
|
|
106
|
+
groupLayers.forEach((groupLayer) => {
|
|
107
|
+
const children = groupLayer?.allLayers?.items || []
|
|
108
|
+
const orderedChildren = [...children]
|
|
109
|
+
.filter(Boolean)
|
|
110
|
+
.sort((a, b) => getLayerSortOrder(a) - getLayerSortOrder(b))
|
|
111
|
+
|
|
112
|
+
orderedChildren.forEach((layer, index) => groupLayer?.reorder?.(layer, index))
|
|
113
|
+
})
|
|
99
114
|
}
|
|
100
115
|
|
|
101
116
|
_addGroupLayer (esriGroupId) {
|
|
@@ -272,6 +287,9 @@ export default class EsriLayerAdapter extends LayerAdapter {
|
|
|
272
287
|
|
|
273
288
|
// mapLayer could be a VectorTileLayer or a FeatureLayer, depending on the dataset type
|
|
274
289
|
const mapLayer = this._mapVisibilityLayers[isSublayer ? parent.id : id]
|
|
290
|
+
if (!mapLayer) {
|
|
291
|
+
return
|
|
292
|
+
}
|
|
275
293
|
if (registryDataset.type === 'FeatureService') {
|
|
276
294
|
// FeatureLayers don't have style layers, so we don't need to apply style layer visibility or paint properties
|
|
277
295
|
mapLayer.renderer = registryDataset.renderer
|
|
@@ -21,10 +21,18 @@ jest.mock('@arcgis/core/layers/GroupLayer.js', () =>
|
|
|
21
21
|
return {
|
|
22
22
|
...opts,
|
|
23
23
|
layers,
|
|
24
|
+
allLayers: { items: layers },
|
|
24
25
|
add: jest.fn(layer => layers.push(layer)),
|
|
25
26
|
remove: jest.fn(layer => {
|
|
26
27
|
const idx = layers.indexOf(layer)
|
|
27
28
|
if (idx !== -1) layers.splice(idx, 1)
|
|
29
|
+
}),
|
|
30
|
+
reorder: jest.fn((layer, index) => {
|
|
31
|
+
const current = layers.indexOf(layer)
|
|
32
|
+
if (current !== -1) {
|
|
33
|
+
layers.splice(current, 1)
|
|
34
|
+
}
|
|
35
|
+
layers.splice(index, 0, layer)
|
|
28
36
|
})
|
|
29
37
|
}
|
|
30
38
|
})
|
|
@@ -349,6 +357,46 @@ describe('esriLayerAdapter', () => {
|
|
|
349
357
|
expect(map.reorder).toHaveBeenCalledWith(baseLayer, 0)
|
|
350
358
|
expect(map.reorder).toHaveBeenCalledWith(emptyGroupLayer, 1)
|
|
351
359
|
})
|
|
360
|
+
|
|
361
|
+
it('reorders the child layers within a group by registry order', () => {
|
|
362
|
+
datasetRegistry._orderedDatasets = ['roads', 'schools']
|
|
363
|
+
const roads = { id: 'roads' }
|
|
364
|
+
const schools = { id: 'schools' }
|
|
365
|
+
const sketch = { id: 'ketchLayer-0' }
|
|
366
|
+
const baseLayer = { id: 'baseLayer' }
|
|
367
|
+
const groupLayer = {
|
|
368
|
+
id: 'group-1',
|
|
369
|
+
type: 'group',
|
|
370
|
+
allLayers: { items: [schools, sketch, roads] },
|
|
371
|
+
reorder: jest.fn()
|
|
372
|
+
}
|
|
373
|
+
adapter._groupLayers[groupLayer.id] = groupLayer
|
|
374
|
+
map.allLayers = { items: [groupLayer, baseLayer] }
|
|
375
|
+
map.reorder = jest.fn()
|
|
376
|
+
|
|
377
|
+
adapter._reorderLayers()
|
|
378
|
+
|
|
379
|
+
expect(groupLayer.reorder).toHaveBeenNthCalledWith(1, roads, 0)
|
|
380
|
+
expect(groupLayer.reorder).toHaveBeenNthCalledWith(2, schools, 1)
|
|
381
|
+
expect(groupLayer.reorder).toHaveBeenNthCalledWith(3, sketch, 2)
|
|
382
|
+
})
|
|
383
|
+
|
|
384
|
+
it('Copes when a groupLayer has no items', () => {
|
|
385
|
+
datasetRegistry._orderedDatasets = ['roads', 'schools']
|
|
386
|
+
const baseLayer = { id: 'baseLayer' }
|
|
387
|
+
const groupLayer = {
|
|
388
|
+
id: 'group-1',
|
|
389
|
+
type: 'group',
|
|
390
|
+
reorder: jest.fn()
|
|
391
|
+
}
|
|
392
|
+
adapter._groupLayers[groupLayer.id] = groupLayer
|
|
393
|
+
map.allLayers = { items: [groupLayer, baseLayer] }
|
|
394
|
+
map.reorder = jest.fn()
|
|
395
|
+
|
|
396
|
+
adapter._reorderLayers()
|
|
397
|
+
|
|
398
|
+
expect(groupLayer.reorder).not.toHaveBeenCalled()
|
|
399
|
+
})
|
|
352
400
|
})
|
|
353
401
|
|
|
354
402
|
// ─── _addFeatureLayers ────────────────────────────────────────────────────────
|
|
@@ -212,12 +212,12 @@ describe('MapLibreDataset', () => {
|
|
|
212
212
|
|
|
213
213
|
it('returns a tiles-based id for a tile dataset (array tiles)', () => {
|
|
214
214
|
const dataset = datasetRegistry.getDataset('existing-fields')
|
|
215
|
-
expect(dataset.sourceId).toBe('tiles-
|
|
215
|
+
expect(dataset.sourceId).toBe('tiles-35m5lrb')
|
|
216
216
|
})
|
|
217
217
|
|
|
218
218
|
it('returns a tiles-based id when tiles is a plain string (line 76 non-array branch)', () => {
|
|
219
219
|
const dataset = datasetRegistry.getDataset('existing-fields')
|
|
220
|
-
expect(dataset.sourceId).toBe('tiles-
|
|
220
|
+
expect(dataset.sourceId).toBe('tiles-35m5lrb')
|
|
221
221
|
})
|
|
222
222
|
|
|
223
223
|
it('returns geojson-dynamic-{id} for a dynamic geojson source', () => {
|
|
@@ -242,7 +242,7 @@ describe('MapLibreDataset', () => {
|
|
|
242
242
|
it('returns a vector source for a tile dataset', () => {
|
|
243
243
|
expect(datasetRegistry.getDataset('existing-fields').source).toEqual({
|
|
244
244
|
type: 'vector',
|
|
245
|
-
tiles: 'https://farming-tiles-702a60f45633.herokuapp.com/
|
|
245
|
+
tiles: 'https://farming-tiles-702a60f45633.herokuapp.com/field_parcels_with_hedges_wgs84/{z}/{x}/{y}',
|
|
246
246
|
minzoom: 10,
|
|
247
247
|
maxzoom: 24
|
|
248
248
|
})
|
|
@@ -395,8 +395,8 @@ describe('MapLibreDataset', () => {
|
|
|
395
395
|
expect(datasetRegistry.getDataset('existing-fields').getFillSource({})).toEqual({
|
|
396
396
|
id: 'existing-fields',
|
|
397
397
|
type: 'fill',
|
|
398
|
-
source: 'tiles-
|
|
399
|
-
'source-layer': '
|
|
398
|
+
source: 'tiles-35m5lrb',
|
|
399
|
+
'source-layer': 'field_parcels_wgs84',
|
|
400
400
|
minzoom: 10,
|
|
401
401
|
maxzoom: 24,
|
|
402
402
|
layout: { visibility: 'visible' },
|
|
@@ -413,7 +413,7 @@ describe('MapLibreDataset', () => {
|
|
|
413
413
|
.toEqual({
|
|
414
414
|
id: 'hedge-control',
|
|
415
415
|
type: 'line',
|
|
416
|
-
source: 'tiles-
|
|
416
|
+
source: 'tiles-35m5lrb',
|
|
417
417
|
'source-layer': 'hedge_control',
|
|
418
418
|
minzoom: 10,
|
|
419
419
|
maxzoom: 24,
|
|
@@ -428,8 +428,8 @@ describe('MapLibreDataset', () => {
|
|
|
428
428
|
.toEqual({
|
|
429
429
|
id: 'existing-fields-stroke',
|
|
430
430
|
type: 'line',
|
|
431
|
-
source: 'tiles-
|
|
432
|
-
'source-layer': '
|
|
431
|
+
source: 'tiles-35m5lrb',
|
|
432
|
+
'source-layer': 'field_parcels_wgs84',
|
|
433
433
|
minzoom: 10,
|
|
434
434
|
maxzoom: 24,
|
|
435
435
|
layout: { visibility: 'visible' },
|
|
@@ -101,8 +101,8 @@ export const datasets = [
|
|
|
101
101
|
id: 'existing-fields', // NOSONAR
|
|
102
102
|
label: 'Existing fields',
|
|
103
103
|
filter: ['all', ['==', ['get', 'sbi'], '106223377'], ['==', ['get', 'is_dominant_land_cover'], true]],
|
|
104
|
-
tiles: 'https://farming-tiles-702a60f45633.herokuapp.com/
|
|
105
|
-
sourceLayer: '
|
|
104
|
+
tiles: 'https://farming-tiles-702a60f45633.herokuapp.com/field_parcels_with_hedges_wgs84/{z}/{x}/{y}',
|
|
105
|
+
sourceLayer: 'field_parcels_wgs84',
|
|
106
106
|
minZoom: 10,
|
|
107
107
|
maxZoom: 24,
|
|
108
108
|
showInKey: true,
|
|
@@ -158,7 +158,7 @@ export const datasets = [
|
|
|
158
158
|
}, {
|
|
159
159
|
id: 'hedge-control', // NOSONAR
|
|
160
160
|
label: 'Hedge control',
|
|
161
|
-
tiles: ['https://farming-tiles-702a60f45633.herokuapp.com/
|
|
161
|
+
tiles: ['https://farming-tiles-702a60f45633.herokuapp.com/field_parcels_with_hedges_wgs84/{z}/{x}/{y}'],
|
|
162
162
|
sourceLayer: 'hedge_control',
|
|
163
163
|
minZoom: 10,
|
|
164
164
|
maxZoom: 24,
|
|
@@ -17,12 +17,12 @@ const symmetricWidth = (left, right) => left || right ? Math.max(left, right) :
|
|
|
17
17
|
const subSlotMaxHeight = (columnHeight, siblingButtons, gap) => columnHeight - (siblingButtons ? siblingButtons + gap : 0)
|
|
18
18
|
|
|
19
19
|
// bottomRightHeight is 0 when empty, falling back to the attributions' own height for spacing.
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
//
|
|
23
|
-
//
|
|
24
|
-
const rightOffsetBottom = (containerPad, bottomRightHeight, attributionsHeight,
|
|
25
|
-
containerPad + (bottomRightHeight > 0 ? bottomRightHeight +
|
|
20
|
+
// rise is how far attributions has lifted .im-o-app__bottom-right above .im-o-app__bottom's
|
|
21
|
+
// own bottom edge (see attributionsRise below) — without adding it here, .im-o-app__right's
|
|
22
|
+
// computed bottom offset stays at the box's un-lifted position and the floating right column
|
|
23
|
+
// overlaps the now-raised bottom-right buttons.
|
|
24
|
+
const rightOffsetBottom = (containerPad, bottomRightHeight, attributionsHeight, rise, gap) =>
|
|
25
|
+
containerPad + (bottomRightHeight > 0 ? bottomRightHeight + rise + gap : attributionsHeight)
|
|
26
26
|
|
|
27
27
|
// Clears the bottom row's own TOP edge (not just its trailing gap below), so the hint
|
|
28
28
|
// never overlaps the logo/attribution row itself, plus a gap above it. That trivially
|
|
@@ -78,13 +78,22 @@ const attributionsNaturalWidth = (attributionsEl) => {
|
|
|
78
78
|
// longer fits, at its natural width, in the space beside the logo column.
|
|
79
79
|
const isAttributionsStacked = (naturalWidth, availableWidth) => naturalWidth > availableWidth
|
|
80
80
|
|
|
81
|
-
//
|
|
82
|
-
//
|
|
83
|
-
//
|
|
84
|
-
//
|
|
85
|
-
//
|
|
86
|
-
|
|
87
|
-
|
|
81
|
+
// How far attributions lifts .im-o-app__bottom-right above .im-o-app__bottom's own bottom
|
|
82
|
+
// edge. The same figure covers both modes, because both bleed primaryGap past that edge and
|
|
83
|
+
// then take attributionsHeight (plus a dividerGap) back up from there:
|
|
84
|
+
// - docked: the box grows upward from the bleed into the row itself, where
|
|
85
|
+
// .im-o-app__bottom-right shares the same horizontal space, so the row needs an explicit
|
|
86
|
+
// margin-bottom (--bottom-right-clearance) to stay clear of it;
|
|
87
|
+
// - stacked: the box is an extra flex line *below* the row, so it lifts the row by exactly
|
|
88
|
+
// that much on its own — no margin needed, hence bottomRightClearance's isStacked branch.
|
|
89
|
+
// Either way .im-o-app__right must clear the lift, so the offsets below use this, not the
|
|
90
|
+
// clearance. Clamped at 0: a box shorter than its own bleed lifts nothing.
|
|
91
|
+
const attributionsRise = (attributionsHeight, dividerGap, primaryGap) =>
|
|
92
|
+
Math.max(0, attributionsHeight + dividerGap - primaryGap)
|
|
93
|
+
|
|
94
|
+
// Stacked gets its lift from the extra flex line itself (see attributionsRise above), so only
|
|
95
|
+
// docked needs the row pushed up by a margin.
|
|
96
|
+
const bottomRightClearance = (isStacked, rise) => isStacked ? 0 : rise
|
|
88
97
|
|
|
89
98
|
// Docks centred between the side columns when there's room, otherwise stacks full-width
|
|
90
99
|
// (mobile always stacks). Sets the banner's own CSS vars and returns what the side-column
|
|
@@ -129,9 +138,9 @@ function applyAttributionsLayout ({ appContainer, bottom, attributions, dividerG
|
|
|
129
138
|
// space-between` gap between the two columns isn't a fixed value (unlike `gap`) to reconstruct.
|
|
130
139
|
const left = attributionsCol ? Math.round(attributionsCol.getBoundingClientRect().left - bottom.getBoundingClientRect().left) : 0
|
|
131
140
|
appContainer.style.setProperty('--attributions-left', `${left}px`)
|
|
132
|
-
const
|
|
133
|
-
appContainer.style.setProperty('--bottom-right-clearance', `${
|
|
134
|
-
return {
|
|
141
|
+
const rise = attributionsRise(attributions.offsetHeight, dividerGap, primaryGap)
|
|
142
|
+
appContainer.style.setProperty('--bottom-right-clearance', `${bottomRightClearance(isStacked, rise)}px`)
|
|
143
|
+
return { rise }
|
|
135
144
|
}
|
|
136
145
|
|
|
137
146
|
/**
|
|
@@ -179,7 +188,7 @@ function calculateLayout (layoutRefs, breakpoint) {
|
|
|
179
188
|
? bannerTop + bannerHeight + dividerGap
|
|
180
189
|
: colHeight + top.offsetTop
|
|
181
190
|
|
|
182
|
-
const {
|
|
191
|
+
const { rise: attributionsLift } = applyAttributionsLayout({ appContainer, bottom, attributions, dividerGap, primaryGap })
|
|
183
192
|
|
|
184
193
|
// === Left container offsets ===
|
|
185
194
|
const leftOffsetTop = sideOffsetTop(topLeftCol.offsetHeight)
|
|
@@ -192,10 +201,10 @@ function calculateLayout (layoutRefs, breakpoint) {
|
|
|
192
201
|
const bottomRightHeight = buttonHeight(bottomRightRef)
|
|
193
202
|
const bottomContainerPad = main.offsetHeight - bottom.offsetTop - bottom.offsetHeight
|
|
194
203
|
const rightOffsetTop = sideOffsetTop(topRightCol.offsetHeight)
|
|
195
|
-
const rightEffectiveBottom = bottom.offsetTop + bottom.offsetHeight - bottomRightHeight -
|
|
204
|
+
const rightEffectiveBottom = bottom.offsetTop + bottom.offsetHeight - bottomRightHeight - attributionsLift
|
|
196
205
|
const rightColumnHeight = rightEffectiveBottom - rightOffsetTop - dividerGap
|
|
197
206
|
appContainer.style.setProperty('--right-offset-top', `${rightOffsetTop}px`)
|
|
198
|
-
appContainer.style.setProperty('--right-offset-bottom', `${rightOffsetBottom(bottomContainerPad, bottomRightHeight, attributions.offsetHeight,
|
|
207
|
+
appContainer.style.setProperty('--right-offset-bottom', `${rightOffsetBottom(bottomContainerPad, bottomRightHeight, attributions.offsetHeight, attributionsLift, dividerGap)}px`)
|
|
199
208
|
appContainer.style.setProperty('--right-top-max-height', `${rightColumnHeight}px`)
|
|
200
209
|
|
|
201
210
|
// === Keyboard hint bottom offset ===
|
|
@@ -235,7 +235,7 @@ describe('useLayoutMeasurements', () => {
|
|
|
235
235
|
appendChild(layoutRefs.attributionsRef.current, { scrollWidth: 250 }) // attribution text, too wide to fit
|
|
236
236
|
renderHook(() => useLayoutMeasurements())
|
|
237
237
|
expect(layoutRefs.attributionsRef.current.classList.contains('im-o-app__attributions--stacked')).toBe(true)
|
|
238
|
-
// stacked
|
|
238
|
+
// stacked is lifted by its own extra flex line, not a margin, so no clearance is needed here
|
|
239
239
|
expect(layoutRefs.appContainerRef.current.style.setProperty).toHaveBeenCalledWith('--bottom-right-clearance', '0px')
|
|
240
240
|
})
|
|
241
241
|
|
|
@@ -263,27 +263,55 @@ describe('useLayoutMeasurements', () => {
|
|
|
263
263
|
})
|
|
264
264
|
renderHook(() => useLayoutMeasurements())
|
|
265
265
|
// bottomContainerPad = 500 - 400 - 0 = 100
|
|
266
|
-
//
|
|
266
|
+
// attributions lift = 16 (default attributions offsetHeight 16 + dividerGap 8 - primaryGap 8)
|
|
267
267
|
// expected = 100 + (20 + 16 + 8) = 144
|
|
268
268
|
expect(layoutRefs.appContainerRef.current.style.setProperty)
|
|
269
269
|
.toHaveBeenCalledWith('--right-offset-bottom', '144px')
|
|
270
270
|
})
|
|
271
271
|
|
|
272
|
-
test('adds
|
|
272
|
+
test('adds the attributions lift into right-offset-bottom so .im-o-app__right does not overlap a bottom-right box pushed up by tall docked attributions', () => {
|
|
273
273
|
const { layoutRefs } = setup({
|
|
274
274
|
refs: {
|
|
275
275
|
bottomRight: { offsetHeight: 20 },
|
|
276
|
-
attributions: { offsetHeight: 40 } //
|
|
276
|
+
attributions: { offsetHeight: 40 } // lift = 40 + 8 (dividerGap) - 8 (primaryGap) = 40
|
|
277
277
|
}
|
|
278
278
|
})
|
|
279
279
|
renderHook(() => useLayoutMeasurements())
|
|
280
280
|
// bottomContainerPad = 500 - 400 - 0 = 100; expected = 100 + (20 + 40 + 8) = 168
|
|
281
|
-
// Without the
|
|
281
|
+
// Without the lift term this regressed to 128px, letting .im-o-app__right's
|
|
282
282
|
// bottom offset sit below the (margin-bottom-raised) bottom-right box's real top edge.
|
|
283
283
|
expect(layoutRefs.appContainerRef.current.style.setProperty)
|
|
284
284
|
.toHaveBeenCalledWith('--right-offset-bottom', '168px')
|
|
285
285
|
})
|
|
286
286
|
|
|
287
|
+
// Stacked attributions takes a flex line of its own BELOW the bottom-right row, lifting the
|
|
288
|
+
// row by the same amount the docked box's margin-bottom does — but as part of
|
|
289
|
+
// .im-o-app__bottom's own grown height, not as a margin, so --bottom-right-clearance is 0.
|
|
290
|
+
// Reading the clearance here (rather than the mode-independent lift) left the right column's
|
|
291
|
+
// bottom offset at the un-lifted position, so a right-bottom scale bar overlapped a
|
|
292
|
+
// bottom-right draw menu on mobile — the breakpoint where attributions always stacks.
|
|
293
|
+
test('adds the attributions lift into right-offset-bottom when attributions is stacked, not just docked', () => {
|
|
294
|
+
const { layoutRefs } = setup({
|
|
295
|
+
refs: {
|
|
296
|
+
bottom: { rect: { left: 0 } },
|
|
297
|
+
bottomRight: { offsetHeight: 20 },
|
|
298
|
+
attributions: { offsetHeight: 40 } // lift = 40 + 8 (dividerGap) - 8 (primaryGap) = 40
|
|
299
|
+
}
|
|
300
|
+
})
|
|
301
|
+
appendChild(layoutRefs.bottomRef.current, {}) // logo column
|
|
302
|
+
appendChild(layoutRefs.bottomRef.current, { offsetWidth: 192, rect: { left: 115 } }) // bottom-right column
|
|
303
|
+
layoutRefs.bottomRef.current.appendChild(layoutRefs.attributionsRef.current) // sibling of both columns
|
|
304
|
+
appendChild(layoutRefs.attributionsRef.current, { scrollWidth: 250 }) // too wide to fit beside the logo
|
|
305
|
+
renderHook(() => useLayoutMeasurements())
|
|
306
|
+
const spy = layoutRefs.appContainerRef.current.style.setProperty
|
|
307
|
+
expect(layoutRefs.attributionsRef.current.classList.contains('im-o-app__attributions--stacked')).toBe(true)
|
|
308
|
+
expect(spy).toHaveBeenCalledWith('--bottom-right-clearance', '0px')
|
|
309
|
+
// Same 168px as the docked case above, despite the 0px clearance.
|
|
310
|
+
expect(spy).toHaveBeenCalledWith('--right-offset-bottom', '168px')
|
|
311
|
+
// rightEffectiveBottom = 400 + 0 - 20 - 40 = 340; 340 - 50 (rightOffsetTop) - 8 = 282
|
|
312
|
+
expect(spy).toHaveBeenCalledWith('--right-top-max-height', '282px')
|
|
313
|
+
})
|
|
314
|
+
|
|
287
315
|
test('uses 0 when sub-slot refs have null current', () => {
|
|
288
316
|
const { layoutRefs } = setup()
|
|
289
317
|
layoutRefs.leftTopRef.current = null
|
|
@@ -266,6 +266,12 @@
|
|
|
266
266
|
flex-wrap: wrap; // lets a stacked attributions row wrap onto its own line
|
|
267
267
|
justify-content: space-between;
|
|
268
268
|
align-items: flex-end;
|
|
269
|
+
// No effect on a single line (the common case), only between wrapped lines — covers both
|
|
270
|
+
// a too-wide .im-o-app__bottom-right dropping below the logo/bottom-left column, and the
|
|
271
|
+
// stacked attributions row below that (see its own comment further down: this same gap
|
|
272
|
+
// replaces what used to be its one-off margin-top, so a doubly-wrapped bottom stays evenly
|
|
273
|
+
// spaced rather than gapping only at the line the margin happened to be declared on).
|
|
274
|
+
row-gap: var(--divider-gap);
|
|
269
275
|
z-index: -2; // Support masking the viewport
|
|
270
276
|
}
|
|
271
277
|
|
|
@@ -323,7 +329,9 @@
|
|
|
323
329
|
// (being bottom-pinned) pushes the logo and bottom-right buttons up rather than squeezing
|
|
324
330
|
// the text into an ever-narrower column. The oversized flex-basis plus negative
|
|
325
331
|
// right/bottom/left margin bleeds that row flush to the true edges the same way the docked
|
|
326
|
-
// box's absolute offsets do above.
|
|
332
|
+
// box's absolute offsets do above; the gap above it comes from .im-o-app__bottom's own
|
|
333
|
+
// row-gap (not a margin-top here), so it stays exactly one gap even if .im-o-app__bottom-right
|
|
334
|
+
// has also wrapped onto a line of its own above it.
|
|
327
335
|
|
|
328
336
|
.im-o-app__attributions:not(:empty) {
|
|
329
337
|
position: absolute;
|
|
@@ -337,7 +345,7 @@
|
|
|
337
345
|
.im-o-app__attributions--stacked:not(:empty) {
|
|
338
346
|
position: static;
|
|
339
347
|
flex-basis: calc(100% + var(--primary-gap) * 2);
|
|
340
|
-
margin:
|
|
348
|
+
margin: 0 calc(var(--primary-gap) * -1) calc(var(--primary-gap) * -1);
|
|
341
349
|
}
|
|
342
350
|
|
|
343
351
|
// ---------------------------------------------------
|