修復 React 聊天輸入框與注音輸入法的 Enter 鍵衝突

全文為AI參考實際開發狀況進行記錄並撰寫。

問題現象

最近在開發一個 Next.js 聊天應用時,遇到了一個惱人的 bug:使用注音輸入法打字時,按下 Enter 鍵選字會同時觸發訊息送出。這對台灣、中國等使用 IME(輸入法編輯器)的用戶來說是災難性的體驗。

想像一下:你正在打「你好」,輸入 ㄋㄧˇ 後按 Enter 選字,結果一個「ㄋㄧˇ」就被送出去了。

問題根源

問題出在 handleKeyDown 事件處理器:

const handleKeyDown = (e: KeyboardEvent<HTMLTextAreaElement>) => {
  if (e.key === "Enter" && !e.shiftKey) {
    e.preventDefault();
    handleSubmit(e);
  }
};

這段程式碼只檢查了 Enter 鍵和 Shift 鍵,卻忽略了 IME 正在組字的狀態。當用戶使用注音輸入法時,按下 Enter 是為了確認候選字,而不是送出訊息。

解決方案

1. 認識 Composition Events

瀏覽器提供了 Composition Events 來處理 IME 輸入:

  • compositionstart:IME 開始組字時觸發
  • compositionupdate:組字內容更新時觸發
  • compositionend:組字結束時觸發

2. 使用 isComposing 屬性

KeyboardEvent 有一個 isComposing 屬性,當 IME 正在組字時會是 true

3. 完整解決方案

export function ChatInput({ onSend, disabled = false }) {
  const [input, setInput] = useState("");
  const [isComposing, setIsComposing] = useState(false);

  const handleKeyDown = (e: KeyboardEvent<HTMLTextAreaElement>) => {
    // 檢查 IME 組字狀態,避免選字時誤送出
    if (
      e.key === "Enter" &&
      !e.shiftKey &&
      !isComposing &&
      !e.nativeEvent.isComposing
    ) {
      e.preventDefault();
      handleSubmit(e);
    }
  };

  const handleCompositionStart = () => setIsComposing(true);
  const handleCompositionEnd = () => setIsComposing(false);

  return (
    <Textarea
      value={input}
      onChange={(e) => setInput(e.target.value)}
      onKeyDown={handleKeyDown}
      onCompositionStart={handleCompositionStart}
      onCompositionEnd={handleCompositionEnd}
    />
  );
}

為什麼需要雙重檢查?

你可能注意到我同時檢查了 isComposing 狀態和 e.nativeEvent.isComposing

!isComposing && !e.nativeEvent.isComposing

這是因為:

  1. Safari 的事件順序不同:Safari 會在 keydown 事件之後才觸發 compositionend,導致用 React 狀態追蹤時可能出現時序問題。

  2. 跨瀏覽器相容性e.nativeEvent.isComposing 是原生屬性,部分舊瀏覽器可能不支援,用 state 追蹤作為備援。

  3. React 的合成事件:React 的 KeyboardEvent 是合成事件,需要透過 nativeEvent 存取原生屬性。

適用場景

這個問題不只出現在注音輸入法,所有 IME 都會受影響:

  • 注音輸入法(台灣)
  • 拼音輸入法(中國)
  • 日文輸入法(日本)
  • 韓文輸入法(韓國)

結語

這是一個容易被忽略但影響廣泛的問題。如果你的應用有亞洲用戶,務必在所有鍵盤事件處理中加入 IME 狀態檢查。