diff --git a/clients/web/src/components/groups/SchemaForm/SchemaForm.test.tsx b/clients/web/src/components/groups/SchemaForm/SchemaForm.test.tsx
index 992bf4ca..a11fe0b1 100644
--- a/clients/web/src/components/groups/SchemaForm/SchemaForm.test.tsx
+++ b/clients/web/src/components/groups/SchemaForm/SchemaForm.test.tsx
@@ -2110,6 +2110,33 @@ describe("SchemaForm enlarge keyboard access (#2138)", () => {
expect(noteField().value).toBe("");
});
+ // WebKit is reported to fire `compositionend` before the committing keydown,
+ // so `isComposing` is already false there and the guard above cannot see it.
+ // 229 is the older sentinel for "this key went to the IME", which that event
+ // still carries (#2139 review).
+ it("ignores a committing IME keydown that only reports keyCode 229", () => {
+ renderWithMantine();
+
+ fireEvent.keyDown(noteField(), {
+ key: "Enter",
+ isComposing: false,
+ keyCode: 229,
+ });
+
+ expect(noteField().tagName).toBe("INPUT");
+ expect(noteField().value).toBe("");
+ });
+
+ // The control for the sentinel: a real Enter reports 13 and must still work,
+ // so the guard cannot be swallowing ordinary keystrokes.
+ it("still enlarges on an Enter reporting keyCode 13", () => {
+ renderWithMantine();
+
+ fireEvent.keyDown(noteField(), { key: "Enter", keyCode: 13 });
+
+ expect(noteField().tagName).toBe("TEXTAREA");
+ });
+
// Same event without the flag, to prove the guard above is what turned it
// away rather than fireEvent simply not reaching the handler.
it("still enlarges on an Enter that is not composing", () => {
diff --git a/clients/web/src/components/groups/SchemaForm/SchemaForm.tsx b/clients/web/src/components/groups/SchemaForm/SchemaForm.tsx
index 1fdd748b..76603591 100644
--- a/clients/web/src/components/groups/SchemaForm/SchemaForm.tsx
+++ b/clients/web/src/components/groups/SchemaForm/SchemaForm.tsx
@@ -119,6 +119,12 @@ const TWO_ACTION_WIDTH = rem(
ENLARGE_WIDTH + ACTION_GAP + CLEAR_WIDTH + SECTION_INSET,
);
+// `keyCode` reported for a keydown the IME consumed — the pre-`isComposing`
+// sentinel every browser still sets during composition. Only needed for the
+// browsers whose composition events land too late for `isComposing` to help
+// (#2138 review); a real Enter reports 13, so this cannot swallow one.
+const IME_KEY_CODE = 229;
+
function serializeJson(value: unknown): string {
return JSON.stringify(value, null, 2);
}
@@ -791,6 +797,13 @@ export function SchemaForm({
// keystroke means "accept this word", not "new line". Acting on it
// would enlarge the field and insert a stray newline every time such a
// user finished a word, making the field unusable for them.
+ // - `keyCode === 229` is the same guard again, for WebKit. It is reported
+ // to fire `compositionend` *before* the committing keydown, which
+ // leaves `isComposing` already false by the time this runs; 229 is the
+ // pre-`isComposing` sentinel for "this key went to the IME" and is
+ // still set there. Kept despite `keyCode` being deprecated because
+ // nothing non-deprecated distinguishes that event, and it cannot misfire
+ // on a real Enter, which reports 13.
// - The Ctrl/Cmd/Alt chords are deliberately left alone: those read as
// "submit" in a form, and a consumer binding one (run the tool) must
// not be overridden into enlarging a field instead.
@@ -798,6 +811,7 @@ export function SchemaForm({
if (
event.key !== "Enter" ||
event.nativeEvent.isComposing ||
+ event.keyCode === IME_KEY_CODE ||
event.ctrlKey ||
event.metaKey ||
event.altKey