diff --git a/src/components/views/rooms/BasicMessageComposer.tsx b/src/components/views/rooms/BasicMessageComposer.tsx index 9aee26072e..d15e90e394 100644 --- a/src/components/views/rooms/BasicMessageComposer.tsx +++ b/src/components/views/rooms/BasicMessageComposer.tsx @@ -482,6 +482,11 @@ export default class BasicMessageEditor extends React.Component private onKeyDown = (event: React.KeyboardEvent): void => { if (!this.editorRef.current) return; + // Ignore any keypress while doing IME compositions to prevent cursor position issues + // This matches the behavior in SendMessageComposer and EditMessageComposer + if (this.isComposing(event)) { + return; + } if (this.isSafari && event.which == 229) { // Swallow the extra keyDown by Safari event.stopPropagation(); diff --git a/test/unit-tests/components/views/rooms/BasicMessageComposer-test.tsx b/test/unit-tests/components/views/rooms/BasicMessageComposer-test.tsx index 569ee80fec..e1b2aa00a5 100644 --- a/test/unit-tests/components/views/rooms/BasicMessageComposer-test.tsx +++ b/test/unit-tests/components/views/rooms/BasicMessageComposer-test.tsx @@ -7,7 +7,7 @@ Please see LICENSE files in the repository root for full details. */ import React from "react"; -import { render, screen } from "jest-matrix-react"; +import { fireEvent, render, screen } from "jest-matrix-react"; import userEvent from "@testing-library/user-event"; import { type MatrixClient, Room } from "matrix-js-sdk/src/matrix"; @@ -125,6 +125,73 @@ describe("BasicMessageComposer", () => { expect(spy).toHaveBeenCalledWith(room.roomId, null, false); spy.mockRestore(); }); + + it("should ignore keydown events during IME composition", () => { + const model = new EditorModel([], pc, renderer); + render(); + const input = screen.getByRole("textbox"); + + // Start IME composition + fireEvent.compositionStart(input); + + // Simulate Tab key during IME composition + // The keydown should be ignored, so we check that the model state doesn't change + const initialAutoComplete = model.autoComplete; + const initialPartsLength = model.parts.length; + + // Create a keyboard event with isComposing flag + const tabKeyEvent = new KeyboardEvent("keydown", { + key: "Tab", + bubbles: true, + cancelable: true, + }); + Object.defineProperty(tabKeyEvent, "isComposing", { + value: true, + writable: false, + }); + + // Fire the keydown event with isComposing flag + fireEvent.keyDown(input, { + ...tabKeyEvent, + nativeEvent: tabKeyEvent, + } as unknown as React.KeyboardEvent); + + // During IME composition, the keydown should be ignored + // The model should not have changed + expect(model.autoComplete).toBe(initialAutoComplete); + expect(model.parts.length).toBe(initialPartsLength); + + // End IME composition + fireEvent.compositionEnd(input); + }); + + it("should handle keydown events normally when not composing", () => { + const model = new EditorModel([], pc, renderer); + render(); + const input = screen.getByRole("textbox"); + + // Simulate Tab key when NOT composing + const tabKeyEvent = new KeyboardEvent("keydown", { + key: "Tab", + bubbles: true, + cancelable: true, + }); + Object.defineProperty(tabKeyEvent, "isComposing", { + value: false, + writable: false, + }); + + // Fire the keydown event without isComposing flag + fireEvent.keyDown(input, { + ...tabKeyEvent, + nativeEvent: tabKeyEvent, + } as unknown as React.KeyboardEvent); + + // The event should be processed normally (not ignored) + // We can't easily verify tabCompleteName was called since it's private, + // but the important thing is that the event wasn't ignored + // The test passes if no errors are thrown and the event is handled + }); }); function generateMockDataTransferForString(string: string): DataTransfer {