やりたきこと
scrollイベントを用いることなく、「topから〇〇pxスクロールした」瞬間を検知したい。
結論
IntersectionObserver
でdocument.body
を監視し、observer
のrootMargin.top
に境界となる値を設定する。
const border = 100; // 100pxスクロールしたらどうのこうのする
const option = {
root: null,
// rootMargin.topに境界を、rootMargin.bottomにbodyの高さを設定
rooMargin: `${border}px 0px ${document.body.clientHeight}px 0px`,
threshold: 1
};
const callback = (entries: IntersectionObserverEntry[]) => {
// IntersectionObserverEntryはundefinedの可能性があるので型ガード
if (typeof entries[0] === 'undefined') return;
if (!entries[0].isIntersecting) {
// isIntersecting === false のとき、100pxスクロールした瞬間
doNanyaKanya(); // なんやかんや
}
};
const observe = new IntersectionObserver(callback, option);
observe.observe(document.body); // body全体を監視
IntersectionObserver
とは
IntersectionObserver
APIは、あるターゲット要素を監視し、ビューポートまたは特定の親要素から出たり入ったりした時、あるいは両者が交差する量が要求された量だけ変化した時に、登録されたコールバック関数を実行するというAPIです。
基本的なIntersectionObserver
の使い方
基本的にはある要素をターゲットとして監視し、画面(viewport)に入ったらどうの・出たらこうのする、という使い方が基本かと思います。
スクロールしていくとふわっ…と現れる要素なんかによく使われます。
See the Pen IntersectionObserver Demo by MenTori (@MentTori) on CodePen.
本題: IntersectionObserver
で「〇〇pxスクロールしたかどうか」を監視する
「スクロール量に応じて」という実装はできない
特定の要素を監視してviewportに入ったか・出ていったかを判定して関数を実行するAPIなので、スクロールしたら関数を実行、ということはできないです。
ターゲット要素をtopから100pxの位置に配置すればもちろん可能ですが、そういうことじゃない….
rootMargin
でviewportを上部に拡張して、topにある要素を監視する
IntersectionObserver
APIにはオプションを色々設定できるのですが、その中にrootMargin
という設定項目があります。こちらはviewportの拡大・縮小をさせることができる設定で、cssのmargin
プロパティのような値を指定します。
rootMargin: 100px 0px 0px 0px;
と指定するとviewportが上部に100px拡張され、画面上端から100px上部に境界を設定できます。
この設定により、rootMargin: 100px 0px 0px 0px;
でviewportを拡張してヘッダーなどのページ最上部に位置する要素を監視すれば、下方向にスクロールしてヘッダーが画面上端から100pxの位置に来た瞬間をオブザーバーで検知することができます!
これはつまり「100pxスクロールした瞬間」に他ならないのです!!やったぜ解決!!👏
より汎用的にbody
を監視する
上記の実装で100pxスクロールした瞬間の検知はできました。しかしページ最上部に必ず要素があるとは限りません。そこで、絶対に画面上部に接しているDOM要素を監視するようにすればいいのではと考えました。
そう、body要素です。
しかし、bodyは大体画面(viewport)より高さがあるため常にisIntersecting === false
となってしまっていることが問題になります。
上記の100pxスクロール検知は境界を画面上端より100pxにまで拡張してそのviewportから要素が少しでも出た瞬間を検知しているのですが、bodyは大きすぎて常にviewportから出てしまっているため検知ができないのです。
そこで再びrootMargin
を用いて、今度は下方向にviewportを拡大します。どれくらい拡大するかというとbodyの高さ分だけ拡大します。
こうすることによりbodyがviewportにすっぽり入るため、スクロールしていない時点ではisIntersecting === true
となり、100pxスクロールしてbodyの上端が境界から少しでも出た瞬間にisIntersecting === false
となります。
これで汎用的に100pxスクロールした瞬間を検知することができました!!💪
(なんだかめっちゃ裏技じみている気がしますが…)
以下が完成形のコードです。
const border = 100; // 100pxスクロールしたらどうのこうのする
const option = {
root: null,
// `rootMargin.top`に境界を、rootMargin.bottomにbodyの高さを設定
rooMargin: `${border}px 0px ${document.body.clientHeight}px 0px`,
threshold: 1
};
const callback = (entries: IntersectionObserverEntry[]) => {
// IntersectionObserverEntryはundefinedの可能性があるので型ガード
if (typeof entries[0] === 'undefined') return;
if (!entries[0].isIntersecting) {
// isIntersecting === false のとき、100pxスクロールした
doNanyaKanya(); // なんやかんや
}
};
const observe = new IntersectionObserver(callback, option);
observe.observe(document.body); // body全体を監視
ちなみに画面下部からdocument.body.clientHeight
分viewportを拡張しているため、画面の高さwindow.height
分だけ余分に広げてしまっています。別に問題ないとは思いますが、気になる方は${document.body.clientHeight - window.height + 1}px
みたいにギリギリを攻めるといいと思います。✍🏻
さいごに
もちろんscrollイベントを使えば簡単なことはわかっております、一応。使いたくないから頑張ったんです、一応。
完全に独自実装ですのでおかしな点多々あるかと思いますし、「いやそもそもそんなややこしいことしなくてもコレでできるが??」ってのがありましたら是非に教えていただきたいです...👀