fix(tabs): close tab tooltips immediately and refine styling
This commit is contained in:
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
|
||||
|
||||
+97
-3
@@ -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");
|
||||
|
||||
Reference in new issue
Block a user