Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Remember most recently expanded panel size in local storage #235

Merged
merged 2 commits into from
Dec 16, 2023
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
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,7 @@ function Content({
</div>
<div className={sharedStyles.PanelGroupWrapper}>
<PanelGroup
autoSaveId="ImperativePanelApi"
className={sharedStyles.PanelGroup}
direction="horizontal"
id="imperative-Panel-api"
Expand Down
160 changes: 133 additions & 27 deletions packages/react-resizable-panels-website/tests/Storage.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,32 +3,26 @@ import { createElement } from "react";
import { Panel, PanelGroup, PanelResizeHandle } from "react-resizable-panels";

import { verifyAriaValues } from "./utils/aria";
import { imperativeCollapsePanel, imperativeExpandPanel } from "./utils/panels";
import { goToUrl } from "./utils/url";

const panelGroupABC = createElement(
PanelGroup,
{ autoSaveId: "test-group", direction: "horizontal" },
createElement(Panel, { minSize: 10, order: 1 }),
createElement(Panel, {
minSize: 10,
order: 1,
}),
createElement(PanelResizeHandle),
createElement(Panel, { minSize: 10, order: 2 }),
createElement(Panel, {
minSize: 10,
order: 2,
}),
createElement(PanelResizeHandle),
createElement(Panel, { minSize: 10, order: 3 })
);

const panelGroupBC = createElement(
PanelGroup,
{ autoSaveId: "test-group", direction: "horizontal" },
createElement(Panel, { minSize: 10, order: 2 }),
createElement(PanelResizeHandle),
createElement(Panel, { minSize: 10, order: 3 })
);

const panelGroupAB = createElement(
PanelGroup,
{ autoSaveId: "test-group", direction: "horizontal" },
createElement(Panel, { minSize: 10, order: 1 }),
createElement(PanelResizeHandle),
createElement(Panel, { minSize: 10, order: 2 })
createElement(Panel, {
minSize: 10,
order: 3,
})
);

test.describe("Storage", () => {
Expand Down Expand Up @@ -64,8 +58,7 @@ test.describe("Storage", () => {
now: 80,
});

// Wait for localStorage write debounce
await new Promise((resolve) => setTimeout(resolve, 250));
await waitForLocalStorageWrite();

// Values should be remembered after a page reload
await page.reload();
Expand All @@ -80,6 +73,22 @@ test.describe("Storage", () => {
test("should store layouts separately per panel combination", async ({
page,
}) => {
const panelGroupBC = createElement(
PanelGroup,
{ autoSaveId: "test-group", direction: "horizontal" },
createElement(Panel, { minSize: 10, order: 2 }),
createElement(PanelResizeHandle),
createElement(Panel, { minSize: 10, order: 3 })
);

const panelGroupAB = createElement(
PanelGroup,
{ autoSaveId: "test-group", direction: "horizontal" },
createElement(Panel, { minSize: 10, order: 1 }),
createElement(PanelResizeHandle),
createElement(Panel, { minSize: 10, order: 2 })
);

await goToUrl(page, panelGroupABC);

const resizeHandles = page.locator("[data-panel-resize-handle-id]");
Expand All @@ -93,8 +102,7 @@ test.describe("Storage", () => {
now: 33,
});

// Wait for localStorage write debounce
await new Promise((resolve) => setTimeout(resolve, 250));
await waitForLocalStorageWrite();

// Hide the first panel and then resize things
await goToUrl(page, panelGroupBC);
Expand All @@ -104,8 +112,7 @@ test.describe("Storage", () => {
now: 10,
});

// Wait for localStorage write debounce
await new Promise((resolve) => setTimeout(resolve, 250));
await waitForLocalStorageWrite();

// Hide the last panel and then resize things
await goToUrl(page, panelGroupAB);
Expand All @@ -115,8 +122,7 @@ test.describe("Storage", () => {
now: 90,
});

// Wait for localStorage write debounce
await new Promise((resolve) => setTimeout(resolve, 250));
await waitForLocalStorageWrite();

// Reload and verify all of the different layouts are remembered individually
await goToUrl(page, panelGroupABC);
Expand All @@ -137,5 +143,105 @@ test.describe("Storage", () => {
now: 90,
});
});

test("should remember the most recent expanded size for collapsed panels", async ({
page,
}) => {
const panelGroup = createElement(
PanelGroup,
{ autoSaveId: "test-group", direction: "horizontal" },
createElement(Panel, {
collapsible: true,
id: "left",
minSize: 10,
order: 1,
}),
createElement(PanelResizeHandle),
createElement(Panel, {
collapsible: true,
id: "middle",
minSize: 10,
order: 2,
}),
createElement(PanelResizeHandle),
createElement(Panel, {
collapsible: true,
id: "right",
minSize: 10,
order: 3,
})
);

await goToUrl(page, panelGroup);

const resizeHandles = page.locator("[data-panel-resize-handle-id]");
const first = resizeHandles.first();
const last = resizeHandles.last();

await verifyAriaValues(first, {
now: 33,
});
await verifyAriaValues(last, {
now: 33,
});

// Change panel sizes
await first.focus();
await page.keyboard.press("ArrowLeft");
await last.focus();
await page.keyboard.press("ArrowRight");

// Verify sizes
await verifyAriaValues(first, {
now: 23,
});
await verifyAriaValues(last, {
now: 53,
});

await waitForLocalStorageWrite();

// Collapse panels
await imperativeCollapsePanel(page, "left");
await imperativeCollapsePanel(page, "right");

// Verify sizes
await verifyAriaValues(first, {
now: 0,
});
await verifyAriaValues(last, {
now: 100,
});

await waitForLocalStorageWrite();

// Reload page
await page.reload();

// Verify collapsed sizes resized
await verifyAriaValues(first, {
now: 0,
});
await verifyAriaValues(last, {
now: 100,
});

// Expand panels
await imperativeExpandPanel(page, "left");
await imperativeExpandPanel(page, "right");

// Verify sizes
await verifyAriaValues(first, {
now: 23,
});
await verifyAriaValues(last, {
now: 53,
});
});
});
});

// Wait for localStorage write debounce
async function waitForLocalStorageWrite() {
await new Promise((resolve) => setTimeout(resolve, 250));
}
20 changes: 18 additions & 2 deletions packages/react-resizable-panels-website/tests/utils/panels.ts
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,22 @@ export async function dragResizeTo(
await page.mouse.up();
}

export async function imperativeCollapsePanel(page: Page, panelId: string) {
const panelIdSelect = page.locator("#panelIdSelect");
await panelIdSelect.selectOption(panelId);

const button = page.locator("#collapseButton");
await button.click();
}

export async function imperativeExpandPanel(page: Page, panelId: string) {
const panelIdSelect = page.locator("#panelIdSelect");
await panelIdSelect.selectOption(panelId);

const button = page.locator("#expandButton");
await button.click();
}

export async function imperativeResizePanel(
page: Page,
panelId: string,
Expand All @@ -161,8 +177,8 @@ export async function imperativeResizePanel(
await sizeInput.focus();
await sizeInput.fill(`${size}%`);

const resizeButton = page.locator("#resizeButton");
await resizeButton.click();
const button = page.locator("#resizeButton");
await button.click();
}

export async function imperativeResizePanelGroup(
Expand Down
6 changes: 5 additions & 1 deletion packages/react-resizable-panels/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
# Changelog

## Unreleased

- Remember most recently expanded panel size in local storage (#234)

## 1.0.2

- Change local storage key for persisted sizes to avoid restoring pixel-based sizes (see #233)
- Change local storage key for persisted sizes to avoid restoring pixel-based sizes (#233)

## 1.0.1

Expand Down
34 changes: 26 additions & 8 deletions packages/react-resizable-panels/src/PanelGroup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,10 @@ import { getResizeHandleElement } from "./utils/dom/getResizeHandleElement";
import { isKeyDown, isMouseEvent, isTouchEvent } from "./utils/events";
import { getResizeEventCursorPosition } from "./utils/getResizeEventCursorPosition";
import { initializeDefaultStorage } from "./utils/initializeDefaultStorage";
import { loadPanelLayout, savePanelGroupLayout } from "./utils/serialization";
import {
loadPanelGroupState,
savePanelGroupState,
} from "./utils/serialization";
import { validatePanelConstraints } from "./utils/validatePanelConstraints";
import { validatePanelGroupLayout } from "./utils/validatePanelGroupLayout";
import {
Expand Down Expand Up @@ -79,7 +82,7 @@ export type PanelGroupProps = Omit<HTMLAttributes<ElementType>, "id"> &
}>;

const debounceMap: {
[key: string]: typeof savePanelGroupLayout;
[key: string]: typeof savePanelGroupState;
} = {};

function PanelGroupWithForwardedRef({
Expand All @@ -102,7 +105,6 @@ function PanelGroupWithForwardedRef({

const [dragState, setDragState] = useState<DragState | null>(null);
const [layout, setLayout] = useState<number[]>([]);
const [panelDataArray, setPanelDataArray] = useState<PanelData[]>([]);

const panelIdToLastNotifiedSizeMapRef = useRef<Record<string, number>>({});
const panelSizeBeforeCollapseRef = useRef<Map<string, number>>(new Map());
Expand Down Expand Up @@ -218,16 +220,26 @@ function PanelGroupWithForwardedRef({
// Limit the frequency of localStorage updates.
if (debouncedSave == null) {
debouncedSave = debounce(
savePanelGroupLayout,
savePanelGroupState,
LOCAL_STORAGE_DEBOUNCE_INTERVAL
);

debounceMap[autoSaveId] = debouncedSave;
}

// Clone panel data array before saving since this array is mutated.
// If we don't clone, we run the risk of saving the wrong panel and layout pair.
debouncedSave(autoSaveId, [...panelDataArray], layout, storage);
// Clone mutable data before passing to the debounced function,
// else we run the risk of saving an incorrect combination of mutable and immutable values to state.
const clonedPanelDataArray = [...panelDataArray];
const clonedPanelSizesBeforeCollapse = new Map(
panelSizeBeforeCollapseRef.current
);
debouncedSave(
autoSaveId,
clonedPanelDataArray,
clonedPanelSizesBeforeCollapse,
layout,
storage
);
}
}, [autoSaveId, layout, storage]);

Expand Down Expand Up @@ -500,7 +512,13 @@ function PanelGroupWithForwardedRef({
// default size should be restored from local storage if possible.
let unsafeLayout: number[] | null = null;
if (autoSaveId) {
unsafeLayout = loadPanelLayout(autoSaveId, panelDataArray, storage);
const state = loadPanelGroupState(autoSaveId, panelDataArray, storage);
if (state) {
panelSizeBeforeCollapseRef.current = new Map(
Object.entries(state.expandToSizes)
);
unsafeLayout = state.layout;
}
}

if (unsafeLayout == null) {
Expand Down
Loading