diff --git a/assets/stylesheets/App.css b/assets/stylesheets/App.css index 705896c9..12600722 100644 --- a/assets/stylesheets/App.css +++ b/assets/stylesheets/App.css @@ -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: ''; + 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; +} diff --git a/src/components/Dashboard/common/DashboardSidebarSplitter.jsx b/src/components/Dashboard/common/DashboardSidebarSplitter.jsx index 10446090..24a0a77b 100644 --- a/src/components/Dashboard/common/DashboardSidebarSplitter.jsx +++ b/src/components/Dashboard/common/DashboardSidebarSplitter.jsx @@ -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 }) => { {sidebarNode}