diff --git a/apps/desktop/src/components/grid/DataGrid.vue b/apps/desktop/src/components/grid/DataGrid.vue
index 43cb8263e1..cc7d37b8a5 100644
--- a/apps/desktop/src/components/grid/DataGrid.vue
+++ b/apps/desktop/src/components/grid/DataGrid.vue
@@ -245,7 +245,7 @@ import {
type DataGridScrollPosition,
} from "@/lib/dataGrid/dataGridInfiniteScroll";
import { resolveDataGridWheelScroll } from "@/lib/dataGrid/dataGridWheel";
-import { CANVAS_DATA_GRID_ROW_HEIGHT, MAX_CANVAS_DATA_GRID_PIXEL_RATIO, canvasDataGridActionOverlayWidth, canvasDataGridActionReservedWidth, dataGridSearchMatchKey, drawCanvasDataGrid, type CanvasDevicePixelSize } from "@/lib/dataGrid/canvasDataGridRenderer";
+import { CANVAS_DATA_GRID_ROW_HEIGHT, MAX_CANVAS_DATA_GRID_PIXEL_RATIO, canvasDataGridActionOverlayWidth, canvasDataGridActionReservedWidth, dataGridSearchMatchKey, drawCanvasDataGrid, resolveCanvasCellTextLayout, type CanvasDevicePixelSize } from "@/lib/dataGrid/canvasDataGridRenderer";
import { resolveCrosshairTarget, type CrosshairTarget } from "@/lib/dataGrid/crosshairHighlight";
import { DATA_GRID_DARK_STRIPED_ROW_BG, DATA_GRID_LIGHT_STRIPED_ROW_BG, dataGridActiveRowBackground } from "@/lib/dataGrid/dataGridPaintTheme";
import { createRowLowerTextCache } from "@/lib/dataGrid/dataGridRowLowerText";
@@ -3451,6 +3451,7 @@ function onScrollerScroll(e: Event) {
} else {
syncHeaderScroll(e);
}
+ if (editingCell.value) scheduleActiveCellEditTextareaResize();
markGridScrolling();
}
@@ -4420,6 +4421,7 @@ async function startCellEdit(rowId: number, columnIndex: number, expanded: boole
}
async function startDomCellEdit(rowId: number, columnIndex: number, displayText: string, event: MouseEvent) {
+ const target = event.currentTarget;
if (!(await hydrateLargeValueCell(rowId, columnIndex))) return;
const item = getRowItem(rowId);
const editText = item ? cellEditorTextForValue(item.data[columnIndex], columnIndex) : displayText;
@@ -4429,7 +4431,7 @@ async function startDomCellEdit(rowId: number, columnIndex: number, displayText:
cellEditContentNeedsExpandedEditor({
displayText,
editText,
- target: event.currentTarget,
+ target,
}),
);
}
@@ -7409,6 +7411,7 @@ function onCanvasScroll(event: Event) {
}
if (headerRef.value && headerRef.value.scrollLeft !== scrollLeft) headerRef.value.scrollLeft = scrollLeft;
recordScrollPosition({ top: scrollTop, left: scrollLeft });
+ if (editingCell.value) scheduleActiveCellEditTextareaResize();
markGridScrolling();
scheduleCanvasDraw();
}
@@ -7643,8 +7646,29 @@ function canvasCellContentOverflows(item: RowItem, actualColIdx: number, visible
const displayText = formatCellCached(item.data[actualColIdx], actualColIdx);
const editText = cellEditorTextForValue(item.data[actualColIdx], actualColIdx);
if (editText.includes("\n") || editText.includes("\r") || editText.length > displayText.length) return true;
- const textWidth = measureCellTextWidth(displayText, `400 ${tableFontSize.value}px ${tableFontFamily.value}`);
- return textWidth > Math.max(0, cellWidth - 24);
+ const canvas = activeCanvasSurface();
+ const context = canvas?.getContext("2d");
+ if (!context) {
+ const textWidth = measureCellTextWidth(displayText, `400 ${tableFontSize.value}px ${tableFontFamily.value}`);
+ return textWidth > Math.max(0, cellWidth - 24);
+ }
+
+ const isRightAlign = columnAligns.value[visibleColIdx] === "right";
+ const actionCell = canvasRightAlignedActionCell.value;
+ const reservedWidth = actionCell?.rowIndex === item.displayIndex && actionCell.visibleColIdx === visibleColIdx ? actionCell.reservedWidth : 0;
+ const { maxWidth } = resolveCanvasCellTextLayout({
+ drawX: 0,
+ colWidth: cellWidth,
+ dpr: 1,
+ isRightAlign,
+ reservedWidth,
+ });
+ context.save();
+ context.font = `400 ${tableFontSize.value}px ${tableFontFamily.value}`;
+ if ("fontVariantNumeric" in context) (context as CanvasRenderingContext2D & { fontVariantNumeric?: string }).fontVariantNumeric = "tabular-nums";
+ const textWidth = context.measureText(displayText).width;
+ context.restore();
+ return textWidth > maxWidth;
}
function canvasCellViewportRect(rowIndex: number, visibleColIdx: number) {
@@ -8511,10 +8535,11 @@ function showTransposeCellDetails(rowIndex: number, actualColIdx: number) {
}
async function onTransposeCellDblClick(rowIndex: number, actualColIdx: number, displayText: string, event: MouseEvent) {
+ const target = event.currentTarget;
const item = displayItemAt(rowIndex);
if (!item) return;
if (!canEditCellItem(item, actualColIdx)) {
- await startReadonlyCellTextSelection(item.id, actualColIdx, displayText, cellEditContentNeedsExpandedEditor({ displayText, editText: cellEditorTextForValue(item.data[actualColIdx], actualColIdx), target: event.currentTarget }));
+ await startReadonlyCellTextSelection(item.id, actualColIdx, displayText, cellEditContentNeedsExpandedEditor({ displayText, editText: cellEditorTextForValue(item.data[actualColIdx], actualColIdx), target }));
return;
}
await startDomCellEdit(item.id, actualColIdx, displayText, event);
@@ -10490,19 +10515,45 @@ function onCellEditTextareaPaste(event: ClipboardEvent) {
function resizeCellEditTextareaElement(textarea: HTMLTextAreaElement | null) {
if (!textarea) return;
- const textareaRect = textarea.getBoundingClientRect();
+ // The result grid is a scroll container, so an absolutely positioned editor
+ // cannot escape its bottom clipping edge. Use the cell's viewport rect as the
+ // anchor and position the editor itself in the viewport instead.
+ const anchorRect = textarea.parentElement?.getBoundingClientRect();
+ if (!anchorRect) return;
const visibleBottom = cellEditVisibleBottom(textarea);
- const availableHeight = Math.max(36, Math.floor(visibleBottom - textareaRect.top - 10));
+ const visibleTop = cellEditVisibleTop(textarea);
const metrics = cellEditTextMetrics(textarea);
const maxVisibleHeight = Math.ceil(metrics.lineHeight * 9.5 + metrics.verticalChrome);
- const maxHeight = Math.min(maxVisibleHeight, availableHeight);
const naturalHeight = cellEditNaturalHeight(textarea, metrics);
const targetMinHeight = Math.max(64, Math.min(naturalHeight, 120));
+ const preferredHeight = Math.min(maxVisibleHeight, Math.max(targetMinHeight, naturalHeight));
+ const spaceBelow = Math.max(0, Math.floor(visibleBottom - anchorRect.top - 10));
+ const spaceAbove = Math.max(0, Math.floor(anchorRect.bottom - visibleTop - 10));
+ const placeAbove = spaceBelow < preferredHeight && spaceAbove > spaceBelow;
+ const availableHeight = placeAbove ? spaceAbove : spaceBelow;
+ const maxHeight = Math.min(maxVisibleHeight, Math.max(36, availableHeight));
const minHeight = Math.min(targetMinHeight, maxHeight);
+ const height = Math.max(minHeight, Math.min(textarea.scrollHeight, maxHeight));
+ const top = placeAbove ? Math.max(visibleTop, Math.min(anchorRect.top, visibleBottom - height)) : anchorRect.top;
+ const horizontalInset = 7;
+ const desiredWidth = Math.max(0, anchorRect.width - horizontalInset * 2);
+ const maxViewportWidth = Math.max(0, window.innerWidth - horizontalInset * 2);
+ const width = Math.min(desiredWidth, maxViewportWidth);
+ const left = Math.min(Math.max(horizontalInset, anchorRect.left + horizontalInset), Math.max(horizontalInset, window.innerWidth - width - horizontalInset));
textarea.style.setProperty("--cell-edit-min-height", `${minHeight}px`);
textarea.style.setProperty("--cell-edit-max-height", `${maxHeight}px`);
+ textarea.style.left = `${left}px`;
+ textarea.style.width = `${width}px`;
textarea.style.height = "auto";
- textarea.style.height = `${Math.max(minHeight, Math.min(textarea.scrollHeight, maxHeight))}px`;
+ textarea.style.height = `${height}px`;
+ textarea.style.top = `${top}px`;
+ // Virtualized and contained scrollers can establish a fixed-position
+ // containing block in either viewport or scrolled-content coordinates. Use
+ // the rendered rect to correct that implementation detail back to viewport
+ // coordinates instead of guessing from the ancestor tree.
+ const positionedRect = textarea.getBoundingClientRect();
+ textarea.style.left = `${left + (left - positionedRect.left)}px`;
+ textarea.style.top = `${top + (top - positionedRect.top)}px`;
if (resetCellEditTextareaScrollOnResize) {
textarea.scrollTop = 0;
textarea.setSelectionRange?.(0, 0);
@@ -10550,6 +10601,16 @@ function cellEditVisibleBottom(textarea: HTMLTextAreaElement): number {
return Math.min(...bottoms.filter((bottom) => Number.isFinite(bottom)));
}
+function cellEditVisibleTop(textarea: HTMLTextAreaElement): number {
+ const tops: number[] = [];
+ const scroller = cellEditScrollerElement(textarea);
+ const root = gridRef.value;
+ if (scroller) tops.push(scroller.getBoundingClientRect().top);
+ if (root) tops.push(root.getBoundingClientRect().top);
+ if (typeof window !== "undefined") tops.push(0);
+ return Math.max(...tops.filter((top) => Number.isFinite(top)));
+}
+
function scheduleCellEditTextareaResize(textarea: HTMLTextAreaElement | null) {
if (!textarea || typeof requestAnimationFrame !== "function") return;
requestAnimationFrame(() => {
@@ -12731,6 +12792,7 @@ function openGridSnapshot() {
@input="onCellEditTextareaInput"
@keydown.stop="onCellEditKeydown"
@paste.stop="onCellEditTextareaPaste"
+ @wheel.stop
/>