Enhance Dashboard Sidebar Resizing and Transition Behavior

- Updated CSS for the DashboardSidebarSplitter to improve sidebar resizing transitions, adding new properties for smoother animations.
- Introduced state management for resize locking and pointer events to enhance user interaction during sidebar adjustments.
- Refactored resizing logic to support dynamic sidebar size adjustments, improving responsiveness and user experience.
This commit is contained in:
Tom Butcher 2026-08-20 22:30:20 +01:00
parent fdb3045053
commit c4b4a88434
2 changed files with 186 additions and 19 deletions

View File

@ -1021,7 +1021,18 @@ span.ant-skeleton-input.ant-skeleton-input-sm.text-skeleton {
.farmcontrol-splitter.ant-splitter-horizontal.is-size-transitioning
.ant-splitter-panel {
transition: flex-basis 0.15s ease-in-out;
transition:
--dashboard-splitter-sidebar-size 0.15s ease-in-out,
flex-basis 0.15s ease-in-out,
width 0.15s ease-in-out,
min-width 0.15s ease-in-out,
max-width 0.15s ease-in-out;
}
@property --dashboard-splitter-sidebar-size {
syntax: '<length>';
inherits: true;
initial-value: 80px;
}
.farmcontrol-splitter
@ -1563,3 +1574,13 @@ span.ant-skeleton-input.ant-skeleton-input-sm.text-skeleton {
.ant-splitter-panel.dashboard-splitter-content-panel {
flex-grow: 1 !important;
}
.dashboard-splitter.farmcontrol-splitter
.ant-splitter-panel.dashboard-splitter-sidebar-panel.is-collapsed {
flex-grow: 0 !important;
flex-shrink: 0 !important;
flex-basis: var(--dashboard-splitter-sidebar-size) !important;
width: var(--dashboard-splitter-sidebar-size) !important;
min-width: var(--dashboard-splitter-sidebar-size) !important;
max-width: var(--dashboard-splitter-sidebar-size) !important;
}

View File

@ -17,6 +17,8 @@ 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 SIZE_TRANSITION_MS = 150
const COLLAPSE_STORAGE_KEY = 'DashboardSidebar_collapseState'
const SIZE_STORAGE_KEY = 'DashboardSidebar_width'
@ -66,8 +68,45 @@ const DashboardSidebarSplitter = ({ sidebar, children }) => {
const lastExpandedSizeRef = useRef(
clampExpandedSize(readStoredNumber(SIZE_STORAGE_KEY, DEFAULT_SIDEBAR_PX))
)
const sidebarSizeRef = useRef(sidebarSize)
sidebarSizeRef.current = sidebarSize
const collapseSizeRafRef = useRef([])
const [sizeTransitioning, setSizeTransitioning] = useState(false)
const sizeTransitionTimeoutRef = useRef(null)
const [resizeLocked, setResizeLocked] = useState(false)
const resizeLockedRef = useRef(false)
const suppressGestureRef = useRef(false)
const lockUntilPointerUpRef = useRef(false)
const lockTimeoutRef = useRef(null)
const pointerUnlockRef = useRef(null)
const ignoreResizeEndRef = useRef(false)
const setResizeLockedState = useCallback((locked) => {
resizeLockedRef.current = locked
setResizeLocked(locked)
}, [])
const clearPointerUnlock = useCallback(() => {
if (!pointerUnlockRef.current) return
window.removeEventListener('pointerup', pointerUnlockRef.current)
window.removeEventListener('pointercancel', pointerUnlockRef.current)
pointerUnlockRef.current = null
}, [])
const attachPointerUnlock = useCallback(() => {
if (pointerUnlockRef.current) return
const handler = () => {
clearPointerUnlock()
suppressGestureRef.current = false
if (lockUntilPointerUpRef.current) {
lockUntilPointerUpRef.current = false
setResizeLockedState(false)
}
}
pointerUnlockRef.current = handler
window.addEventListener('pointerup', handler)
window.addEventListener('pointercancel', handler)
}, [clearPointerUnlock, setResizeLockedState])
const startSizeTransition = useCallback(() => {
setSizeTransitioning(true)
@ -77,16 +116,69 @@ const DashboardSidebarSplitter = ({ sidebar, children }) => {
sizeTransitionTimeoutRef.current = setTimeout(() => {
setSizeTransitioning(false)
sizeTransitionTimeoutRef.current = null
}, 150)
}, SIZE_TRANSITION_MS)
}, [])
const cancelCollapseSizeAnimation = useCallback(() => {
collapseSizeRafRef.current.forEach((id) => cancelAnimationFrame(id))
collapseSizeRafRef.current = []
}, [])
const animateCollapsedSize = useCallback(() => {
cancelCollapseSizeAnimation()
const ids = []
ids.push(
requestAnimationFrame(() => {
ids.push(
requestAnimationFrame(() => {
collapseSizeRafRef.current = []
setSidebarSize(collapsedWidth)
})
)
})
)
collapseSizeRafRef.current = ids
}, [cancelCollapseSizeAnimation, collapsedWidth])
const lockResizeUntilPointerUp = useCallback(() => {
ignoreResizeEndRef.current = true
suppressGestureRef.current = true
lockUntilPointerUpRef.current = true
setResizeLockedState(true)
attachPointerUnlock()
}, [attachPointerUnlock, setResizeLockedState])
const lockResizeFor = useCallback(
(ms) => {
ignoreResizeEndRef.current = true
suppressGestureRef.current = true
setResizeLockedState(true)
attachPointerUnlock()
if (lockTimeoutRef.current) {
clearTimeout(lockTimeoutRef.current)
}
lockTimeoutRef.current = setTimeout(() => {
lockTimeoutRef.current = null
if (!lockUntilPointerUpRef.current) {
setResizeLockedState(false)
}
}, ms)
},
[attachPointerUnlock, setResizeLockedState]
)
useEffect(
() => () => {
if (sizeTransitionTimeoutRef.current) {
clearTimeout(sizeTransitionTimeoutRef.current)
}
if (lockTimeoutRef.current) {
clearTimeout(lockTimeoutRef.current)
}
cancelCollapseSizeAnimation()
clearPointerUnlock()
},
[]
[cancelCollapseSizeAnimation, clearPointerUnlock]
)
const persistCollapsed = (nextCollapsed) => {
@ -104,15 +196,17 @@ const DashboardSidebarSplitter = ({ sidebar, children }) => {
persistSize(expandedSize)
}
collapsedRef.current = true
setCollapsed(true)
persistCollapsed(true)
startSizeTransition()
setSidebarSize(collapsedWidth)
setSidebarSize(sidebarSizeRef.current)
setCollapsed(true)
animateCollapsedSize()
},
[collapsedWidth, startSizeTransition]
[animateCollapsedSize, startSizeTransition]
)
const expandSidebar = useCallback(() => {
cancelCollapseSizeAnimation()
collapsedRef.current = false
setCollapsed(false)
persistCollapsed(false)
@ -120,7 +214,7 @@ const DashboardSidebarSplitter = ({ sidebar, children }) => {
setSidebarSize(DEFAULT_SIDEBAR_PX)
persistSize(DEFAULT_SIDEBAR_PX)
lastExpandedSizeRef.current = DEFAULT_SIDEBAR_PX
}, [startSizeTransition])
}, [cancelCollapseSizeAnimation, startSizeTransition])
const handleSidebarCollapse = useCallback(
(nextCollapsed) => {
@ -129,33 +223,79 @@ const DashboardSidebarSplitter = ({ sidebar, children }) => {
} else {
expandSidebar()
}
lockResizeFor(SIZE_TRANSITION_MS)
},
[collapseSidebar, expandSidebar, sidebarSize]
[collapseSidebar, expandSidebar, lockResizeFor, sidebarSize]
)
const handleResizeStart = useCallback(() => {
if (!resizeLockedRef.current) {
ignoreResizeEndRef.current = false
}
}, [])
const handleResize = useCallback(
(sizes) => {
if (collapsedRef.current) {
setSidebarSize(collapsedWidth)
if (resizeLockedRef.current || suppressGestureRef.current) {
if (collapsedRef.current) {
setSidebarSize(collapsedWidth)
}
return
}
const size = sizes[0]
if (collapsedRef.current) {
if (size - collapsedWidth > EXPAND_DRAG_PX) {
expandSidebar()
lockResizeFor(SIZE_TRANSITION_MS)
return
}
setSidebarSize(Math.max(collapsedWidth, size))
return
}
if (size < COLLAPSE_BELOW_PX) {
collapseSidebar(lastExpandedSizeRef.current)
lockResizeUntilPointerUp()
return
}
setSidebarSize(size)
lastExpandedSizeRef.current = clampExpandedSize(size)
},
[collapseSidebar, collapsedWidth]
[
collapseSidebar,
collapsedWidth,
expandSidebar,
lockResizeFor,
lockResizeUntilPointerUp
]
)
const handleResizeEnd = useCallback(
(sizes) => {
if (
resizeLockedRef.current ||
suppressGestureRef.current ||
ignoreResizeEndRef.current
) {
ignoreResizeEndRef.current = false
if (collapsedRef.current) {
startSizeTransition()
setSidebarSize(collapsedWidth)
}
return
}
if (collapsedRef.current) {
setSidebarSize(collapsedWidth)
if (sizes[0] - collapsedWidth > EXPAND_DRAG_PX) {
expandSidebar()
lockResizeFor(SIZE_TRANSITION_MS)
} else {
startSizeTransition()
setSidebarSize(collapsedWidth)
}
return
}
@ -164,7 +304,7 @@ const DashboardSidebarSplitter = ({ sidebar, children }) => {
setSidebarSize(next)
persistSize(next)
},
[collapsedWidth]
[collapsedWidth, expandSidebar, lockResizeFor, startSizeTransition]
)
const sidebarNode = isValidElement(sidebar)
@ -189,21 +329,27 @@ const DashboardSidebarSplitter = ({ sidebar, children }) => {
<Splitter
className={`farmcontrol-splitter dashboard-splitter${
sizeTransitioning ? ' is-size-transitioning' : ''
}`}
}${collapsed ? ' is-sidebar-collapsed' : ''}`}
style={{ height: '100%' }}
onResizeStart={handleResizeStart}
onResize={handleResize}
onResizeEnd={handleResizeEnd}
>
<Splitter.Panel
className={`dashboard-splitter-sidebar-panel${
collapsed ? ' is-collapsed' : ''
}`}
size={sidebarSize}
min={collapsedWidth}
max={collapsed ? collapsedWidth : MAX_SIDEBAR_PX}
resizable={!collapsed}
max={
collapsed && resizeLocked && !sizeTransitioning
? collapsedWidth
: MAX_SIDEBAR_PX
}
resizable={!resizeLocked}
style={{
overflow: 'visible',
minWidth: collapsedWidth,
maxWidth:
collapsed && !sizeTransitioning ? collapsedWidth : undefined
'--dashboard-splitter-sidebar-size': `${sidebarSize}px`
}}
>
{sidebarNode}