From 9fc13b6819b64ef11a8e3d9f3d8be4d466f83fab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Paulo=20Lagoa=CC=81?= Date: Tue, 12 Nov 2024 15:21:10 +0000 Subject: [PATCH] feat(CanvasSidePanel): add resizable feature --- .../src/Canvas/SidePanel/SidePanel.tsx | 26 ++++- .../src/Canvas/SidePanel/useResizable.tsx | 107 ++++++++++++++++++ 2 files changed, 132 insertions(+), 1 deletion(-) create mode 100644 packages/pentaho/src/Canvas/SidePanel/useResizable.tsx diff --git a/packages/pentaho/src/Canvas/SidePanel/SidePanel.tsx b/packages/pentaho/src/Canvas/SidePanel/SidePanel.tsx index b7e0cade4e..ccd39cca50 100644 --- a/packages/pentaho/src/Canvas/SidePanel/SidePanel.tsx +++ b/packages/pentaho/src/Canvas/SidePanel/SidePanel.tsx @@ -1,3 +1,4 @@ +/* eslint-disable jsx-a11y/no-noninteractive-element-interactions */ import { forwardRef } from "react"; import { ExtractNames, @@ -14,6 +15,7 @@ import { End } from "@hitachivantara/uikit-react-icons"; import { HvCanvasPanelTab } from "../PanelTab"; import { HvCanvasPanelTabs, HvCanvasPanelTabsProps } from "../PanelTabs"; import { staticClasses, useClasses } from "./SidePanel.styles"; +import { useResizable } from "./useResizable"; export { staticClasses as canvasSidePanelClasses }; @@ -49,6 +51,8 @@ export interface HvCanvasSidePanelProps ) => void; /** An object containing all the labels. */ labels?: Partial; + /** Whether the side panel is resizable horizontally. */ + resizable?: boolean; /** The content that will be rendered within the canvas panel. */ children?: React.ReactNode; /** A Jss Object used to override or extend the styles applied. */ @@ -71,6 +75,7 @@ export const HvCanvasSidePanel = forwardRef< onToggle, onTabChange, labels: labelsProp, + resizable = true, className, children, classes: classesProp, @@ -89,6 +94,15 @@ export const HvCanvasSidePanel = forwardRef< tabs?.[0]?.id ?? "none", ); + const { width, isDragging, getContainerProps, getSeparatorProps } = + useResizable({ + resizable, + ref, + initialWidth: 320, + minWidth: 100, + maxWidth: 500, + }); + const handleTogglePanel = (event: React.MouseEvent | React.KeyboardEvent) => { setOpen((prev) => !prev); onToggle?.(event, !open); @@ -105,12 +119,17 @@ export const HvCanvasSidePanel = forwardRef< return ( <>
{tabs && ( @@ -138,6 +157,7 @@ export const HvCanvasSidePanel = forwardRef< {children}
+ {resizable &&
} diff --git a/packages/pentaho/src/Canvas/SidePanel/useResizable.tsx b/packages/pentaho/src/Canvas/SidePanel/useResizable.tsx new file mode 100644 index 0000000000..9f9e1a7ca3 --- /dev/null +++ b/packages/pentaho/src/Canvas/SidePanel/useResizable.tsx @@ -0,0 +1,107 @@ +import { useRef, useState } from "react"; +import { useForkRef } from "@hitachivantara/uikit-react-core"; + +interface ContainerProps { + ref: any; + style: React.CSSProperties; +} + +interface HandleProps { + style: React.CSSProperties; + onMouseMove?: (event: React.MouseEvent) => void; + onMouseLeave?: () => void; + onMouseDown?: () => void; + role: string; +} + +interface ResizableProps { + resizable: boolean; + ref: any; + initialWidth?: number; + minWidth?: number; + maxWidth?: number; +} + +export const useResizable = ( + resizableOptions: ResizableProps, +): { + width: number; + isDragging: boolean; + getContainerProps: () => ContainerProps; + getSeparatorProps: () => HandleProps; +} => { + const { + resizable, + ref, + initialWidth = 320, + minWidth = 100, + maxWidth = 600, + } = resizableOptions; + + const [width, setWidth] = useState(initialWidth); + const [isHover, setIsHover] = useState(false); + const [isDragging, setIsDragging] = useState(false); + + const panelRef = useRef(null); + + const forkedRef = useForkRef(ref, panelRef); + + const mouseMove = (event: any) => { + if (panelRef.current) { + const rect = panelRef.current.getBoundingClientRect(); + const newWidth = event.clientX - rect.left; + if (newWidth >= minWidth && newWidth <= maxWidth) { + setWidth(newWidth); + } + } + }; + + const handleMouseMove = (event: any) => { + if (panelRef.current) { + const rect = panelRef.current.getBoundingClientRect(); + const isHoverBorder = + event.clientX >= rect.right - 5 && event.clientX <= rect.right + 5; + setIsHover(isHoverBorder); + } + }; + + const handleMouseUp = () => { + document.removeEventListener("mousemove", mouseMove); + document.removeEventListener("mouseup", handleMouseUp); + setIsDragging(false); + }; + + const startResizing = () => { + document.addEventListener("mousemove", mouseMove); + document.addEventListener("mouseup", handleMouseUp); + setIsDragging(true); + }; + + const getContainerProps = (): ContainerProps => ({ + ref: forkedRef, + style: { + width, + }, + }); + + const getSeparatorProps = (): HandleProps => ({ + style: { + left: width, + position: "absolute", + top: 0, + bottom: 0, + width: 5, + cursor: resizable ? "col-resize" : "default", + }, + onMouseMove: resizable ? handleMouseMove : undefined, + onMouseLeave: resizable ? () => setIsHover(false) : undefined, + onMouseDown: resizable + ? () => { + if (isHover) startResizing(); + } + : undefined, + role: "separator", + }); + + return { width, isDragging, getContainerProps, getSeparatorProps }; +};