diff --git a/apps/desktop/src/App.vue b/apps/desktop/src/App.vue index 75bed0b06..652fa13b9 100644 --- a/apps/desktop/src/App.vue +++ b/apps/desktop/src/App.vue @@ -2613,6 +2613,7 @@ onUnmounted(() => { @saved="onQueryEditorObjectSourceSaved" /> + diff --git a/apps/desktop/src/components/editor/QueryEditor.vue b/apps/desktop/src/components/editor/QueryEditor.vue index 20df08697..912879a92 100644 --- a/apps/desktop/src/components/editor/QueryEditor.vue +++ b/apps/desktop/src/components/editor/QueryEditor.vue @@ -3857,7 +3857,9 @@ onMounted(async () => { { decorations: (v) => v.decorations }, ); - const tooltipParent = editorRef.value.closest("#root") ?? editorRef.value; + const editorElement = editorRef.value; + if (!editorElement) return; + const tooltipParent = editorElement.closest("#root")?.querySelector("#dbx-query-editor-tooltip-root") ?? editorElement; const state = EditorState.create({ doc: props.modelValue, selection: normalizedEditorSelection(props.initialSelection, props.modelValue.length), @@ -4189,7 +4191,7 @@ onMounted(async () => { ], }); - view.value = new EditorView({ state, parent: editorRef.value }); + view.value = new EditorView({ state, parent: editorElement }); registerEditorScrollbarPointerGuard(view.value); view.value.scrollDOM.addEventListener("scroll", scheduleEditorViewportEmit, { passive: true, diff --git a/apps/desktop/src/lib/__tests__/editor/queryEditorTooltipParent.dom.spec.ts b/apps/desktop/src/lib/__tests__/editor/queryEditorTooltipParent.dom.spec.ts index e2054bcbc..bf8b4035b 100644 --- a/apps/desktop/src/lib/__tests__/editor/queryEditorTooltipParent.dom.spec.ts +++ b/apps/desktop/src/lib/__tests__/editor/queryEditorTooltipParent.dom.spec.ts @@ -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 = '
'; + document.body.innerHTML = '
'; const root = document.querySelector("#root")!; const splitPane = document.querySelector("[data-split-pane]")!; + const tooltipHost = document.querySelector("#dbx-query-editor-tooltip-root")!; const firstEditor = document.querySelector('[data-editor="first"]')!; const secondEditor = document.querySelector('[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"); diff --git a/apps/desktop/src/lib/__tests__/editor/queryEditorTooltipParent.spec.ts b/apps/desktop/src/lib/__tests__/editor/queryEditorTooltipParent.spec.ts index 15fa10ee0..3c5b3c6c2 100644 --- a/apps/desktop/src/lib/__tests__/editor/queryEditorTooltipParent.spec.ts +++ b/apps/desktop/src/lib/__tests__/editor/queryEditorTooltipParent.spec.ts @@ -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("#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("#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", () => {