mirror of
https://github.com/All-Hands-AI/OpenHands.git
synced 2026-04-29 03:00:45 -04:00
Compare commits
1 Commits
ctrl-c
...
fix-panel-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
96fd7c3cc4 |
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user