fix(tabs): close tab tooltips immediately and refine styling

This commit is contained in:
AiLi authored and GitHub committed 2026-09-24 19:19:01 +08:00
1 parent d0497e0ca6
commit 1eb429e137
2 files changed
+149 -28

No files matched your search

@@ -131,6 +131,7 @@ const fixedTabsScroll = useTabScroll(fixedTabsRowRef);
const regularTabsScroll = useTabScroll(regularTabsRowRef);
const editingTabId = ref<string | null>(null);
const editingTitle = ref("");
const openTabTooltipId = ref<string | null>(null);
// Drag suppression must survive pointerup: the browser fires click *after*
// pointerup, so the flag is consumed by the click instead of being cleared
// with the drag state. A fresh pointerdown always resets it.
@@ -912,6 +913,23 @@ const tabTooltipSide = computed(() => {
if (!isVerticalLayout.value) return "bottom" as const;
return settingsStore.editorSettings.tabPlacement === "left" ? ("right" as const) : ("left" as const);
});
function updateTabTooltipOpen(tabId: string, open: boolean) {
if (open) {
openTabTooltipId.value = tabId;
} else if (openTabTooltipId.value === tabId) {
openTabTooltipId.value = null;
}
}
function closeTabTooltip(tabId: string) {
if (openTabTooltipId.value === tabId) {
openTabTooltipId.value = null;
}
}
function isConnectionlessPluginTab(tab: QueryTab): boolean {
return (tab.mode === "plugin-workbench" || tab.mode === "plugin-filesystem") && !tab.connectionId;
}
/**
* Drag visuals for a pill, ported from the legacy tab bar's tabDropStyle: the
@@ -1578,7 +1596,7 @@ watch([() => props.specialPageTabs?.settingsActive, () => props.specialPageTabs?
@contextmenu="onContextMenu"
@transitionend.self="handleTabGroupTransitionEnd"
>
<Tooltip>
<Tooltip :open="openTabTooltipId === entry.tab.id" @update:open="updateTabTooltipOpen(entry.tab.id, $event)">
<TooltipTrigger as-child>
<div
class="app-tab-pill group flex cursor-default items-center gap-1 px-2 text-xs transition-colors whitespace-nowrap select-none"
@@ -1596,6 +1614,7 @@ watch([() => props.specialPageTabs?.settingsActive, () => props.specialPageTabs?
:data-active-tab="isTabActive(entry.tab)"
:data-tab-id="entry.tab.id"
@pointerdown="handleTabPointerDown($event, entry.tab)"
@mouseleave="closeTabTooltip(entry.tab.id)"
@click="handleTabClick(entry.tab)"
@dblclick="handleTabDoubleClick(entry.tab, $event)"
@mousedown.middle.prevent="closeTab(entry.tab)"
@@ -1638,7 +1657,10 @@ watch([() => props.specialPageTabs?.settingsActive, () => props.specialPageTabs?
</button>
</div>
</TooltipTrigger>
<TooltipContent :side="tabTooltipSide" class="grid grid-cols-[auto_minmax(0,1fr)] items-start gap-x-2 text-xs">
<TooltipContent v-if="isConnectionlessPluginTab(entry.tab)" :side="tabTooltipSide" data-plugin-title-tooltip>
{{ tabTitleText(entry.tab) }}
</TooltipContent>
<TooltipContent v-else :side="tabTooltipSide" class="grid grid-cols-[auto_minmax(0,1fr)] items-start gap-x-2 text-xs">
<template v-for="line in tabTooltipLines(entry.tab, t)" :key="line.label">
<span class="whitespace-nowrap font-medium opacity-70">{{ line.label }}</span>
<span class="min-w-0 break-all">{{ line.value }}</span>
@@ -1650,29 +1672,34 @@ watch([() => props.specialPageTabs?.settingsActive, () => props.specialPageTabs?
</template>
<template v-if="!section.pinned && showSpecialPageTabs">
<CustomContextMenu v-if="specialPageTabs?.pluginCenterOpen" :items="getSpecialPageTabMenuItems('pluginCenter')" v-slot="{ onContextMenu }">
<div
data-plugin-center-tab
class="app-tab-pill group flex shrink-0 cursor-default items-center gap-1 px-2 text-xs transition-colors whitespace-nowrap select-none"
:class="specialPageTabClass(!!specialPageTabs?.pluginCenterActive)"
:style="specialPageTabStyle(!!specialPageTabs?.pluginCenterActive)"
:data-active-tab="specialPageTabs?.pluginCenterActive"
:title="t('toolbar.pluginCenter')"
:aria-label="t('toolbar.pluginCenter')"
:aria-pressed="!!specialPageTabs?.pluginCenterActive"
role="button"
tabindex="0"
@click="emit('activate-plugin-center')"
@keydown.enter.self.prevent="emit('activate-plugin-center')"
@keydown.space.self.prevent="emit('activate-plugin-center')"
@contextmenu="onContextMenu"
@mousedown.middle.prevent="emit('close-plugin-center')"
>
<PlugZap class="h-3.5 w-3.5 shrink-0 text-violet-600 dark:text-violet-400" />
<span v-if="!isTabBarCollapsed" class="min-w-0 flex-1 truncate">{{ t("toolbar.pluginCenter") }}</span>
<button v-if="!isTabBarCollapsed" class="shrink-0 rounded p-0.5 hover:bg-muted-foreground/20" :aria-label="t('common.close')" :title="t('common.close')" @click.stop="emit('close-plugin-center')">
<X class="h-3 w-3" />
</button>
</div>
<Tooltip :open="openTabTooltipId === 'special:plugin-center'" @update:open="updateTabTooltipOpen('special:plugin-center', $event)">
<TooltipTrigger as-child>
<div
data-plugin-center-tab
class="app-tab-pill group flex shrink-0 cursor-default items-center gap-1 px-2 text-xs transition-colors whitespace-nowrap select-none"
:class="specialPageTabClass(!!specialPageTabs?.pluginCenterActive)"
:style="specialPageTabStyle(!!specialPageTabs?.pluginCenterActive)"
:data-active-tab="specialPageTabs?.pluginCenterActive"
:aria-label="t('toolbar.pluginCenter')"
:aria-pressed="!!specialPageTabs?.pluginCenterActive"
role="button"
tabindex="0"
@mouseleave="closeTabTooltip('special:plugin-center')"
@click="emit('activate-plugin-center')"
@keydown.enter.self.prevent="emit('activate-plugin-center')"
@keydown.space.self.prevent="emit('activate-plugin-center')"
@contextmenu="onContextMenu"
@mousedown.middle.prevent="emit('close-plugin-center')"
>
<PlugZap class="h-3.5 w-3.5 shrink-0 text-violet-600 dark:text-violet-400" />
<span v-if="!isTabBarCollapsed" class="min-w-0 flex-1 truncate">{{ t("toolbar.pluginCenter") }}</span>
<button v-if="!isTabBarCollapsed" class="shrink-0 rounded p-0.5 hover:bg-muted-foreground/20" :aria-label="t('common.close')" :title="t('common.close')" @click.stop="emit('close-plugin-center')">
<X class="h-3 w-3" />
</button>
</div>
</TooltipTrigger>
<TooltipContent :side="tabTooltipSide">{{ t("toolbar.pluginCenter") }}</TooltipContent>
</Tooltip>
</CustomContextMenu>
<CustomContextMenu v-if="specialPageTabs?.settingsOpen" :items="getSpecialPageTabMenuItems('settings')" v-slot="{ onContextMenu }">
<div
@@ -13,9 +13,14 @@ vi.mock("@/components/ui/CustomContextMenu.vue", () => ({
}));
vi.mock("@/components/ui/tooltip", () => ({
Tooltip: { name: "TooltipStub", template: `<div><slot /></div>` },
Tooltip: {
name: "TooltipStub",
props: ["open"],
emits: ["update:open"],
template: `<div class="tooltip-stub" :data-open="open === true" @mouseover="$emit('update:open', true)"><slot /></div>`,
},
TooltipTrigger: { name: "TooltipTriggerStub", template: `<div><slot /></div>` },
TooltipContent: { name: "TooltipContentStub", template: `<div><slot /></div>` },
TooltipContent: { name: "TooltipContentStub", template: `<div class="tooltip-content-stub"><slot /></div>` },
}));
vi.mock("@/components/ui/popover", () => ({
@@ -50,7 +55,14 @@ interface Mounted {
host: HTMLDivElement;
}
function mountBar(groupId: string, tabs: string[], activeTabId: string | null, activePinia: ReturnType<typeof createPinia>, onActivateTab?: (tabId: string) => void): Mounted {
function mountBar(
groupId: string,
tabs: string[],
activeTabId: string | null,
activePinia: ReturnType<typeof createPinia>,
onActivateTab?: (tabId: string) => void,
specialPageTabs?: { settingsOpen: boolean; settingsActive: boolean; driverStoreOpen: boolean; driverStoreActive: boolean; pluginCenterOpen: boolean; pluginCenterActive: boolean; driverUpdateCount: number },
): Mounted {
const store = useQueryStore();
const host = createHost();
const app = createApp(EditorGroupTabBar, {
@@ -58,6 +70,7 @@ function mountBar(groupId: string, tabs: string[], activeTabId: string | null, a
tabs: tabs.map((id) => store.tabs.find((candidate) => candidate.id === id)!),
activeTabId,
"onActivate-tab": onActivateTab,
specialPageTabs,
});
// Reuse the active pinia so the component's internal store is the same
// instance the test drives — otherwise drag validation runs against an
@@ -69,6 +82,8 @@ function mountBar(groupId: string, tabs: string[], activeTabId: string | null, a
locale: "en",
messages: {
en: {
toolbar: { pluginCenter: "Plugin Center" },
common: { close: "Close" },
contextMenu: {
splitRight: "Split right",
splitDown: "Split down",
@@ -162,6 +177,85 @@ describe("EditorGroupTabBar behavior", () => {
host.remove();
});
it("closes a tab tooltip when the pointer leaves the tab", async () => {
const store = useQueryStore();
const tabId = store.createTab("pg-1", "app", "Plugin", "query");
const mainGroup = store.groups[0];
const { app, host } = mountBar(mainGroup.id, [tabId], tabId, pinia);
await settle();
const pill = tabPill(host, tabId);
pill.dispatchEvent(new MouseEvent("mouseover", { bubbles: true }));
await settle();
expect(host.querySelector<HTMLElement>(".tooltip-stub")?.dataset.open).toBe("true");
pill.dispatchEvent(new MouseEvent("mouseleave"));
await settle();
expect(host.querySelector<HTMLElement>(".tooltip-stub")?.dataset.open).toBe("false");
app.unmount();
host.remove();
});
it("shows a concise DBX tooltip for a connectionless plugin tab", async () => {
const store = useQueryStore();
const tabId = store.openPluginWorkbench("com.example.toolbox", "toolbox");
const mainGroup = store.groups[0];
const { app, host } = mountBar(mainGroup.id, [tabId], tabId, pinia);
await settle();
const tooltip = host.querySelector<HTMLElement>(".tooltip-stub")!;
tabPill(host, tabId).dispatchEvent(new MouseEvent("mouseover", { bubbles: true }));
await settle();
expect(tooltip.dataset.open).toBe("true");
expect(host.querySelector<HTMLElement>("[data-plugin-title-tooltip]")?.textContent?.trim()).toBe("toolbox");
app.unmount();
host.remove();
});
it("keeps the tooltip for a connection-bound plugin tab", async () => {
const store = useQueryStore();
const tabId = store.openPluginWorkbench("com.example.toolbox", "toolbox", { connectionId: "pg-1" });
const mainGroup = store.groups[0];
const { app, host } = mountBar(mainGroup.id, [tabId], tabId, pinia);
await settle();
const tooltip = host.querySelector<HTMLElement>(".tooltip-stub")!;
tabPill(host, tabId).dispatchEvent(new MouseEvent("mouseover", { bubbles: true }));
await settle();
expect(tooltip.dataset.open).toBe("true");
expect(host.querySelector("[data-plugin-title-tooltip]")).toBeNull();
app.unmount();
host.remove();
});
it("uses the DBX tooltip for the Plugin Center tab", async () => {
const store = useQueryStore();
const mainGroup = store.groups[0];
const specialPageTabs = { settingsOpen: false, settingsActive: false, driverStoreOpen: false, driverStoreActive: false, pluginCenterOpen: true, pluginCenterActive: true, driverUpdateCount: 0 };
const { app, host } = mountBar(mainGroup.id, [], null, pinia, undefined, specialPageTabs);
await settle();
const tab = host.querySelector<HTMLElement>("[data-plugin-center-tab]")!;
expect(tab.getAttribute("title")).toBeNull();
tab.dispatchEvent(new MouseEvent("mouseover", { bubbles: true }));
await settle();
expect(host.querySelector<HTMLElement>(".tooltip-stub")?.dataset.open).toBe("true");
expect(host.querySelector<HTMLElement>(".tooltip-content-stub")?.textContent?.trim()).toBe("Plugin Center");
tab.dispatchEvent(new MouseEvent("mouseleave"));
await settle();
expect(host.querySelector<HTMLElement>(".tooltip-stub")?.dataset.open).toBe("false");
app.unmount();
host.remove();
});
it("suppresses the click that follows a drag, and only that click", async () => {
const store = useQueryStore();
const firstId = store.createTab("pg-1", "app", "Query 1", "query");