mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-07-27 15:13:37 +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 />)
|
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.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 () => {
|
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 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(tabBar).toHaveClass('min-h-11')
|
||||||
expect(tab).toHaveClass('min-h-11')
|
expect(tab).toHaveClass('min-h-11')
|
||||||
@ -506,7 +508,7 @@ describe('TabBar', () => {
|
|||||||
expect(screen.queryByTestId('open-project-menu')).not.toBeInTheDocument()
|
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 { TabBar } = await import('./TabBar')
|
||||||
const { useTabStore } = await import('../../stores/tabStore')
|
const { useTabStore } = await import('../../stores/tabStore')
|
||||||
const { useChatStore } = await import('../../stores/chatStore')
|
const { useChatStore } = await import('../../stores/chatStore')
|
||||||
@ -526,14 +528,13 @@ describe('TabBar', () => {
|
|||||||
render(<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).toBeInTheDocument()
|
||||||
|
expect(scrollRegion).toHaveAttribute('data-desktop-drag-region')
|
||||||
|
|
||||||
fireEvent.mouseDown(scrollRegion!)
|
fireEvent.mouseDown(scrollRegion)
|
||||||
|
|
||||||
await waitFor(() => {
|
expect(startDraggingMock).not.toHaveBeenCalled()
|
||||||
expect(startDraggingMock).toHaveBeenCalledTimes(1)
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('does not start dragging when clicking a tab', async () => {
|
it('does not start dragging when clicking a tab', async () => {
|
||||||
@ -582,10 +583,10 @@ describe('TabBar', () => {
|
|||||||
render(<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 firstTab = screen.getByText('First Session').closest('.tab-bar-interactive')
|
||||||
const secondTab = screen.getByText('Second Session').closest('.tab-bar-hit-area')
|
const secondTab = screen.getByText('Second Session').closest('.tab-bar-interactive')
|
||||||
|
|
||||||
expect(firstTab).toBeTruthy()
|
expect(firstTab).toBeTruthy()
|
||||||
expect(secondTab).toBeTruthy()
|
expect(secondTab).toBeTruthy()
|
||||||
@ -630,7 +631,7 @@ describe('TabBar', () => {
|
|||||||
render(<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()
|
expect(firstTab).toBeTruthy()
|
||||||
|
|
||||||
fireEvent.mouseDown(firstTab!, { button: 0, clientX: 20, clientY: 10 })
|
fireEvent.mouseDown(firstTab!, { button: 0, clientX: 20, clientY: 10 })
|
||||||
@ -664,7 +665,7 @@ describe('TabBar', () => {
|
|||||||
render(<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')
|
const closeButton = screen.getByLabelText('Close First Session')
|
||||||
|
|
||||||
expect(firstTab).toHaveClass('group')
|
expect(firstTab).toHaveClass('group')
|
||||||
|
|||||||
@ -22,7 +22,6 @@ const TAB_WIDTH = 180
|
|||||||
const DRAG_START_THRESHOLD = 4
|
const DRAG_START_THRESHOLD = 4
|
||||||
const desktopHost = getDesktopHost()
|
const desktopHost = getDesktopHost()
|
||||||
const isDesktopRuntime = desktopHost.isDesktop
|
const isDesktopRuntime = desktopHost.isDesktop
|
||||||
const canStartWindowDragging = desktopHost.capabilities.windowControls
|
|
||||||
|
|
||||||
type PendingCloseRequest = {
|
type PendingCloseRequest = {
|
||||||
tabs: Tab[]
|
tabs: Tab[]
|
||||||
@ -92,7 +91,6 @@ export function TabBar() {
|
|||||||
const pendingDragRef = useRef<{ index: number; startX: number; startY: number } | null>(null)
|
const pendingDragRef = useRef<{ index: number; startX: number; startY: number } | null>(null)
|
||||||
const suppressClickRef = useRef(false)
|
const suppressClickRef = useRef(false)
|
||||||
const tabRefs = useRef(new Map<string, HTMLDivElement | null>())
|
const tabRefs = useRef(new Map<string, HTMLDivElement | null>())
|
||||||
const startDraggingRef = useRef<(() => Promise<void>) | null>(null)
|
|
||||||
const t = useTranslation()
|
const t = useTranslation()
|
||||||
const runningSessionIds = useMemo(() => {
|
const runningSessionIds = useMemo(() => {
|
||||||
const ids = new Set<string>()
|
const ids = new Set<string>()
|
||||||
@ -105,11 +103,6 @@ export function TabBar() {
|
|||||||
return ids
|
return ids
|
||||||
}, [activeChatSessionIds, tabs])
|
}, [activeChatSessionIds, tabs])
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!canStartWindowDragging) return
|
|
||||||
startDraggingRef.current = () => getDesktopHost().window.startDragging()
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const updateScrollState = useCallback(() => {
|
const updateScrollState = useCallback(() => {
|
||||||
const el = scrollRef.current
|
const el = scrollRef.current
|
||||||
if (!el) return
|
if (!el) return
|
||||||
@ -326,16 +319,10 @@ export function TabBar() {
|
|||||||
setActiveTab(sessionId)
|
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 (
|
return (
|
||||||
<div
|
<div
|
||||||
data-testid="tab-bar"
|
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)]"
|
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
|
<div
|
||||||
ref={scrollRef}
|
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()}
|
onDragOver={(e) => e.preventDefault()}
|
||||||
onMouseDown={handleScrollRegionMouseDown}
|
|
||||||
>
|
>
|
||||||
{tabs.map((tab, index) => (
|
{tabs.map((tab, index) => (
|
||||||
<TabItem
|
<TabItem
|
||||||
@ -522,7 +510,7 @@ const TabItem = forwardRef<HTMLDivElement, {
|
|||||||
onMouseDown={onMouseDown}
|
onMouseDown={onMouseDown}
|
||||||
onContextMenu={onContextMenu}
|
onContextMenu={onContextMenu}
|
||||||
className={`
|
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'}
|
${isDragging ? 'z-20 cursor-grabbing' : 'cursor-grab'}
|
||||||
transition-[background-color,box-shadow,opacity,transform] duration-150 ease-out
|
transition-[background-color,box-shadow,opacity,transform] duration-150 ease-out
|
||||||
${isActive
|
${isActive
|
||||||
|
|||||||
@ -960,16 +960,20 @@ html, body, #root {
|
|||||||
|
|
||||||
/* Desktop drag region */
|
/* Desktop drag region */
|
||||||
[data-desktop-drag-region] {
|
[data-desktop-drag-region] {
|
||||||
|
app-region: drag;
|
||||||
-webkit-app-region: drag;
|
-webkit-app-region: drag;
|
||||||
}
|
}
|
||||||
button, input, textarea, select, a, [role="button"] {
|
button, input, textarea, select, a, [role="button"] {
|
||||||
|
app-region: no-drag;
|
||||||
-webkit-app-region: no-drag;
|
-webkit-app-region: no-drag;
|
||||||
}
|
}
|
||||||
[draggable="true"] {
|
[draggable="true"] {
|
||||||
|
app-region: no-drag;
|
||||||
-webkit-app-region: no-drag;
|
-webkit-app-region: no-drag;
|
||||||
}
|
}
|
||||||
.tab-bar-hit-area,
|
.tab-bar-interactive,
|
||||||
.tab-bar-hit-area * {
|
.tab-bar-interactive * {
|
||||||
|
app-region: no-drag;
|
||||||
-webkit-app-region: no-drag;
|
-webkit-app-region: no-drag;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user