fix(editor): portal SQL hover tooltips above result panes

This commit is contained in:
t8y2 committed 2026-07-31 15:46:15 +08:00
1 parent 1d4ab520f3
commit c78f451177
3 files changed
+79 -2

No files matched your search

@@ -0,0 +1,62 @@
// @vitest-environment happy-dom
import { EditorState } from "@codemirror/state";
import { EditorView, showTooltip, tooltips, type Tooltip } from "@codemirror/view";
import { describe, expect, it } from "vitest";
function staticTooltip(label: string): Tooltip {
return {
pos: 0,
create: () => {
const dom = document.createElement("div");
dom.dataset.tooltip = label;
return { dom };
},
};
}
describe("CodeMirror tooltip app-root portal", () => {
it("keeps containers independent and removes them with each editor", () => {
document.body.innerHTML = '<div id="root"><div data-split-pane><div data-editor="first"></div><div data-editor="second"></div></div></div>';
const root = document.querySelector<HTMLElement>("#root")!;
const splitPane = document.querySelector<HTMLElement>("[data-split-pane]")!;
const firstEditor = document.querySelector<HTMLElement>('[data-editor="first"]')!;
const secondEditor = document.querySelector<HTMLElement>('[data-editor="second"]')!;
root.style.overflow = "hidden";
splitPane.style.overflow = "hidden";
const firstView = new EditorView({
parent: firstEditor,
state: EditorState.create({
doc: "select first",
extensions: [tooltips({ parent: root }), showTooltip.of(staticTooltip("first"))],
}),
});
const secondView = new EditorView({
parent: secondEditor,
state: EditorState.create({
doc: "select second",
extensions: [tooltips({ parent: root }), showTooltip.of(staticTooltip("second"))],
}),
});
const firstTooltip = root.querySelector<HTMLElement>('[data-tooltip="first"]')!;
const secondTooltip = root.querySelector<HTMLElement>('[data-tooltip="second"]')!;
const firstContainer = firstTooltip.parentElement!;
const secondContainer = secondTooltip.parentElement!;
expect(splitPane.contains(firstTooltip)).toBe(false);
expect(firstContainer.parentElement).toBe(root);
expect(secondContainer.parentElement).toBe(root);
expect(firstContainer).not.toBe(secondContainer);
expect(firstTooltip.style.position).toBe("fixed");
expect(secondTooltip.style.position).toBe("fixed");
firstView.destroy();
expect(firstContainer.isConnected).toBe(false);
expect(secondContainer.isConnected).toBe(true);
secondView.destroy();
expect(secondContainer.isConnected).toBe(false);
});
});
@@ -2,9 +2,22 @@ import { readFileSync } from "node:fs";
import { describe, expect, it } from "vitest";
const queryEditorSource = readFileSync(new URL("../../../components/editor/QueryEditor.vue", import.meta.url), "utf8");
const globalStylesSource = readFileSync(new URL("../../../styles/globals.css", import.meta.url), "utf8");
describe("QueryEditor tooltip container", () => {
it("keeps CodeMirror tooltips inside the editor instead of extending document.body", () => {
it("portals CodeMirror tooltips to the stable app root without using document.body", () => {
expect(queryEditorSource).toContain('const tooltipParent = editorRef.value.closest<HTMLElement>("#root") ?? editorRef.value;');
expect(queryEditorSource).toContain("tooltips({ parent: tooltipParent })");
expect(queryEditorSource).not.toContain("tooltips({ parent: document.body })");
});
it("keeps the app root viewport-sized without a transformed containing block", () => {
const rootRule = globalStylesSource.match(/html,\s*body,\s*#root\s*\{(?<declarations>[^}]*)\}/)?.groups?.declarations;
expect(rootRule).toBeDefined();
expect(rootRule).toMatch(/width:\s*100%/);
expect(rootRule).toMatch(/height:\s*100%/);
expect(rootRule).toMatch(/overflow:\s*hidden/);
expect(rootRule).not.toMatch(/(?:^|\s)(?:transform|contain)\s*:/);
});
});