ツールバードロップダウンを `<details>` から自律型カスタム要素へ刷新
Chromeが報告した「<summary> 要素内のインタラクティブ要素」警告を解消するため、ツールバードロップダウンの実装を <details>/<summary> ベースからJSで制御するカスタム要素へ全面的に刷新しました。この変更はドロップダウンのロジックを単一のカスタム要素に集約する構造的整理でもあり、ユーザー向けの動作変更はありません。
背景
ブラウザの警告とアクセシビリティ実装の衝突が、今回の刷新の直接的なきっかけです。アクセシビリティヘルパーがツールバーのインタラクティブ要素に tabindex を付与する際、<summary> 要素も対象となっていました。これがChromeの「interactive element inside of a <summary> element」警告を引き起こしていました。
加えて、リンクURLの <input> に id/name 属性がなく、Chromeが別途警告を出していました。また、エディタからフォーカスが外れると即座にドロップダウンが閉じる動作が「too aggressive(過剰)」と判断され、エディタのblurではドロップダウンを閉じないよう変更されています。
旧実装では、ドロップダウン1つが「ラッパー <div class="lexxy-editor__toolbar-dropdown">」「トリガーとなる兄弟ボタン」「パネルを表す <lexxy-toolbar-dropdown>」の3ピースに分割されており、開閉の制御が toolbar.js と toolbar_dropdown.js に分散していました。
技術的な変更
<lexxy-toolbar-dropdown> カスタム要素がドロップダウン全体(トリガーとパネルの両方)を包含する単一の責任単位となりました。トリガーボタンは data-dropdown-trigger 属性、パネルは data-dropdown-panel 属性で識別され、開閉・フォーカス・Escapeキー・tabindex の管理はすべて要素内部で完結します。
変更前(<details> ベース):
<details class="lexxy-editor__toolbar-overflow">
<summary class="lexxy-editor__toolbar-button" aria-label="Show more toolbar buttons">•••</summary>
<div class="lexxy-editor__toolbar-overflow-menu" aria-label="More toolbar buttons"></div>
</details>
変更後(カスタム要素ベース):
<lexxy-toolbar-dropdown class="lexxy-editor__toolbar-overflow">
<button class="lexxy-editor__toolbar-button" type="button" data-dropdown-trigger
aria-haspopup="menu" aria-expanded="false" aria-label="Show more toolbar buttons">•••</button>
<div data-dropdown-panel role="menu" class="lexxy-editor__toolbar-overflow-menu" aria-label="More toolbar buttons" hidden></div>
</lexxy-toolbar-dropdown>
aria-haspopup="menu" と aria-expanded="false" が明示的に付与され、hidden 属性でパネルの初期状態が制御されます。
HighlightDropdown と LinkDropdown の変更も重要です。これらはカスタム要素のサブクラスではなくなり、HighlightContent / LinkContent というプレーンなコントローラークラスに変わりました。ToolbarDropdown はデータ属性 data-content をキーにしたレジストリでコントローラーを解決し、connect()・onOpen()・onClose() を直接呼び出します。旧来の lexxy:toolbar-dropdown-toggle カスタムイベントは廃止されました。
toolbar_dropdown.js の ToolbarDropdown クラスには、新たに以下のライフサイクルメソッドとゲッターが追加されました:
editorReady() {}
onOpen() {}
onClose() {}
get trigger() {
return this.querySelector(":scope > [data-dropdown-trigger]")
}
get panel() {
return this.querySelector(":scope > [data-dropdown-panel]")
}
get isOpen() {
return this.panel.hidden === false
}
LinkDropdown では、connectedCallback 内の <details> toggle イベント購読が onOpen() / onClose() に分離されました。
変更前:
#handleToggle = ({ newState }) => {
this.input.value = this.#selectedLinkUrl
this.input.required = newState === "open"
}
変更後:
onOpen() {
this.input.value = this.#selectedLinkUrl
this.input.required = true
}
onClose() {
this.input.required = false
}
toolbar.js の変更として、エディタblur時の #closeDropdowns() 呼び出しが削除されました。また、#focusableItems(<summary> を含むすべてのフォーカス可能要素)の代わりに #buttons(ボタンのみ)を参照するよう変更され、アクセシビリティヘルパーが <summary> 要素に tabindex を付与する問題が根本から解消されています。
CSSセレクターの移行も合わせて行われました。lexxy-link-dropdown / lexxy-highlight-dropdown の要素名に依存していたセレクターは、.lexxy-editor__toolbar-link-panel / .lexxy-editor__toolbar-highlight-panel のような明示的なパネルクラスへ移行しています。テストコードでも lexxy-highlight-dropdown .lexxy-highlight-colors のようなセレクターが lexxy-highlight-dropdown [data-dropdown-panel] .lexxy-highlight-colors に更新されています。
html_helper.js の createElement 関数も修正されました。dataset プロパティを直接代入していた箇所が、element.dataset[key] = value による個別代入に変わり、データ属性の設定が正しく機能するようになっています。
CSSには container-type: inline-size が .lexxy-editor__toolbar に追加され、@container (width < 300px) クエリによるコンパクト表示(font-size: 0.8em)が実装されました。ツールバーが狭い場合の表示調整がコンテナクエリで制御されます。
設計判断
ドロップダウンのロジックを単一のカスタム要素に集約する方針が採用されました。旧設計では開閉状態の管理が <details> のブラウザネイティブ動作に依存していたため、tabindex 管理との干渉が避けられませんでした。カスタム要素が hidden 属性と aria-expanded を自身で制御することで、ブラウザのネイティブ動作への依存を排除しています。
エディタblurでドロップダウンを閉じない判断も注目です。PR本文では「the behavior was too aggressive(動作が過剰すぎた)」と説明されており、選択操作やフォーカス移動のたびにドロップダウンが閉じるUXの問題を解消しています。ツールバーが「close all dropdowns」を担うのは選択変更時のみに絞られ、個々のドロップダウンは自律的にopen/closeを管理します。
HighlightDropdown と LinkDropdown がカスタム要素のサブクラスからプレーンなコントローラークラスへ変わったことで、Web Components の継承ヒエラルキーとビジネスロジックが切り離されました。data-content レジストリを介した接続により、新しいドロップダウンコンテンツの追加がカスタム要素の登録なしに行えるようになっています。
まとめ
<details>/<summary> を使った実装からJSで制御するカスタム要素への移行は、ブラウザ警告の解消にとどまらず、ドロップダウンのライフサイクル管理を onOpen() / onClose() として明示的なインターフェースに整理しました。コントローラーとカスタム要素の分離という設計選択により、今後のドロップダウン種別の追加・変更がより局所的な変更で対応できる構造になっています。