feat(tab): 支持鼠标中键关闭标签页

- 在标签标题上识别鼠标中键并阻止默认滚动行为
- 复用现有标签关闭流程,保留 SQL 文件未保存确认
- 修正停靠标签关闭范围断言并增加中键交互回归测试
This commit is contained in:
AutumnNazi
2026-07-16 17:44:18 +08:00
parent 193d68398b
commit 35427d08a8
2 changed files with 30 additions and 2 deletions

View File

@@ -9,6 +9,7 @@ import {
resolveTabHoverTitle,
shouldShowV2ConnectionLabel,
TabHoverInfo,
isMiddleMouseButton,
stopTabHoverDragPropagation,
} from './TabManager';
import { setCurrentLanguage } from '../i18n';
@@ -53,6 +54,12 @@ afterEach(() => {
});
describe('TabManager hover info', () => {
it('recognizes only the auxiliary middle mouse button for tab closing', () => {
expect(isMiddleMouseButton(1)).toBe(true);
expect(isMiddleMouseButton(0)).toBe(false);
expect(isMiddleMouseButton(2)).toBe(false);
});
it('memoizes the tab workbench so parent-only modal state does not repaint open tabs', () => {
const source = readFileSync(new URL('./TabManager.tsx', import.meta.url), 'utf8');
@@ -320,6 +327,10 @@ describe('TabManager hover info', () => {
it('wires hover card tab-switch and drag-blocking handlers with selectable text styles', () => {
const source = readFileSync(new URL('./TabManager.tsx', import.meta.url), 'utf8');
expect(source).toContain('onMouseDown={handleTabLabelMouseDown}');
expect(source).toContain('onAuxClick={handleTabLabelAuxClick}');
expect(source).toContain('event.stopPropagation();');
expect(source).toContain('onClose();');
expect(source).toContain('onPointerDown={stopTabHoverDragPropagation}');
expect(source).toContain('onPointerUp={stopTabHoverDragPropagation}');
expect(source).toContain('onPointerDownCapture={stopTabHoverDragPropagation}');
@@ -374,8 +385,8 @@ describe('TabManager hover info', () => {
expect(source).toContain('clearSQLFileTabDraft(tab.id)');
expect(source).toContain('closeTabsWithSQLFilePrompt([id], () => closeTab(id))');
expect(source).toContain('closeTabsWithSQLFilePrompt(getCloseOtherTabIds(tabs, tab.id), () => closeOtherTabs(tab.id))');
expect(source).toContain('closeTabsWithSQLFilePrompt(getCloseTabsToLeftIds(tabs, tab.id), () => closeTabsToLeft(tab.id))');
expect(source).toContain('closeTabsWithSQLFilePrompt(getCloseTabsToRightIds(tabs, tab.id), () => closeTabsToRight(tab.id))');
expect(source).toContain('closeTabsWithSQLFilePrompt(getCloseTabsToLeftIds(dockedTabs, tab.id), () => closeTabsToLeft(tab.id))');
expect(source).toContain('closeTabsWithSQLFilePrompt(getCloseTabsToRightIds(dockedTabs, tab.id), () => closeTabsToRight(tab.id))');
expect(source).toContain('closeTabsWithSQLFilePrompt(tabs.map((item) => item.id), () => closeAllTabs())');
});

View File

@@ -358,6 +358,8 @@ type SortableTabLabelProps = {
onClose?: () => void;
};
export const isMiddleMouseButton = (button: number): boolean => button === 1;
const renderV2TabDisplayPart = (part: TabDisplayPart) => {
if (part.key === 'kind') {
return (
@@ -399,6 +401,19 @@ const SortableTabLabel: React.FC<SortableTabLabelProps> = ({
setIsTabMenuOpen(true);
};
const handleTabLabelMouseDown = (event: React.MouseEvent<HTMLElement>) => {
if (!onClose || !isMiddleMouseButton(event.button)) return;
event.preventDefault();
event.stopPropagation();
};
const handleTabLabelAuxClick = (event: React.MouseEvent<HTMLElement>) => {
if (!onClose || !isMiddleMouseButton(event.button)) return;
event.preventDefault();
event.stopPropagation();
onClose();
};
const handleTabMenuOpenChange = (open: boolean) => {
setIsTabMenuOpen(open);
setIsHoverInfoOpen(false);
@@ -414,6 +429,8 @@ const SortableTabLabel: React.FC<SortableTabLabelProps> = ({
<span
className={`tab-dnd-label${isV2Ui ? ' gn-v2-tab-label' : ''}${showSecondaryLine ? ' gn-v2-tab-label-double' : ''}${tabDisplayPartCount >= 4 ? ' gn-v2-tab-label-rich' : ''}`}
onContextMenu={handleTabLabelContextMenu}
onMouseDown={handleTabLabelMouseDown}
onAuxClick={handleTabLabelAuxClick}
title={isV2Ui ? undefined : displayTitle}
>
{isV2Ui ? (