0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Vue 3でIMEのcompositionイベントを正しく扱う:Windowsだけ日本語入力が壊れた独自@inputの罠

0
Last updated at Posted at 2026-07-11

Macでは問題なく動いていた入力欄が、WindowsでIMEの変換候補をマウス選択すると壊れました。

日本語対応のWebアプリを開発していると、次のようなQA報告を受けることがあります。

  • Windowsだけ日本語入力がおかしい
  • IME変換中のEnterで検索が実行される
  • Macでは再現しない
  • 特定の入力欄だけ、漢字へ変換してもひらがなのままになる

今回遭遇したのは、Windows環境でIMEの変換候補をマウスで選択すると、特定の入力欄だけひらがなのまま確定される不具合でした。

原因は、独自の@inputハンドラーがComposition中の未確定値をupdate:modelValueとしてemitし、Vueの再レンダリングによって入力欄へ書き戻していたことでした。

この記事では、次の内容を整理します。

  • IME入力中に発生するイベント
  • Vueのネイティブv-modelが持つComposition制御
  • 独自@inputやWrapper Componentで必要になる対策
  • ブラウザによって異なるイベント順序
  • IME確定Enterによる誤送信への対策
  • 自分の環境でイベントを確認するロガー

今回遭遇した不具合

発生条件は次のとおりでした。

  • Windowsでのみ再現
  • IMEの変換候補をマウスで選択すると発生
  • 漢字を選択しても、入力欄が変換前のひらがなへ戻る
  • 他の入力欄では発生しない
  • macOSでは再現しない

問題の入力欄だけが、ネイティブ要素のv-modelではなく、独自の@inputハンドラーからupdate:modelValueをemitしていました。

つまり、ネイティブ要素のv-modelが提供するComposition制御を、独自の値同期処理へ置き換えていたことが原因でした。

IME入力では何が起きているのか

英数字の直接入力と異なり、日本語入力には「未確定」の状態があります。

この間、ブラウザではComposition Eventsが発火します。

重要なのは、Composition中でもinputkeydownkeyupなどのイベントが発火することです。

inputが発火したからといって、その値が確定済みとは限りません。

Composition中のinputを通常の値変更として扱うと、未確定のひらがなをアプリケーション状態へ取り込む可能性があります。

Composition中かどうかを確認する方法

方法 内容
KeyboardEvent.isComposing keydownkeyupがComposition中に発生したかを示す
InputEvent.isComposing beforeinputinputがComposition中に発生したかを示す
InputEvent.inputType insertCompositionTextなど入力の種類を示す
keyCode === 229 IMEが処理するキーボードイベントで現れることがある互換シグナル
compositionstart / compositionend Composition状態を自前で追跡する方法

検証環境

今回の調査期間(2026年1〜2月)に利用していた主要バージョンは次のとおりです。

Browser Version
Chrome 144–145
Safari 26.2–26.3

Windows + ChromeおよびmacOS + Chromeのトレースは、実際の調査時に取得したものです。

イベントトレースと既知のブラウザ差異

Windows + Chrome(144–145) Enterで確定

keydown           key="Process" keyCode=229 isComposing=false
compositionstart
compositionupdate
beforeinput
input             isComposing=true

...

compositionupdate data="漢字"
input             isComposing=true

keydown           key="Process" keyCode=229 isComposing=true
input             isComposing=true
compositionend
keyup             key="Enter" isComposing=false

この環境では、確定値を含むinputcompositionendより前に発火します。

つまり、

if (event.isComposing) return;

だけでは確定値まで無視してしまいます。

Composition中のinputを無視する実装では、compositionendでも同期処理が必要です。

Windows + Chrome(144–145) マウスで候補を確定

今回の不具合はこちらでした。

compositionstart

input
isComposing=true

[候補をマウスクリック]

input
isComposing=true

compositionend

マウスで候補を確定するため、確定操作に対応するキーボードイベントは発火しません。

Compositionの終了を明示的に通知するイベントはcompositionendです。

独自@inputがComposition状態を無視すると、

という状態になります。

macOS + Chrome

compositionupdate
input             isComposing=true
compositionend

同じ実装でも、IME実装・イベント順序・DOM更新タイミングの組み合わせ次第では、独自@inputによる不具合は表面化しないこともあります。

「Macで動く」は、安全性を保証するものではありません。

Safari(26.2–26.3)

Safari 26.2〜26.3では、Composition EventsとKeyboardEventの順序に関する既知のWebKit Issueが存在していました。

compositionupdate
input             isComposing=true
compositionend
keydown           key="Enter" isComposing=false
keyup             key="Enter" isComposing=false

compositionendの後にEnterのkeydownが届くため、

if (event.isComposing)

だけではIME確定Enterを除外できません。

この問題は WebKit Bug 311717 により2026年4月にWebKit本体で修正が確認されています。

ただし、WebKit本体への修正と安定版Safariへの反映時期は一致しません。

修正前のSafariをサポート対象に含める場合は、互換処理を残す必要があります。

Firefoxで報告されている順序

Firefoxでは、少なくとも従来の実装では、

compositionupdate
compositionend
input             isComposing=false

という順序が報告されています。

代表的な違いは次のようになります。

Browser family Representative order
Chromium / WebKit(従来) input → compositionend
Firefox(従来) compositionend → input

重要なのは、特定ブラウザの順序を前提に実装しないことです。

Vueのネイティブv-modelはIMEを考慮している

ネイティブ要素では、

<input v-model="value" />

だけでComposition制御が有効になります。

VueのvModelTextディレクティブは概ね次のような流れで動作します。

  1. compositionstartで内部フラグを立てる
  2. Composition中のinputを無視する
  3. compositionendでフラグを解除する
  4. inputを再発火して確定値を同期する

そのため、通常のネイティブv-modelによる値同期では、Composition制御を自前で実装する必要はほとんどありません。

独自@inputではComposition制御が必要

Wrapper Componentでは次のような実装をよく見かけます。

<input
  :value="modelValue"
  @input="onInput"
/>
const onInput = (event: Event) => {
  const target = event.target;

  if (!(target instanceof HTMLInputElement)) return;

  emit('update:modelValue', target.value);
};

ネイティブv-modelが持つComposition制御は、この実装には引き継がれません。

:value@inputで同期を実装する場合は、自分でComposition状態を扱う必要があります。

修正方法

以下は<script setup lang="ts">内の実装例です。

const props = defineProps<{
  modelValue: string;
}>();

const emit = defineEmits<{
  'update:modelValue': [value: string];
}>();

const isComposing = ref(false);

const updateModelValue = (event: Event) => {
  const target = event.target;

  if (
    !(target instanceof HTMLInputElement) &&
    !(target instanceof HTMLTextAreaElement)
  ) {
    return;
  }

  if (target.value === props.modelValue) return;

  emit('update:modelValue', target.value);
};

const onInput = (event: Event) => {
  if (isComposing.value) return;

  updateModelValue(event);
};

const onCompositionStart = () => {
  isComposing.value = true;
};

const onCompositionEnd = (event: CompositionEvent) => {
  isComposing.value = false;
  updateModelValue(event);
};
<input
  :value="modelValue"
  @input="onInput"
  @compositionstart="onCompositionStart"
  @compositionend="onCompositionEnd"
/>

Composition中は同期を止め、compositionendで確定値だけを同期します。

<textarea>でも考え方は同じです。

上記の実装はHTMLInputElementHTMLTextAreaElementの両方を対象にしています。

Enter送信にも別の罠がある

const onKeydown = (event: KeyboardEvent) => {
  if (event.key !== 'Enter') return;

  search();
};

IME入力中のEnterは「送信」ではなく「変換候補の確定」です。

基本的には

if (event.isComposing)

で除外できます。

修正前のSafariをサポートする場合は、

const onKeydown = (event: KeyboardEvent) => {
  const isImeEvent = event.isComposing || event.keyCode === 229;

  if (isImeEvent || event.key !== 'Enter') return;

  search();
};

のように229をフォールバックとして利用できます。

keyCodeはDeprecatedですが、互換目的では現在も利用されています。

テストするときに確認したい組み合わせ

最低限確認したい組み合わせは次のとおりです。

  • Chrome + Windows
    • Enter確定
    • マウスで候補確定
  • Edge + Windows
  • Chrome + macOS
  • Safari + macOS
  • Firefox(可能であれば)

PlaywrightなどではネイティブIMEを完全には再現できません。

最終確認は実際のOS・IMEで行うことをおすすめします。

どの実装でComposition対応が必要か

実装 対応
<input v-model> 基本不要
:value + 独自@input 必要
Wrapper Component 必要
Enter送信 isComposingでガード
修正前Safari対応 keyCode === 229を検討
watchでDOMへ書き戻す 要注意
DirectiveでDOMを書き換える 要注意
UIライブラリ ライブラリの実装を確認し、独自ハンドラは別途監査

DevToolsでイベントを確認する

イベントロガー

次のJavaScriptをDevToolsのコンソールに貼り付けると、対象のinput要素で発火したKeyboard Events、Input Events、Composition Eventsを確認できます。

const inputElement = document.querySelector('input');

const eventTypes = [
  'keydown',
  'keyup',
  'beforeinput',
  'input',
  'compositionstart',
  'compositionupdate',
  'compositionend',
];

eventTypes.forEach((eventType) => {
  inputElement?.addEventListener(eventType, (event) => {
    console.log(
      event.type.padEnd(18),
      `key=${event.key ?? '-'}`,
      `keyCode=${event.keyCode ?? '-'}`,
      `inputType=${event.inputType ?? '-'}`,
      `data=${event.data ?? '-'}`,
      `isComposing=${event.isComposing ?? '-'}`,
      `value=${inputElement.value}`,
    );
  });
});

複数の入力欄がある場合は、対象を特定できるセレクターへ変更してください。

const inputElement = document.querySelector('[data-ime-debug]');

検証条件も別途記録する

上記のロガーが出力するのは、ブラウザイベントの内容だけです。

イベント順序を比較する際は、OSやブラウザ、IME、候補の確定方法によって結果が変わるため、次の検証条件も別途記録しておくと比較しやすくなります。

項目 記録例
OS Windows 11 24H2
Browser Chrome
Browser version 145.0.x
IME Microsoft IME
IME version 確認できる場合のみ
入力文字列 かんじ
変換方法 Spaceで変換
候補の確定方法 Enter / マウスクリック

ブラウザ名だけでなく、バージョンとIME、候補の確定方法まで残しておくことが重要です。

まとめ

ネイティブ要素のv-modelはComposition中の入力を考慮しています。

しかし、その保護は独自の:value + @inputやWrapper Componentへは引き継がれません。

値同期を自前で実装する場合は、

  • Composition中は同期しない
  • compositionendで確定値を同期する
  • Enter送信はisComposingで除外する
  • 修正前Safariを考慮する場合はkeyCode === 229をフォールバックとして検討する

という方針にすると、主要ブラウザの違いを吸収しやすくなります。

参考資料

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?