Preserve tab reordering without losing title-bar drag fallback

The recent macOS title-bar drag fix marked the entire session tab strip as a
native drag region. That restored window movement in the overlay title area,
but it also intercepted the tab component's own HTML drag events and regressed
manual tab reordering. This narrows the native drag surface to a small gutter
beside the tabs while keeping the explicit empty-space startDragging fallback,
and adds a focused regression test for drag-to-reorder.

Constraint: Must preserve window dragging from non-tab title-bar space on macOS overlay windows
Rejected: Keep the whole tab bar as a drag region | conflicts with tab-level drag-and-drop reordering
Confidence: high
Scope-risk: narrow
Reversibility: clean
Directive: Do not reapply data-tauri-drag-region to the full tab strip unless tab reordering is redesigned around a different drag mechanism
Tested: bun run test -- src/components/layout/TabBar.test.tsx; bun run lint; bun run build
Not-tested: Manual desktop runtime verification after this commit
This commit is contained in:
程序员阿江(Relakkes) 2026-04-20 20:40:39 +08:00
parent eaa4a8435a
commit 5f86585c34
2 changed files with 46 additions and 2 deletions

View File

@ -149,7 +149,8 @@ describe('TabBar', () => {
render(<TabBar />)
})
expect(screen.getByTestId('tab-bar')).toHaveAttribute('data-tauri-drag-region')
expect(screen.getByTestId('tab-bar')).not.toHaveAttribute('data-tauri-drag-region')
expect(screen.getByTestId('tab-bar-drag-gutter')).toHaveAttribute('data-tauri-drag-region')
})
it('starts dragging when clicking the empty tab-bar gutter', async () => {
@ -214,4 +215,39 @@ describe('TabBar', () => {
expect(startDraggingMock).not.toHaveBeenCalled()
})
it('reorders tabs via drag and drop', async () => {
const { TabBar } = await import('./TabBar')
const { useTabStore } = await import('../../stores/tabStore')
const { useChatStore } = await import('../../stores/chatStore')
useTabStore.setState({
tabs: [
{ sessionId: 'tab-1', title: 'First Session', type: 'session', status: 'idle' },
{ sessionId: 'tab-2', title: 'Second Session', type: 'session', status: 'idle' },
],
activeTabId: 'tab-1',
})
useChatStore.setState({
sessions: {},
disconnectSession: vi.fn(),
} as Partial<ReturnType<typeof useChatStore.getState>>)
await act(async () => {
render(<TabBar />)
})
const firstTab = screen.getByText('First Session').closest('[draggable="true"]')
const secondTab = screen.getByText('Second Session').closest('[draggable="true"]')
expect(firstTab).toBeTruthy()
expect(secondTab).toBeTruthy()
fireEvent.dragStart(firstTab!)
fireEvent.dragOver(secondTab!)
fireEvent.drop(secondTab!)
fireEvent.dragEnd(firstTab!)
expect(useTabStore.getState().tabs.map((tab) => tab.sessionId)).toEqual(['tab-2', 'tab-1'])
})
})

View File

@ -169,7 +169,6 @@ export function TabBar() {
return (
<div
data-testid="tab-bar"
data-tauri-drag-region
className="flex items-stretch bg-[var(--color-surface-container)] min-h-[37px] select-none border-b border-[var(--color-border)]"
>
@ -202,6 +201,15 @@ export function TabBar() {
))}
</div>
{isTauri && (
<div
data-testid="tab-bar-drag-gutter"
data-tauri-drag-region
aria-hidden="true"
className={`flex-shrink-0 min-h-[37px] ${showWindowControls ? 'w-3' : 'w-4'}`}
/>
)}
{canScrollRight && (
<button onClick={() => scroll('right')} className="flex-shrink-0 w-7 h-[37px] flex items-center justify-center text-[var(--color-text-tertiary)] hover:text-[var(--color-text-primary)] hover:bg-[var(--color-surface-hover)]">
<span className="material-symbols-outlined text-[16px]">chevron_right</span>