From c229239753d2a0681e43732c1af0e669f7101821 Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Wed, 21 Oct 2026 09:11:33 +0000 Subject: [PATCH] :bug: Keep text editing open when an IME cancels composition Remember when a key-down belongs to native composition. Consume its Escape release after composition ends, preserving the active text editor. Keep ordinary Escape behavior for leaving editing and closing menus. Add regression coverage for native key ordering and composition markers. Verify 1,583 frontend tests, lint and changed-file formatting. Check native Pinyin, Zhuyin and Korean input with eight editable showcase examples. AI-assisted-by: GPT-6 --- .../ui/workspace/shapes/text/v3_editor.cljs | 27 +++++++++++++++---- .../render_wasm/texts_test.cljs | 16 +++++++++++ 2 files changed, 38 insertions(+), 5 deletions(-) diff --git a/frontend/src/app/main/ui/workspace/shapes/text/v3_editor.cljs b/frontend/src/app/main/ui/workspace/shapes/text/v3_editor.cljs index ecf5374f28..6d497fef9c 100644 --- a/frontend/src/app/main/ui/workspace/shapes/text/v3_editor.cljs +++ b/frontend/src/app/main/ui/workspace/shapes/text/v3_editor.cljs @@ -151,6 +151,13 @@ (or (.-isComposing native) (= 229 (.-keyCode event))))) +(defn- editor-escape? + [^js event ime-key?] + (and (kbd/esc? event) + (not ime-key?) + (not (.-isComposing event)) + (not= 229 (.-keyCode event)))) + (defn- secondary-button? "True for a secondary click, which opens the context menu: the right button, or the macOS Ctrl+Click that stands in for it and reports button 0." @@ -364,6 +371,10 @@ ;; True between compositionstart and compositionend; the surface must not move then. composing-ref (mf/use-ref false) + ;; Native IMEs can end composition before the cancelling key's key-up. + ;; Remember its key-down so that Escape only cancels the preedit. + ime-key-ref (mf/use-ref false) + ;; Surface wrapper, moved during a composition (see `follow-ime-caret!`). wrapper-ref (mf/use-ref nil) @@ -590,6 +601,9 @@ on-key-down (mf/use-fn (fn [^js event] + (mf/set-ref-val! ime-key-ref + (or (mf/ref-val composing-ref) + (composing-event? event))) ;; IME keydowns (keyCode 229) must not touch the surface: the IME ;; already owns it, and even a style write before compositionstart ;; makes ibus abort and commit. The surface already sits on the @@ -911,11 +925,14 @@ (let [on-key-up (fn [event] (when (kbd/esc? event) (dom/stop-propagation event) - ;; With the menu open, Escape only closes it (checked - ;; in the DOM: the store may already be cleared). - (if (some? (dom/query menu-selector)) - (st/emit! dw/hide-context-menu) - (st/emit! (dw/clear-edition-mode)))))] + (when (editor-escape? event + (or (mf/ref-val composing-ref) + (mf/ref-val ime-key-ref))) + ;; With the menu open, Escape only closes it (checked + ;; in the DOM: the store may already be cleared). + (if (some? (dom/query menu-selector)) + (st/emit! dw/hide-context-menu) + (st/emit! (dw/clear-edition-mode))))))] (.addEventListener js/document "keyup" on-key-up) #(.removeEventListener js/document "keyup" on-key-up)))) diff --git a/frontend/test/frontend_tests/render_wasm/texts_test.cljs b/frontend/test/frontend_tests/render_wasm/texts_test.cljs index e411446109..2adbea2f47 100644 --- a/frontend/test/frontend_tests/render_wasm/texts_test.cljs +++ b/frontend/test/frontend_tests/render_wasm/texts_test.cljs @@ -181,6 +181,22 @@ (t/is (= 0 (v3-editor/changed-span-end "にほ" ""))) (t/is (= 2 (v3-editor/changed-span-end "😀" "😀😀")))) +(t/deftest escape-that-cancels-native-composition-keeps-text-editing + ;; Fcitx Pinyin ends composition before releasing Escape. That key-up no + ;; longer reports isComposing, so remember that its key-down belonged to IME. + (t/is (false? (@#'v3-editor/editor-escape? + #js {:key "Escape" :isComposing false :keyCode 27} true))) + (t/is (false? (@#'v3-editor/editor-escape? + #js {:key "Escape" :isComposing true :keyCode 27} false))) + (t/is (false? (@#'v3-editor/editor-escape? + #js {:key "Escape" :isComposing false :keyCode 229} false)))) + +(t/deftest ordinary-escape-still-exits-text-editing + (t/is (true? (@#'v3-editor/editor-escape? + #js {:key "Escape" :isComposing false :keyCode 27} false))) + (t/is (false? (@#'v3-editor/editor-escape? + #js {:key "Enter" :isComposing false :keyCode 13} false)))) + (t/deftest ime-debug-caret-bounds-follow-viewport-zoom-and-pan (t/is (= {:x 210 :y 380 :width 48 :height 2} (@#'ime-debug/screen-rect