pankod/refine

View on GitHub
packages/devtools/src/components/resizable-pane.tsx

Summary

Maintainability
A
2 hrs
Test Coverage
import React from "react";
import { Placement } from "src/interfaces/placement";
import {
  getDefaultPanelSize,
  getMaxPanelHeight,
  getMaxPanelWidth,
  getPanelPosition,
  getPanelToggleTransforms,
  MIN_PANEL_HEIGHT,
  MIN_PANEL_WIDTH,
  roundToEven,
} from "src/utilities";
import { ResizeHandleIcon } from "./icons/resize-handle-icon";

type Props = {
  placement: Placement;
  defaultWidth?: number;
  minWidth?: number;
  maxWidth?: number;
  defaultHeight?: number;
  minHeight?: number;
  maxHeight?: number;
  children: ({ resizing }: { resizing: string | null }) => React.ReactNode;
  onResize?: (width: number, height: number) => void;
  visible?: boolean;
};

export const ResizablePane = ({ placement, visible, children }: Props) => {
  const [hover, setHover] = React.useState(false);
  const [resizing, setResizing] = React.useState<
    "lx" | "rx" | "ty" | "by" | null
  >(null);
  const [resizePosition, setResizePosition] = React.useState<{
    x: number;
    y: number;
  } | null>(null);
  const [panelSize, setPanelSize] = React.useState<
    Record<"width" | "height", number>
  >(() => {
    const defaultSize = getDefaultPanelSize(placement);

    return {
      width: roundToEven(defaultSize.width),
      height: roundToEven(defaultSize.height),
    };
  });

  React.useEffect(() => {
    const handleResize = () => {
      setPanelSize((p) => {
        const defaultSize = getDefaultPanelSize(placement, p);

        return {
          width: roundToEven(defaultSize.width),
          height: roundToEven(defaultSize.height),
        };
      });
    };

    handleResize();

    window.addEventListener("resize", handleResize);

    return () => {
      window.removeEventListener("resize", handleResize);
    };
  }, [placement]);

  React.useEffect(() => {
    const handleMouseUp = () => {
      setResizing(null);
    };

    if (resizing !== null) {
      window.addEventListener("mouseup", handleMouseUp);

      return () => {
        window.removeEventListener("mouseup", handleMouseUp);
      };
    }

    return;
  }, [resizing]);

  React.useEffect(() => {
    const currentCursor = document.body.style.cursor;

    if (resizing?.includes("x")) {
      document.body.style.cursor = "col-resize";
    } else if (resizing?.includes("y")) {
      document.body.style.cursor = "row-resize";
    }

    return () => {
      document.body.style.cursor = currentCursor;
    };
  }, [resizing]);

  React.useEffect(() => {
    const handleMouseMove = (e: MouseEvent) => {
      if (resizing?.[1] === "x") {
        const diff = e.clientX - (resizePosition?.x ?? e.clientX);
        const newWidth =
          panelSize.width + (resizing === "lx" ? -diff : diff) * 2;

        setPanelSize((p) => ({
          ...p,
          width: roundToEven(
            Math.min(
              getMaxPanelWidth(placement),
              Math.max(MIN_PANEL_WIDTH, newWidth),
            ),
          ),
        }));
      } else if (resizing?.[1] === "y") {
        const diff = e.clientY - (resizePosition?.y ?? e.clientY);
        const newHeight =
          panelSize.height + (resizing === "ty" ? -diff : diff) * 1;

        setPanelSize((p) => ({
          ...p,
          height: roundToEven(
            Math.min(
              getMaxPanelHeight(placement),
              Math.max(MIN_PANEL_HEIGHT, newHeight),
            ),
          ),
        }));
      }
    };

    if (resizing !== null) {
      window.addEventListener("mousemove", handleMouseMove);

      return () => {
        window.removeEventListener("mousemove", handleMouseMove);
      };
    }

    return;
  }, [resizing, placement]);

  return (
    <div
      style={{
        position: "absolute",
        borderRadius: "8px",
        boxShadow: "0 0 10px rgba(0, 0, 0, 0.5)",
        border: "1px solid rgba(0, 0, 0, 0.5)",
        transitionProperty: "transform, opacity",
        transitionTimingFunction: "ease-in-out",
        transitionDuration: "0.2s",
        ...getPanelPosition(placement),
        opacity: visible ? 1 : 0,
        transform: `${
          getPanelPosition(placement).transform
        } ${getPanelToggleTransforms(visible ?? false)}`,
        ...panelSize,
      }}
      onMouseEnter={() => {
        setHover(true);
      }}
      onMouseLeave={() => {
        setHover(false);
      }}
    >
      {children({ resizing })}
      {/*  */}
      <React.Fragment>
        <div
          style={{
            position: "absolute",
            left: 0,
            top: "50%",
            width: "10px",
            height: "26px",
            transform: "translateY(-13px) translateX(-5px)",
            cursor: "col-resize",
            transition: "opacity ease-in-out 0.2s",
            pointerEvents: hover || resizing ? "auto" : "none",
            opacity: hover || resizing ? 1 : 0,
          }}
          onMouseDown={(event) => {
            setResizing("lx");
            setResizePosition({
              x: event.clientX,
              y: event.clientY,
            });

            event.preventDefault();
          }}
        >
          <ResizeHandleIcon />
        </div>
        <div
          style={{
            position: "absolute",
            right: 0,
            top: "50%",
            width: "10px",
            height: "26px",
            transform: "translateY(-13px) translateX(5px)",
            cursor: "col-resize",
            transition: "opacity ease-in-out 0.2s",
            pointerEvents: hover || resizing ? "auto" : "none",
            opacity: hover || resizing ? 1 : 0,
          }}
          onMouseDown={(event) => {
            setResizing("rx");
            setResizePosition({
              x: event.clientX,
              y: event.clientY,
            });

            event.preventDefault();
          }}
        >
          <ResizeHandleIcon />
        </div>
        <div
          style={{
            position: "absolute",
            left: "50%",
            top: 0,
            width: "26px",
            height: "10px",
            transform: "translateY(-5px) translateX(-13px)",
            cursor: "row-resize",
            transition: "opacity ease-in-out 0.2s",
            pointerEvents: hover || resizing ? "auto" : "none",
            opacity: hover || resizing ? 1 : 0,
          }}
          onMouseDown={(event) => {
            setResizing("ty");
            setResizePosition({
              x: event.clientX,
              y: event.clientY,
            });

            event.preventDefault();
          }}
        >
          <ResizeHandleIcon
            style={{
              transform: "rotate(90deg)",
              transformOrigin: "13px 13px",
            }}
          />
        </div>
        <div
          style={{
            position: "absolute",
            left: "50%",
            bottom: 0,
            width: "26px",
            height: "10px",
            transform: "translateY(5px) translateX(-13px)",
            cursor: "row-resize",
            transition: "opacity ease-in-out 0.2s",
            pointerEvents: hover || resizing ? "auto" : "none",
            opacity: hover || resizing ? 1 : 0,
          }}
          onMouseDown={(event) => {
            setResizing("by");
            setResizePosition({
              x: event.clientX,
              y: event.clientY,
            });

            event.preventDefault();
          }}
        >
          <ResizeHandleIcon
            style={{
              transform: "rotate(90deg)",
              transformOrigin: "13px 13px",
            }}
          />
        </div>
      </React.Fragment>
    </div>
  );
};