Skip to content

Commit dae0065

Browse files
Merge pull request #22 from SunshineLuke90/abls-improvements
Update - Improved expand layers list
2 parents c0c8191 + 99f86bd commit dae0065

6 files changed

Lines changed: 362 additions & 228 deletions

File tree

ABLS/README.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,4 +17,13 @@ A prime example of where this widget is used can be found in the [SEMA Daily Bri
1717

1818
Implementation is simple. Just drag the widget into your application, connect it to a map if it doesn't automatically connect, then create a view and select all of the layers you want visible (Including basemap layers). The layer order will be maintained, it's just the visibility that will be changed. The first view that you choose will be what the map will appear with the first time the ABLS widget is loaded.
1919

20+
Expandable layers can also be set, to act as a sort of "quick reference" layer visibility list. The layers in the expandable list can be either layers that would be visible within the view, or layers that would not be visible in the view. This can be helpful for setups where you have multiple themes of data, but within each theme there are reference layers that won't always be needed. Adding a default layers widget would be too much in this case, as there would be so many layers that it's not easy to scroll through, which is where the expandable layers fits in!
21+
2022
If the data you are switching views between is time enabled, you can also apply time filtering on the view. Time filtering is relatively self explanatory, but in general, is used to apply dynamic time filtering as an offset from the current time. This is good for forecast maps, or to view past information, like in an EOC setting. If your layers are not time enabled, do not turn on time filtering to speed up load time.
23+
24+
As of version 1.1.1, the layer selection in the settings panel has been improved in a couple of ways.
25+
26+
1. The layers show in the same order as the drawing order (Higher layers on top)
27+
2. The layers support group layers and sub layers.
28+
29+
This improves the setup process for the widget when dealing with a map that has lots of layers, especially when it contains many group layers.

ABLS/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "abls",
3-
"version": "1.1.0",
3+
"version": "1.1.1",
44
"description": "A Better Layer Switcher - A custom Experience Widget, to swith between sets of layers using a views navigation like interface.",
55
"keywords": [
66
"exb-widget",

ABLS/src/runtime/style.css

Lines changed: 0 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -23,20 +23,4 @@
2323
margin-left: 4px;
2424
vertical-align: middle;
2525
opacity: 0.75;
26-
}
27-
28-
/* Wrapper that anchors the measurement ref */
29-
.view-buttons-wrapper {
30-
position: relative;
31-
}
32-
33-
/* Floating layer list rendered via portal into document.body */
34-
.expanded-layers-list {
35-
position: fixed;
36-
min-width: 220px;
37-
max-width: 360px;
38-
width: max-content;
39-
max-height: 300px;
40-
overflow-y: auto;
41-
z-index: 9999;
4226
}

ABLS/src/runtime/widget.tsx

Lines changed: 56 additions & 177 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import { React, jsx } from "jimu-core"
2-
import ReactDOM from "react-dom"
32
//import { lazy } from 'react'
43
import type { AllWidgetProps } from "jimu-core"
54
import { JimuMapViewComponent, type JimuMapView } from "jimu-arcgis"
@@ -22,11 +21,6 @@ export default function Widget (props: AllWidgetProps<Config>) {
2221

2322
const [expand, setExpand] = React.useState(false)
2423
const [expandedLayers, setExpandedLayers] = React.useState<Layer[]>([])
25-
const [visibleLayers, setVisibleLayers] = React.useState<Layer[]>([])
26-
const [listStyle, setListStyle] = React.useState<React.CSSProperties>({})
27-
const buttonsWrapperRef = React.useRef<HTMLDivElement>(null)
28-
const listRef = React.useRef<any>(null)
29-
const focusButtonOnCollapseRef = React.useRef(false)
3024

3125
// This is the way that the widget prevents itself from running itself, and from crashing. It checks to see if any maps have been selected, and if any views have been configured.
3226
const isConfigured = useMapWidgetIds?.length > 0 && config.views?.length > 0
@@ -36,26 +30,18 @@ export default function Widget (props: AllWidgetProps<Config>) {
3630
if (expand && activeViewId === view.id) {
3731
setExpand(false)
3832
setExpandedLayers([])
39-
setVisibleLayers([])
4033
return
4134
}
4235
if (jimuMapView?.view?.map?.allLayers) {
43-
const allLayers = jimuMapView.view.map.allLayers.toArray().reverse()
44-
allLayers.forEach((layer) => {
45-
if (view.expandLayerIds.includes(layer.id)) {
46-
setExpandedLayers((prev) => [...prev, layer])
47-
if (layer.visible && !visibleLayers.includes(layer)) {
48-
setVisibleLayers((prev) => [...prev, layer])
49-
}
50-
else if (!layer.visible && visibleLayers.includes(layer)) {
51-
setVisibleLayers((prev) => prev.filter((l) => l !== layer))
52-
}
53-
}
54-
})
55-
setExpand(true)
36+
const nextExpandedLayers = jimuMapView.view.map.allLayers
37+
.toArray()
38+
.reverse()
39+
.filter((layer) => view.expandLayerIds.includes(layer.id))
40+
setExpandedLayers(nextExpandedLayers)
41+
setExpand(nextExpandedLayers.length > 0)
5642
}
5743
},
58-
[expand, activeViewId, jimuMapView, visibleLayers]
44+
[expand, activeViewId, jimuMapView]
5945
)
6046

6147

@@ -121,113 +107,6 @@ export default function Widget (props: AllWidgetProps<Config>) {
121107
[jimuMapView]
122108
)
123109

124-
const focusActiveButton = useCallback(() => {
125-
if (!activeViewId || !buttonsWrapperRef.current) return
126-
const btn = buttonsWrapperRef.current.querySelector(`[data-view-id="${activeViewId}"]`)
127-
if (!btn) return
128-
const setFocusFn = Reflect.get(btn, 'setFocus')
129-
if (typeof setFocusFn === 'function') {
130-
setFocusFn.call(btn)
131-
} else if (btn instanceof HTMLElement) {
132-
btn.focus()
133-
}
134-
}, [activeViewId])
135-
136-
React.useEffect(() => {
137-
if (expand && buttonsWrapperRef.current) {
138-
const rect = buttonsWrapperRef.current.getBoundingClientRect()
139-
const spaceAbove = rect.top
140-
const spaceBelow = window.innerHeight - rect.bottom
141-
const openUpward = spaceAbove >= 150 || spaceAbove >= spaceBelow
142-
setListStyle(
143-
openUpward
144-
? { bottom: window.innerHeight - rect.top, left: rect.left }
145-
: { top: rect.bottom, left: rect.left }
146-
)
147-
}
148-
}, [expand])
149-
150-
// Focus the first list item shortly after the list opens
151-
React.useEffect(() => {
152-
if (!expand) return
153-
const timer = setTimeout(() => {
154-
const firstItem = listRef.current?.querySelector('calcite-list-item')
155-
firstItem?.setFocus?.()
156-
}, 50)
157-
return () => { clearTimeout(timer) }
158-
}, [expand])
159-
160-
// Keyboard navigation: Escape closes the list; ArrowDown on the last item returns focus to the active button
161-
React.useEffect(() => {
162-
const el = listRef.current
163-
if (!expand || !el) return
164-
const handleKeyDown = (e: KeyboardEvent) => {
165-
if (e.key === 'Escape') {
166-
e.stopPropagation()
167-
focusButtonOnCollapseRef.current = true
168-
setExpand(false)
169-
setExpandedLayers([])
170-
setVisibleLayers([])
171-
return
172-
}
173-
if (e.key === 'ArrowDown') {
174-
const allItems = Array.from(el.querySelectorAll('calcite-list-item'))
175-
const lastItem = allItems[allItems.length - 1]
176-
if (lastItem && document.activeElement === lastItem) {
177-
e.preventDefault()
178-
focusActiveButton()
179-
}
180-
}
181-
}
182-
el.addEventListener('keydown', handleKeyDown, true)
183-
return () => { el.removeEventListener('keydown', handleKeyDown, true) }
184-
}, [expand, focusActiveButton])
185-
186-
// ArrowUp on the active button navigates to the last item in the open list
187-
React.useEffect(() => {
188-
if (!expand || !buttonsWrapperRef.current) return
189-
const wrapper = buttonsWrapperRef.current
190-
const handleButtonKeyDown = (e: KeyboardEvent) => {
191-
if (e.key !== 'ArrowUp') return
192-
const target = e.target as HTMLElement
193-
if (!target.closest(`[data-view-id="${activeViewId}"]`)) return
194-
const allItems = Array.from(listRef.current?.querySelectorAll('calcite-list-item') ?? [])
195-
const lastItem = allItems[allItems.length - 1] as any
196-
if (lastItem) {
197-
e.preventDefault()
198-
lastItem.setFocus?.()
199-
}
200-
}
201-
wrapper.addEventListener('keydown', handleButtonKeyDown, true)
202-
return () => { wrapper.removeEventListener('keydown', handleButtonKeyDown, true) }
203-
}, [expand, activeViewId])
204-
205-
// Click outside both the list and the buttons wrapper collapses the list
206-
React.useEffect(() => {
207-
if (!expand) return
208-
const handleClickOutside = (e: MouseEvent) => {
209-
const target = e.target as Node
210-
if (
211-
!(listRef.current?.contains(target)) &&
212-
!(buttonsWrapperRef.current?.contains(target))
213-
) {
214-
setExpand(false)
215-
setExpandedLayers([])
216-
setVisibleLayers([])
217-
}
218-
}
219-
document.addEventListener('mousedown', handleClickOutside)
220-
return () => { document.removeEventListener('mousedown', handleClickOutside) }
221-
}, [expand])
222-
223-
// After a collapse-with-focus was requested, focus the active button once the list unmounts
224-
React.useEffect(() => {
225-
if (!expand && focusButtonOnCollapseRef.current) {
226-
focusButtonOnCollapseRef.current = false
227-
focusActiveButton()
228-
}
229-
}, [expand, focusActiveButton])
230-
231110
React.useEffect(() => {
232111
// Check if the map is loaded, if views are configured, and if no view is active yet.
233112
if (jimuMapView && !activeViewId && config.views?.length > 0) {
@@ -293,6 +172,20 @@ export default function Widget (props: AllWidgetProps<Config>) {
293172
return (
294173
// Outer Div for the entire widget
295174
<div className="widget-abls jimu-widget">
175+
<calcite-popover
176+
referenceElement={activeViewId}
177+
label="expanded layers"
178+
open={expand && expandedLayers.length > 0}
179+
heading="Layers"
180+
>
181+
<calcite-list
182+
label="Expanded Layers"
183+
selectionMode="multiple"
184+
displayMode="nested"
185+
>
186+
{topLevelLayers.map((layer) => renderLayerItem(layer))}
187+
</calcite-list>
188+
</calcite-popover>
296189
{useMapWidgetIds?.length > 0 && (
297190
// Set a link to the map, so that when the views change, the layers on the map will actually change.
298191
<JimuMapViewComponent
@@ -302,56 +195,42 @@ export default function Widget (props: AllWidgetProps<Config>) {
302195
}}
303196
/>
304197
)}
305-
<div className="view-buttons-wrapper" ref={buttonsWrapperRef}>
306-
{expand && expandedLayers.length > 0 && ReactDOM.createPortal(
307-
<calcite-list
308-
ref={listRef}
309-
label="Expanded Layers"
310-
className="expanded-layers-list"
311-
selectionMode="multiple"
312-
displayMode="nested"
313-
style={listStyle}
314-
>
315-
{topLevelLayers.map((layer) => renderLayerItem(layer))}
316-
</calcite-list>,
317-
document.body
198+
<div className="view-buttons-container">
199+
{config.views.map(
200+
(
201+
view //The .map function creates the contained items multiple times, one button for each view in this case.
202+
) => (
203+
<calcite-button
204+
key={view.id}
205+
data-view-id={view.id}
206+
className={`view-button ${activeViewId === view.id ? "active" : ""
207+
}`}
208+
title={view.name}
209+
id={view.id}
210+
onClick={() => {
211+
if (activeViewId === view.id) {
212+
handleViewExpand(view)
213+
return
214+
}
215+
handleViewChange(view)
216+
}}
217+
appearance={activeViewId === view.id ? "solid" : "transparent"}
218+
kind={activeViewId === view.id ? "brand" : "neutral"}
219+
>
220+
{view.icon && (
221+
<Icon icon={view.icon.svg} size="16" className="mr-2" />
222+
)}
223+
{view.name}
224+
{view.expandLayerIds?.length > 0 && (
225+
<Icon
226+
icon={expand && activeViewId === view.id ? chevronUpSvg : chevronDownSvg}
227+
size="12"
228+
className="expand-handle-icon"
229+
/>
230+
)}
231+
</calcite-button>
232+
)
318233
)}
319-
<div className="view-buttons-container">
320-
{config.views.map(
321-
(
322-
view //The .map function creates the contained items multiple times, one button for each view in this case.
323-
) => (
324-
<calcite-button
325-
key={view.id}
326-
data-view-id={view.id}
327-
className={`view-button ${activeViewId === view.id ? "active" : ""
328-
}`}
329-
title={view.name}
330-
onClick={() => {
331-
if (activeViewId === view.id) {
332-
handleViewExpand(view)
333-
return
334-
}
335-
handleViewChange(view)
336-
}}
337-
appearance={activeViewId === view.id ? "solid" : "transparent"}
338-
kind={activeViewId === view.id ? "brand" : "neutral"}
339-
>
340-
{view.icon && (
341-
<Icon icon={view.icon.svg} size="16" className="mr-2" />
342-
)}
343-
{view.name}
344-
{view.expandLayerIds?.length > 0 && (
345-
<Icon
346-
icon={expand && activeViewId === view.id ? chevronUpSvg : chevronDownSvg}
347-
size="12"
348-
className="expand-handle-icon"
349-
/>
350-
)}
351-
</calcite-button>
352-
)
353-
)}
354-
</div>
355234
</div>
356235
</div>
357236
)

0 commit comments

Comments
 (0)