CSS location:sticky etkinliği için

Özet

Size bir sır vereyim: Bir sonraki uygulamanızda scroll etkinliklerine ihtiyacınız olmayabilir. IntersectionObserver kullanarak, position:sticky öğeleri sabitlendiğinde veya yapışmayı bıraktığında nasıl özel etkinlik tetikleyebileceğinizi göstereceğim. Üstelik kaydırma dinleyicileri kullanmadan. Bunu kanıtlamak için harika bir demo bile hazırladık:

Demoyu görüntüleyin | Kaynak

sticky-change etkinliğiyle tanışın

CSS yapışkan konumunun pratik sınırlamalarından biri, mülkün ne zaman etkin olduğunu bilmek için bir platform sinyali sağlamamasıdır. Diğer bir deyişle, bir öğenin ne zaman yapışkan hale geldiğini veya ne zaman yapışkanlığını kaybettiğini bildiren bir etkinlik yoktur.

Aşağıdaki örneği inceleyin. Bu örnekte, <div class="sticky"> üst kapsayıcısının üst kısmından 10 piksel sabitlenmiştir:

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

Tarayıcı, öğelerin bu işarete ne zaman ulaştığını söylese ne iyi olurdu? Bu konuda yalnızca ben değilim. position:sticky sinyali, birçok kullanım alanında fayda sağlayabilir:

  1. Banner'a yapıştırırken gölge uygulayın.
  2. Kullanıcı içeriğinizi okurken ilerleme durumunu öğrenmek için Analytics isabetlerini kaydedin.
  3. Kullanıcı sayfayı kaydırırken, yüzen bir içindekiler widget'ını mevcut bölüme güncelleyin.

Bu kullanım alanlarını göz önünde bulundurarak bir nihai hedef belirledik: Bir position:sticky öğesi düzeltildiğinde tetiklenen bir etkinlik oluşturma. Bu etkinliğe sticky-change etkinliği adını verelim:

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;
});

Demo, sabitlendiğinde başlıklara gölge eklemek için bu etkinliği kullanır. Ayrıca sayfanın üst kısmındaki yeni başlığı da günceller.

Demoda efektler scrollevents olmadan uygulanmaktadır.

Kaydırma etkinliği olmadan kaydırma efektleri?

Sayfanın yapısı.
Sayfanın yapısı.

Yazının geri kalanında bu adlara referans verebilmek için bazı terimleri açıklayalım:

  1. Kaydırma kapsayıcısı: "Blog yayınları" listesini içeren içerik alanı (görünür görüntü alanı).
  2. Başlıklar: Her bölümde position:sticky içeren mavi başlık.
  3. Sabit bölümler: Her içerik bölümü. Sabit üstbilgilerin altında kaydırılan metin.
  4. "Sabit mod": position:sticky öğeye uygulandığında.

Hangi başlığın "yapışkan moda" gireceğini bilmek için kaydırma kapsayıcısının kaydırma ofsetini belirlememiz gerekir. Bu sayede, şu anda gösterilen başlığı hesaplayabiliriz. Ancak bunu scroll etkinlikleri olmadan yapmak oldukça zordur :) Diğer sorun da position:sticky'un sabitlendiğinde öğeyi düzenden kaldırmasıdır.

Bu nedenle, kaydırma etkinlikleri olmadan başlıklarda düzenlemeyle ilgili hesaplamaları yapma özelliğini kaybettik.

Kaydırma konumunu belirlemek için boş DOM ekleme

Başlıklar'ın ne zaman yapışkan moda girip çıktığını belirlemek için scroll etkinlikleri yerine bir IntersectionObserver kullanacağız. Her yapışkan bölüme birer üst ve alt olmak üzere iki düğüm (diğer adıyla gözetleyici) eklemek, kaydırma konumunu belirlemek için yol noktası görevi görür. Bu işaretçiler kapsayıcıya girip çıkarken görünürlükleri değişir ve Intersection Observer bir geri çağırma işlemi tetikler.

Gözetmen öğeleri gösterilmeden
Gizli sentinel öğeleri.

Yukarı ve aşağı kaydırmayla ilgili dört durumu kapsayacak iki gözetmene ihtiyacımız var:

  1. Aşağı kaydırma: Üst gözetleyicisi kapsayıcının üst kısmını geçtiğinde header yapışkan hale gelir.
  2. Sayfayı aşağı kaydırmak: Başlık, bölümün alt kısmına ulaştığında ve alt gözetleyicisi kapsayıcının üst kısmını geçtiğinde yapışkan moddan çıkar.
  3. Yukarı kaydırma: Başlık, üst gözetleyicisi en üstten tekrar görüntüye girdiğinde yapışkan moddan çıkar.
  4. Yukarı kaydırma: Alt gözcü üstten tekrar görüntüye girdiğinde başlık yapışkan hale gelir.

1-4 arasındaki adımları sırayla gösteren bir ekran kaydı görmek faydalı olabilir:

Kesişim gözlemcileri, gözetmenler kaydırma kapsayıcısına girdiğinde/kapsayıcıyı terk ettiğinde geri çağırma işlevlerini tetikler.

CSS

Gözetmenler her bölümün üst ve alt kısmına yerleştirilir. .sticky_sentinel--top, başlığın üst kısmında, .sticky_sentinel--bottom ise bölümün alt kısmında yer alır:

Alt gözetmen eşiğine ulaştı.
Üst ve alt gözetleyici öğelerinin konumu.
: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;
}

Kesişim gözlemcilerini ayarlama

Kesişim gözlemcileri, hedef öğe ile doküman görüntü alanının veya üst kapsayıcının kesişimindeki değişiklikleri eşzamansız olarak gözlemler. Bizim örneğimizde, ebeveyn kapsayıcıyla kesişimleri gözlemliyoruz.

İşin sırrı IntersectionObserver. Her gözetmen, kaydırma kapsayıcısında kesişim görünürlüğünü gözlemlemek için bir IntersectionObserver alır. Bir gözetmen görünür görüntü alanına kaydırıldığında, bir üstbilginin sabitlendiğini veya yapışkanlığını kaybettiğini biliriz. Benzer şekilde, bir gözetleyici görüntü alanından çıktığında da.

Öncelikle, üstbilgi ve altbilgi gözetmenleri için gözlemciler oluşturdum:

/**
 * 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'));

Ardından, .sticky_sentinel--top öğeleri kaydırılabilir kapsayıcının üst kısmından geçtiğinde (her iki yönde de) tetiklenecek bir gözlemci ekledim. observeHeaders işlevi, en iyi gözcüleri oluşturur ve her bölüme ekler. Gözlemci, gözetmenin kapsayıcının üst kısmıyla kesişim noktasını hesaplar ve gözetmenin görüntü alanının içine girip girmediğine karar verir. Bu bilgiler, bölüm başlığının yapışkan olup olmadığını belirler.

/**
 * 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));
}

Gözlemci, threshold: [0] ile yapılandırıldığından geri çağırma işlevi, gözetmen görünür hale gelir gelmez tetiklenir.

Alt gözetleyici (.sticky_sentinel--bottom) için süreç benzerdir. Altbilgiler kaydırma kapsayıcısının alt kısmından geçtiğinde tetiklenecek ikinci bir gözlemci oluşturulur. observeFooters işlevi, gözetmen düğümlerini oluşturur ve her bölüme ekler. Gözlemci, gözetmenin kapsayıcı tabanı ile kesişim noktasını hesaplar ve gözetmenin içeri girip girmediğine karar verir. Bu bilgiler, bölüm başlığının sabitlenip sabitlenmeyeceğini belirler.

/**
 * 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));
}

Gözlemci, threshold: [1] ile yapılandırıldığından geri çağırma işlevi, düğümün tamamı görünümde olduğunda tetiklenir.

Son olarak, sticky-change özel etkinliğini tetiklemek ve gözetmenleri oluşturmak için kullandığım iki yardımcı programımı paylaşmak isterim:

/**
 * @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);
}

İşte bu kadar.

Nihai demo

position:sticky içeren öğeler sabitlendiğinde özel bir etkinlik oluşturduk ve scroll etkinlikleri kullanmadan kaydırma efektleri ekledik.

Demoyu görüntüleyin | Kaynak

Sonuç

IntersectionObserver'un, yıllar içinde geliştirilen scrolletkinlik tabanlı kullanıcı arayüzü kalıplarının bazılarını değiştirmek için yararlı bir araç olup olmadığını sık sık merak ettim. Yanıtın hem evet hem de hayır olduğu ortaya çıktı. IntersectionObserver API'sinin semantikleri, her şey için kullanılmasını zorlaştırıyor. Ancak burada gösterdiğim gibi, bazı ilginç teknikler için kullanabilirsiniz.

Stil değişikliklerini tespit etmenin başka bir yolu var mı?

Pek sayılmaz. Bir DOM öğesindeki stil değişikliklerini gözlemleyebileceğimiz bir yönteme ihtiyacımız vardı. Maalesef web platformu API'lerinde stil değişikliklerini izlemenize olanak tanıyan bir özellik bulunmuyor.

MutationObserver ilk tercih olarak mantıklı bir seçenek olsa da çoğu durumda işe yaramaz. Örneğin, demoda sticky sınıfı bir öğeye eklendiğinde geri çağırma alırız ancak öğenin hesaplanmış stili değiştiğinde geri çağırma almayız. sticky sınıfının sayfa yüklenirken zaten tanımlandığını unutmayın.

Gelecekte, Mutation Observer'lara eklenecek bir"Style Mutation Observer", bir öğenin hesaplanmış stillerindeki değişiklikleri gözlemlemek için yararlı olabilir. position: sticky.