diff --git a/assets/stylesheets/App.css b/assets/stylesheets/App.css index c7e8be02..f5e700d9 100644 --- a/assets/stylesheets/App.css +++ b/assets/stylesheets/App.css @@ -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; diff --git a/src/components/Dashboard/common/DashboardOverviewFlexColumns.jsx b/src/components/Dashboard/common/DashboardOverviewFlexColumns.jsx index 22a06b55..431ddec7 100644 --- a/src/components/Dashboard/common/DashboardOverviewFlexColumns.jsx +++ b/src/components/Dashboard/common/DashboardOverviewFlexColumns.jsx @@ -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 = ({ )} - - {isEmpty && isLayoutEditing ? ( -
Drop sections here
- ) : ( - 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 ? ( - - handlePointerDown(event, index) - } - /> - ) : null} - - ) +
+ + {isEmpty && isLayoutEditing ? ( +
Drop sections here
+ ) : ( + 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 ? ( + + handlePointerDown(event, index) + } + /> + ) : null} + + ) + }) }) - }) - )} -
+ )} + +
) }