mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-07-16 13:03:31 +08:00
fix: correct windows tab bar overflow alignment
This commit is contained in:
parent
d3c62a8bc5
commit
fb011588f9
136
desktop/src/components/layout/TabBar.test.tsx
Normal file
136
desktop/src/components/layout/TabBar.test.tsx
Normal file
@ -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<string, string> = {
|
||||
'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: () => <div data-testid="window-controls" />,
|
||||
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<ReturnType<typeof useChatStore.getState>>)
|
||||
})
|
||||
|
||||
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<ReturnType<typeof useChatStore.getState>>)
|
||||
|
||||
await act(async () => {
|
||||
render(<TabBar />)
|
||||
})
|
||||
|
||||
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<ReturnType<typeof useChatStore.getState>>)
|
||||
|
||||
await act(async () => {
|
||||
render(<TabBar />)
|
||||
})
|
||||
|
||||
expect(screen.getByTestId('tab-bar')).toHaveAttribute('data-tauri-drag-region')
|
||||
})
|
||||
})
|
||||
@ -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<void>) | null>(null)
|
||||
|
||||
const scrollRef = useRef<HTMLDivElement>(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 (
|
||||
<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)]"
|
||||
onMouseDown={handleTabBarDrag}
|
||||
>
|
||||
|
||||
{canScrollLeft && (
|
||||
@ -203,8 +184,6 @@ export function TabBar() {
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* Windows: drag spacer fills remaining area + custom window controls */}
|
||||
{showWindowControls && <div className="flex-1" />}
|
||||
<WindowControls />
|
||||
|
||||
{contextMenu && (
|
||||
|
||||
@ -43,7 +43,7 @@ export function WindowControls() {
|
||||
if (!showWindowControls || !win) return null
|
||||
|
||||
return (
|
||||
<div className="flex items-stretch flex-shrink-0 -my-px">
|
||||
<div data-testid="window-controls" className="flex items-stretch flex-shrink-0 -my-px">
|
||||
{/* Minimize */}
|
||||
<button
|
||||
onClick={() => runWindowAction(() => win.minimize())}
|
||||
|
||||
@ -229,6 +229,9 @@ html, body, #root {
|
||||
button, input, textarea, select, a, [role="button"] {
|
||||
-webkit-app-region: no-drag;
|
||||
}
|
||||
[draggable="true"] {
|
||||
-webkit-app-region: no-drag;
|
||||
}
|
||||
|
||||
/* Custom shadow from prototype */
|
||||
.custom-shadow {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user