AndroidのソフトキーボードでプロンプトアイテムをSpaceキーで選択できない問題を修正
Androidのソフトキーボードが KEY_SPACE_COMMAND を発火しないため、プロンプトのアイテム選択がスペースキーで機能しなかった問題を修正しました。INPUT_COMMAND を新たにリッスンすることで、Android固有のイベント経路を補完しています。
背景
Lexicalエディタのプロンプト機能は、スペースキーによるアイテム選択(#doesSpaceSelect)を KEY_SPACE_COMMAND のリスナーで実装していました。しかしAndroidのソフトキーボードはデスクトップとは異なるイベント経路をたどるため、この実装ではスペース入力を検知できませんでした。
Androidのソフトキーボードでスペースを入力すると、KEY_SPACE_COMMAND の代わりに CONTROLLED_TEXT_INSERTION_COMMAND と INPUT_COMMAND(data=" ")の組み合わせが発火します。Lexicalはこの INPUT_COMMAND をネイティブの input イベントから生成しますが、従来のプロンプト実装はこのコマンドを監視していなかったため、Androidではスペースによる選択が無効でした。
技術的な変更
src/elements/prompt.js に INPUT_COMMAND のリスナー登録と、対応するハンドラメソッドが追加されました。
KEY_SPACE_COMMAND を監視するブロックに、INPUT_COMMAND のリスナーが並列で追加されています。どちらも #doesSpaceSelect フラグが有効な場合にのみ登録されるため、スペース選択を使わないプロンプトへの影響はありません。
変更前:
if (this.#doesSpaceSelect) {
this.#popoverListeners.track(this.#editor.registerCommand(KEY_SPACE_COMMAND, this.#handleSelectedOption.bind(this), COMMAND_PRIORITY_CRITICAL))
}
変更後:
if (this.#doesSpaceSelect) {
this.#popoverListeners.track(this.#editor.registerCommand(KEY_SPACE_COMMAND, this.#handleSelectedOption.bind(this), COMMAND_PRIORITY_CRITICAL))
this.#popoverListeners.track(this.#editor.registerCommand(INPUT_COMMAND, this.#handleInputCommand.bind(this), COMMAND_PRIORITY_CRITICAL))
}
追加された #handleInputCommand は、inputType === "insertText" かつ data === " " の場合にのみ既存の #handleSelectedOption へ処理を委譲します。
// Android Mobile keyboard doesn't trigger KEY_SPACE_COMMAND
#handleInputCommand(event) {
if (event.inputType === "insertText" && event.data === " ") return this.#handleSelectedOption(event)
}
この条件分岐により、スペース以外の insertText イベント(通常の文字入力)や、deleteContentBackward などの他の inputType は素通りし、誤検知のリスクを最小化しています。
合わせて追加されたブラウザテスト test/browser/tests/prompts/space_selection_via_input_event.test.js では、KEY_SPACE_COMMAND を意図的に回避するために InputEvent を直接 dispatchEvent でシミュレートしています。テストコメントにも「Spaceキーを押すと KEY_SPACE_COMMAND も発火してリグレッションを隠す可能性があるため避けた」と明記されており、Androidの実際のイベント経路を再現した意図的な設計です。
設計判断
既存の #handleSelectedOption を再利用し、ハンドラの分岐を最薄に保つ設計が採用されました。
#handleInputCommand は条件判定のみを担い、実際の選択ロジックは既存の #handleSelectedOption にそのまま委譲しています。これにより、アイテム選択のコアロジックを一箇所に保ちつつ、Android固有のイベント経路への対応を独立したメソッドとして分離しています。また、リスナーの登録・解除を管理する #popoverListeners の仕組みをそのまま活用しており、既存のライフサイクル管理に追加コストをかけていません。
まとめ
本PRは、プラットフォーム固有のイベントモデルの差異を、最小限のコード追加で吸収した修正です。INPUT_COMMAND のフィルタリングを薄いハンドラに閉じ込めることで、デスクトップとAndroidの両経路で同一の選択ロジックが動作するようになりました。