;
+ previews: ResizePreview[];
state: "active";
};
diff --git a/lib/global/utils/calculateResizePreviews.test.ts b/lib/global/utils/calculateResizePreviews.test.ts
new file mode 100644
index 000000000..74ea2de9f
--- /dev/null
+++ b/lib/global/utils/calculateResizePreviews.test.ts
@@ -0,0 +1,62 @@
+import { describe, expect, test } from "vitest";
+import { calculateHitRegions } from "../dom/calculateHitRegions";
+import { mockGroup } from "../test/mockGroup";
+import { calculateResizePreviews } from "./calculateResizePreviews";
+
+describe("calculateResizePreviews", () => {
+ for (const orientation of ["horizontal", "vertical"] as const) {
+ for (const explicit of [false, true]) {
+ for (const activeIndex of [0, 1]) {
+ test(`${orientation}, explicit=${explicit}, active edge=${activeIndex}`, () => {
+ const horizontal = orientation === "horizontal";
+ const rect = (start: number, size: number) =>
+ horizontal
+ ? new DOMRect(start, 0, size, 100)
+ : new DOMRect(0, start, 100, size);
+
+ const group = mockGroup(rect(0, 300), { orientation });
+ group.addPanel(rect(0, 100));
+ if (explicit) {
+ group.addSeparator(rect(100, 10));
+ }
+ group.addHTMLElement(rect(explicit ? 110 : 100, explicit ? 90 : 100));
+ group.addPanel(rect(200, 100));
+
+ const hitRegions = calculateHitRegions({ group });
+ const previews = calculateResizePreviews(group, [
+ hitRegions[activeIndex]
+ ]);
+
+ expect(previews).toHaveLength(2);
+ expect(previews.map(({ active }) => active)).toEqual([
+ activeIndex === 0,
+ activeIndex === 1
+ ]);
+ expect(
+ previews.map(({ rect }) => (horizontal ? rect.left : rect.top))
+ ).toEqual([100, 200]);
+ expect(
+ previews.map(({ rect }) => (horizontal ? rect.width : rect.height))
+ ).toEqual([explicit ? 10 : 0, 0]);
+ expect(new Set(previews.map(({ key }) => key)).size).toBe(2);
+ expect(previews[0].separator).toBe(group.separators[0]);
+ expect(previews[1].separator).toBeUndefined();
+ });
+ }
+ }
+ }
+
+ test("includes disabled separators that can move indirectly", () => {
+ const group = mockGroup(new DOMRect(0, 0, 210, 100));
+ group.addPanel(new DOMRect(0, 0, 100, 100));
+ group.addSeparator(new DOMRect(100, 0, 10, 100), "separator", true);
+ group.addPanel(new DOMRect(110, 0, 100, 100));
+
+ expect(calculateHitRegions({ group })).toHaveLength(0);
+
+ const previews = calculateResizePreviews(group, []);
+ expect(previews).toHaveLength(1);
+ expect(previews[0].active).toBe(false);
+ expect(previews[0].separator).toBe(group.separators[0]);
+ });
+});
diff --git a/lib/global/utils/calculateResizePreviews.ts b/lib/global/utils/calculateResizePreviews.ts
new file mode 100644
index 000000000..4244b1eda
--- /dev/null
+++ b/lib/global/utils/calculateResizePreviews.ts
@@ -0,0 +1,73 @@
+import type { RegisteredGroup } from "../../components/group/types";
+import {
+ calculateHitRegions,
+ type HitRegion
+} from "../dom/calculateHitRegions";
+import type { ResizePreview } from "../mutable-state/types";
+import { layoutNumbersEqual } from "./layoutNumbersEqual";
+
+export function calculateResizePreviews(
+ group: RegisteredGroup,
+ hitRegions: HitRegion[]
+): ResizePreview[] {
+ const { element, orientation, panels } = group;
+
+ const groupRect = element.getBoundingClientRect();
+ const horizontal = orientation === "horizontal";
+
+ // Use the same boundaries as hit testing, including separate edges around
+ // static content. Disabled boundaries can still move indirectly during a drag.
+ const boundaries = calculateHitRegions({
+ expandHitTargets: false,
+ group,
+ includeDisabled: true
+ });
+
+ return boundaries.map(
+ ({ panels: boundaryPanels, rect, separator }, index) => {
+ const panelIndex = panels.indexOf(boundaryPanels[0]);
+ const center = horizontal
+ ? rect.left + rect.width / 2
+ : rect.top + rect.height / 2;
+
+ const active = hitRegions.some((region) => {
+ if (region.group !== group || region.panels[0] !== boundaryPanels[0]) {
+ return false;
+ }
+
+ if (separator || region.separator) {
+ return region.separator === separator;
+ }
+
+ const regionCenter = horizontal
+ ? region.rect.left + region.rect.width / 2
+ : region.rect.top + region.rect.height / 2;
+
+ return layoutNumbersEqual(center, regionCenter);
+ });
+
+ return {
+ active,
+ group,
+ key: separator
+ ? `separator-${separator.id}`
+ : `panel-${boundaryPanels[0].id}-${index}`,
+ offset: 0,
+ panelIndex,
+ rect: new DOMRect(
+ (horizontal && !separator ? center : rect.left) -
+ groupRect.left -
+ element.clientLeft +
+ element.scrollLeft,
+ (!horizontal && !separator ? center : rect.top) -
+ groupRect.top -
+ element.clientTop +
+ element.scrollTop,
+ horizontal && !separator ? 0 : rect.width,
+ !horizontal && !separator ? 0 : rect.height
+ ),
+ separator
+ };
+ }
+ );
+}
diff --git a/lib/global/utils/findMatchingHitRegions.ts b/lib/global/utils/findMatchingHitRegions.ts
index ed06e9825..44794e667 100644
--- a/lib/global/utils/findMatchingHitRegions.ts
+++ b/lib/global/utils/findMatchingHitRegions.ts
@@ -21,7 +21,7 @@ export function findMatchingHitRegions(
return;
}
- const hitRegions = calculateHitRegions(groupData);
+ const hitRegions = calculateHitRegions({ group: groupData });
const match = findClosestHitRegion(groupData.orientation, hitRegions, {
x: event.clientX,
y: event.clientY
diff --git a/lib/global/utils/updateActiveHitRegion.test.ts b/lib/global/utils/updateActiveHitRegion.test.ts
new file mode 100644
index 000000000..1ea6aafa7
--- /dev/null
+++ b/lib/global/utils/updateActiveHitRegion.test.ts
@@ -0,0 +1,204 @@
+import { afterEach, describe, expect, test } from "vitest";
+import {
+ CURSOR_FLAG_HORIZONTAL_MAX,
+ CURSOR_FLAG_HORIZONTAL_MIN,
+ CURSOR_FLAG_VERTICAL_MAX,
+ CURSOR_FLAG_VERTICAL_MIN
+} from "../../constants";
+import { calculateHitRegions } from "../dom/calculateHitRegions";
+import { onDocumentPointerMove } from "../event-handlers/onDocumentPointerMove";
+import { mountGroup } from "../mountGroup";
+import {
+ getMountedGroups,
+ getMountedGroupState
+} from "../mutable-state/groups";
+import {
+ getInteractionState,
+ updateInteractionState
+} from "../mutable-state/interactions";
+import { mockGroup } from "../test/mockGroup";
+import { calculateResizePreviews } from "./calculateResizePreviews";
+import { updateActiveHitRegions } from "./updateActiveHitRegion";
+
+describe("updateActiveHitRegions preview bounds", () => {
+ let unmount: (() => void) | undefined;
+
+ afterEach(() => {
+ unmount?.();
+
+ updateInteractionState({
+ state: "inactive",
+ cursorFlags: 0
+ });
+ });
+
+ test("missed pointer-up commits the last preview rather than the later hover position", () => {
+ const group = mockGroup(new DOMRect(0, 0, 200, 100), {
+ resizePreviewMode: "separator"
+ });
+ group.addPanel(new DOMRect(0, 0, 100, 100));
+ group.addPanel(new DOMRect(100, 0, 100, 100));
+ unmount = mountGroup(group);
+
+ const hitRegions = calculateHitRegions({ group });
+ const initialLayoutMap = new Map([
+ [group, getMountedGroupState(group.id, true).layout]
+ ]);
+ const pointerDownAtPoint = { x: 100, y: 50 };
+
+ updateInteractionState({
+ cursorFlags: 0,
+ hitRegions,
+ initialLayoutMap,
+ pointerDownAtPoint,
+ previewLayoutMap: new Map(initialLayoutMap),
+ previews: calculateResizePreviews(group, hitRegions),
+ state: "active"
+ });
+
+ updateActiveHitRegions({
+ commit: false,
+ document,
+ event: { clientX: 140, clientY: 50, movementX: 40, movementY: 0 },
+ hitRegions,
+ initialLayoutMap,
+ mountedGroups: getMountedGroups(),
+ pointerDownAtPoint,
+ prevCursorFlags: 0
+ });
+ expect(
+ getMountedGroupState(group.id, true).layout[group.panels[0].id]
+ ).toBe(50);
+
+ onDocumentPointerMove({
+ buttons: 0,
+ clientX: 100,
+ clientY: 50,
+ currentTarget: document,
+ defaultPrevented: false,
+ movementX: -40,
+ movementY: 0
+ } as unknown as PointerEvent);
+
+ expect(getInteractionState().state).toBe("inactive");
+ expect(
+ getMountedGroupState(group.id, true).layout[group.panels[0].id]
+ ).toBe(70);
+ });
+
+ for (const orientation of ["horizontal", "vertical"] as const) {
+ for (const direction of [-1, 1]) {
+ for (const disableCursor of [false, true]) {
+ test(`${orientation}, direction ${direction}, disableCursor=${disableCursor}`, () => {
+ const horizontal = orientation === "horizontal";
+ const rect = (start: number, size: number) =>
+ horizontal
+ ? new DOMRect(start, 0, size, 100)
+ : new DOMRect(0, start, 100, size);
+
+ const group = mockGroup(rect(0, 200), {
+ orientation,
+ resizePreviewMode: "separator"
+ });
+ group.mutableState.disableCursor = disableCursor;
+ group.addPanel(rect(0, 100), "a", {
+ minSize: "25%"
+ });
+ group.addPanel(rect(100, 100), "b", {
+ minSize: "25%"
+ });
+
+ unmount = mountGroup(group);
+
+ const initialLayout = getMountedGroupState(group.id, true).layout;
+ const hitRegions = calculateHitRegions({ group });
+ const initialLayoutMap = new Map([[group, initialLayout]]);
+ const pointerDownAtPoint = {
+ x: 100,
+ y: 100
+ };
+
+ updateInteractionState({
+ state: "active",
+ cursorFlags: 0,
+ hitRegions,
+ initialLayoutMap,
+ pointerDownAtPoint,
+ previewLayoutMap: new Map(initialLayoutMap),
+ previews: calculateResizePreviews(group, hitRegions)
+ });
+
+ let previousDelta = 0;
+ const move = (delta: number, commit = false) => {
+ const movement = delta - previousDelta;
+ previousDelta = delta;
+
+ updateActiveHitRegions({
+ commit,
+ document,
+ hitRegions,
+ initialLayoutMap,
+ mountedGroups: getMountedGroups(),
+ pointerDownAtPoint,
+ prevCursorFlags: getInteractionState().cursorFlags,
+ event: {
+ clientX: 100 + (horizontal ? delta : 0),
+ clientY: 100 + (horizontal ? 0 : delta),
+ movementX: horizontal ? movement : 0,
+ movementY: horizontal ? 0 : movement
+ }
+ });
+ };
+
+ const expectedFlag = disableCursor
+ ? 0
+ : horizontal
+ ? direction < 0
+ ? CURSOR_FLAG_HORIZONTAL_MIN
+ : CURSOR_FLAG_HORIZONTAL_MAX
+ : direction < 0
+ ? CURSOR_FLAG_VERTICAL_MIN
+ : CURSOR_FLAG_VERTICAL_MAX;
+
+ move(direction * 60);
+ const previousState = getInteractionState();
+
+ move(direction * 70);
+ expect(getInteractionState().cursorFlags).toBe(expectedFlag);
+ expect(getMountedGroupState(group.id, true).layout).toEqual(
+ initialLayout
+ );
+
+ const state = getInteractionState();
+ if (state.state !== "active") {
+ throw Error("Expected active interaction");
+ }
+
+ expect(state.previews[0].offset).toBe(direction * 50);
+ if (previousState.state !== "active") {
+ throw Error("Expected active interaction");
+ }
+ expect(state.previews).toBe(previousState.previews);
+ expect(state.previews[0]).toBe(previousState.previews[0]);
+
+ // A rounded pointer event must preserve the bounds cursor.
+ move(direction * 70);
+ expect(getInteractionState().cursorFlags).toBe(expectedFlag);
+
+ // Returning to the allowed range clears the bounds cursor.
+ move(direction * 20);
+ expect(getInteractionState().cursorFlags).toBe(0);
+
+ move(direction * 60);
+ move(direction * 70);
+
+ // Committing the same preview still updates the mounted layout.
+ move(direction * 70, true);
+ expect(
+ getMountedGroupState(group.id, true).layout[group.panels[0].id]
+ ).toBe(50 + direction * 25);
+ });
+ }
+ }
+ }
+});
diff --git a/lib/global/utils/updateActiveHitRegion.ts b/lib/global/utils/updateActiveHitRegion.ts
index 4ccfef4ec..69df15d87 100644
--- a/lib/global/utils/updateActiveHitRegion.ts
+++ b/lib/global/utils/updateActiveHitRegion.ts
@@ -47,8 +47,10 @@ export function updateActiveHitRegions({
}) {
let nextCursorFlags = 0;
const interaction = getInteractionState();
- let preview =
- interaction.state === "active" ? interaction.preview : undefined;
+ let previews = interaction.state === "active" ? interaction.previews : [];
+ const previewLayoutMap = new Map(
+ interaction.state === "active" ? interaction.previewLayoutMap : undefined
+ );
// Note that HitRegions are frozen once a drag has started
// Modify the Group layouts for all matching HitRegions though
@@ -87,10 +89,14 @@ export function updateActiveHitRegions({
defaultLayoutDeferred,
derivedPanelConstraints,
groupSize: mountedGroupSize,
- layout: prevLayout,
+ layout: mountedLayout,
separatorToPanels
} = groupState;
- if (derivedPanelConstraints && prevLayout && separatorToPanels) {
+ if (derivedPanelConstraints && mountedLayout && separatorToPanels) {
+ const prevLayout =
+ group.resizePreviewMode === "separator"
+ ? (previewLayoutMap.get(group) ?? mountedLayout)
+ : mountedLayout;
const nextLayout = adjustLayoutByDelta({
delta: deltaAsPercentage,
initialLayout,
@@ -100,21 +106,33 @@ export function updateActiveHitRegions({
trigger: "mouse-or-touch"
});
- // The preview implementation hinges on this block: consume resizePreviewMode and use commit to defer the Group layout update until the pointer is released.
- if (group.resizePreviewMode === "separator" && !commit) {
- const pivotIndex = panels.indexOf(current.panels[0]);
- const offset =
- panels.slice(0, pivotIndex + 1).reduce((total, panel) => {
- return total + nextLayout[panel.id] - initialLayout[panel.id];
- }, 0) *
- (groupSize / 100);
+ // Preview every moved boundary, deferring the layout update until release.
+ if (
+ group.resizePreviewMode === "separator" &&
+ !commit &&
+ !layoutsEqual(nextLayout, prevLayout)
+ ) {
+ previewLayoutMap.set(group, nextLayout);
- if (preview?.hitRegion === current) {
- preview = { ...preview, offset };
- }
- } else if (layoutsEqual(nextLayout, prevLayout)) {
+ let total = 0;
+ const offsets = panels.map((panel) => {
+ total += nextLayout[panel.id] - initialLayout[panel.id];
+ return total * (groupSize / 100);
+ });
+
+ previews = previews.map((preview) => {
+ if (preview.group !== group) {
+ return preview;
+ }
+
+ const offset = offsets[preview.panelIndex];
+ return offset === preview.offset ? preview : { ...preview, offset };
+ });
+ }
+
+ if (layoutsEqual(nextLayout, prevLayout)) {
if (deltaAsPercentage !== 0 && !disableCursor) {
- // An unchanged means the cursor has exceeded the allowed bounds
+ // An unchanged layout means the cursor has exceeded the allowed bounds
switch (orientation) {
case "horizontal": {
nextCursorFlags |=
@@ -132,7 +150,12 @@ export function updateActiveHitRegions({
}
}
}
- } else {
+ }
+
+ if (
+ (group.resizePreviewMode !== "separator" || commit) &&
+ !layoutsEqual(nextLayout, mountedLayout)
+ ) {
updateMountedGroup(current.group, {
defaultLayoutDeferred,
derivedPanelConstraints: derivedPanelConstraints,
@@ -159,6 +182,6 @@ export function updateActiveHitRegions({
cursorFlags |= nextCursorFlags & CURSOR_FLAGS_VERTICAL;
}
- updateCursorFlags(cursorFlags, preview);
+ updateCursorFlags(cursorFlags, previews, previewLayoutMap);
updateCursorStyle(document);
}
diff --git a/lib/index.ts b/lib/index.ts
index 584e4bcdc..e67890598 100644
--- a/lib/index.ts
+++ b/lib/index.ts
@@ -6,6 +6,7 @@ export { Panel } from "./components/panel/Panel";
export { usePanelCallbackRef } from "./components/panel/usePanelCallbackRef";
export { usePanelRef } from "./components/panel/usePanelRef";
export { Separator } from "./components/separator/Separator";
+export { SeparatorOverlay } from "./components/separator/SeparatorOverlay";
export { isCoarsePointer } from "./global/utils/isCoarsePointer";
@@ -18,6 +19,7 @@ export type {
OnGroupLayoutChange,
Orientation
} from "./components/group/types";
+
export type {
OnPanelResize,
PanelImperativeHandle,
@@ -25,4 +27,8 @@ export type {
PanelSize,
SizeUnit
} from "./components/panel/types";
-export type { SeparatorProps } from "./components/separator/types";
+
+export type {
+ SeparatorProps,
+ SeparatorOverlayProps
+} from "./components/separator/types";
diff --git a/public/generated/docs/Group.json b/public/generated/docs/Group.json
index b28f63517..4f352646e 100644
--- a/public/generated/docs/Group.json
+++ b/public/generated/docs/Group.json
@@ -162,7 +162,13 @@
"resizePreviewMode": {
"description": [
{
- "content": "Controls whether pointer dragging resizes Panels immediately or only moves\na preview separator element until the pointer is released.\nDefaults to "panel" (immediate resizing); "separator" defers resizing until release.\nA visible preview requires an explicit Separator.
\n"
+ "content": "Controls whether pointer dragging updates Panels sizes immediately,\nor renders overlay separator previews until the pointer is released.
\n"
+ },
+ {
+ "content": "Defaults to "panel" (immediate resizing); "separator" defers resizing until release.
\n"
+ },
+ {
+ "content": "Customize previews using the SeparatorOverlay component.
\n"
}
],
"html": "resizePreviewMode?: ResizePreviewMode = \"panel\"
",
diff --git a/public/generated/docs/Separator.json b/public/generated/docs/Separator.json
index d923e84ce..f25cae91b 100644
--- a/public/generated/docs/Separator.json
+++ b/public/generated/docs/Separator.json
@@ -108,6 +108,16 @@
"html": "elementRef?: Ref<HTMLDivElement>
",
"name": "elementRef",
"required": false
+ },
+ "preview": {
+ "description": [
+ {
+ "content": "Overrides the Group default preview for this Separator when resizePreviewMode is "separator".
\n"
+ }
+ ],
+ "html": "preview?: ReactNode
",
+ "name": "preview",
+ "required": false
}
}
}
\ No newline at end of file
diff --git a/public/generated/examples/ResizePreview.json b/public/generated/examples/ResizePreview.json
deleted file mode 100644
index 9a52c1e9b..000000000
--- a/public/generated/examples/ResizePreview.json
+++ /dev/null
@@ -1,3 +0,0 @@
-{
- "html": "<Group resizePreviewMode=\"separator\">
\n <Panel defaultSize=\"50%\" minSize=\"20%\">left</Panel>
\n <Separator />
\n <Panel minSize=\"20%\">right</Panel>
\n</Group>
"
-}
\ No newline at end of file
diff --git a/public/generated/examples/ResizePreviewMode.json b/public/generated/examples/ResizePreviewMode.json
new file mode 100644
index 000000000..191745a86
--- /dev/null
+++ b/public/generated/examples/ResizePreviewMode.json
@@ -0,0 +1,3 @@
+{
+ "html": "<Group resizePreviewMode=\"separator\">
\n <Panel minSize=\"10%\">left</Panel>
\n <Separator />
\n <Panel minSize=\"10%\">right</Panel>
\n</Group>
"
+}
\ No newline at end of file
diff --git a/public/generated/examples/ResizePreviewWithImplicitSeparator.json b/public/generated/examples/ResizePreviewWithImplicitSeparator.json
new file mode 100644
index 000000000..0d8ed1d51
--- /dev/null
+++ b/public/generated/examples/ResizePreviewWithImplicitSeparator.json
@@ -0,0 +1,3 @@
+{
+ "html": "<Group resizePreviewMode=\"separator\">
\n <Panel minSize=\"10%\">left</Panel>
\n <Panel minSize=\"10%\">center</Panel>
\n <Panel minSize=\"10%\">right</Panel>
\n <Panel minSize=\"10%\">right</Panel>
\n
\n <SeparatorOverlay
\n className=\"w-2 bg-sky-500 data-[separator-overlay=inactive]:bg-sky-700 opacity-80\"
\n />
\n</Group>
"
+}
\ No newline at end of file
diff --git a/public/generated/examples/ResizePreviewWithSeparatorOverlay.json b/public/generated/examples/ResizePreviewWithSeparatorOverlay.json
new file mode 100644
index 000000000..246e78633
--- /dev/null
+++ b/public/generated/examples/ResizePreviewWithSeparatorOverlay.json
@@ -0,0 +1,3 @@
+{
+ "html": "<Group resizePreviewMode=\"separator\">
\n <Panel minSize=\"10%\">left</Panel>
\n <Separator />
\n <Panel minSize=\"10%\">center</Panel>
\n <Separator />
\n <Panel minSize=\"10%\">right</Panel>
\n
\n <SeparatorOverlay
\n className=\"w-2 bg-sky-500 data-[separator-overlay=inactive]:bg-sky-700 opacity-80\"
\n />
\n</Group>
"
+}
\ No newline at end of file
diff --git a/public/generated/site-map.json b/public/generated/site-map.json
index 6112e127b..28285d93d 100644
--- a/public/generated/site-map.json
+++ b/public/generated/site-map.json
@@ -12,7 +12,7 @@
{
"path": "/examples/the-basics",
"section": "Examples",
- "text": " The simplest resizable panel configuration is two panels within a group. import { Group, Panel } from \"react-resizable-panels\";\n \n\n left\n right\n Panel groups use a flexbox layout with a default orientation of horizontal but the orientation prop can be used to specify a vertical layout. \n top\n bottom\n Vertical groups may benefit from an explicit height or min-height (read more). Panels can be resized by clicking on their borders but explicit separators can be rendered to improve UX. Separators provide another benefit: double-clicking on one resets a panel to its default size. \n left\n \n right\n For panels that are expensive to resize, you can use to defer panel resizing until release. See Resize behaviors. Separators improve keyboard accessibility by providing a tab-focusable window splitter element. ",
+ "text": " The simplest resizable panel configuration is two panels within a group. import { Group, Panel } from \"react-resizable-panels\";\n \n\n left\n right\n Panel groups use a flexbox layout with a default orientation of horizontal but the orientation prop can be used to specify a vertical layout. \n top\n bottom\n Vertical groups may benefit from an explicit height or min-height (read more). Panels can be resized by clicking on their borders but explicit separators can be rendered to improve UX. Separators provide another benefit: double-clicking on one resets a panel to its default size. \n left\n \n right\n Separators improve keyboard accessibility by providing a tab-focusable window splitter element. ",
"title": "The basics"
},
{
@@ -75,11 +75,17 @@
"text": " Panel and Separator components can be disabled to disable or limit resize behavior. Below are a few examples of how this can be used to implement different types of UIs. In groups with only two panels, disabling a separator is sufficient to completely prevent resizing. \n left\n \n right\n The same applies to disabling one or both panels when there is no explicit separator element. Note this is functionally the same as disabling the entire Group component. \n left\n right\n In groups with three or more panels, disabling a separator does not completely prevent a panel from being resized. In the example below, resizing the center panel can indirectly cause the left panel to be resized as well. Disabling a panel prevents it from being resized, though its separator can still be used to resize other panels. \n left\n \n center (disabled)\n \n right\n When there is no separator, a disabled panel's edges can also be used to resize other panels. You can also disable both a panel and its separator to completely prevent them from being resized or interacted with in any way. \n left (disabled\n \n center\n \n right\n Note that a disabled Panel can still be resized using the imperative API. ",
"title": "Disabling interactions"
},
+ {
+ "path": "/examples/panel-resize-behavior",
+ "section": "Examples",
+ "text": " Dragging a resize separator causes panels to re-render with updated sizes. In most cases, this is what you want, but if re-rendering the contents of a panel is too slow, the resizePreviewMode prop can be used to defer the re-render until the resize is finished. \n left\n \n right\n In place of a panel update, an overlay separator will be rendered instead, as shown in the group below. By default, this overlay separator is just a partially transparent copy of the separator element being dragged. The `SeparatorOverlay` component allows users to customize the overlay. \n left\n \n center\n \n right\n \n \n Resize preview mode works even for groups with implicit separators. \n left\n center\n right\n right\n \n \n The data-separator-overlay attribute can be used to differentiate between a separator that's being active dragged and one that's being moved as a result of min/max size constraints. ",
+ "title": "Panel resize behavior"
+ },
{
"path": "/examples/group-resize-behavior",
"section": "Examples",
- "text": " Separator preview Set resizePreviewMode=\"separator\" to preview a resize without changing panel sizes until release. The preview respects panel size constraints and reuses the clicked Separator’s class, inline styles, and children. The default, \"panel\", resizes panels while dragging. \n left\n \n right\n Render an explicit Separator for a visible preview. The preview is rendered inside the Group and reuses the Separator’s presentation. Panel-edge drags without a Separator still defer resizing until release, but do not show a preview. Group resize behavior Resizing a group typically affects the size of panels within the group. The groupResizeBehavior prop can be used override this behavior and freeze specific panels (in terms of their pixels sizes) while the group is resized. For an example of this, resize the browser window while keeping an eye on the left panel below. \n \n left\n \n \n main\n Minor pixel changes in the panel above are due to precision/rounding. Groups are required to contain at least one panel without groupResizeBehavior=\"preserve-pixel-size\". ",
- "title": "Resize behaviors"
+ "text": " Resizing a group typically affects the size of panels within the group. The groupResizeBehavior prop can be used override this behavior and freeze specific panels (in terms of their pixels sizes) while the group is resized. For an example of this, resize the browser window while keeping an eye on the left panel below. \n \n left\n \n \n main\n Minor pixel changes in the panel above are due to precision/rounding. Groups are required to contain at least one panel without groupResizeBehavior=\"preserve-pixel-size\". ",
+ "title": "Group resize behavior"
},
{
"path": "/examples/overflow",
@@ -96,7 +102,7 @@
{
"path": "/props/group",
"section": "Props",
- "text": " A Group wraps a set of resizable Panel components.\nGroup content can be resized horizontally or vertically.\n Group elements always include the following attributes:\n < div data-group data-testid = \"group-id-prop\" id = \"group-id-prop\" > Test id can be used to narrow selection when unit testing.\n Optional props children?: ReactNode Panel and Separator components that comprise this group.\n className?: string CSS class name.\n defaultLayout?: Layout Default layout for the Group.\n This value allows layouts to be remembered between page reloads.\n Slight layout shift may occur when server-rendering panels with percentage-based default sizes.\nRefer to the documentation for suggestions on how to minimize the impact of this.\n disableCursor?: boolean This library sets custom mouse cursor styles to indicate drag state.\nUse this prop to disable that behavior for Panels and Separators in this group.\n disabled?: boolean Disable resize functionality.\n elementRef?: Ref Ref attached to the root HTMLDivElement.\n groupRef?: Ref Exposes the following imperative API:\n\n getLayout(): Layout \n setLayout(layout: Layout): void \n\n The useGroupRef and useGroupCallbackRef hooks are exported for convenience use in TypeScript projects.\n id?: string | number Uniquely identifies this group within an application.\nFalls back to useId when not provided.\n This value will also be assigned to the data-group attribute.\n onLayoutChange?: (layout: Layout) => void Called when the Group's layout is changing.\n For layout changes caused by pointer events, this method is called each time the pointer is moved.\nFor most cases, it is recommended to use the onLayoutChanged callback instead.\n onLayoutChanged?: (layout: Layout, meta: LayoutChangedMeta) => void Called after the Group's layout has been changed.\n For layout changes caused by pointer events, this method is not called until the pointer has been released.\nThis method is recommended when saving layouts to some storage api.\n The second argument contains meta information about the layout change.\nThe isUserInteraction attribute signals whether the resize was caused by direct user input.\nIt is true for resizes caused by pointer or keyboard input\nand false for other triggers (e.g. imperative API calls, initial mount, etc.)\n orientation?: \"horizontal\" | \"vertical\" = \"horizontal\" Specifies the resizable orientation (\"horizontal\" or \"vertical\"); defaults to \"horizontal\"\n resizePreviewMode?: ResizePreviewMode = \"panel\" Controls whether pointer dragging resizes Panels immediately or only moves\na preview separator element until the pointer is released.\nDefaults to \"panel\" (immediate resizing); \"separator\" defers resizing until release.\nA visible preview requires an explicit Separator.\n resizeTargetMinimumSize?: { coarse: number; fine: number; } = {\n coarse: 20,\n fine: 10\n } Minimum size of the resizable hit target area (either Separator or Panel edge)\nThis threshold ensures are large enough to avoid mis-clicks.\n \nCoarse inputs (typically a finger on a touchscreen) have reduced accuracy;\nto ensure accessibility and ease of use, hit targets should be larger to prevent mis-clicks.\nFine inputs (typically a mouse) can be smaller\n\n Apple interface guidelines suggest 20pt (27px) on desktops and 28pt (37px) for touch devices\nIn practice this seems to be much larger than many of their own applications use though.\n style?: CSSProperties CSS properties.\n The default inline styles cannot be overridden, except for overflow .\n ",
+ "text": " A Group wraps a set of resizable Panel components.\nGroup content can be resized horizontally or vertically.\n Group elements always include the following attributes:\n < div data-group data-testid = \"group-id-prop\" id = \"group-id-prop\" > Test id can be used to narrow selection when unit testing.\n Optional props children?: ReactNode Panel and Separator components that comprise this group.\n className?: string CSS class name.\n defaultLayout?: Layout Default layout for the Group.\n This value allows layouts to be remembered between page reloads.\n Slight layout shift may occur when server-rendering panels with percentage-based default sizes.\nRefer to the documentation for suggestions on how to minimize the impact of this.\n disableCursor?: boolean This library sets custom mouse cursor styles to indicate drag state.\nUse this prop to disable that behavior for Panels and Separators in this group.\n disabled?: boolean Disable resize functionality.\n elementRef?: Ref Ref attached to the root HTMLDivElement.\n groupRef?: Ref Exposes the following imperative API:\n\n getLayout(): Layout \n setLayout(layout: Layout): void \n\n The useGroupRef and useGroupCallbackRef hooks are exported for convenience use in TypeScript projects.\n id?: string | number Uniquely identifies this group within an application.\nFalls back to useId when not provided.\n This value will also be assigned to the data-group attribute.\n onLayoutChange?: (layout: Layout) => void Called when the Group's layout is changing.\n For layout changes caused by pointer events, this method is called each time the pointer is moved.\nFor most cases, it is recommended to use the onLayoutChanged callback instead.\n onLayoutChanged?: (layout: Layout, meta: LayoutChangedMeta) => void Called after the Group's layout has been changed.\n For layout changes caused by pointer events, this method is not called until the pointer has been released.\nThis method is recommended when saving layouts to some storage api.\n The second argument contains meta information about the layout change.\nThe isUserInteraction attribute signals whether the resize was caused by direct user input.\nIt is true for resizes caused by pointer or keyboard input\nand false for other triggers (e.g. imperative API calls, initial mount, etc.)\n orientation?: \"horizontal\" | \"vertical\" = \"horizontal\" Specifies the resizable orientation (\"horizontal\" or \"vertical\"); defaults to \"horizontal\"\n resizePreviewMode?: ResizePreviewMode = \"panel\" Controls whether pointer dragging updates Panels sizes immediately,\nor renders overlay separator previews until the pointer is released.\n Defaults to \"panel\" (immediate resizing); \"separator\" defers resizing until release.\n Customize previews using the SeparatorOverlay component.\n resizeTargetMinimumSize?: { coarse: number; fine: number; } = {\n coarse: 20,\n fine: 10\n } Minimum size of the resizable hit target area (either Separator or Panel edge)\nThis threshold ensures are large enough to avoid mis-clicks.\n \nCoarse inputs (typically a finger on a touchscreen) have reduced accuracy;\nto ensure accessibility and ease of use, hit targets should be larger to prevent mis-clicks.\nFine inputs (typically a mouse) can be smaller\n\n Apple interface guidelines suggest 20pt (27px) on desktops and 28pt (37px) for touch devices\nIn practice this seems to be much larger than many of their own applications use though.\n style?: CSSProperties CSS properties.\n The default inline styles cannot be overridden, except for overflow .\n ",
"title": "Group component props"
},
{
@@ -108,7 +114,7 @@
{
"path": "/props/separator",
"section": "Props",
- "text": " Separators are not required but they are recommended as they improve keyboard accessibility.\n Separator elements must be direct DOM children of their parent Group elements.\n Separator elements always include the following attributes:\n < div data-separator data-testid = \"separator-id-prop\" id = \"separator-id-prop\" role = \"separator\" > Test id can be used to narrow selection when unit testing.\n In addition to the attributes shown above, separator also renders all required WAI-ARIA properties.\n Optional props className?: string CSS class name.\n Use the data-separator attribute for custom hover and active styles\n The following properties cannot be overridden: flex-grow, flex-shrink \n disabled?: boolean When disabled, the separator cannot be used to resize its neighboring panels.\n The panels may still be resized indirectly (while other panels are being resized).\nTo prevent a panel from being resized at all, it needs to also be disabled.\n disableDoubleClick?: boolean When true, double-clicking this Separator will not reset its Panel to its default size.\n elementRef?: Ref Ref attached to the root HTMLDivElement.\n id?: string | number Uniquely identifies the separator within the parent group.\nFalls back to useId when not provided.\n This value will also be assigned to the data-separator attribute.\n style?: CSSProperties CSS properties.\n Use the data-separator attribute for custom hover and active styles\n The following properties cannot be overridden: flex-grow, flex-shrink \n ",
+ "text": " Separators are not required but they are recommended as they improve keyboard accessibility.\n Separator elements must be direct DOM children of their parent Group elements.\n Separator elements always include the following attributes:\n < div data-separator data-testid = \"separator-id-prop\" id = \"separator-id-prop\" role = \"separator\" > Test id can be used to narrow selection when unit testing.\n In addition to the attributes shown above, separator also renders all required WAI-ARIA properties.\n Optional props className?: string CSS class name.\n Use the data-separator attribute for custom hover and active styles\n The following properties cannot be overridden: flex-grow, flex-shrink \n disabled?: boolean When disabled, the separator cannot be used to resize its neighboring panels.\n The panels may still be resized indirectly (while other panels are being resized).\nTo prevent a panel from being resized at all, it needs to also be disabled.\n disableDoubleClick?: boolean When true, double-clicking this Separator will not reset its Panel to its default size.\n elementRef?: Ref Ref attached to the root HTMLDivElement.\n id?: string | number Uniquely identifies the separator within the parent group.\nFalls back to useId when not provided.\n This value will also be assigned to the data-separator attribute.\n preview?: ReactNode Overrides the Group default preview for this Separator when resizePreviewMode is \"separator\".\n style?: CSSProperties CSS properties.\n Use the data-separator attribute for custom hover and active styles\n The following properties cannot be overridden: flex-grow, flex-shrink \n ",
"title": "Separator component props"
},
{
diff --git a/src/App.tsx b/src/App.tsx
index 8abb9c910..447c6acfe 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -54,8 +54,11 @@ export default function App() {
Fixed size panels