画像キャプションのキーボードショートカット(Ctrl+C/A/X)を修正
Lexicalエディタの contenteditable ルート内に存在するキャプション用 <textarea> のキーボード・クリップボードイベントが上位にバブリングし、Lexicalに横取りされていた問題を修正しました。stopPropagation() の適用範囲を拡張することで、Ctrl+A/C/X が <textarea> 内のテキストに対して正しく機能するようになります。
背景
キャプション <textarea> は Lexical エディタの contenteditable ルート要素の内側に配置されていますが、Lexical のコンテンツモデルの外側に位置します。この構造が問題の根本原因です。<textarea> で発生したキーボードイベントやクリップボードイベントはDOMツリーを上位にバブリングし、Lexical のイベントハンドラに到達します。Lexical はこれらのイベントを自身のノード選択に対するコマンドとして解釈し、SELECT_ALL_COMMAND・CUT_COMMAND・COPY_COMMAND を発火させていました。
結果として、キャプション入力中に Ctrl+A を押すとテキスト全選択ではなくエディタ全体の選択が発動し、Ctrl+X を押すとキャプションテキストではなく画像ノードそのものが削除され、Ctrl+C を押すとフォーカスが失われるという深刻な操作上のバグが発生していました。
既存の実装では stopPropagation() が Enterキーの処理ブランチ内にのみ存在し、他のキーイベントやクリップボードイベントには適用されていなかったことが直接の原因です。
技術的な変更
src/nodes/action_text_attachment_node.js において、イベント伝播の制御を2箇所で修正しました。
変更前(Enter キーのみ stopPropagation):
#handleCaptionInputKeydown(event) {
if (event.key === "Enter") {
event.preventDefault()
event.stopPropagation() // Enter の処理ブランチ内にのみ存在
event.target.blur()
// ...
}
}
変更後(すべての keydown イベントに stopPropagation を適用):
#handleCaptionInputKeydown(event) {
if (event.key === "Enter") {
event.preventDefault()
event.target.blur()
// ...
}
// Stop all keydown events from bubbling to the Lexical root element.
// The caption textarea is outside Lexical's content model and should
// handle its own keyboard events natively (Ctrl+A, Ctrl+C, Ctrl+X, etc.).
event.stopPropagation()
}
さらに、クリップボードイベントに対する伝播停止を <textarea> の初期化時に追加しています。
input.addEventListener("copy", (event) => event.stopPropagation())
input.addEventListener("cut", (event) => event.stopPropagation())
input.addEventListener("paste", (event) => event.stopPropagation())
keydown イベントのみを止めてもクリップボードイベント(copy/cut/paste)は独立して発生するため、両方を塞ぐ必要があります。
テストは test/browser/tests/attachments.test.js に3件のリグレッションテストとして追加されました。各テストは画像をアップロードしてキャプションにテキストを入力した後、ショートカット実行後の選択範囲・テキスト値・図の存在をPlaywrightで検証します。macOS/Linux 両プラットフォームに対応するため、モディファイアキーを process.platform === "darwin" ? "Meta" : "Control" で切り替えています。
設計判断
Lexical のコンテンツモデル外の要素に対する原則として、stopPropagation() の全面適用 が採用されました。
修正はイベントリスナーの追加のみであり、Lexical のコマンド優先度や内部APIには一切触れていません。これは Lexical のアーキテクチャへの最小侵襲な対処です。<textarea> が自身のイベントを自律的に処理し、外部(Lexical)への漏れを遮断するという責任の分離として整理できます。
この判断は .claude/skills/bugs-reproducer.md の知識ベースにも反映されており、「Lexical の contenteditable ルート内に配置した非Lexical要素は、keydown・copy・cut・paste の4イベントすべてに stopPropagation() を設定すること」というルールとして明文化されています。今後同様の要素を追加する際の設計指針となります。
まとめ
本修正は、Lexical の contenteditable ルートに埋め込まれた非Lexical要素のイベント管理を適切に封じ込めることで、標準的なテキスト編集操作を復元したものです。stopPropagation() の適用範囲を keydown 全体とクリップボードイベントに広げるという最小限の変更でありながら、その原則をドキュメント化することで将来の同種バグを予防する設計になっています。