Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions integrations/vite/src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { createRoot } from "react-dom/client";
import { BrowserRouter, Route, Routes } from "react-router";
import "./index.css";
import { Decoder } from "./routes/Decoder";
import { Visibility } from "./routes/Visibility";
import { Edges } from "./routes/Edges";
import { Encoder } from "./routes/Encoder";
import { Home } from "./routes/Home";
Expand All @@ -15,6 +16,10 @@ createRoot(document.getElementById("root")!).render(
<Route path="/e2e/decoder/:encoded" element={<Decoder />} />
<Route path="/e2e/edges" element={<Edges />} />
<Route path="/e2e/encoder" element={<Encoder />} />
<Route
path="/e2e/visibility/:mode/:default/:encoded"
element={<Visibility />}
/>
</Routes>
</BrowserRouter>
</StrictMode>
Expand Down
16 changes: 1 addition & 15 deletions integrations/vite/src/routes/Home.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,3 @@
import { Link } from "react-router";

export function Home() {
return (
<ul>
<li>
<Link to="/e2e/encoder">e2e: encoder</Link>
</li>
<li>
<Link to="/e2e/dynamic">e2e: dynamic</Link>
</li>
<li>
<Link to="/e2e/edges">e2e: edges</Link>
</li>
</ul>
);
return <div>This app exists for e2e tests</div>;
}
68 changes: 68 additions & 0 deletions integrations/vite/src/routes/Visibility.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { Activity as ReactActivity, useMemo, useState } from "react";
import type { Layout } from "react-resizable-panels";
import { useParams } from "react-router";
import { Box } from "../../../../src/components/Box";
import { decode } from "../../tests/utils/serializer/decode";
import { DebugData } from "../components/DebugData";

export function Visibility() {
const { default: defaultValue, encoded, mode } = useParams();

const [hidden, setHidden] = useState(defaultValue === "hidden");

const [state, setState] = useState<{
onLayoutCount: number;
layout: Layout;
}>({
layout: {},
onLayoutCount: 0
});

const children = useMemo(() => {
if (!encoded) {
return null;
}

const group = decode(encoded, {
groupProps: {
onLayoutChange: (layout) => {
setState((prev) => ({
onLayoutCount: prev.onLayoutCount + 1,
layout
}));
}
}
});

return group;
}, [encoded]);

return (
<Box className="p-2" direction="column" gap={2}>
<button
onClick={() => {
setHidden(!hidden);
}}
>
toggle {mode} {hidden ? "hidden" : "visible"}
{" → "}
{hidden ? "visible" : "hidden"}
</button>
{mode === "activity" ? (
<ReactActivity mode={hidden ? "hidden" : "visible"}>
{children}
</ReactActivity>
) : (
<div className={hidden ? "hidden" : "block"}>{children}</div>
)}
<Box className={"p-2"} direction="row" gap={2} wrap>
<DebugData
data={{
layout: state.layout,
onLayoutCount: state.onLayoutCount
}}
/>
</Box>
</Box>
);
}
13 changes: 11 additions & 2 deletions integrations/vite/tests/utils/goToUrl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,20 @@ import { encode } from "./serializer/encode";

export async function goToUrl(
page: Page,
element: ReactElement<GroupProps> | null
element: ReactElement<GroupProps> | null,
routeProp: string = "/e2e/decoder/"
) {
let route = routeProp;
if (route.startsWith("/")) {
route = route.substring(1);
}
if (route.endsWith("/")) {
route = route.substring(0, route.length - 1);
}

const encodedString = element ? encode(element) : "";

const url = new URL(`http://localhost:3012/e2e/decoder/${encodedString}`);
const url = new URL(`http://localhost:3012/${route}/${encodedString}`);

// Uncomment when testing for easier repro
console.log(url.toString());
Expand Down
83 changes: 83 additions & 0 deletions integrations/vite/tests/visibility.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { expect, test } from "@playwright/test";
import { Group, Panel, Separator } from "react-resizable-panels";
import { goToUrl } from "./utils/goToUrl";

test.describe("visibility", () => {
test("Activity API should defer default layout calculation until visible", async ({
page
}) => {
await goToUrl(
page,
<Group>
<Panel defaultSize="25%" id="left" minSize={150} />
<Separator />
<Panel id="right" />
</Group>,
"/e2e/visibility/activity/hidden/"
);

await expect(page.getByText('"onLayoutCount": 0')).toBeVisible();

await page.getByText("toggle activity hidden → visible").click();

await expect(page.getByText('"onLayoutCount": 1')).toBeVisible();
await expect(page.getByText("id: left")).toContainText("25%");
await expect(page.getByRole("separator")).toBeVisible();
await expect(page.getByText("id: right")).toContainText("75%");

await page.getByText("toggle activity visible → hidden").click();

await expect(page.getByText('"onLayoutCount": 1')).toBeVisible();

await page.setViewportSize({
width: 500,
height: 500
});
await page.getByText("toggle activity hidden → visible").click();

await expect(page.getByText('"onLayoutCount": 2')).toBeVisible();
await expect(page.getByText("id: left")).toContainText("33%");
await expect(page.getByRole("separator")).toBeVisible();
await expect(page.getByText("id: right")).toContainText("67%");
});

test("display:hidden should defer default layout calculation until visible", async ({
page
}) => {
await goToUrl(
page,
<Group>
<Panel defaultSize="25%" id="left" minSize={150} />
<Separator />
<Panel id="right" />
</Group>,
"/e2e/visibility/display/hidden/"
);

await expect(page.getByText('"onLayoutCount": 1')).toBeVisible();
// Actual layout values are not meaningful since the Group is not visible

await page.getByText("toggle display hidden → visible").click();

await expect(page.getByText('"onLayoutCount": 2')).toBeVisible();
await expect(page.getByText("id: left")).toContainText("25%");
await expect(page.getByRole("separator")).toBeVisible();
await expect(page.getByText("id: right")).toContainText("75%");

await page.getByText("toggle display visible → hidden").click();

await expect(page.getByText('"onLayoutCount": 2')).toBeVisible();
// Actual layout values are not meaningful since the Group is not visible

await page.setViewportSize({
width: 500,
height: 500
});
await page.getByText("toggle display hidden → visible").click();

await expect(page.getByText('"onLayoutCount": 3')).toBeVisible();
await expect(page.getByText("id: left")).toContainText("33%");
await expect(page.getByRole("separator")).toBeVisible();
await expect(page.getByText("id: right")).toContainText("67%");
});
});
113 changes: 57 additions & 56 deletions lib/components/group/Group.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -100,67 +100,68 @@ export function Group({
// Register Group and child Panels/Separators with global state
// Listen to global state for drag state related to this Group
useIsomorphicLayoutEffect(() => {
if (element !== null && panels.length > 0) {
const group: RegisteredGroup = {
defaultLayout,
disableCursor: !!disableCursor,
disabled: !!disabled,
element,
id,
inMemoryLastExpandedPanelSizes:
inMemoryLastExpandedPanelSizesRef.current,
inMemoryLayouts: inMemoryLayoutsRef.current,
orientation,
panels,
separators
};

const unmountGroup = mountGroup(group);

const globalState = read();
const match = globalState.mountedGroups.get(group);
if (match) {
setLayout(match.layout);
onLayoutChangeStable?.(match.layout);
}
if (element === null || panels.length === 0) {
return;
}

const group: RegisteredGroup = {
defaultLayout,
disableCursor: !!disableCursor,
disabled: !!disabled,
element,
id,
inMemoryLastExpandedPanelSizes: inMemoryLastExpandedPanelSizesRef.current,
inMemoryLayouts: inMemoryLayoutsRef.current,
orientation,
panels,
separators
};

const unmountGroup = mountGroup(group);

const removeInteractionStateChangeListener = eventEmitter.addListener(
"interactionStateChange",
(interactionState) => {
switch (interactionState.state) {
case "active": {
setDragActive(
interactionState.hitRegions.some(
(current) => current.group === group
)
);
break;
}
default: {
setDragActive(false);
break;
}
const globalState = read();
const match = globalState.mountedGroups.get(group);
if (match) {
setLayout(match.layout);
onLayoutChangeStable?.(match.layout);
}

const removeInteractionStateChangeListener = eventEmitter.addListener(
"interactionStateChange",
(interactionState) => {
switch (interactionState.state) {
case "active": {
setDragActive(
interactionState.hitRegions.some(
(current) => current.group === group
)
);
break;
}
}
);

const removeMountedGroupsChangeEventListener = eventEmitter.addListener(
"mountedGroupsChange",
(mountedGroups) => {
const match = mountedGroups.get(group);
if (match && match.derivedPanelConstraints.length > 0) {
setLayout(match.layout);
onLayoutChangeStable?.(match.layout);
default: {
setDragActive(false);
break;
}
}
);
}
);

const removeMountedGroupsChangeEventListener = eventEmitter.addListener(
"mountedGroupsChange",
(mountedGroups) => {
const match = mountedGroups.get(group);
if (match && match.derivedPanelConstraints.length > 0) {
setLayout(match.layout);
onLayoutChangeStable?.(match.layout);
}
}
);

return () => {
unmountGroup();
removeInteractionStateChangeListener();
removeMountedGroupsChangeEventListener();
};
}
return () => {
unmountGroup();
removeInteractionStateChangeListener();
removeMountedGroupsChangeEventListener();
};
}, [
defaultLayout,
disableCursor,
Expand Down
2 changes: 2 additions & 0 deletions lib/components/group/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,11 @@ export type RegisteredGroup = {
disabled: boolean;
element: HTMLElement;
id: string;
// TODO Move to mutable state
inMemoryLastExpandedPanelSizes: {
[panelId: string]: number;
};
// TODO Move to mutable state
inMemoryLayouts: {
[panelIds: string]: Layout;
};
Expand Down
12 changes: 12 additions & 0 deletions lib/global/dom/calculatePanelConstraints.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,18 @@ export function calculatePanelConstraints(group: RegisteredGroup) {
const { panels } = group;

const groupSize = calculateAvailableGroupSize({ group });
if (groupSize === 0) {
// Can't calculate anything meaningful if the group has a width/height of 0
// (This could indicate that it's within a hidden subtree)
return panels.map((current) => ({
collapsedSize: 0,
collapsible: current.panelConstraints.collapsible === true,
defaultSize: undefined,
minSize: 0,
maxSize: 100,
panelId: current.id
}));
}

return panels.map<PanelConstraints>((panel) => {
const { element, panelConstraints } = panel;
Expand Down
Loading
Loading