CSS location:sticky etkinliği için

Eric Bidelman

Özet

İşte sırrımız: Bir sonraki uygulamanızda scroll etkinliklerine ihtiyacınız olmayabilir. IntersectionObserver kullanarak, position:sticky öğeleri düzeltildiğinde veya öğelerin yapışması durduğunda nasıl özel etkinlik tetikleyeceğinizi göstereceğim. Tüm bunları, kaydırma işleyicileri kullanmadan yapabilirsiniz. Bunu kanıtlayacak harika bir demo bile var:

Demoyu göster | Kaynak

Karşınızda sticky-change etkinliği

CSS yapışkan konumunu kullanmanın pratik sınırlamalarından biri, mülkün ne zaman etkin olduğunu bildiren bir platform sinyali sağlamamasıdır. Diğer bir deyişle, bir öğe ne zaman yapışkan hale geldiğini veya yapışkan olmayı bıraktığını bilmek söz konusu değildir.

<div class="sticky"> öğesinin üst kapsayıcısının üst kısmından 10 piksel uzaklıktaki örneği ele alalım:

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

Öğeler bu işarete ulaştığında tarayıcı bildirilse güzel olmaz mıydı? Böyle düşünen tek kişi ben değilim. position:sticky sinyali birçok kullanım alanının kilidini açabilir:

  1. Sabitlenen banner'a gölge uygulayın.
  2. Bir kullanıcı içeriğinizi okurken, ilerleme durumunu öğrenmek için analiz isabetlerini kaydedin.
  3. Kullanıcı sayfayı kaydırdığında, kayan TOC widget'ını geçerli bölüme güncelleyin.

Bu kullanım alanlarını göz önünde bulundurarak nihai bir hedef belirledik: Bir position:sticky öğesi düzeltildiğinde tetiklenen bir etkinlik oluşturmak. Bunu sticky-change etkinliği olarak adlandıralım:

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, düzeltildiğinde bir gölgeye başlık eklemek için bu etkinliği kullanır. Ayrıca sayfanın üst kısmındaki yeni başlık da güncellenir.

Demoda, efektler kaydırma etkinlikleri olmadan uygulanır.

Kaydırma etkinlikleri olmadan kaydırma efektleri var mı?

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

İsterseniz yazının geri kalanında bu adlardan bahsedebilmek için biraz terminolojiyi sadeleştirelim:

  1. Kayan kapsayıcı - "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 bir içerik bölümü. Yapışkan başlıkların altında kayan metin.
  4. "Sabit mod": Öğeye position:sticky uygulanırken.

Hangi üstbilginin "sabit moda" girdiğini bilmek için kaydırma kapsayıcısının kaydırma ofsetini belirlemenin bir yolunu bulmamız gerekir. Bu, bize o anda gösterilen başlığı hesaplamanın bir yolunu sunar. Bununla birlikte, scroll etkinlikleri olmadan bunu yapmak oldukça zor olur :) Diğer sorun, position:sticky öğesinin öğe düzeltildiğinde düzenden kaldırılmasıdır.

Dolayısıyla, kaydırma etkinlikleri olmadığında başlıklarda düzenle ilgili hesaplamalar yapamayız.

Kaydırma konumunu belirlemek için önemsiz DOM ekleniyor

headers ne zaman sabit moda girip çıktığını belirlemek için scroll etkinlikleri yerine IntersectionObserver kullanacağız. Her yapışkan bölüme biri üstte, diğeri altta olacak şekilde iki düğüm (gözetmenler) eklemek, kaydırma konumunu bulmak için ara nokta görevi görür. Bu işaretçiler kapsayıcıya girip çıktıkça, görünürlükleri değişir ve Intersection Observer bir geri çağırma tetikler.

Koruyucu öğeler gösterilmediği
Gizli koruyucu öğeleri.

Yukarı ve aşağı kaydırma ile ilgili dört örneği ele almak için iki koruyucuya ihtiyacımız var:

  1. Aşağı kaydırma: Üst koruyucusu kapsayıcının üst kısmından geçtiğinde başlık yapışkan hale gelir.
  2. Aşağı kaydırma: Başlık, bölümün alt kısmına ulaştığında ve alt koruyucusu kapsayıcının üst kısmından geçtiğinde sabit moddan çıkar.
  3. Yukarı kaydırma: Başlık, üst koruyucusu yukarıdan görünüme geri kaydırdığında sabit moddan çıkar.
  4. Yukarı kaydırma: Başlık, alt koruyucusu üstten görünüme geri döndüğünde yapışkan hale gelir.

1'den 4'e kadar olan ekran video kaydını gerçekleşme sırasına göre izlemek faydalıdır:

Kesişim Gözlemcileri, gözetmenler kaydırma kapsayıcısına girdiğinde/bundan ayrıldığında geri çağırmaları tetikler.

CSS

Gözetmenler her bölümün üstünde ve altında konumlandırılır. .sticky_sentinel--top başlığın en üstünde, .sticky_sentinel--bottom ise bölümün altındadır:

Alt koruyucu, eşiğine ulaşıyor.
Üst ve alt koruyucu öğ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özlemciler, bir hedef öğe ile belge görüntü alanının veya bir üst kapsayıcının kesişimindeki değişiklikleri eşzamansız olarak gözlemler. Örneğimizde, bir üst kapsayıcıyla kesişimleri gözlemliyoruz.

Sihirli sos IntersectionObserver. Her bir koruyucu, kaydırma kapsayıcısı içindeki kesişim görünürlüğünü gözlemlemek için bir IntersectionObserver alır. Bir güvenlik görevlisi, görünür görüntü alanına girdiğinde, bir başlığın sabitlendiğini veya yapışkan durumunun durduğunu anlarız. Benzer bir şekilde, bir güvenlik görevlisi görüntü alanından çıktığında.

İlk olarak, üstbilgi ve altbilgi korumaları için gözlemcileri ayarladım:

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

Daha sonra, .sticky_sentinel--top öğeleri kaydırma kapsayıcısının üst kısmından (her iki yönde) geçtiğinde etkinleşecek bir gözlemci ekledim. observeHeaders işlevi, en iyi koruyucuları oluşturur ve bunları her bir bölüme ekler. Gözlemci, koruyucunun container'ın üst kısmıyla kesişimini hesaplar ve görüntü alanına girip girmediğine karar verir. Bu bilgiler, bölüm başlığının kalıcı 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ılır. Bu nedenle, güvenlik gözetici görünür hale geldiğinde geri çağırma etkinleşir.

Bu süreç alt koruyucu (.sticky_sentinel--bottom) için de geçerlidir. Altbilgiler kaydırma kapsayıcısının altından geçtiğinde etkinleşmek üzere ikinci bir gözlemci oluşturulur. observeFooters işlevi, koruyucu düğümleri oluşturur ve bunları her bir bölüme ekler. Gözlemci, koruyucunun container'ın alt kısmıyla kesişimini hesaplar ve girişe mi yoksa ayrılıyor mu olduğuna karar verir. Bu bilgiler, bölüm başlığının kalıcı olup olmadığını 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ılır. Bu nedenle, düğümün tamamı görünümde olduğunda geri çağırma etkinleşir.

Son olarak, sticky-change özel etkinliğini tetiklemek ve koruyucuları oluşturmak için kullanabileceğim iki yardımcı programım vardır:

/**
 * @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.

Son demo

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

Demoyu göster | Kaynak

Sonuç

IntersectionObserver'in yıllar içinde gelişen scroll etkinliğe dayalı kullanıcı arayüzü kalıplarından bazılarının yerine geçerken faydalı bir araç olup olmayacağını sık sık düşündüm. Cevabın hem evet hem de hayır olduğu ortaya çıktı. IntersectionObserver API'nin anlamları, her şey için kullanılmasını zorlaştırıyor. Ama 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. İhtiyacımız olan şey, DOM öğesindeki stil değişikliklerini gözlemlemenin bir yoluydu. Ne yazık ki web platformu API'lerinde stil değişikliklerini izlemenize olanak tanıyan hiçbir şey yoktur.

MutationObserver, mantıklı bir ilk tercih olur ancak çoğu durumda işe yaramaz. Örneğin, demoda sticky sınıfı bir öğeye eklendiğinde geri çağırma yapılır ancak öğenin hesaplanan stili değiştiğinde geri arama almazsınız. sticky sınıfının sayfa yüklemede zaten bildirilmiş olduğunu unutmayın.

Gelecekte, Mutasyon Gözlemcileri'ne yönelik bir"Style Mutation Observer" uzantısı, bir öğenin hesaplanan stillerindeki değişiklikleri gözlemlemek için yararlı olabilir. position: sticky.