Compare commits

...

1 Commits

Author SHA1 Message Date
openhands
96fd7c3cc4 fix: ensure panels occupy full window size when window is large
- Add proper size constraints based on window dimensions
- Add window resize handling to maintain constraints
- Improve panel styles with proper flex behavior
- Set appropriate min/max dimensions for both panels
2025-01-07 03:39:57 +00:00

View File

@@ -1,4 +1,11 @@
import React, { CSSProperties, JSX, useEffect, useRef, useState } from "react";
import React, {
CSSProperties,
JSX,
useCallback,
useEffect,
useRef,
useState,
} from "react";
import {
VscChevronDown,
VscChevronLeft,
@@ -38,55 +45,70 @@ export function ResizablePanel({
orientation,
initialSize,
}: ResizablePanelProps): JSX.Element {
const [firstSize, setFirstSize] = useState<number>(initialSize);
const isHorizontal = orientation === Orientation.HORIZONTAL;
const getConstraints = useCallback(
() => ({
min: isHorizontal ? 350 : 300,
max: isHorizontal ? window.innerWidth * 0.5 : window.innerHeight * 0.7,
}),
[isHorizontal],
);
const constrainSize = useCallback(
(size: number) => {
const { min, max } = getConstraints();
return Math.min(Math.max(size, min), max);
},
[getConstraints],
);
const [firstSize, setFirstSize] = useState(() => constrainSize(initialSize));
const [dividerPosition, setDividerPosition] = useState<number | null>(null);
const firstRef = useRef<HTMLDivElement>(null);
const secondRef = useRef<HTMLDivElement>(null);
const [collapse, setCollapse] = useState<Collapse>(Collapse.SPLIT);
const isHorizontal = orientation === Orientation.HORIZONTAL;
useEffect(() => {
if (dividerPosition == null || !firstRef.current) {
return undefined;
}
const getFirstSizeFromEvent = (e: MouseEvent) => {
const position = isHorizontal ? e.clientX : e.clientY;
return firstSize + position - dividerPosition;
const handleResize = () => setFirstSize(constrainSize(firstSize));
const timeoutId = setTimeout(handleResize, 100);
window.addEventListener("resize", handleResize);
return () => {
clearTimeout(timeoutId);
window.removeEventListener("resize", handleResize);
};
}, [firstSize, constrainSize]);
useEffect(() => {
if (!dividerPosition) return undefined;
const onMouseMove = (e: MouseEvent) => {
e.preventDefault();
const newFirstSize = `${getFirstSizeFromEvent(e)}px`;
const { current } = firstRef;
if (current) {
if (isHorizontal) {
current.style.width = newFirstSize;
current.style.minWidth = newFirstSize;
} else {
current.style.height = newFirstSize;
current.style.minHeight = newFirstSize;
}
}
const delta = (isHorizontal ? e.clientX : e.clientY) - dividerPosition;
setFirstSize(constrainSize(firstSize + delta));
setDividerPosition(isHorizontal ? e.clientX : e.clientY);
};
const onMouseUp = (e: MouseEvent) => {
e.preventDefault();
if (firstRef.current) {
firstRef.current.style.transition = "";
}
if (secondRef.current) {
secondRef.current.style.transition = "";
}
setFirstSize(getFirstSizeFromEvent(e));
if (firstRef.current) firstRef.current.style.transition = "";
if (secondRef.current) secondRef.current.style.transition = "";
setFirstSize(
constrainSize(
firstSize +
((isHorizontal ? e.clientX : e.clientY) - dividerPosition),
),
);
setDividerPosition(null);
document.removeEventListener("mousemove", onMouseMove);
document.removeEventListener("mouseup", onMouseUp);
};
document.addEventListener("mousemove", onMouseMove);
document.addEventListener("mouseup", onMouseUp);
return () => {
document.removeEventListener("mousemove", onMouseMove);
document.removeEventListener("mouseup", onMouseUp);
};
}, [dividerPosition, firstSize, orientation]);
}, [dividerPosition, firstSize, isHorizontal, constrainSize]);
const onMouseDown = (e: React.MouseEvent) => {
e.preventDefault();
@@ -100,67 +122,82 @@ export function ResizablePanel({
setDividerPosition(position);
};
const getStyleForFirst = () => {
const style: CSSProperties = { overflow: "hidden" };
if (collapse === Collapse.COLLAPSED) {
style.opacity = 0;
style.width = 0;
style.minWidth = 0;
style.height = 0;
style.minHeight = 0;
} else if (collapse === Collapse.SPLIT) {
const firstSizePx = `${firstSize}px`;
if (isHorizontal) {
style.width = firstSizePx;
style.minWidth = firstSizePx;
} else {
style.height = firstSizePx;
style.minHeight = firstSizePx;
const getPanelStyle = useCallback(
(isFirst: boolean): CSSProperties => {
const style: CSSProperties = { overflow: "hidden" };
const { min } = getConstraints();
const isHidden =
(isFirst && collapse === Collapse.COLLAPSED) ||
(!isFirst && collapse === Collapse.FILLED);
const hiddenStyle: CSSProperties = {
...style,
opacity: 0,
width: 0,
minWidth: 0,
height: 0,
minHeight: 0,
};
const expandedStyle: CSSProperties = { ...style, flexGrow: 1 };
if (isHidden) {
return hiddenStyle;
}
} else {
style.flexGrow = 1;
}
return style;
};
const getStyleForSecond = () => {
const style: CSSProperties = { overflow: "hidden" };
if (collapse === Collapse.FILLED) {
style.opacity = 0;
style.width = 0;
style.minWidth = 0;
style.height = 0;
style.minHeight = 0;
} else if (collapse === Collapse.SPLIT) {
style.flexGrow = 1;
} else {
style.flexGrow = 1;
}
return style;
};
if (collapse !== Collapse.SPLIT) {
return expandedStyle;
}
const onCollapse = () => {
if (collapse === Collapse.SPLIT) {
setCollapse(Collapse.COLLAPSED);
} else {
setCollapse(Collapse.SPLIT);
}
};
if (isFirst) {
const dimension = isHorizontal ? "width" : "height";
const minDimension = isHorizontal ? "minWidth" : "minHeight";
const maxDimension = isHorizontal ? "maxWidth" : "maxHeight";
const onExpand = () => {
if (collapse === Collapse.SPLIT) {
setCollapse(Collapse.FILLED);
} else {
setCollapse(Collapse.SPLIT);
}
};
const firstPanelStyle: CSSProperties = {
...style,
[dimension]: `${firstSize}px`,
[minDimension]: `${min}px`,
[maxDimension]: isHorizontal ? "50%" : "70%",
flexShrink: 0,
};
return firstPanelStyle;
}
const secondPanelStyle: CSSProperties = {
...style,
flexGrow: 1,
flexShrink: 1,
...(isHorizontal
? {
minWidth: "30%",
maxWidth: "70%",
}
: {
minHeight: "300px",
display: "flex",
flexDirection: "column",
}),
};
return secondPanelStyle;
},
[collapse, firstSize, isHorizontal, getConstraints],
);
const toggleCollapse = () =>
setCollapse(
collapse === Collapse.SPLIT ? Collapse.COLLAPSED : Collapse.SPLIT,
);
const toggleExpand = () =>
setCollapse(collapse === Collapse.SPLIT ? Collapse.FILLED : Collapse.SPLIT);
return (
<div className={twMerge("flex", !isHorizontal && "flex-col", className)}>
<div
ref={firstRef}
className={twMerge(firstClassName, "transition-all ease-soft-spring")}
style={getStyleForFirst()}
style={getPanelStyle(true)}
>
{firstChild}
</div>
@@ -171,18 +208,18 @@ export function ResizablePanel({
<IconButton
icon={isHorizontal ? <VscChevronLeft /> : <VscChevronUp />}
ariaLabel="Collapse"
onClick={onCollapse}
onClick={toggleCollapse}
/>
<IconButton
icon={isHorizontal ? <VscChevronRight /> : <VscChevronDown />}
ariaLabel="Expand"
onClick={onExpand}
onClick={toggleExpand}
/>
</div>
<div
ref={secondRef}
className={twMerge(secondClassName, "transition-all ease-soft-spring")}
style={getStyleForSecond()}
style={getPanelStyle(false)}
>
{secondChild}
</div>