矢印キーナビゲーションがカーソル位置によらず画像を正しく選択するよう修正

basecamp/lexxy

Up/Downキーで段落から画像(デコレーターノード)へ移動する際、カーソルがテキストの途中にある場合でも画像が確実に選択されるよう、DOM ジオメトリを用いたビジュアル行検出が追加されました。

背景

矢印キーによる画像選択が不安定という問題が報告されていました。カーソルが段落の先頭(offset 0)または末尾にある場合は画像が選択されるものの、単語の途中など中間オフセットにある場合はブラウザのネイティブナビゲーションが非編集可能な デコレーターノード をスキップしてしまうという挙動です。

#871 および #708 にて詳細が報告されており、@lylo によって「単語先頭では画像が選択されるが、単語途中からUpキーを押すと画像が選択されない」という動画付きの再現レポートが提供されています。ネイティブナビゲーションに委ねるのではなく、Lexxy側でビジュアル行の位置を判定して介入する方針が採られました。

技術的な変更

src/editor/selection.js に、カーソルがブロックの先頭または末尾のビジュアル行に位置しているかを DOM ジオメトリで判定するメソッド群が追加されました。

修正前のコードは、テキストノード内のオフセットのみで判断していました。

変更前:

// 下方向
if (offset < anchorNode.getTextContentSize()) return null
return this.#getNextNodeFromTextEnd(anchorNode)

// 上方向
if (offset > 0) return null
return this.#getPreviousNodeFromTextStart(anchorNode)

変更後:

// 下方向
if (offset === anchorNode.getTextContentSize()) return this.#getNextNodeFromTextEnd(anchorNode)
if (this.#isCursorOnLastVisualLineOfBlock(anchorNode)) {
  const topLevelElement = anchorNode.getTopLevelElement()
  return topLevelElement ? topLevelElement.getNextSibling() : null
}
return null

// 上方向
if (offset === 0) return this.#getPreviousNodeFromTextStart(anchorNode)
if (this.#isCursorOnFirstVisualLineOfBlock(anchorNode)) {
  const topLevelElement = anchorNode.getTopLevelElement()
  return topLevelElement ? topLevelElement.getPreviousSibling() : null
}
return null

オフセットが末尾(または先頭)と一致する場合は従来のパスを通り、一致しない場合に #isCursorOnLastVisualLineOfBlock / #isCursorOnFirstVisualLineOfBlock でビジュアル行の端かどうかを判定します。ビジュアル行の端であれば、トップレベル要素の次(または前)の兄弟ノードを返してデコレーターを選択します。ビジュアル行の端でなければ null を返し、ブラウザのネイティブナビゲーションに委ねます。

新たに追加された #isCursorOnEdgeLineOfBlock メソッドは、カーソルの Y 座標とブロック先頭・末尾の Y 座標を DOM ジオメトリで比較することで、カーソルが先頭または末尾のビジュアル行に位置しているかを判定します。コメントにも「Y 座標でブロックの edge line を比較する」と明記されており、テキストオフセットではなく視覚的な行位置を根拠にするアプローチです。

テスト面では test/browser/tests/attachments/arrow_navigation_attachment.test.js に 2 つのケースが追加されました。「Bottom paragraph」の 6 文字目(単語途中)にカーソルを置いてUpキーを押すと figure.attachment.node--selected が 1 件になることを確認するケースと、その対称となるDownキーのケースです。TreeWalker でテキストノードを直接操作して任意オフセットにカーソルを設定しており、回帰テストとして機能します。

設計判断

テキストオフセットによる判定からビジュアル行検出への移行 が本修正の核心です。

変更前の条件式(offset < sizeoffset > 0)は、「オフセットが末尾・先頭でなければブロックの端ではない」という仮定に基づいていました。しかし複数行段落ではこの仮定が崩れ、また単一行でもカーソルが視覚的に行の端にいる場合(単語途中であっても段落が 1 行しかない場合など)はデコレーターへ移動すべきです。DOM ジオメトリ(Y 座標比較)を使うことで、折り返しを含む任意の段落に対して「カーソルが視覚的な端の行にいるか」を正確に判定できます。

既存の末尾・先頭オフセットのパスは削除せず、完全一致の場合の早期リターンとして残しています。これにより、オフセットが確定的な場合はジオメトリ計算を省略できるため、不要なDOM操作を抑えつつ互換性を保つ設計になっています。

まとめ

テキストオフセットという構造上の情報だけでは「カーソルが視覚的な行の端にいるか」を正確に判定できないという根本的な限界に対処した修正です。DOM ジオメトリによるビジュアル行検出を組み合わせることで、カーソル位置によらずデコレーターノードへの矢印キーナビゲーションが安定して動作するようになります。

記事メタデータ

Generated by:
Claude Sonnet 4.6 for DiffDaily
LLM Trace:
3f8d6c0e

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

品質レビュー結果

Review Status:
リトライ後承認
Review Count:
2回 (改善を経て承認)
Reviewed by:
Gemini 2.5 Pro for DiffDaily

Review Criteria:

記事構成 ✓ PASS

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

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

カスタムMarkdown構文 ✓ PASS

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

ファイル名付きシンタックスハイライト(```javascript:src/editor/selection.js```)やGitHubのIssue/PRへのリンク記法が正しく使用されています。

対象読者への適合性 ✓ PASS

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

「デコレーターノード」「DOM ジオメトリ」といった専門用語を前提としており、専門知識を持つエンジニアという対象読者に適合した内容です。

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

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

各セクションが総論→各論の構成になっており、各段落もトピックセンテンスで始まるなど、パラグラフ・ライティングの原則が徹底されています。

Diff内容との照合 ✓ PASS

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

記事で引用されている変更前後のコードは、提供されたDiff情報と完全に一致しています。テストコードに関する言及も正確です。

技術用語の正確性 ✓ PASS

技術用語の正確な使用

「ビジュアル行検出」「テキストオフセット」など、PRの文脈に沿った技術用語が正確に使用されています。

説明の技術的正確性 ✓ PASS

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

オフセットによる判定の限界と、DOMジオメトリ(Y座標)を用いた視覚的な行判定という解決策の技術的説明が、Diffの内容と整合しており正確です。

事実の突合 ✓ PASS

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

記事内のすべての主張(問題の背景、Issue番号、解決策)は、提供されたPRのDescriptionやDiffの内容によって裏付けられています。ハルシネーションは見られません。

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

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

PR番号(#872)、Issue番号(#871, #708)などの固有名詞や数値はすべて正確に記載されています。

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

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

PRのタイトル「Fix arrow key navigation skipping images...」の内容を、「画像を正しく選択するよう修正」とよりポジティブで分かりやすい表現に要約しており、主題と完全に一致しています。

外部知識の正確性 ✓ PASS

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

記事には、PR情報に含まれないバージョン情報やリリース予定などの外部知識は含まれていません。

時間表現の正確性 ✓ PASS

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

「報告されていました」といった過去の事象に関する時間表現は、PRの文脈と一致しており正確です。