Skip to content

Commit

Permalink
Add PanelContext with activeHandleId prop
Browse files Browse the repository at this point in the history
Identifies the resize handle currently being dragged (or null). This enables more customized UI/UX when resizing is in progress.
  • Loading branch information
bvaughn committed Dec 23, 2022
1 parent d9f1da1 commit 8aed257
Show file tree
Hide file tree
Showing 11 changed files with 166 additions and 66 deletions.
22 changes: 18 additions & 4 deletions packages/react-resizable-panels-website/src/HorizontalGroup.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -40,7 +41,7 @@ export default function HorizontalGroup({
id="middle"
minSize={0.25}
>
<PanelResizeHandle className={styles.HorizontalResizeHandle} />
<DragHandle id="left-handle" />
<div
className={styles.HorizontalFiller}
style={{ backgroundColor: "var(--color-horizontal)" }}
Expand Down Expand Up @@ -82,15 +83,15 @@ export default function HorizontalGroup({
It won't shrink beyond 25% of the total width.
</p>
</div>
<PanelResizeHandle className={styles.HorizontalResizeHandle} />
<DragHandle id="middle-handle" />
</Panel>
<Panel className={styles.PanelRow} defaultSize={0.3} id="stacked">
<div className={styles.Grower}>
<VerticalGroup />
</div>
</Panel>
<Panel className={styles.PanelRow} defaultSize={0.2} id="right">
<PanelResizeHandle className={styles.HorizontalResizeHandle} />
<DragHandle id="right-handle" />
<div
className={styles.HorizontalFiller}
style={{ backgroundColor: "var(--color-horizontal)" }}
Expand All @@ -111,3 +112,16 @@ export default function HorizontalGroup({
</PanelGroup>
);
}

function DragHandle({ id }: { id: string }) {
const { activeHandleId } = useContext(PanelContext);
const isDragging = activeHandleId === id;

return (
<PanelResizeHandle className={styles.HorizontalResizeHandle} id={id}>
<div
className={isDragging ? styles.ActiveResizeHandle : styles.ResizeHandle}
/>
</PanelResizeHandle>
);
}
31 changes: 26 additions & 5 deletions packages/react-resizable-panels-website/src/styles.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
2 changes: 2 additions & 0 deletions packages/react-resizable-panels/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
8 changes: 7 additions & 1 deletion packages/react-resizable-panels/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
| `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`)
2 changes: 1 addition & 1 deletion packages/react-resizable-panels/package.json
Original file line number Diff line number Diff line change
@@ -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 <[email protected]>",
"license": "MIT",
Expand Down
6 changes: 6 additions & 0 deletions packages/react-resizable-panels/src/PanelContexts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
51 changes: 23 additions & 28 deletions packages/react-resizable-panels/src/PanelGroup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -38,6 +39,7 @@ export default function PanelGroup({
}: Props) {
const groupId = useUniqueId();

const [activeHandleId, setActiveHandleId] = useState<string | null>(null);
const [panels, setPanels] = useState<Map<string, PanelData>>(new Map());

// 0-1 values representing the relative size of each panel.
Expand Down Expand Up @@ -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) {
Expand All @@ -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]);

Expand Down Expand Up @@ -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,
}),
[
Expand All @@ -238,10 +234,19 @@ export default function PanelGroup({
]
);

const panelContext = useMemo(
() => ({
activeHandleId,
}),
[activeHandleId]
);

return (
<PanelGroupContext.Provider value={context}>
<div className={className}>{children}</div>
</PanelGroupContext.Provider>
<PanelContext.Provider value={panelContext}>
<PanelGroupContext.Provider value={panelGroupContext}>
<div className={className}>{children}</div>
</PanelGroupContext.Provider>
</PanelContext.Provider>
);
}

Expand Down Expand Up @@ -310,16 +315,6 @@ function adjustByDelta(
return nextSizes;
}

function createLocalStorageKey(
autoSaveId: string,
panels: Map<string, PanelData>
): string {
const panelsArray = panelsMapToSortedArray(panels);
const panelIds = panelsArray.map((panel) => panel.id);

return `PanelGroup:sizes:${autoSaveId}${panelIds.join("|")}`;
}

function getOffset(
panels: Map<string, PanelData>,
id: string,
Expand Down
50 changes: 28 additions & 22 deletions packages/react-resizable-panels/src/PanelResizeHandle.tsx
Original file line number Diff line number Diff line change
@@ -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<string | null>(null);
const [resizeHandler, setResizeHandler] = useState<ResizeHandler | null>(
null
);
const [isDragging, setIsDragging] = useState(false);

useEffect(() => {
if (disabled) {
Expand All @@ -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 (
<div
className={className}
data-panel-group-id={groupId}
data-panel-resize-handle-id={id}
onMouseDown={() => setIsDragging(true)}
onMouseUp={() => setIsDragging(false)}
onMouseDown={() => startDragging(id)}
onMouseUp={stopDragging}
onTouchStart={() => startDragging(id)}
onTouchEnd={stopDragging}
style={{
cursor: direction === "horizontal" ? "ew-resize" : "ns-resize",
}}
Expand Down
8 changes: 4 additions & 4 deletions packages/react-resizable-panels/src/hooks/useUniqueId.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@ import { useRef } from "react";

let counter = 0;

export default function useUniqueId(): string {
const idRef = useRef<number | null>(null);
export default function useUniqueId(id: string | null = null): string {
const idRef = useRef<string | null>(id);
if (idRef.current === null) {
idRef.current = counter++;
idRef.current = "" + counter++;
}

return "" + idRef.current;
return idRef.current;
}
3 changes: 2 additions & 1 deletion packages/react-resizable-panels/src/index.ts
Original file line number Diff line number Diff line change
@@ -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 };
Loading

1 comment on commit 8aed257

@vercel
Copy link

@vercel vercel bot commented on 8aed257 Dec 23, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.