Add Adaptive Splitter Component and Update Styles
All checks were successful
farmcontrol/farmcontrol-ui/pipeline/head This commit looks good

- 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.
This commit is contained in:
Tom Butcher 2026-08-23 22:57:36 +01:00
parent 32168be911
commit 1b98f6a716
3 changed files with 1275 additions and 274 deletions

View File

@ -37,6 +37,7 @@
.ant-badge, .ant-badge,
.ant-select-dropdown, .ant-select-dropdown,
.ant-splitter, .ant-splitter,
.adaptive-splitter,
.ant-steps-item-title, .ant-steps-item-title,
.ant-steps-icon, .ant-steps-icon,
.ant-color-picker-presets-label, .ant-color-picker-presets-label,
@ -528,11 +529,13 @@ body {
width: 35%; width: 35%;
} }
.farmcontrol-splitter > .ant-splitter-bar { .farmcontrol-splitter > .ant-splitter-bar,
.farmcontrol-splitter > .adaptive-splitter-bar {
margin: 8px; margin: 8px;
} }
.farmcontrol-splitter.large > .ant-splitter-bar { .farmcontrol-splitter.large > .ant-splitter-bar,
.farmcontrol-splitter.large > .adaptive-splitter-bar {
margin: 11px; margin: 11px;
opacity: 0.6; opacity: 0.6;
} }
@ -938,41 +941,18 @@ span.ant-skeleton-input.ant-skeleton-input-sm.text-skeleton {
} }
.sidebar-scroll .simplebar-track.simplebar-vertical { .sidebar-scroll .simplebar-track.simplebar-vertical {
right: -1px; top: 8px;
width: 0px !important; right: calc(-1 * var(--scrollbox-vertical-right-padding) - 0.5px);
transition: width: 8px !important;
width 0.15s ease-in-out,
right 0.15s ease-in-out;
}
.sidebar-scroll .simplebar-track.simplebar-vertical {
border-radius: 0 0 6.5px 0;
}
.sidebar-scroll.dark .simplebar-track.simplebar-vertical {
background-color: #141414;
border-inline-end: 1px solid rgba(253, 253, 253, 0.12);
border-bottom: 1px solid rgba(253, 253, 253, 0.12);
}
.sidebar-scroll.light .simplebar-track.simplebar-vertical {
background-color: #ffffff;
border-inline-end: 1px solid rgba(5, 5, 5, 0.06);
border-bottom: 1px solid rgba(5, 5, 5, 0.06);
}
.sidebar-scroll.simplebar-scrolling .simplebar-track.simplebar-vertical {
width: 12px !important;
right: -13px;
} }
.sidebar-scroll .sidebar-scroll
.simplebar-track.simplebar-vertical .simplebar-track.simplebar-vertical
.simplebar-scrollbar:before { .simplebar-scrollbar:before {
top: 4px; top: 2px;
bottom: 4px; bottom: 2px;
left: 4px; left: 2px;
right: 4px; right: 2px;
} }
.ant-color-picker .ant-color-picker
@ -1096,29 +1076,229 @@ span.ant-skeleton-input.ant-skeleton-input-sm.text-skeleton {
margin-left: 4px; margin-left: 4px;
} }
.farmcontrol-splitter .ant-splitter-panel { .farmcontrol-splitter .ant-splitter-panel,
.farmcontrol-splitter .adaptive-splitter-panel {
overflow: visible; overflow: visible;
} }
.farmcontrol-splitter.ant-splitter-horizontal .ant-splitter-panel { .farmcontrol-splitter.ant-splitter-horizontal .ant-splitter-panel,
.farmcontrol-splitter.adaptive-splitter-horizontal .adaptive-splitter-panel {
width: 100%; width: 100%;
min-width: 0; min-width: 0;
} }
.farmcontrol-splitter.ant-splitter-horizontal.is-size-transitioning .adaptive-splitter {
.ant-splitter-panel { display: flex;
transition: width: 100%;
--dashboard-splitter-sidebar-size 0.3s cubic-bezier(0.2, 0, 0, 1) 0s, height: 100%;
flex-basis 0.3s cubic-bezier(0.2, 0, 0, 1) 0s, min-height: 0;
width 0.3s cubic-bezier(0.2, 0, 0, 1) 0s, align-items: stretch;
min-width 0.3s cubic-bezier(0.2, 0, 0, 1) 0s, position: relative;
max-width 0.3s cubic-bezier(0.2, 0, 0, 1) 0s;
} }
@property --dashboard-splitter-sidebar-size { .adaptive-splitter-vertical {
syntax: '<length>'; flex-direction: column;
inherits: true; }
initial-value: 80px;
.adaptive-splitter-panel {
min-width: 0;
min-height: 0;
}
.adaptive-splitter-bar {
flex: none;
position: relative;
z-index: 2;
user-select: none;
touch-action: none;
}
.adaptive-splitter-horizontal > .adaptive-splitter-bar {
width: 0;
}
.adaptive-splitter-vertical > .adaptive-splitter-bar {
height: 0;
}
.adaptive-splitter-bar-dragger {
position: absolute;
z-index: 1;
background: transparent;
}
.adaptive-splitter-horizontal
> .adaptive-splitter-bar
.adaptive-splitter-bar-dragger {
top: 0;
bottom: 0;
left: 50%;
width: 6px;
transform: translateX(-50%);
cursor: col-resize;
}
.adaptive-splitter-vertical
> .adaptive-splitter-bar
.adaptive-splitter-bar-dragger {
left: 0;
right: 0;
top: 50%;
height: 6px;
transform: translateY(-50%);
cursor: row-resize;
}
.adaptive-splitter-bar-dragger::before {
content: '';
position: absolute;
inset: 0;
margin: auto;
}
.adaptive-splitter-horizontal
> .adaptive-splitter-bar
.adaptive-splitter-bar-dragger::before {
width: 2px;
height: 100%;
background: var(--ant-color-split, rgba(5, 5, 5, 0.06));
}
.adaptive-splitter-vertical
> .adaptive-splitter-bar
.adaptive-splitter-bar-dragger::before {
height: 2px;
width: 100%;
background: var(--ant-color-split, rgba(5, 5, 5, 0.06));
}
.adaptive-splitter-bar-dragger::after {
content: '';
position: absolute;
inset: 0;
margin: auto;
background: var(--ant-color-text-quaternary, rgba(0, 0, 0, 0.25));
opacity: 0.35;
}
.adaptive-splitter-horizontal
> .adaptive-splitter-bar
.adaptive-splitter-bar-dragger::after {
width: 2px;
height: 20px;
}
.adaptive-splitter-vertical
> .adaptive-splitter-bar
.adaptive-splitter-bar-dragger::after {
height: 2px;
width: 20px;
}
.adaptive-splitter-bar.is-active .adaptive-splitter-bar-dragger::before,
.adaptive-splitter-bar-dragger:hover::before {
background: var(--ant-color-primary-border-hover, #4096ff);
}
.adaptive-splitter-bar-collapse {
position: absolute;
z-index: 2;
display: flex;
align-items: center;
justify-content: center;
width: 16px;
height: 16px;
padding: 0;
border: none;
border-radius: 4px;
background: transparent;
color: var(--ant-color-text-secondary, rgba(0, 0, 0, 0.45));
cursor: pointer;
}
.adaptive-splitter-horizontal
> .adaptive-splitter-bar
.adaptive-splitter-bar-collapse {
top: 50%;
transform: translateY(-50%);
}
.adaptive-splitter-horizontal
> .adaptive-splitter-bar
.adaptive-splitter-bar-collapse-start {
left: -18px;
}
.adaptive-splitter-horizontal
> .adaptive-splitter-bar
.adaptive-splitter-bar-collapse-end {
right: -18px;
}
.adaptive-splitter-bar-preview {
position: absolute;
z-index: 3;
pointer-events: none;
background: var(--ant-color-primary, #1677ff);
opacity: 0.45;
}
.adaptive-splitter-horizontal
> .adaptive-splitter-bar
.adaptive-splitter-bar-preview {
top: 0;
bottom: 0;
width: 2px;
}
.adaptive-splitter-vertical
> .adaptive-splitter-bar
.adaptive-splitter-bar-preview {
left: 0;
right: 0;
height: 2px;
}
.adaptive-splitter.is-dragging {
user-select: none;
}
.adaptive-splitter.is-dragging.adaptive-splitter-horizontal,
.adaptive-splitter.is-dragging.adaptive-splitter-horizontal * {
cursor: col-resize !important;
}
.adaptive-splitter.is-dragging.adaptive-splitter-vertical,
.adaptive-splitter.is-dragging.adaptive-splitter-vertical * {
cursor: row-resize !important;
}
.adaptive-splitter-mask {
position: absolute;
inset: 0;
z-index: 10;
pointer-events: none;
}
.adaptive-splitter.is-ready:not(.is-dragging) > .adaptive-splitter-panel,
.adaptive-splitter.is-size-transitioning > .adaptive-splitter-panel,
.farmcontrol-splitter.is-size-transitioning > .ant-splitter-panel,
.farmcontrol-splitter.is-size-transitioning > .adaptive-splitter-panel {
transition:
flex-basis var(--adaptive-splitter-size-transition, 0.3s)
cubic-bezier(0.2, 0, 0, 1),
width var(--adaptive-splitter-size-transition, 0.3s)
cubic-bezier(0.2, 0, 0, 1),
min-width var(--adaptive-splitter-size-transition, 0.3s)
cubic-bezier(0.2, 0, 0, 1),
max-width var(--adaptive-splitter-size-transition, 0.3s)
cubic-bezier(0.2, 0, 0, 1),
height var(--adaptive-splitter-size-transition, 0.3s)
cubic-bezier(0.2, 0, 0, 1),
min-height var(--adaptive-splitter-size-transition, 0.3s)
cubic-bezier(0.2, 0, 0, 1),
max-height var(--adaptive-splitter-size-transition, 0.3s)
cubic-bezier(0.2, 0, 0, 1);
} }
.farmcontrol-splitter .farmcontrol-splitter
@ -1643,41 +1823,34 @@ span.ant-skeleton-input.ant-skeleton-input-sm.text-skeleton {
stroke-width: 8px; stroke-width: 8px;
} }
.dashboard-splitter.farmcontrol-splitter > .ant-splitter-bar { .dashboard-splitter.farmcontrol-splitter > .ant-splitter-bar,
.dashboard-splitter.farmcontrol-splitter > .adaptive-splitter-bar {
margin: 0; margin: 0;
} }
.dashboard-splitter.farmcontrol-splitter > .ant-splitter-panel { .dashboard-splitter.farmcontrol-splitter > .ant-splitter-panel,
.dashboard-splitter.farmcontrol-splitter > .adaptive-splitter-panel {
padding: 0; padding: 0;
} }
.dashboard-splitter.farmcontrol-splitter .dashboard-splitter.farmcontrol-splitter
> .ant-splitter-bar > .ant-splitter-bar
.ant-splitter-bar-dragger::before { .ant-splitter-bar-dragger::before,
.dashboard-splitter.farmcontrol-splitter
> .adaptive-splitter-bar
.adaptive-splitter-bar-dragger::before {
background: transparent; background: transparent;
} }
.dashboard-splitter.farmcontrol-splitter .dashboard-splitter.farmcontrol-splitter
> .ant-splitter-bar > .ant-splitter-bar
.ant-splitter-bar-dragger::after { .ant-splitter-bar-dragger::after,
.dashboard-splitter.farmcontrol-splitter
> .adaptive-splitter-bar
.adaptive-splitter-bar-dragger::after {
content: none; content: none;
} }
.dashboard-splitter.farmcontrol-splitter
.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;
}
.object-display-tag .object-display-tag
.elipsis-text-wrapper.ant-typography .elipsis-text-wrapper.ant-typography
code.elipsis-text-measure, code.elipsis-text-measure,

View File

@ -0,0 +1,981 @@
import {
Children,
isValidElement,
useCallback,
useEffect,
useLayoutEffect,
useMemo,
useRef,
useState
} from 'react'
import PropTypes from 'prop-types'
import {
DownOutlined,
LeftOutlined,
RightOutlined,
UpOutlined
} from '@ant-design/icons'
const SIZE_TRANSITION_MS = 300
const DEFAULT_EXPAND_DRAG_PX = 10
const KEYBOARD_STEP_PX = 8
const sizeType = PropTypes.oneOfType([PropTypes.number, PropTypes.string])
const AdaptiveSplitterPanel = () => null
AdaptiveSplitterPanel.displayName = 'AdaptiveSplitterPanel'
AdaptiveSplitterPanel.isAdaptiveSplitterPanel = true
AdaptiveSplitterPanel.propTypes = {
children: PropTypes.node,
className: PropTypes.string,
style: PropTypes.object,
size: sizeType,
defaultSize: sizeType,
min: sizeType,
max: sizeType,
resizable: PropTypes.bool,
collapsible: PropTypes.oneOfType([
PropTypes.bool,
PropTypes.shape({
start: PropTypes.bool,
end: PropTypes.bool
})
]),
collapsed: PropTypes.bool,
collapsedSize: PropTypes.number,
collapseThreshold: PropTypes.number,
expandDragPx: PropTypes.number,
expandedSize: PropTypes.number,
onCollapse: PropTypes.func
}
const parseSizeToPx = (size, containerSize) => {
if (size == null || size === '') return null
if (typeof size === 'number') {
return Number.isFinite(size) ? size : null
}
const value = String(size).trim()
if (value.endsWith('%')) {
const pct = Number.parseFloat(value)
return Number.isFinite(pct) ? (pct / 100) * containerSize : null
}
const px = Number.parseFloat(value)
return Number.isFinite(px) ? px : null
}
const clamp = (value, min, max) => Math.min(max, Math.max(min, value))
const getPanelItems = (children) =>
Children.toArray(children)
.filter(
(child) =>
isValidElement(child) &&
(child.type === AdaptiveSplitterPanel ||
child.type?.isAdaptiveSplitterPanel)
)
.map((child) => ({
...child.props,
key: child.key
}))
const getCollapsibleSides = (collapsible) => {
if (!collapsible) return { start: false, end: false }
if (collapsible === true) return { start: true, end: true }
return {
start: !!collapsible.start,
end: !!collapsible.end
}
}
const getBarEdge = (sizes, barIndex) => {
if (barIndex < 0) return 0
let edge = 0
for (let i = 0; i <= barIndex; i += 1) {
edge += sizes[i] || 0
}
return edge
}
const SplitBar = ({
active,
resizable,
isVertical,
lazy,
lazyOffset,
ariaNow,
ariaMin,
ariaMax,
startCollapsible,
endCollapsible,
onPointerDown,
onKeyDown,
onCollapseStart,
onCollapseEnd
}) => {
const startIcon = isVertical ? <UpOutlined /> : <LeftOutlined />
const endIcon = isVertical ? <DownOutlined /> : <RightOutlined />
return (
<div
className={[
'adaptive-splitter-bar',
'ant-splitter-bar',
active ? 'is-active' : '',
resizable ? '' : 'is-disabled'
]
.filter(Boolean)
.join(' ')}
>
{lazy && active && (
<div
className='adaptive-splitter-bar-preview ant-splitter-bar-preview'
style={{
[isVertical ? 'top' : 'left']: '50%',
transform: isVertical
? `translate(-50%, calc(-50% + ${lazyOffset}px))`
: `translate(calc(-50% + ${lazyOffset}px), -50%)`
}}
/>
)}
<div
role='separator'
tabIndex={resizable ? 0 : -1}
aria-orientation={isVertical ? 'horizontal' : 'vertical'}
aria-valuenow={Math.round(ariaNow || 0)}
aria-valuemin={Math.round(ariaMin || 0)}
aria-valuemax={
Number.isFinite(ariaMax) ? Math.round(ariaMax) : undefined
}
className={[
'adaptive-splitter-bar-dragger',
'ant-splitter-bar-dragger',
active ? 'ant-splitter-bar-dragger-active' : '',
resizable ? '' : 'ant-splitter-bar-dragger-disabled'
]
.filter(Boolean)
.join(' ')}
onPointerDown={resizable ? onPointerDown : undefined}
onKeyDown={resizable ? onKeyDown : undefined}
/>
{startCollapsible && (
<button
type='button'
className='adaptive-splitter-bar-collapse adaptive-splitter-bar-collapse-start ant-splitter-bar-collapse-bar ant-splitter-bar-collapse-bar-start'
aria-label='Collapse start panel'
onPointerDown={(event) => event.stopPropagation()}
onClick={onCollapseStart}
>
{startIcon}
</button>
)}
{endCollapsible && (
<button
type='button'
className='adaptive-splitter-bar-collapse adaptive-splitter-bar-collapse-end ant-splitter-bar-collapse-bar ant-splitter-bar-collapse-bar-end'
aria-label='Collapse end panel'
onPointerDown={(event) => event.stopPropagation()}
onClick={onCollapseEnd}
>
{endIcon}
</button>
)}
</div>
)
}
SplitBar.propTypes = {
active: PropTypes.bool,
resizable: PropTypes.bool,
isVertical: PropTypes.bool,
lazy: PropTypes.bool,
lazyOffset: PropTypes.number,
ariaNow: PropTypes.number,
ariaMin: PropTypes.number,
ariaMax: PropTypes.number,
startCollapsible: PropTypes.bool,
endCollapsible: PropTypes.bool,
onPointerDown: PropTypes.func,
onKeyDown: PropTypes.func,
onCollapseStart: PropTypes.func,
onCollapseEnd: PropTypes.func
}
const AdaptiveSplitter = ({
children,
className = '',
style,
layout,
orientation,
vertical = false,
lazy = false,
onResizeStart,
onResize,
onResizeEnd,
onCollapse,
sizeTransitionMs = SIZE_TRANSITION_MS
}) => {
const isVertical =
orientation === 'vertical' || layout === 'vertical' || vertical
const items = useMemo(() => getPanelItems(children), [children])
const containerRef = useRef(null)
const [containerSize, setContainerSize] = useState(0)
const [pxSizes, setPxSizes] = useState([])
const [dragging, setDragging] = useState(false)
const [activeBar, setActiveBar] = useState(null)
const [sizeTransitioning, setSizeTransitioning] = useState(false)
const [ready, setReady] = useState(false)
const [lazyOffset, setLazyOffset] = useState(0)
const pxSizesRef = useRef(pxSizes)
pxSizesRef.current = pxSizes
const itemsRef = useRef(items)
itemsRef.current = items
const containerSizeRef = useRef(containerSize)
containerSizeRef.current = containerSize
const dragRef = useRef(null)
const lastExpandedRef = useRef([])
const sizeTransitionTimeoutRef = useRef(null)
const skipNextTransitionRef = useRef(true)
const lazyOffsetRef = useRef(0)
const pendingAnimateSizesRef = useRef(null)
const animateRafRef = useRef([])
const getItemMin = useCallback((item, fallback = 0) => {
const parsed = parseSizeToPx(item?.min, containerSizeRef.current)
return parsed ?? fallback
}, [])
const getItemMax = useCallback((item, fallback = Infinity) => {
const parsed = parseSizeToPx(item?.max, containerSizeRef.current)
return parsed ?? fallback
}, [])
const rememberExpandedSize = useCallback((size, index, item) => {
if (!item || item.collapsed) return
if (item.collapseThreshold != null && size < item.collapseThreshold) return
lastExpandedRef.current[index] = size
}, [])
const resolveExpandedSize = useCallback(
(index, item) => {
if (item?.expandedSize != null) return item.expandedSize
if (lastExpandedRef.current[index] != null) {
return lastExpandedRef.current[index]
}
const fromDefault = parseSizeToPx(
item?.defaultSize,
containerSizeRef.current
)
if (fromDefault != null) return fromDefault
const max = getItemMax(item)
if (Number.isFinite(max)) return max
return item?.collapsedSize ?? 0
},
[getItemMax]
)
const applyBarResize = useCallback(
(sizes, barIndex, nextStartSize) => {
const currentItems = itemsRef.current
const startItem = currentItems[barIndex]
const endItem = currentItems[barIndex + 1]
if (!startItem || !endItem) return sizes
const next = sizes.slice()
const startMin = getItemMin(startItem)
const startMax = getItemMax(startItem)
const endMin = getItemMin(endItem)
const endMax = getItemMax(endItem)
const oldStart = next[barIndex]
const oldEnd = next[barIndex + 1]
const pairTotal = oldStart + oldEnd
let startSize = clamp(nextStartSize, startMin, startMax)
let endSize = pairTotal - startSize
if (endSize < endMin) {
endSize = endMin
startSize = pairTotal - endMin
} else if (endSize > endMax) {
endSize = endMax
startSize = pairTotal - endMax
}
startSize = clamp(startSize, startMin, startMax)
next[barIndex] = startSize
next[barIndex + 1] = pairTotal - startSize
return next
},
[getItemMax, getItemMin]
)
const emitCollapse = useCallback(
(index, collapsed, sizes) => {
itemsRef.current[index]?.onCollapse?.(collapsed)
onCollapse?.(
itemsRef.current.map((panel, panelIndex) =>
panelIndex === index ? collapsed : !!panel.collapsed
),
sizes
)
},
[onCollapse]
)
const remapDragOrigin = useCallback((pointerPos, edge) => {
const drag = dragRef.current
if (!drag) return
drag.originOffset = pointerPos - edge
drag.lastPointerPos = pointerPos
}, [])
const endDragSession = useCallback(() => {
const drag = dragRef.current
if (drag?.captureTarget?.hasPointerCapture?.(drag.pointerId)) {
drag.captureTarget.releasePointerCapture(drag.pointerId)
}
dragRef.current = null
lazyOffsetRef.current = 0
setLazyOffset(0)
setDragging(false)
setActiveBar(null)
}, [])
const cancelPendingAnimate = useCallback(() => {
animateRafRef.current.forEach((id) => cancelAnimationFrame(id))
animateRafRef.current = []
pendingAnimateSizesRef.current = null
}, [])
const startSizeTransition = useCallback(() => {
setSizeTransitioning(true)
if (sizeTransitionTimeoutRef.current) {
clearTimeout(sizeTransitionTimeoutRef.current)
}
sizeTransitionTimeoutRef.current = setTimeout(() => {
setSizeTransitioning(false)
sizeTransitionTimeoutRef.current = null
}, sizeTransitionMs)
}, [sizeTransitionMs])
const scheduleAnimatedSizes = useCallback(
(nextSizes) => {
startSizeTransition()
cancelPendingAnimate()
pendingAnimateSizesRef.current = nextSizes
const ids = []
ids.push(
requestAnimationFrame(() => {
ids.push(
requestAnimationFrame(() => {
animateRafRef.current = []
const pending = pendingAnimateSizesRef.current
pendingAnimateSizesRef.current = null
if (pending) setPxSizes(pending)
})
)
})
)
animateRafRef.current = ids
},
[cancelPendingAnimate, startSizeTransition]
)
const measureContainer = useCallback(() => {
const node = containerRef.current
if (!node) return
const next = isVertical ? node.clientHeight : node.clientWidth
if (next !== containerSizeRef.current) {
setContainerSize(next)
}
}, [isVertical])
useLayoutEffect(() => {
measureContainer()
const node = containerRef.current
if (!node || typeof ResizeObserver === 'undefined') return undefined
const observer = new ResizeObserver(() => measureContainer())
observer.observe(node)
return () => observer.disconnect()
}, [measureContainer])
useEffect(() => {
const id = requestAnimationFrame(() => setReady(true))
return () => cancelAnimationFrame(id)
}, [])
const deriveSizes = useCallback(() => {
if (!containerSize || !items.length) return []
const parsed = items.map((item) => {
if (item.collapsed && item.collapsedSize != null && item.size == null) {
return item.collapsedSize
}
return parseSizeToPx(item.size ?? item.defaultSize, containerSize)
})
const knownSum = parsed.reduce(
(sum, size) => sum + (size == null ? 0 : size),
0
)
const unknownCount = parsed.filter((size) => size == null).length
const leftover = Math.max(0, containerSize - knownSum)
const unknownShare = unknownCount ? leftover / unknownCount : 0
return parsed.map((size, index) => {
const item = items[index]
const next = size == null ? unknownShare : size
const min = getItemMin(item)
const max = getItemMax(item)
const clamped = clamp(next, min, Number.isFinite(max) ? max : next)
rememberExpandedSize(clamped, index, item)
return clamped
})
}, [containerSize, getItemMax, getItemMin, items, rememberExpandedSize])
useLayoutEffect(() => {
const nextSizes = deriveSizes()
if (!nextSizes.length) return
const drag = dragRef.current
const prev = pxSizesRef.current
const sameLength = prev.length === nextSizes.length
const changed =
!sameLength || nextSizes.some((size, index) => size !== prev[index])
if (drag && sameLength && changed && drag.lastPointerPos != null) {
remapDragOrigin(drag.lastPointerPos, getBarEdge(nextSizes, drag.barIndex))
skipNextTransitionRef.current = true
}
const shouldAnimate =
changed &&
!drag &&
sameLength &&
!skipNextTransitionRef.current &&
prev.length
skipNextTransitionRef.current = false
if (shouldAnimate) {
scheduleAnimatedSizes(nextSizes)
return
}
if (changed) {
cancelPendingAnimate()
setPxSizes(nextSizes)
}
}, [
cancelPendingAnimate,
deriveSizes,
remapDragOrigin,
scheduleAnimatedSizes
])
useEffect(
() => () => {
if (sizeTransitionTimeoutRef.current) {
clearTimeout(sizeTransitionTimeoutRef.current)
}
animateRafRef.current.forEach((id) => cancelAnimationFrame(id))
},
[]
)
const getPointerPos = useCallback(
(event) => {
const node = containerRef.current
if (!node) return 0
const rect = node.getBoundingClientRect()
return isVertical ? event.clientY - rect.top : event.clientX - rect.left
},
[isVertical]
)
const commitSizes = useCallback(
(nextSizes, { notify = true, end = false } = {}) => {
cancelPendingAnimate()
setPxSizes(nextSizes)
nextSizes.forEach((size, index) =>
rememberExpandedSize(size, index, itemsRef.current[index])
)
if (!notify) return
if (end) onResizeEnd?.(nextSizes)
else onResize?.(nextSizes)
},
[cancelPendingAnimate, onResize, onResizeEnd, rememberExpandedSize]
)
const resolveDragSize = useCallback(
(barIndex, pointerPos, originOffset, currentSizes) => {
const item = itemsRef.current[barIndex]
const drag = dragRef.current
const collapsed =
drag?.barIndex === barIndex ? drag.collapsed : !!item?.collapsed
const rawEdge = pointerPos - originOffset
const startSize = rawEdge - getBarEdge(currentSizes, barIndex - 1)
const collapsedSize = item?.collapsedSize ?? getItemMin(item)
const expandDragPx = item?.expandDragPx ?? DEFAULT_EXPAND_DRAG_PX
const collapseThreshold = item?.collapseThreshold
if (collapsed) {
const preview = Math.max(collapsedSize, startSize)
if (preview - collapsedSize > expandDragPx) {
return {
expand: true,
collapse: false,
startSize: preview
}
}
return {
expand: false,
collapse: false,
startSize: preview
}
}
if (drag?.blockCollapse) {
if (startSize <= collapsedSize + expandDragPx) {
return {
expand: false,
collapse: true,
startSize: collapsedSize
}
}
if (collapseThreshold != null && startSize >= collapseThreshold) {
drag.blockCollapse = false
}
return {
expand: false,
collapse: false,
startSize: Math.max(startSize, collapsedSize)
}
}
if (collapseThreshold != null && startSize < collapseThreshold) {
return {
expand: false,
collapse: true,
startSize: collapsedSize
}
}
return {
expand: false,
collapse: false,
startSize
}
},
[getItemMin]
)
const handlePointerMove = useCallback(
(event) => {
const drag = dragRef.current
if (!drag || event.pointerId !== drag.pointerId) return
const pointerPos = getPointerPos(event)
drag.lastPointerPos = pointerPos
const currentSizes = pxSizesRef.current
const resolved = resolveDragSize(
drag.barIndex,
pointerPos,
drag.originOffset,
currentSizes
)
if (lazy) {
const startEdge = getBarEdge(drag.startSizes, drag.barIndex)
const nextOffset =
getBarEdge(
applyBarResize(drag.startSizes, drag.barIndex, resolved.startSize),
drag.barIndex
) - startEdge
lazyOffsetRef.current = nextOffset
setLazyOffset(nextOffset)
return
}
const nextSizes = applyBarResize(
currentSizes,
drag.barIndex,
resolved.startSize
)
if (resolved.expand) {
drag.collapsed = false
drag.blockCollapse = true
skipNextTransitionRef.current = true
remapDragOrigin(pointerPos, getBarEdge(nextSizes, drag.barIndex))
emitCollapse(drag.barIndex, false, nextSizes)
commitSizes(nextSizes)
return
}
if (resolved.collapse) {
const barIndex = drag.barIndex
endDragSession()
emitCollapse(barIndex, true, nextSizes)
onResizeEnd?.(nextSizes)
return
}
commitSizes(nextSizes)
},
[
applyBarResize,
commitSizes,
emitCollapse,
getPointerPos,
lazy,
endDragSession,
onResizeEnd,
remapDragOrigin,
resolveDragSize
]
)
const stopDragging = useCallback(
(event) => {
const drag = dragRef.current
if (!drag || (event && event.pointerId !== drag.pointerId)) return
let nextSizes = pxSizesRef.current.slice()
if (lazy) {
const pointerPos = event ? getPointerPos(event) : drag.lastPointerPos
const resolved = resolveDragSize(
drag.barIndex,
pointerPos,
drag.originOffset,
drag.startSizes
)
nextSizes = applyBarResize(
drag.startSizes,
drag.barIndex,
resolved.startSize
)
if (resolved.expand) {
drag.collapsed = false
drag.blockCollapse = true
emitCollapse(drag.barIndex, false, nextSizes)
}
if (resolved.collapse) {
drag.collapsed = true
drag.blockCollapse = false
emitCollapse(drag.barIndex, true, nextSizes)
}
}
const item = itemsRef.current[drag.barIndex]
const collapsed = drag.collapsed ?? item?.collapsed
if (collapsed) {
const collapsedSize = item.collapsedSize ?? getItemMin(item)
if (nextSizes[drag.barIndex] !== collapsedSize) {
nextSizes = applyBarResize(nextSizes, drag.barIndex, collapsedSize)
}
}
endDragSession()
skipNextTransitionRef.current = true
commitSizes(nextSizes, { end: true })
},
[
applyBarResize,
commitSizes,
emitCollapse,
endDragSession,
getItemMin,
getPointerPos,
lazy,
resolveDragSize
]
)
const startDragging = useCallback(
(barIndex, event) => {
const item = itemsRef.current[barIndex]
const nextItem = itemsRef.current[barIndex + 1]
if (
!pxSizesRef.current.length ||
item?.resizable === false ||
nextItem?.resizable === false
) {
return
}
event.preventDefault()
event.stopPropagation()
event.currentTarget.setPointerCapture?.(event.pointerId)
const pointerPos = getPointerPos(event)
const startSizes = pxSizesRef.current.slice()
dragRef.current = {
barIndex,
pointerId: event.pointerId,
captureTarget: event.currentTarget,
originOffset: pointerPos - getBarEdge(startSizes, barIndex),
lastPointerPos: pointerPos,
startSizes,
collapsed: !!item.collapsed,
blockCollapse: false
}
skipNextTransitionRef.current = true
setSizeTransitioning(false)
setDragging(true)
setActiveBar(barIndex)
onResizeStart?.(startSizes)
},
[getPointerPos, onResizeStart]
)
useEffect(() => {
if (!dragging) return undefined
window.addEventListener('pointermove', handlePointerMove)
window.addEventListener('pointerup', stopDragging)
window.addEventListener('pointercancel', stopDragging)
return () => {
window.removeEventListener('pointermove', handlePointerMove)
window.removeEventListener('pointerup', stopDragging)
window.removeEventListener('pointercancel', stopDragging)
}
}, [dragging, handlePointerMove, stopDragging])
const collapseViaControl = useCallback(
(barIndex, side) => {
const itemIndex = side === 'start' ? barIndex : barIndex + 1
const item = itemsRef.current[itemIndex]
if (!item) return
const collapsedSize = item.collapsedSize ?? getItemMin(item)
const current = pxSizesRef.current[itemIndex]
const alreadyCollapsed =
item.collapsed ||
(item.collapseThreshold != null
? current < item.collapseThreshold
: current <= collapsedSize + 1)
const restored = resolveExpandedSize(itemIndex, item)
let nextStartSize
if (side === 'start') {
nextStartSize = alreadyCollapsed ? restored : collapsedSize
} else {
const neighbor = pxSizesRef.current[barIndex]
const nextPanelSize = alreadyCollapsed ? restored : collapsedSize
nextStartSize = neighbor + (current - nextPanelSize)
}
if (!alreadyCollapsed) {
rememberExpandedSize(current, itemIndex, item)
}
const nextSizes = applyBarResize(
pxSizesRef.current,
barIndex,
nextStartSize
)
emitCollapse(itemIndex, !alreadyCollapsed, nextSizes)
},
[
applyBarResize,
emitCollapse,
getItemMin,
rememberExpandedSize,
resolveExpandedSize
]
)
const handleBarKeyDown = useCallback(
(barIndex, event) => {
const backward = isVertical
? event.key === 'ArrowUp'
: event.key === 'ArrowLeft'
const forward = isVertical
? event.key === 'ArrowDown'
: event.key === 'ArrowRight'
if (!backward && !forward) return
event.preventDefault()
const delta = forward ? KEYBOARD_STEP_PX : -KEYBOARD_STEP_PX
const item = itemsRef.current[barIndex]
const proposed = pxSizesRef.current[barIndex] + delta
const collapsedSize = item?.collapsedSize ?? getItemMin(item)
const expandDragPx = item?.expandDragPx ?? DEFAULT_EXPAND_DRAG_PX
if (
item?.collapseThreshold != null &&
proposed < item.collapseThreshold &&
!item.collapsed
) {
rememberExpandedSize(pxSizesRef.current[barIndex], barIndex, item)
const collapsedSizes = applyBarResize(
pxSizesRef.current,
barIndex,
collapsedSize
)
emitCollapse(barIndex, true, collapsedSizes)
commitSizes(collapsedSizes, { end: true })
return
}
if (item?.collapsed && proposed - collapsedSize > expandDragPx) {
const expandedSizes = applyBarResize(
pxSizesRef.current,
barIndex,
resolveExpandedSize(barIndex, item)
)
emitCollapse(barIndex, false, expandedSizes)
commitSizes(expandedSizes, { end: true })
return
}
commitSizes(applyBarResize(pxSizesRef.current, barIndex, proposed), {
end: true
})
},
[
applyBarResize,
commitSizes,
emitCollapse,
getItemMin,
isVertical,
rememberExpandedSize,
resolveExpandedSize
]
)
const orientationClass = isVertical ? 'vertical' : 'horizontal'
const rootClassName = [
'adaptive-splitter',
'ant-splitter',
`adaptive-splitter-${orientationClass}`,
`ant-splitter-${orientationClass}`,
ready ? 'is-ready' : '',
sizeTransitioning ? 'is-size-transitioning' : '',
dragging ? 'is-dragging' : '',
className
]
.filter(Boolean)
.join(' ')
return (
<div
ref={containerRef}
className={rootClassName}
style={{
'--adaptive-splitter-size-transition': `${sizeTransitionMs}ms`,
...style
}}
>
{items.flatMap((item, index) => {
const size = pxSizes[index] ?? 0
const isFill =
item.size == null &&
item.defaultSize == null &&
!item.collapsed &&
index === items.length - 1
const minPx = getItemMin(item)
const maxPx = getItemMax(item)
const panelStyle = isFill
? {
flex: '1 1 auto',
minWidth: isVertical ? 0 : minPx,
minHeight: isVertical ? minPx : 0,
overflow: 'auto',
...item.style
}
: {
flex: `0 0 ${size}px`,
[isVertical ? 'height' : 'width']: size,
minWidth: isVertical ? undefined : minPx,
maxWidth:
isVertical || !Number.isFinite(maxPx) ? undefined : maxPx,
minHeight: isVertical ? minPx : undefined,
maxHeight:
!isVertical || !Number.isFinite(maxPx) ? undefined : maxPx,
overflow: 'auto',
...item.style
}
const collapsible = getCollapsibleSides(item.collapsible)
const nextCollapsible = getCollapsibleSides(
items[index + 1]?.collapsible
)
const showBar = index < items.length - 1
const barResizable =
item.resizable !== false && items[index + 1]?.resizable !== false
const nodes = [
<div
key={item.key ?? `adaptive-splitter-panel-${index}`}
className={[
'adaptive-splitter-panel',
'ant-splitter-panel',
item.collapsed ? 'is-collapsed' : '',
item.className
]
.filter(Boolean)
.join(' ')}
style={panelStyle}
>
{item.children}
</div>
]
if (showBar) {
nodes.push(
<SplitBar
key={`adaptive-splitter-bar-${index}`}
active={activeBar === index}
resizable={barResizable}
isVertical={isVertical}
lazy={lazy}
lazyOffset={lazyOffset}
ariaNow={size}
ariaMin={minPx}
ariaMax={maxPx}
startCollapsible={collapsible.end}
endCollapsible={nextCollapsible.start}
onPointerDown={(event) => startDragging(index, event)}
onKeyDown={(event) => handleBarKeyDown(index, event)}
onCollapseStart={() => collapseViaControl(index, 'start')}
onCollapseEnd={() => collapseViaControl(index, 'end')}
/>
)
}
return nodes
})}
{dragging && (
<div
className='adaptive-splitter-mask ant-splitter-mask'
style={{ cursor: isVertical ? 'row-resize' : 'col-resize' }}
/>
)}
</div>
)
}
AdaptiveSplitter.Panel = AdaptiveSplitterPanel
AdaptiveSplitter.SIZE_TRANSITION_MS = SIZE_TRANSITION_MS
AdaptiveSplitter.propTypes = {
children: PropTypes.node,
className: PropTypes.string,
style: PropTypes.object,
layout: PropTypes.oneOf(['horizontal', 'vertical']),
orientation: PropTypes.oneOf(['horizontal', 'vertical']),
vertical: PropTypes.bool,
lazy: PropTypes.bool,
onResizeStart: PropTypes.func,
onResize: PropTypes.func,
onResizeEnd: PropTypes.func,
onCollapse: PropTypes.func,
sizeTransitionMs: PropTypes.number
}
export default AdaptiveSplitter

View File

@ -3,22 +3,21 @@ import {
isValidElement, isValidElement,
useCallback, useCallback,
useContext, useContext,
useEffect,
useRef, useRef,
useState useState
} from 'react' } from 'react'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import { Layout, Splitter } from 'antd' import { Layout } from 'antd'
import { useMediaQuery } from 'react-responsive' import { useMediaQuery } from 'react-responsive'
import { ElectronContext } from '../context/ElectronContext' import { ElectronContext } from '../context/ElectronContext'
import AdaptiveSplitter from './AdaptiveSplitter'
const COLLAPSE_BELOW_PX = 150 const COLLAPSE_BELOW_PX = 200
const DEFAULT_SIDEBAR_PX = 300 const DEFAULT_SIDEBAR_PX = 300
const MAX_SIDEBAR_PX = 400 const MAX_SIDEBAR_PX = 400
const COLLAPSED_WIDTH_PX = 80 const COLLAPSED_WIDTH_PX = 80
const ELECTRON_COLLAPSED_WIDTH_PX = 55 const ELECTRON_COLLAPSED_WIDTH_PX = 55
const EXPAND_DRAG_PX = 10 const EXPAND_DRAG_PX = 10
const SIZE_TRANSITION_MS = 300
const COLLAPSE_STORAGE_KEY = 'DashboardSidebar_collapseState' const COLLAPSE_STORAGE_KEY = 'DashboardSidebar_collapseState'
const SIZE_STORAGE_KEY = 'DashboardSidebar_width' const SIZE_STORAGE_KEY = 'DashboardSidebar_width'
@ -56,7 +55,7 @@ const DashboardSidebarSplitter = ({ sidebar, children }) => {
) )
const [sidebarSize, setSidebarSize] = useState(() => { const [sidebarSize, setSidebarSize] = useState(() => {
if (readStoredBoolean(COLLAPSE_STORAGE_KEY, false)) { if (readStoredBoolean(COLLAPSE_STORAGE_KEY, false)) {
return isElectron ? ELECTRON_COLLAPSED_WIDTH_PX : COLLAPSED_WIDTH_PX return collapsedWidth
} }
return clampExpandedSize( return clampExpandedSize(
readStoredNumber(SIZE_STORAGE_KEY, DEFAULT_SIDEBAR_PX) readStoredNumber(SIZE_STORAGE_KEY, DEFAULT_SIDEBAR_PX)
@ -68,118 +67,6 @@ const DashboardSidebarSplitter = ({ sidebar, children }) => {
const lastExpandedSizeRef = useRef( const lastExpandedSizeRef = useRef(
clampExpandedSize(readStoredNumber(SIZE_STORAGE_KEY, DEFAULT_SIDEBAR_PX)) 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)
if (sizeTransitionTimeoutRef.current) {
clearTimeout(sizeTransitionTimeoutRef.current)
}
sizeTransitionTimeoutRef.current = setTimeout(() => {
setSizeTransitioning(false)
sizeTransitionTimeoutRef.current = null
}, 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) => { const persistCollapsed = (nextCollapsed) => {
sessionStorage.setItem(COLLAPSE_STORAGE_KEY, JSON.stringify(nextCollapsed)) sessionStorage.setItem(COLLAPSE_STORAGE_KEY, JSON.stringify(nextCollapsed))
@ -192,113 +79,73 @@ const DashboardSidebarSplitter = ({ sidebar, children }) => {
const collapseSidebar = useCallback( const collapseSidebar = useCallback(
(expandedSize) => { (expandedSize) => {
if (expandedSize >= COLLAPSE_BELOW_PX) { if (expandedSize >= COLLAPSE_BELOW_PX) {
lastExpandedSizeRef.current = expandedSize lastExpandedSizeRef.current = clampExpandedSize(expandedSize)
persistSize(expandedSize) persistSize(lastExpandedSizeRef.current)
} }
collapsedRef.current = true collapsedRef.current = true
persistCollapsed(true) persistCollapsed(true)
startSizeTransition()
setSidebarSize(sidebarSizeRef.current)
setCollapsed(true) setCollapsed(true)
animateCollapsedSize() setSidebarSize(collapsedWidth)
}, },
[animateCollapsedSize, startSizeTransition] [collapsedWidth]
) )
const expandSidebar = useCallback(() => { const expandSidebar = useCallback(() => {
cancelCollapseSizeAnimation()
collapsedRef.current = false collapsedRef.current = false
setCollapsed(false) setCollapsed(false)
persistCollapsed(false) persistCollapsed(false)
startSizeTransition() const nextSize = clampExpandedSize(lastExpandedSizeRef.current)
setSidebarSize(DEFAULT_SIDEBAR_PX) lastExpandedSizeRef.current = nextSize
persistSize(DEFAULT_SIDEBAR_PX) setSidebarSize(nextSize)
lastExpandedSizeRef.current = DEFAULT_SIDEBAR_PX persistSize(nextSize)
}, [cancelCollapseSizeAnimation, startSizeTransition]) }, [])
const handleSidebarCollapse = useCallback( const handleSidebarCollapse = useCallback(
(nextCollapsed) => { (nextCollapsed) => {
if (nextCollapsed === collapsedRef.current) return
if (nextCollapsed) { if (nextCollapsed) {
collapseSidebar(sidebarSize) collapseSidebar(lastExpandedSizeRef.current)
} else { } else {
expandSidebar() expandSidebar()
} }
lockResizeFor(SIZE_TRANSITION_MS)
}, },
[collapseSidebar, expandSidebar, lockResizeFor, sidebarSize] [collapseSidebar, expandSidebar]
) )
const handleResizeStart = useCallback(() => { const handleSplitterCollapse = useCallback(
if (!resizeLockedRef.current) { (nextCollapsed) => {
ignoreResizeEndRef.current = false 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 handleResize = useCallback(
(sizes) => {
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()
lockResizeUntilPointerUp()
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, expandSidebar, lockResizeUntilPointerUp]
)
const handleResizeEnd = useCallback( const handleResizeEnd = useCallback(
(sizes) => { (sizes) => {
if (
resizeLockedRef.current ||
suppressGestureRef.current ||
ignoreResizeEndRef.current
) {
ignoreResizeEndRef.current = false
if (collapsedRef.current) {
startSizeTransition()
setSidebarSize(collapsedWidth)
}
return
}
if (collapsedRef.current) { if (collapsedRef.current) {
if (sizes[0] - collapsedWidth > EXPAND_DRAG_PX) { setSidebarSize(collapsedWidth)
expandSidebar()
lockResizeFor(SIZE_TRANSITION_MS)
} else {
startSizeTransition()
setSidebarSize(collapsedWidth)
}
return return
} }
const next = clampExpandedSize(sizes[0]) const next = clampExpandedSize(sizes[0])
lastExpandedSizeRef.current = next lastExpandedSizeRef.current = next
setSidebarSize(next) setSidebarSize(next)
persistSize(next) persistSize(next)
}, },
[collapsedWidth, expandSidebar, lockResizeFor, startSizeTransition] [collapsedWidth]
) )
const sidebarNode = isValidElement(sidebar) const sidebarNode = isValidElement(sidebar)
@ -320,38 +167,38 @@ const DashboardSidebarSplitter = ({ sidebar, children }) => {
return ( return (
<Layout style={{ height: '100%', minHeight: 0 }}> <Layout style={{ height: '100%', minHeight: 0 }}>
<Splitter <AdaptiveSplitter
className={`farmcontrol-splitter dashboard-splitter${ className={`farmcontrol-splitter dashboard-splitter${
sizeTransitioning ? ' is-size-transitioning' : '' collapsed ? ' is-sidebar-collapsed' : ''
}${collapsed ? ' is-sidebar-collapsed' : ''}`} }`}
style={{ height: '100%' }} style={{ height: '100%' }}
onResizeStart={handleResizeStart}
onResize={handleResize} onResize={handleResize}
onResizeEnd={handleResizeEnd} onResizeEnd={handleResizeEnd}
> >
<Splitter.Panel <AdaptiveSplitter.Panel
className={`dashboard-splitter-sidebar-panel${ className={`dashboard-splitter-sidebar-panel${
collapsed ? ' is-collapsed' : '' collapsed ? ' is-collapsed' : ''
}`} }`}
size={sidebarSize} size={sidebarSize}
min={collapsedWidth} min={collapsedWidth}
max={ max={MAX_SIDEBAR_PX}
collapsed && resizeLocked && !sizeTransitioning collapsed={collapsed}
? collapsedWidth collapsedSize={collapsedWidth}
: MAX_SIDEBAR_PX collapseThreshold={COLLAPSE_BELOW_PX}
} expandDragPx={EXPAND_DRAG_PX}
resizable={!resizeLocked} expandedSize={lastExpandedSizeRef.current}
style={{ onCollapse={handleSplitterCollapse}
overflow: 'visible', style={{ overflow: 'visible' }}
'--dashboard-splitter-sidebar-size': `${sidebarSize}px`
}}
> >
{sidebarNode} {sidebarNode}
</Splitter.Panel> </AdaptiveSplitter.Panel>
<Splitter.Panel min={320} className='dashboard-splitter-content-panel'> <AdaptiveSplitter.Panel
min={320}
className='dashboard-splitter-content-panel'
>
{children} {children}
</Splitter.Panel> </AdaptiveSplitter.Panel>
</Splitter> </AdaptiveSplitter>
</Layout> </Layout>
) )
} }