Kaydırarak Tutturma ile İlgili Anında Geri Bildirim

Genel Bakış

Kullanıcılar, bantlar veya galeriler gibi kullanıcı arayüzü öğeleriyle etkileşim kurarken anında görsel geri bildirim bekler. Geleneksel kaydırarak tutturma, yalnızca kaydırma hareketi tamamlanıp öğe yerleştikten sonra geri bildirim sağlar. Kaydırarak tutturma etkinliklerini, özellikle de scrollsnapchanging etkinliğini kullanarak kaydırma hareketi sırasında anında geri bildirim sağlayabilir, kullanıcı dokunmayı veya fareyi bırakmadan önce bekleyen tutturma hedefini vurgulayabilirsiniz.

Uygulama

1. scrollsnapchanging için dinle

Kaydırma kapsayıcısına scrollsnapchanging için bir etkinlik işleyici ekleyin. Bu etkinlik, tarayıcı yeni bir tutturma hedefinin seçilme olasılığını belirlediğinde tetiklenir.

const container = document.querySelector('#gallery');
const thumbnails = document.querySelectorAll('.thumbnail');
const items = document.querySelectorAll('.gallery-item');

container.addEventListener('scrollsnapchanging', (event) => {
  // Highlight pending snap target during scroll for real-time feedback.
  const pendingTarget = event.snapTargetInline;
  const index = [...items].indexOf(pendingTarget);

  if (index === -1 || !thumbnails[index]) return;

  // Use lightweight class toggle to avoid layout thrashing during rapid events.
  // Note: aria-current is NOT toggled here. It tracks the settled "current"
  // item, which is updated in the scrollsnapchange handler below.
  thumbnails.forEach((thumb) => thumb.classList.remove('pending'));
  thumbnails[index].classList.add('pending');
});

Galeri yatay olarak kaydırıldığı için bu örnekte snapTargetInline kullanılmıştır. Kaydırma kapsayıcınız dikey olarak kaydırılıyorsa bunun yerine snapTargetBlock kullanın.

2. scrollsnapchange için dinle

Kaydırma hareketi tamamlandığında ve öğe gerçekten sabitlendiğinde durumu sonlandırmak için scrollsnapchange etkinliğini dinleyin. Bu, son etkin durumun belirlenmesi için gereklidir.

container.addEventListener('scrollsnapchange', (event) => {
  // Promote pending state to active on scroll completion.
  const snappedTarget = event.snapTargetInline;
  const index = [...items].indexOf(snappedTarget);

  if (index === -1 || !thumbnails[index]) return;

  // Establish final active state and clean up pending.
  thumbnails.forEach((thumb) => {
    thumb.classList.remove('pending', 'active');
    thumb.removeAttribute('aria-current');
  });
  thumbnails[index].classList.add('active');
  thumbnails[index].setAttribute('aria-current', 'true');
});

3. İlk durumu senkronize etme

Sayfa yüklendiğinde, kaydırma konumu tarayıcı tarafından geri yüklenebilir (ör. geçmişte gezinme veya yer işareti bağlantısı aracılığıyla). scrollsnapchange ve scroll etkinlikleri otomatik olarak tetiklenmez. Kullanıcı arayüzünü ilk kaydırma konumuyla senkronize etmek için tek seferlik bir geometrik kontrol çalıştırın.

// Note: For item.offsetLeft to be relative to the container, 
// the container MUST be the offsetParent (e.g., `position: relative`).
const findClosestItemIndex = () => {
  // Center-distance assumes scroll-snap-align: center on items.
  // For start-aligned snap, compare scrollLeft to item.offsetLeft directly.
  const containerCenter = container.scrollLeft + container.clientWidth / 2;
  let closestIndex = 0;
  let minDistance = Infinity;

  items.forEach((item, index) => {
    const itemCenter = item.offsetLeft + item.offsetWidth / 2;
    const distance = Math.abs(containerCenter - itemCenter);
    if (distance < minDistance) {
      minDistance = distance;
      closestIndex = index;
    }
  });
  return closestIndex;
};

const initActiveItem = () => {
  const closestIndex = findClosestItemIndex();
  if (!thumbnails[closestIndex]) return;

  thumbnails.forEach((thumb) => {
    thumb.classList.remove('pending', 'active');
    thumb.removeAttribute('aria-current');
  });
  thumbnails[closestIndex].classList.add('active');
  thumbnails[closestIndex].setAttribute('aria-current', 'true');
};

if (document.readyState === 'complete') {
  initActiveItem();
} else {
  window.addEventListener('load', initActiveItem, { once: true });
}

Yedek stratejiler

scrollsnapchanging özelliğini desteklemeyen tarayıcılarda kullanıcı arayüzü, kaydırma hareketi sırasında varsayılan olarak hızlı geri bildirim sağlamaz ve bağlantılı kullanıcı arayüzü, içerikle senkronize olmaz.

ZORUNLU: Desteklenmeyen tarayıcılar için bir yedek sağlayın. Aksi takdirde, bağlantılı kullanıcı arayüzünün içerikle senkronizasyonu bozulur.

Kullanıcı aktif olarak kaydırma yaparken en yakın tutturma hedefini belirlemek için requestAnimationFrame ve geometrik mesafe hesaplamalarıyla birleştirilmiş bir scroll etkinlik işleyici ile scrollsnapchanging'nin gerçek zamanlı doğasını simüle EDİN.

if ('onscrollsnapchanging' in Element.prototype) {
  // Use native scroll snap events
} else {
  // Fallback: use scroll + requestAnimationFrame for eager feedback
  // (assumes the same container, thumbnails, items defined in step 1)
  let scrollTimeout;
  let rafId = null;

  const promotePendingToActive = () => {
    const closestIndex = findClosestItemIndex();
    if (!thumbnails[closestIndex]) return;
    thumbnails.forEach((thumb) => {
      thumb.classList.remove('pending', 'active');
      thumb.removeAttribute('aria-current');
    });
    thumbnails[closestIndex].classList.add('active');
    thumbnails[closestIndex].setAttribute('aria-current', 'true');
  };

  container.addEventListener('scroll', () => {
    if (rafId) return;
    rafId = requestAnimationFrame(() => {
      rafId = null;
      const closestIndex = findClosestItemIndex();
      if (!thumbnails[closestIndex]) return;

      // DO NOT forget to clean up stale pending classes
      thumbnails.forEach((thumb) => thumb.classList.remove('pending'));
      thumbnails[closestIndex].classList.add('pending');
    });

    // Debounce fallback for browsers that don't support scrollend
    clearTimeout(scrollTimeout);
    scrollTimeout = setTimeout(promotePendingToActive, 100);
  }, { passive: true });

  // Fallback: use Baseline `scrollend` event to promote pending to active cleanly where supported
  container.addEventListener('scrollend', () => {
    clearTimeout(scrollTimeout);
    promotePendingToActive();
  });
}

Geometrik scroll + requestAnimationFrame yedek, programatik kaydırmayı doğru şekilde işleme de dahil olmak üzere yerel kaydırma tahmininin davranışını yakından taklit eder. scrollIntoView gibi işlevler, yürütme sırasında doğal olarak scroll etkinliklerini tetiklediğinden kullanıcı arayüzü, ek özel mantık gerektirmeden kaydırma animasyonu boyunca sorunsuz bir şekilde senkronize kalır.