ツールバーダイアログのクローズ処理をエディタのselection changeイベントに統合

basecamp/lexxy

Lexxyエディタにおいて、ツールバーのダイアログ(ドロップダウン)を閉じる処理を、より適切なイベントハンドリングに移行する変更が行われました。

背景

これまで、ツールバーのドロップダウンメニュー(ハイライトカラー選択など)は、クリックアウトサイドハンドラーを使用して閉じる処理を実装していました。しかし、この実装にはいくつかの問題がありました:

  • ツールバーとエディタの関係性が適切にモデル化されていなかった
  • lexxy:focus/lexxy:blurイベントがエディタの書き込み領域のフォーカスのみを監視していた
  • ツールバーを操作すると不要なlexxy:blurイベントが発火していた

#670では、これらの問題を解決するため、フォーカスイベントの定義を見直し、ダイアログのクローズ処理をエディタの選択変更イベントに統合しています。

技術的な変更

フォーカスイベントの再定義

エディタ要素全体(ツールバーを含む)をフォーカス管理の対象に変更しました。

変更前:

#handleFocus() {
  // LexicalのBLUR_COMMAND/FOCUS_COMMANDを使用
  this.editor.registerCommand(BLUR_COMMAND, () => { dispatch(this, "lexxy:blur") }, COMMAND_PRIORITY_NORMAL)
  this.editor.registerCommand(FOCUS_COMMAND, () => { dispatch(this, "lexxy:focus") }, COMMAND_PRIORITY_NORMAL)
}

変更後:

#registerFocusEvents() {
  this.addEventListener("focusin", this.#handleFocusIn)
  this.addEventListener("focusout", this.#handleFocusOut)
}

#handleFocusIn(event) {
  if (this.#elementInEditorOrToolbar(event.target) && !this.currentlyFocused) {
    dispatch(this, "lexxy:focus")
    this.currentlyFocused = true
  }
}

#handleFocusOut(event) {
  if (!this.#elementInEditorOrToolbar(event.relatedTarget)) {
    dispatch(this, "lexxy:blur")
    this.currentlyFocused = false
  }
}

#elementInEditorOrToolbar(element) {
  return this.contains(element) || this.toolbarElement?.contains(element)
}

Lexicalのコマンドベースのフォーカス管理から、DOMのfocusin/focusoutイベントを直接監視する方式に変更されています。これにより、ツールバーへのフォーカス移動時にlexxy:blurが発火しなくなりました。

ダイアログクローズ処理の簡素化

ツールバーのドロップダウンから、クリックアウトサイドハンドラーを削除しました。

変更前:

#setupClickOutsideHandler() {
  if (this.clickOutsideHandler) return

  this.clickOutsideHandler = this.#handleClickOutside.bind(this)
  document.addEventListener("click", this.clickOutsideHandler, true)
}

#removeClickOutsideHandler() {
  if (!this.clickOutsideHandler) return

  document.removeEventListener("click", this.clickOutsideHandler, true)
  this.clickOutsideHandler = null
}

変更後:

ツールバー側でエディタの選択変更イベントを監視し、選択が変わったタイミングでダイアログを閉じるように変更されました。

#registerSelectionListener() {
  return this.editor.registerCommand(
    SELECTION_CHANGE_COMMAND,
    () => {
      this.#closeAllOpenDropdowns()
      return false
    },
    COMMAND_PRIORITY_HIGH
  )
}

#closeAllOpenDropdowns() {
  this.querySelectorAll("details[open]").forEach(details => {
    details.removeAttribute("open")
  })
}

ツールバーの非同期初期化

ツールバーがエディタ要素への参照を取得するためのPromiseベースの仕組みが追加されました。

#createEditorPromise() {
  this.editorPromise = new Promise((resolve) => {
    this.resolveEditorPromise = resolve
  })
}

connectedCallback() {
  // ...
  this.resolveEditorPromise(editorElement)
  // ...
}

async getEditorElement() {
  return this.editorElement || await this.editorPromise
}

これにより、ツールバーのドロップダウンはinitialize()メソッド内で非同期的にエディタへの参照を取得できるようになりました。

設計判断

この変更の核心は、「ツールバーはエディタの一部である」という設計思想を明確にした点にあります。

従来のクリックアウトサイドハンドラーは、ダイアログとエディタを別々のコンポーネントとして扱う実装でした。しかし実際には:

  • ユーザーがツールバーを操作している間、エディタのコンテキストから離れたわけではない
  • ツールバーの操作は、エディタへの入力の一部である

この認識に基づき、フォーカス管理の範囲をエディタ要素全体に拡大し、ダイアログのクローズタイミングをエディタの選択変更という、より本質的なイベントに紐付けています。

このアプローチにより、グローバルなクリックイベントハンドラーが不要になり、コンポーネント間の結合度が低減されました。

記事メタデータ

Generated by:
Claude Sonnet 4.5 for DiffDaily

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

品質レビュー結果

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

Review Criteria:

ガイドライン準拠 ✓ PASS

記事構成とDiffDaily Styleへの準拠状況

記事構成の必須要素(Title, Context, Technical Detail)がすべて含まれており、任意要素のDesign Insightも適切に記述されています。カスタムMarkdown構文(ファイル名付きコードブロック、GitHubリンク)も正しく使用されており、ガイドラインを完全に遵守しています。

  • 記事構成(Title、Context、Technical Detail)
  • DiffDaily Styleガイド準拠
  • カスタムMarkdown活用
  • 対象読者への適合性
技術的整合性 ⚠ WARNING

技術的な正確性と表現の適切性

Diff情報が提供されていないため、記事内のコード引用が実際の変更と完全に一致するかの照合はできませんでした。しかし、提示されたコード例は技術的に妥当であり、記事内の説明(フォーカスイベントの変更、クリックアウトサイドハンドラーの削除など)とも整合性が取れています。技術用語も正確です。

  • 技術用語の正確性
  • コード例の正確性
  • 説明の技術的正確性
PR内容との整合性 ⚠ WARNING

元のPR情報との一致度

PRのDescriptionが提供されていないため、記事内の主張(特に「背景」や「設計判断」)の完全な裏付けはできませんでした。しかし、すべての主張はPR Title「Dialog close simplification」と記事内のコード変更から妥当に推測・解釈できる範囲であり、明らかなハルシネーションは見られません。

  • タイトル・説明の一致
  • Diff内容の正確な反映
  • 推測の排除