`wa-dropdown-item` の無効状態でもクリックイベントが発火するバグを修正

shoelace-style/webawesome

wa-dropdown-itemdisabled 属性を持つ場合でも、プログラム的な .click() 呼び出しによってクリックイベントが発火してしまうバグが修正されました。

背景

wa-dropdown-item のクリックイベント抑制は、これまで Shadow DOM 内部のハンドラ(handleClick)のみで実装されていました。しかし、Shadow DOM の内側で発火したイベントを止めても、ホスト要素に対してプログラム的に .click() を呼び出すと、ホスト要素のイベントリスナーへ直接イベントが届くため抑制できないという盲点がありました。#1817 で報告されたこの問題では、disabledwa-dropdown-item をクリックしても wa-select イベントが誤って発火することが確認されています。

技術的な変更

ホスト要素に新たなイベントハンドラ handleHostClick を追加し、disabled 状態でのクリックイベントを遮断するようになりました。

追加されたハンドラは以下の通りです:

/** Prevents click events from firing on the host when the item is disabled (e.g. programmatic .click() calls). */
private handleHostClick = (event: MouseEvent) => {
  if (this.disabled) {
    event.preventDefault();
    event.stopImmediatePropagation();
  }
};

このハンドラは connectedCallback でホスト要素自身に登録され、disconnectedCallback で解除されます:

connectedCallback() {
  super.connectedCallback();
  this.addEventListener('click', this.handleHostClick);
  // ...既存のリスナー
}

disconnectedCallback() {
  super.disconnectedCallback();
  this.removeEventListener('click', this.handleHostClick);
  // ...既存のリスナー
}

event.stopImmediatePropagation() を用いることで、同一要素に登録された後続のリスナーへの伝播も含めて完全に遮断します。これにより、ユーザー操作によるクリックだけでなく、el.click() のようなプログラム的呼び出しも確実に無効化されます。

テストも追加されており、disabled な要素に対して .click() を呼び出してもイベントハンドラが呼ばれないことを検証しています:

it('should not fire click event when disabled and .click() is called programmatically', async () => {
  const el = await fixture<WaDropdownItem>(html` <wa-dropdown-item disabled>Item</wa-dropdown-item> `);
  await el.updateComplete;

  const clickHandler = sinon.spy();
  el.addEventListener('click', clickHandler);
  el.click();
  expect(clickHandler).not.to.have.been.called;
});

設計判断

Shadow DOM 内部のハンドラとホスト要素のハンドラを並列に持つ構成が採用されました。

既存の handleClick(Shadow DOM 内部)はそのままに、新たな handleHostClick(ホスト要素)を追加する設計です。Shadow DOM を経由しないプログラム的な .click() 呼び出しは、Shadow DOM 内部のイベントキャプチャでは阻止できないため、ホスト要素側での防御が必要になります。stopImmediatePropagation を選択しているのは、stopPropagation では同一要素の後続リスナーに届いてしまうためです。また、ライフサイクルメソッドで対称的に登録・解除することで、メモリリークを防ぐ丁寧な実装になっています。

まとめ

Webコンポーネントでは Shadow DOM の内外でイベント伝播の経路が異なり、内部だけでのガードでは不十分なケースがあります。本修正はホスト要素での防御レイヤーを追加することで、プログラム的なクリック操作を含むすべての経路を確実にカバーし、disabled の語義通りの動作を保証します。

記事メタデータ

Generated by:
Claude Sonnet 4.6 for DiffDaily
LLM Trace:
6349cd32

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

ファイル名付きシンタックスハイライト(```typescript:path```)やGitHubのPR・Issueリンク記法が正しく使用されています。

対象読者への適合性 ✓ PASS

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

Shadow DOM、ホスト要素、イベント伝播などの専門用語を前提としており、専門知識を持つエンジニアという対象読者に適切です。

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

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

各セクション、各パラグラフが「総論→各論」の構造で書かれ、トピックセンテンスが先頭に置かれているため、非常に読みやすいです。

Diff内容との照合 ✓ PASS

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

記事内で引用されているコードは、提供されたDiff情報と完全に一致しており、ファイルパスも正確です。

技術用語の正確性 ✓ PASS

技術用語の正確な使用

`Shadow DOM`, `ホスト要素`, `stopImmediatePropagation` などの技術用語が文脈に応じて正確に使用されています。

説明の技術的正確性 ✓ PASS

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

プログラム的な`.click()`呼び出しがShadow DOM内部のイベントリスナーをバイパスする点や、`stopImmediatePropagation`の役割など、技術的な説明が正確かつ論理的です。

事実の突合 ✓ PASS

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

記事内の主張はすべてPRのDescriptionやDiff内のコード変更によって裏付けられており、ハルシネーションは見られません。

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

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

PR番号(#2232)やIssue番号(#1817)が正確に記載されています。

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

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

記事タイトルはPRのタイトル「Fix dropdown disabled item click」の内容を的確に日本語で表現しており、主題が一致しています。

外部知識の正確性 ✓ PASS

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

PR情報に記載のないバージョン情報やリリース予定などの外部知識は含まれておらず、PRの内容に忠実です。

時間表現の正確性 ✓ PASS

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

「修正されました」といった過去形の表現が使われており、完了した変更を報告する内容として時間表現は正確です。