farmcontrol-ui/src/components/Dashboard/common/DashboardSidebarSplitter.jsx
Tom Butcher 1b98f6a716
All checks were successful
farmcontrol/farmcontrol-ui/pipeline/head This commit looks good
Add Adaptive Splitter Component and Update Styles
- Introduced a new AdaptiveSplitter component to enhance layout flexibility in the dashboard, allowing for dynamic resizing of panels.
- Updated App.css to include styles for the new adaptive splitter, ensuring consistent appearance and behavior across different orientations.
- Refactored DashboardSidebarSplitter to utilize the AdaptiveSplitter, improving sidebar management and responsiveness during resizing.
- Adjusted transition properties and sizes for smoother user experience when interacting with the splitter components.
2026-08-23 22:57:36 +01:00

212 lines
5.6 KiB
JavaScript

import {
cloneElement,
isValidElement,
useCallback,
useContext,
useRef,
useState
} from 'react'
import PropTypes from 'prop-types'
import { Layout } from 'antd'
import { useMediaQuery } from 'react-responsive'
import { ElectronContext } from '../context/ElectronContext'
import AdaptiveSplitter from './AdaptiveSplitter'
const COLLAPSE_BELOW_PX = 200
const DEFAULT_SIDEBAR_PX = 300
const MAX_SIDEBAR_PX = 400
const COLLAPSED_WIDTH_PX = 80
const ELECTRON_COLLAPSED_WIDTH_PX = 55
const EXPAND_DRAG_PX = 10
const COLLAPSE_STORAGE_KEY = 'DashboardSidebar_collapseState'
const SIZE_STORAGE_KEY = 'DashboardSidebar_width'
const readStoredBoolean = (key, fallback) => {
try {
const saved = sessionStorage.getItem(key)
return saved ? JSON.parse(saved) : fallback
} catch {
return fallback
}
}
const readStoredNumber = (key, fallback) => {
try {
const saved = sessionStorage.getItem(key)
const value = saved ? JSON.parse(saved) : fallback
return typeof value === 'number' && !Number.isNaN(value) ? value : fallback
} catch {
return fallback
}
}
const clampExpandedSize = (size) =>
Math.min(MAX_SIDEBAR_PX, Math.max(size, COLLAPSE_BELOW_PX))
const DashboardSidebarSplitter = ({ sidebar, children }) => {
const isMobile = useMediaQuery({ maxWidth: 768 })
const { isElectron } = useContext(ElectronContext)
const collapsedWidth = isElectron
? ELECTRON_COLLAPSED_WIDTH_PX
: COLLAPSED_WIDTH_PX
const [collapsed, setCollapsed] = useState(() =>
readStoredBoolean(COLLAPSE_STORAGE_KEY, false)
)
const [sidebarSize, setSidebarSize] = useState(() => {
if (readStoredBoolean(COLLAPSE_STORAGE_KEY, false)) {
return collapsedWidth
}
return clampExpandedSize(
readStoredNumber(SIZE_STORAGE_KEY, DEFAULT_SIDEBAR_PX)
)
})
const collapsedRef = useRef(collapsed)
collapsedRef.current = collapsed
const lastExpandedSizeRef = useRef(
clampExpandedSize(readStoredNumber(SIZE_STORAGE_KEY, DEFAULT_SIDEBAR_PX))
)
const persistCollapsed = (nextCollapsed) => {
sessionStorage.setItem(COLLAPSE_STORAGE_KEY, JSON.stringify(nextCollapsed))
}
const persistSize = (size) => {
sessionStorage.setItem(SIZE_STORAGE_KEY, JSON.stringify(size))
}
const collapseSidebar = useCallback(
(expandedSize) => {
if (expandedSize >= COLLAPSE_BELOW_PX) {
lastExpandedSizeRef.current = clampExpandedSize(expandedSize)
persistSize(lastExpandedSizeRef.current)
}
collapsedRef.current = true
persistCollapsed(true)
setCollapsed(true)
setSidebarSize(collapsedWidth)
},
[collapsedWidth]
)
const expandSidebar = useCallback(() => {
collapsedRef.current = false
setCollapsed(false)
persistCollapsed(false)
const nextSize = clampExpandedSize(lastExpandedSizeRef.current)
lastExpandedSizeRef.current = nextSize
setSidebarSize(nextSize)
persistSize(nextSize)
}, [])
const handleSidebarCollapse = useCallback(
(nextCollapsed) => {
if (nextCollapsed === collapsedRef.current) return
if (nextCollapsed) {
collapseSidebar(lastExpandedSizeRef.current)
} else {
expandSidebar()
}
},
[collapseSidebar, expandSidebar]
)
const handleSplitterCollapse = useCallback(
(nextCollapsed) => {
if (nextCollapsed === collapsedRef.current) return
if (nextCollapsed) {
collapseSidebar(lastExpandedSizeRef.current)
return
}
collapsedRef.current = false
persistCollapsed(false)
setCollapsed(false)
},
[collapseSidebar]
)
const handleResize = useCallback((sizes) => {
const size = sizes[0]
setSidebarSize(size)
if (!collapsedRef.current && size >= COLLAPSE_BELOW_PX) {
lastExpandedSizeRef.current = clampExpandedSize(size)
}
}, [])
const handleResizeEnd = useCallback(
(sizes) => {
if (collapsedRef.current) {
setSidebarSize(collapsedWidth)
return
}
const next = clampExpandedSize(sizes[0])
lastExpandedSizeRef.current = next
setSidebarSize(next)
persistSize(next)
},
[collapsedWidth]
)
const sidebarNode = isValidElement(sidebar)
? cloneElement(sidebar, {
collapsed,
collapsedWidth,
onCollapse: handleSidebarCollapse
})
: sidebar
if (isMobile) {
return (
<Layout>
{sidebarNode}
{children}
</Layout>
)
}
return (
<Layout style={{ height: '100%', minHeight: 0 }}>
<AdaptiveSplitter
className={`farmcontrol-splitter dashboard-splitter${
collapsed ? ' is-sidebar-collapsed' : ''
}`}
style={{ height: '100%' }}
onResize={handleResize}
onResizeEnd={handleResizeEnd}
>
<AdaptiveSplitter.Panel
className={`dashboard-splitter-sidebar-panel${
collapsed ? ' is-collapsed' : ''
}`}
size={sidebarSize}
min={collapsedWidth}
max={MAX_SIDEBAR_PX}
collapsed={collapsed}
collapsedSize={collapsedWidth}
collapseThreshold={COLLAPSE_BELOW_PX}
expandDragPx={EXPAND_DRAG_PX}
expandedSize={lastExpandedSizeRef.current}
onCollapse={handleSplitterCollapse}
style={{ overflow: 'visible' }}
>
{sidebarNode}
</AdaptiveSplitter.Panel>
<AdaptiveSplitter.Panel
min={320}
className='dashboard-splitter-content-panel'
>
{children}
</AdaptiveSplitter.Panel>
</AdaptiveSplitter>
</Layout>
)
}
DashboardSidebarSplitter.propTypes = {
sidebar: PropTypes.node.isRequired,
children: PropTypes.node.isRequired
}
export default DashboardSidebarSplitter