Crea el carrusel perfecto, parte 3

Esta es la tercera y última parte de nuestra serie de tutoriales Crear el carrusel perfecto. En la parte 1, evaluamos los carruseles en Netflix y Amazon, dos de los carruseles más utilizados del mundo. Instalamos nuestro carrusel e implementamos touch scroll..

Luego, en la parte 2, agregamos el desplazamiento horizontal del mouse, la paginación y un indicador de progreso. Auge.

Ahora, en nuestra parte final, vamos a investigar el mundo turbio y a menudo olvidado de la accesibilidad del teclado. Ajustaremos nuestro código para volver a medir el carrusel cuando cambie el tamaño de la ventana gráfica. Y, por último, haremos algunos toques finales utilizando la física de primavera..

Puedes continuar donde lo dejamos con este CodePen.

Accesibilidad del teclado

Es cierto que la mayoría de los usuarios no dependen de la navegación con el teclado, por lo que, lamentablemente, a veces nos olvidamos de los usuarios que sí lo hacen. En algunos países, dejar un sitio web inaccesible puede ser ilegal. Pero peor, es un movimiento de pene..

La buena noticia es que generalmente es fácil de implementar. De hecho, los navegadores hacen la mayor parte del trabajo por nosotros. En serio: intenta hojear el carrusel que hemos hecho. Porque hemos usado el marcado semántico, ya puedes!

Excepto, te darás cuenta, nuestros botones de navegación desaparecen. Esto se debe a que el navegador no permite enfocar un elemento fuera de nuestra ventana gráfica. Así que a pesar de que tenemos desbordamiento: oculto conjunto, no podemos desplazar la página horizontalmente; de lo contrario, la página se desplazará para mostrar el elemento enfocado..

Esto está bien, y calificaría, en mi opinión, como "reparable", aunque no exactamente encantador.

El carrusel de Netflix también funciona de esta manera. Pero como la mayoría de sus títulos están cargados perezosamente, y también son pasivamente accesibles desde el teclado (lo que significa que no han escrito ningún código específicamente para manejarlo), no podemos seleccionar ningún título más allá de los pocos que hemos escrito. ya cargado. También se ve terrible: 

Podemos hacerlo mejor.

Manejar el atención Evento

Para ello, vamos a escuchar el atención Evento que dispara sobre cualquier elemento del carrusel. Cuando un elemento recibe el foco, lo vamos a consultar por su posición. Entonces, lo comprobaremos contra sliderX y sliderVisibleWidth para ver si ese elemento está dentro de la ventana visible. Si no lo es, lo paginaremos usando el mismo código que escribimos en la parte 2.

Al final de carrusel función, añadir este detector de eventos:

slider.addEventListener ('focus', onFocus, true);

Notarás que hemos proporcionado un tercer parámetro., cierto. En lugar de agregar un detector de eventos a cada elemento, podemos usar lo que se conoce como delegación de eventos para escuchar eventos en un solo elemento, su padre directo. los atención el evento no burbujea, entonces cierto está diciendo al oyente del evento que escuche la capturar etapa, la etapa donde el evento se dispara en cada elemento de la ventana hasta el objetivo (en este caso, el elemento que recibe el foco).

Por encima de nuestro creciente bloque de oyentes de eventos, agregue el enfocado función:

función onFocus (e) 

Estaremos trabajando en esta función por el resto de esta sección..

Necesitamos medir el artículo izquierda y Correcto compensar y verificar si alguno de los puntos se encuentra fuera del área actualmente visible.

El artículo es proporcionado por el evento objetivo parámetro, y podemos medirlo con getBoundingClientRect

const left, right = e.target.getBoundingClientRect ();

izquierdaCorrecto son relativos a la vista, no el control deslizante Así que tenemos que conseguir el contenedor del carrusel izquierda compensación para tener en cuenta para eso. En nuestro ejemplo, esto será 0, pero para hacer que el carrusel sea robusto, debe ser colocado en cualquier lugar.

const carouselLeft = container.getBoundingClientRect (). left;

Ahora, podemos hacer una simple comprobación para ver si el elemento está fuera del área visible del control deslizante y paginar en esa dirección:

si < carouselLeft)  gotoPrev();  else if (right > carouselLeft + sliderVisibleWidth) gotoNext (); 

Ahora, cuando pasamos por ahí, ¡el carrusel se mueve con confianza con nuestro enfoque del teclado! Solo unas pocas líneas de código para mostrar más amor a nuestros usuarios..

Volver a medir el carrusel

Es posible que haya notado al seguir este tutorial que si cambia el tamaño de la ventana de visualización del navegador, el carrusel ya no se pagina correctamente. Esto se debe a que medimos su ancho en relación con su área visible solo una vez, en el momento de la inicialización..

Para asegurarnos de que nuestro carrusel se comporta correctamente, debemos reemplazar parte de nuestro código de medición con un nuevo detector de eventos que se activa cuando ventana redimensiona.

Ahora, cerca del comienzo de tu carrusel Función, justo después de la línea donde definimos. barra de progreso, queremos reemplazar tres de estos const mediciones con dejar, porque los vamos a cambiar cuando cambie la ventana gráfica:

const totalItemsWidth = getTotalItemsWidth (items); const maxXOffset = 0; deja minXOffset = 0; deja sliderVisibleWidth = 0; vamos a clampXOffset;

Luego, podemos mover la lógica que previamente calculó estos valores a una nueva medida de carrusel función:

function measureCarousel () sliderVisibleWidth = slider.offsetWidth; minXOffset = - (totalItemsWidth - sliderVisibleWidth); clampXOffset = pinza (minXOffset, maxXOffset); 

Queremos invocar inmediatamente esta función, por lo que aún establecemos estos valores en la inicialización. En la siguiente línea, llame medida de carrusel:

measureCarousel ();

El carrusel debería funcionar exactamente como antes. Para actualizar el tamaño de la ventana, simplemente agregamos este detector de eventos al final de nuestro carrusel función:

window.addEventListener ('resize', measureCarousel);

Ahora, si cambia el tamaño del carrusel y trata de paginar, seguirá funcionando como se esperaba..

Una nota sobre el rendimiento

Vale la pena considerar que, en el mundo real, es posible que tenga varios carruseles en la misma página, multiplicando el impacto en el rendimiento de este código de medición por esa cantidad.

Como explicamos brevemente en la parte 2, no es prudente realizar cálculos pesados ​​más a menudo de lo que debe. Con los eventos de puntero y desplazamiento, dijimos que desea realizarlos una vez por fotograma para ayudar a mantener 60 fps. Los eventos de cambio de tamaño son un poco diferentes, ya que todo el documento tendrá un nuevo flujo, probablemente el momento más intensivo de recursos que una página web encontrará..

No necesitamos volver a medir el carrusel hasta que el usuario haya terminado de cambiar el tamaño de la ventana, porque mientras tanto no interactuarán con él. Podemos envolver nuestro medida de carrusel función en una función especial llamada rebotar.

Una función de rebote esencialmente dice: "Solo active esta función cuando no se haya llamado en over X milisegundos. "Puede leer más sobre la publicación de rebotes en el excelente manual de David Walsh, y también puede obtener un código de ejemplo..

Últimos retoques

Hasta ahora, hemos creado un carrusel bastante bueno. Es accesible, anima muy bien, funciona con el tacto y el mouse, y proporciona una gran flexibilidad de diseño de una manera que los carruseles de desplazamiento nativo no permiten.

Pero esta no es la serie de tutoriales "Crear un carrusel bastante bueno". Es hora de que nos mostremos un poco, y para hacer eso, tenemos un arma secreta. muelles.

Vamos a agregar dos interacciones usando resortes. Uno para el tacto, y otro para la paginación. Ambos le harán saber al usuario, de una manera divertida y lúdica, que han llegado al final del carrusel..

Tocar la primavera

Primero, agreguemos un tirón estilo iOS cuando un usuario intenta desplazar el control deslizante más allá de sus límites. Actualmente, estamos limitando el desplazamiento táctil usando clampXOffset. En su lugar, reemplazemos esto con algún código que aplique un tirón cuando el desplazamiento calculado está fuera de sus límites.

Primero, necesitamos importar nuestra primavera. Hay un transformador llamado muelles no lineales que aplica una fuerza exponencialmente creciente contra el número que le proporcionamos, hacia una origen. Lo que significa que cuanto más jalemos el control deslizante, más tirará hacia atrás. Podemos importarlo así:

const applyOffset, clamp, nonlinearSpring, pipe = transform;

En el determinarDragDirection Función, tenemos este código:

action.output (pipe ((x) => x, applyOffset (action.x.get (), sliderX.get ()), clampXOffset, (v) => sliderX.set (v)));

Justo arriba, creemos nuestros dos resortes, uno para cada límite de desplazamiento del carrusel:

elasticidad const = 5; const tugLeft = nonlinearSpring (elasticity, maxXOffset); const tugRight = nonlinearSpring (elasticity, minXOffset);

Decidir sobre un valor para elasticidad Es una cuestión de jugar y ver lo que se siente bien. Un número demasiado bajo, y el resorte se siente demasiado rígido. Demasiado alto y no notará su tirón, o peor, empujará el deslizador aún más lejos del dedo del usuario!

Ahora solo necesitamos escribir una función simple que aplique uno de estos resortes si el valor suministrado está fuera del rango permitido:

const applySpring = (v) => if (v> maxXOffset) devolver tugLeft (v); si < minXOffset) return tugRight(v); return v; ;

Podemos reemplazar clampXOffset en el código de arriba con applySpring. Ahora, si coloca el control deslizante más allá de sus límites, lo arrastrará hacia atrás!

Sin embargo, cuando dejamos la primavera, se encaja de nuevo sin ceremonias en su lugar. Queremos modificar nuestra stopTouchScroll función, que actualmente maneja el desplazamiento momentum, para verificar si el control deslizante todavía está fuera del rango permitido y, si es así, aplicar un resorte con el física acción en cambio.

Física de primavera

los física La acción es capaz de modelar resortes, también. Solo necesitamos proporcionarlo primaveraa propiedades.

En stopTouchScroll, mover el pergamino existente física Inicialización de una lógica que garantiza que estamos dentro de los límites de desplazamiento:

const currentX = sliderX.get (); si (currentX < minXOffset || currentX > maxXOffset)  else action = physics (from: currentX, speed: sliderX.getVelocity (), friction: 0.2). output (pipe (clampXOffset, (v) => sliderX.set (v))). start (); 

Dentro de la primera cláusula del Si declaración, sabemos que el control deslizante está fuera de los límites de desplazamiento, por lo que podemos agregar nuestro resorte:

action = physics (from: currentX, to: (currentX < minXOffset) ? minXOffset : maxXOffset, spring: 800, friction: 0.92 ).output((v) => sliderX.set (v)) .start ();

Queremos crear un resorte que se sienta ágil y sensible. He elegido un relativamente alto primavera valor para tener un apretado "pop", y he bajado el fricción0.92 Para permitir un pequeño rebote. Podrías poner esto a 1 Para eliminar el rebote por completo..

Como un poco de tarea, trata de reemplazar la clampXOffset en el salida función del pergamino física con una función que activa un resorte similar cuando el desplazamiento x alcanza sus límites. En lugar de la parada abrupta actual, intente hacerlo rebotar suavemente al final.

Paginación de primavera

Los usuarios táctiles siempre obtienen la bondad de la primavera, ¿verdad? Compartamos ese amor con los usuarios de escritorio detectando cuando el carrusel está en sus límites de desplazamiento y dándole un tirón indicativo para mostrar al usuario de manera clara y confiable que está al final..

Primero, queremos deshabilitar los botones de paginación cuando se alcanza el límite. Primero agreguemos una regla CSS que estilice los botones para mostrar que están discapacitado. En el botón regla, añadir:

transición: fondo 200ms lineales; & .disabled background: #eee; 

Estamos usando una clase aquí en lugar de la más semántica. discapacitado atributo porque todavía queremos capturar eventos de clic, lo que, como su nombre lo indica, discapacitado bloquearía.

Agrega esto discapacitado clase al botón Prev, porque cada carrusel comienza la vida con un 0 compensar:

Hacia la cima de carrusel, hacer una nueva función llamada checkNavButtonStatus. Queremos que esta función simplemente verifique el valor proporcionado contra MinXOffsetmaxXOffset y configurar el botón discapacitado clase en consecuencia:

función checkNavButtonStatus (x) si (x <= minXOffset)  nextButton.classList.add('disabled');  else  nextButton.classList.remove('disabled'); if (x >= maxXOffset) prevButton.classList.add ('deshabilitado');  else prevButton.classList.remove ('disabled'); 

Sería tentador llamar esto cada vez. sliderX cambios Si lo hiciéramos, los botones comenzaban a parpadear cada vez que un resorte oscilaba alrededor de los límites del desplazamiento. También conduciría a raro comportamiento si uno de los botones fue presionado durante una de esas animaciones de primavera. El tirón del "final de desplazamiento" siempre debe dispararse si estamos al final del carrusel, incluso si hay una animación de primavera alejándola del extremo absoluto.

Así que tenemos que ser más selectivos sobre cuándo llamar a esta función. Parece sensato llamarlo:

En la última línea de la en la rueda, añadir checkNavButtonStatus (newX);.

En la última línea de ir, añadir checkNavButtonStatus (targetX);.

Y finalmente, al final de determinarDragDirection, y en la cláusula de desplazamiento de impulso (el código dentro del más) de stopTouchScroll, reemplazar:

(v) => sliderX.set (v)

Con:

(v) => sliderX.set (v); checkNavButtonStatus (v); 

Ahora todo lo que queda es enmendar gotoPrevgotoSiguiente para comprobar la lista de clases de su botón disparador discapacitado y solo paginar si está ausente:

const gotoNext = (e) =>! e.target.classList.contains ('disabled')? goto (1): notifyEnd (-1, maxXOffset); const gotoPrev = (e) =>! e.target.classList.contains ('disabled')? goto (-1): notifyEnd (1, minXOffset);

los notificar la función es solo otra física Primavera, y se ve así:

function notifyEnd (delta, targetOffset) if (action) action.stop (); action = physics (from: sliderX.get (), to: targetOffset, velocidad: 2000 * delta, spring: 300, friction: 0.9) .output ((v) => sliderX.set (v)) .start ( ); 

Juega con eso, y otra vez, modifica la física params a tu gusto.

Solo queda un pequeño error. Cuando el control deslizante se desplaza más allá de su límite izquierdo, la barra de progreso se está invirtiendo. Podemos arreglarlo rápidamente reemplazando:

progressBarRenderer.set ('scaleX', progress);

Con:

progressBarRenderer.set ('scaleX', Math.max (progress, 0));

Nosotros podríaevita que rebote hacia el otro lado, pero personalmente creo que es muy bueno que refleje el movimiento del resorte. Solo se ve extraño cuando se voltea de adentro hacia afuera.

Limpiar después de ti mismo

Con las aplicaciones de una sola página, los sitios web duran más tiempo en la sesión de un usuario. A menudo, incluso cuando cambia la "página", todavía estamos ejecutando el mismo tiempo de ejecución JS que en la carga inicial. No podemos confiar en una pizarra limpia cada vez que el usuario hace clic en un enlace, y eso significa que tenemos que limpiarnos para evitar que los oyentes de eventos activen elementos muertos..

En React, este código se coloca en el componenteWillLeave método. Usos de vue antesDestroy. Esta es una implementación JS pura, pero aún podemos proporcionar un método de destrucción que funcionaría por igual en cualquiera de los dos marcos..

Hasta ahora, nuestro carrusel La función no ha devuelto nada. Vamos a cambiar eso.

Primero, cambia la línea final, la línea que llama. carrusel, a:

const destroyCarousel = carrusel (document.querySelector ('. container'));

Vamos a devolver sólo una cosa de carrusel, Una función que desenreda a todos nuestros oyentes de eventos. Al final de la carrusel función, escribir:

return () => container.removeEventListener ('touchstart', startTouchScroll); container.removeEventListener ('wheel', onWheel); nextButton.removeEventListener ('click', gotoNext); prevButton.removeEventListener ('click', gotoPrev); slider.removeEventListener ('focus', onFocus); window.removeEventListener ('resize', measureCarousel); ;

Ahora si llamas destruye Y tratar de jugar con el carrusel, ¡no pasa nada! Es casi un poco triste verlo así..

Y eso es eso

Uf. ¡Eso fue mucho! Cuán lejos hemos llegado. Puedes ver el producto terminado en este CodePen. En esta parte final, hemos agregado la accesibilidad del teclado, volviendo a medir el carrusel cuando cambia la ventana gráfica, algunas adiciones divertidas con la física de primavera y el paso desgarrador pero necesario de volver a arrancarlo todo.

Espero que hayas disfrutado este tutorial tanto como yo disfruté escribiéndolo. Me encantaría escuchar sus opiniones sobre otras formas en que podríamos mejorar la accesibilidad o agregar más toques divertidos..