Un evento per il CSSposition:sticky

TL;DR

Ecco un segreto: potresti non aver bisogno di scroll eventi nella tua prossima app. L'utilizzo di un IntersectionObserver, Ti mostro come attivare un evento personalizzato quando gli elementi position:sticky diventano corretti o quando non vengono più applicati. Il tutto senza l'uso di listener di scorrimento. C'è anche una fantastica demo che lo dimostra:

Visualizza demo | Fonte

Presentazione dell'evento sticky-change

Una delle limitazioni pratiche dell'utilizzo della posizione persistente CSS è che non fornisce un indicatore della piattaforma per sapere quando la proprietà è attiva. In altre parole, non esiste alcun evento da sapere quando un elemento diventa fisso o quando smette di essere fisso.

Prendi l'esempio seguente, che corregge un <div class="sticky"> 10 px dalla parte superiore del relativo contenitore principale:

.sticky {
  position: sticky;
  top: 10px;
}

Non sarebbe bello se il browser comunicasse quando gli elementi colpiscono quel segno? A quanto pare non sono il solo che la pensa così. Un indicatore di position:sticky potrebbe sbloccare una serie di casi d'uso:

  1. Applica un'ombra a un banner mentre si fissa.
  2. Mentre un utente legge i tuoi contenuti, registra gli hit di Analytics per conoscere la sua progressi.
  3. Mentre l'utente scorre la pagina, aggiorna il widget Sommario mobile alla versione corrente .

Tenendo a mente questi casi d'uso, abbiamo creato un obiettivo finale: creare un evento che si attiva quando un elemento position:sticky viene corretto. Chiamiamola Evento sticky-change:

document.addEventListener('sticky-change', e => {
  const header = e.detail.target;  // header became sticky or stopped sticking.
  const sticking = e.detail.stuck; // true when header is sticky.
  header.classList.toggle('shadow', sticking); // add drop shadow when sticking.

  document.querySelector('.who-is-sticking').textContent = header.textContent;
});

La demo utilizza questo evento alle intestazioni di un'ombra quando vengono corretti. Inoltre, aggiorna nuovo titolo nella parte superiore della pagina.

Nella demo, gli effetti vengono applicati senza eventi di scorrimento.

Vuoi scorrere gli effetti senza eventi di scorrimento?

Struttura della pagina.
Struttura della pagina.

Togliamo un po' di terminologia in modo da poter fare riferimento a questi nomi nel resto del post:

  1. Contenitore a scorrimento: l'area dei contenuti (area visibile) contenente i l'elenco di "post del blog".
  2. Intestazioni: titolo blu in ogni sezione che contiene position:sticky.
  3. Sezioni persistenti: ogni sezione di contenuti. Il testo che scorre sotto intestazioni fisse.
  4. "Modalità persistente": quando all'elemento viene applicato position:sticky.

Per sapere quale intestazione entra nella "modalità persistente", abbiamo bisogno di un modo per determinare l'offset di scorrimento del container di scorrimento. Questo ci darebbe modo di per calcolare l'intestazione attualmente visualizzata. Tuttavia, diventa un bel po' difficile da fare senza gli eventi scroll :) L'altro problema è che position:sticky rimuove l'elemento dal layout quando diventa fisso.

Pertanto, senza gli eventi di scorrimento, abbiamo perso la possibilità di eseguire query nelle intestazioni.

Aggiunta di un DOM dumby per determinare la posizione di scorrimento

Invece di scroll eventi, useremo un IntersectionObserver per determina quando le intestazioni entrano ed escono dalla modalità persistente. L'aggiunta di due nodi (note anche come sentinelle) in ogni sezione permanente, una in alto e una in basso, serviranno da tappa per capire la posizione di scorrimento. Poiché questi elementi gli indicatori entrano ed escono dal contenitore, la loro visibilità cambia L'osservatore di intersezione attiva un callback.

Senza elementi sentinella
Gli elementi sentinella nascosti.

Abbiamo bisogno di due sentinelle per coprire quattro casi di scorrimento verso l'alto e verso il basso:

  1. Scorrimento verso il basso: l'intestazione diventa fissa quando la parte superiore della sentinella attraversa nella parte superiore del container.
  2. Scorrimento verso il basso: intestazione lascia la modalità persistente quando raggiunge la parte inferiore la sezione e la sentinella inferiore attraversa la parte superiore del container.
  3. Scorrimento verso l'alto: l'intestazione lascia la modalità persistente quando la parte superiore della sentinella scorre. di nuovo visibile dall'alto.
  4. Scorrimento verso l'alto: l'intestazione diventa fissa quando la sentinella inferiore si incrocia visibile dall'alto.

È utile vedere uno screencast di 1-4 nell'ordine in cui avvengono:

Gli osservatori delle intersezioni attivano i callback quando le sentinelle inserisci/esci dal contenitore di scorrimento.

Il CSS

Le sentinelle sono posizionate nella parte superiore e inferiore di ogni sezione. .sticky_sentinel--top si trova in cima all'intestazione .sticky_sentinel--bottom si trova in fondo alla sezione:

La sentinella inferiore sta per raggiungere la soglia.
Posizione degli elementi sentinella superiore e inferiore.
:root {
  --default-padding: 16px;
  --header-height: 80px;
}
.sticky {
  position: sticky;
  top: 10px; /* adjust sentinel height/positioning based on this position. */
  height: var(--header-height);
  padding: 0 var(--default-padding);
}
.sticky_sentinel {
  position: absolute;
  left: 0;
  right: 0; /* needs dimensions */
  visibility: hidden;
}
.sticky_sentinel--top {
  /* Adjust the height and top values based on your on your sticky top position.
  e.g. make the height bigger and adjust the top so observeHeaders()'s
  IntersectionObserver fires as soon as the bottom of the sentinel crosses the
  top of the intersection container. */
  height: 40px;
  top: -24px;
}
.sticky_sentinel--bottom {
  /* Height should match the top of the header when it's at the bottom of the
  intersection container. */
  height: calc(var(--header-height) + var(--default-padding));
  bottom: 0;
}

Configurare gli osservatori delle intersezioni

Gli osservatori delle intersezioni osservano in modo asincrono i cambiamenti nell'intersezione di un elemento target e l'area visibile del documento o un contenitore principale. Nel nostro caso, osserviamo le intersezioni con un contenitore principale.

La salsa magica è IntersectionObserver. Ogni sentinella ha un IntersectionObserver all'osservatore della visibilità dell'intersezione all'interno di container di scorrimento. Quando una sentinella scorre nell'area visibile, sappiamo un'intestazione si corregge o non è più fissa. Allo stesso modo, quando una sentinella esce nell'area visibile.

Per prima cosa, ho impostato degli osservatori per le sentinelle di intestazione e piè di pagina:

/**
 * Notifies when elements w/ the `sticky` class begin to stick or stop sticking.
 * Note: the elements should be children of `container`.
 * @param {!Element} container
 */
function observeStickyHeaderChanges(container) {
  observeHeaders(container);
  observeFooters(container);
}

observeStickyHeaderChanges(document.querySelector('#scroll-container'));

Poi ho aggiunto un osservatore che si attiva quando vengono superati .sticky_sentinel--top elementi dalla parte superiore del container di scorrimento (in entrambe le direzioni). La funzione observeHeaders crea le migliori sentinelle e le aggiunge alle ogni sezione. L'osservatore calcola l'intersezione della sentinella con parte superiore del contenitore e decide se entra o esce dall'area visibile. Questo informazioni determina se l'intestazione della sezione viene applicata o meno.

/**
 * Sets up an intersection observer to notify when elements with the class
 * `.sticky_sentinel--top` become visible/invisible at the top of the container.
 * @param {!Element} container
 */
function observeHeaders(container) {
  const observer = new IntersectionObserver((records, observer) => {
    for (const record of records) {
      const targetInfo = record.boundingClientRect;
      const stickyTarget = record.target.parentElement.querySelector('.sticky');
      const rootBoundsInfo = record.rootBounds;

      // Started sticking.
      if (targetInfo.bottom < rootBoundsInfo.top) {
        fireEvent(true, stickyTarget);
      }

      // Stopped sticking.
      if (targetInfo.bottom >= rootBoundsInfo.top &&
          targetInfo.bottom < rootBoundsInfo.bottom) {
       fireEvent(false, stickyTarget);
      }
    }
  }, {threshold: [0], root: container});

  // Add the top sentinels to each section and attach an observer.
  const sentinels = addSentinels(container, 'sticky_sentinel--top');
  sentinels.forEach(el => observer.observe(el));
}

L'osservatore è configurato con threshold: [0], quindi il suo callback viene attivato non appena quando la sentinella diventa visibile.

Il processo è simile per la sentinella inferiore (.sticky_sentinel--bottom). Viene creato un secondo osservatore che si attiva quando i piè di pagina passano attraverso la parte inferiore. del container di scorrimento. La funzione observeFooters crea i nodi sentinella e li collega a ogni sezione. L'osservatore calcola intersezione della sentinella con il fondo del container e decide se si tratta entrando o uscendo. Queste informazioni determinano se l'intestazione della sezione è mantenere o meno.

/**
 * Sets up an intersection observer to notify when elements with the class
 * `.sticky_sentinel--bottom` become visible/invisible at the bottom of the
 * container.
 * @param {!Element} container
 */
function observeFooters(container) {
  const observer = new IntersectionObserver((records, observer) => {
    for (const record of records) {
      const targetInfo = record.boundingClientRect;
      const stickyTarget = record.target.parentElement.querySelector('.sticky');
      const rootBoundsInfo = record.rootBounds;
      const ratio = record.intersectionRatio;

      // Started sticking.
      if (targetInfo.bottom > rootBoundsInfo.top && ratio === 1) {
        fireEvent(true, stickyTarget);
      }

      // Stopped sticking.
      if (targetInfo.top < rootBoundsInfo.top &&
          targetInfo.bottom < rootBoundsInfo.bottom) {
        fireEvent(false, stickyTarget);
      }
    }
  }, {threshold: [1], root: container});

  // Add the bottom sentinels to each section and attach an observer.
  const sentinels = addSentinels(container, 'sticky_sentinel--bottom');
  sentinels.forEach(el => observer.observe(el));
}

L'osservatore è configurato con threshold: [1], quindi il suo callback viene attivato quando l'intero nodo è visibile.

Infine, ci sono le due utilità per attivare l'evento personalizzato sticky-change. e generando le sentinelle:

/**
 * @param {!Element} container
 * @param {string} className
 */
function addSentinels(container, className) {
  return Array.from(container.querySelectorAll('.sticky')).map(el => {
    const sentinel = document.createElement('div');
    sentinel.classList.add('sticky_sentinel', className);
    return el.parentElement.appendChild(sentinel);
  });
}

/**
 * Dispatches the `sticky-event` custom event on the target element.
 * @param {boolean} stuck True if `target` is sticky.
 * @param {!Element} target Element to fire the event on.
 */
function fireEvent(stuck, target) {
  const e = new CustomEvent('sticky-change', {detail: {stuck, target}});
  document.dispatchEvent(e);
}

È tutto.

Demo finale

Abbiamo creato un evento personalizzato quando gli elementi con position:sticky diventano corretti e aggiunti effetti di scorrimento senza l'utilizzo di eventi scroll.

Visualizza demo | Fonte

Conclusione

Spesso mi chiedevo se IntersectionObserver ci essere uno strumento utile per sostituire alcuni dei scroll pattern di UI basati su eventi che si sono sviluppati nel corso degli anni. E alla fine la risposta è sì e no. La semantica dell'API IntersectionObserver lo rendono difficile da usare per qualsiasi cosa. Ma poiché Ho mostrato qui, puoi usarlo per alcune tecniche interessanti.

Un altro modo per rilevare i cambiamenti di stile?

Non esattamente. Era necessario un modo per osservare le modifiche di stile su un elemento DOM. Sfortunatamente, le API della piattaforma web non consentono di lo stile dell'orologio.

MutationObserver sarebbe una prima scelta logica, ma non funziona per nella maggior parte dei casi. Ad esempio, nella demo, riceveremmo un callback quando sticky viene aggiunta a un elemento, ma non quando lo stile calcolato dell'elemento cambia. Ricorda che la classe sticky è già stata dichiarata durante il caricamento pagina.

In futuro, "Osservatore mutazione stile" agli osservatori delle mutazioni può essere utile per osservare stili calcolati dell'elemento. position: sticky.