feat(ui): close tabs and panel headers with middle mouse button (#1613)

A middle click on a tool window header hides the window, as its Hide button does; presses on the header's tabs and buttons keep their own behaviour. Editor tabs already closed on a middle click, except when Settings › Editor Tabs hides the close button: that marked the tabs as not closable, so a middle click (and Delete) did nothing. A tab item now hides only its close button and still closes.

Co-authored-by: Inf1mum <[email protected]>
Цей коміт міститься в:
Vadim LazukтаInf1mum authored and GitHub committed 2026-11-06 02:55:26 +08:00
1 джерело 8689271e7c
коміт 653f2ff276
4 files changed
+112 -3

No files matched your search

+8
Переглянути файл
@@ -648,6 +648,14 @@ function HeaderRow({
<header
className={clsx(styles.header, tw.headerContent && styles.headerTabbed)}
onDoubleClick={(e) => e.target === e.currentTarget && toggleMaximizeToolWindow(tw.id)}
onMouseDown={(e) => {
// A middle click on the header hides the window (like the Hide button).
// Not a press on the header's tabs or buttons, which close and act
// themselves: their event would otherwise hide the whole window too.
if (e.button !== 1 || (e.target as Element).closest('button, a, [role="tablist"], [role="separator"]')) return;
e.preventDefault();
hideToolWindow(tw.id);
}}
>
{!showTitle ? null : titleItems.length > 0 ? (
<>
+72
Переглянути файл
@@ -0,0 +1,72 @@
import { fireEvent, render, screen } from "@testing-library/react";
import { afterEach, beforeEach, expect, it, vi } from "vitest";
import { Tabs } from "@/ui";
import { registerToolWindow } from "../contributions";
import { isToolWindowVisible, resetToolWindowLayout, showToolWindow } from "./store";
import { ToolWindowLayout } from "./ToolWindows";
const Empty = () => null;
const closeContentTab = vi.fn();
let disposers: Array<() => void> = [];
beforeEach(() => {
resetToolWindowLayout();
closeContentTab.mockClear();
disposers = [
registerToolWindow({
id: "run",
title: "Run",
icon: "terminal",
anchor: "bottom",
requiresProject: false,
component: Empty,
}),
registerToolWindow({
id: "problems",
title: "Problems",
icon: "file",
anchor: "bottom",
requiresProject: false,
component: Empty,
headerContent: () => (
<div role="tablist" aria-label="Views">
<Tabs items={[{ id: "file", label: "File" }]} activeId="file" onSelect={() => {}} onClose={closeContentTab} />
</div>
),
}),
];
});
afterEach(() => disposers.forEach((d) => d()));
const header = (id: string) => document.querySelector<HTMLElement>(`[data-toolwindow="${id}"] header`)!;
it("middle click on the header hides the tool window", () => {
showToolWindow("run");
render(<ToolWindowLayout editor={null} />);
expect(isToolWindowVisible("run")).toBe(true);
fireEvent.mouseDown(header("run"), { button: 1 });
expect(isToolWindowVisible("run")).toBe(false);
});
it("middle click on the header title hides the tool window", () => {
showToolWindow("run");
render(<ToolWindowLayout editor={null} />);
fireEvent.mouseDown(screen.getByText("Run"), { button: 1 });
expect(isToolWindowVisible("run")).toBe(false);
});
it("middle click on a header button leaves the tool window open", () => {
showToolWindow("run");
render(<ToolWindowLayout editor={null} />);
fireEvent.mouseDown(screen.getByRole("button", { name: "Hide" }), { button: 1 });
expect(isToolWindowVisible("run")).toBe(true);
});
it("middle click on a header content tab closes the tab, not the window", () => {
showToolWindow("problems");
render(<ToolWindowLayout editor={null} />);
fireEvent.mouseDown(screen.getByRole("tab", { name: "File" }), { button: 1 });
expect(closeContentTab).toHaveBeenCalledWith("file");
expect(isToolWindowVisible("problems")).toBe(true);
});