Collapsible resizers (#3330)

* Collapsible resizable divs

Co-authored-by: Tim O'Farrell <tofarr@gmai.com>
Co-authored-by: sp.wack <83104063+amanape@users.noreply.github.com>
This commit is contained in:
tofarr
2024-08-12 15:05:59 -06:00
committed by GitHub
parent e2b2f74737
commit 930ee27037
2 changed files with 120 additions and 28 deletions

View File

@@ -83,19 +83,19 @@ function App(): JSX.Element {
className="grow h-full min-h-0 min-w-0 px-3 pt-3"
initialSize={500}
firstChild={<ChatInterface />}
firstClassName="min-w-[500px] rounded-xl overflow-hidden border border-neutral-600"
firstClassName="rounded-xl overflow-hidden border border-neutral-600"
secondChild={
<Container
orientation={Orientation.VERTICAL}
className="grow h-full min-h-0 min-w-0"
className="h-full min-h-0 min-w-0"
initialSize={window.innerHeight - 300}
firstChild={<Workspace />}
firstClassName="min-h-72 rounded-xl border border-neutral-600 bg-neutral-800 flex flex-col overflow-hidden"
firstClassName="rounded-xl border border-neutral-600 bg-neutral-800 flex flex-col overflow-hidden"
secondChild={<Terminal />}
secondClassName="min-h-72 rounded-xl border border-neutral-600 bg-neutral-800"
secondClassName="rounded-xl border border-neutral-600 bg-neutral-800"
/>
}
secondClassName="flex flex-col overflow-hidden grow min-w-[500px]"
secondClassName="flex flex-col overflow-hidden"
/>
</div>
<Controls setSettingOpen={onSettingsModalOpen} />

View File

@@ -1,11 +1,24 @@
import React, { useEffect, useRef, useState } from "react";
import React, { CSSProperties, useEffect, useRef, useState } from "react";
import {
VscChevronDown,
VscChevronLeft,
VscChevronRight,
VscChevronUp,
} from "react-icons/vsc";
import { twMerge } from "tailwind-merge";
import IconButton from "./IconButton";
export enum Orientation {
HORIZONTAL = "horizontal",
VERTICAL = "vertical",
}
enum Collapse {
COLLAPSED = "collapsed",
SPLIT = "split",
FILLED = "filled",
}
type ContainerProps = {
firstChild: React.ReactNode;
firstClassName: string | undefined;
@@ -28,30 +41,40 @@ export function Container({
const [firstSize, setFirstSize] = useState<number>(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 =
orientation === Orientation.HORIZONTAL ? e.clientX : e.clientY;
const position = isHorizontal ? e.clientX : e.clientY;
return firstSize + position - dividerPosition;
};
const onMouseMove = (e: MouseEvent) => {
e.preventDefault();
const newFirstSize = getFirstSizeFromEvent(e);
const newFirstSize = `${getFirstSizeFromEvent(e)}px`;
const { current } = firstRef;
if (current) {
if (orientation === Orientation.HORIZONTAL) {
current.style.width = `${newFirstSize}px`;
if (isHorizontal) {
current.style.width = newFirstSize;
current.style.minWidth = newFirstSize;
} else {
current.style.height = `${newFirstSize}px`;
current.style.height = newFirstSize;
current.style.minHeight = newFirstSize;
}
}
};
const onMouseUp = (e: MouseEvent) => {
e.preventDefault();
if (firstRef.current) {
firstRef.current.style.transition = "";
}
if (secondRef.current) {
secondRef.current.style.transition = "";
}
setFirstSize(getFirstSizeFromEvent(e));
setDividerPosition(null);
document.removeEventListener("mousemove", onMouseMove);
@@ -67,33 +90,102 @@ export function Container({
const onMouseDown = (e: React.MouseEvent) => {
e.preventDefault();
const position =
orientation === Orientation.HORIZONTAL ? e.clientX : e.clientY;
if (firstRef.current) {
firstRef.current.style.transition = "none";
}
if (secondRef.current) {
secondRef.current.style.transition = "none";
}
const position = isHorizontal ? e.clientX : e.clientY;
setDividerPosition(position);
};
const getStyleForFirst = () => {
if (orientation === Orientation.HORIZONTAL) {
return { width: `${firstSize}px` };
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;
}
} 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;
};
const onCollapse = () => {
if (collapse === Collapse.SPLIT) {
setCollapse(Collapse.COLLAPSED);
} else {
setCollapse(Collapse.SPLIT);
}
};
const onExpand = () => {
if (collapse === Collapse.SPLIT) {
setCollapse(Collapse.FILLED);
} else {
setCollapse(Collapse.SPLIT);
}
return { height: `${firstSize}px` };
};
return (
<div
className={twMerge(
`flex ${orientation === Orientation.HORIZONTAL ? "" : "flex-col"}`,
className,
)}
>
<div ref={firstRef} className={firstClassName} style={getStyleForFirst()}>
<div className={twMerge("flex", !isHorizontal && "flex-col", className)}>
<div
ref={firstRef}
className={twMerge(firstClassName, "transition-all ease-soft-spring")}
style={getStyleForFirst()}
>
{firstChild}
</div>
<div
className={`${orientation === Orientation.VERTICAL ? "cursor-ns-resize h-3" : "cursor-ew-resize w-3"} shrink-0`}
onMouseDown={onMouseDown}
/>
<div className={twMerge(secondClassName, "flex-1")}>{secondChild}</div>
className={`${isHorizontal ? "cursor-ew-resize w-3 flex-col" : "cursor-ns-resize h-3 flex-row"} shrink-0 flex justify-center items-center`}
onMouseDown={collapse === Collapse.SPLIT ? onMouseDown : undefined}
>
<IconButton
icon={isHorizontal ? <VscChevronLeft /> : <VscChevronUp />}
ariaLabel="Collapse"
onClick={onCollapse}
/>
<IconButton
icon={isHorizontal ? <VscChevronRight /> : <VscChevronDown />}
ariaLabel="Expand"
onClick={onExpand}
/>
</div>
<div
ref={secondRef}
className={twMerge(secondClassName, "transition-all ease-soft-spring")}
style={getStyleForSecond()}
>
{secondChild}
</div>
</div>
);
}