リストの先頭空アイテムをBackspaceで段落に変換する挙動を修正
リスト先頭の空アイテムでBackspaceを押した際、アイテムを削除するのではなくリストの上に空の段落を挿入する挙動に変更されました。これにより、ユーザーが期待する「リストから抜け出す」操作が正しく機能するようになります。
背景
Backspaceによるリストアイテム削除の挙動が、アイテムの位置によって異なる扱いを必要としていました。Lexicalエディタでは、空のリストアイテムで collapseAtStart が発生すると、デフォルトでアイテムを段落に変換してリストの上に配置する動作をします。しかし、この動作は先頭以外の空アイテムに適用されると、カーソルがリスト外に飛んでしまうという問題を引き起こしていました。
そのため #948 以前は、#removeEmptyListItem メソッドが「次の兄弟が存在する場合のみ」独自処理を行い、それ以外はLexicalのデフォルトに委ねる設計になっていました。しかし、この設計は先頭アイテムのケースを正しく扱えていませんでした。先頭アイテムには前の兄弟がなく、次の兄弟はあるため、独自処理が実行されて削除されてしまい、Lexicalのデフォルト動作(段落への変換)が発動しなかったのです。
結果として、先頭の空アイテムでBackspaceを押すと、期待される「空段落をリスト上部に作る」動作ではなく、アイテムがただ削除される挙動になっていました。
技術的な変更
#removeEmptyListItem メソッドのロジックが、アイテムの位置(先頭か否か)に応じて処理を分岐するように再設計されました。
変更前:
const previousSibling = listItem.getPreviousSibling()
if (previousSibling) {
previousSibling.selectEnd()
} else {
nextSibling.selectStart()
}
listItem.remove()
return true
変更後:
const previousSibling = listItem.getPreviousSibling()
if (previousSibling) {
previousSibling.selectEnd()
listItem.remove()
return true
}
const listNode = $getNearestNodeOfType(listItem, ListNode)
if (!listNode) return false
const paragraph = $createParagraphNode()
listNode.insertBefore(paragraph)
listItem.remove()
paragraph.selectStart()
return true
前の兄弟(previousSibling)が存在する場合は、従来通りそのアイテム末尾にカーソルを移動してアイテムを削除します。前の兄弟が存在しない(=先頭アイテム)の場合は、$getNearestNodeOfType でリストノードを取得し、$createParagraphNode で新しい段落を生成してリストの直前に挿入、その後アイテムを削除して段落の先頭にカーソルを置きます。
なお、リストに兄弟が一切ない(唯一のアイテム)場合は nextSibling の存在チェックにより早期リターンし、Lexicalのデフォルト動作に委ねる設計は維持されています。
テストも同様に更新されており、先頭アイテム削除後のHTMLアサーションが <ul><li>Some text</li></ul> から <p><br></p><ul><li>Some text</li></ul> に変更されています。また、「上に段落がある状態での先頭アイテム削除」シナリオをカバーするテストも追加されました。
設計判断
位置に応じた明示的な分岐 を#removeEmptyListItem 内に設け、Lexicalのデフォルト動作への依存をより意図的に制御する設計が採用されました。
変更前は「次の兄弟があるか」という条件で独自処理の適用を判定していたため、先頭アイテム(次の兄弟はあるが前の兄弟はない)が意図せず独自処理に巻き込まれていました。変更後は「前の兄弟があるか」という条件を主軸にし、先頭アイテムの処理を明示的に独自実装することで、各ケースの挙動がコメントと対応する形で整理されています。
また、段落の挿入に $createParagraphNode と insertBefore を組み合わせる手法はLexicalの標準APIに沿っており、エディタのノードツリーを直接操作する既存の実装スタイルと一貫しています。
まとめ
#removeEmptyListItem の条件分岐を「前の兄弟の有無」を軸に再構成することで、先頭・中間・末尾の各ケースが意図通りの挙動を示すようになりました。Lexicalのデフォルト動作を無効化する範囲を最小限に絞りつつ、先頭アイテムの「リストから抜け出す」操作を正しくハンドルした修正です。