diff --git a/packages/react-resizable-panels-website/src/HorizontalGroup.tsx b/packages/react-resizable-panels-website/src/HorizontalGroup.tsx index b5a6e20e4..c7d07e507 100644 --- a/packages/react-resizable-panels-website/src/HorizontalGroup.tsx +++ b/packages/react-resizable-panels-website/src/HorizontalGroup.tsx @@ -1,4 +1,5 @@ -import { Panel, PanelResizeHandle } from "react-resizable-panels"; +import { useContext } from "react"; +import { Panel, PanelContext, PanelResizeHandle } from "react-resizable-panels"; import PanelGroup from "./AutoSizedPanelGroup"; import styles from "./styles.module.css"; @@ -40,7 +41,7 @@ export default function HorizontalGroup({ id="middle" minSize={0.25} > - +
- +
@@ -90,7 +91,7 @@ export default function HorizontalGroup({
- +
); } + +function DragHandle({ id }: { id: string }) { + const { activeHandleId } = useContext(PanelContext); + const isDragging = activeHandleId === id; + + return ( + +
+ + ); +} diff --git a/packages/react-resizable-panels-website/src/styles.module.css b/packages/react-resizable-panels-website/src/styles.module.css index dd5e9a1e0..163bca0a8 100644 --- a/packages/react-resizable-panels-website/src/styles.module.css +++ b/packages/react-resizable-panels-website/src/styles.module.css @@ -39,15 +39,36 @@ } .HorizontalResizeHandle { - padding: 0 0.25rem; + width: 0.5rem; + position: relative; +} +.ResizeHandle, +.ActiveResizeHandle { + position: absolute; + top: 0.125rem; + bottom: 0.125rem; + left: 0.125rem; + right: 0.125rem; + border-radius: 0.125rem; + background-color: transparent; + transition: background-color 0.2s linear; +} +.ActiveResizeHandle, +.HorizontalResizeHandle:hover .ResizeHandle { + background-color: rgba(255, 255, 255, 0.2); } .VerticalResizeBar { - height: 3px; width: 100%; - background-color: #2b2b2b; - border-bottom: 1px solid #4a4c50; - border-top: 1px solid #4a4c50; + position: relative; + padding: 0.25rem 0; +} +.VerticalResizeBar::after { + content: " "; + display: block; + height: 1px; + width: 100%; + background-color: #4a4c50; } .Button, diff --git a/packages/react-resizable-panels/CHANGELOG.md b/packages/react-resizable-panels/CHANGELOG.md index f74dde80a..22feb2103 100644 --- a/packages/react-resizable-panels/CHANGELOG.md +++ b/packages/react-resizable-panels/CHANGELOG.md @@ -1,5 +1,7 @@ # Changelog +## 0.0.7 +* Add `PanelContext` with `activeHandleId` property identifying the resize handle currently being dragged (or `null`). This enables more customized UI/UX when resizing is in progress. ## 0.0.6 * [#5](https://github.com/bvaughn/react-resizable-panels/issues/5): Removed `panelBefore` and `panelAfter` props from `PanelResizeHandle`. `PanelGroup` now infers this based on position within the group. ## 0.0.5 diff --git a/packages/react-resizable-panels/README.md b/packages/react-resizable-panels/README.md index 916bfd295..a7cee5868 100644 --- a/packages/react-resizable-panels/README.md +++ b/packages/react-resizable-panels/README.md @@ -46,4 +46,10 @@ import { Panel, PanelGroup, PanelResizeHandle } from "react-resizable-panels"; | :------------ | :----------- | :--- | `children` | `?ReactNode` | Custom drag UI; can be any arbitrary React element(s) | `className` | `?string` | Class name -| `disabled` | `?boolean` | Disable drag handle \ No newline at end of file +| `disabled` | `?boolean` | Disable drag handle +| `id` | `?string` | Optional resize handle id (must be unique within the current group) + +### `PanelContext` +| prop | type | description +| :----------- | :------------------- | :--- +| `activeHandleId` | `string \| null` | Resize handle currently being dragged (or `null`) \ No newline at end of file diff --git a/packages/react-resizable-panels/package.json b/packages/react-resizable-panels/package.json index a7dab5e14..172ef05e6 100644 --- a/packages/react-resizable-panels/package.json +++ b/packages/react-resizable-panels/package.json @@ -1,6 +1,6 @@ { "name": "react-resizable-panels", - "version": "0.0.6", + "version": "0.0.7", "description": "React components for resizable panel groups/layouts", "author": "Brian Vaughn ", "license": "MIT", diff --git a/packages/react-resizable-panels/src/PanelContexts.ts b/packages/react-resizable-panels/src/PanelContexts.ts index b949e5dd9..93a767267 100644 --- a/packages/react-resizable-panels/src/PanelContexts.ts +++ b/packages/react-resizable-panels/src/PanelContexts.ts @@ -2,11 +2,17 @@ import { CSSProperties, createContext } from "react"; import { PanelData, ResizeHandler } from "./types"; +export const PanelContext = createContext<{ + activeHandleId: string | null; +} | null>(null); + export const PanelGroupContext = createContext<{ direction: "horizontal" | "vertical"; getPanelStyle: (id: string) => CSSProperties; groupId: string; registerPanel: (id: string, panel: PanelData) => void; registerResizeHandle: (id: string) => ResizeHandler; + startDragging: (id: string) => void; + stopDragging: () => void; unregisterPanel: (id: string) => void; } | null>(null); diff --git a/packages/react-resizable-panels/src/PanelGroup.tsx b/packages/react-resizable-panels/src/PanelGroup.tsx index 8147f283b..7a681743e 100644 --- a/packages/react-resizable-panels/src/PanelGroup.tsx +++ b/packages/react-resizable-panels/src/PanelGroup.tsx @@ -10,8 +10,9 @@ import { } from "react"; import useUniqueId from "./hooks/useUniqueId"; -import { PanelGroupContext } from "./PanelContexts"; +import { PanelContext, PanelGroupContext } from "./PanelContexts"; import { Direction, PanelData } from "./types"; +import { loadPanelLayout, savePanelGroupLayout } from "./utils/serialization"; type Props = { autoSaveId?: string; @@ -38,6 +39,7 @@ export default function PanelGroup({ }: Props) { const groupId = useUniqueId(); + const [activeHandleId, setActiveHandleId] = useState(null); const [panels, setPanels] = useState>(new Map()); // 0-1 values representing the relative size of each panel. @@ -81,14 +83,8 @@ export default function PanelGroup({ // default size should be restored from local storage if possible. let defaultSizes: number[] | undefined = undefined; if (autoSaveId) { - try { - const value = localStorage.getItem( - createLocalStorageKey(autoSaveId, panels) - ); - if (value) { - defaultSizes = JSON.parse(value); - } - } catch (error) {} + const panelIds = panelsMapToSortedArray(panels).map((panel) => panel.id); + defaultSizes = loadPanelLayout(autoSaveId, panelIds); } if (defaultSizes != null) { @@ -104,16 +100,14 @@ export default function PanelGroup({ }, [autoSaveId, panels]); useEffect(() => { + // If this panel has been configured to persist sizing information, save sizes to local storage. if (autoSaveId) { if (sizes.length === 0 || sizes.length !== panels.size) { return; } - // If this panel has been configured to persist sizing information, save sizes to local storage. - localStorage.setItem( - createLocalStorageKey(autoSaveId, panels), - JSON.stringify(sizes) - ); + const panelIds = panelsMapToSortedArray(panels).map((panel) => panel.id); + savePanelGroupLayout(autoSaveId, panelIds, sizes); } }, [autoSaveId, panels, sizes]); @@ -219,13 +213,15 @@ export default function PanelGroup({ }); }, []); - const context = useMemo( + const panelGroupContext = useMemo( () => ({ direction, getPanelStyle, groupId, registerPanel, registerResizeHandle, + startDragging: (id: string) => setActiveHandleId(id), + stopDragging: () => setActiveHandleId(null), unregisterPanel, }), [ @@ -238,10 +234,19 @@ export default function PanelGroup({ ] ); + const panelContext = useMemo( + () => ({ + activeHandleId, + }), + [activeHandleId] + ); + return ( - -
{children}
-
+ + +
{children}
+
+
); } @@ -310,16 +315,6 @@ function adjustByDelta( return nextSizes; } -function createLocalStorageKey( - autoSaveId: string, - panels: Map -): string { - const panelsArray = panelsMapToSortedArray(panels); - const panelIds = panelsArray.map((panel) => panel.id); - - return `PanelGroup:sizes:${autoSaveId}${panelIds.join("|")}`; -} - function getOffset( panels: Map, id: string, diff --git a/packages/react-resizable-panels/src/PanelResizeHandle.tsx b/packages/react-resizable-panels/src/PanelResizeHandle.tsx index 7282aa0aa..fa9490ffe 100644 --- a/packages/react-resizable-panels/src/PanelResizeHandle.tsx +++ b/packages/react-resizable-panels/src/PanelResizeHandle.tsx @@ -1,34 +1,44 @@ import { ReactNode, useContext, useEffect, useState } from "react"; import useUniqueId from "./hooks/useUniqueId"; -import { PanelGroupContext } from "./PanelContexts"; +import { PanelContext, PanelGroupContext } from "./PanelContexts"; import { ResizeHandler } from "./types"; export default function PanelResizeHandle({ children = null, className = "", disabled = false, + id: idProp = null, }: { children?: ReactNode; className?: string; disabled?: boolean; + id?: string | null; }) { - const context = useContext(PanelGroupContext); - if (context === null) { + const panelContext = useContext(PanelContext); + const panelGroupContext = useContext(PanelGroupContext); + if (panelContext === null || panelGroupContext === null) { throw Error( `PanelResizeHandle components must be rendered within a PanelGroup container` ); } - const id = useUniqueId(); + const id = useUniqueId(idProp); - const { direction, groupId, registerResizeHandle } = context; + const { activeHandleId } = panelContext; + const { + direction, + groupId, + registerResizeHandle, + startDragging, + stopDragging, + } = panelGroupContext; + + const isDragging = activeHandleId === id; - const setGroupId = useState(null); const [resizeHandler, setResizeHandler] = useState( null ); - const [isDragging, setIsDragging] = useState(false); useEffect(() => { if (disabled) { @@ -47,38 +57,34 @@ export default function PanelResizeHandle({ document.body.style.cursor = direction === "horizontal" ? "ew-resize" : "ns-resize"; - const onMouseLeave = (_: MouseEvent) => { - setIsDragging(false); - }; - const onMouseMove = (event: MouseEvent) => { resizeHandler(event); }; - const onMouseUp = (_: MouseEvent) => { - setIsDragging(false); - }; - - document.body.addEventListener("mouseleave", onMouseLeave); + document.body.addEventListener("mouseleave", stopDragging); document.body.addEventListener("mousemove", onMouseMove); - document.body.addEventListener("mouseup", onMouseUp); + document.body.addEventListener("touchmove", onMouseMove); + document.body.addEventListener("mouseup", stopDragging); return () => { document.body.style.cursor = ""; - document.body.removeEventListener("mouseleave", onMouseLeave); + document.body.removeEventListener("mouseleave", stopDragging); document.body.removeEventListener("mousemove", onMouseMove); - document.body.removeEventListener("mouseup", onMouseUp); + document.body.removeEventListener("touchmove", onMouseMove); + document.body.removeEventListener("mouseup", stopDragging); }; - }, [direction, disabled, isDragging, resizeHandler]); + }, [direction, disabled, isDragging, resizeHandler, stopDragging]); return (
setIsDragging(true)} - onMouseUp={() => setIsDragging(false)} + onMouseDown={() => startDragging(id)} + onMouseUp={stopDragging} + onTouchStart={() => startDragging(id)} + onTouchEnd={stopDragging} style={{ cursor: direction === "horizontal" ? "ew-resize" : "ns-resize", }} diff --git a/packages/react-resizable-panels/src/hooks/useUniqueId.ts b/packages/react-resizable-panels/src/hooks/useUniqueId.ts index dcf9cb795..23c58ac16 100644 --- a/packages/react-resizable-panels/src/hooks/useUniqueId.ts +++ b/packages/react-resizable-panels/src/hooks/useUniqueId.ts @@ -2,11 +2,11 @@ import { useRef } from "react"; let counter = 0; -export default function useUniqueId(): string { - const idRef = useRef(null); +export default function useUniqueId(id: string | null = null): string { + const idRef = useRef(id); if (idRef.current === null) { - idRef.current = counter++; + idRef.current = "" + counter++; } - return "" + idRef.current; + return idRef.current; } diff --git a/packages/react-resizable-panels/src/index.ts b/packages/react-resizable-panels/src/index.ts index a63327e04..ad2ab6159 100644 --- a/packages/react-resizable-panels/src/index.ts +++ b/packages/react-resizable-panels/src/index.ts @@ -1,5 +1,6 @@ import Panel from "./Panel"; +import { PanelContext } from "./PanelContexts"; import PanelGroup from "./PanelGroup"; import PanelResizeHandle from "./PanelResizeHandle"; -export { Panel, PanelGroup, PanelResizeHandle }; +export { Panel, PanelContext, PanelGroup, PanelResizeHandle }; diff --git a/packages/react-resizable-panels/src/utils/serialization.ts b/packages/react-resizable-panels/src/utils/serialization.ts new file mode 100644 index 000000000..18834d648 --- /dev/null +++ b/packages/react-resizable-panels/src/utils/serialization.ts @@ -0,0 +1,49 @@ +import { PanelData } from "../types"; + +type SerializedPanelGroupState = { [panelIds: string]: number[] }; + +function loadSerializedPanelGroupState( + autoSaveId: string +): SerializedPanelGroupState | null { + try { + const serialized = localStorage.getItem(`PanelGroup:sizes:${autoSaveId}`); + if (serialized) { + const parsed = JSON.parse(serialized); + if (typeof parsed === "object" && parsed != null) { + return parsed; + } + } + } catch (error) {} + + return null; +} + +export function loadPanelLayout( + autoSaveId: string, + panelIds: string[] +): number[] | null { + const state = loadSerializedPanelGroupState(autoSaveId); + if (state) { + return state[panelIds.join(",")] ?? null; + } + + return null; +} + +export function savePanelGroupLayout( + autoSaveId: string, + panelIds: string[], + sizes: number[] +): void { + const state = loadSerializedPanelGroupState(autoSaveId) || {}; + state[panelIds.join(",")] = sizes; + + try { + localStorage.setItem( + `PanelGroup:sizes:${autoSaveId}`, + JSON.stringify(state) + ); + } catch (error) { + console.error(error); + } +}