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%; width: 100%;
} }
.overview-flex-columns.is-vertical .overview-flex-columns-item {
flex: none;
width: 100%;
transition: none;
}
.overview-flex-columns-empty { .overview-flex-columns-empty {
flex: 1; flex: 1;
min-height: 48px; min-height: 48px;

View File

@ -4,6 +4,7 @@ import {
isValidElement, isValidElement,
useCallback, useCallback,
useEffect, useEffect,
useLayoutEffect,
useMemo, useMemo,
useRef, useRef,
useState useState
@ -55,8 +56,114 @@ const DashboardOverviewFlexColumns = ({
[childList.length, proportions] [childList.length, proportions]
) )
const dragRef = useRef(null) const dragRef = useRef(null)
const rowRef = useRef(null)
const onProportionsChangeRef = useRef(onProportionsChange) const onProportionsChangeRef = useRef(onProportionsChange)
const stackAtWidthRef = useRef(null)
const isVerticalRef = useRef(false)
const [activeSeparator, setActiveSeparator] = useState(null) 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(() => { useEffect(() => {
onProportionsChangeRef.current = onProportionsChange onProportionsChangeRef.current = onProportionsChange
@ -170,7 +277,9 @@ const DashboardOverviewFlexColumns = ({
isLayoutEditing ? 'is-editing' : '' isLayoutEditing ? 'is-editing' : ''
} ${isDragOver ? 'is-drag-over' : ''} ${ } ${isDragOver ? 'is-drag-over' : ''} ${
isEmpty && isLayoutEditing ? 'is-empty' : '' isEmpty && isLayoutEditing ? 'is-empty' : ''
} ${activeSeparator != null ? 'is-resizing' : ''}`} } ${activeSeparator != null ? 'is-resizing' : ''} ${
isVertical ? 'is-vertical' : ''
}`}
onDragOver={onDragOver} onDragOver={onDragOver}
onDrop={onDrop} onDrop={onDrop}
onDragLeave={onDragLeave} onDragLeave={onDragLeave}
@ -195,44 +304,48 @@ const DashboardOverviewFlexColumns = ({
</Flex> </Flex>
</Card> </Card>
)} )}
<Flex <div ref={rowRef} className='overview-flex-columns-row'>
gap='large' <Flex
align='stretch' vertical={isVertical}
className='overview-flex-columns-row' gap='large'
wrap='nowrap' align='stretch'
> wrap='nowrap'
{isEmpty && isLayoutEditing ? ( style={{ width: '100%' }}
<div className='overview-flex-columns-empty'>Drop sections here</div> >
) : ( {isEmpty && isLayoutEditing ? (
childList.map((child, index) => { <div className='overview-flex-columns-empty'>Drop sections here</div>
if (!isValidElement(child)) return child ) : (
const proportion = resolvedProportions[index] ?? 0 childList.map((child, index) => {
const showSeparator = if (!isValidElement(child)) return child
isLayoutEditing && index < childList.length - 1 const proportion = resolvedProportions[index] ?? 0
return cloneElement(child, { const showSeparator =
style: { !isVertical && isLayoutEditing && index < childList.length - 1
...child.props.style, return cloneElement(child, {
flex: `${proportion} 1 0`, style: {
minWidth: 0, ...child.props.style,
position: 'relative' ...(isVertical
}, ? { width: '100%', flex: 'none' }
children: ( : { flex: `${proportion} 1 0`, minWidth: 0 }),
<> position: 'relative'
{child.props.children} },
{showSeparator ? ( children: (
<ColumnSeparator <>
active={activeSeparator === index} {child.props.children}
onPointerDown={(event) => {showSeparator ? (
handlePointerDown(event, index) <ColumnSeparator
} active={activeSeparator === index}
/> onPointerDown={(event) =>
) : null} handlePointerDown(event, index)
</> }
) />
) : null}
</>
)
})
}) })
}) )}
)} </Flex>
</Flex> </div>
</div> </div>
) )
} }