ブラウザイベント入門:キーボード・マウス・フォーカスの発火順序

スポンサーリンク

ブラウザイベント入門:キーボード・マウス・フォーカスの発火順序

イベントの伝播: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', () => {
  // ここは実行されない
})

stopPropagationpreventDefault は別物です。セットで使うことが多いですが、それぞれ独立した効果があります。


キーボードイベント

発火順序

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() でデフォルト動作を止める
  • 親要素でまとめて監視したいときはバブリングするイベント(focusoutmouseover)を使う