feat(grid): support selecting readonly cell text
This commit is contained in:
4 files changed
+268
-16
No files matched your search
@@ -68,6 +68,7 @@ import DataGridColumnHeader from "@/components/grid/DataGridColumnHeader.vue";
|
||||
import DataGridQueryControls from "@/components/grid/DataGridQueryControls.vue";
|
||||
import TemporalCellEditor from "@/components/grid/TemporalCellEditor.vue";
|
||||
import EnumCellEditor from "@/components/grid/EnumCellEditor.vue";
|
||||
import DataGridReadonlyTextSelection from "@/components/grid/DataGridReadonlyTextSelection.vue";
|
||||
import type { QueryResult, ColumnInfo, DatabaseType, ForeignKeyInfo, IndexInfo, TriggerInfo, TableInfoTab } from "@/types/database";
|
||||
import { isQueryExecutionErrorResult } from "@/lib/query/queryResultError";
|
||||
import { tableObjectSourceKind } from "@/lib/table/tableObjectSourceKind";
|
||||
@@ -560,6 +561,12 @@ const infiniteScrollEnabled = computed(() => props.paginationEnabled && settings
|
||||
const infiniteScrollMaxRows = computed(() => continuousQueryResultMaxRows(settingsStore.editorSettings.queryResultMaxRowsEnabled, settingsStore.editorSettings.queryResultMaxRows));
|
||||
const flatteningMultiLineEnabled = computed(() => settingsStore.editorSettings.flatteningMultiLineText);
|
||||
const expandedCellEditor = ref<{ rowId: number; col: number } | null>(null);
|
||||
const readonlyTextCell = ref<{
|
||||
rowId: number;
|
||||
col: number;
|
||||
value: string;
|
||||
expanded: boolean;
|
||||
} | null>(null);
|
||||
|
||||
function resolvedColumnComment(column: string, actualColIdx: number): string | undefined {
|
||||
return dataGridColumnCommentFor(columnCommentMap.value, column, props.sourceColumns?.[actualColIdx]);
|
||||
@@ -3357,6 +3364,7 @@ function cellUsesExpandedEditor(rowId: number | undefined, columnIndex: number):
|
||||
|
||||
async function startCellEdit(rowId: number, columnIndex: number, expanded: boolean) {
|
||||
if (!(await hydrateLargeValueCell(rowId, columnIndex))) return;
|
||||
closeReadonlyCellTextSelection();
|
||||
expandedCellEditor.value = expanded ? { rowId, col: columnIndex } : null;
|
||||
startEdit(rowId, columnIndex);
|
||||
}
|
||||
@@ -3376,15 +3384,39 @@ async function startDomCellEdit(rowId: number, columnIndex: number, displayText:
|
||||
);
|
||||
}
|
||||
|
||||
function showReadonlyCellDetailsOnDblClick(item: RowItem, rowIndex: number, visibleColIdx: number, actualColIdx: number): boolean {
|
||||
if (canEditCellItem(item, actualColIdx)) return false;
|
||||
showCellDetailsForVisibleCell(rowIndex, visibleColIdx, actualColIdx);
|
||||
return true;
|
||||
function readonlyTextCellMatches(rowId: number | undefined, columnIndex: number): boolean {
|
||||
return !!readonlyTextCell.value && readonlyTextCell.value.rowId === rowId && readonlyTextCell.value.col === columnIndex;
|
||||
}
|
||||
|
||||
async function onDomCellDblClick(item: RowItem, rowIndex: number, visibleColIdx: number, actualColIdx: number, event: MouseEvent) {
|
||||
async function startReadonlyCellTextSelection(rowId: number, columnIndex: number, displayText: string, expanded: boolean) {
|
||||
if (!(await hydrateLargeValueCell(rowId, columnIndex))) return;
|
||||
const item = getRowItem(rowId);
|
||||
if (!item) return;
|
||||
const value = cellEditorTextForValue(item.data[columnIndex], columnIndex);
|
||||
readonlyTextCell.value = {
|
||||
rowId,
|
||||
col: columnIndex,
|
||||
value,
|
||||
expanded: expanded || value.includes("\n") || value.includes("\r") || value.length > displayText.length,
|
||||
};
|
||||
}
|
||||
|
||||
function closeReadonlyCellTextSelection(restoreGridFocus = false) {
|
||||
readonlyTextCell.value = null;
|
||||
if (restoreGridFocus) nextTick(() => gridRef.value?.focus({ preventScroll: true }));
|
||||
}
|
||||
|
||||
function escapeReadonlyCellTextSelection() {
|
||||
closeReadonlyCellTextSelection(true);
|
||||
}
|
||||
|
||||
async function onDomCellDblClick(item: RowItem, actualColIdx: number, event: MouseEvent) {
|
||||
if (booleanCellsUseCheckbox.value && isBooleanGridCell(item, actualColIdx) && canEditCellItem(item, actualColIdx)) return;
|
||||
if (showReadonlyCellDetailsOnDblClick(item, rowIndex, visibleColIdx, actualColIdx)) return;
|
||||
if (!canEditCellItem(item, actualColIdx)) {
|
||||
const displayText = formatCellCached(item.data[actualColIdx], actualColIdx);
|
||||
await startReadonlyCellTextSelection(item.id, actualColIdx, displayText, cellEditContentNeedsExpandedEditor({ displayText, editText: cellEditorTextForValue(item.data[actualColIdx], actualColIdx), target: event.currentTarget }));
|
||||
return;
|
||||
}
|
||||
await startDomCellEdit(item.id, actualColIdx, formatCellCached(item.data[actualColIdx], actualColIdx), event);
|
||||
}
|
||||
|
||||
@@ -5794,7 +5826,7 @@ function onCanvasMouseMove(event: MouseEvent) {
|
||||
if (canvasRef.value) {
|
||||
const overBooleanInteractive =
|
||||
booleanCellsUseCheckbox.value && hit != null && !hit.rowNumber && hitItem != null && actualColIdx !== undefined && isBooleanGridCell(hitItem, actualColIdx) && canEditCellItem(hitItem, actualColIdx) && booleanInteractiveHitFromCanvasEvent(hitItem, hit, actualColIdx, event);
|
||||
canvasRef.value.style.cursor = hit?.rowNumber ? "default" : overBooleanInteractive ? "pointer" : hitItem && actualColIdx !== undefined && canEditCellItem(hitItem, actualColIdx) ? "text" : "cell";
|
||||
canvasRef.value.style.cursor = hit?.rowNumber ? "default" : overBooleanInteractive ? "pointer" : hitItem && actualColIdx !== undefined ? "text" : "cell";
|
||||
}
|
||||
if (next?.rowIndex === canvasHoverCell.value?.rowIndex && next?.visibleColIdx === canvasHoverCell.value?.visibleColIdx) {
|
||||
return;
|
||||
@@ -5933,7 +5965,10 @@ async function onCanvasDblClick(event: MouseEvent) {
|
||||
const item = displayItemAt(hit.rowIndex);
|
||||
const actualColIdx = visibleColumnIndexes.value[hit.visibleColIdx];
|
||||
if (!item || actualColIdx === undefined) return;
|
||||
if (showReadonlyCellDetailsOnDblClick(item, item.displayIndex, hit.visibleColIdx, actualColIdx)) return;
|
||||
if (!canEditCellItem(item, actualColIdx)) {
|
||||
await startReadonlyCellTextSelection(item.id, actualColIdx, formatCellCached(item.data[actualColIdx], actualColIdx), canvasCellContentOverflows(item, actualColIdx, hit.visibleColIdx));
|
||||
return;
|
||||
}
|
||||
if (booleanCellsUseCheckbox.value && isBooleanGridCell(item, actualColIdx) && canEditCellItem(item, actualColIdx)) return;
|
||||
await startCellEdit(item.id, actualColIdx, canvasCellContentOverflows(item, actualColIdx, hit.visibleColIdx));
|
||||
}
|
||||
@@ -6005,6 +6040,17 @@ const canvasEditingCell = computed(() => {
|
||||
};
|
||||
});
|
||||
|
||||
const canvasReadonlyTextCell = computed(() => {
|
||||
const cell = readonlyTextCell.value;
|
||||
if (!cell || !useCanvasGridRows.value) return null;
|
||||
const rowIndex = displayRowIndexById(cell.rowId);
|
||||
const visibleColIdx = visibleColumnIndexes.value.indexOf(cell.col);
|
||||
if (rowIndex < 0 || visibleColIdx < 0) return null;
|
||||
const rect = canvasCellViewportRect(rowIndex, visibleColIdx);
|
||||
if (!rect) return null;
|
||||
return { ...cell, rowIndex, visibleColIdx, actualColIdx: cell.col, rect };
|
||||
});
|
||||
|
||||
function canvasEffectiveViewportWidth(): number {
|
||||
return canvasViewportWidth.value || canvasScrollerElement()?.clientWidth || 0;
|
||||
}
|
||||
@@ -6036,6 +6082,20 @@ const canvasEditingCellStyle = computed(() => {
|
||||
};
|
||||
});
|
||||
|
||||
const canvasReadonlyTextCellStyle = computed(() => {
|
||||
const cell = canvasReadonlyTextCell.value;
|
||||
if (!cell) return {};
|
||||
const viewportWidth = canvasEffectiveViewportWidth();
|
||||
const clippedLeft = Math.max(rowNumberWidth.value, cell.rect.left);
|
||||
const clippedRight = viewportWidth > 0 ? Math.min(viewportWidth, cell.rect.left + cell.rect.width) : cell.rect.left + cell.rect.width;
|
||||
return {
|
||||
left: `${clippedLeft}px`,
|
||||
top: `${cell.rect.top}px`,
|
||||
width: `${Math.max(0, clippedRight - clippedLeft)}px`,
|
||||
height: `${cell.rect.height}px`,
|
||||
};
|
||||
});
|
||||
|
||||
const canvasDetailButtonCell = computed(() => {
|
||||
if (!useCanvasGridRows.value || isScrolling.value) return null;
|
||||
const target = hoveredDetailCell.value ?? quickDownloadMenuCell.value ?? (showCellDetail.value ? detailCell.value : null);
|
||||
@@ -6718,7 +6778,7 @@ async function onTransposeCellDblClick(rowIndex: number, actualColIdx: number, d
|
||||
const item = displayItemAt(rowIndex);
|
||||
if (!item) return;
|
||||
if (!canEditCellItem(item, actualColIdx)) {
|
||||
showTransposeCellDetails(rowIndex, actualColIdx);
|
||||
await startReadonlyCellTextSelection(item.id, actualColIdx, displayText, cellEditContentNeedsExpandedEditor({ displayText, editText: cellEditorTextForValue(item.data[actualColIdx], actualColIdx), target: event.currentTarget }));
|
||||
return;
|
||||
}
|
||||
await startDomCellEdit(item.id, actualColIdx, displayText, event);
|
||||
@@ -8292,6 +8352,7 @@ watch(
|
||||
clearResetScrollAfterResult();
|
||||
resetGridVerticalScroll();
|
||||
}
|
||||
closeReadonlyCellTextSelection();
|
||||
clearCellSelection();
|
||||
clearRowSelection();
|
||||
invalidateSyntheticContextSelection();
|
||||
@@ -10227,7 +10288,7 @@ const gridContextMenuItems = computed<ContextMenuItem[]>(() => {
|
||||
'bg-yellow-500/10 cell-dirty': displayItems[cell.recordIndex]?.isDirtyCol[cell.valueIndex],
|
||||
'bg-yellow-200/60 dark:bg-yellow-500/20': cellIsSearchMatch(cell.recordIndex, cell.valueIndex),
|
||||
'ring-2 ring-inset ring-yellow-500 bg-yellow-300/60 dark:bg-yellow-500/40': cellIsCurrentMatch(cell.recordIndex, cell.valueIndex),
|
||||
'cursor-text': !isScrolling && canEditCellItem(displayItems[cell.recordIndex], cell.valueIndex),
|
||||
'cursor-text': !isScrolling,
|
||||
'hover:bg-gray-200 hover:text-foreground dark:hover:bg-gray-800':
|
||||
!isScrolling && canEditCellItem(displayItems[cell.recordIndex], cell.valueIndex) && !transposeRecordUsesSelectionVisual(cell.recordIndex) && !transposeRecordUsesActiveHighlight(cell.recordIndex) && !transposeCellIsSelected(cell.recordIndex, cell.valueIndex),
|
||||
},
|
||||
@@ -10243,7 +10304,10 @@ const gridContextMenuItems = computed<ContextMenuItem[]>(() => {
|
||||
@contextmenu="onTransposeCellContext(cell.recordIndex, cell.valueIndex, $event)"
|
||||
@dblclick.stop="onTransposeCellDblClick(cell.recordIndex, cell.valueIndex, cell.display, $event)"
|
||||
>
|
||||
<template v-if="editingCell?.rowId === displayItems[cell.recordIndex]?.id && editingCell?.col === cell.valueIndex">
|
||||
<template v-if="readonlyTextCellMatches(displayItems[cell.recordIndex]?.id, cell.valueIndex)">
|
||||
<DataGridReadonlyTextSelection :value="readonlyTextCell!.value" :expanded="readonlyTextCell!.expanded" @close="closeReadonlyCellTextSelection" @escape="escapeReadonlyCellTextSelection" />
|
||||
</template>
|
||||
<template v-else-if="editingCell?.rowId === displayItems[cell.recordIndex]?.id && editingCell?.col === cell.valueIndex">
|
||||
<TemporalCellEditor
|
||||
v-if="temporalEditorConfigForColumn(cell.valueIndex)"
|
||||
v-model="editValue"
|
||||
@@ -10803,6 +10867,9 @@ const gridContextMenuItems = computed<ContextMenuItem[]>(() => {
|
||||
@dblclick="onCanvasDblClick"
|
||||
/>
|
||||
<div ref="canvasOverlayRef" class="canvas-grid-overlay dbx-data-grid-font-family sticky left-0 top-0 z-10 overflow-visible" :style="canvasOverlayStyle">
|
||||
<div v-if="canvasReadonlyTextCell" class="absolute pointer-events-auto z-20 tabular-nums" :style="canvasReadonlyTextCellStyle" @mousedown.stop @click.stop>
|
||||
<DataGridReadonlyTextSelection :value="canvasReadonlyTextCell.value" :expanded="canvasReadonlyTextCell.expanded" @close="closeReadonlyCellTextSelection" @escape="escapeReadonlyCellTextSelection" />
|
||||
</div>
|
||||
<div v-if="canvasEditingCell" class="absolute pointer-events-auto z-20 tabular-nums" :style="canvasEditingCellStyle" @mousedown.stop @click.stop>
|
||||
<TemporalCellEditor
|
||||
v-if="temporalEditorConfigForColumn(canvasEditingCell.actualColIdx)"
|
||||
@@ -10936,7 +11003,7 @@ const gridContextMenuItems = computed<ContextMenuItem[]>(() => {
|
||||
'data-grid-row--draft': item.isDraft && !isRowActive(item.displayIndex),
|
||||
'data-grid-row--striped': !item.isNew && !item.isDraft && !item.isDeleted && !isRowActive(item.displayIndex) && item.displayIndex % 2 === 1,
|
||||
'active-row': isRowActive(item.displayIndex) && !item.isDeleted,
|
||||
'relative z-20 overflow-visible': editingCell?.rowId === item.id,
|
||||
'relative z-20 overflow-visible': editingCell?.rowId === item.id || readonlyTextCell?.rowId === item.id,
|
||||
}"
|
||||
:style="dataGridRowStyle(item)"
|
||||
:data-row-index="item.displayIndex"
|
||||
@@ -10987,11 +11054,12 @@ const gridContextMenuItems = computed<ContextMenuItem[]>(() => {
|
||||
'bg-yellow-200/60 dark:bg-yellow-500/20': cellIsSearchMatch(item.displayIndex, col.actualColIdx),
|
||||
'ring-2 ring-inset ring-yellow-500 bg-yellow-300/60 dark:bg-yellow-500/40': cellIsCurrentMatch(item.displayIndex, col.actualColIdx),
|
||||
'tabular-nums': typeof item.data[col.actualColIdx] === 'number',
|
||||
'cursor-text': !isScrolling && !canEditCellItem(item, col.actualColIdx),
|
||||
'cursor-text hover:bg-gray-200 hover:text-foreground dark:hover:bg-gray-800': !isScrolling && canEditCellItem(item, col.actualColIdx) && !(booleanCellsUseCheckbox && isBooleanGridCell(item, col.actualColIdx) && item.data[col.actualColIdx] !== null),
|
||||
'cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-800': !isScrolling && booleanCellsUseCheckbox && isBooleanGridCell(item, col.actualColIdx) && item.data[col.actualColIdx] !== null && canEditCellItem(item, col.actualColIdx),
|
||||
'line-through': item.isDeleted,
|
||||
'overflow-visible z-20 border-r-transparent': editingCell?.rowId === item.id && editingCell?.col === col.actualColIdx,
|
||||
'overflow-hidden': !(editingCell?.rowId === item.id && editingCell?.col === col.actualColIdx),
|
||||
'overflow-visible z-20 border-r-transparent': (editingCell?.rowId === item.id && editingCell?.col === col.actualColIdx) || readonlyTextCellMatches(item.id, col.actualColIdx),
|
||||
'overflow-hidden': !((editingCell?.rowId === item.id && editingCell?.col === col.actualColIdx) || readonlyTextCellMatches(item.id, col.actualColIdx)),
|
||||
},
|
||||
]"
|
||||
@mousedown="
|
||||
@@ -11000,11 +11068,14 @@ const gridContextMenuItems = computed<ContextMenuItem[]>(() => {
|
||||
"
|
||||
@mouseenter="onCellMouseenter(item.displayIndex, col.visibleColIdx, col.actualColIdx)"
|
||||
@mouseleave="onCellMouseleave(item.displayIndex, col.actualColIdx)"
|
||||
@dblclick="onDomCellDblClick(item, item.displayIndex, col.visibleColIdx, col.actualColIdx, $event)"
|
||||
@dblclick="onDomCellDblClick(item, col.actualColIdx, $event)"
|
||||
:data-visible-col-index="col.visibleColIdx"
|
||||
@contextmenu="onCellContext(item.id, item.displayIndex, col.actualColIdx, col.visibleColIdx, $event)"
|
||||
>
|
||||
<template v-if="editingCell?.rowId === item.id && editingCell?.col === col.actualColIdx">
|
||||
<template v-if="readonlyTextCellMatches(item.id, col.actualColIdx)">
|
||||
<DataGridReadonlyTextSelection :value="readonlyTextCell!.value" :expanded="readonlyTextCell!.expanded" @close="closeReadonlyCellTextSelection" @escape="escapeReadonlyCellTextSelection" />
|
||||
</template>
|
||||
<template v-else-if="editingCell?.rowId === item.id && editingCell?.col === col.actualColIdx">
|
||||
<TemporalCellEditor
|
||||
v-if="temporalEditorConfigForColumn(col.actualColIdx)"
|
||||
v-model="editValue"
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref } from "vue";
|
||||
|
||||
const props = defineProps<{
|
||||
value: string;
|
||||
expanded: boolean;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
close: [];
|
||||
escape: [];
|
||||
}>();
|
||||
|
||||
const inputRef = ref<HTMLInputElement | HTMLTextAreaElement>();
|
||||
|
||||
onMounted(() => {
|
||||
inputRef.value?.focus({ preventScroll: true });
|
||||
inputRef.value?.select();
|
||||
});
|
||||
|
||||
function onKeydown(event: KeyboardEvent) {
|
||||
if (event.key !== "Escape") return;
|
||||
event.preventDefault();
|
||||
emit("escape");
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<textarea
|
||||
v-if="props.expanded"
|
||||
ref="inputRef"
|
||||
:value="props.value"
|
||||
readonly
|
||||
data-native-clipboard
|
||||
data-readonly-cell-selection="true"
|
||||
rows="1"
|
||||
spellcheck="false"
|
||||
class="cell-readonly-input cell-readonly-input--expanded absolute left-0 top-0 z-10 min-h-full px-2.5 py-1 leading-[18px] outline-none"
|
||||
@blur="emit('close')"
|
||||
@mousedown.stop
|
||||
@click.stop
|
||||
@dblclick.stop
|
||||
@keydown.stop="onKeydown"
|
||||
@paste.prevent.stop
|
||||
/>
|
||||
<input
|
||||
v-else
|
||||
ref="inputRef"
|
||||
:value="props.value"
|
||||
readonly
|
||||
data-native-clipboard
|
||||
data-readonly-cell-selection="true"
|
||||
spellcheck="false"
|
||||
class="cell-readonly-input absolute inset-0 z-10 px-2.5 py-0 leading-[22px] outline-none"
|
||||
@blur="emit('close')"
|
||||
@mousedown.stop
|
||||
@click.stop
|
||||
@dblclick.stop
|
||||
@keydown.stop="onKeydown"
|
||||
@paste.prevent.stop
|
||||
/>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.cell-readonly-input {
|
||||
border: 2px solid var(--primary);
|
||||
background-color: var(--background);
|
||||
color: var(--foreground);
|
||||
font-family: inherit;
|
||||
font-size: var(--dbx-table-font-size, 13px);
|
||||
cursor: text;
|
||||
user-select: text;
|
||||
}
|
||||
|
||||
.cell-readonly-input--expanded {
|
||||
left: 7px;
|
||||
width: calc(100% - 14px);
|
||||
min-height: var(--cell-edit-min-height, 54px);
|
||||
max-height: var(--cell-edit-max-height, calc(9.5lh + 10px));
|
||||
overflow: auto;
|
||||
resize: none;
|
||||
white-space: pre-wrap;
|
||||
overflow-wrap: anywhere;
|
||||
border-width: 1px;
|
||||
border-color: color-mix(in oklab, var(--primary) 62%, var(--border));
|
||||
border-radius: var(--dbx-radius-fixed-6);
|
||||
box-shadow: 0 12px 30px rgb(0 0 0 / 24%);
|
||||
}
|
||||
</style>
|
||||
@@ -11,4 +11,17 @@ describe("DataGrid native clipboard regions", () => {
|
||||
it("keeps transposed field-name text selection out of grid copy shortcuts", () => {
|
||||
expect(dataGridSource).toMatch(/<div\b(?=[^>]*\bdata-native-clipboard)(?=[^>]*class="sticky left-0)[^>]*>/);
|
||||
});
|
||||
|
||||
it("opens read-only text selection from DOM, canvas, and transposed cells", () => {
|
||||
expect(dataGridSource.match(/startReadonlyCellTextSelection\(/g)).toHaveLength(4);
|
||||
expect(dataGridSource).not.toContain("showReadonlyCellDetailsOnDblClick");
|
||||
expect(dataGridSource).toContain("if (!canEditCellItem(item, actualColIdx)) {");
|
||||
expect(dataGridSource.match(/<DataGridReadonlyTextSelection/g)).toHaveLength(3);
|
||||
});
|
||||
|
||||
it("uses a text cursor for read-only cells across grid render modes", () => {
|
||||
expect(dataGridSource).toContain('hitItem && actualColIdx !== undefined ? "text" : "cell"');
|
||||
expect(dataGridSource).toContain("'cursor-text': !isScrolling && !canEditCellItem(item, col.actualColIdx)");
|
||||
expect(dataGridSource).toContain("'cursor-text': !isScrolling,");
|
||||
});
|
||||
});
|
||||
+79
@@ -0,0 +1,79 @@
|
||||
// @vitest-environment happy-dom
|
||||
|
||||
import { readFileSync } from "node:fs";
|
||||
import { resolve } from "node:path";
|
||||
import { createApp, nextTick } from "vue";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import DataGridReadonlyTextSelection from "../DataGridReadonlyTextSelection.vue";
|
||||
|
||||
const mountedRoots: HTMLDivElement[] = [];
|
||||
const dataGridSource = readFileSync(resolve(process.cwd(), "apps/desktop/src/components/grid/DataGrid.vue"), "utf8");
|
||||
|
||||
function mountSelection(options: { value?: string; expanded?: boolean } = {}) {
|
||||
const root = document.createElement("div");
|
||||
document.body.appendChild(root);
|
||||
mountedRoots.push(root);
|
||||
const onClose = vi.fn();
|
||||
const onEscape = vi.fn();
|
||||
const app = createApp(DataGridReadonlyTextSelection, {
|
||||
value: options.value ?? "alpha-copy-test",
|
||||
expanded: options.expanded ?? false,
|
||||
onClose,
|
||||
onEscape,
|
||||
});
|
||||
app.mount(root);
|
||||
return { root, app, onClose, onEscape };
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
for (const root of mountedRoots.splice(0)) root.remove();
|
||||
});
|
||||
|
||||
describe("DataGridReadonlyTextSelection", () => {
|
||||
it("closes a stale read-only selection when a result is replaced", () => {
|
||||
const resultWatcher = dataGridSource.match(/watch\(\s*\(\) => props\.result,\s*\(result, previousResult\) => \{[\s\S]*?\n\);/)?.[0] ?? "";
|
||||
|
||||
expect(resultWatcher).toContain("if (isDataGridPrefixAppend(previousResult, result)) return;");
|
||||
expect(resultWatcher).toContain("closeReadonlyCellTextSelection();");
|
||||
});
|
||||
|
||||
it("focuses and selects the complete read-only value on mount", async () => {
|
||||
const { root } = mountSelection();
|
||||
await nextTick();
|
||||
const input = root.querySelector("input");
|
||||
|
||||
expect(input).not.toBeNull();
|
||||
expect(input?.readOnly).toBe(true);
|
||||
expect(input?.dataset.nativeClipboard).toBe("");
|
||||
expect(document.activeElement).toBe(input);
|
||||
expect(input?.selectionStart).toBe(0);
|
||||
expect(input?.selectionEnd).toBe("alpha-copy-test".length);
|
||||
});
|
||||
|
||||
it("renders multiline or overflowing content in a read-only textarea", async () => {
|
||||
const value = "first line\nsecond line";
|
||||
const { root } = mountSelection({ value, expanded: true });
|
||||
await nextTick();
|
||||
const textarea = root.querySelector("textarea");
|
||||
|
||||
expect(textarea?.value).toBe(value);
|
||||
expect(textarea?.readOnly).toBe(true);
|
||||
expect(textarea?.selectionStart).toBe(0);
|
||||
expect(textarea?.selectionEnd).toBe(value.length);
|
||||
});
|
||||
|
||||
it("prevents paste and emits close signals for Escape and blur", async () => {
|
||||
const { root, onClose, onEscape } = mountSelection();
|
||||
await nextTick();
|
||||
const input = root.querySelector("input")!;
|
||||
const paste = new Event("paste", { bubbles: true, cancelable: true });
|
||||
input.dispatchEvent(paste);
|
||||
input.dispatchEvent(new KeyboardEvent("keydown", { key: "Escape", bubbles: true, cancelable: true }));
|
||||
input.dispatchEvent(new FocusEvent("blur"));
|
||||
|
||||
expect(paste.defaultPrevented).toBe(true);
|
||||
expect(input.value).toBe("alpha-copy-test");
|
||||
expect(onEscape).toHaveBeenCalledOnce();
|
||||
expect(onClose).toHaveBeenCalledOnce();
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user