Descripción general
Los usuarios esperan comentarios visuales inmediatos cuando interactúan con elementos de la IU, como carruseles o galerías. El ajuste de desplazamiento tradicional solo proporciona comentarios después de que se completa el gesto de desplazamiento y se asienta el elemento. Con los eventos de ajuste de desplazamiento, específicamente scrollsnapchanging, puedes proporcionar comentarios en tiempo real durante el gesto de desplazamiento, lo que destaca el objetivo de ajuste pendiente antes de que el usuario suelte el toque o el mouse.
Implementación
1. Escuchar scrollsnapchanging
Adjunta un objeto de escucha de eventos para scrollsnapchanging al contenedor de desplazamiento. Este evento se activa cuando el navegador determina que es probable que se seleccione un nuevo objetivo de ajuste.
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');
});
En este ejemplo, se usa snapTargetInline porque la galería se desplaza de forma horizontal. Si tu contenedor de desplazamiento se desplaza verticalmente, usa snapTargetBlock en su lugar.
2. Escuchar scrollsnapchange
Para finalizar el estado cuando se completa el gesto de desplazamiento y el elemento se ajusta, escucha el evento scrollsnapchange. Esto es necesario para establecer el estado activo final.
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. Estado inicial de la sincronización
Cuando se carga la página, el navegador puede restablecer la posición de desplazamiento (p.ej., a través del recorrido del historial o un vínculo de anclaje). Ni los eventos scrollsnapchange ni los eventos scroll se activarán automáticamente. Ejecuta una verificación geométrica única para sincronizar la IU con la posición de desplazamiento inicial.
// 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 });
}
Estrategias de resguardo
En el caso de los navegadores que no admiten scrollsnapchanging, la IU no proporcionará comentarios inmediatos durante el gesto de desplazamiento de forma predeterminada, y la IU vinculada se desincronizará del contenido.
OBLIGATORIO: Proporciona un resguardo para los navegadores sin compatibilidad; de lo contrario, la IU vinculada se desincronizará del contenido.
SÍ simula la naturaleza en tiempo real de scrollsnapchanging con un objeto de escucha de eventos scroll junto con requestAnimationFrame y cálculos de distancia geométrica para determinar el objetivo de ajuste más cercano mientras el usuario se desplaza de forma activa.
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();
});
}
El resguardo geométrico scroll + requestAnimationFrame emula de cerca el comportamiento de la predicción de ajuste nativa, incluido el manejo correcto del desplazamiento programático. Dado que las funciones como scrollIntoView activan de forma natural eventos de scroll durante su ejecución, la IU se mantendrá sincronizada sin problemas durante la animación de desplazamiento sin necesidad de lógica personalizada adicional.