fix(grid): align expanded condition caret with wrapped text

This commit is contained in:
zdzfeer
2026-09-26 21:47:10 +08:00
committed by GitHub
parent c5e1cc35d2
commit 4425fb72c6
2 changed files with 71 additions and 3 deletions
@@ -84,8 +84,18 @@ const highlightTokens = computed(() => tokenizeDataGridCondition(modelValue.valu
const collapsedHighlightScrollLeft = ref(0);
const expandedHighlightScrollLeft = ref(0);
const expandedHighlightScrollTop = ref(0);
// The highlight layer is a plain div and never gives up content width to a
// scrollbar, while the transparent-text textarea above it does. Both wrap with
// `white-space: pre-wrap` + `overflow-wrap: anywhere`, so an uncompensated
// scrollbar makes the two layers break at different characters and the visible
// text drifts away from the caret. Compensate with the measured width instead
// of assuming one.
const expandedHighlightScrollbarWidth = ref(0);
const collapsedHighlightStyle = computed<CSSProperties>(() => ({ transform: `translateX(${-collapsedHighlightScrollLeft.value}px)` }));
const expandedHighlightStyle = computed<CSSProperties>(() => ({ transform: `translate(${-expandedHighlightScrollLeft.value}px, ${-expandedHighlightScrollTop.value}px)` }));
const expandedHighlightStyle = computed<CSSProperties>(() => ({
transform: `translate(${-expandedHighlightScrollLeft.value}px, ${-expandedHighlightScrollTop.value}px)`,
"--data-grid-condition-highlight-scrollbar": `${expandedHighlightScrollbarWidth.value}px`,
}));
function highlightTokenClass(type: DataGridConditionTokenType): string | undefined {
if (type === "plain") return undefined;
@@ -177,6 +187,21 @@ function fitExpandedHeightToOverlay() {
}
}
function syncExpandedHighlightScrollbar() {
const overlay = overlayRef.value;
if (!overlay) return;
const scrollbar = Math.max(0, overlay.offsetWidth - overlay.clientWidth);
if (scrollbar !== expandedHighlightScrollbarWidth.value) expandedHighlightScrollbarWidth.value = scrollbar;
}
function syncExpandedLayout() {
syncExpandedHighlightScrollbar();
// Re-fit once the compensation has been applied: the highlight layer now
// wraps exactly like the textarea, so the pane can size itself to the real
// content height instead of leaving a scrollbar that only the textarea sees.
void nextTick(fitExpandedHeightToOverlay);
}
function measureExpandedRect(input: HTMLTextAreaElement) {
const inputRect = input.getBoundingClientRect();
const control = controlRef.value;
@@ -238,7 +263,7 @@ function resizeEditor(forceExpand = false) {
const nextRect = measureExpandedRect(input);
expandedRect.value = nextRect;
expandedHeight.value = measureExpandedHeight(input, nextRect);
void nextTick(fitExpandedHeightToOverlay);
void nextTick(syncExpandedLayout);
}
expanded.value = nextExpanded;
updateSuggestionPosition();
@@ -923,7 +948,7 @@ defineExpose({ focus, dismiss: editor.dismiss, rememberHistory: editor.rememberH
min-width: 0;
height: auto;
margin-right: calc(-1 * var(--data-grid-expanded-scrollbar-offset));
padding: 0 calc(var(--data-grid-condition-suffix-width) + 0.5rem) 0.0625rem calc(var(--data-grid-condition-prefix-indent) + 0.125rem);
padding: 0 calc(var(--data-grid-condition-suffix-width) + 0.5rem + var(--data-grid-condition-highlight-scrollbar, 0px)) 0.0625rem calc(var(--data-grid-condition-prefix-indent) + 0.125rem);
white-space: pre-wrap;
overflow-wrap: anywhere;
}
@@ -379,6 +379,49 @@ describe("DataGridConditionEditor quote completion", () => {
expect(expandedHighlight?.style.transform).toBe("translate(0px, 0px)");
vi.unstubAllGlobals();
});
it("compensates the expanded highlight layer for the textarea scrollbar", 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.focus();
input.setSelectionRange(input.value.length, input.value.length);
input.dispatchEvent(new Event("focus", { bubbles: true }));
await nextTick();
await nextTick();
const overlay = document.body.querySelector(".data-grid-topbar-condition-input--expanded") as HTMLTextAreaElement | null;
const expandedHighlight = document.body.querySelector(".data-grid-condition-highlight--expanded") as HTMLElement | null;
expect(overlay).toBeTruthy();
expect(expandedHighlight).toBeTruthy();
// A vertical scrollbar takes content width away from the textarea but not
// from the plain-div highlight layer. Both wrap with `pre-wrap` +
// `overflow-wrap: anywhere`, so unless the layer gives up the same width the
// two break at different characters and the caret drifts off the visible text.
Object.defineProperties(overlay as HTMLTextAreaElement, {
offsetWidth: { configurable: true, value: 178 },
clientWidth: { configurable: true, value: 168 },
});
overlay?.dispatchEvent(new Event("input", { bubbles: true }));
await nextTick();
await nextTick();
await nextTick();
expect(expandedHighlight?.style.getPropertyValue("--data-grid-condition-highlight-scrollbar")).toBe("10px");
vi.unstubAllGlobals();
});
});
describe("DataGridConditionEditor Chinese column matching", () => {