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
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@

## Unreleased

- Add `displayName` property to `Group`, `Panel`, and `Separator` components for better debugging experience.
- [2a6b03f](https://github.com/bvaughn/react-resizable-panels/commit/2a6b03f67d7d8fea8483a6a69bcdaebbe1b18a7a): Add `displayName` property to `Group`, `Panel`, and `Separator` components for better debugging experience.
- [577](https://github.com/bvaughn/react-resizable-panels/pull/577): `Group` handles newly registered `Panels` + `Separators` during mount so that user code can safely call imperative APIs earlier

## 4.2.0

Expand Down
60 changes: 32 additions & 28 deletions integrations/vite/tests/imperative-api-hooks.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,22 +10,24 @@ test.describe("imperative API hooks", () => {
{ useGroupRef: true },
{ useGroupCallbackRef: true }
]) {
test(
test.describe(
useGroupCallbackRef ? "useGroupCallbackRef" : "useGroupRef",
async ({ page: mainPage }) => {
await goToUrl(
mainPage,
<Group>
<Panel id="left" defaultSize="30" />
<Separator />
<Panel id="right" />
</Group>,
{ usePopUpWindow, useGroupCallbackRef, useGroupRef }
);
() => {
test("should work", async ({ page: mainPage }) => {
await goToUrl(
mainPage,
<Group>
<Panel id="left" defaultSize="30" />
<Separator />
<Panel id="right" />
</Group>,
{ usePopUpWindow, useGroupCallbackRef, useGroupRef }
);

await expect(
mainPage.getByText("imperativeGroupApiLayout")
).toContainText('"left": 30');
await expect(
mainPage.getByText("imperativeGroupApiLayout")
).toContainText('"left": 30');
});
}
);
}
Expand All @@ -34,22 +36,24 @@ test.describe("imperative API hooks", () => {
{ usePanelRef: true },
{ usePanelCallbackRef: true }
]) {
test(
test.describe(
usePanelCallbackRef ? "usePanelCallbackRef" : "usePanelRef",
async ({ page: mainPage }) => {
await goToUrl(
mainPage,
<Group>
<Panel id="left" defaultSize="30" />
<Separator />
<Panel id="right" />
</Group>,
{ usePopUpWindow, usePanelCallbackRef, usePanelRef }
);
() => {
test("should work", async ({ page: mainPage }) => {
await goToUrl(
mainPage,
<Group>
<Panel id="left" defaultSize="30" />
<Separator />
<Panel id="right" />
</Group>,
{ usePopUpWindow, usePanelCallbackRef, usePanelRef }
);

await expect(
mainPage.getByText("imperativePanelApiSize")
).toContainText('"asPercentage": 70');
await expect(
mainPage.getByText("imperativePanelApiSize")
).toContainText('"asPercentage": 70');
});
}
);
}
Expand Down
42 changes: 41 additions & 1 deletion lib/components/group/Group.test.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { render } from "@testing-library/react";
import { createRef, useEffect } from "react";
import { beforeEach, describe, expect, test, vi } from "vitest";
import { eventEmitter } from "../../global/mutableState";
import { moveSeparator } from "../../global/test/moveSeparator";
Expand All @@ -10,8 +11,8 @@ import {
import { Panel } from "../panel/Panel";
import { Separator } from "../separator/Separator";
import { Group } from "./Group";
import { createRef } from "react";
import type { GroupImperativeHandle } from "./types";
import { useGroupRef } from "./useGroupRef";

describe("Group", () => {
test("changes to defaultProps or disableCursor should not cause Group to remount", () => {
Expand Down Expand Up @@ -211,6 +212,45 @@ describe("Group", () => {
right: 65
});
});

// See github.com/bvaughn/react-resizable-panels/issues/576
test("should allow layout to be read or written on mount", () => {
setElementBoundsFunction((element) => {
if (element.hasAttribute("data-panel")) {
return new DOMRect(0, 0, 50, 50);
} else {
return new DOMRect(0, 0, 100, 50);
}
});

function Repro() {
const groupRef = useGroupRef();

useEffect(() => {
const group = groupRef.current;
assert(group);

expect(group.getLayout()).toEqual({
left: 25,
right: 75
});

// Should not throw
group.setLayout({ left: 50, right: 50 });
}, [groupRef]);

return (
<Group groupRef={groupRef}>
<Panel defaultSize="25" id="left">
Left
</Panel>
<Panel id="right">Right</Panel>
</Group>
);
}

render(<Repro />);
});
});

describe("onLayoutChange", () => {
Expand Down
63 changes: 43 additions & 20 deletions lib/components/group/Group.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { useEffect, useMemo, useRef, useState } from "react";
import { mountGroup } from "../../global/mountGroup";
import { eventEmitter, read } from "../../global/mutableState";
import { layoutsEqual } from "../../global/utils/layoutsEqual";
import { useForceUpdate } from "../../hooks/useForceUpdate";
import { useId } from "../../hooks/useId";
import { useIsomorphicLayoutEffect } from "../../hooks/useIsomorphicLayoutEffect";
import { useMergedRefs } from "../../hooks/useMergedRefs";
Expand Down Expand Up @@ -57,18 +58,22 @@ export function Group({

const id = useId(idProp);

const [dragActive, setDragActive] = useState(false);
const elementRef = useRef<HTMLDivElement | null>(null);
const [layout, setLayout] = useState<Layout>(defaultLayout ?? {});
const [panels, setPanels] = useState<RegisteredPanel[]>([]);
const [separators, setSeparators] = useState<RegisteredSeparator[]>([]);

const [dragActive, setDragActive] = useState(false);
const [layout, setLayout] = useState(defaultLayout ?? {});
const [panelOrSeparatorChangeSigil, forceUpdate] = useForceUpdate();

const inMemoryValuesRef = useRef<{
lastExpandedPanelSizes: { [panelIds: string]: number };
layouts: { [panelIds: string]: Layout };
panels: RegisteredPanel[];
separators: RegisteredSeparator[];
}>({
lastExpandedPanelSizes: {},
layouts: {}
layouts: {},
panels: [],
separators: []
});

const mergedRef = useMergedRefs(elementRef, elementRefProp);
Expand All @@ -80,23 +85,41 @@ export function Group({
id,
orientation,
registerPanel: (panel: RegisteredPanel) => {
setPanels((prev) => sortByElementOffset(orientation, [...prev, panel]));
const inMemoryValues = inMemoryValuesRef.current;
inMemoryValues.panels = sortByElementOffset(orientation, [
...inMemoryValues.panels,
panel
]);

forceUpdate();

return () => {
setPanels((prev) => prev.filter((current) => current !== panel));
inMemoryValues.panels = inMemoryValues.panels.filter(
(current) => current !== panel
);

forceUpdate();
};
},
registerSeparator: (separator: RegisteredSeparator) => {
setSeparators((prev) =>
sortByElementOffset(orientation, [...prev, separator])
);
const inMemoryValues = inMemoryValuesRef.current;
inMemoryValues.separators = sortByElementOffset(orientation, [
...inMemoryValues.separators,
separator
]);

forceUpdate();

return () => {
setSeparators((prev) =>
prev.filter((current) => current !== separator)
inMemoryValues.separators = inMemoryValues.separators.filter(
(current) => current !== separator
);

forceUpdate();
};
}
}),
[id, orientation]
[id, forceUpdate, orientation]
);

const stableProps = useStableObject({
Expand All @@ -114,6 +137,8 @@ export function Group({
return;
}

const inMemoryValues = inMemoryValuesRef.current;

const group: RegisteredGroup = {
defaultLayout: stableProps.defaultLayout,
disableCursor: !!stableProps.disableCursor,
Expand All @@ -124,8 +149,8 @@ export function Group({
inMemoryValuesRef.current.lastExpandedPanelSizes,
inMemoryLayouts: inMemoryValuesRef.current.layouts,
orientation,
panels,
separators
panels: inMemoryValues.panels,
separators: inMemoryValues.separators
};

registeredGroupRef.current = group;
Expand All @@ -138,10 +163,8 @@ export function Group({
const { defaultLayoutDeferred, derivedPanelConstraints, layout } = match;

if (!defaultLayoutDeferred && derivedPanelConstraints.length > 0) {
// This indicates that the Group has not finished mounting yet
// Likely because it has been rendered inside of a hidden DOM subtree
// Ignore layouts in this case because they will not have been validated
setLayout(layout);

onLayoutChangeStable?.(layout);
}
}
Expand Down Expand Up @@ -182,6 +205,7 @@ export function Group({
}

setLayout(layout);

onLayoutChangeStable?.(layout);
}
}
Expand All @@ -199,8 +223,7 @@ export function Group({
id,
onLayoutChangeStable,
orientation,
panels,
separators,
panelOrSeparatorChangeSigil,
stableProps
]);

Expand Down
Loading
Loading