テーブルのドラッグ選択が2セルで止まるバグを修正
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.move に steps: 8 を指定して段階的にカーソルを移動させ、最終的に .lexxy-content__table-cell--selected クラスを持つセルが3つになることを検証します。steps オプションによる細かいポインター移動がドラッグ選択の実挙動を正確に再現している点が特徴的です。
設計判断
初期化タイミングをリスナーで制御するアプローチが採用されました。
単純な修正としては registerTableSelectionObserver の呼び出し順を変える、あるいは遅延初期化を挟むといった手もありますが、本PRでは registerRootListener という Lexical が提供するライフサイクルフックを活用しています。これにより、ルートの存在を前提とした初期化コードをエクステンション登録の流れの中に自然に組み込みつつ、ルート取り外し時のクリーンアップも同一のコールバック内で完結させています。
if (rootElement) の条件分岐は単純ですが、nullチェックを省くとルートが取り外されるタイミングで不正な呼び出しが発生するため、この1行が正確なライフサイクル管理の鍵となっています。
まとめ
エクステンション登録とDOMルート初期化の順序という、フレームワーク利用時に踏みやすい落とし穴を registerRootListener で解決した変更です。初期化依存関係をライフサイクルフックに委ねることで、Lexicalのエクステンション体系との整合性を保ちながらリグレッションを修正しています。