diff --git a/apps/desktop/src/composables/__tests__/useDataGridEditor.spec.ts b/apps/desktop/src/composables/__tests__/useDataGridEditor.spec.ts index c25f2dbd40..a250a0257b 100644 --- a/apps/desktop/src/composables/__tests__/useDataGridEditor.spec.ts +++ b/apps/desktop/src/composables/__tests__/useDataGridEditor.spec.ts @@ -1,3 +1,4 @@ +// @vitest-environment happy-dom import { computed, nextTick, ref, type Ref } from "vue"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { clearDataGridPendingSnapshot, DATA_GRID_QUICK_ENTRY_DRAFT_ROW_ID, useDataGridEditor } from "@/composables/useDataGridEditor"; @@ -874,3 +875,31 @@ describe("useDataGridEditor saveChanges reload", () => { expect(emit).not.toHaveBeenCalledWith("reload", expect.anything()); }); }); + +describe("useDataGridEditor cell edit focus", () => { + it("selects the editor value only on the first frame so fast typing is not clobbered (#7336)", async () => { + const editor = createEditor(undefined, true, undefined, undefined, [["long json value", "hidden", "last"]]); + const select = vi.fn(); + const setSelectionRange = vi.fn(); + const focus = vi.fn(); + const input = { focus, select, setSelectionRange, dataset: {}, value: "long json value" }; + const rafCallbacks: Array<(time: number) => void> = []; + vi.stubGlobal("document", { querySelector: () => input }); + vi.stubGlobal("requestAnimationFrame", (callback: (time: number) => void) => { + rafCallbacks.push(callback); + return rafCallbacks.length; + }); + + editor.startEdit(0, 0); + await nextTick(); + for (let frame = 0; frame < 3; frame += 1) { + const callbacks = rafCallbacks.splice(0); + callbacks.forEach((callback) => callback(0)); + await nextTick(); + } + + expect(select).toHaveBeenCalledTimes(1); + expect(setSelectionRange).toHaveBeenCalledTimes(1); + vi.unstubAllGlobals(); + }); +}); diff --git a/apps/desktop/src/composables/useDataGridEditor.ts b/apps/desktop/src/composables/useDataGridEditor.ts index c83f647026..4bfef5971e 100644 --- a/apps/desktop/src/composables/useDataGridEditor.ts +++ b/apps/desktop/src/composables/useDataGridEditor.ts @@ -361,13 +361,20 @@ export function useDataGridEditor(options: UseDataGridEditorOptions) { } function focusEditInput(select = true) { - const focusInput = () => { + // `selectText` is tri-state: true selects the full value once when the + // editor opens, false places the caret at the end, and undefined only + // refocuses without touching the selection. The requestAnimationFrame + // retry below uses the undefined mode so a user who starts typing or + // moving the caret immediately after the editor opens is not clobbered by + // a second select-all on a later frame. + const focusInput = (selectText?: boolean) => { if (typeof document === "undefined") return; const scroller = getScrollerElement(); const root = scroller?.closest("[data-grid-root]"); const input = (root ?? document).querySelector(".cell-edit-input") as HTMLInputElement | HTMLTextAreaElement | null; if (input) focusDataGridEditorWithoutScrolling(input, scroller); - if (select && input) { + if (selectText === undefined) return; + if (selectText && input) { if (input instanceof HTMLTextAreaElement && input.dataset.expandedCellEditor === "true") { // Expanded editors must match single-line editors: a double-click selects the whole value. input.select(); @@ -382,11 +389,11 @@ export function useDataGridEditor(options: UseDataGridEditorOptions) { } }; nextTick(() => { - focusInput(); + focusInput(select); if (typeof requestAnimationFrame === "undefined") return; let attempts = 0; const focusNextFrame = () => { - focusInput(); + focusInput(undefined); attempts += 1; if (attempts < 3) requestAnimationFrame(focusNextFrame); };