diff --git a/CHANGELOG.md b/CHANGELOG.md index 51dd5de3f..dad4a6f05 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/README.md b/README.md index eb6cca08c..f6160ae66 100644 --- a/README.md +++ b/README.md @@ -235,6 +235,11 @@ Falls back to useId when not provided.

defaultSize

Default size of Panel within its parent group; default is auto-assigned based on the total number of Panels.

+ + + + disabled +

When disabled, a panel cannot be resized either directly or indirectly (by resizing another panel).

@@ -333,6 +338,13 @@ Falls back to useId when not provided.

CSS properties.

ℹ️ Use the data-separator attribute for custom hover and active styles

⚠️ The following properties cannot be overridden: flex-grow, flex-shrink

+ + + + disabled +

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.

diff --git a/integrations/tests/tests/cursor.spec.tsx b/integrations/tests/tests/cursor.spec.tsx index 994ecc2fd..162bcc693 100644 --- a/integrations/tests/tests/cursor.spec.tsx +++ b/integrations/tests/tests/cursor.spec.tsx @@ -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, @@ -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, + + + + + ); + + 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, + + + + + + ); + + 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"); + }); }); diff --git a/integrations/tests/tests/keyboard-interactions.spec.tsx b/integrations/tests/tests/keyboard-interactions.spec.tsx index 37b0f0d35..84b039b86 100644 --- a/integrations/tests/tests/keyboard-interactions.spec.tsx +++ b/integrations/tests/tests/keyboard-interactions.spec.tsx @@ -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, + + + + + + ); + + const separator = page.getByTestId("separator"); + await expect(separator).toHaveAttribute("aria-disabled", "true"); + await expect(separator).not.toHaveAttribute("tabIndex"); + }); }); diff --git a/integrations/tests/tests/pointer-interactions.spec.tsx b/integrations/tests/tests/pointer-interactions.spec.tsx index 14e7837c5..a850c7c32 100644 --- a/integrations/tests/tests/pointer-interactions.spec.tsx +++ b/integrations/tests/tests/pointer-interactions.spec.tsx @@ -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, + + + + + + ); + + 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, + + + + + + ); + + 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(); + }); }); diff --git a/lib/components/panel/Panel.tsx b/lib/components/panel/Panel.tsx index 2dd972050..aba4497a3 100644 --- a/lib/components/panel/Panel.tsx +++ b/lib/components/panel/Panel.tsx @@ -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. @@ -41,6 +41,7 @@ export function Panel({ collapsedSize = "0%", collapsible = false, defaultSize, + disabled, elementRef: elementRefProp, id: idProp, maxSize = "100%", @@ -75,7 +76,7 @@ export function Panel({ useIsomorphicLayoutEffect(() => { const element = elementRef.current; if (element !== null) { - return registerPanel({ + const registeredPanel: RegisteredPanel = { element, id, idIsStable, @@ -88,15 +89,19 @@ export function Panel({ collapsedSize, collapsible, defaultSize, + disabled, maxSize, minSize } - }); + }; + + return registerPanel(registeredPanel); } }, [ collapsedSize, collapsible, defaultSize, + disabled, hasOnResize, id, idIsStable, @@ -126,6 +131,7 @@ export function Panel({ return (
; diff --git a/lib/components/separator/Separator.tsx b/lib/components/separator/Separator.tsx index a73756f69..dbfdd962b 100644 --- a/lib/components/separator/Separator.tsx +++ b/lib/components/separator/Separator.tsx @@ -28,6 +28,7 @@ import type { RegisteredSeparator, SeparatorProps } from "./types"; export function Separator({ children, className, + disabled, elementRef: elementRefProp, id: idProp, style, @@ -64,6 +65,7 @@ export function Separator({ const element = elementRef.current; if (element !== null) { const separator: RegisteredSeparator = { + disabled, element, id }; @@ -119,30 +121,32 @@ export function Separator({ unregisterSeparator(); }; } - }, [groupId, id, registerSeparator]); + }, [disabled, groupId, id, registerSeparator]); return (