Enhance DashboardOverviewFlexColumns with Vertical Layout Support
All checks were successful
farmcontrol/farmcontrol-ui/pipeline/head This commit looks good

- Added functionality to toggle between vertical and horizontal layouts in the DashboardOverviewFlexColumns component, improving responsiveness based on available space.
- Implemented useLayoutEffect to manage layout changes dynamically, ensuring optimal display of child components.
- Updated CSS styles to support the new vertical layout, enhancing the overall user experience in the dashboard.
This commit is contained in:
Tom Butcher 2026-08-30 14:42:15 +01:00
parent e3a20e6b48
commit b347ec3637
2 changed files with 157 additions and 38 deletions

View File

@ -2005,6 +2005,12 @@ span.ant-skeleton-input.ant-skeleton-input-sm.text-skeleton {
width: 100%;
}
.overview-flex-columns.is-vertical .overview-flex-columns-item {
flex: none;
width: 100%;
transition: none;
}
.overview-flex-columns-empty {
flex: 1;
min-height: 48px;

View File

@ -4,6 +4,7 @@ import {
isValidElement,
useCallback,
useEffect,
useLayoutEffect,
useMemo,
useRef,
useState
@ -55,8 +56,114 @@ const DashboardOverviewFlexColumns = ({
[childList.length, proportions]
)
const dragRef = useRef(null)
const rowRef = useRef(null)
const onProportionsChangeRef = useRef(onProportionsChange)
const stackAtWidthRef = useRef(null)
const isVerticalRef = useRef(false)
const [activeSeparator, setActiveSeparator] = useState(null)
const [isVertical, setIsVertical] = useState(false)
const setVertical = useCallback((next) => {
isVerticalRef.current = next
setIsVertical((previous) => (previous === next ? previous : next))
}, [])
useLayoutEffect(() => {
const row = rowRef.current
if (!row || isEmpty) {
stackAtWidthRef.current = null
setVertical(false)
return undefined
}
const hasHorizontalOverflow = (items) => {
if (row.scrollWidth > row.clientWidth + 1) {
return true
}
return items.some((item) => item.scrollWidth > item.clientWidth + 1)
}
let frame = null
const update = () => {
const rowWidth = row.clientWidth
if (rowWidth < 1) {
return
}
const flexEl = row.firstElementChild
if (!flexEl) {
return
}
const items = [...row.querySelectorAll('.overview-flex-columns-item')]
if (!items.length) {
return
}
const stackState = stackAtWidthRef.current
if (!isVerticalRef.current) {
if (
stackState &&
stackState.childCount === items.length &&
rowWidth > stackState.width
) {
return
}
if (hasHorizontalOverflow(items)) {
stackAtWidthRef.current = {
width: rowWidth,
childCount: items.length
}
setVertical(true)
return
}
stackAtWidthRef.current = null
setVertical(false)
return
}
if (!stackState || stackState.childCount !== items.length) {
stackAtWidthRef.current = null
setVertical(false)
return
}
if (rowWidth > stackState.width) {
setVertical(false)
}
}
const scheduleUpdate = () => {
if (frame != null) {
cancelAnimationFrame(frame)
}
frame = requestAnimationFrame(() => {
frame = null
update()
})
}
scheduleUpdate()
const resizeObserver = new ResizeObserver(scheduleUpdate)
resizeObserver.observe(row)
row.querySelectorAll('.overview-flex-columns-item').forEach((item) => {
resizeObserver.observe(item)
})
return () => {
if (frame != null) {
cancelAnimationFrame(frame)
}
resizeObserver.disconnect()
}
}, [childList, isEmpty, resolvedProportions, setVertical])
useEffect(() => {
onProportionsChangeRef.current = onProportionsChange
@ -170,7 +277,9 @@ const DashboardOverviewFlexColumns = ({
isLayoutEditing ? 'is-editing' : ''
} ${isDragOver ? 'is-drag-over' : ''} ${
isEmpty && isLayoutEditing ? 'is-empty' : ''
} ${activeSeparator != null ? 'is-resizing' : ''}`}
} ${activeSeparator != null ? 'is-resizing' : ''} ${
isVertical ? 'is-vertical' : ''
}`}
onDragOver={onDragOver}
onDrop={onDrop}
onDragLeave={onDragLeave}
@ -195,44 +304,48 @@ const DashboardOverviewFlexColumns = ({
</Flex>
</Card>
)}
<Flex
gap='large'
align='stretch'
className='overview-flex-columns-row'
wrap='nowrap'
>
{isEmpty && isLayoutEditing ? (
<div className='overview-flex-columns-empty'>Drop sections here</div>
) : (
childList.map((child, index) => {
if (!isValidElement(child)) return child
const proportion = resolvedProportions[index] ?? 0
const showSeparator =
isLayoutEditing && index < childList.length - 1
return cloneElement(child, {
style: {
...child.props.style,
flex: `${proportion} 1 0`,
minWidth: 0,
position: 'relative'
},
children: (
<>
{child.props.children}
{showSeparator ? (
<ColumnSeparator
active={activeSeparator === index}
onPointerDown={(event) =>
handlePointerDown(event, index)
}
/>
) : null}
</>
)
<div ref={rowRef} className='overview-flex-columns-row'>
<Flex
vertical={isVertical}
gap='large'
align='stretch'
wrap='nowrap'
style={{ width: '100%' }}
>
{isEmpty && isLayoutEditing ? (
<div className='overview-flex-columns-empty'>Drop sections here</div>
) : (
childList.map((child, index) => {
if (!isValidElement(child)) return child
const proportion = resolvedProportions[index] ?? 0
const showSeparator =
!isVertical && isLayoutEditing && index < childList.length - 1
return cloneElement(child, {
style: {
...child.props.style,
...(isVertical
? { width: '100%', flex: 'none' }
: { flex: `${proportion} 1 0`, minWidth: 0 }),
position: 'relative'
},
children: (
<>
{child.props.children}
{showSeparator ? (
<ColumnSeparator
active={activeSeparator === index}
onPointerDown={(event) =>
handlePointerDown(event, index)
}
/>
) : null}
</>
)
})
})
})
)}
</Flex>
)}
</Flex>
</div>
</div>
)
}