返回 DeepSeek-Reasonix
TabBar.tsx
1 import { useCallback, useState } from "react";
2 import { createPortal } from "react-dom";
3 import { Plus, X } from "lucide-react";
4 import type { MouseEvent as ReactMouseEvent, RefObject } from "react";
5 import { DOCK_ENTRY_ICONS } from "../dockEntryIcons";
6 import { useT } from "../../lib/i18n";
7 import type { TabItem } from "../../store/activityBar";
8 import { ContextMenu, contextMenuPointFromEvent, type ContextMenuItem, type ContextMenuPoint } from "../ContextMenu";
9 import { useDockTabDrag } from "../../lib/useDockTabDrag";
10 import { TabOverviewMenu } from "./TabOverviewMenu";
11
12 interface TabBarProps {
13 tabs: TabItem[];
14 activeTabId: string | null;
15 onActivate: (tabId: string) => void;
16 onClose: (tabId: string) => void;
17 onMoveTab: (fromId: string, toId: string, side: "left" | "right") => void;
18 onAdd: () => void;
19 addButtonRef: RefObject<HTMLButtonElement | null>;
20 onClosePanel?: () => void;
21 }
22
23 export function TabBar({ tabs, activeTabId, onActivate, onClose, onMoveTab, onAdd, addButtonRef, onClosePanel }: TabBarProps) {
24 const t = useT();
25 const [menuTabId, setMenuTabId] = useState<string | null>(null);
26 const [menuPoint, setMenuPoint] = useState<ContextMenuPoint | null>(null);
27 const {
28 draggingTabId, dragElRefs, floatingRef, tabsRef, tabsOverflow, suppressClickRef, startTabDrag, clearDragState, ghost, slotWidthFor,
29 } = useDockTabDrag({ tabs, onActivate, onMoveTab });
30
31 const closeMenu = useCallback(() => {
32 setMenuTabId(null);
33 setMenuPoint(null);
34 }, []);
35
36 const openTabMenu = useCallback((event: ReactMouseEvent<HTMLDivElement>, tab: TabItem) => {
37 event.preventDefault();
38 event.stopPropagation();
39 setMenuTabId(tab.id);
40 setMenuPoint(contextMenuPointFromEvent(event));
41 }, []);
42
43 const menuTabIndex = menuTabId ? tabs.findIndex((tab) => tab.id === menuTabId) : -1;
44
45 const closeThen = useCallback((action: () => void) => {
46 closeMenu();
47 action();
48 }, [closeMenu]);
49
50 const menuItems = useCallback((): ContextMenuItem[] => {
51 const items: ContextMenuItem[] = [];
52 items.push(
53 {
54 key: "close-current",
55 label: t("tabBar.closeTab"),
56 disabled: menuTabIndex < 0,
57 onSelect: () => closeThen(() => { if (menuTabId) onClose(menuTabId); }),
58 },
59 {
60 key: "close-others",
61 label: t("tabBar.closeOtherTabs"),
62 disabled: menuTabIndex < 0 || tabs.length <= 1,
63 onSelect: () => closeThen(() => {
64 if (!menuTabId) return;
65 tabs.filter((tab) => tab.id !== menuTabId).forEach((tab) => onClose(tab.id));
66 onActivate(menuTabId);
67 }),
68 },
69 {
70 key: "close-right",
71 label: t("tabBar.closeTabsToRight"),
72 disabled: menuTabIndex < 0 || menuTabIndex >= tabs.length - 1,
73 onSelect: () => closeThen(() => {
74 if (menuTabIndex < 0) return;
75 tabs.slice(menuTabIndex + 1).forEach((tab) => onClose(tab.id));
76 onActivate(menuTabId ?? tabs[menuTabIndex].id);
77 }),
78 },
79 );
80 return items;
81 }, [closeThen, menuTabId, menuTabIndex, onActivate, onClose, t, tabs]);
82
83 return (
84 <div className="workbench-dock__tools">
85 <TabOverviewMenu />
86 <div
87 ref={tabsRef}
88 className={["workbench-dock__tabs", tabsOverflow ? "workbench-dock__tabs--overflow" : ""].filter(Boolean).join(" ")}
89 role="tablist"
90 aria-label={t("rightDock.views")}
91 >
92 {tabs.map((tab) => {
93 const TabIcon = DOCK_ENTRY_ICONS[tab.type];
94 const active = tab.id === activeTabId;
95 const dragging = draggingTabId === tab.id;
96 if (dragging) {
97 const slotWidth = slotWidthFor(tab.id);
98 return (
99 <div
100 key={tab.id}
101 className="workbench-dock__tab-slot"
102 style={{ width: slotWidth }}
103 aria-hidden="true"
104 />
105 );
106 }
107 return (
108 <div
109 key={tab.id}
110 ref={(node) => {
111 if (node) dragElRefs.current.set(tab.id, node);
112 else dragElRefs.current.delete(tab.id);
113 }}
114 role="tab"
115 aria-selected={active}
116 aria-label={tab.label}
117 className={[
118 "workbench-dock__tab",
119 active ? "workbench-dock__tab--active" : "",
120 ].filter(Boolean).join(" ")}
121 onClick={() => {
122 if (suppressClickRef.current) {
123 suppressClickRef.current = false;
124 return;
125 }
126 onActivate(tab.id);
127 }}
128 tabIndex={active ? 0 : -1}
129 onContextMenu={(event) => openTabMenu(event, tab)}
130 onKeyDown={(event) => {
131 if (event.target !== event.currentTarget) return;
132 if (event.key === "Enter" || event.key === " ") { event.preventDefault(); onActivate(tab.id); }
133 if (event.key === "ContextMenu" || (event.shiftKey && event.key === "F10")) {
134 event.preventDefault();
135 const rect = event.currentTarget.getBoundingClientRect();
136 setMenuTabId(tab.id);
137 setMenuPoint({ left: rect.left, top: rect.bottom, keyboardTarget: event.currentTarget });
138 }
139 const index = tabs.indexOf(tab);
140 const next = event.key === "ArrowRight" ? tabs[(index + 1) % tabs.length]
141 : event.key === "ArrowLeft" ? tabs[(index + tabs.length - 1) % tabs.length] : null;
142 if (next) { event.preventDefault(); onActivate(next.id); dragElRefs.current.get(next.id)?.focus(); }
143 }}
144 onPointerDown={(event) => startTabDrag(event, tab.id)}
145 >
146 <TabIcon size={13} />
147 <span className="workbench-dock__tab-label">{tab.label}</span>
148 <button
149 type="button"
150 className="workbench-dock__tab-close"
151 aria-label={t("rightDock.closeTab")}
152 onPointerDown={(event) => event.stopPropagation()}
153 onClick={(event) => {
154 event.stopPropagation();
155 onClose(tab.id);
156 }}
157 >
158 <X size={12} />
159 </button>
160 </div>
161 );
162 })}
163 </div>
164 <button
165 ref={addButtonRef}
166 type="button"
167 className="workbench-dock__tab-button workbench-dock__tab-add"
168 aria-label={t("rightDock.addTab")}
169 onClick={onAdd}
170 >
171 <Plus size={14} />
172 </button>
173 {onClosePanel && <button type="button" className="workbench-dock__tab-button workbench-dock__collapse"
174 aria-label={t("workspace.close")} onClick={onClosePanel}><X size={14} /></button>}
175 {draggingTabId !== null && (() => {
176 const draggedTab = tabs.find((tab) => tab.id === draggingTabId);
177 if (!draggedTab) return null;
178 const FloatIcon = DOCK_ENTRY_ICONS[draggedTab.type];
179 return createPortal(
180 <div
181 ref={floatingRef}
182 className={[
183 "workbench-dock__tab",
184 "workbench-dock__tab--floating",
185 draggedTab.id === activeTabId ? "workbench-dock__tab--active" : "",
186 ].filter(Boolean).join(" ")}
187 role="presentation"
188 style={{ left: ghost.left, top: ghost.top, width: ghost.width }}
189 >
190 <FloatIcon size={13} />
191 <span className="workbench-dock__tab-label">{draggedTab.label}</span>
192 {/* Keep the whole tab (including its close button) in the drag
193 ghost so it looks like the tab itself is being dragged. */}
194 <button
195 type="button"
196 className="workbench-dock__tab-close"
197 aria-label={t("rightDock.closeTab")}
198 onClick={(event) => {
199 event.stopPropagation();
200 clearDragState();
201 onClose(draggedTab.id);
202 }}
203 >
204 <X size={12} />
205 </button>
206 </div>,
207 document.body,
208 );
209 })()}
210 <ContextMenu
211 open={menuPoint !== null}
212 point={menuPoint}
213 items={menuItems()}
214 onClose={closeMenu}
215 ariaLabel={t("rightDock.views")}
216 />
217 </div>
218 );
219 }
220
220 lines Plain Text