Skip to content

SidebarSplitView: native content-list column + fix divider-drag tearing - #1

Open
sethwebster wants to merge 2 commits into
mainfrom
seth/split-view-content-list
Open

sethwebster wants to merge 2 commits into
mainfrom
seth/split-view-content-list

Conversation

@sethwebster

Copy link
Copy Markdown
Collaborator

Problem

Dragging a SidebarSplitView divider janks badly when an app needs three panes (mailboxes | messages | message). The only option today is nesting a second SidebarSplitView in content, and that tears on every drag:

  • Content lags native panes. Pane frames move natively, but pane content is sized from paneMetrics after onSplitViewDidResize round-trips through React. A nested split only learns its own frame after the outer split's round trip, then starts its own, so its panes lag twice. Result: gaps of window material between panes and a list pane that intermittently balloons to full width.
  • Dragged dividers snap back. Every layout pass (layoutSplitView → applyDividerPositionForBounds) re-applies the preferred width, so a user-dragged width is lost on the next layout (e.g. window resize).
  • Transient widths reach JS. adjustSubviews scales panes proportionally mid-layout and posts NSSplitViewDidResizeSubviewsNotification, publishing sizes that are immediately corrected.
  • White divider in dark mode. Pane containers are transparent, so a visible divider showed the window's white background. This never surfaced with two panes because a sidebar split draws no visible divider.

Changes

  • list pane (optional): AppKit's contentList split item between sidebar and content, with listWidth / listMinWidth. onResize gains listWidth, listHeight, listX (zero without a list). The list mounts as child index 2, so existing two-pane mounts (0 sidebar, 1 content) are unchanged.
  • Holding priorities: sidebar and list keep their widths on window resize; content absorbs the change.
  • Divider ownership: preferred widths seed the dividers on first layout and again only when sidebarWidth, listWidth, list presence or sidebarCollapsed change.
  • One event per layout: our own layout pass suppresses resize notifications and publishes final sizes once; the proportional adjustSubviews call is removed.
  • Opaque panes / divider: list and content containers fill with windowBackgroundColor (resolved per appearance via updateLayer), and the divider paints separatorColor over it. The sidebar container stays clear so its material shows.
  • Docs: docs/ui.md covers the list pane and divider behavior.

Verification

  • bun install --frozen-lockfile, bun run typecheck, bun run test (Node 24.21): 724 passed, 1 skipped. New test covers list props, mount order and list metrics.
  • Native: compiled and run in a consumer app's custom dev build (macOS, Apple Silicon, Expo Desktop 1.0.0).
  • Drag measurement: a scripted 120 Hz divider drag recorded at 120 fps in a Mail-style app.
    • Before (nested splits): constant gaps and a flapping list pane, visible in most mid-drag frames.
    • After (single split with list): 0 of 476 frames showed a seam between list and content. The list holds its width and the divider stays dark.

Not addressed

  • Pane content still follows a native resize by one React commit, so centered content trails slightly during a drag. Removing that needs pane sizing through Fabric shadow-node state instead of paneMetrics, which is a larger change.
  • The window-level startup placeholder (RNSidebarSplitViewStartupView) still draws two panes; with a list, the middle column appears once React mounts.
  • Windows: list renders in the existing non-native fallback row only.

@sethwebster

Copy link
Copy Markdown
Collaborator Author

CI note: the failing check, Source and package gates (ubuntu-latest), is pre-existing and not caused by this PR.

  • The same Linux job fails on main, on every one of the last 5 pushes, including 5aa7d34 (this branch's base): run 37673152340.
  • The set of failing tests is identical between that main run and this PR's run (37973799512). None of them touch the split view: helpers, release, windows, local-discovery and native-graph tests.
  • The cause is host assumptions in those tests: /usr/bin/lockf (macOS-only), helper bundles resolving the host as macos-x64, and expectations of macOS runtime paths.
  • Source and package gates (macos-latest), which exercises this change, passes.

The Linux test failures will be fixed in a separate PR.

@sethwebster

Copy link
Copy Markdown
Collaborator Author

The Source and package gates (ubuntu-latest) failures here also fail on main and don't come from this PR. Fixed in #2: the test fixtures assumed an arm64 host, and the Runner install test calls /usr/bin/lockf, which only exists on macOS. Rebase onto main once #2 merges and that job should go green.

…earing

- Add optional `list` pane (AppKit contentList item) between sidebar and
  content, with listWidth/listMinWidth and list metrics on onResize. Replaces
  nesting a second SidebarSplitView, whose frame lags a React commit behind
  the outer split and tears on every divider drag.
- Seed divider positions from preferred widths only on first layout or when
  those props change; layout no longer snaps a dragged divider back.
- Publish one resize event per native layout pass instead of transient
  mid-layout widths; drop the proportional adjustSubviews call.
- Paint list/content panes opaque (windowBackgroundColor) and draw the
  divider with separatorColor, so a one-commit content lag and the divider no
  longer show the window's white background (bright stripe in dark mode).
@sethwebster
sethwebster force-pushed the seth/split-view-content-list branch from ae8124d to 7298bba Compare October 9, 2026 19:21
- Claim hit testing while a divider is tracked so panes sliding under a
  still cursor don't flicker hover states (and start hover animations)
  every frame of the drag.
- Pin the content pane's React content to the window edge while a divider
  drags (bounds offset, re-applied after React commits) so right-aligned
  content no longer shakes while React layout catches up; window resizes
  keep it pinned left.
- Clip the content pane's React wrapper.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant