@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.
Files changed (31) hide show
  1. package/dist/esm/im-core.js +1 -1
  2. package/dist/umd/im-core.js +1 -1
  3. package/package.json +1 -1
  4. package/plugins/datasets/dist/esm/esriLayerAdapter.js +1 -1
  5. package/plugins/datasets/dist/esm/im-datasets-plugin.js +1 -1
  6. package/plugins/datasets/dist/umd/im-datasets-esri-adapter.js +1 -1
  7. package/plugins/datasets/dist/umd/im-datasets-plugin.js +1 -1
  8. package/plugins/datasets/src/adapters/esri/esriLayerAdapter.js +42 -9
  9. package/plugins/datasets/src/adapters/esri/esriLayerAdapter.test.js +42 -4
  10. package/plugins/datasets/src/registry/datasetRegistry.js +6 -6
  11. package/plugins/map-key/dist/esm/im-map-key-plugin.js +1 -1
  12. package/plugins/map-key/dist/umd/im-map-key-plugin.js +1 -1
  13. package/plugins/map-key/dist/umd/index.js +1 -1
  14. package/plugins/map-key/src/api/addSymbol.js +3 -0
  15. package/plugins/map-key/src/api/addSymbol.test.js +14 -0
  16. package/plugins/map-key/src/api/index.js +7 -0
  17. package/plugins/map-key/src/api/index.test.js +13 -0
  18. package/plugins/map-key/src/api/removeSymbol.js +3 -0
  19. package/plugins/map-key/src/api/removeSymbol.test.js +14 -0
  20. package/plugins/map-key/src/components/Key/MapKey.jsx +11 -3
  21. package/plugins/map-key/src/initialise/MapKeyInit.jsx +18 -3
  22. package/plugins/map-key/src/initialise/MapKeyInit.test.jsx +44 -8
  23. package/plugins/map-key/src/manifest.js +5 -1
  24. package/plugins/map-key/src/reducers/mergeKeyGroupItems.js +68 -0
  25. package/plugins/map-key/src/reducers/mergeKeyGroupItems.test.js +91 -0
  26. package/plugins/map-key/src/reducers/pluginState.js +68 -0
  27. package/plugins/map-key/src/reducers/pluginState.test.js +82 -0
  28. package/src/App/hooks/useLayoutMeasurements.js +10 -5
  29. package/src/App/hooks/useLayoutMeasurements.test.js +28 -9
  30. package/plugins/map-key/src/utils/mergeKeyGroupItems.js +0 -12
  31. 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
- const rightOffsetBottom = (containerPad, bottomRightHeight, attributionsHeight, gap) =>
21
- containerPad + (bottomRightHeight > 0 ? bottomRightHeight + gap : attributionsHeight)
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; rightColumnHeight = 400 - (40+10) - 8 = 342
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', {}, '342px']
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
- ['--right-top-panel-max-height', {}, '342px'],
107
- ['--right-top-panel-max-height', { rightBottom: { offsetHeight: 60 } }, '274px'], // 342 - 60 - 8
108
- ['--right-bottom-panel-max-height', {}, '342px'],
109
- ['--right-bottom-panel-max-height', { rightTop: { offsetHeight: 30 } }, '304px'] // 342 - 30 - 8
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
- // expected = 100 + (20 + 8) = 128
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', '128px')
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', '342px')
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,12 +0,0 @@
1
- export const mergeKeyGroupItems = (groups, items) => {
2
- if (!groups) {
3
- return items
4
- }
5
- return items.map((item) => {
6
- const { id } = item
7
- if (groups[id]) {
8
- return { ...item, ...groups[id] }
9
- }
10
- return item
11
- })
12
- }
@@ -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
- })