mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-07-16 13:03:31 +08:00
Merge pull request #94 from NanmiCoder/codex/fix-issue-92-tabbar-drag
fix: restore desktop tab bar window dragging
This commit is contained in:
commit
d8e5ab6469
@ -2,6 +2,15 @@ import { act, fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import '@testing-library/jest-dom'
|
||||
|
||||
const startDraggingMock = vi.hoisted(() => vi.fn(() => Promise.resolve()))
|
||||
const getCurrentWindowMock = vi.hoisted(() => vi.fn(() => ({
|
||||
startDragging: startDraggingMock,
|
||||
})))
|
||||
|
||||
vi.mock('@tauri-apps/api/window', () => ({
|
||||
getCurrentWindow: getCurrentWindowMock,
|
||||
}))
|
||||
|
||||
vi.mock('../../i18n', () => ({
|
||||
useTranslation: () => (key: string) => {
|
||||
const translations: Record<string, string> = {
|
||||
@ -42,6 +51,13 @@ describe('TabBar', () => {
|
||||
value: ResizeObserverMock,
|
||||
})
|
||||
|
||||
Object.defineProperty(window, '__TAURI__', {
|
||||
configurable: true,
|
||||
value: {},
|
||||
})
|
||||
|
||||
startDraggingMock.mockClear()
|
||||
getCurrentWindowMock.mockClear()
|
||||
vi.resetModules()
|
||||
})
|
||||
|
||||
@ -53,6 +69,8 @@ describe('TabBar', () => {
|
||||
useChatStore.setState({
|
||||
sessions: {},
|
||||
} as Partial<ReturnType<typeof useChatStore.getState>>)
|
||||
|
||||
delete (window as typeof window & { __TAURI__?: unknown }).__TAURI__
|
||||
})
|
||||
|
||||
it('keeps the overflow button flush against window controls on Windows', async () => {
|
||||
@ -133,4 +151,67 @@ describe('TabBar', () => {
|
||||
|
||||
expect(screen.getByTestId('tab-bar')).toHaveAttribute('data-tauri-drag-region')
|
||||
})
|
||||
|
||||
it('starts dragging when clicking the empty tab-bar gutter', 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: 'Untitled 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 />)
|
||||
})
|
||||
|
||||
await waitFor(() => {
|
||||
expect(getCurrentWindowMock).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
const scrollRegion = screen.getByTestId('tab-bar').querySelector('.overflow-x-hidden')
|
||||
expect(scrollRegion).toBeInTheDocument()
|
||||
|
||||
fireEvent.mouseDown(scrollRegion!)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(startDraggingMock).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
|
||||
it('does not start dragging when clicking a tab', 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: 'Untitled 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 />)
|
||||
})
|
||||
|
||||
await waitFor(() => {
|
||||
expect(getCurrentWindowMock).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
fireEvent.mouseDown(screen.getByText('Untitled Session'))
|
||||
|
||||
expect(startDraggingMock).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
@ -5,6 +5,7 @@ import { useTranslation } from '../../i18n'
|
||||
import { WindowControls, showWindowControls } from './WindowControls'
|
||||
|
||||
const TAB_WIDTH = 180
|
||||
const isTauri = typeof window !== 'undefined' && ('__TAURI_INTERNALS__' in window || '__TAURI__' in window)
|
||||
|
||||
export function TabBar() {
|
||||
const tabs = useTabStore((s) => s.tabs)
|
||||
@ -21,8 +22,19 @@ export function TabBar() {
|
||||
const [closingTabId, setClosingTabId] = useState<string | null>(null)
|
||||
const [dragOverIndex, setDragOverIndex] = useState<number | null>(null)
|
||||
const dragIndexRef = useRef<number | null>(null)
|
||||
const startDraggingRef = useRef<(() => Promise<void>) | null>(null)
|
||||
const t = useTranslation()
|
||||
|
||||
useEffect(() => {
|
||||
if (!isTauri) return
|
||||
import(/* @vite-ignore */ '@tauri-apps/api/window')
|
||||
.then(({ getCurrentWindow }) => {
|
||||
const win = getCurrentWindow()
|
||||
startDraggingRef.current = () => win.startDragging()
|
||||
})
|
||||
.catch(() => {})
|
||||
}, [])
|
||||
|
||||
const updateScrollState = useCallback(() => {
|
||||
const el = scrollRef.current
|
||||
if (!el) return
|
||||
@ -145,6 +157,13 @@ export function TabBar() {
|
||||
setDragOverIndex(null)
|
||||
}
|
||||
|
||||
const handleScrollRegionMouseDown = useCallback((event: React.MouseEvent<HTMLDivElement>) => {
|
||||
if (event.button !== 0 || event.target !== scrollRef.current) return
|
||||
const startDragging = startDraggingRef.current
|
||||
if (!startDragging) return
|
||||
void startDragging().catch(() => {})
|
||||
}, [])
|
||||
|
||||
if (tabs.length === 0 && !showWindowControls) return null
|
||||
|
||||
return (
|
||||
@ -160,7 +179,12 @@ export function TabBar() {
|
||||
</button>
|
||||
)}
|
||||
|
||||
<div ref={scrollRef} className="flex-1 flex items-stretch overflow-x-hidden" onDragOver={(e) => e.preventDefault()}>
|
||||
<div
|
||||
ref={scrollRef}
|
||||
className="flex-1 flex items-stretch overflow-x-hidden"
|
||||
onDragOver={(e) => e.preventDefault()}
|
||||
onMouseDown={handleScrollRegionMouseDown}
|
||||
>
|
||||
{tabs.map((tab, index) => (
|
||||
<TabItem
|
||||
key={tab.sessionId}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user