Skip to content

Panel layout gets permanently corrupted on iPadOS Safari after backgrounding the app (transient resize burst skews percentage, doesn't self-correct) #731

Description

@h4nyu

Version: react-resizable-panels@4.12.2
Platform: iPadOS Safari (reproduces both as regular Safari UA and as the "Macintosh" desktop-site UA iPadOS sometimes reports)

Summary

After a user manually resizes a Panel inside a Group (drag), backgrounding the Safari tab (e.g. switching to another app via the App Switcher) and then returning to it can leave the panel's live layout visually corrupted — a Panel with a pixel minSize ends up much larger than the user set, and does not shrink back even though the container has returned to its original width. A full page reload restores the correct layout, which confirms only the live layout state is affected, not anything persisted via useDefaultLayout/localStorage.

What we found

On-device console logging (attaching listeners for resize, orientationchange, visibilitychange, and a ResizeObserver on the Group element) showed:

  1. Right as the app starts moving to the background (well before visibilitychange fires "hidden"), a burst of resize/orientationchange events fires with transiently wrong container widths — in our case the sequence was roughly 1180px → 820px → 585px → 1180px within ~700ms (this looks like it's coming from the OS-level app-switch transition animation, not a real resize).
  2. During the transient narrow reading (e.g. 540px), a Panel with minSize={380} no longer fits at its current percentage, so the library recomputes a much larger percentage to satisfy the pixel minSize (e.g. jumping from ~49% to ~72%).
  3. Once the container returns to its real width (1180px again), that skewed percentage is not re-derived — because the default groupResizeBehavior is "preserve-relative-size", the percentage (not the pixel size) is what's preserved across the resize, so the panel stays pinned at the inflated percentage even though nothing about the current width requires it anymore.
  4. In our case this all happens before visibilitychange turns "hidden" — in one capture, the last resize event and visibilitychange:"hidden" were only ~15ms apart. We were unable to observe onLayoutChanged firing at all for this transient burst in that scenario (it's possible the notification is scheduled via requestAnimationFrame internally and never gets a chance to run before rAF is suspended for the hidden document) — so consumers can't reliably detect the corruption via the public callback API either.
  5. Once the tab becomes "visible" again, no further resize/layout event fires at all — the container's width is already correct at that point but the skewed percentage from step 3 is simply left as-is, with nothing to trigger a recompute.

We were able to reproduce this consistently by: dragging a Separator to resize a Panel with a pixel minSize, then switching to another app via iPadOS's App Switcher and back to Safari.

Minimal repro

import { Group, Panel, Separator, useDefaultLayout } from "react-resizable-panels";

function App() {
  const { defaultLayout, onLayoutChanged } = useDefaultLayout({
    id: "demo",
    panelIds: ["sidebar", "main"],
    storage: localStorage,
    onlySaveAfterUserInteractions: true,
  });

  return (
    <div style={{ display: "flex", height: "100vh" }}>
      <Group
        defaultLayout={defaultLayout}
        onLayoutChanged={onLayoutChanged}
        orientation="horizontal"
        style={{ flex: 1 }}
      >
        <Panel id="sidebar" minSize={380} defaultSize={380} style={{ background: "#eef" }}>
          sidebar
        </Panel>
        <Separator style={{ width: 8, cursor: "col-resize", background: "#ccc" }} />
        <Panel id="main" minSize={150} style={{ background: "#fee" }}>
          main
        </Panel>
      </Group>
    </div>
  );
}

Steps on an iPad (Safari):

  1. Open the page above.
  2. Drag the separator to make the sidebar noticeably narrower or wider than its default 380px.
  3. Swipe up to the App Switcher, open a different app, then switch back to Safari.
  4. The sidebar's width has changed significantly from what was set in step 2 (usually much wider); a full page reload restores the correct width.

Workaround (application level)

We worked around this by listening for document.visibilitychange turning "visible" and imperatively calling the Group's setLayout() back to the last user-set layout (tracked via onLayoutChanged's isUserInteraction flag) if it differs from the panel's current getLayout(). This isn't something library consumers should have to do, though — a library-level fix (e.g. ignoring/debouncing resize readings that are followed almost immediately by a very different one, or treating a minSize-driven percentage bump as provisional until the resize settles) would remove the need for it.

Happy to share the full console log timeline if useful.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions