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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
3 changes: 3 additions & 0 deletions integrations/tests/tests/pointer-interactions.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
16 changes: 0 additions & 16 deletions lib/components/group/Group.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -226,10 +226,6 @@ export function Group({
if (!defaultLayoutDeferred && derivedPanelConstraints.length > 0) {
onLayoutChangeStable(layout);
onLayoutChangedStable(layout);

inMemoryValues.panels.forEach((panel) => {
panel.scheduleUpdate();
});
}
}

Expand All @@ -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();
});
}
}
);
Expand All @@ -275,10 +263,6 @@ export function Group({
if (isCompleted) {
onLayoutChangedStable(layout);
}

inMemoryValues.panels.forEach((panel) => {
panel.scheduleUpdate();
});
}
}
);
Expand Down
27 changes: 18 additions & 9 deletions lib/components/panel/Panel.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
"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";
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.
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -92,15 +90,13 @@ export function Panel({
defaultSize,
maxSize,
minSize
},
scheduleUpdate: forceUpdate
}
});
}
}, [
collapsedSize,
collapsible,
defaultSize,
forceUpdate,
hasOnResize,
id,
idIsStable,
Expand All @@ -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 (
<div
Expand All @@ -131,7 +140,7 @@ export function Panel({
// Prevent Panel content from interfering with panel size
overflow: "hidden",

...panelStyles
...JSON.parse(panelStylesString)
}}
>
<div
Expand Down
1 change: 0 additions & 1 deletion lib/components/panel/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@ export type RegisteredPanel = {
};
onResize: OnPanelResize | undefined;
panelConstraints: PanelConstraintProps;
scheduleUpdate: () => void;
};

/**
Expand Down
3 changes: 1 addition & 2 deletions lib/global/test/mockGroup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,8 +103,7 @@ export function mockGroup(
prevSize: undefined
},
panelConstraints: constraints,
onResize: vi.fn(),
scheduleUpdate: vi.fn()
onResize: vi.fn()
};

mockPanels.add(panel);
Expand Down
45 changes: 34 additions & 11 deletions lib/global/utils/adjustLayoutByDelta.test.ts
Original file line number Diff line number Diff line change
@@ -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<typeof adjustLayoutByDeltaExternal>[0];

Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -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])],
Expand Down
36 changes: 18 additions & 18 deletions lib/global/utils/adjustLayoutByDelta.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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}`);
Expand All @@ -184,7 +185,6 @@ export function adjustLayoutByDelta({
}
break;
}
*/
}
// DEBUG.push("");
}
Expand Down
18 changes: 18 additions & 0 deletions lib/global/utils/getImperativeGroupMethods.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
});
});
});
});
4 changes: 4 additions & 0 deletions lib/global/utils/getImperativeGroupMethods.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "react-resizable-panels",
"version": "4.5.8",
"version": "4.5.9-alpha.0",
"type": "module",
"author": "Brian Vaughn <brian.david.vaughn@gmail.com> (https://github.com/bvaughn/)",
"contributors": [
Expand Down Expand Up @@ -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}\"",
Expand Down
Loading