diff --git a/CHANGELOG.md b/CHANGELOG.md
index 8de0a5785..5f502b7ea 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,10 +1,23 @@
# Changelog
-## Unreleased
+## 4.5.1
+
+- [624](https://github.com/bvaughn/react-resizable-panels/pull/624): **Bugfix**: Fallback to alternate CSS cursor styles for Safari
+
+| Safari | Chrome, Firefox
+| :--- | :---
+| `grab` | `move`
+| `col-resize` | `ew-resize`
+| `row-resize` | `ns-resize`
+
+## 4.5.0
- [616](https://github.com/bvaughn/react-resizable-panels/pull/616): Replace `Separator` and `Panel` edge hit-area padding with a minimum size threshold based on [Apple's user interface guidelines](https://developer.apple.com/design/human-interface-guidelines/accessibility). Separators that are large enough will no longer be padded; separators that are too small (or panels without separators) will more or less function like before. This should not have much of a user-facing impact other than an increase in the click target area. (Previously I was not padding enough, as per Apple's guidelines.)
-- [615](https://github.com/bvaughn/react-resizable-panels/pull/615): Double-clicking on a `Separator` resets its associated `Panel` to its default-size (see video below); double-click will have no impact on panels without default sizes
-- [618](https://github.com/bvaughn/react-resizable-panels/pull/618): Bugfix: Don't override `adoptedStyleSheets`
+- [615](https://github.com/bvaughn/react-resizable-panels/pull/615), [620](https://github.com/bvaughn/react-resizable-panels/pull/620): Double-clicking on a `Separator` resets its associated `Panel` to its default-size (see video below); double-click will have no impact on panels without default sizes
+- [622](https://github.com/bvaughn/react-resizable-panels/pull/622): **Bugfix**: Panels within vertical groups are now properly sized in Safari
+- [618](https://github.com/bvaughn/react-resizable-panels/pull/618): **Bugfix**: Don't override `adoptedStyleSheets`
+
+Demo of double-clicking on a separator:
https://github.com/user-attachments/assets/f19f6c5e-d290-455e-9bad-20e5038c3508
diff --git a/integrations/tests/tests/pointer-interactions.spec.tsx b/integrations/tests/tests/pointer-interactions.spec.tsx
index f33129c07..ed8c7c678 100644
--- a/integrations/tests/tests/pointer-interactions.spec.tsx
+++ b/integrations/tests/tests/pointer-interactions.spec.tsx
@@ -366,7 +366,7 @@ test.describe("pointer interactions", () => {
await expect(mainPage.getByText('"right": 33')).toBeVisible();
});
- test("double-clicking a separator resets panel to default size", async ({
+ test("double-clicking a separator resets primary panel to default size", async ({
page: mainPage
}) => {
const page = await goToUrl(
@@ -394,6 +394,34 @@ test.describe("pointer interactions", () => {
await expect(mainPage.getByText('"left": 30')).toBeVisible();
});
+ test("double-clicking a separator resets secondary panel to default size", async ({
+ page: mainPage
+ }) => {
+ const page = await goToUrl(
+ mainPage,
+
+
+
+
+ ,
+ { usePopUpWindow }
+ );
+
+ await assertLayoutChangeCounts(mainPage, 1);
+ await expect(mainPage.getByText('"left": 30')).toBeVisible();
+
+ await resizeHelper(page, ["left", "right"], 100, 0);
+
+ await assertLayoutChangeCounts(mainPage, 2);
+ await expect(mainPage.getByText('"left": 40')).toBeVisible();
+
+ const separator = page.getByTestId("separator");
+ await separator.dblclick();
+
+ await assertLayoutChangeCounts(mainPage, 3);
+ await expect(mainPage.getByText('"left": 30')).toBeVisible();
+ });
+
test.describe("focus", () => {
test("should update focus to the nearest separator", async ({
page: mainPage
diff --git a/lib/components/panel/Panel.tsx b/lib/components/panel/Panel.tsx
index e5d4b9dbc..8814e39d8 100644
--- a/lib/components/panel/Panel.tsx
+++ b/lib/components/panel/Panel.tsx
@@ -124,6 +124,7 @@ export function Panel({
style={{
...PROHIBITED_CSS_PROPERTIES,
+ display: "flex",
flexBasis: 0,
flexShrink: 1,
@@ -136,8 +137,8 @@ export function Panel({
diff --git a/lib/global/cursor/getCursorStyle.test.ts b/lib/global/cursor/getCursorStyle.test.ts
index 565e0204d..5a69e36d0 100644
--- a/lib/global/cursor/getCursorStyle.test.ts
+++ b/lib/global/cursor/getCursorStyle.test.ts
@@ -1,4 +1,4 @@
-import { describe, expect, test } from "vitest";
+import { beforeEach, describe, expect, test } from "vitest";
import {
CURSOR_FLAG_HORIZONTAL_MAX,
CURSOR_FLAG_HORIZONTAL_MIN,
@@ -7,6 +7,7 @@ import {
} from "../../constants";
import { mockGroup } from "../test/mockGroup";
import { getCursorStyle } from "./getCursorStyle";
+import { overrideSupportsAdvancedCursorStylesForTesting } from "./supportsAdvancedCursorStyles";
describe("getCursorStyle", () => {
const horizontalGroup = mockGroup(new DOMRect(0, 0, 100, 50));
@@ -24,128 +25,265 @@ describe("getCursorStyle", () => {
disabledGroup.addPanel(new DOMRect(0, 0, 50, 50));
disabledGroup.addPanel(new DOMRect(50, 0, 50, 50));
- describe("state: inactive", () => {
- test("should return null", () => {
- expect(
- getCursorStyle({
- cursorFlags: 0,
- groups: [horizontalGroup, verticalGroup],
- state: "inactive"
- })
- ).toBeNull();
+ describe("advanced cursor style support", () => {
+ beforeEach(() => {
+ overrideSupportsAdvancedCursorStylesForTesting(true);
});
- });
- describe("state: hover", () => {
- test("horizontal", () => {
- expect(
- getCursorStyle({
- cursorFlags: 0,
- groups: [horizontalGroup],
- state: "hover"
- })
- ).toBe("ew-resize");
+ describe("state: inactive", () => {
+ test("should return null", () => {
+ expect(
+ getCursorStyle({
+ cursorFlags: 0,
+ groups: [horizontalGroup, verticalGroup],
+ state: "inactive"
+ })
+ ).toBeNull();
+ });
});
- test("vertical", () => {
- expect(
- getCursorStyle({
- cursorFlags: 0,
- groups: [verticalGroup],
- state: "hover"
- })
- ).toBe("ns-resize");
- });
+ describe("state: hover", () => {
+ test("horizontal", () => {
+ expect(
+ getCursorStyle({
+ cursorFlags: 0,
+ groups: [horizontalGroup],
+ state: "hover"
+ })
+ ).toBe("ew-resize");
+ });
- test("horizontal and vertical", () => {
- expect(
- getCursorStyle({
- cursorFlags: 0,
- groups: [horizontalGroup, verticalGroup],
- state: "hover"
- })
- ).toBe("move");
- });
+ test("vertical", () => {
+ expect(
+ getCursorStyle({
+ cursorFlags: 0,
+ groups: [verticalGroup],
+ state: "hover"
+ })
+ ).toBe("ns-resize");
+ });
+
+ test("horizontal and vertical", () => {
+ expect(
+ getCursorStyle({
+ cursorFlags: 0,
+ groups: [horizontalGroup, verticalGroup],
+ state: "hover"
+ })
+ ).toBe("move");
+ });
+
+ test("cursor flags should be ignored", () => {
+ expect(
+ getCursorStyle({
+ cursorFlags: CURSOR_FLAG_HORIZONTAL_MAX,
+ groups: [horizontalGroup],
+ state: "hover"
+ })
+ ).toBe("ew-resize");
+ });
- test("cursor flags should be ignored", () => {
- expect(
- getCursorStyle({
- cursorFlags: CURSOR_FLAG_HORIZONTAL_MAX,
- groups: [horizontalGroup],
- state: "hover"
- })
- ).toBe("ew-resize");
+ test("disabled groups", () => {
+ expect(
+ getCursorStyle({
+ cursorFlags: 0,
+ groups: [disabledGroup],
+ state: "hover"
+ })
+ ).toBeNull();
+ });
});
- test("disabled groups", () => {
- expect(
- getCursorStyle({
- cursorFlags: 0,
- groups: [disabledGroup],
- state: "hover"
- })
- ).toBeNull();
+ describe("state: active", () => {
+ test("horizontal", () => {
+ expect(
+ getCursorStyle({
+ cursorFlags: 0,
+ groups: [horizontalGroup],
+ state: "active"
+ })
+ ).toBe("ew-resize");
+ });
+
+ test("vertical", () => {
+ expect(
+ getCursorStyle({
+ cursorFlags: 0,
+ groups: [verticalGroup],
+ state: "active"
+ })
+ ).toBe("ns-resize");
+ });
+
+ test("horizontal and vertical", () => {
+ expect(
+ getCursorStyle({
+ cursorFlags: 0,
+ groups: [horizontalGroup, verticalGroup],
+ state: "active"
+ })
+ ).toBe("move");
+ });
+
+ test.each([
+ [CURSOR_FLAG_HORIZONTAL_MIN, "e-resize"],
+ [CURSOR_FLAG_HORIZONTAL_MIN | CURSOR_FLAG_VERTICAL_MIN, "se-resize"],
+ [CURSOR_FLAG_HORIZONTAL_MIN | CURSOR_FLAG_VERTICAL_MAX, "ne-resize"],
+ [CURSOR_FLAG_HORIZONTAL_MAX, "w-resize"],
+ [CURSOR_FLAG_HORIZONTAL_MAX | CURSOR_FLAG_VERTICAL_MIN, "sw-resize"],
+ [CURSOR_FLAG_HORIZONTAL_MAX | CURSOR_FLAG_VERTICAL_MAX, "nw-resize"],
+ [CURSOR_FLAG_VERTICAL_MIN, "s-resize"],
+ [CURSOR_FLAG_VERTICAL_MAX, "n-resize"]
+ ])("cursor flags: %i -> %s", (cursorFlags, expected) => {
+ expect(
+ getCursorStyle({
+ cursorFlags,
+ groups: [horizontalGroup, verticalGroup],
+ state: "active"
+ })
+ ).toBe(expected);
+ });
+
+ test("disabled groups", () => {
+ expect(
+ getCursorStyle({
+ cursorFlags: 0,
+ groups: [disabledGroup],
+ state: "active"
+ })
+ ).toBeNull();
+ });
});
});
- describe("state: active", () => {
- test("horizontal", () => {
- expect(
- getCursorStyle({
- cursorFlags: 0,
- groups: [horizontalGroup],
- state: "active"
- })
- ).toBe("ew-resize");
+ describe("basic cursor style support", () => {
+ beforeEach(() => {
+ overrideSupportsAdvancedCursorStylesForTesting(false);
});
- test("vertical", () => {
- expect(
- getCursorStyle({
- cursorFlags: 0,
- groups: [verticalGroup],
- state: "active"
- })
- ).toBe("ns-resize");
+ describe("state: inactive", () => {
+ test("should return null", () => {
+ expect(
+ getCursorStyle({
+ cursorFlags: 0,
+ groups: [horizontalGroup, verticalGroup],
+ state: "inactive"
+ })
+ ).toBeNull();
+ });
});
- test("horizontal and vertical", () => {
- expect(
- getCursorStyle({
- cursorFlags: 0,
- groups: [horizontalGroup, verticalGroup],
- state: "active"
- })
- ).toBe("move");
- });
+ describe("state: hover", () => {
+ test("horizontal", () => {
+ expect(
+ getCursorStyle({
+ cursorFlags: 0,
+ groups: [horizontalGroup],
+ state: "hover"
+ })
+ ).toBe("col-resize");
+ });
+
+ test("vertical", () => {
+ expect(
+ getCursorStyle({
+ cursorFlags: 0,
+ groups: [verticalGroup],
+ state: "hover"
+ })
+ ).toBe("row-resize");
+ });
+
+ test("horizontal and vertical", () => {
+ expect(
+ getCursorStyle({
+ cursorFlags: 0,
+ groups: [horizontalGroup, verticalGroup],
+ state: "hover"
+ })
+ ).toBe("grab");
+ });
+
+ test("cursor flags should be ignored", () => {
+ expect(
+ getCursorStyle({
+ cursorFlags: CURSOR_FLAG_HORIZONTAL_MAX,
+ groups: [horizontalGroup],
+ state: "hover"
+ })
+ ).toBe("col-resize");
+ });
- test.each([
- [CURSOR_FLAG_HORIZONTAL_MIN, "e-resize"],
- [CURSOR_FLAG_HORIZONTAL_MIN | CURSOR_FLAG_VERTICAL_MIN, "se-resize"],
- [CURSOR_FLAG_HORIZONTAL_MIN | CURSOR_FLAG_VERTICAL_MAX, "ne-resize"],
- [CURSOR_FLAG_HORIZONTAL_MAX, "w-resize"],
- [CURSOR_FLAG_HORIZONTAL_MAX | CURSOR_FLAG_VERTICAL_MIN, "sw-resize"],
- [CURSOR_FLAG_HORIZONTAL_MAX | CURSOR_FLAG_VERTICAL_MAX, "nw-resize"],
- [CURSOR_FLAG_VERTICAL_MIN, "s-resize"],
- [CURSOR_FLAG_VERTICAL_MAX, "n-resize"]
- ])("cursor flags: %i -> %s", (cursorFlags, expected) => {
- expect(
- getCursorStyle({
- cursorFlags,
- groups: [horizontalGroup, verticalGroup],
- state: "active"
- })
- ).toBe(expected);
+ test("disabled groups", () => {
+ expect(
+ getCursorStyle({
+ cursorFlags: 0,
+ groups: [disabledGroup],
+ state: "hover"
+ })
+ ).toBeNull();
+ });
});
- test("disabled groups", () => {
- expect(
- getCursorStyle({
- cursorFlags: 0,
- groups: [disabledGroup],
- state: "active"
- })
- ).toBeNull();
+ describe("state: active", () => {
+ test("horizontal", () => {
+ expect(
+ getCursorStyle({
+ cursorFlags: 0,
+ groups: [horizontalGroup],
+ state: "active"
+ })
+ ).toBe("col-resize");
+ });
+
+ test("vertical", () => {
+ expect(
+ getCursorStyle({
+ cursorFlags: 0,
+ groups: [verticalGroup],
+ state: "active"
+ })
+ ).toBe("row-resize");
+ });
+
+ test("horizontal and vertical", () => {
+ expect(
+ getCursorStyle({
+ cursorFlags: 0,
+ groups: [horizontalGroup, verticalGroup],
+ state: "active"
+ })
+ ).toBe("grab");
+ });
+
+ test.each([
+ [CURSOR_FLAG_HORIZONTAL_MIN, "grab"],
+ [CURSOR_FLAG_HORIZONTAL_MIN | CURSOR_FLAG_VERTICAL_MIN, "grab"],
+ [CURSOR_FLAG_HORIZONTAL_MIN | CURSOR_FLAG_VERTICAL_MAX, "grab"],
+ [CURSOR_FLAG_HORIZONTAL_MAX, "grab"],
+ [CURSOR_FLAG_HORIZONTAL_MAX | CURSOR_FLAG_VERTICAL_MIN, "grab"],
+ [CURSOR_FLAG_HORIZONTAL_MAX | CURSOR_FLAG_VERTICAL_MAX, "grab"],
+ [CURSOR_FLAG_VERTICAL_MIN, "grab"],
+ [CURSOR_FLAG_VERTICAL_MAX, "grab"]
+ ])("cursor flags: %i -> %s", (cursorFlags, expected) => {
+ expect(
+ getCursorStyle({
+ cursorFlags,
+ groups: [horizontalGroup, verticalGroup],
+ state: "active"
+ })
+ ).toBe(expected);
+ });
+
+ test("disabled groups", () => {
+ expect(
+ getCursorStyle({
+ cursorFlags: 0,
+ groups: [disabledGroup],
+ state: "active"
+ })
+ ).toBeNull();
+ });
});
});
});
diff --git a/lib/global/cursor/getCursorStyle.ts b/lib/global/cursor/getCursorStyle.ts
index f7db51169..d7ecfce23 100644
--- a/lib/global/cursor/getCursorStyle.ts
+++ b/lib/global/cursor/getCursorStyle.ts
@@ -7,6 +7,7 @@ import {
CURSOR_FLAG_VERTICAL_MIN
} from "../../constants";
import type { InteractionState } from "../types";
+import { supportsAdvancedCursorStyles } from "./supportsAdvancedCursorStyles";
export function getCursorStyle({
cursorFlags,
@@ -48,43 +49,57 @@ export function getCursorStyle({
switch (state) {
case "active": {
- const horizontalMin = (cursorFlags & CURSOR_FLAG_HORIZONTAL_MIN) !== 0;
- const horizontalMax = (cursorFlags & CURSOR_FLAG_HORIZONTAL_MAX) !== 0;
- const verticalMin = (cursorFlags & CURSOR_FLAG_VERTICAL_MIN) !== 0;
- const verticalMax = (cursorFlags & CURSOR_FLAG_VERTICAL_MAX) !== 0;
-
if (cursorFlags) {
- if (horizontalMin) {
- if (verticalMin) {
- return "se-resize";
- } else if (verticalMax) {
- return "ne-resize";
- } else {
- return "e-resize";
- }
- } else if (horizontalMax) {
- if (verticalMin) {
- return "sw-resize";
+ if (supportsAdvancedCursorStyles()) {
+ const horizontalMin =
+ (cursorFlags & CURSOR_FLAG_HORIZONTAL_MIN) !== 0;
+ const horizontalMax =
+ (cursorFlags & CURSOR_FLAG_HORIZONTAL_MAX) !== 0;
+ const verticalMin = (cursorFlags & CURSOR_FLAG_VERTICAL_MIN) !== 0;
+ const verticalMax = (cursorFlags & CURSOR_FLAG_VERTICAL_MAX) !== 0;
+
+ if (horizontalMin) {
+ if (verticalMin) {
+ return "se-resize";
+ } else if (verticalMax) {
+ return "ne-resize";
+ } else {
+ return "e-resize";
+ }
+ } else if (horizontalMax) {
+ if (verticalMin) {
+ return "sw-resize";
+ } else if (verticalMax) {
+ return "nw-resize";
+ } else {
+ return "w-resize";
+ }
+ } else if (verticalMin) {
+ return "s-resize";
} else if (verticalMax) {
- return "nw-resize";
- } else {
- return "w-resize";
+ return "n-resize";
}
- } else if (verticalMin) {
- return "s-resize";
- } else if (verticalMax) {
- return "n-resize";
}
}
break;
}
}
- if (horizontalCount > 0 && verticalCount > 0) {
- return "move";
- } else if (horizontalCount > 0) {
- return "ew-resize";
+ if (supportsAdvancedCursorStyles()) {
+ if (horizontalCount > 0 && verticalCount > 0) {
+ return "move";
+ } else if (horizontalCount > 0) {
+ return "ew-resize";
+ } else {
+ return "ns-resize";
+ }
} else {
- return "ns-resize";
+ if (horizontalCount > 0 && verticalCount > 0) {
+ return "grab";
+ } else if (horizontalCount > 0) {
+ return "col-resize";
+ } else {
+ return "row-resize";
+ }
}
}
diff --git a/lib/global/cursor/supportsAdvancedCursorStyles.ts b/lib/global/cursor/supportsAdvancedCursorStyles.ts
new file mode 100644
index 000000000..09c779e42
--- /dev/null
+++ b/lib/global/cursor/supportsAdvancedCursorStyles.ts
@@ -0,0 +1,27 @@
+let cached: boolean | undefined = undefined;
+
+export function overrideSupportsAdvancedCursorStylesForTesting(
+ override: boolean
+) {
+ cached = override;
+}
+
+/**
+ * Caches and returns if advanced cursor CSS styles are supported.
+ */
+export function supportsAdvancedCursorStyles(): boolean {
+ if (cached === undefined) {
+ cached = false;
+
+ if (typeof window !== "undefined") {
+ if (
+ window.navigator.userAgent.includes("Chrome") ||
+ window.navigator.userAgent.includes("Firefox")
+ ) {
+ cached = true;
+ }
+ }
+ }
+
+ return cached;
+}
diff --git a/lib/global/cursor/updateCursorStyle.ts b/lib/global/cursor/updateCursorStyle.ts
index 84c8fc7d8..0c9671b13 100644
--- a/lib/global/cursor/updateCursorStyle.ts
+++ b/lib/global/cursor/updateCursorStyle.ts
@@ -38,7 +38,7 @@ export function updateCursorStyle(ownerDocument: Document) {
state: interactionState.state
});
- const nextStyle = `*{cursor: ${cursorStyle} !important; ${interactionState.state === "active" ? "touch-action: none;" : ""} }`;
+ const nextStyle = `*, *:hover {cursor: ${cursorStyle} !important; ${interactionState.state === "active" ? "touch-action: none;" : ""} }`;
if (prevStyle === nextStyle) {
return;
}
diff --git a/lib/global/event-handlers/onDocumentDoubleClick.ts b/lib/global/event-handlers/onDocumentDoubleClick.ts
index ea590aee3..744fc682d 100644
--- a/lib/global/event-handlers/onDocumentDoubleClick.ts
+++ b/lib/global/event-handlers/onDocumentDoubleClick.ts
@@ -23,14 +23,17 @@ export function onDocumentDoubleClick(event: MouseEvent) {
});
if (current.separator) {
- const primaryPanel = current.panels[0];
- if (primaryPanel.panelConstraints.defaultSize !== undefined) {
+ const panelWithDefaultSize = current.panels.find(
+ (panel) => panel.panelConstraints.defaultSize !== undefined
+ );
+ if (panelWithDefaultSize) {
+ const defaultSize = panelWithDefaultSize.panelConstraints.defaultSize;
const api = getImperativePanelMethods({
groupId: current.group.id,
- panelId: primaryPanel.id
+ panelId: panelWithDefaultSize.id
});
- if (api) {
- api.resize(primaryPanel.panelConstraints.defaultSize);
+ if (api && defaultSize !== undefined) {
+ api.resize(defaultSize);
event.preventDefault();
}
diff --git a/package.json b/package.json
index 4af327781..a7e4295b9 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "react-resizable-panels",
- "version": "4.4.2",
+ "version": "4.5.1",
"type": "module",
"author": "Brian Vaughn
(https://github.com/bvaughn/)",
"contributors": [
diff --git a/src/routes/CustomStylesRoute.tsx b/src/routes/CustomStylesRoute.tsx
index f1556e9d7..70988e427 100644
--- a/src/routes/CustomStylesRoute.tsx
+++ b/src/routes/CustomStylesRoute.tsx
@@ -36,7 +36,7 @@ export default function CustomStylesRoute() {
left
- right
+ right
);