テーブルのドラッグ選択が2セルで止まるバグを修正

basecamp/lexxy

Lexicalの registerTableSelectionObserver をルートDOM要素が存在するタイミングで登録するよう変更し、マウスのクリック+ドラッグで3セル以上を選択できないリグレッションを修正しました。

背景

テーブルセルをマウスのドラッグで選択すると、3セル目以降が選択されず必ず2セルで止まるという動作が報告されていました。この問題はリグレッション、すなわち以前は正しく動作していた機能の劣化です。

再現手順は単純で、テーブルを挿入し、任意のセルからクリック+ホールドで複数セルをまたいでカーソルを動かすと発生します。期待動作はカーソルが通過したすべてのセルが選択されることですが、実際には隣接する2セルしか選択されませんでした。

PRのコメントが問題の本質を端的に説明しています。Lexxyはエクステンションを setRootElement() の呼び出しより前に登録しますが、テーブルのドラッグ選択機能はポインターイベントのハンドラーを接続するためにDOMのルート要素を必要とします。ルートが存在しない状態で registerTableSelectionObserver を呼び出していたことが、選択ロジックの不完全な初期化につながっていました。

技術的な変更

src/extensions/tables_extension.js の初期化処理を変更し、registerTableSelectionObserver の呼び出しをルートリスナーのコールバック内に移しました。

変更前:

return mergeRegister(
  // Register Lexical table plugins
  registerTablePlugin(editor),
  registerTableSelectionObserver(editor, true),
  // ...
)

変更後:

return mergeRegister(
  registerTablePlugin(editor),

  // Lexxy registers extensions before setRootElement(), but table
  // drag-selection needs a root before wiring its pointer handlers.
  editor.registerRootListener((rootElement) => {
    if (rootElement) {
      return registerTableSelectionObserver(editor, true)
    }
  }),

  // Bug fix: Prevent hardcoded background color (Lexical #8089)
  editor.registerNodeTransform(TableCellNode, (node) => {

editor.registerRootListener はルートDOM要素が設定または解除されるたびに呼ばれるコールバックを登録します。rootElement が存在する場合(null でない場合)にのみ registerTableSelectionObserver を呼び出すことで、ポインターハンドラーがDOMに確実にアタッチされた状態で初期化されます。なお、registerTableSelectionObserver が返すクリーンアップ関数をリスナーのコールバックからそのまま返しているため、ルートが取り外された際にはオブザーバーも正しく解除されます。

同時に、ブラウザテスト test/browser/tests/tables/drag_selection.test.js が新規追加されました。このテストはPlaywrightを使用し、3×3テーブルを挿入したうえで page.mouse.movesteps: 8 を指定して段階的にカーソルを移動させ、最終的に .lexxy-content__table-cell--selected クラスを持つセルが3つになることを検証します。steps オプションによる細かいポインター移動がドラッグ選択の実挙動を正確に再現している点が特徴的です。

設計判断

初期化タイミングをリスナーで制御するアプローチが採用されました。

単純な修正としては registerTableSelectionObserver の呼び出し順を変える、あるいは遅延初期化を挟むといった手もありますが、本PRでは registerRootListener という Lexical が提供するライフサイクルフックを活用しています。これにより、ルートの存在を前提とした初期化コードをエクステンション登録の流れの中に自然に組み込みつつ、ルート取り外し時のクリーンアップも同一のコールバック内で完結させています。

if (rootElement) の条件分岐は単純ですが、nullチェックを省くとルートが取り外されるタイミングで不正な呼び出しが発生するため、この1行が正確なライフサイクル管理の鍵となっています。

まとめ

エクステンション登録とDOMルート初期化の順序という、フレームワーク利用時に踏みやすい落とし穴を registerRootListener で解決した変更です。初期化依存関係をライフサイクルフックに委ねることで、Lexicalのエクステンション体系との整合性を保ちながらリグレッションを修正しています。

記事メタデータ

Generated by:
Claude Sonnet 4.6 for DiffDaily
LLM Trace:
2af77253

この記事は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:ファイルパス)とPR番号のリンク記法が正しく使用されています。

対象読者への適合性 ✓ PASS

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

LexicalのライフサイクルやDOM初期化に関する内容は専門性が高く、対象読者であるエンジニアに適した技術レベルです。過度な説明もありません。

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

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

各セクション・各パラグラフが総論から始まる構成になっており、トピックセンテンスが明確です。1段落1トピックの原則や適切な段落長も守られており、非常に読みやすいです。

Diff内容との照合 ✓ PASS

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

記事で引用されている変更前後のコードは、提供されたDiffの内容と完全に一致しています。新規追加されたテストファイルに関する説明も正確です。

技術用語の正確性 ✓ PASS

技術用語の正確な使用

`registerRootListener`や`registerTableSelectionObserver`など、Lexicalフレームワークの専門用語が正確に使用されています。

説明の技術的正確性 ✓ PASS

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

エクステンションの初期化タイミングが問題の原因であること、そして`registerRootListener`がその解決策となる理由についての説明は、技術的に正確かつ論理的です。

事実の突合 ✓ PASS

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

記事内の主張はすべてPRのDescription、Diff内のコード、およびコードコメントによって裏付けられており、ハルシネーションは検出されませんでした。

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

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

PR番号(#1026)やファイルパスなどの固有名詞、数値はすべて正確です。

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

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

記事タイトルはPRの主題である「テーブルのドラッグ選択に関するバグ修正」を正確に反映しており、内容との一貫性が保たれています。

外部知識の正確性 ✓ PASS

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

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

時間表現の正確性 ✓ PASS

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

問題が「リグレッション」であることなど、時間的な背景に関する表現はPR情報と正確に一致しています。