resizable-container
Version:
A resizable and collapsible container component for React applications.
334 lines (330 loc) • 10.1 kB
JavaScript
// src/components/ResizableContainer.tsx
import { useCallback as useCallback2 } from "react";
import classNames from "classnames";
import styles from "./assets/ResizableContainer.module-G42OESSY.module.css";
// src/hooks/useResize.ts
import { useState, useEffect, useCallback, useRef } from "react";
var useResize = ({
direction = "right",
initialSize,
minSize,
maxSize,
boundSize,
onResize,
storageKey,
toggleKey
}) => {
const containerRef = useRef(null);
const defaultInitialSize = initialSize !== void 0 ? parseInt(initialSize, 10) : 200;
const parseSize = (value, defaultVal = 0) => {
if (typeof value === "number") return value;
if (typeof value === "string") {
const parsed = parseInt(value, 10);
return isNaN(parsed) ? defaultVal : parsed;
}
return defaultVal;
};
const minSizeNum = parseSize(minSize, 0);
const maxSizeNum = maxSize !== void 0 ? parseSize(maxSize) : void 0;
const boundSizeNum = parseSize(boundSize, 0);
const getStoredSize = () => {
if (typeof window === "undefined") return defaultInitialSize;
try {
const storedData = localStorage.getItem(storageKey);
if (storedData) {
const parsed = JSON.parse(storedData);
return parsed.currentSize || defaultInitialSize;
}
} catch (err) {
console.error("Error reading stored size:", err);
}
return defaultInitialSize;
};
const [size, setSize] = useState(() => getStoredSize());
const [isAnimating, setIsAnimating] = useState(false);
const startPosRef = useRef(null);
const lastExpandedSizeRef = useRef(getStoredSize());
const frameRef = useRef(null);
const latestEventRef = useRef(null);
const isHorizontal = direction === "right" || direction === "left";
const updateStorage = useCallback((currentSize, previousSize) => {
try {
localStorage.setItem(
storageKey,
JSON.stringify({ currentSize, previousSize })
);
} catch (err) {
console.error("Error saving size to localStorage:", err);
}
}, [storageKey]);
const startResizing = useCallback((clientX, clientY) => {
startPosRef.current = { x: clientX, y: clientY };
}, []);
const shouldIgnoreResize = (e) => {
let target = e.target;
while (target) {
if (target.getAttribute("data-ignore-resize") === "true") {
return true;
}
target = target.parentElement;
}
return false;
};
const handleMouseDown = useCallback(
(e) => {
if (shouldIgnoreResize(e.nativeEvent)) return;
e.preventDefault();
startResizing(e.clientX, e.clientY);
},
[startResizing]
);
const handlePointerDown = useCallback(
(e) => {
if (shouldIgnoreResize(e.nativeEvent)) return;
e.preventDefault();
if (e.currentTarget && e.currentTarget.setPointerCapture) {
e.currentTarget.setPointerCapture(e.pointerId);
}
startResizing(e.clientX, e.clientY);
},
[startResizing]
);
const processMove = useCallback(
(clientX, clientY) => {
if (!startPosRef.current) return;
const offsetX = clientX - startPosRef.current.x;
const offsetY = clientY - startPosRef.current.y;
setSize((prevSize) => {
let newSize = prevSize;
if (isHorizontal) {
newSize = prevSize + (direction === "right" ? offsetX : -offsetX);
} else {
newSize = prevSize + (direction === "top" ? offsetY : -offsetY);
}
newSize = Math.max(newSize, minSizeNum);
if (maxSizeNum !== void 0) {
newSize = Math.min(newSize, maxSizeNum);
}
if (newSize > minSizeNum) {
lastExpandedSizeRef.current = newSize;
}
if (onResize) onResize(newSize);
return newSize;
});
startPosRef.current = { x: clientX, y: clientY };
},
[isHorizontal, direction, minSizeNum, maxSizeNum, onResize]
);
const handleMove = useCallback(
(e) => {
latestEventRef.current = e;
if (frameRef.current === null) {
frameRef.current = requestAnimationFrame(() => {
if (latestEventRef.current) {
processMove(
latestEventRef.current.clientX,
latestEventRef.current.clientY
);
}
frameRef.current = null;
});
}
},
[processMove]
);
const finishResizing = useCallback(() => {
if (startPosRef.current) {
setSize((currentSize) => {
const finalSize = currentSize < boundSizeNum ? minSizeNum : currentSize;
if (finalSize > minSizeNum) {
lastExpandedSizeRef.current = finalSize;
}
updateStorage(finalSize, lastExpandedSizeRef.current);
if (currentSize < boundSizeNum) {
setIsAnimating(true);
}
return finalSize;
});
startPosRef.current = null;
}
}, [boundSizeNum, minSizeNum, updateStorage]);
const handleMouseUp = useCallback(() => {
finishResizing();
}, [finishResizing]);
const handlePointerUp = useCallback(() => {
finishResizing();
}, [finishResizing]);
const toggleCollapse = useCallback(() => {
setSize((currentSize) => {
const newSize = currentSize === minSizeNum ? lastExpandedSizeRef.current : minSizeNum;
console.log(currentSize, newSize);
setIsAnimating(true);
updateStorage(newSize, lastExpandedSizeRef.current);
return newSize;
});
}, [minSizeNum, updateStorage]);
const handleToggleKeyDown = useCallback(
(e) => {
if (e.ctrlKey && e.key === toggleKey) {
e.preventDefault();
toggleCollapse();
}
},
[toggleCollapse, toggleKey]
);
useEffect(() => {
document.addEventListener("mousemove", handleMove);
document.addEventListener("mouseup", handleMouseUp);
document.addEventListener("pointermove", handleMove);
document.addEventListener("pointerup", handlePointerUp);
document.addEventListener("keydown", handleToggleKeyDown);
return () => {
document.removeEventListener("mousemove", handleMove);
document.removeEventListener("mouseup", handleMouseUp);
document.removeEventListener("pointermove", handleMove);
document.removeEventListener("pointerup", handlePointerUp);
document.removeEventListener("keydown", handleToggleKeyDown);
if (frameRef.current) {
cancelAnimationFrame(frameRef.current);
}
};
}, [handleMove, handleMouseUp, handlePointerUp, handleToggleKeyDown]);
const handleTransitionEnd = useCallback(() => {
setIsAnimating(false);
}, []);
return {
containerRef,
size,
isAnimating,
handleMouseDown,
handlePointerDown,
toggleCollapse,
isHorizontal,
handleMouseUp,
isResizing: !!startPosRef.current,
handleTransitionEnd
};
};
var useResize_default = useResize;
// src/components/ResizableContainer.tsx
import { jsx, jsxs } from "react/jsx-runtime";
var ResizableContainer = ({
children,
direction = "right",
initialSize,
maxSize,
minSize,
boundSize,
onResize,
toggleKey = "[",
animationDuration = 300,
storageKey,
ariaLabel,
containerClassName,
sliderClassName,
toggleButtonClassName,
toggleButtonIcon
}) => {
const {
containerRef,
size,
isAnimating,
handleMouseDown,
handlePointerDown,
toggleCollapse,
isHorizontal,
isResizing,
handleTransitionEnd
} = useResize_default({
direction,
initialSize,
minSize,
maxSize,
boundSize,
onResize,
animationDuration,
storageKey,
toggleKey
});
const handleSliderKeyDown = useCallback2(
(e) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
toggleCollapse();
}
},
[toggleCollapse]
);
return /* @__PURE__ */ jsxs(
"div",
{
ref: containerRef,
className: classNames(styles.container, containerClassName, {
[styles.animating]: isAnimating
}),
style: {
[isHorizontal ? "width" : "height"]: `${size}px`,
transition: isAnimating ? `${isHorizontal ? "width" : "height"} ${animationDuration}ms ease-in-out` : "none"
},
onTransitionEnd: handleTransitionEnd,
role: "region",
"aria-label": ariaLabel,
children: [
children,
/* @__PURE__ */ jsxs(
"div",
{
className: classNames(
styles.slider,
styles[direction],
sliderClassName
),
onMouseDown: handleMouseDown,
onPointerDown: handlePointerDown,
onKeyDown: handleSliderKeyDown,
role: "separator",
"aria-valuenow": Number(size),
"aria-valuemin": Number(minSize),
"aria-valuemax": Number(maxSize),
"aria-orientation": isHorizontal ? "horizontal" : "vertical",
tabIndex: 0,
children: [
/* @__PURE__ */ jsx("div", { className: styles.resizer, "aria-hidden": "true" }),
/* @__PURE__ */ jsx(
"button",
{
className: classNames(styles.toggleButton, toggleButtonClassName),
onClick: (e) => {
console.log("click");
e.stopPropagation();
toggleCollapse();
},
"data-ignore-resize": "true",
"aria-label": `Toggle ${direction} panel`,
"aria-expanded": size !== Number(minSize),
children: toggleButtonIcon
}
),
/* @__PURE__ */ jsx(
"div",
{
className: classNames(
styles.shadow,
size <= Number(boundSize || 0) && isResizing && styles.infoShadow
),
"aria-hidden": "true"
}
)
]
}
)
]
}
);
};
// src/index.ts
var index_default = ResizableContainer;
export {
ResizableContainer,
index_default as default
};