diff --git a/apps/desktop/src/components/pane-shell/tree/renderer/tree-split-cascade-resize.test.tsx b/apps/desktop/src/components/pane-shell/tree/renderer/tree-split-cascade-resize.test.tsx index b5a77ce5fa..22d256c8b7 100644 --- a/apps/desktop/src/components/pane-shell/tree/renderer/tree-split-cascade-resize.test.tsx +++ b/apps/desktop/src/components/pane-shell/tree/renderer/tree-split-cascade-resize.test.tsx @@ -76,6 +76,13 @@ function setWidth(element: HTMLElement, width: number) { Object.defineProperty(element, 'getBoundingClientRect', { configurable: true, value: () => rect(width) }) } +function setHeight(element: HTMLElement, height: number) { + Object.defineProperty(element, 'getBoundingClientRect', { + configurable: true, + value: () => ({ ...rect(1000), bottom: height, height }) + }) +} + function row(): SplitNode { const tree = $layoutTree.get() @@ -180,4 +187,47 @@ describe('TreeSplit cascading expansion', () => { expect(row().weights[0]).toBeCloseTo(2.2) expect(row().children[3]).toMatchObject({ id: 'terminal-zone', minimized: true }) }) + it('folding a tool zone at its floor leaves no drag preview pinned on the flex sibling', () => { + markCollapsePane('terminal') + disposers.push( + registry.register({ + area: 'panes', + data: { height: '200px', placement: 'bottom' }, + id: 'terminal', + render: () => null, + title: 'Terminal' + }) + ) + + const tree = split( + 'column', + [group(['chat'], { id: 'chat-zone' }), group(['terminal'], { id: 'terminal-zone' })], + [1, 1], + 'root-column' + ) + + $layoutTree.set(tree) + + render() + + const container = document.querySelector('[data-tree-split="root-column"]')! + const [chat, terminal] = [...container.children] as HTMLElement[] + setHeight(container, 800) + setHeight(chat, 600) + setHeight(terminal, 200) + setHeight(document.querySelector('[data-tree-group="terminal-zone"]')!, 200) + + const chatFlex = chat.style.flex + const terminalSash = document.querySelectorAll('[role="separator"]')[0]! + fireEvent.pointerDown(terminalSash, { button: 0, clientY: 600, pointerId: 1, pointerType: 'mouse' }) + fireEvent.pointerMove(window, { clientY: 790, pointerId: 1, pointerType: 'mouse' }) + fireEvent.pointerUp(window, { clientY: 790, pointerId: 1, pointerType: 'mouse' }) + + // The zone folds to its rail (no sliver persisted) and the chat wrapper — + // which the commit does not re-render — is back on React's own flex, not + // the `0 1 ` pin the gesture previewed. + expect(row().children[1]).toMatchObject({ id: 'terminal-zone', minimized: true }) + expect($paneStates.get().terminal?.heightOverride).toBeUndefined() + expect(chat.style.flex).toBe(chatFlex) + }) }) diff --git a/apps/desktop/src/components/pane-shell/tree/renderer/tree-split.tsx b/apps/desktop/src/components/pane-shell/tree/renderer/tree-split.tsx index f147b855ea..f51069d6ed 100644 --- a/apps/desktop/src/components/pane-shell/tree/renderer/tree-split.tsx +++ b/apps/desktop/src/components/pane-shell/tree/renderer/tree-split.tsx @@ -387,10 +387,8 @@ export function TreeSplit({ node, root, rootRow }: { node: SplitNode; root?: boo // - every visible flex track is pinned to `0 1 `. The plan keeps // the run's total px constant, so no leftover gap can open. // - hidden (display:none) and minimized tracks are left untouched. - // - cleanup: a real drag commits the store once, and React's re-render - // rewrites the `flex` shorthand, which clears the overrides (writing - // the shorthand resets the longhands). A no-movement click restores - // the captured style attributes instead, since nothing re-renders. + // - cleanup: release restores the captured style attributes, then a + // real drag commits the store once and React re-renders from it. const previewPlan = (plan: ReturnType) => { sashTracks.forEach((track, index) => { if (!track.visible || track.minimized) { @@ -458,6 +456,14 @@ export function TreeSplit({ node, root, rootRow }: { node: SplitNode; root?: boo done = true resize.finish() + // Put every wrapper's inline style back exactly as React last wrote + // it BEFORE the store commit. React only rewrites a wrapper whose + // style prop changed; a preview pinned on a track the commit leaves + // alone (the flex run beside a zone that folded to its rail) would + // otherwise survive as a stale `flex: 0 1 ` and stop it growing. + // A no-movement click has no commit, so this is also its whole cleanup. + restoreStyles() + if (lastPlan && lastPlan.moved !== 0) { // Dragged a tool panel down to its collapsed header? Fold the zone // to its rail instead of persisting a sliver — and DON'T write the @@ -473,10 +479,6 @@ export function TreeSplit({ node, root, rootRow }: { node: SplitNode; root?: boo } else { commitPlan(lastPlan) } - } else { - // Click without movement: nothing will re-render, so put the - // wrappers' inline styles back exactly as React last wrote them. - restoreStyles() } // Geometry vars re-enable AFTER the final store commit above, so the