IntersectionObserverで要素の位置を知る

IntersectionObserverで要素の位置を知る

ブラウザへの表示に際し、要素が表示されたかどうかを知る非同期のインターフェイス IntersectionObservver に関してです。

要素がどの位置にあるか、つまりビューポートの下にあるのか上にあるのかを知る方法です。

01IntersectionObserver に問題発生

まず、元記事はこちらです。

このサイトのリニューアルのために IntersectionObserver を使った UI をつくったという記事で、デモサイトがあります。

  • デモサイト (現在停止中)

で、その後いろいろ試すうちに IntersectionObserver のバグ的な症状に出会ったということです。

交差を監視している対象要素内のある要素に本来の高さよりも低い高さを指定しますと思うような結果が得られなくなります。具体的にはデモサイトの「Pick Up セクション」の中に20記事のリストがあり、そのままでは長くなり見栄えが悪いですので高さを指定してスクロールするようにしたところ、次のセクションに移動しますとリンク先が次にならずに上に戻ってしまいます。

02プロバティの entry.isIntersecting

原因は、コールバック関数で監視対象要素が交差領域に入ったかどうかをみているプロバティの entry.isIntersecting にありました。いや、問題があるわけではなく、この値だけでは監視対象要素が交差領域の上下どちらに出たのかわからないという意味です。

このプロパティは、監視対象要素が交差領域に入ると true になり、出ると false になるのですが、通常は監視対象要素がくっついているときでも、まずひとつ目の要素が交差領域から出たときにコールバック関数が呼ばれ、次に2つ目の要素が交差領域に入ってコールバック関数が呼ばれますが、つまり entry.isIntersecting の値は false そして true となるわけですが、上の問題発生のときには true が先に来て、次に false になります。ふたつ目の要素が入ってからひとつ目の要素が出るということになります。

原因は定かではありませんが、監視対象要素内で実際の高さよりも低い高さを指定したために IntersectionObserver が監視対象要素の高さを正しく認識しなくなったからではないかと思います。ただし、今回のケースの場合、交差領域を 1% にしており正確なピクセル値にしていませんのでそこに原因がある可能性もあります。

03監視対象要素の位置を知ることで解決

元記事のコールバック関数です。

// コールバック関数
const callback = (entries, observer) => {
	entries.forEach((entry) => {
		let next = Number(entry.target.id.replace(/[^0-9]/g, ''));
		if(entry.isIntersecting){
			next += 1;
			if(null === document.getElementById('section' + next)){
				document.querySelector('.section-link-wrap').style.display = 'none';
			}else{
				document.querySelector('.section-link-wrap').style.display = 'block';
				document.getElementById('section-link').setAttribute('href', '#section' + next);
			}
		}else{
			if(0 === next){
				document.getElementById('section-link').setAttribute('href', '#section' + next);
			}
		}
	});
};

今回のケースでは監視対象要素が交差領域から出た時を知る必要があるために entry.isIntersecing をチェックしていますが、それに加えて、監視対象要素が交差領域の下にあるのか上にあるのかをチェックする必要があります。

プロパティの entry.boundingClientRect は監視対象要素の寸法とビューポートに対する相対位置を持っています。

Element: getBoundingClientRect() メソッド

ですので、entry.boundingClientRect.y が「+」のときは要素は下にあり、「−」のときは要素は上にあるということがわかります。

ということでコールバック関数を書き換えました。

// コールバック関数
const callback = (entries, observer) => {
	entries.forEach((entry) => {
		let next = Number(entry.target.id.replace(/[^0-9]/g, ''));
		if(entry.isIntersecting){
			next += 1;
			if(null === document.getElementById('section' + next)){
				document.querySelector('.section-link-wrap').style.display = 'none';
			}else{
				document.querySelector('.section-link-wrap').style.display = 'block';
				document.getElementById('section-link').setAttribute('href', '#section' + next);
			}
		}else{
			if(0 === next && 0 < entry.boundingClientRect.y){
				document.getElementById('section-link').setAttribute('href', '#section' + next);
			}
		}
	});
};

ということで問題は解決です。

なお、やはり問題はバグというわけではなく交差領域の指定の仕方に問題があるようです。でも問題が出るまでこれでいきます(笑)。