Skip to content

Commit

Permalink
Handle pointer "up" events outside of a cross origin iframe (#374)
Browse files Browse the repository at this point in the history
Fixes #340; alternative for #373
  • Loading branch information
bvaughn authored Jul 18, 2024
1 parent 0586271 commit 4576d36
Show file tree
Hide file tree
Showing 11 changed files with 133 additions and 25 deletions.
3 changes: 2 additions & 1 deletion packages/react-resizable-panels-website/playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ const config: PlaywrightTestConfig = {
reuseExistingServer: true,
url: "http://localhost:1234",
},
timeout: 60_000,
};

if (process.env.DEBUG) {
Expand All @@ -23,7 +24,7 @@ if (process.env.DEBUG) {
headless: false,

launchOptions: {
slowMo: DEBUG ? 50 : undefined,
// slowMo: DEBUG ? 250 : undefined,
},
};
}
Expand Down
31 changes: 20 additions & 11 deletions packages/react-resizable-panels-website/src/routes/iframe/index.tsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,32 @@
import { useState } from "react";
import { useMemo, useSyncExternalStore } from "react";
import styles from "./styles.module.css";

export default function Page() {
const [url] = useState(() => {
const url = new URL(
typeof window !== undefined ? window.location.href : ""
);
const urlString = useSyncExternalStore(
function subscribe(onChange) {
window.addEventListener("navigate", onChange);
return function unsubscribe() {
window.removeEventListener("navigate", onChange);
};
},
function read() {
return window.location.href;
}
);

return `${url.origin}/__e2e/?urlPanelGroup=${url.searchParams.get(
"urlPanelGroup"
)}`;
});
const url = useMemo(() => new URL(urlString), [urlString]);

return (
<div className={styles.Root}>
<iframe
className={styles.IFrame}
sandbox="allow-scripts"
src={url}
id="frame"
sandbox={
url.searchParams.has("sameOrigin") ? undefined : "allow-scripts"
}
src={`${url.origin}/__e2e/?urlPanelGroup=${url.searchParams.get(
"urlPanelGroup"
)}`}
></iframe>
</div>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
}

.IFrame {
width: 400px;
width: 300px;
height: 200px;
border: none;
}
77 changes: 74 additions & 3 deletions packages/react-resizable-panels-website/tests/ResizeHandle.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ import { expect, test } from "@playwright/test";
import { createElement } from "react";
import { Panel, PanelGroup, PanelResizeHandle } from "react-resizable-panels";

import { goToUrl } from "./utils/url";
import { goToUrl, goToUrlWithIframe } from "./utils/url";
import assert from "assert";

test.describe("Resize handle", () => {
test("should set 'data-resize-handle-active' attribute when active", async ({
Expand All @@ -14,9 +15,9 @@ test.describe("Resize handle", () => {
PanelGroup,
{ direction: "horizontal" },
createElement(Panel, { minSize: 10 }),
createElement(PanelResizeHandle, { style: { height: 10, width: 10 } }),
createElement(PanelResizeHandle),
createElement(Panel, { minSize: 10 }),
createElement(PanelResizeHandle, { style: { height: 10, width: 10 } }),
createElement(PanelResizeHandle),
createElement(Panel, { minSize: 10 })
)
);
Expand Down Expand Up @@ -70,4 +71,74 @@ test.describe("Resize handle", () => {
await last.getAttribute("data-resize-handle-active")
).toBeNull();
});

test("should stop dragging if the mouse is released outside of the document/owner", async ({
page,
}) => {
for (let sameOrigin of [true, false]) {
await goToUrlWithIframe(
page,
createElement(
PanelGroup,
{ direction: "horizontal" },
createElement(Panel, { minSize: 10 }),
createElement(PanelResizeHandle),
createElement(Panel, { minSize: 10 })
),
sameOrigin
);

const iframe = page.locator("iframe").first();
const iframeBounds = await iframe.boundingBox();
assert(iframeBounds);

const panel = page.frameLocator("#frame").locator("[data-panel]").first();
await expect(await panel.getAttribute("data-panel-size")).toBe("50.0");

const handle = page
.frameLocator("#frame")
.locator("[data-panel-resize-handle-id]")
.first();
const handleBounds = await handle.boundingBox();
assert(handleBounds);

// Mouse down
await page.mouse.move(handleBounds.x, handleBounds.y);
await page.mouse.down();

// Mouse move to iframe edge (and verify resize)
await page.mouse.move(iframeBounds.x, iframeBounds.y);
await expect(await panel.getAttribute("data-panel-size")).toBe("10.0");

// Mouse move outside of iframe (and verify no resize)
await page.mouse.move(iframeBounds.x - 10, iframeBounds.y - 10);
await expect(await panel.getAttribute("data-panel-size")).toBe("10.0");

// Mouse move within frame (and verify resize)
await page.mouse.move(iframeBounds.x, iframeBounds.y);
await page.mouse.move(handleBounds.x, handleBounds.y);
await expect(await panel.getAttribute("data-panel-size")).toBe("50.0");

// Mouse move to iframe edge
await page.mouse.move(
iframeBounds.x + iframeBounds.width,
iframeBounds.y + iframeBounds.height
);
await expect(await panel.getAttribute("data-panel-size")).toBe("90.0");

// Mouse move outside of iframe and release
await page.mouse.move(
iframeBounds.x + iframeBounds.width + 10,
iframeBounds.y + iframeBounds.height + 10
);
await expect(await panel.getAttribute("data-panel-size")).toBe("90.0");
await page.mouse.up();

// Mouse move within frame (and verify no resize)
await page.mouse.move(handleBounds.x, handleBounds.y);
await expect(await panel.getAttribute("data-panel-size")).toBe("90.0");
await page.mouse.move(iframeBounds.x, iframeBounds.y);
await expect(await panel.getAttribute("data-panel-size")).toBe("90.0");
}
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ test.describe("stacking order", () => {
const pageX = dragHandleRect.x + dragHandleRect.width / 2;
const pageY = dragHandleRect.y + dragHandleRect.height / 2;

page.mouse.down();

{
page.mouse.move(pageX, pageY);

Expand Down
19 changes: 19 additions & 0 deletions packages/react-resizable-panels-website/tests/utils/url.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,25 @@ export async function goToUrl(
await page.goto(url.toString());
}

export async function goToUrlWithIframe(
page: Page,
element: ReactElement<PanelGroupProps>,
sameOrigin: boolean
) {
const encodedString = UrlPanelGroupToEncodedString(element);

const url = new URL("http://localhost:1234/__e2e/iframe");
url.searchParams.set("urlPanelGroup", encodedString);
if (sameOrigin) {
url.searchParams.set("sameOrigin", "");
}

// Uncomment when testing for easier repros
// console.log(url.toString());

await page.goto(url.toString());
}

export async function updateUrl(
page: Page,
element: ReactElement<PanelGroupProps> | null
Expand Down
3 changes: 0 additions & 3 deletions packages/react-resizable-panels/src/PanelGroup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -638,9 +638,6 @@ function PanelGroupWithForwardedRef({
keyboardResizeBy,
panelGroupElement
);
if (delta === 0) {
return;
}

// Support RTL layouts
const isHorizontal = direction === "horizontal";
Expand Down
12 changes: 10 additions & 2 deletions packages/react-resizable-panels/src/PanelResizeHandleRegistry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ export function registerResizeHandle(
};
}

function handlePointerDown(event: ResizeEvent) {
function handlePointerDown(event: PointerEvent) {
const { target } = event;
const { x, y } = getResizeEventCoordinates(event);

Expand All @@ -104,9 +104,17 @@ function handlePointerDown(event: ResizeEvent) {
}
}

function handlePointerMove(event: ResizeEvent) {
function handlePointerMove(event: PointerEvent) {
const { x, y } = getResizeEventCoordinates(event);

// Edge case (see #340)
// Detect when the pointer has been released outside an iframe on a different domain
if (event.buttons === 0) {
isPointerDown = false;

updateResizeHandlerStates("up", event);
}

if (!isPointerDown) {
const { target } = event;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,8 +71,8 @@ export function useWindowSplitterResizeHandlerBehavior({
? index - 1
: handles.length - 1
: index + 1 < handles.length
? index + 1
: 0;
? index + 1
: 0;

const nextHandle = handles[nextIndex] as HTMLElement;
nextHandle.focus();
Expand Down
1 change: 1 addition & 0 deletions packages/react-resizable-panels/src/utils/test-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export function dispatchPointerEvent(type: string, target: HTMLElement) {
bubbles: true,
clientX,
clientY,
buttons: 1,
});
Object.defineProperties(event, {
pageX: {
Expand Down
4 changes: 2 additions & 2 deletions tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@
"noUncheckedIndexedAccess": true,
"strict": true,
"typeRoots": ["node_modules/@types"],
"types": ["jest", "node"]
"types": ["jest", "node"],
},
"exclude": ["node_modules"],
"include": ["declaration.d.ts", "packages/**/*.ts", "packages/**/*.tsx"]
"include": ["declaration.d.ts", "packages/**/*.ts", "packages/**/*.tsx"],
}

0 comments on commit 4576d36

Please sign in to comment.