Lambdaカクテル

京都在住Webエンジニアの日記です

Invite link for Scalaわいわいランド

一定の範囲内からフォーカスが外れた場合にのみ発動するblurイベントを作る

blurイベントとev.currentTarget / ev.relatedTarget を活用することで、範囲で作用するblurイベントを作れる。

<div id="area">
  <input type="text" />
  <input type="text" />
</div>
const area = document.querySelector('#area')!;

area.addEventListener('blur', (ev) => {
  if (!ev.currentTarget.contains(ev.relatedTarget)) {
    console.log('範囲からフォーカスが外れた');
  }
}, true);

このようにすると、#areaの外側にフォーカスが移動して初めて発火するようなイベントを書ける。

いつ使う

Reactなどでインタラクティブな編集フォームを作っていて、フォーカスが外れると自動的に取り消されるようなコンポーネントを作る場合に便利だ。具体的には、テキストボックスとボタンがあり、その両者を包むdiv要素からフォーカスが外れたら自動的に取り消したい、といった場合だ。何も考えずにblurを利用すると、テキストボックスからフォーカスが離れた時点で取り消されてしまい、ボタンを押すことができなくなる。

blurイベントとは

blurとは、その要素からフォーカスが外れた場合に発火するイベントだ。

developer.mozilla.org

useCapture

addEventListenerの第三引数としてtrueを指定すると、useCaptureを有効化できる。

イベントfoobarに対するuseCaptureが有効なとき、このイベントはキャプチャーフェイズで捕捉される。

キャプチャーフェイズについても説明しよう。イベントが発生したとき、まずキャプチャーフェイズが始まる。root要素からそのイベントを起こした要素までが辿られ、各要素に仕掛けられたハンドラがイベントを捕捉していき、末端まで到達したら今度はバブリングフェイズに移行する。バブリングフェイズでは方向が逆になり、イベントを起こした要素からroot要素までハンドラが捕捉していく。こうしてイベントが捕捉される。

developer.mozilla.org

通常、イベントはバブリングフェイズに仕掛けられる(子要素が処理してから親要素が処理していくのが直感的だからだ)。useCaptureを使うとこれをキャプチャーフェイズに仕掛けることができるようになるわけだが、これには理由がある。blurイベントはバブリングしないのだ。つまりキャプチャーフェイズが終わったらそこで終わりだ。

focusOutという、バブリングを行う特殊なイベントもある。しかしこれは多くのブラウザで部分的にしか実装されていない。

useCaptureを有効化することで、うまく広い範囲でblurを検出できるようになるのだ。

ev.currentTarget / ev.relatedTarget

ev.currentTargetは、常にイベントハンドラーが装着されている要素を指す。 つまりこの場合は#areaがそうだ。

developer.mozilla.org

対して、ev.relatedTargetに何が入るかはイベントの種類による。blurが発するFocusEventでは、ev.relatedTargetとは新たにフォーカスを得る要素のことである。

developer.mozilla.org

すると、以下の条件式を考えることができる:

// 新たにフォーカスされた要素が、#areaに含まれて**いない**
!ev.currentTarget.contains(ev.relatedTarget)

これこそが求めていたものだ。

従って、前掲の通りのイベントハンドラで「一定範囲からフォーカスが外れた場合にのみ発動」するような処理が書ける:

area.addEventListener('blur', (ev) => {
  if (!ev.currentTarget.contains(ev.relatedTarget)) {
    console.log('範囲からフォーカスが外れた');
  }
}, true);
★記事をRTしてもらえると喜びます
Webアプリケーション開発関連の記事を投稿しています.読者になってみませんか?