disabled状態のドロップダウンアイテムでclickイベントが発火する問題を修正
wa-dropdown-itemコンポーネントで、disabled属性が設定されている場合でもclickイベントが発火してしまう問題が修正されました。CSSとJavaScriptの両面からイベント伝播を防ぐことで、動的な状態変更にも対応した実装になっています。
背景
#1817で報告されたように、wa-dropdown-itemがdisabled状態でもclickイベントを発火していました。これはUIコンポーネントとして期待される動作ではなく、無効化されたアイテムをクリックした際にアプリケーションロジックが実行されてしまう可能性がありました。
一般的なフォーム要素や他のUIコンポーネントでは、disabled状態ではユーザー操作を受け付けないことが標準的な動作です。この不整合を解消する必要がありました。
技術的な変更
修正はCSSとJavaScriptの二段構えで実装されています。これにより、静的なdisabled属性の指定と動的な状態変更の両方に対応しています。
CSS層での対応
dropdown-item.styles.tsにpointer-events: noneを追加し、ブラウザレベルでポインタイベントを無効化しました。
変更後:
:host(:state(disabled)),
:host([disabled]) {
opacity: 0.5;
cursor: not-allowed;
pointer-events: none;
}
:host(:state(disabled))と:host([disabled])の両方にスタイルを適用することで、カスタムステートと属性の両方の指定方法に対応しています。
JavaScript層での対応
dropdown-item.tsでは、2つの変更が加えられました。
1. インラインスタイルの動的設定
updated()ライフサイクルメソッドで、disabledプロパティの変更時にインラインスタイルを設定します。
updated(changedProperties: PropertyValues) {
if (changedProperties.has('disabled')) {
this.setAttribute('aria-disabled', this.disabled ? 'true' : 'false');
this.customStates.set('disabled', this.disabled);
this.style.pointerEvents = this.disabled ? 'none' : '';
}
}
2. イベントハンドラによる伝播防止
handleClickメソッドをキャプチャフェーズで登録し、disabled状態ではイベントを停止します。
private handleClick = (event: MouseEvent) => {
if (this.disabled) {
event.preventDefault();
event.stopImmediatePropagation();
}
};
connectedCallback() {
super.connectedCallback();
this.addEventListener('mouseenter', this.handleMouseEnter.bind(this));
this.shadowRoot!.addEventListener('click', this.handleClick, { capture: true });
this.shadowRoot!.addEventListener('slotchange', this.handleSlotChange);
}
{ capture: true }オプションにより、イベントがバブリングフェーズに到達する前に捕捉できます。これにより、disabled状態でのイベント発火を確実に防止しています。
リグレッションテストの追加
dropdown-item.test.tsに2つのテストケースが追加され、修正の正しさを保証しています。
it('should not fire click event when disabled', 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);
await clickOnElement(el);
expect(clickHandler).not.to.have.been.called;
});
it('should fire click event when not disabled', async () => {
const el = await fixture<WaDropdownItem>(html` <wa-dropdown-item>Item</wa-dropdown-item> `);
const clickHandler = sinon.spy();
el.addEventListener('click', clickHandler);
await clickOnElement(el);
expect(clickHandler).to.have.been.calledOnce;
});
設計判断
PRのDescriptionでは、CSSとJavaScriptの両方を使用する理由が明示されています。
CSSは静的なdisabled状態に対して基本的なスタイリングを提供します。一方、JavaScriptのインラインスタイルは、disabledプロパティが動的に変更された際の処理を担当します。
この役割分担により、コンポーネントの初期状態と実行時の状態変更の両方に対応した実装になっています。
本PRは、UIコンポーネントの基本的な期待動作を満たすための実装です。CSSとJavaScriptの役割を明確に分離し、静的・動的な状態変更の両方に対応することで、disabled状態でのクリックイベント発火を防止しています。リグレッションテストによって動作が保証されています。