From fb011588f974773cda457075d36c4edef0c59bf6 Mon Sep 17 00:00:00 2001 From: Relakkes Yang Date: Sun, 19 Apr 2026 17:56:42 +0800 Subject: [PATCH] fix: correct windows tab bar overflow alignment --- desktop/src/components/layout/TabBar.test.tsx | 136 ++++++++++++++++++ desktop/src/components/layout/TabBar.tsx | 25 +--- .../src/components/layout/WindowControls.tsx | 2 +- desktop/src/theme/globals.css | 3 + 4 files changed, 142 insertions(+), 24 deletions(-) create mode 100644 desktop/src/components/layout/TabBar.test.tsx diff --git a/desktop/src/components/layout/TabBar.test.tsx b/desktop/src/components/layout/TabBar.test.tsx new file mode 100644 index 00000000..b38d04c0 --- /dev/null +++ b/desktop/src/components/layout/TabBar.test.tsx @@ -0,0 +1,136 @@ +import { act, fireEvent, render, screen, waitFor } from '@testing-library/react' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import '@testing-library/jest-dom' + +vi.mock('../../i18n', () => ({ + useTranslation: () => (key: string) => { + const translations: Record = { + 'tabs.close': 'Close', + 'tabs.closeOthers': 'Close Others', + 'tabs.closeLeft': 'Close Left', + 'tabs.closeRight': 'Close Right', + 'tabs.closeAll': 'Close All', + 'tabs.closeConfirmTitle': 'Session Running', + 'tabs.closeConfirmMessage': 'Still running', + 'tabs.closeConfirmKeep': 'Keep Running', + 'tabs.closeConfirmStop': 'Stop & Close', + 'common.cancel': 'Cancel', + } + + return translations[key] ?? key + }, +})) + +vi.mock('./WindowControls', () => ({ + WindowControls: () =>
, + showWindowControls: true, +})) + +describe('TabBar', () => { + beforeEach(() => { + class ResizeObserverMock { + constructor(_callback: ResizeObserverCallback) {} + + observe(_target: Element) {} + + disconnect() {} + unobserve() {} + } + + Object.defineProperty(window, 'ResizeObserver', { + configurable: true, + value: ResizeObserverMock, + }) + + vi.resetModules() + }) + + afterEach(async () => { + const { useTabStore } = await import('../../stores/tabStore') + const { useChatStore } = await import('../../stores/chatStore') + + useTabStore.setState({ tabs: [], activeTabId: null }) + useChatStore.setState({ + sessions: {}, + } as Partial>) + }) + + it('keeps the overflow button flush against window controls on Windows', 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' }, + { sessionId: 'tab-2', title: 'Settings', type: 'settings', status: 'idle' }, + { sessionId: 'tab-3', title: 'hello', type: 'session', status: 'idle' }, + { sessionId: 'tab-4', title: 'overflow', type: 'session', status: 'idle' }, + ], + activeTabId: 'tab-1', + }) + useChatStore.setState({ + sessions: {}, + disconnectSession: vi.fn(), + } as Partial>) + + await act(async () => { + render() + }) + + const scrollRegion = screen.getByTestId('tab-bar').querySelector('.overflow-x-hidden') + expect(scrollRegion).toBeInTheDocument() + + Object.defineProperty(scrollRegion!, 'clientWidth', { + configurable: true, + get: () => 240, + }) + Object.defineProperty(scrollRegion!, 'scrollWidth', { + configurable: true, + get: () => 720, + }) + Object.defineProperty(scrollRegion!, 'scrollLeft', { + configurable: true, + get: () => 0, + }) + Object.defineProperty(scrollRegion!, 'scrollBy', { + configurable: true, + value: vi.fn(), + }) + + act(() => { + fireEvent.scroll(scrollRegion!) + }) + + await waitFor(() => { + expect(screen.getByTestId('window-controls')).toBeInTheDocument() + expect(screen.getByText('chevron_right').closest('button')).toBeInTheDocument() + }) + + const rightButton = screen.getByText('chevron_right').closest('button') + expect(rightButton?.nextElementSibling).toBe(screen.getByTestId('window-controls')) + }) + + it('marks the tab bar as a native drag region', 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>) + + await act(async () => { + render() + }) + + expect(screen.getByTestId('tab-bar')).toHaveAttribute('data-tauri-drag-region') + }) +}) diff --git a/desktop/src/components/layout/TabBar.tsx b/desktop/src/components/layout/TabBar.tsx index 508a75d3..54fb2b68 100644 --- a/desktop/src/components/layout/TabBar.tsx +++ b/desktop/src/components/layout/TabBar.tsx @@ -5,7 +5,6 @@ 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) @@ -15,8 +14,6 @@ export function TabBar() { const disconnectSession = useChatStore((s) => s.disconnectSession) const moveTab = useTabStore((s) => s.moveTab) - const startDraggingRef = useRef<(() => Promise) | null>(null) - const scrollRef = useRef(null) const [canScrollLeft, setCanScrollLeft] = useState(false) const [canScrollRight, setCanScrollRight] = useState(false) @@ -53,16 +50,6 @@ export function TabBar() { return () => document.removeEventListener('click', close) }, [contextMenu]) - useEffect(() => { - if (!isTauri) return - import(/* @vite-ignore */ '@tauri-apps/api/window') - .then(({ getCurrentWindow }) => { - const win = getCurrentWindow() - startDraggingRef.current = () => win.startDragging() - }) - .catch(() => {}) - }, []) - const scroll = (direction: 'left' | 'right') => { const el = scrollRef.current if (!el) return @@ -158,19 +145,13 @@ export function TabBar() { setDragOverIndex(null) } - const handleTabBarDrag = useCallback((e: React.MouseEvent) => { - if ((e.target as HTMLElement).closest('button, input, textarea, select, a, [role="button"], [draggable="true"]')) { - return - } - startDraggingRef.current?.() - }, []) - if (tabs.length === 0 && !showWindowControls) return null return (
{canScrollLeft && ( @@ -203,8 +184,6 @@ export function TabBar() { )} - {/* Windows: drag spacer fills remaining area + custom window controls */} - {showWindowControls &&
} {contextMenu && ( diff --git a/desktop/src/components/layout/WindowControls.tsx b/desktop/src/components/layout/WindowControls.tsx index 2825e80a..95ec10c2 100644 --- a/desktop/src/components/layout/WindowControls.tsx +++ b/desktop/src/components/layout/WindowControls.tsx @@ -43,7 +43,7 @@ export function WindowControls() { if (!showWindowControls || !win) return null return ( -
+
{/* Minimize */}