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

## 4.6.5

- [670](https://github.com/bvaughn/react-resizable-panels/pull/670): Check for undefined `adoptedStyleSheets` (to better support environments like jsdom)
- [671](https://github.com/bvaughn/react-resizable-panels/pull/671): Bug-fix: Update in-memory layout cache when group is resized by double-clicking on a separator

## 4.6.4

- [664](https://github.com/bvaughn/react-resizable-panels/pull/664), [665](https://github.com/bvaughn/react-resizable-panels/pull/665): Resize actions sometimes "jump" on touch devices
Expand Down
180 changes: 177 additions & 3 deletions lib/components/group/Group.test.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { render } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import {
createRef,
useEffect,
useLayoutEffect,
useRef,
type PropsWithChildren
type PropsWithChildren,
type RefObject
} from "react";
import { beforeEach, describe, expect, test, vi } from "vitest";
import { eventEmitter } from "../../global/mutableState";
Expand All @@ -18,7 +20,7 @@ import { Panel } from "../panel/Panel";
import type { PanelImperativeHandle } from "../panel/types";
import { Separator } from "../separator/Separator";
import { Group } from "./Group";
import type { GroupImperativeHandle } from "./types";
import type { GroupImperativeHandle, Layout } from "./types";
import { useGroupRef } from "./useGroupRef";

describe("Group", () => {
Expand Down Expand Up @@ -92,6 +94,130 @@ describe("Group", () => {
);
});

describe("in-memory layout cache", () => {
async function runTest(
callback: (args: {
container: HTMLElement;
groupRef: RefObject<GroupImperativeHandle | null>;
panelRef: RefObject<PanelImperativeHandle | null>;
}) => Promise<void>,
expectedLayout: Layout
) {
setElementBoundsFunction((element) => {
switch (element.id) {
case "group": {
return new DOMRect(0, 0, 100, 50);
}
case "left": {
return new DOMRect(0, 0, 50, 50);
}
case "right": {
return new DOMRect(50, 0, 50, 50);
}
case "separator": {
return new DOMRect(50, 0, 0, 50);
}
}
});

const onLayoutChanged = vi.fn();

const groupRef = createRef<GroupImperativeHandle>();
const panelRef = createRef<PanelImperativeHandle>();

const { container, rerender } = render(
<Group groupRef={groupRef} onLayoutChanged={onLayoutChanged}>
<Panel id="left" panelRef={panelRef} />
<Separator id="separator" />
<Panel id="right" />
</Group>
);

expect(onLayoutChanged).toHaveBeenCalledTimes(1);
expect(onLayoutChanged).toHaveBeenLastCalledWith({
left: 50,
right: 50
});

await callback({ container, groupRef, panelRef });

expect(onLayoutChanged).toHaveBeenCalledTimes(2);
expect(onLayoutChanged).toHaveBeenLastCalledWith(expectedLayout);

rerender(
<Group groupRef={groupRef} onLayoutChanged={onLayoutChanged}>
<Panel id="right" />
</Group>
);

expect(onLayoutChanged).toHaveBeenCalledTimes(3);
expect(onLayoutChanged).toHaveBeenLastCalledWith({ right: 100 });

rerender(
<Group groupRef={groupRef} onLayoutChanged={onLayoutChanged}>
<Panel id="left" panelRef={panelRef} />
<Separator id="separator" />
<Panel id="right" />
</Group>
);

expect(onLayoutChanged).toHaveBeenCalledTimes(4);
expect(onLayoutChanged).toHaveBeenLastCalledWith(expectedLayout);
}

test("should update when resized via pointer", async () => {
await runTest(
async () => {
await moveSeparator(10, "separator");
},
{
left: 60,
right: 40
}
);
});

test("should update when resized via keyboard", async () => {
await runTest(
async ({ container }) => {
const separator = container.querySelector("#separator")!;
await userEvent.type(separator, " {ArrowRight}");
},
{
left: 55,
right: 45
}
);
});

test("should update when resized via Group imperative API", async () => {
await runTest(
async ({ groupRef }) => {
groupRef.current?.setLayout({
left: 75,
right: 25
});
},
{
left: 75,
right: 25
}
);
});

test("should update when resized via Panel imperative API", async () => {
await runTest(
async ({ panelRef }) => {
panelRef.current?.resize(35);
},
{
left: 35,
right: 65
}
);
});
});

describe("defaultLayout", () => {
test("should be ignored if it does not match Panel ids", () => {
const onLayoutChange = vi.fn();
Expand Down Expand Up @@ -260,7 +386,7 @@ describe("Group", () => {
});
});

test("should note require multiple render passes", () => {
test("should not require multiple render passes", () => {
setElementBoundsFunction((element) => {
if (element.hasAttribute("data-panel")) {
return new DOMRect(0, 0, 50, 50);
Expand Down Expand Up @@ -642,6 +768,54 @@ describe("Group", () => {
expect(onLayoutChange).not.toHaveBeenCalled();
expect(onLayoutChanged).not.toHaveBeenCalled();
});

test("should be called in response to imperative API", async () => {
const onLayoutChange = vi.fn();
const onLayoutChanged = vi.fn();

const groupRef = createRef<GroupImperativeHandle>();

const { rerender } = render(
<Group
groupRef={groupRef}
onLayoutChange={onLayoutChange}
onLayoutChanged={onLayoutChanged}
>
<Panel id="a" />
<Panel id="b" />
</Group>
);

onLayoutChange.mockReset();
onLayoutChanged.mockReset();

groupRef.current?.setLayout({ a: 25, b: 75 });

expect(onLayoutChange).toHaveBeenCalledTimes(1);
expect(onLayoutChange).toHaveBeenCalledWith({
a: 25,
b: 75
});

expect(onLayoutChanged).toHaveBeenCalledTimes(1);
expect(onLayoutChanged).toHaveBeenCalledWith({
a: 25,
b: 75
});

rerender(
<Group
onLayoutChange={onLayoutChange}
onLayoutChanged={onLayoutChanged}
>
<Panel id="a" />
<Panel id="b" />
</Group>
);

expect(onLayoutChange).toHaveBeenCalledTimes(1);
expect(onLayoutChanged).toHaveBeenCalledTimes(1);
});
});

describe("invariants", () => {
Expand Down
5 changes: 5 additions & 0 deletions lib/components/group/Group.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -305,6 +305,11 @@ export function Group({
return;
}

// Save the layout to in-memory cache so it persists when panel configuration changes
// This improves UX for conditionally rendered panels without requiring defaultLayout
const panelIdsKey = group.panels.map(({ id }) => id).join(",");
group.inMemoryLayouts[panelIdsKey] = layout;

const { interactionState } = read();
const isCompleted = interactionState.state !== "active";

Expand Down
5 changes: 4 additions & 1 deletion lib/global/cursor/updateCursorStyle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,10 @@ export function updateCursorStyle(ownerDocument: Document) {
if (styleSheet === undefined) {
styleSheet = new ownerDocument.defaultView.CSSStyleSheet();

ownerDocument.adoptedStyleSheets.push(styleSheet);
// adoptedStyleSheets is undefined in jsdom
if (ownerDocument.adoptedStyleSheets) {
ownerDocument.adoptedStyleSheets.push(styleSheet);
}
}

const { cursorFlags, interactionState } = read();
Expand Down
18 changes: 0 additions & 18 deletions lib/global/utils/getImperativeGroupMethods.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,24 +138,6 @@ describe("getImperativeGroupMethods", () => {
`);
});

test("persists layout to inMemoryLayouts cache", () => {
const { api, group } = init([
{ defaultSize: 200, minSize: 100 },
{ defaultSize: 800 }
]);

api.setLayout({
"A-1": 30,
"A-2": 70
});

const panelIdsKey = "A-1,A-2";
expect(group.inMemoryLayouts[panelIdsKey]).toEqual({
"A-1": 30,
"A-2": 70
});
});

test("allows disabled panels to be resized", () => {
const { api } = init([
{ defaultSize: 200, disabled: true, minSize: 100 },
Expand Down
4 changes: 0 additions & 4 deletions lib/global/utils/getImperativeGroupMethods.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,10 +67,6 @@ export function getImperativeGroupMethods({
separatorToPanels
})
}));

// Save the layout to in-memory cache so it persists when panel configuration changes
const panelIdsKey = group.panels.map(({ id }) => id).join(",");
group.inMemoryLayouts[panelIdsKey] = nextLayout;
}

return nextLayout;
Expand Down
5 changes: 0 additions & 5 deletions lib/global/utils/updateActiveHitRegion.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,11 +113,6 @@ export function updateActiveHitRegions({
layout: nextLayout,
separatorToPanels
});

// Save the most recent layout for this group of panels in-memory
// so that layouts will be remembered between different sets of conditionally rendered panels
const panelIdsKey = current.group.panels.map(({ id }) => id).join(",");
current.group.inMemoryLayouts[panelIdsKey] = nextLayout;
}
}
});
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "react-resizable-panels",
"version": "4.6.4",
"version": "4.6.5",
"type": "module",
"author": "Brian Vaughn <brian.david.vaughn@gmail.com> (https://github.com/bvaughn/)",
"contributors": [
Expand Down
7 changes: 0 additions & 7 deletions vitest.setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,13 +65,6 @@ beforeAll(() => {
vi.spyOn(console, "warn").mockImplementation(() => {
throw Error("Unexpected console warning");
});

Object.defineProperty(Document.prototype, "adoptedStyleSheets", {
value: []
});
Object.defineProperty(ShadowRoot.prototype, "adoptedStyleSheets", {
value: []
});
});

afterAll(() => {
Expand Down
Loading