From 5fabcb2cb06637efc55e98b5a080250c6569abfb Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Sat, 30 May 2026 11:51:53 -0700 Subject: [PATCH] perf: clean tab drag passthrough cleanup from effect (#3710) * perf: clean tab drag passthrough cleanup from effect * test: cover tab drag root cleanup wiring Co-authored-by: Orca --------- Co-authored-by: Jinwoo-H Co-authored-by: Orca --- .../tab-group/TabGroupSplitLayout.test.ts | 18 +++++++++++++++++- .../tab-group/TabGroupSplitLayout.tsx | 5 ++++- .../components/tab-group/useTabDragSplit.ts | 18 +++++++++++++++--- 3 files changed, 36 insertions(+), 5 deletions(-) diff --git a/src/renderer/src/components/tab-group/TabGroupSplitLayout.test.ts b/src/renderer/src/components/tab-group/TabGroupSplitLayout.test.ts index 0bfe5f89aa2..d80b474ea9b 100644 --- a/src/renderer/src/components/tab-group/TabGroupSplitLayout.test.ts +++ b/src/renderer/src/components/tab-group/TabGroupSplitLayout.test.ts @@ -2,6 +2,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' const setTabGroupSplitRatioMock = vi.fn() const recordFeatureInteractionMock = vi.fn() +const setDragRootNodeMock = vi.fn() const useAppStoreMock = vi.fn( ( selector: (state: { @@ -37,7 +38,8 @@ vi.mock('./useTabDragSplit', () => ({ onDragMove: vi.fn(), onDragOver: vi.fn(), onDragStart: vi.fn(), - sensors: [] + sensors: [], + setDragRootNode: setDragRootNodeMock }) })) @@ -47,6 +49,7 @@ describe('TabGroupSplitLayout', () => { beforeEach(() => { setTabGroupSplitRatioMock.mockClear() recordFeatureInteractionMock.mockClear() + setDragRootNodeMock.mockClear() useAppStoreMock.mockClear() }) @@ -101,6 +104,19 @@ describe('TabGroupSplitLayout', () => { ) }) + it('wires the split layout root to drag cleanup ownership', () => { + const element = TabGroupSplitLayout({ + layout: { type: 'leaf', groupId: 'group-1' }, + worktreeId: 'wt-1', + focusedGroupId: 'group-1', + isWorktreeActive: true + }) + + const layoutWrapper = element.props.children[0] + + expect(layoutWrapper.props.ref).toBe(setDragRootNodeMock) + }) + it('only reserves top-right header space for the floating explorer toggle', () => { const element = TabGroupSplitLayout({ layout: { diff --git a/src/renderer/src/components/tab-group/TabGroupSplitLayout.tsx b/src/renderer/src/components/tab-group/TabGroupSplitLayout.tsx index 5cc911cad95..2bbf85f153a 100644 --- a/src/renderer/src/components/tab-group/TabGroupSplitLayout.tsx +++ b/src/renderer/src/components/tab-group/TabGroupSplitLayout.tsx @@ -238,7 +238,10 @@ export default function TabGroupSplitLayout({ state. The leftmost pane suppresses its own `border-l` via `touchesLeftEdge`, so the seam is always exactly 1px — previously both painted and stacked into a 2px bar below the drag strip. */} -
+
void onDragStart: (event: DragStartEvent) => void sensors: ReturnType + setDragRootNode: (node: HTMLDivElement | null) => void } { const reorderUnifiedTabs = useAppStore((state) => state.reorderUnifiedTabs) const dropUnifiedTab = useAppStore((state) => state.dropUnifiedTab) @@ -235,7 +236,17 @@ export function useTabDragSplit({ releaseWebviewDragPassthroughRef.current = acquireWebviewsDragPassthrough() }, [releaseWebviewDragPassthrough]) - useEffect(() => () => releaseWebviewDragPassthrough(), [releaseWebviewDragPassthrough]) + const setDragRootNode = useCallback( + (node: HTMLDivElement | null): void => { + if (node) { + return + } + // Why: this root owns the dnd-kit gesture that temporarily puts browser + // webviews in pointer passthrough, so root teardown must release it. + releaseWebviewDragPassthrough() + }, + [releaseWebviewDragPassthrough] + ) const clearDragState = useCallback(() => { releaseWebviewDragPassthrough() @@ -465,6 +476,7 @@ export function useTabDragSplit({ onDragMove, onDragOver, onDragStart, - sensors + sensors, + setDragRootNode } }