diff --git a/CHANGELOG.md b/CHANGELOG.md
index 917328d79..de12bac9a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,11 @@
# Changelog
+## Unreleased
+
+- [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
+- [654](https://github.com/bvaughn/react-resizable-panels/pull/654): **Bugfix** Imperative `Group` method `setLayout` persists layout to in-memory cache
+- [652](https://github.com/bvaughn/react-resizable-panels/pull/652): Re-enable collapsible panel bugfix after fixing another reported issue
+
## 4.5.8
- [651](https://github.com/bvaughn/react-resizable-panels/pull/651): Disabled the change to collapsible panel behavior that was originally made in [635](https://github.com/bvaughn/react-resizable-panels/pull/635) due to another reported regression
diff --git a/integrations/tests/tests/pointer-interactions.spec.tsx b/integrations/tests/tests/pointer-interactions.spec.tsx
index f314353ac..d9f87cff8 100644
--- a/integrations/tests/tests/pointer-interactions.spec.tsx
+++ b/integrations/tests/tests/pointer-interactions.spec.tsx
@@ -296,6 +296,9 @@ test.describe("pointer interactions", () => {
await expect(panel).toHaveCSS("pointer-events", "auto");
await page.mouse.down();
+ await expect(panel).toHaveCSS("pointer-events", "auto");
+
+ await page.mouse.move(x + 1, y);
await expect(panel).toHaveCSS("pointer-events", "none");
await page.mouse.up();
diff --git a/lib/components/group/Group.tsx b/lib/components/group/Group.tsx
index 8b992a137..e3df0fcda 100644
--- a/lib/components/group/Group.tsx
+++ b/lib/components/group/Group.tsx
@@ -226,10 +226,6 @@ export function Group({
if (!defaultLayoutDeferred && derivedPanelConstraints.length > 0) {
onLayoutChangeStable(layout);
onLayoutChangedStable(layout);
-
- inMemoryValues.panels.forEach((panel) => {
- panel.scheduleUpdate();
- });
}
}
@@ -241,14 +237,6 @@ export function Group({
const nextInteractionStateActive = interactionState.state === "active";
if (prevInteractionStateActive !== nextInteractionStateActive) {
prevInteractionStateActive = nextInteractionStateActive;
-
- // The only reason to schedule a re-render in response to this event type
- // is to disable pointer-events within a Panel while a drag is in progress
- // (This is done to prevent text from being selected, etc)
- // Unnecessary updates should be very fast in this case but we can still avoid them
- inMemoryValues.panels.forEach((panel) => {
- panel.scheduleUpdate();
- });
}
}
);
@@ -275,10 +263,6 @@ export function Group({
if (isCompleted) {
onLayoutChangedStable(layout);
}
-
- inMemoryValues.panels.forEach((panel) => {
- panel.scheduleUpdate();
- });
}
}
);
diff --git a/lib/components/panel/Panel.tsx b/lib/components/panel/Panel.tsx
index 1db49bc85..2dd972050 100644
--- a/lib/components/panel/Panel.tsx
+++ b/lib/components/panel/Panel.tsx
@@ -1,7 +1,6 @@
"use client";
-import { useRef, type CSSProperties } from "react";
-import { useForceUpdate } from "../../hooks/useForceUpdate";
+import { useRef, useSyncExternalStore, type CSSProperties } from "react";
import { useId } from "../../hooks/useId";
import { useIsomorphicLayoutEffect } from "../../hooks/useIsomorphicLayoutEffect";
import { useMergedRefs } from "../../hooks/useMergedRefs";
@@ -9,6 +8,7 @@ import { useStableCallback } from "../../hooks/useStableCallback";
import { useGroupContext } from "../group/useGroupContext";
import type { PanelProps, PanelSize } 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.
@@ -58,8 +58,6 @@ export function Panel({
const mergedRef = useMergedRefs(elementRef, elementRefProp);
- const [, forceUpdate] = useForceUpdate();
-
const { getPanelStyles, id: groupId, registerPanel } = useGroupContext();
const hasOnResize = onResizeUnstable !== null;
@@ -92,15 +90,13 @@ export function Panel({
defaultSize,
maxSize,
minSize
- },
- scheduleUpdate: forceUpdate
+ }
});
}
}, [
collapsedSize,
collapsible,
defaultSize,
- forceUpdate,
hasOnResize,
id,
idIsStable,
@@ -112,7 +108,20 @@ export function Panel({
usePanelImperativeHandle(id, panelRef);
- const panelStyles = getPanelStyles(groupId, id);
+ const panelStylesString = useSyncExternalStore(
+ (subscribe) => {
+ eventEmitter.addListener("mountedGroupsChange", subscribe);
+
+ return () => {
+ eventEmitter.removeListener("mountedGroupsChange", subscribe);
+ };
+ },
+
+ // useSyncExternalStore does not support a custom equality check
+ // stringify avoids re-rendering when the style value hasn't changed
+ () => JSON.stringify(getPanelStyles(groupId, id)),
+ () => JSON.stringify(getPanelStyles(groupId, id))
+ );
return (
void;
};
/**
diff --git a/lib/global/test/mockGroup.ts b/lib/global/test/mockGroup.ts
index 76f618673..ba4405780 100644
--- a/lib/global/test/mockGroup.ts
+++ b/lib/global/test/mockGroup.ts
@@ -103,8 +103,7 @@ export function mockGroup(
prevSize: undefined
},
panelConstraints: constraints,
- onResize: vi.fn(),
- scheduleUpdate: vi.fn()
+ onResize: vi.fn()
};
mockPanels.add(panel);
diff --git a/lib/global/utils/adjustLayoutByDelta.test.ts b/lib/global/utils/adjustLayoutByDelta.test.ts
index 59976e5fe..fa58ec32b 100644
--- a/lib/global/utils/adjustLayoutByDelta.test.ts
+++ b/lib/global/utils/adjustLayoutByDelta.test.ts
@@ -1,7 +1,7 @@
import { describe, expect, test } from "vitest";
-import { adjustLayoutByDelta as adjustLayoutByDeltaExternal } from "./adjustLayoutByDelta";
import type { Layout } from "../../components/group/types";
import type { PanelConstraints } from "../../components/panel/types";
+import { adjustLayoutByDelta as adjustLayoutByDeltaExternal } from "./adjustLayoutByDelta";
type Args = Parameters[0];
@@ -2013,8 +2013,7 @@ describe("adjustLayoutByDelta", () => {
).toEqual(closed);
});
- // TODO Re-enable this once issues/650 is resolved
- test.skip("open if delta is greater than minimum threshold", () => {
+ test("open if delta is greater than minimum threshold", () => {
expect(
adjustLayoutByDelta({
delta: panelId === "left" ? 6 : -6,
@@ -2026,8 +2025,7 @@ describe("adjustLayoutByDelta", () => {
).toEqual(open);
});
- // TODO Re-enable this once issues/650 is resolved
- test.skip("close if delta is less than minimum threshold", () => {
+ test("close if delta is less than minimum threshold", () => {
expect(
adjustLayoutByDelta({
delta: panelId === "left" ? 4 : -4,
@@ -2126,8 +2124,7 @@ describe("adjustLayoutByDelta", () => {
).toEqual(closed);
});
- // TODO Re-enable this once issues/650 is resolved
- test.skip("open if delta is greater than minimum threshold", () => {
+ test("open if delta is greater than minimum threshold", () => {
expect(
adjustLayoutByDelta({
delta: panelId === "left" ? 6 : -6,
@@ -2139,8 +2136,7 @@ describe("adjustLayoutByDelta", () => {
).toEqual(open);
});
- // TODO Re-enable this once issues/650 is resolved
- test.skip("close if delta is less than minimum threshold", () => {
+ test("close if delta is less than minimum threshold", () => {
expect(
adjustLayoutByDelta({
delta: panelId === "left" ? 4 : -4,
@@ -2239,8 +2235,35 @@ describe("adjustLayoutByDelta", () => {
});
});
- // TODO Re-enable this once issues/650 is resolved
- test.skip("edge case discussions/643", () => {
+ test("edge case issues/650", () => {
+ const collapsible = {
+ collapsedSize: 0,
+ collapsible: true,
+ minSize: 10
+ };
+
+ (
+ [
+ [-4, c([collapsible, {}]), l([46, 54])],
+ [-4, c([{}, collapsible]), l([46, 54])],
+ [4, c([collapsible, {}]), l([54, 46])],
+ [4, c([{}, collapsible]), l([54, 46])]
+ ] satisfies [number, PanelConstraints[], Layout][]
+ ).forEach(([delta, panelConstraints, expectedLayout]) => {
+ expect(
+ adjustLayoutByDelta({
+ delta,
+ initialLayout: l([50, 50]),
+ panelConstraints,
+ prevLayout: l([50, 50]),
+ pivotIndices: [0, 1],
+ trigger: "mouse-or-touch"
+ })
+ ).toEqual(expectedLayout);
+ });
+ });
+
+ test("edge case discussions/643", () => {
(
[
[4, l([10, 90])],
diff --git a/lib/global/utils/adjustLayoutByDelta.ts b/lib/global/utils/adjustLayoutByDelta.ts
index 553320f8d..a3c18c575 100644
--- a/lib/global/utils/adjustLayoutByDelta.ts
+++ b/lib/global/utils/adjustLayoutByDelta.ts
@@ -131,8 +131,6 @@ export function adjustLayoutByDelta({
}
break;
}
- // TODO Re-enable this once issues/650 is resolved
- /*
default: {
// If we're starting from a collapsed state, dragging past the halfway point should cause the panel to expand
// This can happen for positive or negative drags, and panels on either side of the separator can be collapsible
@@ -146,36 +144,39 @@ export function adjustLayoutByDelta({
`Panel constraints not found for index ${index}`
);
+ const prevSize = initialLayout[index];
+
const { collapsible, collapsedSize, minSize } = panelConstraints;
- if (collapsible) {
- // DEBUG.push(` -> collapsible ${isSecondPanel ? "2nd" : "1st"} panel`);
+ if (collapsible && compareLayoutNumbers(prevSize, minSize) < 0) {
+ // DEBUG.push(` -> collapsible ${delta < 0 ? "2nd" : "1st"} panel`);
if (delta > 0) {
const gapSize = minSize - collapsedSize;
- const halfwayPoint = gapSize / 2;
- // DEBUG.push(` -> halfway point: ${halfwayPoint}`);
- // DEBUG.push(` between collapsed: ${collapsedSize}`);
- // DEBUG.push(` and min: ${minSize}`);
+ const halfwayDelta = gapSize / 2;
+ // DEBUG.push(` -> halfway delta: ${halfwayDelta}`);
+ // DEBUG.push(` collapsed: ${collapsedSize}`);
+ // DEBUG.push(` min: ${minSize}`);
- if (compareLayoutNumbers(delta, gapSize) < 0) {
+ const nextSize = prevSize + delta;
+ if (compareLayoutNumbers(nextSize, minSize) < 0) {
// DEBUG.push(" -> adjusting delta");
// DEBUG.push(` from: ${delta}`);
delta =
- compareLayoutNumbers(delta, halfwayPoint) <= 0 ? 0 : gapSize;
+ compareLayoutNumbers(delta, halfwayDelta) <= 0 ? 0 : gapSize;
// DEBUG.push(` to: ${delta}`);
}
} else {
const gapSize = minSize - collapsedSize;
- const halfwayPoint = 100 - gapSize / 2;
- // DEBUG.push(` -> halfway point: ${halfwayPoint}`);
- // DEBUG.push(` between collapsed: ${100 - collapsedSize}`);
- // DEBUG.push(` and min: ${100 - minSize}`);
+ const halfwayDelta = 100 - gapSize / 2;
+ // DEBUG.push(` -> halfway delta: ${halfwayDelta}`);
+ // DEBUG.push(` collapsed: ${100 - collapsedSize}`);
+ // DEBUG.push(` min: ${100 - minSize}`);
- //if (isSecondPanel) {
- if (compareLayoutNumbers(Math.abs(delta), gapSize) < 0) {
+ const nextSize = prevSize - delta;
+ if (compareLayoutNumbers(nextSize, minSize) < 0) {
// DEBUG.push(" -> adjusting delta");
// DEBUG.push(` from: ${delta}`);
delta =
- compareLayoutNumbers(100 + delta, halfwayPoint) > 0
+ compareLayoutNumbers(100 + delta, halfwayDelta) > 0
? 0
: -gapSize;
// DEBUG.push(` to: ${delta}`);
@@ -184,7 +185,6 @@ export function adjustLayoutByDelta({
}
break;
}
- */
}
// DEBUG.push("");
}
diff --git a/lib/global/utils/getImperativeGroupMethods.test.ts b/lib/global/utils/getImperativeGroupMethods.test.ts
index 9a0d991fa..580b1c611 100644
--- a/lib/global/utils/getImperativeGroupMethods.test.ts
+++ b/lib/global/utils/getImperativeGroupMethods.test.ts
@@ -137,5 +137,23 @@ 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
+ });
+ });
});
});
diff --git a/lib/global/utils/getImperativeGroupMethods.ts b/lib/global/utils/getImperativeGroupMethods.ts
index a294e26fe..673853533 100644
--- a/lib/global/utils/getImperativeGroupMethods.ts
+++ b/lib/global/utils/getImperativeGroupMethods.ts
@@ -67,6 +67,10 @@ 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/package.json b/package.json
index feaaee3d4..c56ddb47e 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "react-resizable-panels",
- "version": "4.5.8",
+ "version": "4.5.9-alpha.0",
"type": "module",
"author": "Brian Vaughn (https://github.com/bvaughn/)",
"contributors": [
@@ -33,6 +33,8 @@
"compress:og-image": "tsx ./scripts/compress-og-image",
"e2e:install": "pnpm -C integrations/tests exec playwright install --with-deps",
"e2e:test": "pnpm -C integrations/tests run test",
+ "e2e:test:main": "pnpm -C integrations/tests run test --project=chromium",
+ "e2e:test:popup": "pnpm -C integrations/tests run test --project=chromium:popup",
"lint": "eslint .",
"prerelease": "rimraf dist && pnpm run build:lib",
"prettier": "prettier --write \"**/*.{css,html,js,json,jsx,ts,tsx}\"",