diff --git a/src/editor/core/view/EditorArea.tsx b/src/editor/core/view/EditorArea.tsx
index aa67dec7..fd7394dc 100644
--- a/src/editor/core/view/EditorArea.tsx
+++ b/src/editor/core/view/EditorArea.tsx
@@ -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);
diff --git a/src/ui/Tabs.test.tsx b/src/ui/Tabs.test.tsx
index 701429bb..c81c642a 100644
--- a/src/ui/Tabs.test.tsx
+++ b/src/ui/Tabs.test.tsx
@@ -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();
+ 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();
+ 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();
+ 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();
+ 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));
diff --git a/src/ui/Tabs.tsx b/src/ui/Tabs.tsx
index ad4e4b12..078118fc 100644
--- a/src/ui/Tabs.tsx
+++ b/src/ui/Tabs.tsx
@@ -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)}