Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Changelog

## 4.6.0

- [657](https://github.com/bvaughn/react-resizable-panels/pull/657): Allow `Panel` and `Separator` components to be disabled

## 4.5.9

- [649](https://github.com/bvaughn/react-resizable-panels/pull/649): Optimization: Replace `useForceUpdate` with `useSyncExternalStore` to avoid side effect of swallowing "click" events in certain cases
Expand Down
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,11 @@ Falls back to <code>useId</code> when not provided.</p>
<tr>
<td>defaultSize</td>
<td><p>Default size of Panel within its parent group; default is auto-assigned based on the total number of Panels.</p>
</td>
</tr>
<tr>
<td>disabled</td>
<td><p>When disabled, a panel cannot be resized either directly or indirectly (by resizing another panel).</p>
</td>
</tr>
<tr>
Expand Down Expand Up @@ -333,6 +338,13 @@ Falls back to <code>useId</code> when not provided.</p>
<td><p>CSS properties.</p>
<p>ℹ️ Use the <code>data-separator</code> attribute for custom <em>hover</em> and <em>active</em> styles</p>
<p>⚠️ The following properties cannot be overridden: <code>flex-grow</code>, <code>flex-shrink</code></p>
</td>
</tr>
<tr>
<td>disabled</td>
<td><p>When disabled, the separator cannot be used to resize its neighboring panels.</p>
<p>ℹ️ The panels may still be resized indirectly (while other panels are being resized).
To prevent a panel from being resized at all, it needs to also be disabled.</p>
</td>
</tr>
<tr>
Expand Down
54 changes: 53 additions & 1 deletion integrations/tests/tests/cursor.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -211,7 +211,7 @@ test.describe("cursor", () => {
).toBe("move");
});

test("disabled", async ({ page: mainPage }) => {
test("disabled group cursor", async ({ page: mainPage }) => {
const page = await goToUrl(
mainPage,
<Group disableCursor orientation="vertical">
Expand All @@ -234,4 +234,56 @@ test.describe("cursor", () => {
await page.evaluate(() => getComputedStyle(document.body).cursor)
).toBe("auto");
});

test("disabled panel(s)", async ({ page: mainPage }) => {
const page = await goToUrl(
mainPage,
<Group>
<Panel disabled id="left" />
<Panel id="right" />
</Group>
);

const hitAreaBox = await calculateHitArea(page, ["left", "right"]);
const { x, y } = getCenterCoordinates(hitAreaBox);

expect(
await page.evaluate(() => getComputedStyle(document.body).cursor)
).toBe("auto");

await page.mouse.move(x, y, moveConfig);

expect(
await page.evaluate(() => getComputedStyle(document.body).cursor)
).toBe("auto");
});

test("disabled separator", async ({ page: mainPage }) => {
const page = await goToUrl(
mainPage,
<Group>
<Panel id="left" />
<Separator disabled id="separator" />
<Panel id="right" />
</Group>
);

const separator = page.getByTestId("separator");
const boundingBox = (await separator.boundingBox())!;
const x = boundingBox.x + boundingBox.width / 2;
const y = boundingBox.y + boundingBox.height / 2;

expect(
await page.evaluate(() => getComputedStyle(document.body).cursor)
).toBe("auto");

await page.mouse.move(x, y, moveConfig);

expect(
await page.evaluate(
() =>
getComputedStyle(document.querySelector('[role="separator"]')!).cursor
)
).toBe("not-allowed");
});
});
17 changes: 17 additions & 0 deletions integrations/tests/tests/keyboard-interactions.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -313,4 +313,21 @@ test.describe("keyboard interactions: window splitter api", () => {
await assertLayoutChangeCounts(mainPage, 1);
await expect(mainPage.getByText('"left": 30')).toBeVisible();
});

test("should not allow disabled separators to be focused", async ({
page: mainPage
}) => {
const page = await goToUrl(
mainPage,
<Group disabled>
<Panel />
<Separator disabled id="separator" />
<Panel />
</Group>
);

const separator = page.getByTestId("separator");
await expect(separator).toHaveAttribute("aria-disabled", "true");
await expect(separator).not.toHaveAttribute("tabIndex");
});
});
42 changes: 42 additions & 0 deletions integrations/tests/tests/pointer-interactions.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -570,4 +570,46 @@ test.describe("pointer interactions", () => {
await page.mouse.up();
await expect(panel).toContainText("click:1");
});

test("should not allow a disabled separator to be used to resize a panels", async ({
page: mainPage
}) => {
const page = await goToUrl(
mainPage,
<Group>
<Panel id="left" />
<Separator disabled />
<Panel id="right" />
</Group>
);

await assertLayoutChangeCounts(mainPage, 1);
await expect(mainPage.getByText('"left": 50')).toBeVisible();

await resizeHelper(page, ["left", "right"], 100, 0);

await assertLayoutChangeCounts(mainPage, 1);
await expect(mainPage.getByText('"left": 50')).toBeVisible();
});

test("should not allow a disabled panel to be resized", async ({
page: mainPage
}) => {
const page = await goToUrl(
mainPage,
<Group>
<Panel disabled id="left" />
<Separator />
<Panel id="right" />
</Group>
);

await assertLayoutChangeCounts(mainPage, 1);
await expect(mainPage.getByText('"left": 50')).toBeVisible();

await resizeHelper(page, ["left", "right"], 100, 0);

await assertLayoutChangeCounts(mainPage, 1);
await expect(mainPage.getByText('"left": 50')).toBeVisible();
});
});
14 changes: 10 additions & 4 deletions lib/components/panel/Panel.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
"use client";

import { useRef, useSyncExternalStore, type CSSProperties } from "react";
import { eventEmitter } from "../../global/mutableState";
import { useId } from "../../hooks/useId";
import { useIsomorphicLayoutEffect } from "../../hooks/useIsomorphicLayoutEffect";
import { useMergedRefs } from "../../hooks/useMergedRefs";
import { useStableCallback } from "../../hooks/useStableCallback";
import { useGroupContext } from "../group/useGroupContext";
import type { PanelProps, PanelSize } from "./types";
import type { PanelProps, PanelSize, RegisteredPanel } from "./types";
import { usePanelImperativeHandle } from "./usePanelImperativeHandle";
import { eventEmitter } from "../../global/mutableState";

/**
* A Panel wraps resizable content and can be configured with min/max size constraints and collapsible behavior.
Expand Down Expand Up @@ -41,6 +41,7 @@ export function Panel({
collapsedSize = "0%",
collapsible = false,
defaultSize,
disabled,
elementRef: elementRefProp,
id: idProp,
maxSize = "100%",
Expand Down Expand Up @@ -75,7 +76,7 @@ export function Panel({
useIsomorphicLayoutEffect(() => {
const element = elementRef.current;
if (element !== null) {
return registerPanel({
const registeredPanel: RegisteredPanel = {
element,
id,
idIsStable,
Expand All @@ -88,15 +89,19 @@ export function Panel({
collapsedSize,
collapsible,
defaultSize,
disabled,
maxSize,
minSize
}
});
};

return registerPanel(registeredPanel);
}
}, [
collapsedSize,
collapsible,
defaultSize,
disabled,
hasOnResize,
id,
idIsStable,
Expand Down Expand Up @@ -126,6 +131,7 @@ export function Panel({
return (
<div
{...rest}
aria-disabled={disabled}
data-panel
data-testid={id}
id={id}
Expand Down
13 changes: 12 additions & 1 deletion lib/components/panel/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export type PanelConstraints = {
collapsedSize: number;
collapsible: boolean;
defaultSize: number | undefined;
disabled?: boolean | undefined;
maxSize: number;
minSize: number;
panelId: string;
Expand Down Expand Up @@ -115,6 +116,11 @@ export type PanelProps = BasePanelAttributes & {
*/
defaultSize?: number | string | undefined;

/**
* When disabled, a panel cannot be resized either directly or indirectly (by resizing another panel).
*/
disabled?: boolean | undefined;

/**
* Ref attached to the root `HTMLDivElement`.
*/
Expand Down Expand Up @@ -194,5 +200,10 @@ export type OnPanelResize = PanelProps["onResize"];
*/
export type PanelConstraintProps = Pick<
PanelProps,
"collapsedSize" | "collapsible" | "defaultSize" | "maxSize" | "minSize"
| "collapsedSize"
| "collapsible"
| "defaultSize"
| "disabled"
| "maxSize"
| "minSize"
>;
10 changes: 7 additions & 3 deletions lib/components/separator/Separator.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import type { RegisteredSeparator, SeparatorProps } from "./types";
export function Separator({
children,
className,
disabled,
elementRef: elementRefProp,
id: idProp,
style,
Expand Down Expand Up @@ -64,6 +65,7 @@ export function Separator({
const element = elementRef.current;
if (element !== null) {
const separator: RegisteredSeparator = {
disabled,
element,
id
};
Expand Down Expand Up @@ -119,30 +121,32 @@ export function Separator({
unregisterSeparator();
};
}
}, [groupId, id, registerSeparator]);
}, [disabled, groupId, id, registerSeparator]);

return (
<div
{...rest}
aria-controls={aria.valueControls}
aria-disabled={disabled}
aria-orientation={orientation}
aria-valuemax={aria.valueMax}
aria-valuemin={aria.valueMin}
aria-valuenow={aria.valueNow}
children={children}
className={className}
data-separator={dragState}
data-separator={disabled ? "disabled" : dragState}
data-testid={id}
id={id}
ref={mergedRef}
role="separator"
style={{
flexBasis: "auto",
cursor: disabled ? "not-allowed" : undefined,
...style,
flexGrow: 0,
flexShrink: 0
}}
tabIndex={0}
tabIndex={disabled ? undefined : 0}
/>
);
}
Expand Down
9 changes: 9 additions & 0 deletions lib/components/separator/types.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { CSSProperties, HTMLAttributes, Ref } from "react";

export type RegisteredSeparator = {
disabled?: boolean | undefined;
element: HTMLDivElement;
id: string;
};
Expand All @@ -20,6 +21,14 @@ export type SeparatorProps = BaseSeparatorAttributes & {
*/
className?: string | undefined;

/**
* When disabled, the separator cannot be used to resize its neighboring panels.
*
* ℹ️ The panels may still be resized indirectly (while other panels are being resized).
* To prevent a panel from being resized at all, it needs to also be disabled.
*/
disabled?: boolean | undefined;

/**
* Ref attached to the root `HTMLDivElement`.
*/
Expand Down
12 changes: 6 additions & 6 deletions lib/global/cursor/getCursorStyle.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ describe("getCursorStyle", () => {
groups: [horizontalGroup, verticalGroup],
state: "inactive"
})
).toBeNull();
).toBeUndefined();
});
});

Expand Down Expand Up @@ -90,7 +90,7 @@ describe("getCursorStyle", () => {
groups: [disabledGroup],
state: "hover"
})
).toBeNull();
).toBeUndefined();
});
});

Expand Down Expand Up @@ -151,7 +151,7 @@ describe("getCursorStyle", () => {
groups: [disabledGroup],
state: "active"
})
).toBeNull();
).toBeUndefined();
});
});
});
Expand All @@ -169,7 +169,7 @@ describe("getCursorStyle", () => {
groups: [horizontalGroup, verticalGroup],
state: "inactive"
})
).toBeNull();
).toBeUndefined();
});
});

Expand Down Expand Up @@ -221,7 +221,7 @@ describe("getCursorStyle", () => {
groups: [disabledGroup],
state: "hover"
})
).toBeNull();
).toBeUndefined();
});
});

Expand Down Expand Up @@ -282,7 +282,7 @@ describe("getCursorStyle", () => {
groups: [disabledGroup],
state: "active"
})
).toBeNull();
).toBeUndefined();
});
});
});
Expand Down
Loading
Loading