ブラウザイベント入門:キーボード・マウス・フォーカスの発火順序
イベントの伝播:3つのフェーズ
ブラウザのイベントは次の3フェーズで伝播します。
① キャプチャリング:document → 親 → 対象要素(上から下) ② ターゲット:対象要素でイベント処理 ③ バブリング:対象要素 → 親 → document(下から上)
<div id="parent"> <button id="child">クリック</button> </div>
const parent = document.getElementById('parent')! const child = document.getElementById('child')! // デフォルトはバブリングフェーズで受け取る parent.addEventListener('click', () => console.log('parent(バブリング)')) child.addEventListener('click', () => console.log('child')) // capture: true でキャプチャリングフェーズで受け取る parent.addEventListener('click', () => console.log('parent(キャプチャ)'), { capture: true })
ボタンをクリックしたときの出力順:
parent(キャプチャ) child parent(バブリング)
伝播の抑止
| メソッド | 効果 |
|---|---|
event.stopPropagation() |
バブリング・キャプチャリングの伝播を止める |
event.stopImmediatePropagation() |
同じ要素の残りのハンドラも止める |
event.preventDefault() |
ブラウザのデフォルト動作を止める |
// バブリングを止める(親には伝わらない) child.addEventListener('click', (event: MouseEvent) => { event.stopPropagation() console.log('child のみ処理') }) // デフォルト動作を止める(フォーム送信・リンク遷移など) form.addEventListener('submit', (event: SubmitEvent) => { event.preventDefault() // ページリロードを防ぐ // 独自の送信処理 }) // 同じ要素の他ハンドラも止める button.addEventListener('click', (event: MouseEvent) => { event.stopImmediatePropagation() }) button.addEventListener('click', () => { // ここは実行されない })
stopPropagation と preventDefault は別物です。セットで使うことが多いですが、それぞれ独立した効果があります。
キーボードイベント
発火順序
keydown → keypress(非推奨)→ input → keyup
const input = document.getElementById('input') as HTMLInputElement input.addEventListener('keydown', (event: KeyboardEvent) => { console.log('keydown', event.key) // まだ入力値は変わっていない }) input.addEventListener('input', () => { console.log('input', input.value) // 値が変わった後 }) input.addEventListener('keyup', (event: KeyboardEvent) => { console.log('keyup', event.key) // キーを離した })
input イベントは keydown の後・keyup の前に発火し、このタイミングで input.value が更新されています。
event.key と event.code の違い
| プロパティ | 意味 | 例(Aキー) |
|---|---|---|
event.key |
入力される文字(修飾キーの影響を受ける) | 'a' / 'A'(Shift時) |
event.code |
物理的なキーの位置(レイアウト非依存) | 'KeyA' |
input.addEventListener('keydown', (event: KeyboardEvent) => { if (event.key === 'Enter') { /* Enterキー */ } if (event.code === 'Space') { /* スペースキー */ } if (event.ctrlKey && event.key === 's') { /* Ctrl+S */ } })
keypress は非推奨のため、keydown + input で代替します。
マウスイベント
発火順序(クリック時)
mousedown → mouseup → click → dblclick(ダブルクリック時)
フォーカス可能な要素(input など)をクリックした場合:
mousedown → focus / focusin → mouseup → click
button.addEventListener('mousedown', () => console.log('mousedown')) button.addEventListener('mouseup', () => console.log('mouseup')) button.addEventListener('click', () => console.log('click'))
マウスの移動イベント
(要素に入る)mouseenter → mouseover → mousemove → mouseout / mouseleave(要素から出る)
| イベント | バブリング | 子要素 |
|---|---|---|
mouseenter |
しない | 子要素では発火しない |
mouseover |
する | 子要素に入っても発火する |
mouseleave |
しない | 子要素では発火しない |
mouseout |
する | 子要素から出ても発火する |
const box = document.getElementById('box')! // mouseenter:ボックスに入ったときだけ発火(子要素を行き来しても発火しない) box.addEventListener('mouseenter', () => { console.log('ボックスに入った') }) // mouseover:子要素に入るたびに発火する box.addEventListener('mouseover', (event: MouseEvent) => { const target = event.target as HTMLElement console.log('要素に入った:', target.tagName) })
子要素が多いリストで「ボックス全体に入ったとき1回だけ処理する」なら mouseenter、「どの子要素にいるか追跡する」なら mouseover を使います。
フォーカス / ブラーイベント
発火順序(フォーカス取得時)
focusin → focus
発火順序(フォーカス喪失時)
blur → focusout
フォーカスが別の要素に移るときの全順序
(旧要素)blur → focusout → (新要素)focusin → focus
| イベント | バブリング | タイミング |
|---|---|---|
focus |
しない | フォーカスを得た後 |
focusin |
する | フォーカスを得た後 |
blur |
しない | フォーカスを失った後 |
focusout |
する | フォーカスを失った後 |
const input = document.getElementById('input') as HTMLInputElement const form = document.getElementById('form')! // input 単体を監視 input.addEventListener('focus', () => console.log('フォーカス取得')) input.addEventListener('blur', () => console.log('フォーカス喪失')) // フォーム全体の子要素のフォーカス変化をまとめて監視(バブリングを利用) form.addEventListener('focusin', (event: FocusEvent) => { const target = event.target as HTMLElement console.log('フォーカス:', target.id) }) form.addEventListener('focusout', (event: FocusEvent) => { const target = event.target as HTMLElement console.log('ブラー:', target.id) })
フォーム全体のバリデーションは focusout を親要素に付けると、どの入力から外れても1つのハンドラで処理できます。
よく使うイベントと型まとめ
| カテゴリ | イベント | TypeScriptの型 |
|---|---|---|
| キーボード | keydown / keyup / input |
KeyboardEvent / Event |
| マウス | click / mousedown / mouseenter / mouseover |
MouseEvent |
| フォーカス | focus / focusin / blur / focusout |
FocusEvent |
| フォーム | submit / change / input |
SubmitEvent / Event |
// 型を明示した書き方 element.addEventListener('click', (event: MouseEvent) => { ... }) element.addEventListener('keydown', (event: KeyboardEvent) => { ... }) element.addEventListener('focus', (event: FocusEvent) => { ... })
まとめ
クリック時の順序
mousedown → focus(フォーカス可能な場合)→ mouseup → click
キー入力時の順序
keydown → input(値が変わる)→ keyup
フォーカス切り替えの順序
(旧)blur → focusout → (新)focusin → focus
バブリングする / しないの対比
| バブリングする | バブリングしない |
|---|---|
click / mouseover / mouseout |
mouseenter / mouseleave |
focusin / focusout |
focus / blur |
keydown / keyup / input |
— |
stopPropagation()でバブリングを止め、preventDefault()でデフォルト動作を止める- 親要素でまとめて監視したいときはバブリングするイベント(
focusout・mouseover)を使う