サブメニュー内の無効化アイテムで `cursor: not-allowed` が機能しなかった問題を修正
wa-dropdown-item を無効化した際に適用されるはずの cursor: not-allowed が、サブメニュー内に配置した場合のみ pointer カーソルに上書きされてしまうバグが修正されました。原因は pointer-events: none の適用により、カーソルスタイルの決定が親要素に委ねられていたことです。
背景
<wa-dropdown-item disabled> を通常のドロップダウン内に配置した場合と、サブメニュー内に配置した場合とで、ホバー時のカーソルに不一致が生じていました。通常のドロップダウンでは cursor: default が表示され、サブメニュー内では cursor: pointer が表示されていました。いずれの場合も cursor: not-allowed が表示されることが期待された動作です。
#2276 で報告されたこの問題では、ブラウザの開発者ツールの計算済みスタイルに cursor: not-allowed と表示されているにもかかわらず、実際に表示されるカーソルは pointer になるという奇妙な症状が確認されていました。!important を使用しても解決しなかったとの報告があり、CSSによる上書きが不可能な状態でした。
この現象が起きていたのは、pointer-events: none がブラウザのカーソル解決メカニズムに影響を与えるためです。pointer-events: none が設定された要素は、マウスイベントを「素通り」させます。このとき表示されるカーソルは、自身のスタイルではなく下に位置する要素のスタイルに基づいて決定されます。通常のドロップダウンでは背後の要素が cursor: default を持つため問題になりませんでしたが、サブメニューのポップアップ親要素は cursor: pointer を持っていたため、不一致が生じていました。
技術的な変更
pointer-events: none を適用していた2箇所を削除することで修正されました。変更量は最小限であり、いずれも削除のみです。
1つ目は dropdown-item.styles.ts の CSS ルールです。:host([disabled]) セレクタ内の pointer-events: none; 宣言が削除されました。
変更前:
:host([disabled]) {
opacity: 0.5;
cursor: not-allowed;
pointer-events: none;
}
変更後:
:host([disabled]) {
opacity: 0.5;
cursor: not-allowed;
}
2つ目は dropdown-item.ts の updated() ライフサイクルコールバックです。disabled プロパティが変更された際にインラインスタイルとして pointer-events を書き込んでいた行が削除されました。
変更前:
if (changedProperties.has('disabled')) {
this.setAttribute('aria-disabled', this.disabled ? 'true' : 'false');
this.customStates.set('disabled', this.disabled);
this.style.pointerEvents = this.disabled ? 'none' : '';
}
変更後:
if (changedProperties.has('disabled')) {
this.setAttribute('aria-disabled', this.disabled ? 'true' : 'false');
this.customStates.set('disabled', this.disabled);
}
クリックイベントのブロックについては、handleHostClick および handleClick のイベントリスナーが既に無効化された項目へのクリック操作を処理しているため、pointer-events: none を除去しても機能上の損失はありません。
設計判断
pointer-events: none を除去してクリック制御をイベントリスナーに一本化するアプローチが採用されました。
pointer-events: none は視覚的フィードバック(カーソル表示)とイベント抑制の両方を同時に担うプロパティです。しかし今回のケースのように、要素のスタック順によってカーソル表示が親要素に依存してしまうと、CSSだけでは上書きできない状態が生まれます。クリック制御を専用のイベントハンドラに委ねることで、pointer-events をカーソルスタイル制御の用途のみに限定し、この干渉を排除しています。
インラインスタイルでの pointer-events 上書きはCSS宣言よりも詳細度が高いため、CSSで cursor: not-allowed を定義していても無効化アイテムへの cursor 制御が CSSだけでは不可能になっていました。updated() からのインラインスタイル削除は、この問題を根本から解消します。
まとめ
pointer-events: none の「カーソルを素通りさせる」副作用が、サブメニュー特有の要素スタック構造と組み合わさってバグを引き起こしていた事例です。クリック制御とカーソル制御の責務を分離し、それぞれを適切な機構(イベントリスナーとCSS)に委ねることで、文脈に依存しない一貫したUI動作が実現されています。