矢印キーナビゲーションで添付ファイルに意図せずジャンプする問題を修正
Selectionクラスの#getNextNodeFromTextEndと#getPreviousNodeFromTextStartを修正し、隣接するノードがDecoratorNodeでない場合にnullを返すことで、テキスト境界での矢印キーナビゲーションがLexicalのデフォルト動作に委ねられるようになりました。
背景
イタリックと通常テキストのようなフォーマット境界を矢印キーで移動すると、添付ファイルに予期せずフォーカスが移動するという問題がありました。例えば「<em>italic</em> plain」に続いて画像添付がある場合、italicの末尾で右矢印を押すとplainではなく添付ファイルが選択されてしまっていました。
この原因は、#getNextNodeFromTextEndと#getPreviousNodeFromTextStartが隣接ノードを確認する際の判定ロジックにありました。隣接ノードがDecoratorNodeでない場合(異なるフォーマットのテキストノードなど)、そのノードをスキップして親要素の次/前の兄弟ノードに移動していました。この「スキップして親の兄弟へ」という動作が、段落の末尾にある添付ファイルへの意図しないジャンプを引き起こしていました。
技術的な変更
src/editor/selection.jsにおいて、隣接ノードへの参照を変数にキャッシュした上で、DecoratorNodeか否かに加え「隣接ノードが存在するか否か」の判定を追加しました。
変更前:
#getNextNodeFromTextEnd(anchorNode) {
if (anchorNode.getNextSibling() instanceof DecoratorNode) {
return anchorNode.getNextSibling()
}
const parent = anchorNode.getParent()
return parent ? parent.getNextSibling() : null
}
#getPreviousNodeFromTextStart(anchorNode) {
if (anchorNode.getPreviousSibling() instanceof DecoratorNode) {
return anchorNode.getPreviousSibling()
}
const parent = anchorNode.getParent()
return parent.getPreviousSibling()
}
変更後:
#getNextNodeFromTextEnd(anchorNode) {
const nextSibling = anchorNode.getNextSibling()
if ($isDecoratorNode(nextSibling)) {
return nextSibling
}
if (nextSibling != null) {
return null
}
const parent = anchorNode.getParent()
return parent ? parent.getNextSibling() : null
}
#getPreviousNodeFromTextStart(anchorNode) {
const previousSibling = anchorNode.getPreviousSibling()
if ($isDecoratorNode(previousSibling)) {
return previousSibling
}
if (previousSibling != null) {
return null
}
const parent = anchorNode.getParent()
return parent ? parent.getPreviousSibling() : null
}
nextSibling != nullの条件が真の場合にnullを返すことで、Lexical自身のデフォルトカーソル移動ロジックがテキストノード間のナビゲーションを処理するようになります。また、#getPreviousNodeFromTextStartではparent.getPreviousSibling()を直接呼び出していたためparentがnullの場合にクラッシュする可能性があったことも、parent ? ... : nullのnullガードで修正されています。
あわせて、instanceof DecoratorNodeという判定からLexical公式の型チェック関数$isDecoratorNode()への変更も行われました。これはLexicalのAPIコンベンションに沿った修正であり、DecoratorNodeクラスのimportも不要になっています。
設計判断
「カスタムロジックが処理すべきケースを明確に限定する」アプローチが採用されました。
修正前のコードは「DecoratorNodeでなければ親の兄弟へ」という過剰な責任を持っていました。修正後は「DecoratorNodeなら自分で処理、それ以外の兄弟があるならLexicalに任せる(nullを返す)、兄弟がないなら親の兄弟へ」という責任分担が明確になっています。nullを返すことがLexicalのデフォルト動作への委譲シグナルとして機能しており、エッジケースへの対処を上位ライブラリのロジックに委ねる設計です。
回帰テストとしてtest/browser/tests/attachments/arrow_navigation_attachment.test.jsが追加され、イタリック→通常テキスト境界での右矢印・左矢印ナビゲーション後に添付ファイルが選択されないことをPlaywrightで検証しています。カーソル位置の確認にはテキスト入力(Xキー送信)を用いており、値にXが含まれることでカーソルがテキスト内にとどまっていることを間接的に確認しています。
まとめ
カスタムのカーソル移動ロジックが処理すべき範囲をDecoratorNodeに限定し、それ以外の隣接ノードが存在する場合はLexical本体に処理を委譲することで、フォーマット境界での矢印キーナビゲーションが期待通りに動作するようになりました。nullを返すという単純な変更が「過剰な介入を避ける」設計の修正として機能している点が、このPRの核心です。