javascript - Evento para detectar cuándo se activa position:sticky
Estoy usando el nuevo position: sticky (información) para crear una lista de contenido similar a iOS.
Funciona bien y es muy superior a la alternativa anterior en JavaScript (por ejemplo), sin embargo, hasta donde sé, no se dispara ningún evento cuando se activa, lo que significa que no puedo hacer nada cuando la barra llega a la parte superior de la página, a diferencia de la solución anterior.
Me gustaría agregar una clase (por ejemplo, stuck) cuando un elemento con position: sticky llegue a la parte superior de la página. ¿Hay alguna manera de escuchar esto con JavaScript? El uso de jQuery está bien.
Respuesta #1
Demostración con IntersectionObserver (usa un truco):
// get the sticky element
const stickyElm = document.querySelector('header')
const observer = new IntersectionObserver(
([e]) => e.target.classList.toggle('isSticky', e.intersectionRatio < 1),
{threshold: [1]}
);
observer.observe(stickyElm)body{ height: 200vh; font:20px Arial; }
section{
background: lightblue;
padding: 2em 1em;
}
header{
position: sticky;
top: -1px; /* ➜ the trick */
padding: 1em;
padding-top: calc(1em + 1px); /* ➜ compensate for the trick */
background: salmon;
transition: .1s;
}
/* styles for when the header is in sticky mode */
header.isSticky{
font-size: .8em;
opacity: .5;
}<section>Space</section>
<header>Sticky Header</header>El valor de top debe ser -1px o el elemento nunca intersectará con la parte superior de la ventana del navegador (y, por lo tanto, nunca activará el observador de intersección).
Para contrarrestar este 1px de contenido oculto, se debe agregar un 1px adicional de espacio ya sea al borde o al relleno del elemento sticky.
💡 Alternativamente, si deseas mantener el CSS tal como está (top:0), entonces puedes aplicar la "corrección" a nivel del intersection observer agregando la configuración rootMargin: '-1px 0px 0px 0px' (como @mattrick mostró en su respuesta)
Demostración con el antiguo listener de eventos scroll:
- detección automática del primer elemento desplazable padre
- Limitando el evento de desplazamiento
- Composición funcional para la separación de preocupaciones
- Caché de devolución de llamada de evento:
scrollCallback(para poder desvincular si es necesario)
// get the sticky element
const stickyElm = document.querySelector('header');
// get the first parent element which is scrollable
const stickyElmScrollableParent = getScrollParent(stickyElm);
// save the original offsetTop. when this changes, it means stickiness has begun.
stickyElm._originalOffsetTop = stickyElm.offsetTop;
// compare previous scrollTop to current one
const detectStickiness = (elm, cb) => () => cb & cb(elm.offsetTop != elm._originalOffsetTop)
// Act if sticky or not
const onSticky = isSticky => {
console.clear()
console.log(isSticky)
stickyElm.classList.toggle('isSticky', isSticky)
}
// bind a scroll event listener on the scrollable parent (whatever it is)
// in this exmaple I am throttling the "scroll" event for performance reasons.
// I also use functional composition to diffrentiate between the detection function and
// the function which acts uppon the detected information (stickiness)
const scrollCallback = throttle(detectStickiness(stickyElm, onSticky), 100)
stickyElmScrollableParent.addEventListener('scroll', scrollCallback)
// OPTIONAL CODE BELOW ///////////////////
// find-first-scrollable-parent
// Credit: https://stackoverflow.com/a/42543908/104380
function getScrollParent(element, includeHidden) {
var style = getComputedStyle(element),
excludeStaticParent = style.position === "absolute",
overflowRegex = includeHidden ? /(auto|scroll|hidden)/ : /(auto|scroll)/;
if (style.position !== "fixed")
for (var parent = element; (parent = parent.parentElement); ){
style = getComputedStyle(parent);
if (excludeStaticParent && style.position === "static")
continue;
if (overflowRegex.test(style.overflow + style.overflowY + style.overflowX))
return parent;
}
return window
}
// Throttle
// Credit: https://jsfiddle.net/jonathansampson/m7G64
function throttle (callback, limit) {
var wait = false; // Initially, we're not waiting
return function () { // We return a throttled function
if (!wait) { // If we're not waiting
callback.call(); // Execute users function
wait = true; // Prevent future invocations
setTimeout(function () { // After a period of time
wait = false; // And allow future invocations
}, limit);
}
}
}header{
position: sticky;
top: 0;
/* not important styles */
background: salmon;
padding: 1em;
transition: .1s;
}
header.isSticky{
/* styles for when the header is in sticky mode */
font-size: .8em;
opacity: .5;
}
/* not important styles*/
body{ height: 200vh; font:20px Arial; }
section{
background: lightblue;
padding: 2em 1em;
}<section>Space</section>
<header>Sticky Header</header>Aquí hay una demostración de un componente de React que utiliza la primera técnica
Respuesta #2
Encontré una solución algo similar a la respuesta de @vsync, pero no requiere el "truco" que necesitas agregar a tus hojas de estilo. Simplemente puedes cambiar los límites del IntersectionObserver para evitar tener que mover el propio elemento fuera del área visible:
const observer = new IntersectionObserver(callback, {
rootMargin: '-1px 0px 0px 0px',
threshold: [1],
});
observer.observe(element);
Respuesta #3
Puedes hacer esto muy fácilmente de la siguiente manera:
- Adjuntar un listener de evento de desplazamiento
- Obtén la posición
topsticky deseada del elemento mediantegetComputedStyle() - Obtén el desplazamiento actual de la ventana gráfica del elemento usando
getBoundingClientRect() - Agrega la clase si ambos valores coinciden usando
classList.toggle()
Así es como se ve en el código:
const el = document.querySelector(".my-sticky-element");
window.addEventListener("scroll", () => {
const stickyTop = parseInt(window.getComputedStyle(el).top);
const currentTop = el.getBoundingClientRect().top;
el.classList.toggle("is-sticky", currentTop === stickyTop);
});
