@defra/interactive-map 0.0.10-alpha → 0.0.11-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/README.md +1 -1
- package/dist/css/index.css +1 -1
- package/dist/esm/im-core.js +1 -1
- package/dist/esm/im-shell.js +1 -1
- package/dist/umd/im-core.js +1 -1
- package/dist/umd/index.js +1 -1
- package/docs/api/button-definition.md +21 -3
- package/docs/api/panel-definition.md +10 -12
- package/docs/api.md +80 -7
- package/docs/demo.mdx +70 -0
- package/docs/index.md +0 -4
- package/docs/plugins/plugin-context.md +3 -3
- package/docs/plugins/plugin-manifest.md +1 -1
- package/docusaurus.config.cjs +55 -25
- package/package.json +12 -7
- package/plugins/beta/datasets/dist/esm/im-datasets-plugin.js +1 -1
- package/plugins/beta/datasets/dist/umd/im-datasets-plugin.js +1 -1
- package/plugins/beta/datasets/src/manifest.js +3 -3
- package/plugins/beta/draw-ml/dist/umd/im-draw-ml-plugin.js +1 -1
- package/plugins/beta/map-styles/dist/esm/im-map-styles-plugin.js +1 -1
- package/plugins/beta/map-styles/dist/umd/im-map-styles-plugin.js +1 -1
- package/plugins/beta/map-styles/src/manifest.js +3 -3
- package/plugins/beta/use-location/dist/esm/im-use-location-plugin.js +1 -1
- package/plugins/beta/use-location/dist/umd/im-use-location-plugin.js +1 -1
- package/plugins/beta/use-location/src/manifest.js +7 -7
- package/plugins/search/dist/css/index.css +1 -1
- package/plugins/search/dist/esm/im-search-plugin.js +1 -1
- package/plugins/search/dist/esm/index.js +1 -1
- package/plugins/search/dist/umd/im-search-plugin.js +1 -1
- package/plugins/search/dist/umd/index.js +1 -1
- package/plugins/search/src/Search.jsx +9 -3
- package/plugins/search/src/Search.test.jsx +26 -6
- package/plugins/search/src/components/Form/Form.jsx +35 -7
- package/plugins/search/src/components/Form/Form.module.scss +27 -0
- package/plugins/search/src/components/Form/Form.test.jsx +99 -2
- package/plugins/search/src/components/SubmitButton/SubmitButton.jsx +28 -0
- package/plugins/search/src/components/SubmitButton/SubmitButton.module.scss +8 -0
- package/plugins/search/src/components/SubmitButton/SubmitButton.test.jsx +33 -0
- package/plugins/search/src/datasets.js +15 -11
- package/plugins/search/src/datasets.test.js +17 -2
- package/plugins/search/src/events/fetchSuggestions.js +1 -1
- package/plugins/search/src/index.js +1 -1
- package/plugins/search/src/index.test.js +4 -4
- package/plugins/search/src/reducer.js +9 -4
- package/plugins/search/src/reducer.test.js +12 -7
- package/plugins/search/src/search.scss +5 -1
- package/plugins/search/src/utils/parseOsNamesResults.js +18 -2
- package/plugins/search/src/utils/parseOsNamesResults.test.js +33 -15
- package/providers/beta/esri/dist/esm/im-esri-provider.js +1 -1
- package/providers/beta/esri/src/appEvents.js +8 -2
- package/providers/beta/esri/src/esriProvider.js +6 -14
- package/providers/beta/esri/src/mapEvents.js +7 -1
- package/providers/beta/esri/src/utils/coords.js +33 -1
- package/providers/beta/esri/src/utils/coords.test.js +126 -0
- package/providers/maplibre/dist/esm/im-maplibre-provider.js +1 -1
- package/providers/maplibre/dist/esm/index.js +1 -1
- package/providers/maplibre/dist/umd/im-maplibre-provider.js +1 -1
- package/providers/maplibre/dist/umd/index.js +1 -1
- package/providers/maplibre/src/appEvents.js +10 -1
- package/providers/maplibre/src/appEvents.test.js +13 -4
- package/providers/maplibre/src/index.js +5 -13
- package/providers/maplibre/src/index.test.js +34 -15
- package/providers/maplibre/src/mapEvents.js +9 -1
- package/providers/maplibre/src/maplibreProvider.js +14 -15
- package/providers/maplibre/src/maplibreProvider.test.js +14 -1
- package/providers/maplibre/src/utils/spatial.js +11 -0
- package/providers/maplibre/src/utils/spatial.test.js +12 -0
- package/src/App/components/Actions/Actions.module.scss +5 -4
- package/src/App/components/MapButton/MapButton.jsx +4 -16
- package/src/App/components/MapButton/MapButton.module.scss +12 -12
- package/src/App/components/MapButton/MapButton.test.jsx +0 -9
- package/src/App/components/Panel/Panel.jsx +6 -6
- package/src/App/components/Panel/Panel.test.jsx +14 -15
- package/src/App/components/Viewport/MapController.jsx +2 -1
- package/src/App/hooks/useLayoutMeasurements.js +1 -1
- package/src/App/hooks/useLayoutMeasurements.test.js +1 -1
- package/src/App/hooks/useMapProviderOverrides.js +21 -1
- package/src/App/hooks/useMapProviderOverrides.test.js +51 -2
- package/src/App/layout/Layout.jsx +4 -4
- package/src/App/layout/layout.module.scss +1 -0
- package/src/App/registry/panelRegistry.js +1 -10
- package/src/App/registry/panelRegistry.test.js +6 -11
- package/src/App/renderer/HtmlElementHost.jsx +11 -3
- package/src/App/renderer/HtmlElementHost.test.jsx +89 -0
- package/src/App/renderer/mapButtons.js +128 -28
- package/src/App/renderer/mapButtons.test.js +119 -19
- package/src/App/store/MapProvider.jsx +18 -5
- package/src/App/store/MapProvider.test.jsx +56 -1
- package/src/App/store/appActionsMap.js +17 -9
- package/src/App/store/appActionsMap.test.js +33 -7
- package/src/App/store/mapActionsMap.js +4 -7
- package/src/InteractiveMap/InteractiveMap.js +18 -0
- package/src/InteractiveMap/InteractiveMap.test.js +12 -0
- package/src/config/appConfig.js +17 -15
- package/src/config/events.js +41 -4
- package/src/config/getInitialOpenPanels.js +2 -2
- package/src/config/getInitialOpenPanels.test.js +7 -7
- package/src/types.js +13 -11
- package/src/utils/getValueForStyle.js +1 -1
|
@@ -11,12 +11,17 @@
|
|
|
11
11
|
padding-top var(--duration) ease, padding-bottom var(--duration) ease;
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
+
&--border-top {
|
|
15
|
+
border-top: 1px solid var(--app-border-color);
|
|
16
|
+
}
|
|
17
|
+
|
|
14
18
|
&--hidden {
|
|
15
19
|
max-height: 0;
|
|
16
20
|
overflow: hidden;
|
|
17
21
|
opacity: 0;
|
|
18
22
|
padding-top: 0;
|
|
19
23
|
padding-bottom: 0;
|
|
24
|
+
border: 0;
|
|
20
25
|
}
|
|
21
26
|
|
|
22
27
|
.im-c-button-wrapper--wide {
|
|
@@ -30,10 +35,6 @@
|
|
|
30
35
|
}
|
|
31
36
|
}
|
|
32
37
|
|
|
33
|
-
.im-c-actions--border-top {
|
|
34
|
-
border-top: 1px solid var(--app-border-color);
|
|
35
|
-
}
|
|
36
|
-
|
|
37
38
|
.im-o-app--tablet, .im-o-app--desktop {
|
|
38
39
|
.im-c-actions {
|
|
39
40
|
min-width: 0;
|
|
@@ -26,18 +26,12 @@ const buildButtonClassNames = (buttonId, variant, showLabel) => [
|
|
|
26
26
|
* Builds CSS class names for the wrapper div that contains the button.
|
|
27
27
|
* @param {string} buttonId - Unique identifier for the button
|
|
28
28
|
* @param {boolean} showLabel - Whether the button label is displayed
|
|
29
|
-
* @param {boolean} groupStart - Whether this button is at the start of a button group
|
|
30
|
-
* @param {boolean} groupMiddle - Whether this button is in the middle of a button group
|
|
31
|
-
* @param {boolean} groupEnd - Whether this button is at the end of a button group
|
|
32
29
|
* @returns {string} Space-separated CSS class names for the wrapper
|
|
33
30
|
*/
|
|
34
|
-
const buildWrapperClassNames = (buttonId, showLabel
|
|
31
|
+
const buildWrapperClassNames = (buttonId, showLabel) => [
|
|
35
32
|
'im-c-button-wrapper',
|
|
36
33
|
buttonId && `im-c-button-wrapper--${stringToKebab(buttonId)}`,
|
|
37
|
-
showLabel && 'im-c-button-wrapper--wide'
|
|
38
|
-
groupStart && 'im-c-button-wrapper--group-start',
|
|
39
|
-
groupMiddle && 'im-c-button-wrapper--group-middle',
|
|
40
|
-
groupEnd && 'im-c-button-wrapper--group-end'
|
|
34
|
+
showLabel && 'im-c-button-wrapper--wide'
|
|
41
35
|
].filter(Boolean).join(' ')
|
|
42
36
|
|
|
43
37
|
/**
|
|
@@ -159,9 +153,6 @@ const buildButtonProps = ({
|
|
|
159
153
|
* @param {Array<Object>} [props.menuItems] - Array of items for popup menu
|
|
160
154
|
* @param {string} [props.idPrefix=''] - Prefix for generated panel/popup IDs
|
|
161
155
|
* @param {string} [props.href] - URL for anchor element; if provided, renders as <a> instead of <button>
|
|
162
|
-
* @param {boolean} [props.groupStart=false] - Whether button is at start of button group
|
|
163
|
-
* @param {boolean} [props.groupMiddle=false] - Whether button is in middle of button group
|
|
164
|
-
* @param {boolean} [props.groupEnd=false] - Whether button is at end of button group
|
|
165
156
|
* @returns {JSX.Element} The rendered button component
|
|
166
157
|
*/
|
|
167
158
|
export const MapButton = ({
|
|
@@ -180,10 +171,7 @@ export const MapButton = ({
|
|
|
180
171
|
panelId,
|
|
181
172
|
menuItems,
|
|
182
173
|
idPrefix,
|
|
183
|
-
href
|
|
184
|
-
groupMiddle,
|
|
185
|
-
groupStart,
|
|
186
|
-
groupEnd
|
|
174
|
+
href
|
|
187
175
|
}) => {
|
|
188
176
|
const { id: appId } = useConfig()
|
|
189
177
|
const { buttonRefs } = useApp()
|
|
@@ -255,7 +243,7 @@ export const MapButton = ({
|
|
|
255
243
|
|
|
256
244
|
return (
|
|
257
245
|
<div
|
|
258
|
-
className={buildWrapperClassNames(buttonId, showLabel
|
|
246
|
+
className={buildWrapperClassNames(buttonId, showLabel)}
|
|
259
247
|
style={isHidden ? { display: 'none' } : undefined}
|
|
260
248
|
>
|
|
261
249
|
{showLabel ? buttonEl : <Tooltip content={label}>{buttonEl}</Tooltip>}
|
|
@@ -138,16 +138,16 @@
|
|
|
138
138
|
}
|
|
139
139
|
|
|
140
140
|
.im-o-app__right {
|
|
141
|
-
.im-c-button-
|
|
142
|
-
|
|
143
|
-
|
|
141
|
+
.im-c-button-group {
|
|
142
|
+
display: flex;
|
|
143
|
+
flex-direction: column;
|
|
144
144
|
}
|
|
145
145
|
|
|
146
|
-
.im-c-button-
|
|
146
|
+
.im-c-button-group .im-c-button-wrapper:first-child .im-c-map-button {
|
|
147
147
|
@include tools.border-focus-corner-override($corners: 'top');
|
|
148
148
|
}
|
|
149
149
|
|
|
150
|
-
.im-c-button-
|
|
150
|
+
.im-c-button-group .im-c-button-wrapper:not(:first-child):not(:last-child) .im-c-map-button {
|
|
151
151
|
@include tools.border-focus-corner-override($corners: 'none');
|
|
152
152
|
|
|
153
153
|
&::before {
|
|
@@ -155,23 +155,23 @@
|
|
|
155
155
|
}
|
|
156
156
|
}
|
|
157
157
|
|
|
158
|
-
.im-c-button-
|
|
158
|
+
.im-c-button-group .im-c-button-wrapper:last-child .im-c-map-button {
|
|
159
159
|
margin-top: 0;
|
|
160
160
|
@include tools.border-focus-corner-override($corners: 'bottom');
|
|
161
161
|
}
|
|
162
162
|
}
|
|
163
163
|
|
|
164
164
|
.im-o-app__top {
|
|
165
|
-
.im-c-button-
|
|
166
|
-
|
|
167
|
-
|
|
165
|
+
.im-c-button-group {
|
|
166
|
+
display: flex;
|
|
167
|
+
flex-direction: row;
|
|
168
168
|
}
|
|
169
169
|
|
|
170
|
-
.im-c-button-
|
|
170
|
+
.im-c-button-group .im-c-button-wrapper:first-child .im-c-map-button {
|
|
171
171
|
@include tools.border-focus-corner-override($corners: 'left');
|
|
172
172
|
}
|
|
173
173
|
|
|
174
|
-
.im-c-button-
|
|
174
|
+
.im-c-button-group .im-c-button-wrapper:not(:first-child):not(:last-child) .im-c-map-button {
|
|
175
175
|
@include tools.border-focus-corner-override($corners: 'none');
|
|
176
176
|
|
|
177
177
|
&::before {
|
|
@@ -179,7 +179,7 @@
|
|
|
179
179
|
}
|
|
180
180
|
}
|
|
181
181
|
|
|
182
|
-
.im-c-button-
|
|
182
|
+
.im-c-button-group .im-c-button-wrapper:last-child .im-c-map-button {
|
|
183
183
|
@include tools.border-focus-corner-override($corners: 'right');
|
|
184
184
|
}
|
|
185
185
|
}
|
|
@@ -66,15 +66,6 @@ describe('MapButton', () => {
|
|
|
66
66
|
expect(container.firstChild).toHaveStyle('display: none')
|
|
67
67
|
})
|
|
68
68
|
|
|
69
|
-
it.each([
|
|
70
|
-
['groupStart', 'im-c-button-wrapper--group-start'],
|
|
71
|
-
['groupMiddle', 'im-c-button-wrapper--group-middle'],
|
|
72
|
-
['groupEnd', 'im-c-button-wrapper--group-end']
|
|
73
|
-
])('applies wrapper %s class', (prop, className) => {
|
|
74
|
-
const { container } = renderButton({ [prop]: true })
|
|
75
|
-
expect(container.firstChild).toHaveClass(className)
|
|
76
|
-
})
|
|
77
|
-
|
|
78
69
|
it('handles panelId aria attributes', () => {
|
|
79
70
|
renderButton({ panelId: 'Settings', idPrefix: 'prefix', isDisabled: true, isPanelOpen: false })
|
|
80
71
|
const button = getButton()
|
|
@@ -7,10 +7,10 @@ import { useIsScrollable } from '../../hooks/useIsScrollable.js'
|
|
|
7
7
|
import { Icon } from '../Icon/Icon'
|
|
8
8
|
|
|
9
9
|
const computePanelState = (bpConfig, triggeringElement) => {
|
|
10
|
-
const isAside = bpConfig.slot === 'side' && bpConfig.
|
|
11
|
-
const isDialog = !isAside && bpConfig.
|
|
10
|
+
const isAside = bpConfig.slot === 'side' && bpConfig.open && !bpConfig.modal
|
|
11
|
+
const isDialog = !isAside && bpConfig.dismissible
|
|
12
12
|
const isModal = bpConfig.modal === true
|
|
13
|
-
const isDismissable = bpConfig.
|
|
13
|
+
const isDismissable = bpConfig.dismissible !== false
|
|
14
14
|
const shouldFocus = Boolean(isModal || triggeringElement)
|
|
15
15
|
const buttonContainerEl = bpConfig.slot.endsWith('button') ? triggeringElement?.parentNode : undefined
|
|
16
16
|
return { isAside, isDialog, isModal, isDismissable, shouldFocus, buttonContainerEl }
|
|
@@ -96,8 +96,8 @@ export const Panel = ({ panelId, panelConfig, props, WrappedChild, label, html,
|
|
|
96
96
|
}
|
|
97
97
|
}, [isOpen])
|
|
98
98
|
|
|
99
|
-
const panelClass = buildPanelClassNames(bpConfig.slot,
|
|
100
|
-
const panelBodyClass = buildPanelBodyClassNames(
|
|
99
|
+
const panelClass = buildPanelClassNames(bpConfig.slot, bpConfig.showLabel ?? true)
|
|
100
|
+
const panelBodyClass = buildPanelBodyClassNames(bpConfig.showLabel ?? true, isDismissable)
|
|
101
101
|
const innerHtmlProp = useMemo(() => html ? { __html: html } : null, [html])
|
|
102
102
|
|
|
103
103
|
const panelProps = buildPanelProps({ elementId, shouldFocus, isDialog, isDismissable, isModal, width: bpConfig.width, panelClass })
|
|
@@ -110,7 +110,7 @@ export const Panel = ({ panelId, panelConfig, props, WrappedChild, label, html,
|
|
|
110
110
|
>
|
|
111
111
|
<h2
|
|
112
112
|
id={`${elementId}-label`}
|
|
113
|
-
className={
|
|
113
|
+
className={(bpConfig.showLabel ?? true) ? 'im-c-panel__heading im-e-heading-m' : 'im-u-visually-hidden'}
|
|
114
114
|
>
|
|
115
115
|
{label}
|
|
116
116
|
</h2>
|
|
@@ -32,8 +32,7 @@ describe('Panel', () => {
|
|
|
32
32
|
|
|
33
33
|
const renderPanel = (config = {}, props = {}) => {
|
|
34
34
|
const panelConfig = {
|
|
35
|
-
showLabel: true,
|
|
36
|
-
desktop: { slot: 'side', initiallyOpen: true, dismissable: false, modal: false },
|
|
35
|
+
desktop: { slot: 'side', open: true, dismissible: false, modal: false, showLabel: true },
|
|
37
36
|
...config
|
|
38
37
|
}
|
|
39
38
|
return render(<Panel panelId='Settings' panelConfig={panelConfig} label='Settings' {...props} />)
|
|
@@ -49,17 +48,17 @@ describe('Panel', () => {
|
|
|
49
48
|
})
|
|
50
49
|
|
|
51
50
|
it('renders visually hidden label when showLabel=false', () => {
|
|
52
|
-
renderPanel({ showLabel: false })
|
|
51
|
+
renderPanel({ desktop: { slot: 'side', open: true, dismissible: false, modal: false, showLabel: false } })
|
|
53
52
|
expect(screen.getByText('Settings')).toHaveClass('im-u-visually-hidden')
|
|
54
53
|
})
|
|
55
54
|
|
|
56
|
-
it('applies offset class to body when showLabel=false and
|
|
57
|
-
renderPanel({
|
|
55
|
+
it('applies offset class to body when showLabel=false and dismissible', () => {
|
|
56
|
+
renderPanel({ desktop: { slot: 'side', dismissible: true, open: false, showLabel: false } })
|
|
58
57
|
expect(screen.getByRole('dialog').querySelector('.im-c-panel__body')).toHaveClass('im-c-panel__body--offset')
|
|
59
58
|
})
|
|
60
59
|
|
|
61
60
|
it('applies width style if provided', () => {
|
|
62
|
-
renderPanel({ desktop: { slot: 'side',
|
|
61
|
+
renderPanel({ desktop: { slot: 'side', dismissible: true, open: true, width: '300px' } })
|
|
63
62
|
expect(screen.getByRole('complementary')).toHaveStyle({ width: '300px' })
|
|
64
63
|
})
|
|
65
64
|
|
|
@@ -82,23 +81,23 @@ describe('Panel', () => {
|
|
|
82
81
|
})
|
|
83
82
|
|
|
84
83
|
describe('role and aria attributes', () => {
|
|
85
|
-
it('renders region role for non-
|
|
84
|
+
it('renders region role for non-dismissible panels', () => {
|
|
86
85
|
renderPanel()
|
|
87
86
|
expect(screen.getByRole('region')).toBeInTheDocument()
|
|
88
87
|
})
|
|
89
88
|
|
|
90
|
-
it('renders dialog role for
|
|
91
|
-
renderPanel({ desktop: { slot: 'side',
|
|
89
|
+
it('renders dialog role for dismissible non-aside panels', () => {
|
|
90
|
+
renderPanel({ desktop: { slot: 'side', dismissible: true, open: false } })
|
|
92
91
|
expect(screen.getByRole('dialog')).toBeInTheDocument()
|
|
93
92
|
})
|
|
94
93
|
|
|
95
|
-
it('renders complementary role for
|
|
96
|
-
renderPanel({ desktop: { slot: 'side',
|
|
94
|
+
it('renders complementary role for dismissible aside panels', () => {
|
|
95
|
+
renderPanel({ desktop: { slot: 'side', open: true, dismissible: true } })
|
|
97
96
|
expect(screen.getByRole('complementary')).toBeInTheDocument()
|
|
98
97
|
})
|
|
99
98
|
|
|
100
99
|
it('sets aria-modal and tabIndex for modal dialogs', () => {
|
|
101
|
-
renderPanel({ desktop: { slot: 'overlay',
|
|
100
|
+
renderPanel({ desktop: { slot: 'overlay', dismissible: true, modal: true } })
|
|
102
101
|
const dialog = screen.getByRole('dialog')
|
|
103
102
|
expect(dialog).toHaveAttribute('aria-modal', 'true')
|
|
104
103
|
expect(dialog).toHaveAttribute('tabIndex', '-1')
|
|
@@ -111,7 +110,7 @@ describe('Panel', () => {
|
|
|
111
110
|
const triggeringElement = { focus: focusMock, parentNode: document.createElement('div') }
|
|
112
111
|
|
|
113
112
|
renderPanel(
|
|
114
|
-
{ desktop: { slot: 'top-button',
|
|
113
|
+
{ desktop: { slot: 'top-button', dismissible: true, open: false } },
|
|
115
114
|
{ props: { triggeringElement } }
|
|
116
115
|
)
|
|
117
116
|
|
|
@@ -125,7 +124,7 @@ describe('Panel', () => {
|
|
|
125
124
|
const triggeringElement = { focus: focusMock, parentNode: document.createElement('div') }
|
|
126
125
|
|
|
127
126
|
renderPanel(
|
|
128
|
-
{ desktop: { slot: 'overlay',
|
|
127
|
+
{ desktop: { slot: 'overlay', dismissible: true, modal: true } },
|
|
129
128
|
{ props: { triggeringElement } }
|
|
130
129
|
)
|
|
131
130
|
|
|
@@ -134,7 +133,7 @@ describe('Panel', () => {
|
|
|
134
133
|
})
|
|
135
134
|
|
|
136
135
|
it('falls back to viewportRef focus when no triggeringElement', () => {
|
|
137
|
-
renderPanel({ desktop: { slot: 'side',
|
|
136
|
+
renderPanel({ desktop: { slot: 'side', dismissible: true, open: false } })
|
|
138
137
|
|
|
139
138
|
fireEvent.click(screen.getByRole('button', { name: 'Close Settings' }))
|
|
140
139
|
expect(layoutRefs.viewportRef.current.focus).toHaveBeenCalled()
|
|
@@ -81,7 +81,7 @@ export function useLayoutMeasurements () {
|
|
|
81
81
|
// --------------------------------
|
|
82
82
|
// 3. Recaluclate CSS vars when elements resize
|
|
83
83
|
// --------------------------------
|
|
84
|
-
useResizeObserver([bannerRef, mainRef, topRef, actionsRef, footerRef], () => {
|
|
84
|
+
useResizeObserver([bannerRef, mainRef, topRef, topLeftColRef, topRightColRef, actionsRef, footerRef], () => {
|
|
85
85
|
requestAnimationFrame(() => {
|
|
86
86
|
calculateLayout()
|
|
87
87
|
})
|
|
@@ -114,7 +114,7 @@ describe('useLayoutMeasurements', () => {
|
|
|
114
114
|
const { layoutRefs } = setup()
|
|
115
115
|
renderHook(() => useLayoutMeasurements())
|
|
116
116
|
expect(useResizeObserver).toHaveBeenCalledWith(
|
|
117
|
-
[layoutRefs.bannerRef, layoutRefs.mainRef, layoutRefs.topRef, layoutRefs.actionsRef, layoutRefs.footerRef],
|
|
117
|
+
[layoutRefs.bannerRef, layoutRefs.mainRef, layoutRefs.topRef, layoutRefs.topLeftColRef, layoutRefs.topRightColRef, layoutRefs.actionsRef, layoutRefs.footerRef],
|
|
118
118
|
expect.any(Function)
|
|
119
119
|
)
|
|
120
120
|
layoutRefs.appContainerRef.current.style.setProperty.mockClear()
|
|
@@ -2,12 +2,13 @@ import { useEffect, useRef } from 'react'
|
|
|
2
2
|
import { useConfig } from '../store/configContext.js'
|
|
3
3
|
import { useApp } from '../store/appContext.js'
|
|
4
4
|
import { useMap } from '../store/mapContext.js'
|
|
5
|
+
import { EVENTS as events } from '../../config/events.js'
|
|
5
6
|
import { getSafeZoneInset } from '../../utils/getSafeZoneInset.js'
|
|
6
7
|
import { scalePoints } from '../../utils/scalePoints.js'
|
|
7
8
|
import { scaleFactor } from '../../config/appConfig.js'
|
|
8
9
|
|
|
9
10
|
export const useMapProviderOverrides = () => {
|
|
10
|
-
const { mapProvider } = useConfig()
|
|
11
|
+
const { mapProvider, eventBus } = useConfig()
|
|
11
12
|
const { dispatch: appDispatch, layoutRefs } = useApp()
|
|
12
13
|
const { mapSize } = useMap()
|
|
13
14
|
|
|
@@ -67,4 +68,23 @@ export const useMapProviderOverrides = () => {
|
|
|
67
68
|
mapProvider.setView = originalSetView
|
|
68
69
|
}
|
|
69
70
|
}, [mapProvider, appDispatch, layoutRefs, mapSize])
|
|
71
|
+
|
|
72
|
+
// Forward public API events to the (overridden) mapProvider methods so that
|
|
73
|
+
// interactiveMap.fitToBounds() and interactiveMap.setView() respect safe zone padding.
|
|
74
|
+
useEffect(() => {
|
|
75
|
+
if (!mapProvider || !eventBus) {
|
|
76
|
+
return undefined
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const handleFitToBounds = (bbox) => mapProvider.fitToBounds(bbox)
|
|
80
|
+
const handleSetView = (opts) => mapProvider.setView(opts)
|
|
81
|
+
|
|
82
|
+
eventBus.on(events.MAP_FIT_TO_BOUNDS, handleFitToBounds)
|
|
83
|
+
eventBus.on(events.MAP_SET_VIEW, handleSetView)
|
|
84
|
+
|
|
85
|
+
return () => {
|
|
86
|
+
eventBus.off(events.MAP_FIT_TO_BOUNDS, handleFitToBounds)
|
|
87
|
+
eventBus.off(events.MAP_SET_VIEW, handleSetView)
|
|
88
|
+
}
|
|
89
|
+
}, [mapProvider, eventBus])
|
|
70
90
|
}
|
|
@@ -22,15 +22,21 @@ const setup = (overrides = {}) => {
|
|
|
22
22
|
setPadding: jest.fn(),
|
|
23
23
|
...overrides.mapProvider
|
|
24
24
|
}
|
|
25
|
+
const capturedHandlers = {}
|
|
26
|
+
const eventBus = {
|
|
27
|
+
on: jest.fn((event, handler) => { capturedHandlers[event] = handler }),
|
|
28
|
+
off: jest.fn(),
|
|
29
|
+
...overrides.eventBus
|
|
30
|
+
}
|
|
25
31
|
|
|
26
|
-
useConfig.mockReturnValue({ mapProvider, ...overrides.config })
|
|
32
|
+
useConfig.mockReturnValue({ mapProvider, eventBus, ...overrides.config })
|
|
27
33
|
useApp.mockReturnValue({ dispatch, layoutRefs, ...overrides.app })
|
|
28
34
|
useMap.mockReturnValue({ mapSize: 'md', ...overrides.map })
|
|
29
35
|
|
|
30
36
|
getSafeZoneInset.mockReturnValue({ top: 10, right: 5, bottom: 15, left: 5 })
|
|
31
37
|
scalePoints.mockReturnValue({ top: 20, right: 10, bottom: 30, left: 10 })
|
|
32
38
|
|
|
33
|
-
return { dispatch, layoutRefs, mapProvider }
|
|
39
|
+
return { dispatch, layoutRefs, mapProvider, eventBus, capturedHandlers }
|
|
34
40
|
}
|
|
35
41
|
|
|
36
42
|
describe('useMapProviderOverrides', () => {
|
|
@@ -133,4 +139,47 @@ describe('useMapProviderOverrides', () => {
|
|
|
133
139
|
|
|
134
140
|
expect(mapProvider.fitToBounds).not.toBe(firstOverride)
|
|
135
141
|
})
|
|
142
|
+
|
|
143
|
+
test('subscribes to MAP_FIT_TO_BOUNDS and MAP_SET_VIEW on eventBus', () => {
|
|
144
|
+
const { eventBus } = setup()
|
|
145
|
+
renderHook(() => useMapProviderOverrides())
|
|
146
|
+
|
|
147
|
+
expect(eventBus.on).toHaveBeenCalledWith('map:fittobounds', expect.any(Function))
|
|
148
|
+
expect(eventBus.on).toHaveBeenCalledWith('map:setview', expect.any(Function))
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
test('MAP_FIT_TO_BOUNDS event forwards bbox to mapProvider.fitToBounds', () => {
|
|
152
|
+
const { mapProvider, capturedHandlers } = setup()
|
|
153
|
+
const originalFitToBounds = mapProvider.fitToBounds
|
|
154
|
+
renderHook(() => useMapProviderOverrides())
|
|
155
|
+
|
|
156
|
+
capturedHandlers['map:fittobounds']([0, 0, 1, 1])
|
|
157
|
+
|
|
158
|
+
expect(originalFitToBounds).toHaveBeenCalledWith([0, 0, 1, 1])
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
test('MAP_SET_VIEW event forwards opts to mapProvider.setView', () => {
|
|
162
|
+
const { mapProvider, capturedHandlers } = setup()
|
|
163
|
+
const originalSetView = mapProvider.setView
|
|
164
|
+
renderHook(() => useMapProviderOverrides())
|
|
165
|
+
|
|
166
|
+
capturedHandlers['map:setview']({ center: [1, 2], zoom: 10 })
|
|
167
|
+
|
|
168
|
+
expect(originalSetView).toHaveBeenCalledWith({ center: [1, 2], zoom: 10 })
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
test('unsubscribes from MAP_FIT_TO_BOUNDS and MAP_SET_VIEW on unmount', () => {
|
|
172
|
+
const { eventBus } = setup()
|
|
173
|
+
const { unmount } = renderHook(() => useMapProviderOverrides())
|
|
174
|
+
|
|
175
|
+
unmount()
|
|
176
|
+
|
|
177
|
+
expect(eventBus.off).toHaveBeenCalledWith('map:fittobounds', expect.any(Function))
|
|
178
|
+
expect(eventBus.off).toHaveBeenCalledWith('map:setview', expect.any(Function))
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
test('skips event subscriptions when eventBus is null', () => {
|
|
182
|
+
setup({ config: { eventBus: null } })
|
|
183
|
+
expect(() => renderHook(() => useMapProviderOverrides())).not.toThrow()
|
|
184
|
+
})
|
|
136
185
|
})
|
|
@@ -94,13 +94,13 @@ export const Layout = () => {
|
|
|
94
94
|
<div className='im-o-app__actions' ref={layoutRefs.actionsRef}>
|
|
95
95
|
<SlotRenderer slot={layoutSlots.ACTIONS} />
|
|
96
96
|
</div>
|
|
97
|
+
<div className='im-o-app__modal' ref={layoutRefs.modalRef}>
|
|
98
|
+
<SlotRenderer slot={layoutSlots.MODAL} />
|
|
99
|
+
<div className='im-o-app__modal-backdrop' />
|
|
100
|
+
</div>
|
|
97
101
|
</div>
|
|
98
102
|
</div>
|
|
99
103
|
<HtmlElementHost />
|
|
100
|
-
<div className='im-o-app__modal' ref={layoutRefs.modalRef}>
|
|
101
|
-
<SlotRenderer slot={layoutSlots.MODAL} />
|
|
102
|
-
<div className='im-o-app__modal-backdrop' />
|
|
103
|
-
</div>
|
|
104
104
|
</div>
|
|
105
105
|
)
|
|
106
106
|
}
|
|
@@ -4,16 +4,7 @@ import { deepMerge } from '../../utils/deepMerge.js'
|
|
|
4
4
|
|
|
5
5
|
// Pure utility functions for panel registry operations
|
|
6
6
|
export const registerPanel = (currentConfig, panel) => {
|
|
7
|
-
|
|
8
|
-
Object.entries(panel).map(([key, value]) => [
|
|
9
|
-
key,
|
|
10
|
-
{
|
|
11
|
-
showLabel: true,
|
|
12
|
-
...value
|
|
13
|
-
}
|
|
14
|
-
])
|
|
15
|
-
)
|
|
16
|
-
return { ...currentConfig, ...normalizedPanelConfig }
|
|
7
|
+
return { ...currentConfig, ...panel }
|
|
17
8
|
}
|
|
18
9
|
|
|
19
10
|
export const addPanel = (currentConfig, id, config) => {
|
|
@@ -2,31 +2,28 @@ import { createPanelRegistry, registerPanel, addPanel, removePanel, getPanelConf
|
|
|
2
2
|
import { defaultPanelConfig } from '../../config/appConfig.js'
|
|
3
3
|
|
|
4
4
|
describe('panelRegistry', () => {
|
|
5
|
-
test('registerPanel should store a panel
|
|
5
|
+
test('registerPanel should store a panel', () => {
|
|
6
6
|
const panel = { settings: { title: 'Settings Panel' } }
|
|
7
7
|
const config = registerPanel({}, panel)
|
|
8
8
|
expect(config).toEqual({
|
|
9
9
|
settings: {
|
|
10
|
-
title: 'Settings Panel'
|
|
11
|
-
showLabel: true
|
|
10
|
+
title: 'Settings Panel'
|
|
12
11
|
}
|
|
13
12
|
})
|
|
14
13
|
})
|
|
15
14
|
|
|
16
15
|
test('registerPanel should merge multiple panels', () => {
|
|
17
16
|
const panel1 = { settings: { title: 'Settings Panel' } }
|
|
18
|
-
const panel2 = { dashboard: { title: 'Dashboard Panel'
|
|
17
|
+
const panel2 = { dashboard: { title: 'Dashboard Panel' } }
|
|
19
18
|
let config = {}
|
|
20
19
|
config = registerPanel(config, panel1)
|
|
21
20
|
config = registerPanel(config, panel2)
|
|
22
21
|
expect(config).toEqual({
|
|
23
22
|
settings: {
|
|
24
|
-
title: 'Settings Panel'
|
|
25
|
-
showLabel: true
|
|
23
|
+
title: 'Settings Panel'
|
|
26
24
|
},
|
|
27
25
|
dashboard: {
|
|
28
|
-
title: 'Dashboard Panel'
|
|
29
|
-
showLabel: false
|
|
26
|
+
title: 'Dashboard Panel'
|
|
30
27
|
}
|
|
31
28
|
})
|
|
32
29
|
})
|
|
@@ -37,8 +34,7 @@ describe('panelRegistry', () => {
|
|
|
37
34
|
const result = getPanelConfig(config)
|
|
38
35
|
expect(result).toEqual({
|
|
39
36
|
reports: {
|
|
40
|
-
title: 'Reports Panel'
|
|
41
|
-
showLabel: true
|
|
37
|
+
title: 'Reports Panel'
|
|
42
38
|
}
|
|
43
39
|
})
|
|
44
40
|
})
|
|
@@ -100,7 +96,6 @@ describe('panelRegistry', () => {
|
|
|
100
96
|
// Test registerPanel state
|
|
101
97
|
registry.registerPanel({ p1: { title: 'P1' } })
|
|
102
98
|
expect(registry.getPanelConfig()).toHaveProperty('p1')
|
|
103
|
-
expect(registry.getPanelConfig().p1.showLabel).toBe(true)
|
|
104
99
|
|
|
105
100
|
// Test addPanel state and return value
|
|
106
101
|
const added = registry.addPanel('p2', { title: 'P2' })
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// src/App/renderer/HtmlElementHost.jsx
|
|
2
|
-
import React, { useRef,
|
|
2
|
+
import React, { useRef, useLayoutEffect, useMemo } from 'react'
|
|
3
3
|
import { useApp } from '../store/appContext.js'
|
|
4
4
|
import { Panel } from '../components/Panel/Panel.jsx'
|
|
5
5
|
import { resolveTargetSlot, isModeAllowed, isControlVisible, isConsumerHtml } from './slotHelpers.js'
|
|
@@ -31,16 +31,24 @@ export const getSlotRef = (slot, layoutRefs) => {
|
|
|
31
31
|
* (e.g. the banner slot swaps DOM nodes between mobile and desktop).
|
|
32
32
|
*/
|
|
33
33
|
export const useDomProjection = (wrapperRef, targetSlot, isVisible, layoutRefs, breakpoint) => {
|
|
34
|
-
|
|
34
|
+
useLayoutEffect(() => {
|
|
35
35
|
const wrapper = wrapperRef.current
|
|
36
36
|
|
|
37
37
|
if (isVisible) {
|
|
38
38
|
const slotRef = getSlotRef(targetSlot, layoutRefs)
|
|
39
39
|
if (slotRef?.current) {
|
|
40
|
-
slotRef.current.
|
|
40
|
+
const backdrop = slotRef.current.querySelector(':scope > .im-o-app__modal-backdrop')
|
|
41
|
+
if (backdrop) {
|
|
42
|
+
slotRef.current.insertBefore(wrapper, backdrop)
|
|
43
|
+
} else {
|
|
44
|
+
slotRef.current.appendChild(wrapper)
|
|
45
|
+
}
|
|
41
46
|
wrapper.style.display = ''
|
|
42
47
|
}
|
|
43
48
|
} else {
|
|
49
|
+
if (wrapper.parentElement === layoutRefs.modalRef?.current) {
|
|
50
|
+
layoutRefs.appContainerRef?.current?.appendChild(wrapper)
|
|
51
|
+
}
|
|
44
52
|
wrapper.style.display = 'none'
|
|
45
53
|
}
|
|
46
54
|
|
|
@@ -199,6 +199,95 @@ describe('HtmlElementHost', () => {
|
|
|
199
199
|
expect(container.firstChild).toBeNull() // still renders safely
|
|
200
200
|
})
|
|
201
201
|
|
|
202
|
+
it('inserts panel before backdrop when modal slot contains a backdrop element', () => {
|
|
203
|
+
const modalRef = React.createRef()
|
|
204
|
+
const appContainerRef = React.createRef()
|
|
205
|
+
const refs = { ...layoutRefs, modalRef, appContainerRef }
|
|
206
|
+
|
|
207
|
+
mockApp({
|
|
208
|
+
panelConfig: { p1: { html: '<p>Hi</p>', label: 'Menu', desktop: { slot: 'side', modal: true } } },
|
|
209
|
+
openPanels: { p1: { props: {} } },
|
|
210
|
+
layoutRefs: refs
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
render(
|
|
214
|
+
<div>
|
|
215
|
+
<div ref={appContainerRef}>
|
|
216
|
+
<div ref={modalRef}>
|
|
217
|
+
<div className='im-o-app__modal-backdrop' />
|
|
218
|
+
</div>
|
|
219
|
+
</div>
|
|
220
|
+
<HtmlElementHost />
|
|
221
|
+
</div>
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
const children = Array.from(modalRef.current.children)
|
|
225
|
+
const panelIndex = children.findIndex(el => el.dataset.testid === 'panel-p1')
|
|
226
|
+
const backdropIndex = children.findIndex(el => el.classList.contains('im-o-app__modal-backdrop'))
|
|
227
|
+
|
|
228
|
+
expect(panelIndex).toBeGreaterThanOrEqual(0)
|
|
229
|
+
expect(panelIndex).toBeLessThan(backdropIndex)
|
|
230
|
+
})
|
|
231
|
+
|
|
232
|
+
it('moves panel from modal to appContainerRef when hidden', () => {
|
|
233
|
+
// Create refs
|
|
234
|
+
const modalRef = React.createRef()
|
|
235
|
+
const appContainerRef = React.createRef()
|
|
236
|
+
const refs = { ...layoutRefs, modalRef, appContainerRef }
|
|
237
|
+
|
|
238
|
+
// Panel config: modal slot
|
|
239
|
+
const panelConfig = {
|
|
240
|
+
p1: { html: '<p>Hi</p>', label: 'Menu', desktop: { slot: 'modal', modal: true } }
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// App state: panel initially open
|
|
244
|
+
mockApp({
|
|
245
|
+
panelConfig,
|
|
246
|
+
openPanels: { p1: { props: {} } },
|
|
247
|
+
layoutRefs: refs
|
|
248
|
+
})
|
|
249
|
+
|
|
250
|
+
// Render a harness that includes both refs
|
|
251
|
+
const { rerender } = render(
|
|
252
|
+
<div>
|
|
253
|
+
<div ref={appContainerRef} />
|
|
254
|
+
<div ref={modalRef} />
|
|
255
|
+
<HtmlElementHost />
|
|
256
|
+
</div>
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
// Now it should actually be inside modalRef
|
|
260
|
+
const panelInModal = modalRef.current.querySelector('[data-testid="panel-p1"]')
|
|
261
|
+
expect(panelInModal).toBeTruthy()
|
|
262
|
+
expect(modalRef.current.contains(panelInModal)).toBe(true)
|
|
263
|
+
|
|
264
|
+
// Close the panel
|
|
265
|
+
useApp.mockReturnValue({
|
|
266
|
+
breakpoint: 'desktop',
|
|
267
|
+
mode: 'view',
|
|
268
|
+
isFullscreen: false,
|
|
269
|
+
panelConfig,
|
|
270
|
+
controlConfig: {},
|
|
271
|
+
openPanels: {}, // closed now
|
|
272
|
+
layoutRefs: refs,
|
|
273
|
+
dispatch: jest.fn()
|
|
274
|
+
})
|
|
275
|
+
|
|
276
|
+
rerender(
|
|
277
|
+
<div>
|
|
278
|
+
<div ref={appContainerRef} />
|
|
279
|
+
<div ref={modalRef} />
|
|
280
|
+
<HtmlElementHost />
|
|
281
|
+
</div>
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
// It should have moved to appContainerRef
|
|
285
|
+
const panelInApp = appContainerRef.current.querySelector('[data-testid="panel-p1"]')
|
|
286
|
+
expect(panelInApp).toBeTruthy()
|
|
287
|
+
expect(appContainerRef.current.contains(panelInApp)).toBe(true)
|
|
288
|
+
expect(modalRef.current.contains(panelInApp)).toBe(false)
|
|
289
|
+
})
|
|
290
|
+
|
|
202
291
|
test('getSlotRef returns null for unknown slot', () => {
|
|
203
292
|
expect(getSlotRef('unknown-slot', {})).toBeNull()
|
|
204
293
|
})
|