ブロック引用内リスト編集のバックスペースと挿入バグを修正
Lexical エディタで、ブロック引用内部の箇条書きリストに対する Backspace とリストトグルの動作が期待と異なる2件の欠陥を、選択判定とリスト挿入ロジックの拡張で根本的に解消しました。
背景
引用内部のリスト項目で Backspace を押すと、空行が <p><br></p> として残り、ユーザーがギャップを閉じられないという報告がありました。また、引用内の段落にリストを適用すると、引用全体が単一のリスト項目に吸収され、引用が壊れるという別の不具合も同時に報告されていました。これらは QuoteNode と ListNode の階層判定が不十分だったことが根本原因です。
技術的な変更
選択が引用内部かを判定するプロパティ追加
src/editor/selection.js に isInsideBlockQuote プロパティを導入し、現在の選択が QuoteNode 配下にあるかを nearestNodeOfType(QuoteNode) で判定できるようにしました。
+ get isInsideBlockQuote() {
+ return this.nearestNodeOfType(QuoteNode)
+ }
このプロパティはリスト操作ロジックで引用内部かどうかを分岐させる基盤となります。
リスト適用ロジックの統合と引用対応
src/editor/contents.js の applyUnorderedListFormat と applyOrderedListFormat は共通メソッド #applyListFormat に集約され、引用内部かどうかを判定した上で処理を分岐します。引用内部の場合は新メソッド #insertListInsideQuote が呼び出され、選択されたブロックをグループ化して個別にリスト化します。
+ #applyListFormat(listType, command) {
+ if (this.selection.isInsideBlockQuote) {
+ this.#insertListInsideQuote(listType)
+ } else {
+ this.#splitParagraphsAtLineBreaksUnlessInsideList()
+ this.editor.dispatchCommand(command)
+ }
+ }
連続ブロックをグループ化するヘルパー追加
src/helpers/lexical_helper.js に $consecutiveSiblingGroups を導入し、選択範囲内で隣接したブロック要素を同一リストにまとめるロジックを提供します。これにより、引用内部の複数段落を一括でリスト化しつつ、引用構造を保持できます。
+export function $consecutiveSiblingGroups(blocks) {
+ const ordered = [...blocks].sort((a, b) => a.getIndexWithinParent() - b.getIndexWithinParent())
+ const groups = []
+ for (const block of ordered) {
+ const lastGroup = groups.at(-1)
+ const previous = lastGroup?.at(-1)
+ if (previous && previous.getParent().is(block.getParent()) && previous.getNextSibling()?.is(block)) {
+ lastGroup.push(block)
+ } else {
+ groups.push([block])
+ }
+ }
+ return groups
+}
空リスト項目のBackspace処理修正
src/editor/selection.js の空リスト項目削除ロジックに、対象が QuoteNode 配下にある場合の特別分岐を追加しました。引用内部では空項目を削除しカーソルを次の項目へ移動させ、段落を挿入してギャップを残す既存の振る舞いは保持します。
if (previousSibling) {
previousSibling.selectEnd()
listItem.remove()
} else if ($isQuoteNode(listNode.getParent())) {
nextSibling.selectStart()
listItem.remove()
} else {
const paragraph = $createParagraphNode()
listNode.insertBefore(paragraph)
listItem.remove()
paragraph.selectStart()
}
テスト追加
Playwright テスト test/browser/tests/formatting/list_in_quote.test.js が新規追加され、空リスト項目での Backspace、引用内段落へのリスト挿入、リストトグルのオンオフすべてが期待通りに動作することを自動検証しています。5 件の失敗テストが main ブランチで失敗し、今回の変更で全て成功します。
設計判断
引用内部かどうかの判定と処理分岐 を Selection オブジェクトにプロパティとして持たせ、Contents はそのフラグだけでリスト挿入ロジックを切り替える設計にしました。これにより、既存のリスト処理コードを大幅に改変せずに引用対応を追加でき、後方互換性と拡張性のバランスが取れています。
ヘルパー関数 $consecutiveSiblingGroups の導入は、引用内部でのブロックグルーピングという特定ニーズに応える汎用化です。現時点では contents.js のみで使用されていますが、将来的に類似パターンが増える場合に再利用が期待できます。
空リスト項目のBackspace挙動 の分岐は、ユーザー体験(ギャップが閉じない)に直結する UI の微調整です。引用外での既存振る舞いは保持しつつ、引用内部だけ特化したパスを追加した点が、UX 改善と後方互換性のトレードオフとして評価できます。
まとめ
今回の PR は、ブロック引用内部でのリスト編集に関わる 2 つの欠陥を、選択判定の拡張 と リストロジックの分離 で根本的に解消しました。isInsideBlockQuote による判定と $consecutiveSiblingGroups を用いたリスト化で、既存コードへの侵入度を最小限に抑えつつ UI の一貫性を回復しています。追加された自動テストにより回帰防止も確保され、エディタ全体の信頼性が向上しました。