diff --git a/CHANGELOG.md b/CHANGELOG.md
index b253330fe..282fa8fca 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,8 +1,9 @@
# Changelog
-## Unreleased
+## 4.5.7
- [646](https://github.com/bvaughn/react-resizable-panels/pull/646): Re-enable the collapsible `Panel` from 4.5.3 that was disabled in 4.5.6
+- [648](https://github.com/bvaughn/react-resizable-panels/pull/648): **Bugfix**: Reset `Separator` hover-state on `Document` "pointerout"
## 4.5.6
diff --git a/integrations/tests/src/components/IFrame.tsx b/integrations/tests/src/components/IFrame.tsx
new file mode 100644
index 000000000..4aa4f9744
--- /dev/null
+++ b/integrations/tests/src/components/IFrame.tsx
@@ -0,0 +1,9 @@
+import { type PropsWithChildren } from "react";
+
+export type IFrameProps = PropsWithChildren<{
+ className?: string | undefined;
+}>;
+
+export function IFrame({ className }: IFrameProps) {
+ return ;
+}
diff --git a/integrations/tests/src/utils/serializer/decode.ts b/integrations/tests/src/utils/serializer/decode.ts
index 8f6413710..7b24de032 100644
--- a/integrations/tests/src/utils/serializer/decode.ts
+++ b/integrations/tests/src/utils/serializer/decode.ts
@@ -17,12 +17,14 @@ import type {
EncodedDisplayModeToggleElement,
EncodedElement,
EncodedGroupElement,
+ EncodedIFrameElement,
EncodedPanelElement,
EncodedPopupWindowElement,
EncodedSeparatorElement,
EncodedTextElement,
TextProps
} from "./types";
+import { IFrame } from "../../components/IFrame";
type Config = {
groupProps?: Partial;
@@ -65,6 +67,10 @@ function decodeChildren(
elements.push(decodeGroup(current, config));
break;
}
+ case "IFrame": {
+ elements.push(decodeIFrame(current));
+ break;
+ }
case "Panel": {
elements.push(decodePanel(current, config));
break;
@@ -137,6 +143,13 @@ function decodeGroup(
});
}
+function decodeIFrame(json: EncodedIFrameElement): ReactElement {
+ return createElement(IFrame, {
+ key: ++key,
+ ...json.props
+ });
+}
+
function decodePanel(
json: EncodedPanelElement,
config: Config
diff --git a/integrations/tests/src/utils/serializer/encode.ts b/integrations/tests/src/utils/serializer/encode.ts
index a12f3c880..ca4fb2533 100644
--- a/integrations/tests/src/utils/serializer/encode.ts
+++ b/integrations/tests/src/utils/serializer/encode.ts
@@ -29,12 +29,14 @@ import type {
EncodedDisplayModeToggleElement,
EncodedElement,
EncodedGroupElement,
+ EncodedIFrameElement,
EncodedPanelElement,
EncodedPopupWindowElement,
EncodedSeparatorElement,
EncodedTextElement,
TextProps
} from "./types";
+import { IFrame, type IFrameProps } from "../../components/IFrame";
export function encode(element: ReactElement) {
const json = encodeChildren([element]);
@@ -72,6 +74,10 @@ function encodeChildren(children: ReactElement[]): EncodedElement[] {
elements.push(encodeGroup(current as ReactElement));
break;
}
+ case IFrame: {
+ elements.push(encodeIFrame(current as ReactElement));
+ break;
+ }
case Panel: {
elements.push(encodePanel(current as ReactElement));
break;
@@ -163,6 +169,15 @@ function encodeGroup(element: ReactElement): EncodedGroupElement {
};
}
+function encodeIFrame(
+ element: ReactElement
+): EncodedIFrameElement {
+ return {
+ props: element.props,
+ type: "IFrame"
+ };
+}
+
function encodePanel(element: ReactElement): EncodedPanelElement {
const { children, onResize: __, ...props } = element.props;
diff --git a/integrations/tests/src/utils/serializer/types.ts b/integrations/tests/src/utils/serializer/types.ts
index 6b92b32f7..4d3c32bfb 100644
--- a/integrations/tests/src/utils/serializer/types.ts
+++ b/integrations/tests/src/utils/serializer/types.ts
@@ -7,6 +7,7 @@ import type { ContainerProps } from "../../../src/components/Container";
import type { DisplayModeToggleProps } from "../../../src/components/DisplayModeToggle";
import type { PopupWindowProps } from "../../../src/components/PopupWindow";
import type { ClickableProps } from "../../../src/components/Clickable";
+import type { IFrameProps } from "../../components/IFrame";
type EncodedElementWithChildren = Omit<
Props,
@@ -33,6 +34,11 @@ export interface EncodedGroupElement {
type: "Group";
}
+export interface EncodedIFrameElement {
+ props: IFrameProps;
+ type: "IFrame";
+}
+
export interface EncodedPanelElement {
props: EncodedElementWithChildren;
type: "Panel";
@@ -63,6 +69,7 @@ export type EncodedElement =
| EncodedContainerElement
| EncodedDisplayModeToggleElement
| EncodedGroupElement
+ | EncodedIFrameElement
| EncodedPanelElement
| EncodedPopupWindowElement
| EncodedSeparatorElement
diff --git a/integrations/tests/tests/pointer-interactions.spec.tsx b/integrations/tests/tests/pointer-interactions.spec.tsx
index ed8c7c678..2c612999b 100644
--- a/integrations/tests/tests/pointer-interactions.spec.tsx
+++ b/integrations/tests/tests/pointer-interactions.spec.tsx
@@ -2,6 +2,7 @@ import { expect, test } from "@playwright/test";
import { Group, Panel, Separator } from "react-resizable-panels";
import { Clickable } from "../src/components/Clickable";
import { Container } from "../src/components/Container";
+import { IFrame } from "../src/components/IFrame";
import { assertLayoutChangeCounts } from "../src/utils/assertLayoutChangeCounts";
import { calculateHitArea } from "../src/utils/calculateHitArea";
import { getCenterCoordinates } from "../src/utils/getCenterCoordinates";
@@ -532,6 +533,35 @@ test.describe("pointer interactions", () => {
}
});
});
+
+ // See github.com/bvaughn/react-resizable-panels/issues/645
+ test("should update separator state when the cursor mouses over an iframe", async ({
+ page: mainPage
+ }) => {
+ const page = await goToUrl(
+ mainPage,
+
+
+
+
+
+
+ ,
+ { usePopUpWindow }
+ );
+
+ const separator = page.getByTestId("separator");
+
+ const { x, y } = getCenterCoordinates((await separator.boundingBox())!);
+
+ await expect(separator).toHaveAttribute("data-separator", "inactive");
+
+ await page.mouse.move(x, y);
+ await expect(separator).toHaveAttribute("data-separator", "hover");
+
+ await page.mouse.move(x - 25, y);
+ await expect(separator).toHaveAttribute("data-separator", "inactive");
+ });
});
}
});
diff --git a/lib/global/event-handlers/onDocumentPointerOut.ts b/lib/global/event-handlers/onDocumentPointerOut.ts
new file mode 100644
index 000000000..3d63d66c7
--- /dev/null
+++ b/lib/global/event-handlers/onDocumentPointerOut.ts
@@ -0,0 +1,20 @@
+import { read, update } from "../mutableState";
+
+export function onDocumentPointerOut(event: PointerEvent) {
+ // For some reason, "pointerout" events don't fire if the `relatedTarget` is an iframe
+ // This can leave the `data-separator` attribute in an invalid state ("hover") which in turn might break styles
+ // The easiest fix for this case is to reset the interaction state in this specific circumstance
+ // See issues/645
+ if (event.relatedTarget instanceof HTMLIFrameElement) {
+ const { interactionState } = read();
+ switch (interactionState.state) {
+ case "hover": {
+ update({
+ interactionState: {
+ state: "inactive"
+ }
+ });
+ }
+ }
+ }
+}
diff --git a/lib/global/mountGroup.ts b/lib/global/mountGroup.ts
index d16fb8075..dea067fa3 100644
--- a/lib/global/mountGroup.ts
+++ b/lib/global/mountGroup.ts
@@ -8,6 +8,7 @@ import { onDocumentKeyDown } from "./event-handlers/onDocumentKeyDown";
import { onDocumentPointerDown } from "./event-handlers/onDocumentPointerDown";
import { onDocumentPointerLeave } from "./event-handlers/onDocumentPointerLeave";
import { onDocumentPointerMove } from "./event-handlers/onDocumentPointerMove";
+import { onDocumentPointerOut } from "./event-handlers/onDocumentPointerOut";
import { onDocumentPointerUp } from "./event-handlers/onDocumentPointerUp";
import { update, type SeparatorToPanelsMap } from "./mutableState";
import { calculateDefaultLayout } from "./utils/calculateDefaultLayout";
@@ -175,6 +176,7 @@ export function mountGroup(group: RegisteredGroup) {
ownerDocument.addEventListener("pointerdown", onDocumentPointerDown, true);
ownerDocument.addEventListener("pointerleave", onDocumentPointerLeave);
ownerDocument.addEventListener("pointermove", onDocumentPointerMove);
+ ownerDocument.addEventListener("pointerout", onDocumentPointerOut);
ownerDocument.addEventListener("pointerup", onDocumentPointerUp, true);
}
@@ -211,6 +213,7 @@ export function mountGroup(group: RegisteredGroup) {
);
ownerDocument.removeEventListener("pointerleave", onDocumentPointerLeave);
ownerDocument.removeEventListener("pointermove", onDocumentPointerMove);
+ ownerDocument.removeEventListener("pointerout", onDocumentPointerOut);
ownerDocument.removeEventListener("pointerup", onDocumentPointerUp, true);
}
diff --git a/package.json b/package.json
index ce4d8cee4..2922812be 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "react-resizable-panels",
- "version": "4.5.6",
+ "version": "4.5.7",
"type": "module",
"author": "Brian Vaughn (https://github.com/bvaughn/)",
"contributors": [