mirror of
https://github.com/NanmiCoder/claude-code-haha.git
synced 2026-10-10 20:03:13 +08:00
fix(desktop): restore window dragging on the sidebar and workspace headers
The sidebar shell's permanent compositing layer dropped macOS app-region hits around the traffic lights. The workspace header slot also marked its leftover titlebar space as no-drag, so the empty strip could not move the window on either platform.
This commit is contained in:
@@ -1634,6 +1634,40 @@ describe('TabBar', () => {
|
||||
expect(frame).toContainElement(gutter)
|
||||
})
|
||||
|
||||
it('keeps the open workspace header as window chrome instead of a no-drag tab strip', async () => {
|
||||
const { TabBar } = await import('./TabBar')
|
||||
const { useTabStore } = await import('../../stores/tabStore')
|
||||
const { useChatStore } = await import('../../stores/chatStore')
|
||||
const { useWorkspaceStore } = await import('../../stores/workspaceStore')
|
||||
const { WorkspaceHeaderProvider } = await import('./WorkspaceHeaderContext')
|
||||
const sessionId = 'header-drag-session'
|
||||
|
||||
useTabStore.setState({
|
||||
tabs: [{ sessionId, title: 'Header drag session', type: 'session', status: 'idle' }],
|
||||
activeTabId: sessionId,
|
||||
})
|
||||
useChatStore.setState({
|
||||
sessions: {},
|
||||
disconnectSession: vi.fn(),
|
||||
} as Partial<ReturnType<typeof useChatStore.getState>>)
|
||||
|
||||
await act(async () => {
|
||||
render(<WorkspaceHeaderProvider><TabBar /></WorkspaceHeaderProvider>)
|
||||
})
|
||||
await act(async () => {
|
||||
useWorkspaceStore.getState().toggleWorkspace(sessionId)
|
||||
})
|
||||
|
||||
const slot = screen.getByTestId('workspace-header-slot')
|
||||
const header = screen.getByTestId('workspace-window-header')
|
||||
// `tab-bar-interactive` marks the node *and every descendant* as no-drag.
|
||||
// The slot hosts the resource strip's leftover flex space, so that class
|
||||
// would make the circled empty titlebar undraggable on every platform.
|
||||
expect(slot).toHaveAttribute('data-desktop-drag-region')
|
||||
expect(slot.className).not.toMatch(/\btab-bar-interactive\b/)
|
||||
expect(header).toHaveAttribute('data-desktop-drag-region')
|
||||
})
|
||||
|
||||
it('lifts the active tab onto the paper ground without turning it into a pill', async () => {
|
||||
const { TabBar } = await import('./TabBar')
|
||||
const { useTabStore } = await import('../../stores/tabStore')
|
||||
|
||||
@@ -636,11 +636,22 @@ export function TabBar() {
|
||||
>
|
||||
<div
|
||||
data-testid="workspace-window-header"
|
||||
data-desktop-drag-region={hasWorkspaceHeader && isDesktopRuntime ? true : undefined}
|
||||
className={hasWorkspaceHeader
|
||||
? 'flex min-w-0 flex-1 items-center gap-1 pr-2'
|
||||
: 'relative flex shrink-0 items-center gap-1 px-2 before:absolute before:left-0 before:top-1/2 before:h-4 before:w-px before:-translate-y-1/2 before:bg-[var(--color-tab-separator)]'}
|
||||
>
|
||||
{hasWorkspaceHeader ? <div ref={workspaceHeader.ref} data-testid="workspace-header-slot" className="tab-bar-interactive flex h-[52px] min-w-0 flex-1" /> : null}
|
||||
{hasWorkspaceHeader ? (
|
||||
<div
|
||||
ref={workspaceHeader.ref}
|
||||
data-testid="workspace-header-slot"
|
||||
data-desktop-drag-region={isDesktopRuntime ? true : undefined}
|
||||
// The slot is titlebar chrome: empty space must drag the window.
|
||||
// `tab-bar-interactive` here would also mark every descendant as
|
||||
// no-drag, including the resource strip's leftover flex space.
|
||||
className="flex h-[52px] min-w-0 flex-1"
|
||||
/>
|
||||
) : null}
|
||||
{showActivityButton && activeTabId && (
|
||||
<SessionActivityButton sessionId={activeTabId} />
|
||||
)}
|
||||
|
||||
@@ -450,6 +450,9 @@ describe('WorkspaceSurface', () => {
|
||||
expect(sessionHeader).toHaveClass('min-w-0', 'overflow-hidden')
|
||||
expect(sessionHeader).not.toContainElement(screen.getByTestId('workspace-header-frame'))
|
||||
expect(within(header).getByTestId('workspace-tab-strip-side')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('workspace-header-slot').className).not.toMatch(/\btab-bar-interactive\b/)
|
||||
expect(within(header).getByTestId('workspace-tab-strip-side')).toHaveAttribute('data-desktop-drag-region')
|
||||
expect(within(header).getByTestId(`workspace-tab-wrap-${sideId}`)).toHaveClass('tab-bar-interactive')
|
||||
expect(within(header).getByTestId('workspace-toggle-side')).toBeInTheDocument()
|
||||
expect(within(screen.getByTestId('workspace-surface-side')).queryByRole('tablist')).toBeNull()
|
||||
expect(within(screen.getByTestId('workspace-surface-bottom')).getByRole('tablist')).toBeInTheDocument()
|
||||
|
||||
@@ -97,6 +97,23 @@ describe('WorkspaceTabStrip', () => {
|
||||
expect(screen.getAllByRole('tab')).toHaveLength(1)
|
||||
})
|
||||
|
||||
it('lets leftover window-header space drag the window without capturing tab controls', () => {
|
||||
renderStrip({ placement: 'window' })
|
||||
const strip = screen.getByTestId('workspace-tab-strip-side')
|
||||
const tab = screen.getByTestId('workspace-tab-wrap-tab-file')
|
||||
const add = screen.getByTestId('workspace-add-tab-side').closest('span')
|
||||
|
||||
expect(strip).toHaveAttribute('data-desktop-drag-region')
|
||||
expect(tab).toHaveClass('tab-bar-interactive')
|
||||
expect(add).toHaveClass('tab-bar-interactive')
|
||||
expect(tab).not.toHaveAttribute('data-desktop-drag-region')
|
||||
})
|
||||
|
||||
it('does not turn a dock strip into window chrome', () => {
|
||||
renderStrip({ placement: 'dock' })
|
||||
expect(screen.getByTestId('workspace-tab-strip-side')).not.toHaveAttribute('data-desktop-drag-region')
|
||||
})
|
||||
|
||||
it('titles each kind from its own identity', () => {
|
||||
renderStrip({ tabs: [FILE_TAB, BROWSER_TAB, REVIEW_TAB, TERMINAL_TAB] })
|
||||
|
||||
|
||||
@@ -209,6 +209,7 @@ export function WorkspaceTabStrip({
|
||||
return (
|
||||
<div
|
||||
data-testid={`workspace-tab-strip-${dock}`}
|
||||
data-desktop-drag-region={placement === 'window' ? true : undefined}
|
||||
className={`flex min-w-0 shrink-0 items-stretch gap-1 bg-[var(--color-surface)] pl-2 pr-1 ${placement === 'window' ? 'h-[52px] flex-1' : 'h-10 border-b border-[var(--color-border)]'}`}
|
||||
>
|
||||
<div className="flex min-w-0 flex-1 items-stretch gap-1">
|
||||
@@ -251,7 +252,7 @@ export function WorkspaceTabStrip({
|
||||
onDoubleClick={() => onPin(tab.id)}
|
||||
onContextMenu={(event) => openMenuAt(event, tab.id)}
|
||||
className={[
|
||||
`group ${placement === 'window' ? 'my-2.5' : 'my-1'} flex min-w-[112px] max-w-[200px] cursor-default items-center gap-0.5 rounded-[var(--radius-md)] pl-2 pr-1 transition-colors`,
|
||||
`tab-bar-interactive group ${placement === 'window' ? 'my-2.5' : 'my-1'} flex min-w-[112px] max-w-[200px] cursor-default items-center gap-0.5 rounded-[var(--radius-md)] pl-2 pr-1 transition-colors`,
|
||||
isActive
|
||||
? 'bg-[var(--color-surface-selected)] text-[var(--color-text-primary)]'
|
||||
: 'text-[var(--color-text-secondary)] hover:bg-[var(--color-surface-hover)]',
|
||||
@@ -346,7 +347,7 @@ export function WorkspaceTabStrip({
|
||||
})}
|
||||
</div>
|
||||
|
||||
<span className="flex shrink-0 items-center">
|
||||
<span className="tab-bar-interactive flex shrink-0 items-center">
|
||||
<IconButton
|
||||
icon={<Plus size={15} strokeWidth={2} />}
|
||||
label={t('workspace.tabAdd')}
|
||||
@@ -369,7 +370,7 @@ export function WorkspaceTabStrip({
|
||||
</div>
|
||||
|
||||
{activeTerminalTab ? (
|
||||
<span className="flex shrink-0 items-center">
|
||||
<span className="tab-bar-interactive flex shrink-0 items-center">
|
||||
<IconButton
|
||||
icon={<MoreHorizontal size={16} />}
|
||||
label={t('workspace.tabMenu')}
|
||||
|
||||
@@ -1830,7 +1830,9 @@ button, input, textarea, select, a, [role="button"] {
|
||||
flex-shrink: 0;
|
||||
overflow: hidden;
|
||||
transition: width var(--motion-sidebar-duration) var(--motion-sidebar-easing);
|
||||
will-change: width;
|
||||
/* Do not promote this shell with will-change. A permanent compositing
|
||||
layer makes Electron drop -webkit-app-region hits on macOS, so the
|
||||
title padding around the traffic lights cannot drag the window. */
|
||||
}
|
||||
|
||||
.sidebar-shell[data-state="closed"] {
|
||||
|
||||
@@ -397,6 +397,17 @@ describe('overlay opacity contract', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('desktop window drag', () => {
|
||||
it('does not promote the sidebar shell into a compositing layer', () => {
|
||||
// Electron drops -webkit-app-region hits on a macOS compositing layer, so
|
||||
// a permanent will-change on this shell makes the traffic-light padding
|
||||
// undraggable. The comment is allowed to mention the property; a real
|
||||
// declaration is not.
|
||||
const shellCss = getCssBetween('.sidebar-shell {', '.sidebar-shell[data-state="closed"] {')
|
||||
expect(shellCss).not.toMatch(/will-change\s*:/)
|
||||
})
|
||||
})
|
||||
|
||||
describe('layering scale', () => {
|
||||
const scale = (() => {
|
||||
const start = normalizedCss.indexOf('/* ─── Layering scale')
|
||||
|
||||
Reference in New Issue
Block a user