@defra/interactive-map 0.0.22-alpha → 0.0.24-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/assets/images/light-raster-thumb.jpg +0 -0
- package/assets/images/outdoor-raster-thumb.jpg +0 -0
- package/assets/images/road-raster-thumb.jpg +0 -0
- 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 +39 -0
- package/docs/api/context.md +4 -4
- package/docs/api/map-style-config.md +11 -2
- package/docs/api/marker-config.md +38 -7
- package/docs/api/panel-definition.md +7 -3
- package/docs/api/symbol-config.md +10 -26
- package/docs/api/symbol-registry.md +28 -13
- package/docs/api.md +24 -2
- package/docs/assets/css/{docusaurus.css → docusaurus.scss} +18 -12
- package/docs/assets/images/toggle-marker-label.jpg +0 -0
- package/docs/examples/index.mdx +12 -0
- package/docs/examples/toggle-marker-label.mdx +89 -0
- package/docs/plugins/interact.md +4 -11
- package/docs/plugins/search.md +70 -0
- package/docusaurus.config.cjs +2 -1
- package/package.json +23 -9
- package/plugins/beta/datasets/dist/adapters/maplibre/esm/im-datasets-ml-adapter.js +1 -1
- package/plugins/beta/datasets/dist/css/index.css +2 -2
- package/plugins/beta/datasets/dist/esm/im-datasets-plugin.js +1 -1
- package/plugins/beta/datasets/dist/umd/maplibre/im-datasets-maplibre-adapter.js +1 -1
- package/plugins/beta/datasets/dist/umd/maplibre/im-datasets-plugin.js +1 -1
- package/plugins/beta/datasets/dist/umd/maplibre/index.js +1 -1
- package/plugins/beta/datasets/src/adapters/maplibre/layerBuilders.js +5 -5
- package/plugins/beta/datasets/src/adapters/maplibre/maplibreLayerAdapter.js +24 -29
- package/plugins/beta/datasets/src/adapters/maplibre/patternImages.js +2 -2
- package/plugins/beta/datasets/src/adapters/maplibre/symbolImages.js +3 -3
- package/plugins/beta/datasets/src/components/KeySvg.jsx +2 -2
- package/plugins/beta/datasets/src/components/KeySvgPattern.jsx +1 -2
- package/plugins/beta/datasets/src/components/svgProperties.js +2 -2
- package/plugins/beta/datasets/src/panels/Key.module.scss +2 -2
- package/plugins/beta/draw-es/dist/esm/im-draw-es-plugin.js +1 -1
- package/plugins/beta/draw-ml/dist/css/index.css +1 -21
- package/plugins/beta/draw-ml/dist/esm/im-draw-ml-plugin.js +1 -1
- package/plugins/beta/draw-ml/dist/umd/im-draw-ml-plugin.js +1 -1
- package/plugins/beta/draw-ml/src/api/editFeature.js +9 -4
- package/plugins/beta/draw-ml/src/modes/createDrawMode.js +11 -3
- 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/dist/umd/index.js +1 -1
- 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 +1 -0
- package/plugins/interact/dist/esm/im-interact-plugin.js +1 -1
- package/plugins/interact/dist/umd/im-interact-plugin.js +1 -1
- package/plugins/interact/dist/umd/index.js +1 -1
- package/plugins/interact/src/InteractInit.jsx +23 -55
- package/plugins/interact/src/InteractInit.test.js +58 -80
- package/plugins/interact/src/defaults.js +1 -3
- package/plugins/interact/src/hooks/useAttachEvents.js +46 -0
- package/plugins/interact/src/hooks/useAttachEvents.test.js +68 -0
- package/plugins/interact/src/hooks/useCrossHairVisibility.js +47 -0
- package/plugins/interact/src/hooks/useCrossHairVisibility.test.js +54 -0
- package/plugins/interact/src/hooks/useHighlightSync.js +30 -8
- package/plugins/interact/src/hooks/useHighlightSync.test.js +127 -80
- package/plugins/interact/src/hooks/useInteractionHandlers.js +18 -17
- package/plugins/interact/src/hooks/useInteractionHandlers.test.js +22 -61
- package/plugins/interact/src/hooks/useMapItemList.js +187 -0
- package/plugins/interact/src/hooks/useMapItemList.test.js +584 -0
- package/plugins/interact/src/index.js +0 -2
- package/plugins/interact/src/index.test.js +0 -2
- package/plugins/interact/src/manifest.js +37 -6
- package/plugins/interact/src/reducer.js +8 -4
- package/plugins/interact/src/reducer.test.js +18 -3
- package/plugins/interact/src/utils/buildStylesMap.js +21 -10
- package/plugins/interact/src/utils/buildStylesMap.test.js +29 -29
- package/plugins/interact/src/utils/spatial.js +0 -15
- package/plugins/interact/src/utils/spatial.test.js +1 -13
- 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 +2 -1
- package/plugins/search/src/components/CloseButton/CloseButton.module.scss +2 -0
- package/plugins/search/src/components/Form/Form.jsx +11 -7
- package/plugins/search/src/components/Form/Form.test.jsx +23 -1
- package/plugins/search/src/components/OpenButton/OpenButton.jsx +1 -1
- package/plugins/search/src/components/OpenButton/OpenButton.module.scss +3 -0
- package/plugins/search/src/datasets.js +2 -2
- package/plugins/search/src/datasets.test.js +1 -1
- package/plugins/search/src/defaults.js +7 -1
- package/plugins/search/src/events/fetchSuggestions.js +13 -10
- package/plugins/search/src/events/fetchSuggestions.test.js +14 -1
- package/plugins/search/src/events/formHandlers.js +2 -4
- package/plugins/search/src/events/formHandlers.test.js +3 -3
- package/plugins/search/src/events/inputHandlers.js +1 -1
- package/plugins/search/src/events/inputHandlers.test.js +1 -1
- package/plugins/search/src/events/suggestionHandlers.js +2 -1
- package/plugins/search/src/events/suggestionHandlers.test.js +7 -1
- package/plugins/search/src/index.js +3 -7
- package/plugins/search/src/reducer.js +10 -5
- package/plugins/search/src/reducer.test.js +2 -1
- package/plugins/search/src/search.scss +1 -0
- package/plugins/search/src/utils/parseOsNamesResults.js +3 -3
- package/providers/beta/esri/dist/esm/im-esri-provider.js +1 -1
- package/providers/beta/esri/src/esriProvider.js +4 -0
- package/providers/beta/openlayers/dist/esm/im-openlayers-provider.js +1 -0
- package/providers/beta/openlayers/dist/esm/index.js +1 -0
- package/providers/beta/openlayers/dist/umd/im-openlayers-provider.js +2 -0
- package/providers/beta/openlayers/dist/umd/im-openlayers-provider.js.LICENSE.txt +1 -0
- package/providers/beta/openlayers/dist/umd/index.js +2 -0
- package/providers/beta/openlayers/dist/umd/index.js.LICENSE.txt +1 -0
- package/providers/beta/openlayers/src/appEvents.js +50 -0
- package/providers/beta/openlayers/src/appEvents.test.js +140 -0
- package/providers/beta/openlayers/src/defaults.js +36 -0
- package/providers/beta/openlayers/src/index.js +21 -0
- package/providers/beta/openlayers/src/mapEvents.js +132 -0
- package/providers/beta/openlayers/src/mapEvents.test.js +197 -0
- package/providers/beta/openlayers/src/openlayersProvider.js +234 -0
- package/providers/beta/openlayers/src/openlayersProvider.test.js +331 -0
- package/providers/beta/openlayers/src/utils/spatial.js +142 -0
- package/providers/beta/openlayers/src/utils/spatial.test.js +117 -0
- package/providers/beta/openlayers/src/utils/zoom.js +26 -0
- package/providers/beta/openlayers/src/utils/zoom.test.js +23 -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/index.js +1 -1
- package/providers/maplibre/src/index.test.js +9 -14
- package/providers/maplibre/src/mapEvents.js +11 -5
- package/providers/maplibre/src/mapEvents.test.js +14 -3
- package/providers/maplibre/src/maplibreProvider.js +18 -11
- package/providers/maplibre/src/maplibreProvider.test.js +43 -36
- package/providers/maplibre/src/utils/highlightFeatures.js +112 -57
- package/providers/maplibre/src/utils/highlightFeatures.test.js +179 -51
- package/providers/maplibre/src/utils/maplibreFixes.js +10 -0
- package/providers/maplibre/src/utils/maplibreFixes.test.js +31 -0
- package/providers/maplibre/src/utils/patternImages.js +22 -19
- package/providers/maplibre/src/utils/patternImages.test.js +146 -167
- package/providers/maplibre/src/utils/rasteriseToImageData.js +1 -4
- package/providers/maplibre/src/utils/rasteriseToImageData.test.js +23 -24
- package/providers/maplibre/src/utils/symbolImages.js +23 -86
- package/providers/maplibre/src/utils/symbolImages.test.js +60 -105
- package/rollup.esm.mjs +14 -3
- package/src/App/components/Actions/Actions.module.scss +1 -0
- package/src/App/components/Actions/Actions.test.jsx +17 -4
- package/src/App/components/Hints/Hints.jsx +52 -0
- package/src/App/components/Hints/Hints.module.scss +37 -0
- package/src/App/components/Hints/Hints.test.jsx +104 -0
- package/src/App/components/KeyboardHelp/KeyboardHelp.jsx +57 -19
- package/src/App/components/KeyboardHelp/KeyboardHelp.module.scss +10 -0
- package/src/App/components/KeyboardHelp/KeyboardHelp.test.jsx +140 -31
- package/src/App/components/Markers/LabelMarker.jsx +14 -0
- package/src/App/components/Markers/LabelMarker.test.jsx +36 -0
- package/src/App/components/Markers/Markers.jsx +44 -75
- package/src/App/components/Markers/Markers.module.scss +58 -0
- package/src/App/components/Markers/Markers.test.jsx +90 -113
- package/src/App/components/Markers/SymbolLabelMarker.jsx +43 -0
- package/src/App/components/Markers/SymbolLabelMarker.test.jsx +56 -0
- package/src/App/components/Markers/SymbolMarker.jsx +31 -0
- package/src/App/components/Markers/SymbolMarker.test.jsx +72 -0
- package/src/App/components/Panel/Panel.jsx +1 -1
- package/src/App/components/Panel/Panel.module.scss +1 -0
- package/src/App/components/PopupMenu/PopupMenu.test.jsx +117 -1
- package/src/App/components/PopupMenu/usePopupMenu.js +132 -85
- package/src/App/components/Tabs/Tabs.jsx +68 -0
- package/src/App/components/Tabs/Tabs.module.scss +57 -0
- package/src/App/components/Tabs/Tabs.test.jsx +121 -0
- package/src/App/components/Tooltip/Tooltip.module.scss +1 -1
- package/src/App/components/Viewport/Features.jsx +35 -0
- package/src/App/components/Viewport/Features.module.scss +31 -0
- package/src/App/components/Viewport/Features.test.jsx +124 -0
- package/src/App/components/Viewport/MapStatus.jsx +0 -4
- package/src/App/components/Viewport/Viewport.jsx +35 -29
- package/src/App/components/Viewport/Viewport.module.scss +7 -21
- package/src/App/components/Viewport/Viewport.test.jsx +108 -65
- package/src/App/controls/keyboardActions.js +9 -3
- package/src/App/controls/keyboardActions.test.js +122 -87
- package/src/App/controls/keyboardShortcuts.js +1 -14
- package/src/App/hooks/useFeatureFocus.js +199 -0
- package/src/App/hooks/useFeatureFocus.test.js +635 -0
- package/src/App/hooks/useFeatureItems.js +30 -0
- package/src/App/hooks/useFeatureItems.test.js +98 -0
- package/src/App/hooks/useFocusVisible.js +4 -7
- package/src/App/hooks/useFocusVisible.test.js +6 -6
- package/src/App/hooks/useKeyboardHint.js +9 -34
- package/src/App/hooks/useKeyboardHint.test.js +34 -84
- package/src/App/hooks/useKeyboardShortcuts.js +8 -5
- package/src/App/hooks/useKeyboardShortcuts.test.js +16 -12
- package/src/App/hooks/useLayoutMeasurements.js +10 -4
- package/src/App/hooks/useLayoutMeasurements.test.js +30 -1
- package/src/App/hooks/useMarkerCursor.js +33 -0
- package/src/App/hooks/useMarkerCursor.test.js +87 -0
- package/src/App/hooks/useMarkersAPI.js +32 -9
- package/src/App/hooks/useMarkersAPI.test.js +163 -94
- package/src/App/hooks/useModalPanelBehaviour.test.js +2 -1
- package/src/App/layout/Layout.jsx +2 -0
- package/src/App/layout/Layout.test.jsx +3 -0
- package/src/App/registry/keyboardShortcutRegistry.js +3 -2
- package/src/App/registry/keyboardShortcutRegistry.test.js +6 -8
- package/src/App/renderer/mapButtons.js +5 -1
- package/src/App/renderer/mapButtons.test.js +37 -1
- package/src/App/store/AppProvider.test.jsx +5 -0
- package/src/App/store/MapProvider.test.jsx +8 -1
- package/src/App/store/ServiceProvider.jsx +4 -1
- package/src/App/store/ServiceProvider.test.jsx +11 -0
- package/src/App/store/appActionsMap.js +5 -1
- package/src/App/store/appActionsMap.test.js +13 -0
- package/src/App/store/appReducer.js +1 -0
- package/src/App/store/mapActionsMap.js +2 -1
- package/src/InteractiveMap/InteractiveMap.js +12 -0
- package/src/InteractiveMap/InteractiveMap.test.js +2 -0
- package/src/config/appConfig.js +3 -1
- package/src/config/appConfig.test.js +4 -0
- package/src/config/defaults.js +1 -1
- package/src/config/events.js +8 -0
- package/src/config/mapTheme.js +8 -4
- package/src/config/symbolConfig.js +23 -16
- package/src/scss/main.scss +3 -0
- package/src/scss/tools/_border-focus.scss +0 -1
- package/src/services/announcer.test.js +5 -7
- package/src/services/eventBus.js +15 -0
- package/src/services/eventBus.test.js +11 -0
- package/src/services/hints.js +47 -0
- package/src/services/hints.test.js +110 -0
- package/src/services/patternRegistry.js +69 -1
- package/src/services/patternRegistry.test.js +112 -1
- package/src/services/reverseGeocode.js +4 -0
- package/src/services/reverseGeocode.test.js +11 -1
- package/src/services/symbolRegistry.js +152 -17
- package/src/services/symbolRegistry.test.js +141 -31
- package/src/types.js +71 -14
- package/src/utils/constrainKeyboardFocus.js +5 -5
- package/src/utils/detectInterfaceType.js +27 -5
- package/src/utils/detectInterfaceType.test.js +29 -3
- package/src/utils/patternUtils.js +4 -67
- package/src/utils/patternUtils.test.js +1 -123
- package/src/utils/symbolUtils.js +11 -13
- package/src/utils/symbolUtils.test.js +18 -26
- package/webpack.dev.mjs +6 -1
- package/webpack.umd.mjs +1 -0
- package/plugins/interact/dist/css/index.css +0 -1
- package/plugins/interact/src/interact.scss +0 -24
|
@@ -23,7 +23,11 @@ const mockUseApp = {
|
|
|
23
23
|
hiddenButtons: new Set(),
|
|
24
24
|
disabledButtons: new Set(),
|
|
25
25
|
pressedButtons: new Set(),
|
|
26
|
-
|
|
26
|
+
dispatch: jest.fn(),
|
|
27
|
+
layoutRefs: {
|
|
28
|
+
appContainerRef: { current: document.body },
|
|
29
|
+
viewportRef: { current: { focus: jest.fn() } }
|
|
30
|
+
}
|
|
27
31
|
}
|
|
28
32
|
jest.mock('../../store/appContext', () => ({
|
|
29
33
|
useApp: jest.fn(() => mockUseApp)
|
|
@@ -148,6 +152,36 @@ describe('PopupMenu', () => {
|
|
|
148
152
|
expect(mockSetIsOpen).toHaveBeenCalled()
|
|
149
153
|
})
|
|
150
154
|
|
|
155
|
+
it('click on item with panelId dispatches OPEN_PANEL and closes menu', () => {
|
|
156
|
+
mockUseApp.buttonConfig = { item1: { panelId: 'myPanel' } }
|
|
157
|
+
renderMenu()
|
|
158
|
+
fireEvent.click(screen.getByText('Item 1'))
|
|
159
|
+
expect(mockUseApp.dispatch).toHaveBeenCalledWith({
|
|
160
|
+
type: 'OPEN_PANEL',
|
|
161
|
+
payload: { panelId: 'myPanel', props: { triggeringElement: mockUseApp.buttonRefs.current.instigator } }
|
|
162
|
+
})
|
|
163
|
+
expect(mockSetIsOpen).toHaveBeenCalledWith(false)
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
it('click on item with panelId and keepFocus includes focusOnOpen: false in dispatch', () => {
|
|
167
|
+
mockUseApp.buttonConfig = { item1: { panelId: 'myPanel', keepFocus: true } }
|
|
168
|
+
renderMenu()
|
|
169
|
+
fireEvent.click(screen.getByText('Item 1'))
|
|
170
|
+
expect(mockUseApp.dispatch).toHaveBeenCalledWith({
|
|
171
|
+
type: 'OPEN_PANEL',
|
|
172
|
+
payload: { panelId: 'myPanel', props: { triggeringElement: mockUseApp.buttonRefs.current.instigator }, focusOnOpen: false }
|
|
173
|
+
})
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
it('click on item with keepFocus returns focus to instigator instead of viewport', () => {
|
|
177
|
+
const focusSpy = jest.fn()
|
|
178
|
+
mockUseApp.buttonRefs.current.instigator.focus = focusSpy
|
|
179
|
+
mockUseApp.buttonConfig = { item1: { keepFocus: true } }
|
|
180
|
+
renderMenu()
|
|
181
|
+
fireEvent.click(screen.getByText('Item 1'))
|
|
182
|
+
expect(focusSpy).toHaveBeenCalled()
|
|
183
|
+
})
|
|
184
|
+
|
|
151
185
|
it('calls buttonConfig.onClick with evaluateProp if defined', () => {
|
|
152
186
|
const mockOnClick = jest.fn()
|
|
153
187
|
mockUseApp.buttonConfig = { item2: { onClick: mockOnClick } }
|
|
@@ -186,6 +220,47 @@ describe('PopupMenu', () => {
|
|
|
186
220
|
expect(mockSetIsOpen).toHaveBeenCalled()
|
|
187
221
|
})
|
|
188
222
|
|
|
223
|
+
it('Enter on item with panelId dispatches OPEN_PANEL and closes menu', () => {
|
|
224
|
+
mockUseApp.buttonConfig = { item1: { panelId: 'myPanel' } }
|
|
225
|
+
renderMenu({ startIndex: 0 })
|
|
226
|
+
fireEvent.keyDown(screen.getByRole('menu'), { key: 'Enter' })
|
|
227
|
+
expect(mockUseApp.dispatch).toHaveBeenCalledWith({
|
|
228
|
+
type: 'OPEN_PANEL',
|
|
229
|
+
payload: { panelId: 'myPanel', props: { triggeringElement: mockUseApp.buttonRefs.current.instigator } }
|
|
230
|
+
})
|
|
231
|
+
expect(mockSetIsOpen).toHaveBeenCalledWith(false)
|
|
232
|
+
})
|
|
233
|
+
|
|
234
|
+
it('Enter on item with panelId and keepFocus includes focusOnOpen: false in dispatch', () => {
|
|
235
|
+
mockUseApp.buttonConfig = { item1: { panelId: 'myPanel', keepFocus: true } }
|
|
236
|
+
renderMenu({ startIndex: 0 })
|
|
237
|
+
fireEvent.keyDown(screen.getByRole('menu'), { key: 'Enter' })
|
|
238
|
+
expect(mockUseApp.dispatch).toHaveBeenCalledWith({
|
|
239
|
+
type: 'OPEN_PANEL',
|
|
240
|
+
payload: { panelId: 'myPanel', props: { triggeringElement: mockUseApp.buttonRefs.current.instigator }, focusOnOpen: false }
|
|
241
|
+
})
|
|
242
|
+
})
|
|
243
|
+
|
|
244
|
+
it('Enter on item with keepFocus returns focus to instigator instead of viewport', () => {
|
|
245
|
+
const focusSpy = jest.fn()
|
|
246
|
+
mockUseApp.buttonRefs.current.instigator.focus = focusSpy
|
|
247
|
+
mockUseApp.buttonConfig = { item1: { keepFocus: true } }
|
|
248
|
+
renderMenu({ startIndex: 0 })
|
|
249
|
+
fireEvent.keyDown(screen.getByRole('menu'), { key: 'Enter' })
|
|
250
|
+
expect(focusSpy).toHaveBeenCalled()
|
|
251
|
+
expect(mockSetIsOpen).toHaveBeenCalledWith(false)
|
|
252
|
+
})
|
|
253
|
+
|
|
254
|
+
it('Enter on regular item focuses viewport via requestAnimationFrame', () => {
|
|
255
|
+
const rafSpy = jest.spyOn(global, 'requestAnimationFrame').mockImplementation(cb => { cb(); return 1 })
|
|
256
|
+
const focusSpy = jest.spyOn(mockUseApp.layoutRefs.viewportRef.current, 'focus')
|
|
257
|
+
renderMenu({ startIndex: 0 })
|
|
258
|
+
fireEvent.keyDown(screen.getByRole('menu'), { key: 'Enter' })
|
|
259
|
+
expect(focusSpy).toHaveBeenCalled()
|
|
260
|
+
rafSpy.mockRestore()
|
|
261
|
+
focusSpy.mockRestore()
|
|
262
|
+
})
|
|
263
|
+
|
|
189
264
|
it('does nothing if click is inside menu', () => {
|
|
190
265
|
renderMenu()
|
|
191
266
|
const element = document.createElement('div')
|
|
@@ -387,6 +462,47 @@ describe('PopupMenu', () => {
|
|
|
387
462
|
expect(items[1].onClick).not.toHaveBeenCalled()
|
|
388
463
|
expect(mockSetIsOpen).not.toHaveBeenCalled()
|
|
389
464
|
})
|
|
465
|
+
|
|
466
|
+
it('Space on item with panelId dispatches OPEN_PANEL and closes menu', () => {
|
|
467
|
+
mockUseApp.buttonConfig = { item1: { panelId: 'myPanel' } }
|
|
468
|
+
renderMenu({ startIndex: 0 })
|
|
469
|
+
fireEvent.keyDown(screen.getByRole('menu'), { key: ' ' })
|
|
470
|
+
expect(mockUseApp.dispatch).toHaveBeenCalledWith({
|
|
471
|
+
type: 'OPEN_PANEL',
|
|
472
|
+
payload: { panelId: 'myPanel', props: { triggeringElement: mockUseApp.buttonRefs.current.instigator } }
|
|
473
|
+
})
|
|
474
|
+
expect(mockSetIsOpen).toHaveBeenCalledWith(false)
|
|
475
|
+
})
|
|
476
|
+
|
|
477
|
+
it('Space on item with panelId and keepFocus includes focusOnOpen: false in dispatch', () => {
|
|
478
|
+
mockUseApp.buttonConfig = { item1: { panelId: 'myPanel', keepFocus: true } }
|
|
479
|
+
renderMenu({ startIndex: 0 })
|
|
480
|
+
fireEvent.keyDown(screen.getByRole('menu'), { key: ' ' })
|
|
481
|
+
expect(mockUseApp.dispatch).toHaveBeenCalledWith({
|
|
482
|
+
type: 'OPEN_PANEL',
|
|
483
|
+
payload: { panelId: 'myPanel', props: { triggeringElement: mockUseApp.buttonRefs.current.instigator }, focusOnOpen: false }
|
|
484
|
+
})
|
|
485
|
+
})
|
|
486
|
+
|
|
487
|
+
it('Space on item with keepFocus returns focus to instigator instead of viewport', () => {
|
|
488
|
+
const focusSpy = jest.fn()
|
|
489
|
+
mockUseApp.buttonRefs.current.instigator.focus = focusSpy
|
|
490
|
+
mockUseApp.buttonConfig = { item1: { keepFocus: true } }
|
|
491
|
+
renderMenu({ startIndex: 0 })
|
|
492
|
+
fireEvent.keyDown(screen.getByRole('menu'), { key: ' ' })
|
|
493
|
+
expect(focusSpy).toHaveBeenCalled()
|
|
494
|
+
expect(mockSetIsOpen).toHaveBeenCalledWith(false)
|
|
495
|
+
})
|
|
496
|
+
|
|
497
|
+
it('Space on regular item focuses viewport via requestAnimationFrame', () => {
|
|
498
|
+
const rafSpy = jest.spyOn(global, 'requestAnimationFrame').mockImplementation(cb => { cb(); return 1 })
|
|
499
|
+
const focusSpy = jest.spyOn(mockUseApp.layoutRefs.viewportRef.current, 'focus')
|
|
500
|
+
renderMenu({ startIndex: 0 })
|
|
501
|
+
fireEvent.keyDown(screen.getByRole('menu'), { key: ' ' })
|
|
502
|
+
expect(focusSpy).toHaveBeenCalled()
|
|
503
|
+
rafSpy.mockRestore()
|
|
504
|
+
focusSpy.mockRestore()
|
|
505
|
+
})
|
|
390
506
|
})
|
|
391
507
|
|
|
392
508
|
describe('buttonRect positioning', () => {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { useState, useMemo, useEffect } from 'react'
|
|
2
2
|
import { stringToKebab } from '../../../utils/stringToKebab.js'
|
|
3
|
+
import { useApp } from '../../store/appContext.js'
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* Computes the position and alignment style for the popup menu based on the
|
|
@@ -44,121 +45,164 @@ const getMenuStyle = (buttonRect) => {
|
|
|
44
45
|
* The synthetic event is marked _fromKeyboardActivation so handleItemClick can
|
|
45
46
|
* ignore it and avoid double-activation.
|
|
46
47
|
*
|
|
47
|
-
* @param {React.SyntheticEvent}
|
|
48
|
-
* @param {object} item
|
|
49
|
-
* @param {object} ctx
|
|
48
|
+
* @param {React.SyntheticEvent} event - The triggering React event.
|
|
49
|
+
* @param {object} item - The item being activated.
|
|
50
|
+
* @param {object} ctx - Dependencies: { buttonConfig, evaluateProp, pluginId, id }.
|
|
50
51
|
*/
|
|
51
|
-
const activateItem = (
|
|
52
|
+
const activateItem = (event, item, { buttonConfig, evaluateProp, pluginId, id }) => {
|
|
52
53
|
const menuItemConfig = buttonConfig[item.id]
|
|
53
54
|
if (typeof menuItemConfig?.onClick === 'function') {
|
|
54
|
-
menuItemConfig.onClick(
|
|
55
|
+
menuItemConfig.onClick(event, evaluateProp(ctx => ctx, pluginId))
|
|
55
56
|
} else if (typeof item.onClick === 'function') {
|
|
56
|
-
item.onClick(
|
|
57
|
+
item.onClick(event.nativeEvent)
|
|
57
58
|
} else {
|
|
58
59
|
// No action
|
|
59
60
|
}
|
|
60
|
-
if (
|
|
61
|
-
const
|
|
62
|
-
if (
|
|
61
|
+
if (event.nativeEvent instanceof KeyboardEvent) {
|
|
62
|
+
const menuItemEl = document.getElementById(`${id}-${stringToKebab(item.id)}`)
|
|
63
|
+
if (menuItemEl) {
|
|
63
64
|
const click = new MouseEvent('click', { bubbles: true, cancelable: true })
|
|
64
65
|
click._fromKeyboardActivation = true
|
|
65
|
-
|
|
66
|
+
menuItemEl.dispatchEvent(click)
|
|
66
67
|
}
|
|
67
68
|
}
|
|
68
69
|
}
|
|
69
70
|
|
|
71
|
+
const resolveInitialIndex = (startIndex, startPos, visibleIndices) => {
|
|
72
|
+
if (typeof startIndex === 'number') {
|
|
73
|
+
return startIndex
|
|
74
|
+
}
|
|
75
|
+
if (startPos === 'first') {
|
|
76
|
+
return visibleIndices[0] ?? -1
|
|
77
|
+
}
|
|
78
|
+
if (startPos === 'last') {
|
|
79
|
+
return visibleIndices[visibleIndices.length - 1] ?? -1
|
|
80
|
+
}
|
|
81
|
+
return -1
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const resolveItemFocus = (item, buttonConfig) => ({
|
|
85
|
+
panelId: buttonConfig[item.id]?.panelId ?? item.panelId,
|
|
86
|
+
keepFocus: buttonConfig[item.id]?.keepFocus ?? item.keepFocus
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
const handleMenuEnter = (event, { items, index, disabledButtons, activateCtx, instigator, dispatch, viewportRef, setIsOpen }) => {
|
|
90
|
+
event.preventDefault()
|
|
91
|
+
const item = items[index]
|
|
92
|
+
if (item && !disabledButtons.has(item.id)) {
|
|
93
|
+
const { panelId, keepFocus } = resolveItemFocus(item, activateCtx.buttonConfig)
|
|
94
|
+
if (panelId) {
|
|
95
|
+
dispatch({ type: 'OPEN_PANEL', payload: { panelId, props: { triggeringElement: instigator }, ...(keepFocus && { focusOnOpen: false }) } })
|
|
96
|
+
setIsOpen(false)
|
|
97
|
+
return
|
|
98
|
+
}
|
|
99
|
+
activateItem(event, item, activateCtx)
|
|
100
|
+
if (keepFocus) {
|
|
101
|
+
instigator.focus()
|
|
102
|
+
setIsOpen(false)
|
|
103
|
+
return
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
requestAnimationFrame(() => viewportRef.current?.focus())
|
|
107
|
+
setIsOpen(false)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const handleMenuSpace = (event, { items, index, disabledButtons, activateCtx, instigator, dispatch, viewportRef, setIsOpen }) => {
|
|
111
|
+
event.preventDefault()
|
|
112
|
+
const item = items[index]
|
|
113
|
+
if (!item || disabledButtons.has(item.id)) {
|
|
114
|
+
return
|
|
115
|
+
}
|
|
116
|
+
const { panelId, keepFocus } = resolveItemFocus(item, activateCtx.buttonConfig)
|
|
117
|
+
if (panelId) {
|
|
118
|
+
dispatch({ type: 'OPEN_PANEL', payload: { panelId, props: { triggeringElement: instigator }, ...(keepFocus && { focusOnOpen: false }) } })
|
|
119
|
+
setIsOpen(false)
|
|
120
|
+
return
|
|
121
|
+
}
|
|
122
|
+
activateItem(event, item, activateCtx)
|
|
123
|
+
if (!(item.isPressed !== undefined || item.pressedWhen)) {
|
|
124
|
+
if (keepFocus) {
|
|
125
|
+
instigator.focus()
|
|
126
|
+
} else {
|
|
127
|
+
requestAnimationFrame(() => viewportRef.current?.focus())
|
|
128
|
+
}
|
|
129
|
+
setIsOpen(false)
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
70
133
|
/**
|
|
71
134
|
* Builds the keydown handler for the menu UL. Handles Escape/Tab (close & focus),
|
|
72
135
|
* ArrowDown/Up (navigate visible items), Home/End (jump to ends),
|
|
73
136
|
* Enter (activate and close), Space (activate; close only for non-checkbox items).
|
|
74
137
|
*
|
|
75
|
-
* @param {object}
|
|
76
|
-
* @param {Array}
|
|
77
|
-
* @param {number[]}
|
|
78
|
-
* @param {number}
|
|
79
|
-
* @param {Function}
|
|
80
|
-
* @param {Set}
|
|
81
|
-
* @param {object}
|
|
82
|
-
* @param {Function}
|
|
83
|
-
* @param {object}
|
|
138
|
+
* @param {object} params
|
|
139
|
+
* @param {Array} params.items - All menu item descriptors.
|
|
140
|
+
* @param {number[]} params.visibleIndices - Indices of non-hidden items.
|
|
141
|
+
* @param {number} params.index - Currently highlighted index.
|
|
142
|
+
* @param {Function} params.setIndex - State setter for highlighted index.
|
|
143
|
+
* @param {Set} params.disabledButtons - IDs of disabled items.
|
|
144
|
+
* @param {object} params.instigator - DOM node of the trigger button.
|
|
145
|
+
* @param {Function} params.setIsOpen - Callback to close the menu.
|
|
146
|
+
* @param {object} params.activateCtx - Context passed through to activateItem.
|
|
147
|
+
* @param {Function} params.dispatch - App dispatch for OPEN_PANEL actions.
|
|
148
|
+
* @param {object} params.viewportRef - Ref to the map viewport element.
|
|
84
149
|
* @returns {Function} onKeyDown handler for the menu element.
|
|
85
150
|
*/
|
|
86
|
-
const createMenuKeyDownHandler = ({ items, visibleIndices, index, setIndex, disabledButtons, instigator, setIsOpen, activateCtx }) => {
|
|
87
|
-
const closeAndFocus = (
|
|
88
|
-
if (preventDefault &&
|
|
89
|
-
|
|
151
|
+
const createMenuKeyDownHandler = ({ items, visibleIndices, index, setIndex, disabledButtons, instigator, setIsOpen, activateCtx, dispatch, viewportRef }) => {
|
|
152
|
+
const closeAndFocus = (event, preventDefault = false) => {
|
|
153
|
+
if (preventDefault && event?.preventDefault) {
|
|
154
|
+
event.preventDefault()
|
|
90
155
|
}
|
|
91
156
|
instigator.focus()
|
|
92
157
|
setIsOpen(false)
|
|
93
158
|
}
|
|
94
159
|
|
|
95
|
-
const navigateVisible = (
|
|
96
|
-
|
|
97
|
-
const
|
|
98
|
-
if (
|
|
160
|
+
const navigateVisible = (event) => {
|
|
161
|
+
event.preventDefault()
|
|
162
|
+
const visibleCount = visibleIndices.length
|
|
163
|
+
if (visibleCount === 0) {
|
|
99
164
|
return
|
|
100
165
|
}
|
|
101
166
|
const pos = visibleIndices.indexOf(index)
|
|
102
167
|
let nextPos
|
|
103
|
-
if (
|
|
104
|
-
nextPos = pos === -1 ? 0 : (pos + 1) %
|
|
168
|
+
if (event.key === 'ArrowDown') {
|
|
169
|
+
nextPos = pos === -1 ? 0 : (pos + 1) % visibleCount
|
|
105
170
|
} else if (pos === -1) {
|
|
106
|
-
nextPos =
|
|
171
|
+
nextPos = visibleCount - 1
|
|
107
172
|
} else {
|
|
108
|
-
nextPos = (pos - 1 +
|
|
173
|
+
nextPos = (pos - 1 + visibleCount) % visibleCount
|
|
109
174
|
}
|
|
110
175
|
setIndex(visibleIndices[nextPos])
|
|
111
176
|
}
|
|
112
177
|
|
|
113
|
-
const
|
|
114
|
-
e.preventDefault()
|
|
115
|
-
const item = items[index]
|
|
116
|
-
if (item && !disabledButtons.has(item.id)) {
|
|
117
|
-
activateItem(e, item, activateCtx)
|
|
118
|
-
}
|
|
119
|
-
instigator.focus()
|
|
120
|
-
setIsOpen(false)
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
const handleSpace = (e) => {
|
|
124
|
-
e.preventDefault()
|
|
125
|
-
const item = items[index]
|
|
126
|
-
if (!item || disabledButtons.has(item.id)) {
|
|
127
|
-
return
|
|
128
|
-
}
|
|
129
|
-
activateItem(e, item, activateCtx)
|
|
130
|
-
if (!(item.isPressed !== undefined || item.pressedWhen)) {
|
|
131
|
-
instigator.focus()
|
|
132
|
-
setIsOpen(false)
|
|
133
|
-
}
|
|
134
|
-
}
|
|
178
|
+
const actionCtx = { items, index, disabledButtons, activateCtx, instigator, dispatch, viewportRef, setIsOpen }
|
|
135
179
|
|
|
136
|
-
return (
|
|
137
|
-
if (['Escape', 'Esc'].includes(
|
|
138
|
-
closeAndFocus(
|
|
180
|
+
return (event) => {
|
|
181
|
+
if (['Escape', 'Esc'].includes(event.key)) {
|
|
182
|
+
closeAndFocus(event, true)
|
|
139
183
|
return
|
|
140
184
|
}
|
|
141
|
-
if (
|
|
142
|
-
closeAndFocus(
|
|
185
|
+
if (event.key === 'Tab') {
|
|
186
|
+
closeAndFocus(event)
|
|
143
187
|
return
|
|
144
188
|
}
|
|
145
|
-
if (['ArrowDown', 'ArrowUp'].includes(
|
|
146
|
-
navigateVisible(
|
|
189
|
+
if (['ArrowDown', 'ArrowUp'].includes(event.key)) {
|
|
190
|
+
navigateVisible(event)
|
|
147
191
|
return
|
|
148
192
|
}
|
|
149
|
-
if (
|
|
193
|
+
if (event.key === 'Home' && visibleIndices.length) {
|
|
150
194
|
setIndex(visibleIndices[0])
|
|
151
195
|
return
|
|
152
196
|
}
|
|
153
|
-
if (
|
|
197
|
+
if (event.key === 'End' && visibleIndices.length) {
|
|
154
198
|
setIndex(visibleIndices[visibleIndices.length - 1])
|
|
155
199
|
return
|
|
156
200
|
}
|
|
157
|
-
if (
|
|
158
|
-
|
|
201
|
+
if (event.key === 'Enter') {
|
|
202
|
+
handleMenuEnter(event, actionCtx)
|
|
159
203
|
}
|
|
160
|
-
if (
|
|
161
|
-
|
|
204
|
+
if (event.key === ' ') {
|
|
205
|
+
handleMenuSpace(event, actionCtx)
|
|
162
206
|
}
|
|
163
207
|
}
|
|
164
208
|
}
|
|
@@ -174,7 +218,7 @@ const createMenuKeyDownHandler = ({ items, visibleIndices, index, setIndex, disa
|
|
|
174
218
|
* @param {object} params.instigator - DOM node of the button that opened the menu.
|
|
175
219
|
* @param {string} params.instigatorKey - Key used to look up instigator in buttonRefs.
|
|
176
220
|
* @param {object} params.buttonRefs - Ref map of all registered button DOM nodes.
|
|
177
|
-
* @param {object} params.buttonConfig - Config map that may override item onClick
|
|
221
|
+
* @param {object} params.buttonConfig - Config map that may override item onClick, panelId, and keepFocus.
|
|
178
222
|
* @param {Set} params.disabledButtons - IDs of currently disabled items.
|
|
179
223
|
* @param {string} params.pluginId - Plugin context passed to evaluateProp.
|
|
180
224
|
* @param {Function} params.evaluateProp - Context evaluator from useEvaluateProp.
|
|
@@ -189,6 +233,9 @@ export const usePopupMenu = ({
|
|
|
189
233
|
items, hiddenButtons, startIndex, startPos, instigator, instigatorKey,
|
|
190
234
|
buttonRefs, buttonConfig, disabledButtons, pluginId, evaluateProp, id, menuRef, setIsOpen, buttonRect
|
|
191
235
|
}) => {
|
|
236
|
+
const { dispatch, layoutRefs } = useApp()
|
|
237
|
+
const viewportRef = layoutRefs.viewportRef
|
|
238
|
+
|
|
192
239
|
const visibleIndices = useMemo(() => {
|
|
193
240
|
const visible = []
|
|
194
241
|
items.forEach((item, idx) => {
|
|
@@ -199,38 +246,38 @@ export const usePopupMenu = ({
|
|
|
199
246
|
return visible
|
|
200
247
|
}, [items, hiddenButtons])
|
|
201
248
|
|
|
202
|
-
const [index, setIndex] = useState(() =>
|
|
203
|
-
if (typeof startIndex === 'number') {
|
|
204
|
-
return startIndex
|
|
205
|
-
}
|
|
206
|
-
if (startPos === 'first') {
|
|
207
|
-
return visibleIndices[0] ?? -1
|
|
208
|
-
}
|
|
209
|
-
if (startPos === 'last') {
|
|
210
|
-
return visibleIndices[visibleIndices.length - 1] ?? -1
|
|
211
|
-
}
|
|
212
|
-
return -1
|
|
213
|
-
})
|
|
249
|
+
const [index, setIndex] = useState(() => resolveInitialIndex(startIndex, startPos, visibleIndices))
|
|
214
250
|
|
|
215
251
|
const activateCtx = { buttonConfig, evaluateProp, pluginId, id }
|
|
216
252
|
|
|
217
253
|
const handleMenuKeyDown = createMenuKeyDownHandler({
|
|
218
|
-
items, visibleIndices, index, setIndex, disabledButtons, instigator, setIsOpen, activateCtx
|
|
254
|
+
items, visibleIndices, index, setIndex, disabledButtons, instigator, setIsOpen, activateCtx, dispatch, viewportRef
|
|
219
255
|
})
|
|
220
256
|
|
|
221
|
-
const handleOutside = (
|
|
222
|
-
if (menuRef.current?.contains(
|
|
257
|
+
const handleOutside = (event) => {
|
|
258
|
+
if (menuRef.current?.contains(event.target) || buttonRefs.current[instigatorKey]?.contains(event.target)) {
|
|
223
259
|
return
|
|
224
260
|
}
|
|
225
261
|
setIsOpen(false)
|
|
226
262
|
}
|
|
227
263
|
|
|
228
|
-
const handleItemClick = (
|
|
229
|
-
if (
|
|
264
|
+
const handleItemClick = (event, item) => {
|
|
265
|
+
if (event.nativeEvent._fromKeyboardActivation || disabledButtons.has(item.id)) {
|
|
266
|
+
return
|
|
267
|
+
}
|
|
268
|
+
const { panelId, keepFocus } = resolveItemFocus(item, buttonConfig)
|
|
269
|
+
if (panelId) {
|
|
270
|
+
dispatch({ type: 'OPEN_PANEL', payload: { panelId, props: { triggeringElement: instigator }, ...(keepFocus && { focusOnOpen: false }) } })
|
|
271
|
+
setIsOpen(false)
|
|
230
272
|
return
|
|
231
273
|
}
|
|
232
274
|
setIsOpen(false)
|
|
233
|
-
activateItem(
|
|
275
|
+
activateItem(event, item, activateCtx)
|
|
276
|
+
if (keepFocus) {
|
|
277
|
+
instigator.focus()
|
|
278
|
+
} else {
|
|
279
|
+
viewportRef.current?.focus()
|
|
280
|
+
}
|
|
234
281
|
}
|
|
235
282
|
|
|
236
283
|
useEffect(() => {
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import React, { useState, useCallback } from 'react'
|
|
2
|
+
|
|
3
|
+
const toTabId = (name) => `im-c-tabs-tab-${name.toLowerCase().replaceAll(/\s+/g, '-')}`
|
|
4
|
+
const toPanelId = (name) => `im-c-tabs-panel-${name.toLowerCase().replaceAll(/\s+/g, '-')}`
|
|
5
|
+
|
|
6
|
+
// eslint-disable-next-line camelcase, react/jsx-pascal-case
|
|
7
|
+
// sonarjs/disable-next-line function-name
|
|
8
|
+
export const Tabs = ({ tabs, defaultTab }) => { // NOSONAR: project does not use PropTypes
|
|
9
|
+
const names = tabs.map(t => t.name)
|
|
10
|
+
const [activeTab, setActiveTab] = useState(defaultTab ?? names[0])
|
|
11
|
+
|
|
12
|
+
const activateTab = useCallback((name) => {
|
|
13
|
+
setActiveTab(name)
|
|
14
|
+
document.getElementById(toTabId(name))?.focus()
|
|
15
|
+
}, [])
|
|
16
|
+
|
|
17
|
+
const handleKeyDown = useCallback((e) => {
|
|
18
|
+
const idx = names.indexOf(activeTab)
|
|
19
|
+
if (e.key === 'ArrowRight') {
|
|
20
|
+
e.preventDefault()
|
|
21
|
+
activateTab(names[(idx + 1) % names.length])
|
|
22
|
+
} else if (e.key === 'ArrowLeft') {
|
|
23
|
+
e.preventDefault()
|
|
24
|
+
activateTab(names[(idx - 1 + names.length) % names.length])
|
|
25
|
+
} else if (e.key === 'Home') {
|
|
26
|
+
e.preventDefault()
|
|
27
|
+
activateTab(names[0])
|
|
28
|
+
} else if (e.key === 'End') {
|
|
29
|
+
e.preventDefault()
|
|
30
|
+
activateTab(names.at(-1))
|
|
31
|
+
} else {
|
|
32
|
+
// no action
|
|
33
|
+
}
|
|
34
|
+
}, [names, activeTab, activateTab])
|
|
35
|
+
|
|
36
|
+
const activeContent = tabs.find(t => t.name === activeTab)?.content
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<div className='im-c-tabs'>
|
|
40
|
+
<div role='tablist' className='im-c-tabs__list'>
|
|
41
|
+
{tabs.map(({ name }) => (
|
|
42
|
+
<button
|
|
43
|
+
key={name}
|
|
44
|
+
id={toTabId(name)}
|
|
45
|
+
role='tab'
|
|
46
|
+
aria-selected={activeTab === name}
|
|
47
|
+
aria-controls={toPanelId(name)}
|
|
48
|
+
tabIndex={activeTab === name ? 0 : -1}
|
|
49
|
+
className='im-c-tabs__tab'
|
|
50
|
+
onClick={() => activateTab(name)}
|
|
51
|
+
onKeyDown={handleKeyDown}
|
|
52
|
+
>
|
|
53
|
+
<span className='im-c-tabs__label'>{name}</span>
|
|
54
|
+
</button>
|
|
55
|
+
))}
|
|
56
|
+
</div>
|
|
57
|
+
<div
|
|
58
|
+
id={toPanelId(activeTab)}
|
|
59
|
+
role='tabpanel'
|
|
60
|
+
aria-labelledby={toTabId(activeTab)}
|
|
61
|
+
tabIndex={-1} // nosonar — panel has no focusable children; -1 per ARIA tabs pattern (AT reads via role/aria-labelledby)
|
|
62
|
+
className='im-c-tabs__panel'
|
|
63
|
+
>
|
|
64
|
+
{activeContent}
|
|
65
|
+
</div>
|
|
66
|
+
</div>
|
|
67
|
+
)
|
|
68
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
@use '../../../scss/tools/index' as tools;
|
|
2
|
+
|
|
3
|
+
// ===================================================
|
|
4
|
+
// Component: Tabs
|
|
5
|
+
// ===================================================
|
|
6
|
+
|
|
7
|
+
.im-c-tabs__list {
|
|
8
|
+
position: relative;
|
|
9
|
+
display: flex;
|
|
10
|
+
flex-wrap: wrap;
|
|
11
|
+
padding: 0;
|
|
12
|
+
margin: 0;
|
|
13
|
+
border-bottom: 1px solid var(--app-border-color);
|
|
14
|
+
z-index: 1;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
.im-c-tabs__tab {
|
|
18
|
+
position: relative;
|
|
19
|
+
appearance: none;
|
|
20
|
+
background: var(--button-hover-color);
|
|
21
|
+
border: 1px solid transparent;
|
|
22
|
+
border-bottom: none;
|
|
23
|
+
padding: 7.5px 15px;
|
|
24
|
+
margin: 5px 5px 5px 0;
|
|
25
|
+
cursor: pointer;
|
|
26
|
+
color: var(--foreground-color);
|
|
27
|
+
font-size: inherit;
|
|
28
|
+
font-weight: normal;
|
|
29
|
+
outline: 3px solid transparent;
|
|
30
|
+
|
|
31
|
+
&[aria-selected='true'] {
|
|
32
|
+
background: var(--background-color);
|
|
33
|
+
border-color: var(--app-border-color);
|
|
34
|
+
border-bottom-color: var(--background-color);
|
|
35
|
+
margin-top: 0;
|
|
36
|
+
margin-bottom: -1px;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
&:focus .im-c-tabs__label {
|
|
40
|
+
@include tools.link-focus;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
.im-c-tabs__tab:not([aria-selected='true']) {
|
|
45
|
+
&:hover {
|
|
46
|
+
background-color: var(--content-border-color);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
.im-c-tabs__label {
|
|
50
|
+
text-decoration: underline;
|
|
51
|
+
text-underline-offset: 3px;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
&:hover .im-c-tabs__label {
|
|
55
|
+
text-decoration-thickness: 3px;
|
|
56
|
+
}
|
|
57
|
+
}
|