ツールバーボタンクリック後にCtrl+Zが効かない問題の修正
ツールバーをマウスでクリックしたあとキーボードショートカットが機能しなくなるバグを、2つの独立した原因を修正することで解消しました。フォーカス管理の不備と不要な editor.update() ネストが原因でした。
背景
ツールバーボタンをマウスでクリックしたあと、Ctrl+Zを押してもアンドゥが効かない問題が報告されていました。原因は単一ではなく、フォーカスの問題とネストされた editor.update() の問題という2つの独立したバグが組み合わさっていました。
問題の一つ目は、マウスでツールバーボタンをクリックするとフォーカスがそのボタンに移動し、エディタに戻らないことです。この状態でCtrl+Zを押すと、キーイベントはLexicalのUndo Handlerではなくブラウザに送られるため、アンドゥが機能しませんでした。ツールバーの「元に戻す」ボタン自体は正常に動作していましたが、それはボタンが UNDO_COMMAND を明示的にLexicalへディスパッチしているからです。
問題の二つ目は、dispatchInsertCodeBlock が自身の処理を this.editor.update() でラップしていたことです。しかしこのメソッドはツールバーの editor.update() 呼び出しの内部からすでに実行されており、ネストした update() が生まれていました。bold・italic・strikethrough・quoteなど他のフォーマットディスパッチャーとも一貫性がなく、不整合な状態でした。
技術的な変更
2つの問題はそれぞれ独立したファイルへの修正で対処されました。
src/elements/toolbar.js には、マウスクリックによるコマンドディスパッチのあとに editor.focus() を呼び出す1行が追加されました。
変更前:
this.editor.update(() => {
this.editor.dispatchCommand(command, payload)
}, { tag: isKeyboard ? SKIP_DOM_SELECTION_TAG : undefined })
変更後:
this.editor.update(() => {
this.editor.dispatchCommand(command, payload)
}, { tag: isKeyboard ? SKIP_DOM_SELECTION_TAG : undefined })
if (!isKeyboard) this.editor.focus()
isKeyboard フラグで分岐しており、キーボード操作の場合は focus() を呼びません。これはキーボードでツールバーを操作している場合にフォーカスをエディタへ戻してしまうと、アクセシビリティ上の問題が生じるためです。
src/editor/command_dispatcher.js では、dispatchInsertCodeBlock から不要な editor.update() ラッパーが取り除かれました。
変更前:
dispatchInsertCodeBlock() {
this.editor.update(() => {
if (this.selection.hasSelectedWordsInSingleLine) {
this.editor.dispatchCommand(FORMAT_TEXT_COMMAND, "code")
} else {
this.contents.toggleNodeWrappingAllSelectedLines((node) => $isCodeNode(node), () => new CodeNode("plain"))
}
})
}
変更後:
dispatchInsertCodeBlock() {
if (this.selection.hasSelectedWordsInSingleLine) {
this.editor.dispatchCommand(FORMAT_TEXT_COMMAND, "code")
} else {
this.contents.toggleNodeWrappingAllSelectedLines((node) => $isCodeNode(node), () => new CodeNode("plain"))
}
}
あわせて test/browser/tests/toolbar.test.js に4つのリグレッションテストが追加されました。インラインコード・部分選択・単一行コードブロック・複数行コードブロックの各シナリオで、フォーマット適用後にCtrl+Zでアンドゥできることを検証しています。
設計判断
isKeyboard による分岐でアクセシビリティとUXを両立する設計が採用されました。マウス操作後は editor.focus() でエディタに戻し、キーボード操作後はツールバー上のフォーカスをそのまま維持します。キーボードユーザーがTab/Enterでツールバーを操作する場合、フォーカスをエディタへ強制的に戻すとツールバー上のナビゲーションが壊れるため、この分岐は意図的です。
dispatchInsertCodeBlock の修正は、他のフォーマットディスパッチャーとの一貫性を回復する変更です。PRでは「bold・italic・strikethrough・quoteは同様のラッパーを持っていない」と明示されており、コードブロックだけが特殊扱いになっていた状態を解消しています。
まとめ
この修正は、フォーカス管理の欠如とLexical更新コンテキストの誤ったネストという2つの独立した原因を同時に解消しています。if (!isKeyboard) this.editor.focus() という1行の追加と editor.update() ラッパーの削除という最小限の変更で、他のフォーマット操作との整合性を保ちながら問題を修正した判断といえます。