Skip to content

Commit 9a374c0

Browse files
committed
Improve popup support by using ownerDocument and defaultView
1 parent ca9b9b7 commit 9a374c0

14 files changed

Lines changed: 257 additions & 71 deletions

File tree

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import { useLayoutEffect, useState, type PropsWithChildren } from "react";
2+
import { createPortal } from "react-dom";
3+
4+
export function PopupWindow({
5+
children,
6+
className,
7+
height = 500,
8+
width = 500
9+
}: PropsWithChildren<{
10+
className?: string | undefined;
11+
height?: number | undefined;
12+
width?: number | undefined;
13+
}>) {
14+
const [open, setOpen] = useState(false);
15+
16+
const [container] = useState(() => {
17+
const div = document.createElement("div");
18+
if (className) {
19+
div.classList.add(...className.split(" "));
20+
}
21+
return div;
22+
});
23+
24+
useLayoutEffect(() => {
25+
if (!open) {
26+
return;
27+
}
28+
29+
const popup = window.open(
30+
"",
31+
"",
32+
`width=${width},height=${height},left=0,top=0`
33+
);
34+
if (popup) {
35+
const styleSheet = popup.document.createElement("style");
36+
for (const currentStyleSheet of document.styleSheets) {
37+
for (const currentCssRule of currentStyleSheet.cssRules) {
38+
styleSheet.textContent += `${currentCssRule.cssText}\n`;
39+
}
40+
}
41+
42+
popup.document.head.appendChild(styleSheet);
43+
popup.document.body.appendChild(container);
44+
45+
const onBeforeUnload = () => {
46+
popup.close();
47+
};
48+
49+
window.addEventListener("beforeunload", onBeforeUnload);
50+
51+
return () => {
52+
window.removeEventListener("beforeunload", onBeforeUnload);
53+
54+
popup.close();
55+
};
56+
}
57+
}, [container, height, open, width]);
58+
59+
return (
60+
<>
61+
{open && createPortal(children, container)}
62+
<button onClick={() => setOpen(!open)}>{open ? "close" : "open"}</button>
63+
</>
64+
);
65+
}

integrations/vite/tests/cursor.spec.tsx

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import { expect, test } from "@playwright/test";
22
import { Group, Panel, Separator } from "react-resizable-panels";
3-
import { goToUrl } from "./utils/goToUrl";
3+
import { PopupWindow } from "../src/components/PopupWindow";
44
import { calculateHitArea } from "./utils/calculateHitArea";
55
import { getCenterCoordinates } from "./utils/getCenterCoordinates";
6+
import { goToUrl } from "./utils/goToUrl";
67

78
test.describe("cursor", () => {
89
test("horizontal", async ({ page }) => {
@@ -201,4 +202,51 @@ test.describe("cursor", () => {
201202
await page.evaluate(() => getComputedStyle(document.body).cursor)
202203
).toBe("auto");
203204
});
205+
206+
test("should target ownerDocument to support popup windows", async ({
207+
page: mainPage
208+
}) => {
209+
await goToUrl(
210+
mainPage,
211+
<PopupWindow className="dark" height={250} width={500}>
212+
<Group className="h-full w-full" orientation="horizontal">
213+
<Panel id="left" minSize="25%" />
214+
<Separator />
215+
<Panel id="right" minSize="25%" />
216+
</Group>
217+
</PopupWindow>
218+
);
219+
220+
const popupPromise = mainPage.waitForEvent("popup");
221+
await mainPage.getByRole("button").click();
222+
const popupPage = await popupPromise;
223+
224+
const hitAreaBox = await calculateHitArea(popupPage, ["left", "right"]);
225+
const { x, y } = getCenterCoordinates(hitAreaBox);
226+
227+
expect(
228+
await popupPage.evaluate(() => getComputedStyle(document.body).cursor)
229+
).toBe("auto");
230+
231+
await popupPage.mouse.move(x, y);
232+
233+
expect(
234+
await popupPage.evaluate(() => getComputedStyle(document.body).cursor)
235+
).toBe("ew-resize");
236+
237+
await popupPage.mouse.down();
238+
await popupPage.mouse.move(50, y);
239+
await popupPage.mouse.move(25, y);
240+
241+
expect(
242+
await popupPage.evaluate(() => getComputedStyle(document.body).cursor)
243+
).toBe("e-resize");
244+
245+
await popupPage.mouse.move(950, y);
246+
await popupPage.mouse.move(975, y);
247+
248+
expect(
249+
await popupPage.evaluate(() => getComputedStyle(document.body).cursor)
250+
).toBe("w-resize");
251+
});
204252
});

integrations/vite/tests/utils/serializer/decode.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,13 @@ import type {
66
} from "react-resizable-panels";
77
import { Group } from "../../../src/components/Group";
88
import { Panel } from "../../../src/components/Panel";
9+
import { PopupWindow } from "../../../src/components/PopupWindow";
910
import { Separator } from "../../../src/components/Separator";
1011
import type {
1112
EncodedElement,
1213
EncodedGroupElement,
1314
EncodedPanelElement,
15+
EncodedPopupWindowElement,
1416
EncodedSeparatorElement,
1517
EncodedTextElement,
1618
TextProps
@@ -49,6 +51,10 @@ function decodeChildren(
4951
elements.push(decodePanel(current, config));
5052
break;
5153
}
54+
case "PopupWindow": {
55+
elements.push(decodePopupWindow(current, config));
56+
break;
57+
}
5258
case "Separator": {
5359
elements.push(decodeSeparator(current));
5460
break;
@@ -94,6 +100,20 @@ function decodePanel(
94100
});
95101
}
96102

103+
function decodePopupWindow(
104+
json: EncodedPopupWindowElement,
105+
config: Config
106+
): ReactElement<unknown> {
107+
const { children, ...props } = json.props;
108+
109+
return createElement(PopupWindow, {
110+
key: ++key,
111+
...props,
112+
...config.panelProps,
113+
children: children ? decodeChildren(children, config) : undefined
114+
});
115+
}
116+
97117
function decodeSeparator(
98118
json: EncodedSeparatorElement
99119
): ReactElement<SeparatorProps> {

integrations/vite/tests/utils/serializer/encode.ts

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { type ReactElement } from "react";
1+
import { type PropsWithChildren, type ReactElement } from "react";
22
import {
33
Group,
44
Panel,
@@ -7,12 +7,15 @@ import {
77
type PanelProps,
88
type SeparatorProps
99
} from "react-resizable-panels";
10+
import { PopupWindow } from "../../../src/components/PopupWindow";
1011
import type {
1112
EncodedElement,
1213
EncodedGroupElement,
1314
EncodedPanelElement,
15+
EncodedPopupWindowElement,
1416
EncodedSeparatorElement,
1517
EncodedTextElement,
18+
PopupWindowProps,
1619
TextProps
1720
} from "./types";
1821

@@ -40,6 +43,12 @@ function encodeChildren(children: ReactElement<unknown>[]): EncodedElement[] {
4043
elements.push(encodePanel(current as ReactElement<PanelProps>));
4144
break;
4245
}
46+
case PopupWindow: {
47+
elements.push(
48+
encodePopupWindow(current as ReactElement<PropsWithChildren>)
49+
);
50+
break;
51+
}
4352
case Separator: {
4453
elements.push(encodeSeparator(current as ReactElement<SeparatorProps>));
4554
break;
@@ -90,6 +99,24 @@ function encodePanel(element: ReactElement<PanelProps>): EncodedPanelElement {
9099
};
91100
}
92101

102+
function encodePopupWindow(
103+
element: ReactElement<PopupWindowProps>
104+
): EncodedPopupWindowElement {
105+
const { children, ...props } = element.props;
106+
107+
const encodedChildren = encodeChildren(
108+
Array.isArray(children) ? children : [children]
109+
);
110+
111+
return {
112+
props: {
113+
...props,
114+
children: encodedChildren.length > 0 ? encodedChildren : undefined
115+
},
116+
type: "PopupWindow"
117+
};
118+
}
119+
93120
function encodeSeparator(
94121
element: ReactElement<SeparatorProps>
95122
): EncodedSeparatorElement {

integrations/vite/tests/utils/serializer/types.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import type { PropsWithChildren } from "react";
12
import type {
23
GroupProps,
34
PanelProps,
@@ -19,6 +20,17 @@ export interface EncodedPanelElement {
1920
type: "Panel";
2021
}
2122

23+
export type PopupWindowProps = PropsWithChildren<{
24+
className?: string | undefined;
25+
height?: number | undefined;
26+
width?: number | undefined;
27+
}>;
28+
29+
export interface EncodedPopupWindowElement {
30+
props: EncodedElementWithChildren<PopupWindowProps>;
31+
type: "PopupWindow";
32+
}
33+
2234
export interface EncodedSeparatorElement {
2335
props: SeparatorProps;
2436
type: "Separator";
@@ -37,5 +49,6 @@ export interface EncodedTextElement {
3749
export type EncodedElement =
3850
| EncodedGroupElement
3951
| EncodedPanelElement
52+
| EncodedPopupWindowElement
4053
| EncodedSeparatorElement
4154
| EncodedTextElement;

lib/global/cursor/updateCursorStyle.ts

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,23 @@
11
import { read } from "../mutableState";
22
import { getCursorStyle } from "./getCursorStyle";
33

4-
let prevStyle: string | undefined = undefined;
5-
let styleSheet: CSSStyleSheet | undefined = undefined;
4+
const documentToStyleMap = new WeakMap<
5+
Document,
6+
{
7+
prevStyle: string | undefined;
8+
styleSheet: CSSStyleSheet;
9+
}
10+
>();
11+
12+
export function updateCursorStyle(document: Document) {
13+
if (document.defaultView === null) {
14+
return;
15+
}
16+
17+
let { prevStyle, styleSheet } = documentToStyleMap.get(document) ?? {};
618

7-
export function updateCursorStyle() {
819
if (styleSheet === undefined) {
9-
styleSheet = new CSSStyleSheet();
20+
styleSheet = new document.defaultView.CSSStyleSheet();
1021

1122
document.adoptedStyleSheets = [styleSheet];
1223
}
@@ -49,4 +60,9 @@ export function updateCursorStyle() {
4960
break;
5061
}
5162
}
63+
64+
documentToStyleMap.set(document, {
65+
prevStyle,
66+
styleSheet
67+
});
5268
}

lib/global/event-handlers/onWindowKeyDown.ts renamed to lib/global/event-handlers/onDocumentKeyDown.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { adjustLayoutForSeparator } from "../utils/adjustLayoutForSeparator";
33
import { findSeparatorGroup } from "../utils/findSeparatorGroup";
44
import { getMountedGroup } from "../utils/getMountedGroup";
55

6-
export function onWindowKeyDown(event: KeyboardEvent) {
6+
export function onDocumentKeyDown(event: KeyboardEvent) {
77
if (event.defaultPrevented) {
88
return;
99
}

lib/global/event-handlers/onWindowPointerDown.ts renamed to lib/global/event-handlers/onDocumentPointerDown.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import type { RegisteredSeparator } from "../../components/separator/types";
44
import { read, update } from "../mutableState";
55
import { findMatchingHitRegions } from "../utils/findMatchingHitRegions";
66

7-
export function onWindowPointerDown(event: PointerEvent) {
7+
export function onDocumentPointerDown(event: PointerEvent) {
88
if (event.defaultPrevented) {
99
return;
1010
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { read } from "../mutableState";
2+
import { updateActiveHitRegions } from "../utils/updateActiveHitRegion";
3+
4+
export function onDocumentPointerLeave(event: PointerEvent) {
5+
const { interactionState, mountedGroups } = read();
6+
7+
switch (interactionState.state) {
8+
case "active": {
9+
updateActiveHitRegions({
10+
document: event.currentTarget as Document,
11+
event,
12+
hitRegions: interactionState.hitRegions,
13+
initialLayoutMap: interactionState.initialLayoutMap,
14+
mountedGroups
15+
});
16+
}
17+
}
18+
}

lib/global/event-handlers/onWindowPointerMove.ts renamed to lib/global/event-handlers/onDocumentPointerMove.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { read, update } from "../mutableState";
33
import { findMatchingHitRegions } from "../utils/findMatchingHitRegions";
44
import { updateActiveHitRegions } from "../utils/updateActiveHitRegion";
55

6-
export function onWindowPointerMove(event: PointerEvent) {
6+
export function onDocumentPointerMove(event: PointerEvent) {
77
if (event.defaultPrevented) {
88
return;
99
}
@@ -16,7 +16,6 @@ export function onWindowPointerMove(event: PointerEvent) {
1616
// Detect when the pointer has been released outside an iframe on a different domain
1717
if (
1818
// Skip this check for "pointerleave" events, else Firefox triggers a false positive (see #514)
19-
event.type !== "pointerleave" &&
2019
event.buttons === 0
2120
) {
2221
update((prevState) =>
@@ -34,6 +33,7 @@ export function onWindowPointerMove(event: PointerEvent) {
3433
}
3534

3635
updateActiveHitRegions({
36+
document: event.currentTarget as Document,
3737
event,
3838
hitRegions: interactionState.hitRegions,
3939
initialLayoutMap: interactionState.initialLayoutMap,
@@ -61,7 +61,7 @@ export function onWindowPointerMove(event: PointerEvent) {
6161
});
6262
}
6363

64-
updateCursorStyle();
64+
updateCursorStyle(event.currentTarget as Document);
6565
break;
6666
}
6767
}

0 commit comments

Comments
 (0)