Skip to content

Commit

Permalink
Remember most recently expanded panel size in local storage (#235)
Browse files Browse the repository at this point in the history
Resolves #234
  • Loading branch information
bvaughn authored Dec 16, 2023
1 parent 972e09e commit 00a8436
Show file tree
Hide file tree
Showing 6 changed files with 216 additions and 56 deletions.
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

1 comment on commit 00a8436

@vercel
Copy link

@vercel vercel bot commented on 00a8436 Dec 16, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.