diff --git a/apps/desktop/src/lib/__tests__/dataGrid/dataGridNativeSelection.spec.ts b/apps/desktop/src/lib/__tests__/dataGrid/dataGridNativeSelection.spec.ts index f7b80445a2..971d888160 100644 --- a/apps/desktop/src/lib/__tests__/dataGrid/dataGridNativeSelection.spec.ts +++ b/apps/desktop/src/lib/__tests__/dataGrid/dataGridNativeSelection.spec.ts @@ -2,7 +2,7 @@ import { afterEach, describe, expect, it, vi } from "vitest"; import { beginDataGridNativeSelectionBlock, DATA_GRID_NATIVE_SELECTION_BLOCK_CLASS, DATA_GRID_NATIVE_SELECTION_RELEASE_DELAY_MS, finishDataGridNativeSelectionBlock } from "@/lib/dataGrid/dataGridNativeSelection"; -function selectionEnvironment() { +function selectionEnvironment(options: { editableActiveElement?: boolean } = {}) { const classes = new Set(); const removeAllRanges = vi.fn(); return { @@ -16,6 +16,11 @@ function selectionEnvironment() { remove: (className: string) => classes.delete(className), }, }, + activeElement: options.editableActiveElement + ? { + closest: vi.fn(() => ({})), + } + : null, }, getSelection: () => ({ removeAllRanges }), setTimeout, @@ -39,6 +44,16 @@ describe("data grid native selection block", () => { expect(removeAllRanges).toHaveBeenCalledOnce(); }); + it("preserves the native selection while an editable control is focused", () => { + const { classes, removeAllRanges, environment } = selectionEnvironment({ editableActiveElement: true }); + const owner = {}; + + beginDataGridNativeSelectionBlock(owner, environment); + + expect(classes.has(DATA_GRID_NATIVE_SELECTION_BLOCK_CLASS)).toBe(true); + expect(removeAllRanges).not.toHaveBeenCalled(); + }); + it("keeps the application-level block across a fast component replacement", () => { vi.useFakeTimers(); const { classes, environment } = selectionEnvironment(); diff --git a/apps/desktop/src/lib/dataGrid/dataGridNativeSelection.ts b/apps/desktop/src/lib/dataGrid/dataGridNativeSelection.ts index 6f6528f7aa..4de02cf491 100644 --- a/apps/desktop/src/lib/dataGrid/dataGridNativeSelection.ts +++ b/apps/desktop/src/lib/dataGrid/dataGridNativeSelection.ts @@ -9,6 +9,9 @@ interface NativeSelectionBlockEnvironment { remove(className: string): void; }; }; + activeElement?: { + closest?(selector: string): unknown; + } | null; }; getSelection?: () => { removeAllRanges(): void } | null; setTimeout(callback: () => void, delay: number): ReturnType; @@ -40,10 +43,17 @@ function hasActiveBlockForEnvironment(environment: NativeSelectionBlockEnvironme return [...activeSelectionBlocks.values()].some((activeBlock) => activeBlock.environment.document?.documentElement === documentElement); } +function activeElementUsesNativeSelection(environment: NativeSelectionBlockEnvironment): boolean { + return !!environment.document?.activeElement?.closest?.("input, textarea, [contenteditable='true'], [role='textbox']"); +} + export function beginDataGridNativeSelectionBlock(owner: DataGridNativeSelectionBlockOwner, environment: NativeSelectionBlockEnvironment = defaultEnvironment()) { clearReleaseTimer(owner); activeSelectionBlocks.set(owner, { environment }); environment.document?.documentElement?.classList.add(DATA_GRID_NATIVE_SELECTION_BLOCK_CLASS); + // Clearing the document selection while a text editor is focused can leave + // CodeMirror's caret visible but detach the browser's active editing range. + if (activeElementUsesNativeSelection(environment)) return; environment.getSelection?.()?.removeAllRanges(); }