From 165c10fa33946eca9781d84f31ccab70eb3228d7 Mon Sep 17 00:00:00 2001 From: Nick Rogers <1903140+rogersnm@users.noreply.github.com> Date: Mon, 14 Sep 2026 11:35:37 +0100 Subject: [PATCH] fix(overlay-editor): only motion keys schedule a commit The overlay editor's keydown handler scheduled a zero-delay commit timer on every key, not only Escape, Enter and Tab. When a person types quickly and presses Enter, an ordinary key's timer can fire after Enter has set the motion: it commits with no value and marks the edit finished, and Enter's own timer then does nothing, so the typed value is discarded. Return early for keys that are not a motion key, so only those schedule a commit. Adds a test that fires a plain key and Enter before any timer runs. --- .../data-grid-overlay-editor.tsx | 6 +++ packages/core/test/data-editor.test.tsx | 48 +++++++++++++++++++ 2 files changed, 54 insertions(+) diff --git a/packages/core/src/internal/data-grid-overlay-editor/data-grid-overlay-editor.tsx b/packages/core/src/internal/data-grid-overlay-editor/data-grid-overlay-editor.tsx index bac91ef44..94d280ed9 100644 --- a/packages/core/src/internal/data-grid-overlay-editor/data-grid-overlay-editor.tsx +++ b/packages/core/src/internal/data-grid-overlay-editor/data-grid-overlay-editor.tsx @@ -147,6 +147,12 @@ const DataGridOverlayEditor: React.FunctionComponent event.preventDefault(); customMotion.current = [event.shiftKey ? -1 : 1, 0]; save = true; + } else { + // Only a motion key may schedule a commit. Any other key's timer + // could fire after a fast Enter has set the motion, committing + // with no value and marking the edit finished, so the commit + // that follows would be dropped. + return; } window.setTimeout(() => { diff --git a/packages/core/test/data-editor.test.tsx b/packages/core/test/data-editor.test.tsx index 4a0d97369..ae83f87ac 100644 --- a/packages/core/test/data-editor.test.tsx +++ b/packages/core/test/data-editor.test.tsx @@ -1620,6 +1620,54 @@ describe("data-editor", () => { expect(document.body.contains(overlay)).toBe(false); }); + test("Send edit when Enter follows another key before its timer fires", async () => { + const spy = vi.fn(); + vi.useFakeTimers(); + render(, { + wrapper: Context, + }); + prep(false); + + const canvas = screen.getByTestId("data-grid-canvas"); + sendClick(canvas, { + clientX: 300, // Col B + clientY: 36 + 32 + 16, // Row 1 (0 indexed) + }); + + act(() => { + vi.runAllTimers(); + }); + + fireEvent.keyDown(canvas, { + keyCode: 74, + key: "j", + }); + + act(() => { + vi.runAllTimers(); + }); + + const overlay = screen.getByDisplayValue("j"); + + // A fast typist: the "k" keydown and Enter both land before any timer + // runs. The "k" timer must not commit on Enter's behalf with no value. + fireEvent.keyDown(overlay, { + keyCode: 75, + key: "k", + }); + fireEvent.change(overlay, { target: { value: "jk" } }); + fireEvent.keyDown(overlay, { + key: "Enter", + }); + + act(() => { + vi.runAllTimers(); + }); + + expect(spy).toBeCalledWith([1, 1], expect.objectContaining({ data: "jk" })); + expect(document.body.contains(overlay)).toBe(false); + }); + test("Send edit with click off", async () => { const spy = vi.fn(); vi.useFakeTimers();