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とは、その要素からフォーカスが外れた場合に発火するイベントだ。
useCapture
addEventListenerの第三引数としてtrueを指定すると、useCaptureを有効化できる。
イベントfoobarに対するuseCaptureが有効なとき、このイベントはキャプチャーフェイズで捕捉される。
キャプチャーフェイズについても説明しよう。イベントが発生したとき、まずキャプチャーフェイズが始まる。root要素からそのイベントを起こした要素までが辿られ、各要素に仕掛けられたハンドラがイベントを捕捉していき、末端まで到達したら今度はバブリングフェイズに移行する。バブリングフェイズでは方向が逆になり、イベントを起こした要素からroot要素までハンドラが捕捉していく。こうしてイベントが捕捉される。
通常、イベントはバブリングフェイズに仕掛けられる(子要素が処理してから親要素が処理していくのが直感的だからだ)。useCaptureを使うとこれをキャプチャーフェイズに仕掛けることができるようになるわけだが、これには理由がある。blurイベントはバブリングしないのだ。つまりキャプチャーフェイズが終わったらそこで終わりだ。
focusOutという、バブリングを行う特殊なイベントもある。しかしこれは多くのブラウザで部分的にしか実装されていない。
useCaptureを有効化することで、うまく広い範囲でblurを検出できるようになるのだ。
ev.currentTarget / ev.relatedTarget
ev.currentTargetは、常にイベントハンドラーが装着されている要素を指す。 つまりこの場合は#areaがそうだ。
対して、ev.relatedTargetに何が入るかはイベントの種類による。blurが発するFocusEventでは、ev.relatedTargetとは新たにフォーカスを得る要素のことである。
すると、以下の条件式を考えることができる:
// 新たにフォーカスされた要素が、#areaに含まれて**いない** !ev.currentTarget.contains(ev.relatedTarget)
これこそが求めていたものだ。
従って、前掲の通りのイベントハンドラで「一定範囲からフォーカスが外れた場合にのみ発動」するような処理が書ける:
area.addEventListener('blur', (ev) => { if (!ev.currentTarget.contains(ev.relatedTarget)) { console.log('範囲からフォーカスが外れた'); } }, true);