fix(grid): skip cell edit commit during IME composition

This commit is contained in:
zipg
2026-10-19 03:21:14 +08:00
committed by GitHub
parent d6cc18b2a9
commit 42890875b9
3 changed files with 67 additions and 2 deletions
+21 -1
View File
@@ -2,6 +2,7 @@
import QueryTimingDetails from "./QueryTimingDetails.vue";
import { applyDdlStoragePreference } from "@/lib/sql/ddlStorage";
import { applyDdlSearchMarks } from "@/lib/sql/ddlSearchMarks";
import { isAiPromptImeCompositionEvent } from "@/lib/ai/aiPromptKeyboard";
import DdlStorageToggle from "@/components/objects/DdlStorageToggle.vue";
import { useUpdateBlocker } from "@/lib/app/updatePreparation";
@@ -10818,7 +10819,14 @@ async function onTemporalCellEditorSave() {
}
}
const cellEditCompositionActive = ref(false);
async function onCellEditKeydown(event: KeyboardEvent) {
// The composition guard must also cover the save-shortcut and inline bulk
// edit branches below: they run before the delegation to onEditKeydown and
// reuse the same cell edit input, so an IME Enter would otherwise commit
// the bulk fill (#12470).
if (isAiPromptImeCompositionEvent(event, cellEditCompositionActive.value)) return;
if (isSaveShortcut(event, settingsStore.editorSettings.shortcuts)) {
event.preventDefault();
event.stopPropagation();
@@ -10841,7 +10849,7 @@ async function onCellEditKeydown(event: KeyboardEvent) {
}
return;
}
onEditKeydown(event);
onEditKeydown(event, cellEditCompositionActive.value);
}
function undoGridChange(): boolean {
@@ -14850,6 +14858,8 @@ useUpdateBlocker(() => (hasPendingChanges.value || hasPendingDataEditorDraft.val
@click.stop
@focus="onCellEditTextareaInput"
@input="onCellEditTextareaInput"
@compositionstart="cellEditCompositionActive = true"
@compositionend="cellEditCompositionActive = false"
@keydown.stop="onCellEditKeydown"
@paste.stop="onCellEditTextareaPaste"
@wheel.stop
@@ -14865,6 +14875,8 @@ useUpdateBlocker(() => (hasPendingChanges.value || hasPendingDataEditorDraft.val
@blur="commitEditFromCellBlur"
@click.stop
@input="onCellEditTextareaInput"
@compositionstart="cellEditCompositionActive = true"
@compositionend="cellEditCompositionActive = false"
@keydown.stop="onCellEditKeydown"
@paste.stop="onCellEditTextareaPaste"
/>
@@ -15550,6 +15562,8 @@ useUpdateBlocker(() => (hasPendingChanges.value || hasPendingDataEditorDraft.val
@click.stop
@focus="onCellEditTextareaInput"
@input="onCellEditTextareaInput"
@compositionstart="cellEditCompositionActive = true"
@compositionend="cellEditCompositionActive = false"
@keydown.stop="onCellEditKeydown"
@paste.stop="onCellEditTextareaPaste"
@wheel.stop
@@ -15565,6 +15579,8 @@ useUpdateBlocker(() => (hasPendingChanges.value || hasPendingDataEditorDraft.val
@blur="commitEditFromCellBlur"
@click.stop
@input="onCellEditTextareaInput"
@compositionstart="cellEditCompositionActive = true"
@compositionend="cellEditCompositionActive = false"
@keydown.stop="onCellEditKeydown"
@paste.stop="onCellEditTextareaPaste"
/>
@@ -15774,6 +15790,8 @@ useUpdateBlocker(() => (hasPendingChanges.value || hasPendingDataEditorDraft.val
@click.stop
@focus="onCellEditTextareaInput"
@input="onCellEditTextareaInput"
@compositionstart="cellEditCompositionActive = true"
@compositionend="cellEditCompositionActive = false"
@keydown.stop="onCellEditKeydown"
@paste.stop="onCellEditTextareaPaste"
@wheel.stop
@@ -15789,6 +15807,8 @@ useUpdateBlocker(() => (hasPendingChanges.value || hasPendingDataEditorDraft.val
@blur="commitEditFromCellBlur"
@click.stop
@input="onCellEditTextareaInput"
@compositionstart="cellEditCompositionActive = true"
@compositionend="cellEditCompositionActive = false"
@keydown.stop="onCellEditKeydown"
@paste.stop="onCellEditTextareaPaste"
/>
@@ -2339,6 +2339,46 @@ describe("useDataGridEditor saveChanges reload", () => {
});
});
describe("useDataGridEditor IME composition", () => {
it("does not commit or cancel the cell while Enter/Escape belongs to an IME composition", () => {
const editor = createEditor(undefined, true, undefined, undefined, [["Ada", "hidden", "Lovelace"]]);
editor.startEdit(0, 0);
editor.editValue.value = "中";
const composing = { key: "Enter", isComposing: true, keyCode: 229, preventDefault: vi.fn(), stopPropagation: vi.fn() } as unknown as KeyboardEvent;
editor.onEditKeydown(composing);
expect(composing.preventDefault).not.toHaveBeenCalled();
expect(editor.editingCell.value).not.toBeNull();
const escape = { key: "Escape", isComposing: true, keyCode: 229, preventDefault: vi.fn(), stopPropagation: vi.fn() } as unknown as KeyboardEvent;
editor.onEditKeydown(escape);
expect(escape.preventDefault).not.toHaveBeenCalled();
expect(editor.editingCell.value).not.toBeNull();
});
it("keeps the guard for the Process key without keyCode 229", () => {
const editor = createEditor(undefined, true, undefined, undefined, [["Ada", "hidden", "Lovelace"]]);
editor.startEdit(0, 0);
const process = { key: "Process", isComposing: false, keyCode: 13, preventDefault: vi.fn(), stopPropagation: vi.fn() } as unknown as KeyboardEvent;
editor.onEditKeydown(process);
expect(process.preventDefault).not.toHaveBeenCalled();
expect(editor.editingCell.value).not.toBeNull();
});
it("defers to the composition tracking flag when the keydown itself carries no composition signal", () => {
const editor = createEditor(undefined, true, undefined, undefined, [["Ada", "hidden", "Lovelace"]]);
editor.startEdit(0, 0);
// WebKit fires compositionend before the committing keydown, so the input's
// composition tracking ref is the only reliable signal in that window.
const enter = { key: "Enter", isComposing: false, keyCode: 13, preventDefault: vi.fn(), stopPropagation: vi.fn() } as unknown as KeyboardEvent;
editor.onEditKeydown(enter, true);
expect(enter.preventDefault).not.toHaveBeenCalled();
expect(editor.editingCell.value).not.toBeNull();
editor.onEditKeydown(enter, false);
expect(enter.preventDefault).toHaveBeenCalled();
});
});
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"]]);
@@ -1,4 +1,5 @@
import { joinedSaveOptions } from "@/lib/dataGrid/joinedRowChanges";
import { isAiPromptImeCompositionEvent } from "@/lib/ai/aiPromptKeyboard";
import { ref, shallowRef, triggerRef, computed, nextTick, watch, getCurrentInstance, onActivated, onBeforeUnmount, onDeactivated, onMounted, toRaw, type ComputedRef, type Ref } from "vue";
import * as api from "@/lib/backend/api";
import type { DataGridSaveGuard } from "@/lib/backend/tauri";
@@ -1248,7 +1249,11 @@ export function useDataGridEditor(options: UseDataGridEditorOptions) {
restoreScrollAcrossFrames(restoreScroll);
}
function onEditKeydown(e: KeyboardEvent) {
function onEditKeydown(e: KeyboardEvent, compositionActive = false) {
// Enter/Escape can also be the key that commits an IME candidate. Never
// treat that key as the grid shortcut; otherwise a Chinese/Japanese input
// commits the cell before the composition is inserted (#12470).
if (isAiPromptImeCompositionEvent(e, compositionActive)) return;
const isExpandedTextarea = typeof HTMLTextAreaElement !== "undefined" && e.target instanceof HTMLTextAreaElement && e.target.dataset.expandedCellEditor === "true";
if (e.key === "Enter" && (!isExpandedTextarea || e.metaKey || e.ctrlKey)) {
e.preventDefault();