shadowDOM 以下の要素で発生したイベントの発生元要素を特定する
document レベルでイベント捕捉、target で発生元を識別
HTML/JavaScript でのプログラムの話です。
最近の Web アプリケーションフレームワークを使っていると、あまり出番は薄いのですが、希に、「下位要素からバブリングしてくるイベントを、document レベルでリッスンする」みたいな実装を行なうことがあります。そしてこのようなパターンでは、「どの要素でそのイベントが発生したのか」を、イベント引数の target
プロパティを見て判断することがままあります。
// このページで発生したすべての click イベントを捕捉し...
document.addEventListener("click", e => {
// click イベントの発生元が href 属性付きの a 要素であれば...
if (e.target.matches('a[href]')) {
// 既定の動作をキャンセルして...
e.preventDefault();
// 代わりに何かする
}
});
shadowDOM 内の要素でイベント発生しても target はカスタム要素
しかしところで、ここで、以下のようなカスタム要素が定義されていたとしましょう。
class AwesomeAnchor extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
this.input = document.createElement('a');
...
this.shadowRoot.appendChild(this.input);
}
...
}
customElements.define('awesome-anchor', AwesomeAnchor);
そしてページ上でこのカスタム要素 <awesome-anchor>
が使われていたとします。
<awesome-anchor href="/foo/bar">
Awesome!
</awesome-anchor>
すると、ブラウザ上で構築される DOM ツリーは以下のような感じになります。
<awesome-anchor href="/foo/bar">
shadowRoot
<a href="/foo/bar">
Awesome!
</a>
</awesome-anchor>
さてここで、このカスタム要素 <awesome-anchor>
の shadowDOM 内にレンダリングされた anchor 要素 (<a>
) をクリックしたとき、先の document
オブジェクトに取り付けた click
イベントハンドラーは期待どおり機能するでしょうか? 残念ながら答えは「NO」です。この場合のイベント引数の target
プロパティには、shadowDOM 配下にレンダリングされた anchor 要素 (<a>
) ではなく、カスタム要素 <awesome-anchor>
が格納されているのです。
document.addEventListener("click", e => {
// 👇 ここで e.target には <awesome-anchor> が格納されている
if (e.target.matches('a[href]')) {
e.preventDefault();
...
}
});
これはちょっと期待した動作とは違いますね。このシナリオでは、例え shadowDOM の下にあるものであっても、anchor 要素 (<a>
) で発生したイベントであれば、すべて拾いたいのです。どうしたらいいでしょうか?
composedPath()
メソッドを使えばよさそう
ブラウザの開発者ツールでデバッグしながら試行錯誤していたところ、イベント引数に、composedPath()
というメソッドがあるのに気がつきました。MDN で調べてみます。
"composedPath() は Event インターフェイスのメソッドで、イベントの経路をリスナーが呼び出されるオブジェクトの配列で返します。"
つまり、イベント引数の composedPath()
メソッドを呼び出すと、shadowDOM 配下の要素であっても、そのイベント発生元の要素から document
に至るまでの経路要素が配列で返ってくるようなのです。
ということで、先のイベントハンドラーを以下のように実装すれば解決できます。
document.addEventListener("click", e => {
// 👇 composedPath() メソッドを使って、イベント発生元を特定する!
const target = e.composedPath()[0] || e.target;
if (target.matches('a[href]')) {
e.preventDefault();
...
}
});
おめでとうございます🎉
注意: shadowRoot が open のときだけ通用します
残念ながら、この方法は、shadowRoot の mode が open のときだけ通用します。もしも mode が closed を指定してshadowDOM が作られていた場合、イベント引数の composedPath()
メソッドが返す要素配列はカスタム要素から始まり、shadowDOM 配下の要素の様子は伺うことができません。この場合はあきらめるしかなさそうです。
Discussion