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 <help@stably.ai>

---------

Co-authored-by: Jinwoo-H <jinwoo0825@gmail.com>
Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
Neil
2026-05-30 11:51:53 -07:00
committed by GitHub
co-authored by Orca Jinwoo-H
parent ce3d2d71ae
commit 5fabcb2cb0
3 changed files with 36 additions and 5 deletions
@@ -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: {
@@ -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. */}
<div className="flex flex-col flex-1 min-w-0 min-h-0 overflow-hidden border-l border-border">
<div
ref={dragSplit.setDragRootNode}
className="flex flex-col flex-1 min-w-0 min-h-0 overflow-hidden border-l border-border"
>
<div
className="h-[4px] shrink-0 bg-card"
style={{ WebkitAppRegion: 'drag' } as React.CSSProperties}
@@ -1,7 +1,7 @@
/* oxlint-disable max-lines -- Why: the drag-split hook co-locates drop-zone
* resolution, same-group reordering, and cross-group handoff so state
* transitions stay readable in one place. */
import { useCallback, useEffect, useRef, useState } from 'react'
import { useCallback, useRef, useState } from 'react'
import {
closestCenter,
pointerWithin,
@@ -204,6 +204,7 @@ export function useTabDragSplit({
onDragOver: (event: DragOverEvent) => void
onDragStart: (event: DragStartEvent) => void
sensors: ReturnType<typeof useSensors>
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
}
}