feat(editor): close editor tabs with Shift+click (#581)

Add Shift+left-click as a way to close editor tabs. The tab strip gets a closeOnShiftClick option, enabled for the editor area only, so tool window tabs such as Run and Terminal still select on Shift+click. Keyboard activation with Shift held still selects, non-closable tabs still select, and Shift+click does not start a tab drag.

Co-authored-by: iGink <5266594+iGink@users.noreply.github.com>
This commit is contained in:
iGink
2026-10-31 23:38:44 +08:00
committed by GitHub
co-authored by iGink
parent 5280c9e584
commit 347e3525d2
3 changed files with 51 additions and 2 deletions
+1
View File
@@ -279,6 +279,7 @@ function PaneView({ paneId }: { paneId: string }) {
onSelect={(id) => ctrl.activate(paneId, id)}
onClose={(id) => void ctrl.closeTab(paneId, id)}
onCloseOthers={(id) => void ctrl.closeOthers(paneId, id)}
closeOnShiftClick
onDoubleClick={(id) => {
// IDEA: a preview tab becomes a regular one; any other tab maximizes the editor.
if (pane.tabs.find((t) => t.id === id)?.preview) ctrl.keepTab(paneId, id);
+36
View File
@@ -71,6 +71,42 @@ describe("Tabs interactions", () => {
expect(onClose).toHaveBeenCalledWith("b");
});
it("Shift+left-click closes an editor tab without selecting it", () => {
const onClose = vi.fn();
const onSelect = vi.fn();
render(<Tabs items={items} activeId="a" onSelect={onSelect} onClose={onClose} closeOnShiftClick />);
fireEvent.click(screen.getByRole("tab", { name: "B" }), { shiftKey: true, detail: 1 });
expect(onClose).toHaveBeenCalledWith("b");
expect(onSelect).not.toHaveBeenCalled();
});
it("Shift+click only closes closable tabs", () => {
const onClose = vi.fn();
const onSelect = vi.fn();
render(<Tabs items={items} activeId="a" onSelect={onSelect} onClose={onClose} closeOnShiftClick />);
fireEvent.click(screen.getByRole("tab", { name: "C" }), { shiftKey: true, detail: 1 });
expect(onClose).not.toHaveBeenCalled();
expect(onSelect).toHaveBeenCalledWith("c");
});
it("keeps Shift+click selection where the strip does not opt in", () => {
const onClose = vi.fn();
const onSelect = vi.fn();
render(<Tabs items={items} activeId="a" onSelect={onSelect} onClose={onClose} />);
fireEvent.click(screen.getByRole("tab", { name: "B" }), { shiftKey: true, detail: 1 });
expect(onClose).not.toHaveBeenCalled();
expect(onSelect).toHaveBeenCalledWith("b");
});
it("does not treat a keyboard click with Shift held as a close", () => {
const onClose = vi.fn();
const onSelect = vi.fn();
render(<Tabs items={items} activeId="a" onSelect={onSelect} onClose={onClose} closeOnShiftClick />);
fireEvent.click(screen.getByRole("tab", { name: "B" }), { shiftKey: true });
expect(onClose).not.toHaveBeenCalled();
expect(onSelect).toHaveBeenCalledWith("b");
});
it("lets a container close the selected tab when it is closable", () => {
const handles: TabStripHandle[] = [];
const register = (h: TabStripHandle) => (handles.push(h), () => void handles.splice(handles.indexOf(h), 1));
+14 -2
View File
@@ -88,6 +88,8 @@ export interface TabsProps {
onClose?: (id: string) => void;
/** Alt+click on a close button (IDEA: close all other tabs). */
onCloseOthers?: (id: string) => void;
/** Shift+left-click a tab closes it (editor tabs; plain clicks still select). */
closeOnShiftClick?: boolean;
onDoubleClick?: (id: string) => void;
onContextMenu?: (id: string, e: MouseEvent) => void;
/** `editor`: 30px with separators (editor tabs); `compact`: 28px plain (popups, tool window sub-tabs). */
@@ -114,6 +116,7 @@ export function Tabs({
onSelect,
onClose,
onCloseOthers,
closeOnShiftClick,
onDoubleClick,
onContextMenu,
variant = "editor",
@@ -287,7 +290,8 @@ export function Tabs({
onPointerDown={
dnd
? (e) => {
if ((e.target as Element).closest("[data-tab-close]")) return;
if ((closeOnShiftClick && e.shiftKey) || (e.target as Element).closest("[data-tab-close]"))
return;
startPointerDrag(
e,
e.currentTarget,
@@ -336,7 +340,15 @@ export function Tabs({
tabIndex={selected || (!activeId && index === 0) ? 0 : -1}
className={styles.tabButton}
style={item.color ? { color: item.color } : undefined}
onClick={() => onSelect(item.id)}
onClick={(e) => {
// Shift+click is mouse-only: a keyboard Enter with Shift held still selects.
if (closeOnShiftClick && e.shiftKey && e.detail > 0 && closable) {
e.preventDefault();
onClose!(item.id);
return;
}
onSelect(item.id);
}}
onDoubleClick={onDoubleClick ? () => onDoubleClick(item.id) : undefined}
onKeyDown={(e) => onKeyDown(e, index, item)}
{...tooltipAttrs(item.tooltip)}