mirror of
https://github.com/l0ng-ai/openidea.git
synced 2026-11-06 16:08:05 +00:00
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 <8135225+Inf1mum@users.noreply.github.com>
This commit is contained in:
@@ -1,13 +1,14 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { cleanup, render, screen, within } from "@testing-library/react";
|
||||
import { cleanup, fireEvent, render, screen, within } from "@testing-library/react";
|
||||
import { useProject } from "@/platform/project";
|
||||
import { useTabs } from "../controller";
|
||||
import type { Tab } from "../model/tabs";
|
||||
import { EditorAreaView } from "./EditorArea";
|
||||
|
||||
const { closeTab } = vi.hoisted(() => ({ closeTab: vi.fn() }));
|
||||
vi.mock("../controller", async (original) => ({
|
||||
...(await original<object>()),
|
||||
editorController: () => ({ publishContext: vi.fn(), tabMenu: () => [] }),
|
||||
editorController: () => ({ publishContext: vi.fn(), tabMenu: () => [], closeTab }),
|
||||
}));
|
||||
vi.mock("./TextEditorHost", () => ({ TextEditorHost: () => <input aria-label="Source" /> }));
|
||||
vi.mock("./FindBar", () => ({ FindBar: () => null }));
|
||||
@@ -34,7 +35,10 @@ function seed(pinned: boolean[], tabs: Record<string, unknown>) {
|
||||
useProject.setState({ settings: { editor: { tabs } } as never });
|
||||
}
|
||||
|
||||
beforeEach(() => cleanup());
|
||||
beforeEach(() => {
|
||||
cleanup();
|
||||
closeTab.mockClear();
|
||||
});
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
useTabs.setState(originalTabs, true);
|
||||
@@ -69,4 +73,11 @@ describe("pinned tabs in a separate row", () => {
|
||||
render(<EditorAreaView />);
|
||||
expect(screen.getAllByRole("tablist", { name: "Editor tabs" })).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("middle click closes an editor tab", () => {
|
||||
seed([false, false, false], { placement: "top" });
|
||||
render(<EditorAreaView />);
|
||||
fireEvent.mouseDown(screen.getByRole("tab", { name: /f2/ }), { button: 1 });
|
||||
expect(closeTab).toHaveBeenCalledWith("p1", "t2");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -117,6 +117,24 @@ describe("Tabs interactions", () => {
|
||||
expect(onSelect).toHaveBeenCalledWith("c");
|
||||
});
|
||||
|
||||
it("middle click closes a tab without selecting it", () => {
|
||||
const onClose = vi.fn();
|
||||
const onSelect = vi.fn();
|
||||
render(<Tabs items={items} activeId="a" onSelect={onSelect} onClose={onClose} />);
|
||||
fireEvent.mouseDown(screen.getByRole("tab", { name: "B" }), { button: 1 });
|
||||
expect(onClose).toHaveBeenCalledWith("b");
|
||||
expect(onSelect).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("middle click ignores tabs that cannot close", () => {
|
||||
const onClose = vi.fn();
|
||||
const onSelect = vi.fn();
|
||||
render(<Tabs items={items} activeId="a" onSelect={onSelect} onClose={onClose} />);
|
||||
fireEvent.mouseDown(screen.getByRole("tab", { name: "C" }), { button: 1 });
|
||||
expect(onClose).not.toHaveBeenCalled();
|
||||
expect(onSelect).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("keeps Shift+click selection where the strip does not opt in", () => {
|
||||
const onClose = vi.fn();
|
||||
const onSelect = vi.fn();
|
||||
|
||||
@@ -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 ? (
|
||||
<>
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
Reference in New Issue
Block a user