fix(desktop): restore drag previews before the release commit folds a zone

When a sash drag folds a tool zone to its rail, the store commit re-renders
only the wrappers whose style prop changed; the flex sibling the gesture
had pinned to `flex: 0 1 <px>` kept that inline preview and could not grow
into the freed space. Restore every captured style attribute before the
commit, on every release path.
This commit is contained in:
Teknium
2026-09-06 02:54:55 -07:00
parent e9c527eeb6
commit 817c0ce08a
2 changed files with 60 additions and 8 deletions
@@ -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(<TreeSplit node={tree} root />)
const container = document.querySelector<HTMLElement>('[data-tree-split="root-column"]')!
const [chat, terminal] = [...container.children] as HTMLElement[]
setHeight(container, 800)
setHeight(chat, 600)
setHeight(terminal, 200)
setHeight(document.querySelector<HTMLElement>('[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 <px>` 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)
})
})
@@ -387,10 +387,8 @@ export function TreeSplit({ node, root, rootRow }: { node: SplitNode; root?: boo
// - every visible flex track is pinned to `0 1 <px>`. 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<typeof planFor>) => {
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 <px>` 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