From 8e8cb0f58ca9729f983959ce135a9ec488497f5b Mon Sep 17 00:00:00 2001 From: Freedom <48708512+xddcode@users.noreply.github.com> Date: Mon, 20 Jul 2026 13:51:05 +0800 Subject: [PATCH] fix(grid): preserve IME composition when condition expands --- .../grid/DataGridConditionEditor.vue | 28 +++++++++++++++++-- .../useDataGridConditionEditor.spec.ts | 21 ++++++++++++-- .../composables/useDataGridConditionEditor.ts | 1 + 3 files changed, 46 insertions(+), 4 deletions(-) diff --git a/apps/desktop/src/components/grid/DataGridConditionEditor.vue b/apps/desktop/src/components/grid/DataGridConditionEditor.vue index fc04b80b94..758d3630e5 100644 --- a/apps/desktop/src/components/grid/DataGridConditionEditor.vue +++ b/apps/desktop/src/components/grid/DataGridConditionEditor.vue @@ -44,12 +44,14 @@ const overlayRef = ref(); const controlRef = ref(); const dropdownRef = ref(); const expanded = ref(false); +const composing = ref(false); const expandedRect = ref({ left: 0, top: 0, width: 0, controlsTop: 0, inputTop: 0, prefix: 0, suffix: 28 }); const expandedHeight = ref(56); const suggestionPosition = ref({ left: 0, top: 0, width: 180 }); const historyPreview = ref<{ value: string; left: number; top: number; maxWidth: number; arrowTop: number; side: "left" | "right" } | null>(null); let collapseTimer: ReturnType | undefined; let resizeObserver: ResizeObserver | undefined; +let expandAfterComposition = false; const editor = useDataGridConditionEditor({ kind: props.kind, @@ -146,6 +148,12 @@ function resizeEditor(forceExpand = false) { void nextTick(() => { const input = inputRef.value; if (!input) return; + // Expanding swaps focus to a teleported overlay textarea; doing that mid-IME + // composition truncates unfinished pinyin. Defer until composition ends. + if (composing.value && !expanded.value) { + expandAfterComposition = true; + return; + } const focused = document.activeElement === input || document.activeElement === overlayRef.value; const nextExpanded = focused && shouldExpand(input) && (forceExpand || expanded.value); if (nextExpanded) { @@ -154,10 +162,10 @@ function resizeEditor(forceExpand = false) { } expanded.value = nextExpanded; updateSuggestionPosition(); - if (nextExpanded && document.activeElement === input) { + if (nextExpanded && document.activeElement === input && !composing.value) { void nextTick(() => { const overlay = overlayRef.value; - if (!overlay) return; + if (!overlay || composing.value) return; const start = input.selectionStart; overlay.focus(); overlay.setSelectionRange(start, start); @@ -166,6 +174,17 @@ function resizeEditor(forceExpand = false) { }); } +function onCompositionStart() { + composing.value = true; +} + +function onCompositionEnd() { + composing.value = false; + if (!expandAfterComposition) return; + expandAfterComposition = false; + resizeEditor(true); +} + function focus(select = false) { const target = activeEditor.value ?? inputRef.value; target?.focus(); @@ -284,6 +303,7 @@ onMounted(() => { onUnmounted(() => { if (collapseTimer) clearTimeout(collapseTimer); + expandAfterComposition = false; resizeObserver?.disconnect(); window.removeEventListener("resize", onViewportResize); window.visualViewport?.removeEventListener("resize", onViewportResize); @@ -323,6 +343,8 @@ defineExpose({ focus, dismiss: editor.dismiss, rememberHistory: editor.rememberH @focus="resizeEditor(true)" @blur="scheduleCollapse" @click="resizeEditor(true)" + @compositionstart="onCompositionStart" + @compositionend="onCompositionEnd" @input="onInput" @keydown="onKeydown" /> @@ -351,6 +373,8 @@ defineExpose({ focus, dismiss: editor.dismiss, rememberHistory: editor.rememberH class="data-grid-topbar-condition-input data-grid-topbar-condition-input--expanded absolute resize-none outline-none" :class="[props.kind === 'where' ? 'data-grid-topbar-condition-input--where' : 'data-grid-topbar-condition-input--order', { 'data-grid-topbar-condition-input--compact': props.compact }]" @blur="scheduleCollapse" + @compositionstart="onCompositionStart" + @compositionend="onCompositionEnd" @input="onInput" @keydown="onKeydown" /> diff --git a/apps/desktop/src/composables/__tests__/useDataGridConditionEditor.spec.ts b/apps/desktop/src/composables/__tests__/useDataGridConditionEditor.spec.ts index c3c130c383..896400151a 100644 --- a/apps/desktop/src/composables/__tests__/useDataGridConditionEditor.spec.ts +++ b/apps/desktop/src/composables/__tests__/useDataGridConditionEditor.spec.ts @@ -11,8 +11,8 @@ vi.stubGlobal("localStorage", { clear: () => storage.clear(), }); -function keyboardEvent(key: string) { - return { key, shiftKey: false, preventDefault: vi.fn() } as unknown as KeyboardEvent; +function keyboardEvent(key: string, extras: Partial = {}) { + return { key, shiftKey: false, preventDefault: vi.fn(), ...extras } as unknown as KeyboardEvent; } describe("useDataGridConditionEditor", () => { @@ -192,4 +192,21 @@ describe("useDataGridConditionEditor", () => { expect(editor.handleKeydown(escape)).toBe("dismiss"); expect(editor.dropdownOpen.value).toBe(false); }); + + it("ignores shortcut keys while an IME composition is active", async () => { + const value = ref(""); + const editor = useDataGridConditionEditor({ kind: "where", value, columns: ["name"], historyScope: {} }); + value.value = "na"; + await nextTick(); + await vi.waitFor(() => expect(editor.suggestions.value).toHaveLength(1)); + + const composingEnter = keyboardEvent("Enter", { isComposing: true }); + expect(editor.handleKeydown(composingEnter)).toBeUndefined(); + expect(composingEnter.preventDefault).not.toHaveBeenCalled(); + expect(value.value).toBe("na"); + + const processEnter = keyboardEvent("Process"); + expect(editor.handleKeydown(processEnter)).toBeUndefined(); + expect(processEnter.preventDefault).not.toHaveBeenCalled(); + }); }); diff --git a/apps/desktop/src/composables/useDataGridConditionEditor.ts b/apps/desktop/src/composables/useDataGridConditionEditor.ts index e30705e149..32bd8e0848 100644 --- a/apps/desktop/src/composables/useDataGridConditionEditor.ts +++ b/apps/desktop/src/composables/useDataGridConditionEditor.ts @@ -183,6 +183,7 @@ export function useDataGridConditionEditor(options: UseDataGridConditionEditorOp } function handleKeydown(event: KeyboardEvent): "accept" | "apply" | "dismiss" | "navigate" | undefined { + if (event.isComposing || event.key === "Process" || event.keyCode === 229) return undefined; if (dropdownOpen.value && event.key === "Escape") { event.preventDefault(); dismiss();