ツールバードロップダウンを `<details>` から自律型カスタム要素へ刷新

basecamp/lexxy

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.jstoolbar_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">&bull;&bull;&bull;</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">&bull;&bull;&bull;</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 属性でパネルの初期状態が制御されます。

HighlightDropdownLinkDropdown の変更も重要です。これらはカスタム要素のサブクラスではなくなり、HighlightContent / LinkContent というプレーンなコントローラークラスに変わりました。ToolbarDropdown はデータ属性 data-content をキーにしたレジストリでコントローラーを解決し、connect()onOpen()onClose() を直接呼び出します。旧来の lexxy:toolbar-dropdown-toggle カスタムイベントは廃止されました。

toolbar_dropdown.jsToolbarDropdown クラスには、新たに以下のライフサイクルメソッドとゲッターが追加されました:

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.jscreateElement 関数も修正されました。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を管理します。

HighlightDropdownLinkDropdown がカスタム要素のサブクラスからプレーンなコントローラークラスへ変わったことで、Web Components の継承ヒエラルキーとビジネスロジックが切り離されました。data-content レジストリを介した接続により、新しいドロップダウンコンテンツの追加がカスタム要素の登録なしに行えるようになっています。

まとめ

<details>/<summary> を使った実装からJSで制御するカスタム要素への移行は、ブラウザ警告の解消にとどまらず、ドロップダウンのライフサイクル管理を onOpen() / onClose() として明示的なインターフェースに整理しました。コントローラーとカスタム要素の分離という設計選択により、今後のドロップダウン種別の追加・変更がより局所的な変更で対応できる構造になっています。

記事メタデータ

Generated by:
Claude Sonnet 4.6 for DiffDaily
LLM Trace:
f77f5d5e

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

品質レビュー結果

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

Review Criteria:

記事構成 ✓ PASS

Title, Context, Technical Detailの存在と明確さ

「総論→各論→結論」の構成が明確です。リード文、背景、技術的な変更、設計判断、まとめの各要素が過不足なく含まれており、理想的な構成です。

カスタムMarkdown構文 ✓ PASS

シンタックスハイライト・GitHubリンク記法の正確性

ファイル名付きシンタックスハイライトやGitHubのPRリンク記法が、ガイドライン通りに正しく使用されています。

対象読者への適合性 ✓ PASS

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

カスタム要素やアクセシビリティに関する専門的な内容を、冗長な説明なく記述しており、対象読者であるエンジニアに適合しています。

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

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

各セクションが総論・各論・結論の構造を持ち、各段落がトピックセンテンスで始まるなど、パラグラフ・ライティングの原則が徹底されています。非常に読みやすいです。

Diff内容との照合 ✓ PASS

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

記事内のHTML、JavaScript、CSSのコード引用や説明は、提供されたDiffの内容と完全に一致しており、正確に技術的変更を反映しています。

技術用語の正確性 ✓ PASS

技術用語の正確な使用

「カスタム要素」「コンテナクエリ」「ライフサイクルメソッド」などの技術用語が、文脈に応じて正確かつ適切に使用されています。

説明の技術的正確性 ✓ PASS

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

技術的な変更に関する説明は、すべてPR DescriptionやDiffの内容によって裏付けられており、論理的で正確です。

事実の突合 ✓ PASS

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

記事内のすべての主張はPR情報に基づいており、根拠のない推測や憶測(ハルシネーション)は見られません。

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

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

PR番号、ファイルパス、CSSの数値(`@container (width < 300px)`)など、記事内のすべての数値・固有名詞は正確です。

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

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

記事のタイトルは、PRの主題である「ツールバーの刷新」を、具体的な技術(カスタム要素への移行)に触れつつ正確に表現しています。

外部知識の正確性 ✓ PASS

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

記事は提供されたPR情報のみに基づいており、バージョンサポート状況などの外部知識の捏造はありません。

時間表現の正確性 ✓ PASS

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

「刷新しました」といった時間表現は、PRの文脈と一致しており、正確です。