アップロード完了時のフォーカス奪取を防ぐ非同期更新タグの制御

basecamp/lexxy

Lexicalエディタのアップロードライフサイクルメソッドが非同期に完了した際、ユーザーが別のフィールドにフォーカスを移していても、エディタにフォーカスが戻ってしまう問題を修正しました。SKIP_DOM_SELECTION_TAG の条件付き適用により、エディタがフォーカスを持たない状態での不要なDOM選択の復元を防ぎます。

背景

ActionTextAttachmentUploadNode のアップロードライフサイクルメソッド(進捗更新・完了・エラー処理)は非同期で実行されるため、ユーザーがアップロード中にタイトルフィールドなど別の入力欄へフォーカスを移した後に完了する可能性があります。この状況でLexicalのリコンサイラが動作すると、DOM選択がエディタ内に戻されてしまい、ユーザーの作業を中断させていました。

具体的には、Lexicalの更新処理に SKIP_DOM_SELECTION_TAG を付与しない場合、リコンサイラはDOM選択をエディタ内の状態に同期しようとします。アップロード完了時にノードの置換と選択転送が行われると、エディタ外にあったフォーカスがエディタに引き戻されるという副作用が生じていました。

従来の実装では、アップロード関連の更新すべてに SILENT_UPDATE_TAGS が使われていましたが、このタグにはDOM選択の同期をスキップする効果が含まれておらず、フォーカス奪取を防ぐには不十分でした。

技術的な変更

#backgroundUpdateTags ゲッターを新設し、エディタのフォーカス状態に応じてLexical更新タグを動的に切り替える仕組みを導入しました。

ソースファイル src/nodes/action_text_attachment_upload_node.js の変更内容は以下の通りです。まず lexical からの SKIP_DOM_SELECTION_TAG のインポートが追加されました:

// 変更前
import { $setSelection } from "lexical"

// 変更後
import { $setSelection, SKIP_DOM_SELECTION_TAG } from "lexical"

次に #backgroundUpdateTags ゲッターが追加されました:

// Upload lifecycle methods (progress, completion, errors) run asynchronously and may
// fire while the user is focused on another element (e.g., a title field). Without
// SKIP_DOM_SELECTION_TAG, Lexical's reconciler would move the DOM selection back into
// the editor, stealing focus from wherever the user is currently typing.
get #backgroundUpdateTags() {
  if (this.#editorHasFocus) {
    return SILENT_UPDATE_TAGS
  } else {
    return [SILENT_UPDATE_TAGS, SKIP_DOM_SELECTION_TAG].flat()
  }
}

すべてのライフサイクルメソッドで SILENT_UPDATE_TAGS を直接渡していた箇所を this.#backgroundUpdateTags に置き換えることで、エディタがフォーカスを持たない場合に限り SKIP_DOM_SELECTION_TAG が追加されます。

#showUploadedAttachment では、ノード選択の転送もエディタのフォーカス状態で制御するよう変更されています。editor.update() コールバック内でフォーカス状態を参照すると更新時点のスナップショットと異なる可能性があるため、コールバックの外でフォーカス状態を事前に取得しています:

// 変更前
#showUploadedAttachment(blob) {
  this.editor.update(() => {
    const shouldTransferNodeSelection = this.isSelected()
    // ...
  }, { tag: SILENT_UPDATE_TAGS })
}

// 変更後
#showUploadedAttachment(blob) {
  const editorHasFocus = this.#editorHasFocus

  this.editor.update(() => {
    const shouldTransferNodeSelection = editorHasFocus && this.isSelected()
    // ...
  }, { tag: this.#backgroundUpdateTags })
}

動作のフローを整理すると以下のようになります:

システムテスト test/system/upload_focus_test.rb も追加されています。XHRインターセプトによってアップロードの完了タイミングを制御し、アップロード中にタイトルフィールドへフォーカスを移した後でアップロードを完了させ、フォーカスが保持されることを検証しています。

設計判断

エディタのフォーカス状態に応じてタグを動的に選択するアプローチが採用されました。

フォーカスがある場合は従来通り SILENT_UPDATE_TAGS のみを使い、ない場合にのみ SKIP_DOM_SELECTION_TAG を追加する設計です。これにより、エディタにフォーカスがある通常のアップロード完了時の挙動(ノードが選択された状態になる)を維持しつつ、フォーカスが外れた場合の副作用だけを除去しています。

editor.update() コールバックの外でフォーカス状態をキャプチャしている点も注目です。Lexicalの更新はバッチ処理されることがあるため、コールバック内でフォーカス状態を参照すると実際の更新タイミングとずれが生じる可能性があります。const editorHasFocus = this.#editorHasFocus を先に評価することで、更新タグの決定と一貫した状態を参照しています。

まとめ

本PRは、非同期処理とUIフォーカス管理という典型的な競合状態を、LexicalのDOM選択制御タグを活用して解決した変更です。「エディタがフォーカスを持つ場合は通常動作、持たない場合はDOM選択を変更しない」という明確な条件分岐を1つのゲッターに集約することで、各ライフサイクルメソッドの変更を最小限に抑えつつ問題を根本から解消しています。

記事メタデータ

Generated by:
Claude Sonnet 4.6 for DiffDaily
LLM Trace:
6c89bf5f

この記事は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リンク記法の正確性

ファイル名付きシンタックスハイライト(`言語:ファイルパス`)とPR番号のリンク記法(`[#123](URL)`)が正しく使用されています。

対象読者への適合性 ✓ PASS

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

Lexicalのリコンサイラや更新タグなど、専門用語を前提としており、対象読者であるエンジニアに適した技術レベルで書かれています。

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

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

各セクションが総論→各論の構成になっており、各段落はトピックセンテンスで始まり、1段落1トピックの原則が守られています。可読性が非常に高いです。

Diff内容との照合 ⚠ WARNING

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

提供されたDiff情報が不完全なため、`#backgroundUpdateTags`ゲッターのコードを完全には照合できませんでした。しかし、照合可能な範囲のコード引用は正確であり、記事内のコードも技術的に妥当です。

技術用語の正確性 ✓ PASS

技術用語の正確な使用

`SKIP_DOM_SELECTION_TAG`や`reconciler`などの技術用語が、PR情報と一致しており、文脈上も正しく使用されています。

説明の技術的正確性 ✓ PASS

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

フォーカス状態に応じて更新タグを切り替える仕組みや、`editor.update()`コールバック外で状態を取得する理由など、技術的な説明は正確かつ論理的です。

事実の突合 ✓ PASS

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

記事内の主張はすべてPRのDescriptionやDiff内のコードで裏付けられており、ハルシネーション(捏造)は見られません。

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

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

PR番号(#843)が正確に記載・リンクされています。

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

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

記事のタイトル「アップロード完了時のフォーカス奪取を防ぐ非同期更新タグの制御」は、PRの主題「Prevent upload completion from stealing focus」を的確に反映しています。

外部知識の正確性 ✓ PASS

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

バージョン情報やサポート状況など、PR情報に記載のない外部知識の追加はなく、事実に忠実です。

時間表現の正確性 ✓ PASS

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

時間表現に関する記述はなく、問題の発生状況と解決策を事実として述べており、時間的な歪曲はありません。