From 930ee2703709b8e812195e2566f85d330a5a983e Mon Sep 17 00:00:00 2001 From: tofarr Date: Mon, 12 Aug 2024 15:05:59 -0600 Subject: [PATCH] Collapsible resizers (#3330) * Collapsible resizable divs Co-authored-by: Tim O'Farrell Co-authored-by: sp.wack <83104063+amanape@users.noreply.github.com> --- frontend/src/App.tsx | 10 +- frontend/src/components/Resizable.tsx | 138 +++++++++++++++++++++----- 2 files changed, 120 insertions(+), 28 deletions(-) diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 08491c805e..5cd81ecc81 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -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={} - firstClassName="min-w-[500px] rounded-xl overflow-hidden border border-neutral-600" + firstClassName="rounded-xl overflow-hidden border border-neutral-600" secondChild={ } - 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={} - 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" /> diff --git a/frontend/src/components/Resizable.tsx b/frontend/src/components/Resizable.tsx index 5bdc98afb2..fd82a3b9c5 100644 --- a/frontend/src/components/Resizable.tsx +++ b/frontend/src/components/Resizable.tsx @@ -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(initialSize); const [dividerPosition, setDividerPosition] = useState(null); const firstRef = useRef(null); + const secondRef = useRef(null); + const [collapse, setCollapse] = useState(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 ( -
-
+
+
{firstChild}
-
{secondChild}
+ 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} + > + : } + ariaLabel="Collapse" + onClick={onCollapse} + /> + : } + ariaLabel="Expand" + onClick={onExpand} + /> +
+
+ {secondChild} +
); }