矢印キーナビゲーションで添付ファイルに意図せずジャンプする問題を修正

basecamp/lexxy

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の核心です。

記事メタデータ

Generated by:
Claude Sonnet 4.6 for DiffDaily
LLM Trace:
6473efe8

この記事はAIによって自動生成されています。内容の正確性については、必ずソースコードやPRを確認してください。

品質レビュー結果

Review Status:
承認済み
Review Count:
1回
Reviewed by:
Gemini 2.5 Pro for DiffDaily

Review Criteria:

記事構成 ✓ PASS

Title, Context, Technical Detailの存在と明確さ

「リード文(総論)→背景・技術的変更・設計判断(各論)→まとめ(結論)」という3部構成が明確に適用されており、非常に分かりやすい記事構成です。

カスタムMarkdown構文 ✓ PASS

シンタックスハイライト・GitHubリンク記法の正確性

ファイル名付きシンタックスハイライト(```javascript:src/editor/selection.js)およびPR番号のリンク記法([PR #842](URL))は、ガイドラインに準拠して正しく使用されています。

対象読者への適合性 ✓ PASS

エンジニア向けの適切な技術レベルと表現

Lexicalライブラリの内部実装に関する内容であり、専門知識を持つエンジニアを対象として適切に記述されています。不要な初心者向けの説明はありません。

パラグラフ・ライティング ✓ PASS

トピックセンテンス・1段落1トピック・段落長

各セクションが総論→各論の構成で書かれ、各段落の冒頭にトピックセンテンスが置かれているため、非常に読みやすいです。1段落1トピックの原則も守られています。

Diff内容との照合 ✓ PASS

コードブロックとDiff内容の一致

記事内のコードブロック(変更前・変更後)は、提供されたDiff情報と正確に一致しています。コードの引用は適切です。

技術用語の正確性 ✓ PASS

技術用語の正確な使用

`DecoratorNode`, `$isDecoratorNode`, `Playwright`といった技術用語が、PR情報と整合性を保ちながら正確に使用されています。

説明の技術的正確性 ✓ PASS

技術的主張の正確性と論理性

変更の理由(意図しないフォーカスジャンプ)と解決策(nullを返してデフォルト動作に委譲)の説明は、技術的に正確かつ論理的です。

事実の突合 ✓ PASS

PR情報による主張の裏付け(ハルシネーション検出)

記事内のすべての主張(問題の背景、コード変更の意図、テスト内容)は、PRのDescriptionやDiff内のコードによって裏付けられており、ハルシネーションは検出されませんでした。

数値・固有名詞の確認 ✓ PASS

PR番号・コミットID・バージョン等の正確性

PR番号(#842)やファイルパス(src/editor/selection.jsなど)は、提供された情報と完全に一致しています。

タイトル・説明との一致 ✓ PASS

記事タイトル・説明とPR内容の一致

記事のタイトル「矢印キーナビゲーションで添付ファイルに意図せずジャンプする問題を修正」は、PRのタイトル「Arrow key navigation no longer jumps to attachment unexpectedly」の内容を正確に反映しています。

外部知識の正確性 ✓ PASS

PRに記載のない外部知識(LTS、サポート状況など)の不使用

PR情報に含まれないバージョンサポート状況やリリース日程などの外部知識は含まれていません。

時間表現の正確性 ✓ PASS

時間表現がPR情報と一致しているか

「修正し」「なりました」といった時間表現は、完了した変更を指しており、PRのコンテキストと一致しています。