概要
次のようなUIを実装しました。
- inputにfocus → ドロップダウン表示
- optionを選択 → inputに値を反映
- inputがblur → ドロップダウン非表示
このUIにおいて、
- マウス操作では正常に動作する
- タッチパッド操作ではoptionのclickが反映されない
という差が発生しました。
本記事では、イベントの発火順とタイミングに絞って、原因と解決策を説明します。
実装していたUI(簡略)
input.addEventListener('focus', () => {
dropdown.style.display = 'block';
});
input.addEventListener('blur', () => {
setTimeout(() => {
dropdown.style.display = 'none';
}, 200);
});
option.addEventListener('click', () => {
input.value = hoge;
dropdown.style.display = 'none';
});
なぜclickが失敗するのか
clickは「遅いイベント」
clickは、次の操作がすべて完了した後に発火します。
pointerdown / mousedown
↓
pointerup / mouseup
↓
click
一方で、blurはフォーカスが外れた瞬間に即座に発火します。
イベント順の違い
マウス操作(多くの環境)
mousedown
↓
mouseup
↓
click
↓
blur
→ clickが先に処理されるため問題にならない。
タッチパッド操作(発生しうるケース)
pointerdown
↓
blur
↓
pointerup
↓
click
👉 clickより前にblurが発火するケースがある
タッチパッドでclickが効かないパターン
- optionを押す
-
pointerdown時点でinputのフォーカスが外れる -
blurによりドロップダウンが閉じられる -
clickが発火する頃にはDOMが消えている
結果として、clickが「効かない」ように見える。
※ これはバグではなく、clickとblurの発火順が仕様で保証されていないことによる挙動です。
解決策:pointerdownを使う
option.addEventListener('pointerdown', (event) => {
input.value = hoge;
dropdown.style.display = 'none';
});
なぜpointerdownで解決するのか
-
clickやblurより前に発火する - フォーカスが切れる前に処理できる
そのため、タッチパッド・マウス両方で安定した挙動になります。