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
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
# Changelog

## 4.4.0

- [599](https://github.com/bvaughn/react-resizable-panels/pull/599): Add new `onLayoutChanged` prop to `Group`.

For layout changes caused by pointer events, this method is not called until the pointer has been released. This callback should be used if you're doing something like saving a layout as it is called less frequently than the previous approach.

The `useDefaultLayout` hook has also been updated to use this callback (though it will continue to support the old callback as well, with a `@deprecation` tag).

## 4.3.3

- [595](https://github.com/bvaughn/react-resizable-panels/pull/595): Don't call `event.preventDefault()` on "pointerup" unless a handle was actively dragged
Expand Down
11 changes: 10 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,16 @@ Use this prop to disable that behavior for Panels and Separators in this group.<
</tr>
<tr>
<td>onLayoutChange</td>
<td><p>Called when panel sizes change; receives a map of Panel id to size.</p>
<td><p>Called when the Group&#39;s layout is changing.</p>
<p>⚠️ For layout changes caused by pointer events, this method is called each time the pointer is moved.
For most cases, it is recommended to use the <code>onLayoutChanged</code> callback instead.</p>
</td>
</tr>
<tr>
<td>onLayoutChanged</td>
<td><p>Called after the Group&#39;s layout has been changed.</p>
<p>ℹ️ For layout changes caused by pointer events, this method is not called until the pointer has been released.
This method is recommended when saving layouts to some storage api.</p>
</td>
</tr>
<tr>
Expand Down
18 changes: 14 additions & 4 deletions integrations/tests/src/components/Decoder.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,8 @@ export function Decoder({
imperativeGroupApiLayout: Layout | undefined;
imperativePanelApiSize: PanelSize | undefined;
layout: Layout;
onLayoutCount: number;
onLayoutChangeCount: number;
onLayoutChangedCount: number;
panels: {
[id: number | string]: {
onResizeCount: number;
Expand All @@ -75,7 +76,8 @@ export function Decoder({
imperativeGroupApiLayout: undefined,
imperativePanelApiSize: undefined,
layout: {},
onLayoutCount: 0,
onLayoutChangeCount: 0,
onLayoutChangedCount: 0,
panels: {}
});

Expand All @@ -94,7 +96,14 @@ export function Decoder({

setState((prev) => ({
...prev,
onLayoutCount: prev.onLayoutCount + 1,
onLayoutChangeCount: prev.onLayoutChangeCount + 1,
layout
}));
},
onLayoutChanged: (layout) => {
setState((prev) => ({
...prev,
onLayoutChangedCount: prev.onLayoutChangedCount + 1,
layout
}));
}
Expand Down Expand Up @@ -153,7 +162,8 @@ export function Decoder({
<DebugData
data={{
layout: state.layout,
onLayoutCount: state.onLayoutCount
onLayoutChangeCount: state.onLayoutChangeCount,
onLayoutChangedCount: state.onLayoutChangedCount
}}
/>
{Array.from(Object.keys(state.panels)).map((panelId) => (
Expand Down
14 changes: 14 additions & 0 deletions integrations/tests/src/utils/assertLayoutChangeCounts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { expect, type Page } from "@playwright/test";

export async function assertLayoutChangeCounts(
page: Page,
layoutChangeCount: number,
layoutChangedCount: number | undefined = layoutChangeCount
) {
await expect(
page.getByText(`"onLayoutChangeCount": ${layoutChangeCount}`)
).toBeVisible();
await expect(
page.getByText(`"onLayoutChangedCount": ${layoutChangedCount}`)
).toBeVisible();
}
9 changes: 6 additions & 3 deletions integrations/tests/src/utils/expectLayout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,20 @@ import type { Layout } from "react-resizable-panels";
export async function expectLayout({
layout,
mainPage,
onLayoutCount
onLayoutChangeCount,
onLayoutChangedCount
}: {
layout: Layout;
mainPage: Page;
onLayoutCount: number;
onLayoutChangeCount: number;
onLayoutChangedCount: number;
}) {
await expect(mainPage.getByText('"layout"')).toHaveText(
JSON.stringify(
{
layout,
onLayoutCount
onLayoutChangeCount,
onLayoutChangedCount
},
null,
2
Expand Down
21 changes: 11 additions & 10 deletions integrations/tests/tests/fixed-size-elements.spec.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { expect, test } from "@playwright/test";
import { test } from "@playwright/test";
import { Group, Panel, Separator } from "react-resizable-panels";
import { assertLayoutChangeCounts } from "../src/utils/assertLayoutChangeCounts";
import { goToUrl } from "../src/utils/goToUrl";

test.describe("fixed size elements", () => {
Expand All @@ -23,7 +24,7 @@ test.describe("fixed size elements", () => {
{ usePopUpWindow }
);

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

for (const text of ["foo+bar", "bar+baz", "baz+qux"]) {
const box = (await page.getByText(text).boundingBox())!;
Expand All @@ -34,7 +35,7 @@ test.describe("fixed size elements", () => {
await page.mouse.up();
}

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

test("should work without an explicit separator", async ({
Expand All @@ -57,7 +58,7 @@ test.describe("fixed size elements", () => {
{ usePopUpWindow }
);

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

const boxFoo = (await page.getByText("id: foo").boundingBox())!;

Expand All @@ -66,7 +67,7 @@ test.describe("fixed size elements", () => {
await page.mouse.move(0, 0);
await page.mouse.up();

await expect(mainPage.getByText('"onLayoutCount": 2')).toBeVisible();
await assertLayoutChangeCounts(mainPage, 2);

const boxBar = (await page.getByText("id: bar").boundingBox())!;

Expand All @@ -75,7 +76,7 @@ test.describe("fixed size elements", () => {
await page.mouse.move(1000, 0);
await page.mouse.up();

await expect(mainPage.getByText('"onLayoutCount": 3')).toBeVisible();
await assertLayoutChangeCounts(mainPage, 3);
});

test("should work with an explicit separator", async ({
Expand Down Expand Up @@ -105,7 +106,7 @@ test.describe("fixed size elements", () => {
await page.mouse.move(0, 0);
await page.mouse.up();

await expect(mainPage.getByText('"onLayoutCount": 2')).toBeVisible();
await assertLayoutChangeCounts(mainPage, 2);

const boxBaz = (await page.getByText("id: baz").boundingBox())!;

Expand All @@ -114,7 +115,7 @@ test.describe("fixed size elements", () => {
await page.mouse.move(1000, 0);
await page.mouse.up();

await expect(mainPage.getByText('"onLayoutCount": 3')).toBeVisible();
await assertLayoutChangeCounts(mainPage, 3);
});

test("should work with two explicit separators", async ({
Expand Down Expand Up @@ -146,7 +147,7 @@ test.describe("fixed size elements", () => {
await page.mouse.move(0, 0);
await page.mouse.up();

await expect(mainPage.getByText('"onLayoutCount": 2')).toBeVisible();
await assertLayoutChangeCounts(mainPage, 2);

separatorBox = (await page.getByTestId("baz+qux+right").boundingBox())!;

Expand All @@ -155,7 +156,7 @@ test.describe("fixed size elements", () => {
await page.mouse.move(1000, 0);
await page.mouse.up();

await expect(mainPage.getByText('"onLayoutCount": 3')).toBeVisible();
await assertLayoutChangeCounts(mainPage, 3);
});
});
}
Expand Down
43 changes: 22 additions & 21 deletions integrations/tests/tests/keyboard-interactions.spec.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { expect, test } from "@playwright/test";
import { Group, Panel, Separator } from "react-resizable-panels";
import { assertLayoutChangeCounts } from "../src/utils/assertLayoutChangeCounts";
import { getSeparatorAriaAttributes } from "../src/utils/getSeparatorAriaAttributes";
import { goToUrl } from "../src/utils/goToUrl";

Expand All @@ -21,7 +22,7 @@ test.describe("keyboard interactions: window splitter api", () => {
{ usePopUpWindow }
);

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

await expect(await getSeparatorAriaAttributes(page)).toEqual({
Expand All @@ -36,7 +37,7 @@ test.describe("keyboard interactions: window splitter api", () => {
await separator.focus();
await page.keyboard.press("ArrowLeft");

await expect(mainPage.getByText('"onLayoutCount": 2')).toBeVisible();
await assertLayoutChangeCounts(mainPage, 2);
await expect(mainPage.getByText('"left": 25')).toBeVisible();

await expect(await getSeparatorAriaAttributes(page)).toEqual({
Expand All @@ -48,7 +49,7 @@ test.describe("keyboard interactions: window splitter api", () => {

await page.keyboard.press("ArrowRight");

await expect(mainPage.getByText('"onLayoutCount": 3')).toBeVisible();
await assertLayoutChangeCounts(mainPage, 3);
await expect(mainPage.getByText('"left": 30')).toBeVisible();

await expect(await getSeparatorAriaAttributes(page)).toEqual({
Expand All @@ -61,7 +62,7 @@ test.describe("keyboard interactions: window splitter api", () => {
// Up/down are no-ops
await page.keyboard.press("ArrowUp");
await page.keyboard.press("ArrowDown");
await expect(mainPage.getByText('"onLayoutCount": 3')).toBeVisible();
await assertLayoutChangeCounts(mainPage, 3);
});

test("vertical: arrow keys", async ({ page: mainPage }) => {
Expand All @@ -75,7 +76,7 @@ test.describe("keyboard interactions: window splitter api", () => {
{ usePopUpWindow }
);

await expect(mainPage.getByText('"onLayoutCount": 1')).toBeVisible();
await assertLayoutChangeCounts(mainPage, 1);
await expect(mainPage.getByText('"top": 30')).toBeVisible();

await expect(await getSeparatorAriaAttributes(page)).toEqual({
Expand All @@ -90,7 +91,7 @@ test.describe("keyboard interactions: window splitter api", () => {
await separator.focus();
await page.keyboard.press("ArrowDown");

await expect(mainPage.getByText('"onLayoutCount": 2')).toBeVisible();
await assertLayoutChangeCounts(mainPage, 2);
await expect(mainPage.getByText('"top": 35')).toBeVisible();

await expect(await getSeparatorAriaAttributes(page)).toEqual({
Expand All @@ -102,7 +103,7 @@ test.describe("keyboard interactions: window splitter api", () => {

await page.keyboard.press("ArrowUp");

await expect(mainPage.getByText('"onLayoutCount": 3')).toBeVisible();
await assertLayoutChangeCounts(mainPage, 3);
await expect(mainPage.getByText('"top": 30')).toBeVisible();

await expect(await getSeparatorAriaAttributes(page)).toEqual({
Expand All @@ -115,7 +116,7 @@ test.describe("keyboard interactions: window splitter api", () => {
// Left/right are no-ops
await page.keyboard.press("ArrowLeft");
await page.keyboard.press("ArrowRight");
await expect(mainPage.getByText('"onLayoutCount": 3')).toBeVisible();
await assertLayoutChangeCounts(mainPage, 3);
});

test("enter key and collapsible panel", async ({ page: mainPage }) => {
Expand All @@ -129,7 +130,7 @@ test.describe("keyboard interactions: window splitter api", () => {
{ usePopUpWindow }
);

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

Expand All @@ -144,7 +145,7 @@ test.describe("keyboard interactions: window splitter api", () => {
await separator.focus();
await page.keyboard.press("Enter");

await expect(mainPage.getByText('"onLayoutCount": 2')).toBeVisible();
await assertLayoutChangeCounts(mainPage, 2);
await expect(mainPage.getByText('"left": 5')).toBeVisible();
await expect(mainPage.getByText('"right": 95')).toBeVisible();

Expand All @@ -157,7 +158,7 @@ test.describe("keyboard interactions: window splitter api", () => {

await page.keyboard.press("Enter");

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

Expand All @@ -170,7 +171,7 @@ test.describe("keyboard interactions: window splitter api", () => {

await page.keyboard.press("ArrowLeft");

await expect(mainPage.getByText('"onLayoutCount": 4')).toBeVisible();
await assertLayoutChangeCounts(mainPage, 4);
await expect(mainPage.getByText('"left": 45')).toBeVisible();
await expect(mainPage.getByText('"right": 55')).toBeVisible();

Expand All @@ -183,7 +184,7 @@ test.describe("keyboard interactions: window splitter api", () => {

await page.keyboard.press("Enter");

await expect(mainPage.getByText('"onLayoutCount": 5')).toBeVisible();
await assertLayoutChangeCounts(mainPage, 5);
await expect(mainPage.getByText('"left": 5')).toBeVisible();
await expect(mainPage.getByText('"right": 95')).toBeVisible();

Expand All @@ -196,7 +197,7 @@ test.describe("keyboard interactions: window splitter api", () => {

await page.keyboard.press("Enter");

await expect(mainPage.getByText('"onLayoutCount": 6')).toBeVisible();
await assertLayoutChangeCounts(mainPage, 6);
await expect(mainPage.getByText('"left": 45')).toBeVisible();
await expect(mainPage.getByText('"right": 55')).toBeVisible();

Expand All @@ -221,15 +222,15 @@ test.describe("keyboard interactions: window splitter api", () => {
{ usePopUpWindow }
);

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

const separator = page.getByRole("separator");
await separator.focus();
await page.keyboard.press("Enter");

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

test("home and end keys", async ({ page: mainPage }) => {
Expand All @@ -243,7 +244,7 @@ test.describe("keyboard interactions: window splitter api", () => {
{ usePopUpWindow }
);

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

Expand All @@ -252,13 +253,13 @@ test.describe("keyboard interactions: window splitter api", () => {
await separator.focus();
await page.keyboard.press("Home");

await expect(mainPage.getByText('"onLayoutCount": 2')).toBeVisible();
await assertLayoutChangeCounts(mainPage, 2);
await expect(mainPage.getByText('"left": 20')).toBeVisible();
await expect(mainPage.getByText('"right": 80')).toBeVisible();

await page.keyboard.press("End");

await expect(mainPage.getByText('"onLayoutCount": 3')).toBeVisible();
await assertLayoutChangeCounts(mainPage, 3);
await expect(mainPage.getByText('"left": 95')).toBeVisible();
await expect(mainPage.getByText('"right": 5')).toBeVisible();
});
Expand Down Expand Up @@ -307,7 +308,7 @@ test.describe("keyboard interactions: window splitter api", () => {
{ usePopUpWindow }
);

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

const separator = page.getByRole("separator");
Expand All @@ -320,7 +321,7 @@ test.describe("keyboard interactions: window splitter api", () => {
await page.keyboard.press("Enter");
await page.keyboard.press("Home");

await expect(mainPage.getByText('"onLayoutCount": 1')).toBeVisible();
await assertLayoutChangeCounts(mainPage, 1);
await expect(mainPage.getByText('"left": 30')).toBeVisible();
});
});
Expand Down
Loading
Loading