`<wa-select>` の `change`/`input` イベントタイミングバグを修正
Web Awesome 3.4 で導入されたバグにより、<wa-select>・<wa-combobox>・<wa-option> の change および input イベントが誤ったタイミングで発火していた問題を修正しました。イベントハンドラ内で event.target.value を参照した際に、選択後の正しい値が取得できなかった不具合が解消されます。
背景
change / input イベントのタイミングがずれると、イベントハンドラ内で event.target.value を読み取っても選択前の古い値を返すという実用上の問題が生じます。この問題はDiscordコミュニティで報告され、<wa-select> に加えて関連するProコンポーネントリポジトリにも影響することが確認されていました。
原因は2つのコンポーネントにまたがる実装にありました。<wa-option> の selected プロパティが変更された際に handleDefaultSlotChange() を呼び出していたこと、および <wa-select> の optionValues キャッシュ構築ロジックが value が null の場合に空の Set を返していたことが、イベント発火タイミングのずれを引き起こしていました。
技術的な変更
修正は option.ts と select.ts の2ファイルにわたる最小限の変更で行われました。
option.ts の変更: selected プロパティの変更時に呼び出されていた handleDefaultSlotChange() の呼び出しを削除しました。
変更前:
if (changedProperties.has('selected')) {
this.setAttribute('aria-selected', this.selected ? 'true' : 'false');
this.customStates.set('selected', this.selected);
this.handleDefaultSlotChange();
}
変更後:
if (changedProperties.has('selected')) {
this.setAttribute('aria-selected', this.selected ? 'true' : 'false');
this.customStates.set('selected', this.selected);
}
selected の変更のたびにスロット変更処理が再実行されることで、<wa-select> 側の値更新と change イベント発火の順序が乱れていたと考えられます。
select.ts の変更: optionValues キャッシュの構築において、value が null の場合に空の Set を生成していた分岐を削除し、常にDOM上の <wa-option> 要素から値を収集するよう統一しました。
変更前:
if (this.optionValues === undefined) {
if (value == null) {
this.optionValues = new Set(null);
} else {
this.optionValues = new Set(
this.getAllOptions()
.filter(option => !option.disabled)
.map(option => option.value),
);
}
}
変更後:
if (this.optionValues === undefined) {
this.optionValues = new Set(
this.getAllOptions()
.filter(option => !option.disabled)
.map(option => option.value),
);
}
初期値なしの状態(value == null)でも optionValues が正しく構築されることで、選択操作後の値の更新とイベント発火が同期するようになります。
テストの追加
リグレッション防止のため、select.test.ts に2つのテストケースが追加されました。どちらも change / input イベントハンドラ内での event.target.value の値を検証することで、タイミングのずれを直接検出できるようになっています。
1つ目は初期値ありの状態(value="option-1")から別のオプションを選択した際に event.target.value が新しい値を返すことを確認します。2つ目は初期値なしの状態から選択した際のケースを対象としており、今回の select.ts の修正が必要になったシナリオに対応しています。
el.addEventListener('change', (event: Event) => {
valueAtChangeTime = (event.target as WaSelect).value;
});
// ...
expect(valueAtChangeTime).to.equal('option-2');
設計判断
selected 変更時の handleDefaultSlotChange() 呼び出しの削除という判断は、副作用の排除を優先したものです。selected はオプションの状態を表すプロパティであり、その変更がスロットの再評価を引き起こすのは責務の観点から適切ではありません。この呼び出しを除去することで、<wa-option> の更新サイクルが <wa-select> の値確定・イベント発火と競合する経路を断ち切っています。
select.ts の null チェック削除については、value == null の場合に空の Set を返すことで「利用可能なオプションが存在しない」という誤ったキャッシュが生成され、後続の値検証ロジックに影響を与えていたと推測できます。DOM から常にオプションを収集する単一パスに統一することで、初期値の有無に関わらず一貫した動作を保証しています。
まとめ
今回の修正は、<wa-option> の selected 変更時の不要なスロット再評価と <wa-select> のキャッシュ構築における null 値の特殊扱いという2つの根本原因を除去することで、イベントタイミングの一貫性を回復しています。変更行数は最小限でありながら、テストケースの追加によって同種のリグレッションを将来的に検出できる体制も整備されました。