diff --git a/CHANGELOG.md b/CHANGELOG.md index 5b5b5551d..5ce484e0c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/lib/components/group/Group.test.tsx b/lib/components/group/Group.test.tsx index b4c59e029..b6b352ea9 100644 --- a/lib/components/group/Group.test.tsx +++ b/lib/components/group/Group.test.tsx @@ -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"; @@ -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", () => { @@ -92,6 +94,130 @@ describe("Group", () => { ); }); + describe("in-memory layout cache", () => { + async function runTest( + callback: (args: { + container: HTMLElement; + groupRef: RefObject; + panelRef: RefObject; + }) => Promise, + 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(); + const panelRef = createRef(); + + const { container, rerender } = render( + + + + + + ); + + 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( + + + + ); + + expect(onLayoutChanged).toHaveBeenCalledTimes(3); + expect(onLayoutChanged).toHaveBeenLastCalledWith({ right: 100 }); + + rerender( + + + + + + ); + + 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(); @@ -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); @@ -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(); + + const { rerender } = render( + + + + + ); + + 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( + + + + + ); + + expect(onLayoutChange).toHaveBeenCalledTimes(1); + expect(onLayoutChanged).toHaveBeenCalledTimes(1); + }); }); describe("invariants", () => { diff --git a/lib/components/group/Group.tsx b/lib/components/group/Group.tsx index 35ef37f30..17b5e2a35 100644 --- a/lib/components/group/Group.tsx +++ b/lib/components/group/Group.tsx @@ -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"; diff --git a/lib/global/cursor/updateCursorStyle.ts b/lib/global/cursor/updateCursorStyle.ts index bf9fb9f1e..30be3cb14 100644 --- a/lib/global/cursor/updateCursorStyle.ts +++ b/lib/global/cursor/updateCursorStyle.ts @@ -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(); diff --git a/lib/global/utils/getImperativeGroupMethods.test.ts b/lib/global/utils/getImperativeGroupMethods.test.ts index b1e9853cf..298e069f9 100644 --- a/lib/global/utils/getImperativeGroupMethods.test.ts +++ b/lib/global/utils/getImperativeGroupMethods.test.ts @@ -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 }, diff --git a/lib/global/utils/getImperativeGroupMethods.ts b/lib/global/utils/getImperativeGroupMethods.ts index 673853533..a294e26fe 100644 --- a/lib/global/utils/getImperativeGroupMethods.ts +++ b/lib/global/utils/getImperativeGroupMethods.ts @@ -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; diff --git a/lib/global/utils/updateActiveHitRegion.ts b/lib/global/utils/updateActiveHitRegion.ts index d077a071f..3f1a3e471 100644 --- a/lib/global/utils/updateActiveHitRegion.ts +++ b/lib/global/utils/updateActiveHitRegion.ts @@ -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; } } }); diff --git a/package.json b/package.json index 13eb26c9d..b1d86b032 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-resizable-panels", - "version": "4.6.4", + "version": "4.6.5", "type": "module", "author": "Brian Vaughn (https://github.com/bvaughn/)", "contributors": [ diff --git a/vitest.setup.ts b/vitest.setup.ts index b16b61ae7..64eebe83c 100644 --- a/vitest.setup.ts +++ b/vitest.setup.ts @@ -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(() => {