見出し内での水平線挿入クラッシュを `$insertNodeToNearestRoot` で修正
HorizontalDividerNode を見出しなど特定ノード内のカーソル位置に挿入しようとするとエラーが発生していた問題を、$insertNodeToNearestRoot を使ってルート直下に挿入する方式へ変更することで解消しました。
背景
HorizontalDividerNode はドキュメントのルート直下に配置される必要があるノードです。しかし従来の実装では、カーソル位置に直接挿入する insertAtCursorEnsuringLineBelow を使っていたため、見出し(<h2> など)の内部にカーソルがある状態で水平線を挿入しようとするとエラーが発生していました。
Lexical のノードツリーでは、すべてのノードが親子関係を持ちます。見出しノードのような ElementNode の直接の子として HorizontalDividerNode を挿入しようとすると、その配置が不正なツリー構造となり、エラーを引き起こします。本PRはこの制約を正しく扱う挿入APIへの切り替えによって問題を解消しています。
技術的な変更
@lexical/utils の $insertNodeToNearestRoot を使うことで、カーソルの祖先ノードを辿って最も近いルート直下の位置にノードを挿入するよう変更されました。
変更前:
dispatchInsertHorizontalDivider() {
this.contents.insertAtCursorEnsuringLineBelow(new HorizontalDividerNode())
this.editor.focus()
}
変更後:
dispatchInsertHorizontalDivider() {
$insertNodeToNearestRoot(new HorizontalDividerNode)
}
変更点は2つあります。まず、insertAtCursorEnsuringLineBelow の呼び出しを $insertNodeToNearestRoot に置き換えています。$insertNodeToNearestRoot はカーソル位置の祖先ノードを辿り、ルート直下に挿入可能な位置を自動的に選択します。次に、this.editor.focus() の呼び出しが削除されています。これは $insertNodeToNearestRoot 自体が挿入後のフォーカス管理を担うためです。
インポート文にも $insertNodeToNearestRoot が追加されており、@lexical/utils からの取得対象として明示されています。
テストでは、見出し先頭にカーソルを置いた状態で水平線を挿入するシナリオが追加されました。期待されるHTMLは <h2><br></h2><hr><h2>Heading</h2> であり、見出しの前にルート直下の <hr> が挿入され、元の見出し内容が後続の <h2> として残ることが確認されています。
設計判断
カーソル位置への直接挿入からルート直下への挿入へという方針転換が、この変更の核心です。
$insertNodeToNearestRoot はLexicalが公式に提供するユーティリティ関数であり、ブロックレベルのノードをルート直下に配置する際の標準的なアプローチです。独自の挿入ロジック(insertAtCursorEnsuringLineBelow)を維持するよりも、フレームワークが提供するAPIに移譲することで、コードが3行から1行に削減されると同時に、エッジケースへの対応がLexical側に委ねられます。
また、this.editor.focus() の削除はAPIの責務分離の観点からも自然な変更です。挿入APIが挿入後の状態を適切に管理するなら、呼び出し側が追加のフォーカス操作を行う必要はありません。
まとめ
独自の挿入ロジックをLexical標準の $insertNodeToNearestRoot に置き換えるという最小限の変更で、見出し内での水平線挿入クラッシュが解消されました。ノードの配置制約をフレームワーク側のAPIに委ねることで、コードの簡潔さと堅牢性を同時に達成した判断といえます。