From f3e55a8337f4643040b77cb5e3c1c6f665ded7bd Mon Sep 17 00:00:00 2001 From: zipg Date: Wed, 9 Sep 2026 22:35:58 +0800 Subject: [PATCH] fix(grid): reset filter editor caret offset on expand --- .../grid/DataGridConditionEditor.vue | 22 +++++++++++---- .../__tests__/DataGridConditionEditor.spec.ts | 28 +++++++++++++++++++ 2 files changed, 44 insertions(+), 6 deletions(-) diff --git a/apps/desktop/src/components/grid/DataGridConditionEditor.vue b/apps/desktop/src/components/grid/DataGridConditionEditor.vue index 6c242ab7b8..a175adb8ec 100644 --- a/apps/desktop/src/components/grid/DataGridConditionEditor.vue +++ b/apps/desktop/src/components/grid/DataGridConditionEditor.vue @@ -81,10 +81,11 @@ const hasValue = computed(() => modelValue.value.trim().length > 0); // textarea, so keywords / fields / values are colored without losing caret // and selection behavior. const highlightTokens = computed(() => tokenizeDataGridCondition(modelValue.value)); -const highlightScrollLeft = ref(0); -const highlightScrollTop = ref(0); -const collapsedHighlightStyle = computed(() => ({ transform: `translateX(${-highlightScrollLeft.value}px)` })); -const expandedHighlightStyle = computed(() => ({ transform: `translate(${-highlightScrollLeft.value}px, ${-highlightScrollTop.value}px)` })); +const collapsedHighlightScrollLeft = ref(0); +const expandedHighlightScrollLeft = ref(0); +const expandedHighlightScrollTop = ref(0); +const collapsedHighlightStyle = computed(() => ({ transform: `translateX(${-collapsedHighlightScrollLeft.value}px)` })); +const expandedHighlightStyle = computed(() => ({ transform: `translate(${-expandedHighlightScrollLeft.value}px, ${-expandedHighlightScrollTop.value}px)` })); function highlightTokenClass(type: DataGridConditionTokenType): string | undefined { if (type === "plain") return undefined; @@ -93,8 +94,12 @@ function highlightTokenClass(type: DataGridConditionTokenType): string | undefin function onEditorScroll(event: Event) { const target = event.currentTarget as HTMLTextAreaElement; - highlightScrollLeft.value = target.scrollLeft; - highlightScrollTop.value = target.scrollTop; + if (target === overlayRef.value) { + expandedHighlightScrollLeft.value = target.scrollLeft; + expandedHighlightScrollTop.value = target.scrollTop; + } else { + collapsedHighlightScrollLeft.value = target.scrollLeft; + } } const emptyHistoryText = computed(() => (modelValue.value.trim() ? props.historyNoMatchesText : props.historyEmptyText)); const activeSuggestionId = computed(() => (editor.highlightedIndex.value >= 0 ? `${suggestionListId}-${editor.highlightedIndex.value}` : undefined)); @@ -221,10 +226,15 @@ function resizeEditor(forceExpand = false) { expandAfterComposition = true; return; } + const wasExpanded = expanded.value; const overlayFocused = document.activeElement === overlayRef.value; const focused = document.activeElement === input || overlayFocused; const nextExpanded = focused && shouldExpand(input) && (forceExpand || expanded.value); if (nextExpanded) { + if (!wasExpanded) { + expandedHighlightScrollLeft.value = 0; + expandedHighlightScrollTop.value = 0; + } const nextRect = measureExpandedRect(input); expandedRect.value = nextRect; expandedHeight.value = measureExpandedHeight(input, nextRect); diff --git a/apps/desktop/src/components/grid/__tests__/DataGridConditionEditor.spec.ts b/apps/desktop/src/components/grid/__tests__/DataGridConditionEditor.spec.ts index d87a2caf8e..52d83fca33 100644 --- a/apps/desktop/src/components/grid/__tests__/DataGridConditionEditor.spec.ts +++ b/apps/desktop/src/components/grid/__tests__/DataGridConditionEditor.spec.ts @@ -445,6 +445,34 @@ describe("DataGridConditionEditor quote completion", () => { vi.unstubAllGlobals(); }); + it("does not carry the collapsed horizontal scroll into the expanded highlight layer", async () => { + vi.spyOn(HTMLElement.prototype, "getBoundingClientRect").mockImplementation(function (this: HTMLElement) { + const textWidth = (this.textContent?.length ?? 0) * 8; + const width = this.classList.contains("data-grid-topbar-condition-pane--expanded") ? 160 : textWidth; + return { x: 0, y: 0, left: 0, top: 0, right: width, bottom: 24, width, height: 24, toJSON: () => ({}) } as DOMRect; + }); + vi.spyOn(HTMLCanvasElement.prototype, "getContext").mockReturnValue(null); + vi.stubGlobal("requestAnimationFrame", (callback: FrameRequestCallback) => { + callback(0); + return 1; + }); + const { input } = mountEditor("where", "test_item_id=12 and test_item_name=''"); + mockTextareaMetrics(input, { clientWidth: 80, scrollWidth: 320 }); + input.scrollLeft = 128; + input.dispatchEvent(new Event("scroll", { bubbles: true })); + input.focus(); + input.setSelectionRange(input.value.length, input.value.length); + input.dispatchEvent(new Event("focus", { bubbles: true })); + + await nextTick(); + await nextTick(); + + const expandedHighlight = document.body.querySelector(".data-grid-condition-highlight--expanded") as HTMLElement | null; + expect(expandedHighlight).toBeTruthy(); + expect(expandedHighlight?.style.transform).toBe("translate(0px, 0px)"); + vi.unstubAllGlobals(); + }); + it("positions suggestions below the measured expanded editor height", () => { const source = readFileSync(resolve(process.cwd(), "apps/desktop/src/components/grid/DataGridConditionEditor.vue"), "utf8"); const expandedPaneCss = source.match(/\.data-grid-topbar-condition-pane--expanded\s*\{(?[\s\S]*?)\n\}/)?.groups?.body;