Défilement infini

Cette implémentation du défilement infini est conçue pour éviter tout décalage de mise en page, quel que soit le temps nécessaire au serveur pour répondre avec le nouveau contenu.

L'un des problèmes les plus courants liés aux nombreuses implémentations de défilement infini est que le pied de page (ou un élément d'expérience utilisateur similaire) est poussé plus bas sur la page chaque fois que de nouveaux éléments sont ajoutés. Avec cette implémentation du défilement infini, cela ne se produit jamais.

Approche globale

Dans la mesure du possible, les nouveaux éléments sont insérés dans la page avant que l'utilisateur ne les atteigne. Étant donné que cette insertion s'effectue hors écran (et n'est pas visible par l'utilisateur), l'utilisateur ne subit aucun décalage de mise en page.

Si le nouveau contenu ne peut pas être inséré à temps, un bouton "Afficher plus" s'affiche à la place. Toutefois, le bouton n'est activé que lorsque de nouveaux éléments sont prêts à être affichés. Ainsi, l'utilisateur ne clique pas sur le bouton uniquement pour constater que rien ne se passe. Ainsi, quelle que soit la vitesse à laquelle le serveur répond avec le nouveau contenu (ou la vitesse à laquelle l'utilisateur fait défiler la page), il n'y aura jamais de décalages de mise en page inattendus.

Mise en œuvre

L'API Intersection Observer est un moyen performant de surveiller la position et la visibilité des éléments de la page. Cette conception est implémentée à l'aide de deux observateurs d'intersertion distincts:

  • listObserver observe la position de l'élément #infinite-scroll-button situé à la fin de la liste de défilement infini. Lorsque le bouton se rapproche de la fenêtre d'affichage, le contenu non inséré est ajouté au DOM.
  • sentinelObserver observe la position de l'élément #sentinel. Lorsque la sentinelle devient visible, davantage de contenu est demandé au serveur. L'ajustement de la position de la sentinelle permet de contrôler le délai de demande du nouveau contenu au serveur.

Ce n'est pas le seul moyen de gérer les décalages de mise en page résultant de l'utilisation du défilement infini. D'autres façons d'aborder ce problème incluent le passage à la pagination, l'utilisation de la virtualisation des listes et l'ajustement de la mise en page.

HTML

<div id="infinite-scroll-container">
    <div id="sentinel"></div>
    <div class="item">A</div>
    <div class="item">B</div>
    <div class="item">C</div>
    <div class="item">D</div>
    <div class="item">E</div>
    <button id="infinite-scroll-button" disabled>
        <span class="disabled-text">Loading more items...</span>
        <span class="active-text">Show more</span>
    </button>
</div>

CSS


        :root {
    --active-button-primary: #0080ff;
    --active-button-font:#ffffff;
    --disabled-button-primary: #f5f5f5;
    --disabled-button-secondary: #c4c4c4;
    --disabled-button-font: #000000;
}
#infinite-scroll-container {
    position: relative;
}
#sentinel {
    position: absolute;
    bottom: 150vh;
}
#infinite-scroll-button {
    cursor: pointer;
    border: none;
    padding: 1em;
    width: 100%;
    font-size: 1em;
}
#infinite-scroll-button:enabled {
    color: var(--active-button-font);
    background-color: var(--active-button-primary)
}
#infinite-scroll-button:disabled {
    color: var(--disabled-button-font);
    background-color: var(--disabled-button-primary);
    cursor: not-allowed;
    animation: 3s ease-in-out infinite loadingAnimation;
}
#infinite-scroll-button:enabled .disabled-text {
    display: none;
}
#infinite-scroll-button:disabled .active-text {
    display: none;
}
@keyframes loadingAnimation {
    0% {
        background-color: var(--disabled-button-primary);
    }
    50% {
        background-color: var(--disabled-button-secondary);
    }
    100% {
        background-color: var(--disabled-button-primary);
    }
}
        

JS


        function infiniteScroll() {
    let responseBuffer = [];
    let hasMore;
    let requestPending = false;
    const loadingButtonEl = document.querySelector('#infinite-scroll-button');
    const containerEl = document.querySelector('#infinite-scroll-container');
    const sentinelEl = document.querySelector("#sentinel");
    const insertNewItems = () => {
        while (responseBuffer.length > 0) {
            const data = responseBuffer.shift();
            const el = document.createElement("div");
            el.textContent = data;
            el.classList.add("item");
            el.classList.add("new");
            containerEl.insertBefore(el, loadingButtonEl);
            console.log(`inserted: ${data}`);
        }
        sentinelObserver.observe(sentinelEl);
        if (hasMore === false) {
            loadingButtonEl.style = "display: none";
            sentinelObserver.unobserve(sentinelEl);
            listObserver.unobserve(loadingButtonEl);
        }
        loadingButtonEl.disabled = true
    }
    loadingButtonEl.addEventListener("click", insertNewItems);
    const requestHandler = () => {
        if (requestPending) return;
        console.log("making request");
        requestPending = true;
        fakeServer.fakeRequest().then((response) => {
            console.log("server response", response);
            requestPending = false;
            responseBuffer = responseBuffer.concat(response.items);
            hasMore = response.hasMore;
            loadingButtonEl.disabled = false;;
        });
    }
    const sentinelObserver = new IntersectionObserver((entries, observer) => {
        entries.forEach(entry => {
            if (entry.intersectionRatio > 0) {
                observer.unobserve(sentinelEl);
                requestHandler();
            }
        });
    });
    const listObserver = new IntersectionObserver((entries, observer) => {
        entries.forEach(entry => {
            if (entry.intersectionRatio > 0 && entry.intersectionRatio < 1) {
                insertNewItems();
            }
        });
    }, {
        rootMargin: "0px 0px 200px 0px"
    });
    sentinelObserver.observe(sentinelEl);
    listObserver.observe(loadingButtonEl);
}
infiniteScroll();