mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-07-16 13:03:31 +08:00
fix(desktop): restore tab bar window dragging
Electron handles custom draggable chrome through CSS app-region rules, not the old runtime startDragging path. Mark the tab strip and empty scroll gutter as native drag regions while keeping tab items and controls explicitly no-drag so tab reordering and close/tool buttons keep receiving pointer events. Constraint: Electron drag regions swallow pointer events unless interactive children are marked no-drag Rejected: Keep calling startDragging from the empty gutter | Electron desktopHost does not expose that as the active migration path Confidence: high Scope-risk: narrow Directive: Do not mark tab items themselves as drag regions without revalidating tab reorder behavior Tested: cd desktop && bun run test src/components/layout/TabBar.test.tsx --run Tested: cd desktop && bun run lint Tested: bun run check:desktop
This commit is contained in:
parent
349955dbf2
commit
7e737cb4aa
@ -307,8 +307,10 @@ describe('TabBar', () => {
|
||||
render(<TabBar />)
|
||||
})
|
||||
|
||||
expect(screen.getByTestId('tab-bar')).not.toHaveAttribute('data-desktop-drag-region')
|
||||
expect(screen.getByTestId('tab-bar')).toHaveAttribute('data-desktop-drag-region')
|
||||
expect(screen.getByTestId('tab-bar-scroll-region')).toHaveAttribute('data-desktop-drag-region')
|
||||
expect(screen.getByTestId('tab-bar-drag-gutter')).toHaveAttribute('data-desktop-drag-region')
|
||||
expect(screen.getByText('Untitled Session').closest('.tab-bar-interactive')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('keeps the desktop tab strip at a roomier titlebar height', async () => {
|
||||
@ -332,7 +334,7 @@ describe('TabBar', () => {
|
||||
})
|
||||
|
||||
const tabBar = screen.getByTestId('tab-bar')
|
||||
const tab = screen.getByText('Untitled Session').closest('.tab-bar-hit-area')
|
||||
const tab = screen.getByText('Untitled Session').closest('.tab-bar-interactive')
|
||||
|
||||
expect(tabBar).toHaveClass('min-h-11')
|
||||
expect(tab).toHaveClass('min-h-11')
|
||||
@ -506,7 +508,7 @@ describe('TabBar', () => {
|
||||
expect(screen.queryByTestId('open-project-menu')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('starts dragging when clicking the empty tab-bar gutter', async () => {
|
||||
it('marks the empty tab-bar gutter as a native drag region without runtime dragging', async () => {
|
||||
const { TabBar } = await import('./TabBar')
|
||||
const { useTabStore } = await import('../../stores/tabStore')
|
||||
const { useChatStore } = await import('../../stores/chatStore')
|
||||
@ -526,14 +528,13 @@ describe('TabBar', () => {
|
||||
render(<TabBar />)
|
||||
})
|
||||
|
||||
const scrollRegion = screen.getByTestId('tab-bar').querySelector('.overflow-x-hidden')
|
||||
const scrollRegion = screen.getByTestId('tab-bar-scroll-region')
|
||||
expect(scrollRegion).toBeInTheDocument()
|
||||
expect(scrollRegion).toHaveAttribute('data-desktop-drag-region')
|
||||
|
||||
fireEvent.mouseDown(scrollRegion!)
|
||||
fireEvent.mouseDown(scrollRegion)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(startDraggingMock).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
expect(startDraggingMock).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('does not start dragging when clicking a tab', async () => {
|
||||
@ -582,10 +583,10 @@ describe('TabBar', () => {
|
||||
render(<TabBar />)
|
||||
})
|
||||
|
||||
expect(screen.getByTestId('tab-bar').querySelector('.tab-bar-hit-area')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('tab-bar').querySelector('.tab-bar-interactive')).toBeInTheDocument()
|
||||
|
||||
const firstTab = screen.getByText('First Session').closest('.tab-bar-hit-area')
|
||||
const secondTab = screen.getByText('Second Session').closest('.tab-bar-hit-area')
|
||||
const firstTab = screen.getByText('First Session').closest('.tab-bar-interactive')
|
||||
const secondTab = screen.getByText('Second Session').closest('.tab-bar-interactive')
|
||||
|
||||
expect(firstTab).toBeTruthy()
|
||||
expect(secondTab).toBeTruthy()
|
||||
@ -630,7 +631,7 @@ describe('TabBar', () => {
|
||||
render(<TabBar />)
|
||||
})
|
||||
|
||||
const firstTab = screen.getByText('First Session').closest('.tab-bar-hit-area')
|
||||
const firstTab = screen.getByText('First Session').closest('.tab-bar-interactive')
|
||||
expect(firstTab).toBeTruthy()
|
||||
|
||||
fireEvent.mouseDown(firstTab!, { button: 0, clientX: 20, clientY: 10 })
|
||||
@ -664,7 +665,7 @@ describe('TabBar', () => {
|
||||
render(<TabBar />)
|
||||
})
|
||||
|
||||
const firstTab = screen.getByText('First Session').closest('.tab-bar-hit-area')
|
||||
const firstTab = screen.getByText('First Session').closest('.tab-bar-interactive')
|
||||
const closeButton = screen.getByLabelText('Close First Session')
|
||||
|
||||
expect(firstTab).toHaveClass('group')
|
||||
|
||||
@ -22,7 +22,6 @@ const TAB_WIDTH = 180
|
||||
const DRAG_START_THRESHOLD = 4
|
||||
const desktopHost = getDesktopHost()
|
||||
const isDesktopRuntime = desktopHost.isDesktop
|
||||
const canStartWindowDragging = desktopHost.capabilities.windowControls
|
||||
|
||||
type PendingCloseRequest = {
|
||||
tabs: Tab[]
|
||||
@ -92,7 +91,6 @@ export function TabBar() {
|
||||
const pendingDragRef = useRef<{ index: number; startX: number; startY: number } | null>(null)
|
||||
const suppressClickRef = useRef(false)
|
||||
const tabRefs = useRef(new Map<string, HTMLDivElement | null>())
|
||||
const startDraggingRef = useRef<(() => Promise<void>) | null>(null)
|
||||
const t = useTranslation()
|
||||
const runningSessionIds = useMemo(() => {
|
||||
const ids = new Set<string>()
|
||||
@ -105,11 +103,6 @@ export function TabBar() {
|
||||
return ids
|
||||
}, [activeChatSessionIds, tabs])
|
||||
|
||||
useEffect(() => {
|
||||
if (!canStartWindowDragging) return
|
||||
startDraggingRef.current = () => getDesktopHost().window.startDragging()
|
||||
}, [])
|
||||
|
||||
const updateScrollState = useCallback(() => {
|
||||
const el = scrollRef.current
|
||||
if (!el) return
|
||||
@ -326,16 +319,10 @@ export function TabBar() {
|
||||
setActiveTab(sessionId)
|
||||
}
|
||||
|
||||
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(() => {})
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div
|
||||
data-testid="tab-bar"
|
||||
data-desktop-drag-region={isDesktopRuntime ? true : undefined}
|
||||
className="flex min-h-11 items-stretch bg-[var(--color-surface-container)] select-none border-b border-[var(--color-border)]"
|
||||
>
|
||||
|
||||
@ -347,9 +334,10 @@ export function TabBar() {
|
||||
|
||||
<div
|
||||
ref={scrollRef}
|
||||
className="tab-bar-hit-area flex-1 flex items-stretch overflow-x-hidden"
|
||||
data-testid="tab-bar-scroll-region"
|
||||
data-desktop-drag-region={isDesktopRuntime ? true : undefined}
|
||||
className="flex-1 flex items-stretch overflow-x-hidden"
|
||||
onDragOver={(e) => e.preventDefault()}
|
||||
onMouseDown={handleScrollRegionMouseDown}
|
||||
>
|
||||
{tabs.map((tab, index) => (
|
||||
<TabItem
|
||||
@ -522,7 +510,7 @@ const TabItem = forwardRef<HTMLDivElement, {
|
||||
onMouseDown={onMouseDown}
|
||||
onContextMenu={onContextMenu}
|
||||
className={`
|
||||
tab-bar-hit-area group relative flex min-h-11 flex-shrink-0 items-center gap-1.5 px-3
|
||||
tab-bar-interactive group relative flex min-h-11 flex-shrink-0 items-center gap-1.5 px-3
|
||||
${isDragging ? 'z-20 cursor-grabbing' : 'cursor-grab'}
|
||||
transition-[background-color,box-shadow,opacity,transform] duration-150 ease-out
|
||||
${isActive
|
||||
|
||||
@ -960,16 +960,20 @@ html, body, #root {
|
||||
|
||||
/* Desktop drag region */
|
||||
[data-desktop-drag-region] {
|
||||
app-region: drag;
|
||||
-webkit-app-region: drag;
|
||||
}
|
||||
button, input, textarea, select, a, [role="button"] {
|
||||
app-region: no-drag;
|
||||
-webkit-app-region: no-drag;
|
||||
}
|
||||
[draggable="true"] {
|
||||
app-region: no-drag;
|
||||
-webkit-app-region: no-drag;
|
||||
}
|
||||
.tab-bar-hit-area,
|
||||
.tab-bar-hit-area * {
|
||||
.tab-bar-interactive,
|
||||
.tab-bar-interactive * {
|
||||
app-region: no-drag;
|
||||
-webkit-app-region: no-drag;
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user