ツールバーダイアログのクローズ処理をエディタのselection changeイベントに統合
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()メソッド内で非同期的にエディタへの参照を取得できるようになりました。
設計判断
この変更の核心は、「ツールバーはエディタの一部である」という設計思想を明確にした点にあります。
従来のクリックアウトサイドハンドラーは、ダイアログとエディタを別々のコンポーネントとして扱う実装でした。しかし実際には:
- ユーザーがツールバーを操作している間、エディタのコンテキストから離れたわけではない
- ツールバーの操作は、エディタへの入力の一部である
この認識に基づき、フォーカス管理の範囲をエディタ要素全体に拡大し、ダイアログのクローズタイミングをエディタの選択変更という、より本質的なイベントに紐付けています。
このアプローチにより、グローバルなクリックイベントハンドラーが不要になり、コンポーネント間の結合度が低減されました。