@defra/interactive-map 0.0.48-alpha → 0.0.48-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/esm/im-core.js +1 -1
- package/dist/umd/im-core.js +1 -1
- package/package.json +1 -1
- package/plugins/datasets/dist/esm/esriLayerAdapter.js +1 -1
- package/plugins/datasets/dist/esm/im-datasets-plugin.js +1 -1
- package/plugins/datasets/dist/umd/im-datasets-esri-adapter.js +1 -1
- package/plugins/datasets/dist/umd/im-datasets-plugin.js +1 -1
- package/plugins/datasets/src/adapters/esri/esriLayerAdapter.js +42 -9
- package/plugins/datasets/src/adapters/esri/esriLayerAdapter.test.js +42 -4
- package/plugins/datasets/src/registry/datasetRegistry.js +6 -6
- 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/api/addSymbol.js +3 -0
- package/plugins/map-key/src/api/addSymbol.test.js +14 -0
- package/plugins/map-key/src/api/index.js +7 -0
- package/plugins/map-key/src/api/index.test.js +13 -0
- package/plugins/map-key/src/api/removeSymbol.js +3 -0
- package/plugins/map-key/src/api/removeSymbol.test.js +14 -0
- package/plugins/map-key/src/components/Key/MapKey.jsx +11 -3
- package/plugins/map-key/src/initialise/MapKeyInit.jsx +18 -3
- package/plugins/map-key/src/initialise/MapKeyInit.test.jsx +44 -8
- package/plugins/map-key/src/manifest.js +5 -1
- package/plugins/map-key/src/reducers/mergeKeyGroupItems.js +68 -0
- package/plugins/map-key/src/reducers/mergeKeyGroupItems.test.js +91 -0
- package/plugins/map-key/src/reducers/pluginState.js +68 -0
- package/plugins/map-key/src/reducers/pluginState.test.js +82 -0
- package/src/App/hooks/useLayoutMeasurements.js +10 -5
- package/src/App/hooks/useLayoutMeasurements.test.js +28 -9
- package/plugins/map-key/src/utils/mergeKeyGroupItems.js +0 -12
- package/plugins/map-key/src/utils/mergeKeyGroupItems.test.js +0 -53
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
let _pluginStateRef = {}
|
|
2
|
+
export const attachPluginStateRef = (pluginStateRef) => { _pluginStateRef = pluginStateRef }
|
|
3
|
+
|
|
4
|
+
let previousRef = null
|
|
5
|
+
let addedKeyGroups = []
|
|
6
|
+
|
|
7
|
+
const getStateKeyGroups = () => {
|
|
8
|
+
if (!_pluginStateRef?.current) {
|
|
9
|
+
return []
|
|
10
|
+
}
|
|
11
|
+
if (previousRef === _pluginStateRef.current) {
|
|
12
|
+
return addedKeyGroups
|
|
13
|
+
}
|
|
14
|
+
previousRef = _pluginStateRef.current
|
|
15
|
+
const { groups = [], keyDefinitions = [] } = _pluginStateRef.current
|
|
16
|
+
|
|
17
|
+
addedKeyGroups = groups.map(group => {
|
|
18
|
+
if (group.type === 'flat') {
|
|
19
|
+
return {
|
|
20
|
+
...group,
|
|
21
|
+
keyDefinition: keyDefinitions.find(item => item.id === group.id)
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return {
|
|
26
|
+
...group,
|
|
27
|
+
keyDefinitions: keyDefinitions.filter(item => item.groupId === group.id)
|
|
28
|
+
}
|
|
29
|
+
})
|
|
30
|
+
return addedKeyGroups
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Merges the state key group items with items provided by another plugin,
|
|
34
|
+
// specifically the datasets plugin
|
|
35
|
+
export const mergeKeyGroupItems = (keyGroupItemsToMerge = []) => {
|
|
36
|
+
// Clone the current state key groups from the plugin state reference
|
|
37
|
+
// to avoid mutating the original array
|
|
38
|
+
const stateKeyGroups = [...getStateKeyGroups()]
|
|
39
|
+
if (stateKeyGroups.length === 0) {
|
|
40
|
+
// if there is nothing in the state key groups, just return the items that were passed in
|
|
41
|
+
return keyGroupItemsToMerge
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const mergedItems = keyGroupItemsToMerge.map((keyGroupItem) => {
|
|
45
|
+
const groupIndex = stateKeyGroups.findIndex((_group) => _group.id === keyGroupItem.id)
|
|
46
|
+
if (groupIndex === -1) {
|
|
47
|
+
return keyGroupItem
|
|
48
|
+
}
|
|
49
|
+
// Splice the matched group from the stateKeyGroups and merge it with the item
|
|
50
|
+
const group = stateKeyGroups.splice(groupIndex, 1)[0]
|
|
51
|
+
return {
|
|
52
|
+
...keyGroupItem,
|
|
53
|
+
...group,
|
|
54
|
+
keyDefinitions: [
|
|
55
|
+
...(group.keyDefinitions || []),
|
|
56
|
+
...(keyGroupItem.keyDefinitions || [])
|
|
57
|
+
]
|
|
58
|
+
}
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
// Append any remaining pluginConfigGroups that were not matched with datasetItems
|
|
62
|
+
// and filter out any groups that have no key definitions
|
|
63
|
+
const finalMergedItems = [...mergedItems, ...stateKeyGroups]
|
|
64
|
+
.filter(item =>
|
|
65
|
+
((item.type === 'flat' && item.keyDefinition) ||
|
|
66
|
+
(item.type === 'group' && item.keyDefinitions?.length)))
|
|
67
|
+
return finalMergedItems
|
|
68
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { attachPluginStateRef, mergeKeyGroupItems } from './mergeKeyGroupItems.js'
|
|
2
|
+
|
|
3
|
+
describe('mergeKeyGroupItems', () => {
|
|
4
|
+
afterEach(() => {
|
|
5
|
+
attachPluginStateRef({ current: null })
|
|
6
|
+
})
|
|
7
|
+
|
|
8
|
+
it('returns the incoming items unchanged when no plugin state is attached', () => {
|
|
9
|
+
attachPluginStateRef({ current: null })
|
|
10
|
+
const items = [{ id: 'dataset-1', keyDefinitions: [{ id: 'k1' }] }]
|
|
11
|
+
|
|
12
|
+
expect(mergeKeyGroupItems(items)).toBe(items)
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
it('returns dataset items unchanged when plugin state groups are empty', () => {
|
|
16
|
+
attachPluginStateRef({ current: { groups: [], keyDefinitions: [] } })
|
|
17
|
+
const items = [{ id: 'dataset-1', keyDefinitions: [{ id: 'k1' }] }]
|
|
18
|
+
|
|
19
|
+
expect(mergeKeyGroupItems(items)).toEqual(items)
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
it('uses default empty state values and empty arguments when no groups or items are supplied', () => {
|
|
23
|
+
attachPluginStateRef({ current: {} })
|
|
24
|
+
|
|
25
|
+
expect(mergeKeyGroupItems()).toEqual([])
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
it('merges a matched group even when the state group has no keyDefinitions array', () => {
|
|
29
|
+
attachPluginStateRef({
|
|
30
|
+
current: {
|
|
31
|
+
groups: [{ id: 'dataset-1', type: 'group', groupLabel: 'Dataset 1' }],
|
|
32
|
+
keyDefinitions: []
|
|
33
|
+
}
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
const items = [{ id: 'dataset-1', keyDefinitions: [{ id: 'd1' }] }]
|
|
37
|
+
|
|
38
|
+
expect(mergeKeyGroupItems(items)).toEqual([
|
|
39
|
+
{ id: 'dataset-1', type: 'group', groupLabel: 'Dataset 1', keyDefinitions: [{ id: 'd1' }] }
|
|
40
|
+
])
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
it('reuses the cached plugin-state groups when the ref object is unchanged', () => {
|
|
44
|
+
const ref = { current: { groups: [{ id: 'group-1', type: 'group', groupLabel: 'Group 1' }], keyDefinitions: [{ id: 'k1', groupId: 'group-1' }] } }
|
|
45
|
+
attachPluginStateRef(ref)
|
|
46
|
+
|
|
47
|
+
const first = mergeKeyGroupItems([{ id: 'dataset-1', keyDefinitions: [{ id: 'd1' }] }])
|
|
48
|
+
const second = mergeKeyGroupItems([{ id: 'dataset-1', keyDefinitions: [{ id: 'd2' }] }])
|
|
49
|
+
|
|
50
|
+
expect(first).toEqual([{ id: 'group-1', type: 'group', groupLabel: 'Group 1', keyDefinitions: [{ id: 'k1', groupId: 'group-1' }] }])
|
|
51
|
+
expect(second).toEqual(first)
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
it('merges a matching flat group into the dataset item when the key definition matches the same id', () => {
|
|
55
|
+
attachPluginStateRef({
|
|
56
|
+
current: {
|
|
57
|
+
groups: [{ id: 'dataset-1', type: 'flat' }],
|
|
58
|
+
keyDefinitions: [{ id: 'dataset-1' }]
|
|
59
|
+
}
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
const items = [{ id: 'dataset-1', keyDefinitions: [{ id: 'dataset-1' }] }]
|
|
63
|
+
|
|
64
|
+
expect(mergeKeyGroupItems(items)).toEqual([
|
|
65
|
+
{
|
|
66
|
+
id: 'dataset-1',
|
|
67
|
+
type: 'flat',
|
|
68
|
+
keyDefinition: { id: 'dataset-1' },
|
|
69
|
+
keyDefinitions: [{ id: 'dataset-1' }]
|
|
70
|
+
}
|
|
71
|
+
])
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
it('keeps only unmatched state groups that still have key definitions', () => {
|
|
75
|
+
attachPluginStateRef({
|
|
76
|
+
current: {
|
|
77
|
+
groups: [
|
|
78
|
+
{ id: 'group-1', type: 'group', groupLabel: 'Group 1' },
|
|
79
|
+
{ id: 'group-2', type: 'group', groupLabel: 'Group 2' }
|
|
80
|
+
],
|
|
81
|
+
keyDefinitions: [{ id: 'k1', groupId: 'group-1' }]
|
|
82
|
+
}
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
const items = [{ id: 'dataset-1', keyDefinitions: [{ id: 'd1' }] }]
|
|
86
|
+
|
|
87
|
+
expect(mergeKeyGroupItems(items)).toEqual([
|
|
88
|
+
{ id: 'group-1', type: 'group', groupLabel: 'Group 1', keyDefinitions: [{ id: 'k1', groupId: 'group-1' }] }
|
|
89
|
+
])
|
|
90
|
+
})
|
|
91
|
+
})
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
const initialState = {
|
|
2
|
+
keyDefinitions: [],
|
|
3
|
+
groups: []
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
const labelToId = (label) => label ? label.toLowerCase().replace(/\s+/g, '-') : null
|
|
7
|
+
const createNewGroup = (id, groupId, groupLabel) => {
|
|
8
|
+
return groupId
|
|
9
|
+
? { id: groupId, type: 'group', groupLabel }
|
|
10
|
+
: { id, type: 'flat' }
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const findGroup = (state, id, groupId) => {
|
|
14
|
+
return state.groups.find(group => {
|
|
15
|
+
return (group.type === 'group' && group.id === groupId) ||
|
|
16
|
+
(group.type === 'flat' && group.id === id)
|
|
17
|
+
})
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const addSymbol = (state, keyDefinition) => {
|
|
21
|
+
const { id, groupLabel } = keyDefinition
|
|
22
|
+
const groupId = keyDefinition.groupId || labelToId(groupLabel)
|
|
23
|
+
// Check for an existingGroup
|
|
24
|
+
const existingGroup = findGroup(state, id, groupId)
|
|
25
|
+
// If no existing group is found, create a new one
|
|
26
|
+
const newGroup = existingGroup ? null : createNewGroup(id, groupId, groupLabel)
|
|
27
|
+
// Add the new group to the state if it was created
|
|
28
|
+
const groups = newGroup ? [...state.groups, newGroup] : state.groups
|
|
29
|
+
// Add the key definition to the state
|
|
30
|
+
const keyDefinitions = [...state.keyDefinitions, {
|
|
31
|
+
...keyDefinition,
|
|
32
|
+
type: 'manual',
|
|
33
|
+
...(groupId ? { groupId } : {})
|
|
34
|
+
}]
|
|
35
|
+
|
|
36
|
+
// Return the updated state with the new key definitions and groups
|
|
37
|
+
return {
|
|
38
|
+
...state,
|
|
39
|
+
keyDefinitions,
|
|
40
|
+
groups
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const removeSymbol = (state, keyDefinition) => {
|
|
45
|
+
const { id } = keyDefinition
|
|
46
|
+
return {
|
|
47
|
+
...state,
|
|
48
|
+
keyDefinitions: state.keyDefinitions.filter(key => key.id !== id)
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const addKeyGroups = (state, groups) => {
|
|
53
|
+
return {
|
|
54
|
+
...state,
|
|
55
|
+
groups: [...state.groups, ...groups]
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const actions = {
|
|
60
|
+
ADD_KEY_SYMBOL: addSymbol,
|
|
61
|
+
REMOVE_KEY_SYMBOL: removeSymbol,
|
|
62
|
+
ADD_KEY_GROUPS: addKeyGroups
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export {
|
|
66
|
+
initialState,
|
|
67
|
+
actions
|
|
68
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { actions, initialState } from './pluginState.js'
|
|
2
|
+
|
|
3
|
+
describe('pluginState reducer helpers', () => {
|
|
4
|
+
it('returns the initial state', () => {
|
|
5
|
+
expect(initialState).toEqual({ keyDefinitions: [], groups: [] })
|
|
6
|
+
})
|
|
7
|
+
|
|
8
|
+
it('adds a flat key item and creates a flat group when needed', () => {
|
|
9
|
+
const nextState = actions.ADD_KEY_SYMBOL(initialState, { id: 'flat-key', label: 'Flat key' })
|
|
10
|
+
|
|
11
|
+
expect(nextState).toEqual({
|
|
12
|
+
keyDefinitions: [{ id: 'flat-key', label: 'Flat key', type: 'manual' }],
|
|
13
|
+
groups: [{ id: 'flat-key', type: 'flat' }]
|
|
14
|
+
})
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
it('adds a grouped key item when the label can be converted to a group id', () => {
|
|
18
|
+
const nextState = actions.ADD_KEY_SYMBOL(initialState, { id: 'group-key', groupLabel: 'Group Label', label: 'Group key' })
|
|
19
|
+
|
|
20
|
+
expect(nextState).toEqual({
|
|
21
|
+
keyDefinitions: [{
|
|
22
|
+
id: 'group-key',
|
|
23
|
+
groupId: 'group-label',
|
|
24
|
+
groupLabel: 'Group Label',
|
|
25
|
+
label: 'Group key',
|
|
26
|
+
type: 'manual'
|
|
27
|
+
}],
|
|
28
|
+
groups: [{ id: 'group-label', type: 'group', groupLabel: 'Group Label' }]
|
|
29
|
+
})
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
it('reuses an existing group when the key item matches a flat group already in state', () => {
|
|
33
|
+
const state = {
|
|
34
|
+
keyDefinitions: [{ id: 'existing-item' }],
|
|
35
|
+
groups: [{ id: 'existing-item', type: 'flat' }]
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
expect(actions.ADD_KEY_SYMBOL(state, { id: 'existing-item', label: 'Existing item' })).toEqual({
|
|
39
|
+
keyDefinitions: [
|
|
40
|
+
{ id: 'existing-item' },
|
|
41
|
+
{ id: 'existing-item', label: 'Existing item', type: 'manual' }
|
|
42
|
+
],
|
|
43
|
+
groups: [{ id: 'existing-item', type: 'flat' }]
|
|
44
|
+
})
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
it('reuses an existing grouped definition when the same group id is already present', () => {
|
|
48
|
+
const state = {
|
|
49
|
+
keyDefinitions: [{ id: 'existing-group-item', groupId: 'group-1' }],
|
|
50
|
+
groups: [{ id: 'group-1', type: 'group', groupLabel: 'Group 1' }]
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
expect(actions.ADD_KEY_SYMBOL(state, { id: 'new-item', groupId: 'group-1', groupLabel: 'Group 1', label: 'New item' })).toEqual({
|
|
54
|
+
keyDefinitions: [
|
|
55
|
+
{ id: 'existing-group-item', groupId: 'group-1' },
|
|
56
|
+
{ id: 'new-item', groupId: 'group-1', groupLabel: 'Group 1', label: 'New item', type: 'manual' }
|
|
57
|
+
],
|
|
58
|
+
groups: [{ id: 'group-1', type: 'group', groupLabel: 'Group 1' }]
|
|
59
|
+
})
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
it('removes a key definition by id', () => {
|
|
63
|
+
const state = {
|
|
64
|
+
keyDefinitions: [{ id: 'a' }, { id: 'b' }],
|
|
65
|
+
groups: [{ id: 'a', type: 'flat' }]
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
expect(actions.REMOVE_KEY_SYMBOL(state, { id: 'a' })).toEqual({
|
|
69
|
+
keyDefinitions: [{ id: 'b' }],
|
|
70
|
+
groups: [{ id: 'a', type: 'flat' }]
|
|
71
|
+
})
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
it('adds configured groups to state without mutating the source array', () => {
|
|
75
|
+
const groups = [{ id: 'g1', type: 'group', groupLabel: 'G1' }]
|
|
76
|
+
|
|
77
|
+
expect(actions.ADD_KEY_GROUPS(initialState, groups)).toEqual({
|
|
78
|
+
keyDefinitions: [],
|
|
79
|
+
groups: [{ id: 'g1', type: 'group', groupLabel: 'G1' }]
|
|
80
|
+
})
|
|
81
|
+
})
|
|
82
|
+
})
|
|
@@ -17,8 +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
|
-
|
|
20
|
+
// clearance is how far docked attributions have pushed .im-o-app__bottom-right up via
|
|
21
|
+
// --bottom-right-clearance (see attributionsBottomRightClearance below) — without adding it
|
|
22
|
+
// here, .im-o-app__right's computed bottom offset stays at the box's un-pushed position and
|
|
23
|
+
// the floating right column overlaps the now-raised bottom-right buttons.
|
|
24
|
+
const rightOffsetBottom = (containerPad, bottomRightHeight, attributionsHeight, clearance, gap) =>
|
|
25
|
+
containerPad + (bottomRightHeight > 0 ? bottomRightHeight + clearance + gap : attributionsHeight)
|
|
22
26
|
|
|
23
27
|
// Clears the bottom row's own TOP edge (not just its trailing gap below), so the hint
|
|
24
28
|
// never overlaps the logo/attribution row itself, plus a gap above it. That trivially
|
|
@@ -127,6 +131,7 @@ function applyAttributionsLayout ({ appContainer, bottom, attributions, dividerG
|
|
|
127
131
|
appContainer.style.setProperty('--attributions-left', `${left}px`)
|
|
128
132
|
const clearance = attributionsBottomRightClearance(isStacked, attributions.offsetHeight, dividerGap, primaryGap)
|
|
129
133
|
appContainer.style.setProperty('--bottom-right-clearance', `${clearance}px`)
|
|
134
|
+
return { clearance }
|
|
130
135
|
}
|
|
131
136
|
|
|
132
137
|
/**
|
|
@@ -174,7 +179,7 @@ function calculateLayout (layoutRefs, breakpoint) {
|
|
|
174
179
|
? bannerTop + bannerHeight + dividerGap
|
|
175
180
|
: colHeight + top.offsetTop
|
|
176
181
|
|
|
177
|
-
applyAttributionsLayout({ appContainer, bottom, attributions, dividerGap, primaryGap })
|
|
182
|
+
const { clearance: bottomRightClearance } = applyAttributionsLayout({ appContainer, bottom, attributions, dividerGap, primaryGap })
|
|
178
183
|
|
|
179
184
|
// === Left container offsets ===
|
|
180
185
|
const leftOffsetTop = sideOffsetTop(topLeftCol.offsetHeight)
|
|
@@ -187,10 +192,10 @@ function calculateLayout (layoutRefs, breakpoint) {
|
|
|
187
192
|
const bottomRightHeight = buttonHeight(bottomRightRef)
|
|
188
193
|
const bottomContainerPad = main.offsetHeight - bottom.offsetTop - bottom.offsetHeight
|
|
189
194
|
const rightOffsetTop = sideOffsetTop(topRightCol.offsetHeight)
|
|
190
|
-
const rightEffectiveBottom = bottom.offsetTop + bottom.offsetHeight - bottomRightHeight
|
|
195
|
+
const rightEffectiveBottom = bottom.offsetTop + bottom.offsetHeight - bottomRightHeight - bottomRightClearance
|
|
191
196
|
const rightColumnHeight = rightEffectiveBottom - rightOffsetTop - dividerGap
|
|
192
197
|
appContainer.style.setProperty('--right-offset-top', `${rightOffsetTop}px`)
|
|
193
|
-
appContainer.style.setProperty('--right-offset-bottom', `${rightOffsetBottom(bottomContainerPad, bottomRightHeight, attributions.offsetHeight, dividerGap)}px`)
|
|
198
|
+
appContainer.style.setProperty('--right-offset-bottom', `${rightOffsetBottom(bottomContainerPad, bottomRightHeight, attributions.offsetHeight, bottomRightClearance, dividerGap)}px`)
|
|
194
199
|
appContainer.style.setProperty('--right-top-max-height', `${rightColumnHeight}px`)
|
|
195
200
|
|
|
196
201
|
// === Keyboard hint bottom offset ===
|
|
@@ -88,9 +88,11 @@ describe('useLayoutMeasurements', () => {
|
|
|
88
88
|
test.each([
|
|
89
89
|
['right-offset-top', { topRightCol: { offsetHeight: 80 }, top: { offsetTop: 15 } }, '95px'],
|
|
90
90
|
['right-offset-bottom', { main: { offsetHeight: 600 }, bottom: { offsetTop: 500 } }, '116px'],
|
|
91
|
-
// leftColumnHeight = 400 - (50+10) - 8 = 332
|
|
91
|
+
// leftColumnHeight = 400 - (50+10) - 8 = 332
|
|
92
|
+
// rightColumnHeight: rightEffectiveBottom = 400 - 0 (bottomRightHeight) - 16 (bottom-right-clearance,
|
|
93
|
+
// since default attributions offsetHeight 16 + dividerGap 8 - primaryGap 8 = 16) = 384; 384 - 50 - 8 = 326
|
|
92
94
|
['left-top-max-height', {}, '332px'],
|
|
93
|
-
['right-top-max-height', {}, '
|
|
95
|
+
['right-top-max-height', {}, '326px']
|
|
94
96
|
])('calculates %s correctly', (name, refOverrides, expected) => {
|
|
95
97
|
const { layoutRefs } = setup({ refs: refOverrides })
|
|
96
98
|
renderHook(() => useLayoutMeasurements())
|
|
@@ -103,10 +105,11 @@ describe('useLayoutMeasurements', () => {
|
|
|
103
105
|
['--left-top-panel-max-height', { leftBottom: { offsetHeight: 50 } }, '274px'], // 332 - 50 - 8
|
|
104
106
|
['--left-bottom-panel-max-height', {}, '332px'],
|
|
105
107
|
['--left-bottom-panel-max-height', { leftTop: { offsetHeight: 40 } }, '284px'], // 332 - 40 - 8
|
|
106
|
-
|
|
107
|
-
['--right-top-panel-max-height', {
|
|
108
|
-
['--right-
|
|
109
|
-
['--right-bottom-panel-max-height', {
|
|
108
|
+
// rightColumnHeight is 326 by default here (see the right-top-max-height case above)
|
|
109
|
+
['--right-top-panel-max-height', {}, '326px'],
|
|
110
|
+
['--right-top-panel-max-height', { rightBottom: { offsetHeight: 60 } }, '258px'], // 326 - 60 - 8
|
|
111
|
+
['--right-bottom-panel-max-height', {}, '326px'],
|
|
112
|
+
['--right-bottom-panel-max-height', { rightTop: { offsetHeight: 30 } }, '288px'] // 326 - 30 - 8
|
|
110
113
|
])('calculates %s with sibling buttons=%o correctly', (varName, refOverrides, expected) => {
|
|
111
114
|
const { layoutRefs } = setup({ refs: refOverrides })
|
|
112
115
|
renderHook(() => useLayoutMeasurements())
|
|
@@ -260,9 +263,25 @@ describe('useLayoutMeasurements', () => {
|
|
|
260
263
|
})
|
|
261
264
|
renderHook(() => useLayoutMeasurements())
|
|
262
265
|
// bottomContainerPad = 500 - 400 - 0 = 100
|
|
263
|
-
//
|
|
266
|
+
// bottom-right-clearance = 16 (default attributions offsetHeight 16 + dividerGap 8 - primaryGap 8)
|
|
267
|
+
// expected = 100 + (20 + 16 + 8) = 144
|
|
264
268
|
expect(layoutRefs.appContainerRef.current.style.setProperty)
|
|
265
|
-
.toHaveBeenCalledWith('--right-offset-bottom', '
|
|
269
|
+
.toHaveBeenCalledWith('--right-offset-bottom', '144px')
|
|
270
|
+
})
|
|
271
|
+
|
|
272
|
+
test('adds bottom-right-clearance into right-offset-bottom so .im-o-app__right does not overlap a bottom-right box pushed up by tall attributions', () => {
|
|
273
|
+
const { layoutRefs } = setup({
|
|
274
|
+
refs: {
|
|
275
|
+
bottomRight: { offsetHeight: 20 },
|
|
276
|
+
attributions: { offsetHeight: 40 } // clearance = 40 + 8 (dividerGap) - 8 (primaryGap) = 40
|
|
277
|
+
}
|
|
278
|
+
})
|
|
279
|
+
renderHook(() => useLayoutMeasurements())
|
|
280
|
+
// bottomContainerPad = 500 - 400 - 0 = 100; expected = 100 + (20 + 40 + 8) = 168
|
|
281
|
+
// Without the clearance term this regressed to 128px, letting .im-o-app__right's
|
|
282
|
+
// bottom offset sit below the (margin-bottom-raised) bottom-right box's real top edge.
|
|
283
|
+
expect(layoutRefs.appContainerRef.current.style.setProperty)
|
|
284
|
+
.toHaveBeenCalledWith('--right-offset-bottom', '168px')
|
|
266
285
|
})
|
|
267
286
|
|
|
268
287
|
test('uses 0 when sub-slot refs have null current', () => {
|
|
@@ -274,7 +293,7 @@ describe('useLayoutMeasurements', () => {
|
|
|
274
293
|
renderHook(() => useLayoutMeasurements())
|
|
275
294
|
// With all sub-slot refs null, buttons = 0 ?? 0 = 0, so max-heights equal full column height
|
|
276
295
|
expect(layoutRefs.appContainerRef.current.style.setProperty).toHaveBeenCalledWith('--left-top-panel-max-height', '332px')
|
|
277
|
-
expect(layoutRefs.appContainerRef.current.style.setProperty).toHaveBeenCalledWith('--right-bottom-panel-max-height', '
|
|
296
|
+
expect(layoutRefs.appContainerRef.current.style.setProperty).toHaveBeenCalledWith('--right-bottom-panel-max-height', '326px')
|
|
278
297
|
})
|
|
279
298
|
|
|
280
299
|
test('dispatches safe zone inset on desktop (post-batch RAF read only)', () => {
|
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
import { mergeKeyGroupItems } from './mergeKeyGroupItems'
|
|
2
|
-
|
|
3
|
-
describe('mergeKeyGroupItems', () => {
|
|
4
|
-
it('returns items unchanged when groups is not provided', () => {
|
|
5
|
-
const items = [{ id: 'a', label: 'A' }, { id: 'b', label: 'B' }]
|
|
6
|
-
|
|
7
|
-
expect(mergeKeyGroupItems(undefined, items)).toBe(items)
|
|
8
|
-
})
|
|
9
|
-
|
|
10
|
-
it('returns items unchanged when groups is null', () => {
|
|
11
|
-
const items = [{ id: 'a', label: 'A' }]
|
|
12
|
-
|
|
13
|
-
expect(mergeKeyGroupItems(null, items)).toBe(items)
|
|
14
|
-
})
|
|
15
|
-
|
|
16
|
-
it('merges matching group data into an item', () => {
|
|
17
|
-
const items = [{ id: 'a', label: 'A' }]
|
|
18
|
-
const groups = { a: { group: 'Group 1' } }
|
|
19
|
-
|
|
20
|
-
expect(mergeKeyGroupItems(groups, items)).toEqual([
|
|
21
|
-
{ id: 'a', label: 'A', group: 'Group 1' }
|
|
22
|
-
])
|
|
23
|
-
})
|
|
24
|
-
|
|
25
|
-
it('overrides item properties with matching group properties', () => {
|
|
26
|
-
const items = [{ id: 'a', label: 'A' }]
|
|
27
|
-
const groups = { a: { label: 'Overridden' } }
|
|
28
|
-
|
|
29
|
-
expect(mergeKeyGroupItems(groups, items)).toEqual([
|
|
30
|
-
{ id: 'a', label: 'Overridden' }
|
|
31
|
-
])
|
|
32
|
-
})
|
|
33
|
-
|
|
34
|
-
it('leaves items without a matching group unchanged', () => {
|
|
35
|
-
const items = [{ id: 'a', label: 'A' }, { id: 'b', label: 'B' }]
|
|
36
|
-
const groups = { a: { group: 'Group 1' } }
|
|
37
|
-
|
|
38
|
-
expect(mergeKeyGroupItems(groups, items)).toEqual([
|
|
39
|
-
{ id: 'a', label: 'A', group: 'Group 1' },
|
|
40
|
-
{ id: 'b', label: 'B' }
|
|
41
|
-
])
|
|
42
|
-
})
|
|
43
|
-
|
|
44
|
-
it('returns an empty array when items is empty', () => {
|
|
45
|
-
expect(mergeKeyGroupItems({ a: { group: 'Group 1' } }, [])).toEqual([])
|
|
46
|
-
})
|
|
47
|
-
|
|
48
|
-
it('handles an empty groups object as no matches', () => {
|
|
49
|
-
const items = [{ id: 'a', label: 'A' }]
|
|
50
|
-
|
|
51
|
-
expect(mergeKeyGroupItems({}, items)).toEqual(items)
|
|
52
|
-
})
|
|
53
|
-
})
|