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 );