UNPKG

resizable-container

Version:

A resizable and collapsible container component for React applications.

334 lines (330 loc) 10.1 kB
// 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 };