Feedback in tempo reale sull'allineamento dello scorrimento

Panoramica

Gli utenti si aspettano un feedback visivo immediato quando interagiscono con elementi UI come caroselli o gallerie. L'allineamento dello scorrimento tradizionale fornisce un feedback solo dopo il completamento del gesto di scorrimento e l'assestamento dell'elemento. Utilizzando gli eventi di allineamento dello scorrimento, in particolare scrollsnapchanging, puoi fornire un feedback in tempo reale durante il gesto di scorrimento, evidenziando il target di allineamento in attesa prima che l'utente rilasci il tocco o il mouse.

Implementazione

1. Ascolta scrollsnapchanging

Collega un listener di eventi per scrollsnapchanging al container di scorrimento. Questo evento viene attivato quando il browser determina che è probabile che venga selezionato un nuovo target di aggancio.

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

Questo esempio utilizza snapTargetInline perché la galleria scorre in orizzontale. Se il contenitore di scorrimento scorre verticalmente, utilizza snapTargetBlock.

2. Ascolta scrollsnapchange

Per finalizzare lo stato quando il gesto di scorrimento viene completato e l'elemento si aggancia effettivamente, ascolta l'evento scrollsnapchange. Questo è necessario per stabilire lo stato attivo finale.

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. Stato iniziale della sincronizzazione

Quando la pagina viene caricata, la posizione di scorrimento potrebbe essere ripristinata dal browser (ad es. tramite l'attraversamento della cronologia o un link di ancoraggio). Né gli eventi scrollsnapchange né gli eventi scroll verranno attivati automaticamente. Esegui un controllo geometrico una tantum per sincronizzare la UI con la posizione di scorrimento iniziale.

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

Strategie di riserva

Per i browser che non supportano scrollsnapchanging, l'interfaccia utente non fornirà un feedback immediato durante il gesto di scorrimento per impostazione predefinita e l'interfaccia utente collegata non sarà sincronizzata con i contenuti.

OBBLIGATORIO: fornisci un comportamento di riserva per i browser senza supporto, altrimenti la UI collegata non sarà sincronizzata con i contenuti.

SÌ: simula la natura in tempo reale di scrollsnapchanging con un listener di eventi scroll associato a requestAnimationFrame e calcoli della distanza geometrica per determinare il target di aggancio più vicino mentre l'utente scorre attivamente.

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

Il comportamento di riserva geometrico scroll + requestAnimationFrame emula fedelmente il comportamento della previsione di aggancio nativa, inclusa la gestione corretta dello scorrimento programmatico. Poiché funzioni come scrollIntoView attivano naturalmente gli eventi scroll durante la loro esecuzione, la UI rimarrà sincronizzata senza problemi durante l'animazione di scorrimento senza richiedere ulteriore logica personalizzata.