18
8

More than 1 year has passed since last update.

scrollイベントを用いずに「topから〇〇pxスクロールしたらどうのこうの」する

Last updated at Posted at 2022-04-15

やりたきこと

scrollイベントを用いることなく、「topから〇〇pxスクロールした」瞬間を検知したい。

結論

IntersectionObserverdocument.bodyを監視し、observerrootMargin.topに境界となる値を設定する。

Typescript
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 とは

IntersectionObserverAPIは、あるターゲット要素を監視し、ビューポートまたは特定の親要素から出たり入ったりした時、あるいは両者が交差する量が要求された量だけ変化した時に、登録されたコールバック関数を実行するというAPIです。

交差オブザーバー API - Web API | MDN

基本的なIntersectionObserverの使い方

基本的にはある要素をターゲットとして監視し、画面(viewport)に入ったらどうの・出たらこうのする、という使い方が基本かと思います。
スクロールしていくとふわっ…と現れる要素なんかによく使われます。

See the Pen IntersectionObserver Demo by MenTori (@MentTori) on CodePen.

本題: IntersectionObserverで「〇〇pxスクロールしたかどうか」を監視する

「スクロール量に応じて」という実装はできない

特定の要素を監視してviewportに入ったか・出ていったかを判定して関数を実行するAPIなので、スクロールしたら関数を実行、ということはできないです。
ターゲット要素をtopから100pxの位置に配置すればもちろん可能ですが、そういうことじゃない….

rootMarginでviewportを上部に拡張して、topにある要素を監視する

IntersectionObserverAPIにはオプションを色々設定できるのですが、その中に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イベントを使えば簡単なことはわかっております、一応。使いたくないから頑張ったんです、一応。
完全に独自実装ですのでおかしな点多々あるかと思いますし、「いやそもそもそんなややこしいことしなくてもコレでできるが??」ってのがありましたら是非に教えていただきたいです...👀

18
8
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
18
8