fix(editor): prevent tooltip portal from covering app

This commit is contained in:
t8y2 committed 2026-07-31 17:46:11 +08:00
1 parent 12373868ec
commit 4f9ea3f5cc
4 files changed
+28 -10

No files matched your search

@@ -3,6 +3,7 @@
import { EditorState } from "@codemirror/state";
import { EditorView, showTooltip, tooltips, type Tooltip } from "@codemirror/view";
import { describe, expect, it } from "vitest";
import { editorFontTheme } from "@/lib/editor/editorThemes";
function staticTooltip(label: string): Tooltip {
return {
@@ -15,28 +16,31 @@ function staticTooltip(label: string): Tooltip {
};
}
describe("CodeMirror tooltip app-root portal", () => {
describe("CodeMirror tooltip host", () => {
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>';
document.body.innerHTML = '<div id="root"><div data-app><div data-split-pane><div data-editor="first"></div><div data-editor="second"></div></div><div id="dbx-query-editor-tooltip-root" style="position: fixed; width: 0; height: 0; overflow: visible"></div></div></div>';
const root = document.querySelector<HTMLElement>("#root")!;
const splitPane = document.querySelector<HTMLElement>("[data-split-pane]")!;
const tooltipHost = document.querySelector<HTMLElement>("#dbx-query-editor-tooltip-root")!;
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 editorTheme = EditorView.theme({ "&": { backgroundColor: "rgb(255, 255, 255)" } });
const editorExtensions = [editorTheme, editorFontTheme(EditorView, 13, "monospace", { fixedHeight: true })];
const firstView = new EditorView({
parent: firstEditor,
state: EditorState.create({
doc: "select first",
extensions: [tooltips({ parent: root }), showTooltip.of(staticTooltip("first"))],
extensions: [...editorExtensions, tooltips({ parent: tooltipHost }), showTooltip.of(staticTooltip("first"))],
}),
});
const secondView = new EditorView({
parent: secondEditor,
state: EditorState.create({
doc: "select second",
extensions: [tooltips({ parent: root }), showTooltip.of(staticTooltip("second"))],
extensions: [...editorExtensions, tooltips({ parent: tooltipHost }), showTooltip.of(staticTooltip("second"))],
}),
});
@@ -46,8 +50,13 @@ describe("CodeMirror tooltip app-root portal", () => {
const secondContainer = secondTooltip.parentElement!;
expect(splitPane.contains(firstTooltip)).toBe(false);
expect(firstContainer.parentElement).toBe(root);
expect(secondContainer.parentElement).toBe(root);
expect(firstContainer.parentElement).toBe(tooltipHost);
expect(secondContainer.parentElement).toBe(tooltipHost);
expect(root.children).toHaveLength(1);
expect(tooltipHost.style.width).toBe("0px");
expect(tooltipHost.style.height).toBe("0px");
expect(getComputedStyle(firstContainer).height).toBe("100%");
expect(getComputedStyle(firstContainer).backgroundColor).toBe("rgb(255, 255, 255)");
expect(firstContainer).not.toBe(secondContainer);
expect(firstTooltip.style.position).toBe("fixed");
expect(secondTooltip.style.position).toBe("fixed");
@@ -2,13 +2,19 @@ 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 appSource = readFileSync(new URL("../../../App.vue", import.meta.url), "utf8");
const globalStylesSource = readFileSync(new URL("../../../styles/globals.css", import.meta.url), "utf8");
describe("QueryEditor tooltip container", () => {
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;');
it("portals CodeMirror tooltips to a zero-sized app host", () => {
expect(queryEditorSource).toContain("const editorElement = editorRef.value;");
expect(queryEditorSource).toContain("if (!editorElement) return;");
expect(queryEditorSource).toContain('querySelector<HTMLElement>("#dbx-query-editor-tooltip-root") ?? editorElement');
expect(queryEditorSource).toContain("tooltips({ parent: tooltipParent })");
expect(queryEditorSource).toContain("new EditorView({ state, parent: editorElement })");
expect(queryEditorSource).not.toContain("tooltips({ parent: document.body })");
expect(appSource).toContain('id="dbx-query-editor-tooltip-root"');
expect(appSource).toContain('class="fixed left-0 top-0 z-[70] h-0 w-0 overflow-visible"');
});
it("keeps the app root viewport-sized without a transformed containing block", () => {