Crear el Carrusel Perfecto, Parte 3
() translation by (you can also view the original English article)
Se trata de la tercera y última parte de nuestro crear la serie de tutoriales de carrusel perfecto. En la parte 1, se evaluaron los carruseles en Netflix y Amazon, dos de los carruseles más utilizados en el mundo. Configurar nuestro carrusel y ejecución desplazamiento táctil.
Luego en la parte 2, hemos añadido scroll horizontal del ratón, paginación y un indicador de progreso. Boom.
Ahora, en la parte final, vamos a ver en el mundo turbio y oft-olvidado de accesibilidad del teclado. Ajustaremos nuestro código para que se vuelva a medir el carrusel cuando cambia el tamaño del viewport. Y por último, veremos algunos toques con resorte física.
Usted puede recoger donde nos quedamos con este CodePen.
Accesibilidad del Teclado
Es cierto que la mayoría de los usuarios no dependan de navegación mediante el teclado, tan tristemente a veces olvidamos acerca de nuestros usuarios que lo hacen. En algunos países puede ser ilegal salir de un sitio web inaccesible. Pero lo que es peor, un movimiento de dick.
La buena noticia es que es generalmente fácil de implementar! De hecho, los navegadores hacen la mayoría del trabajo para nosotros. En serio: trate de tabulación mediante el carrusel que hemos hecho. Porque hemos usado marcado semántico, puede ya!
Excepto usted notará, desaparecen los botones de navegación. Esto es porque el navegador no permite el enfoque en un elemento fuera de nuestra vista. Así que aunque tengamos overflow: hidden
conjunto oculto, somos incapaces de desplazarse a la página horizontalmente; de lo contrario, la página de hecho se desplazará para mostrar el elemento con foco.
Esto está bien, y calificaría, en mi opinión, como "útil", aunque no es exactamente agradable.
Carrusel de Netflix también funciona de esta manera. Pero porque la mayoría de sus títulos son cargadas de perezoso, y también son pasivamente teclado accesible (es decir que no han escrito ningún código específicamente para manejar), no podemos realmente seleccionamos cualquier títulos más allá de los pocos que ya hemos cargado. También se ve terrible:



Nosotros podemos hacer mejor.
Manejar el focus
evento
Para ello, vamos a escuchar el evento focus
que se activa en cualquier item en el carrusel. Cuando un elemento recibe el foco, vamos a consultar para su posición. Entonces, te comprobamos contra sliderX
y sliderVisibleWidth
para ver si ese elemento está dentro de la ventana visible. Si no es así, tenemos a paginas a él usando el mismo código que escribió en la parte 2.
Al final de la función de carrusel
, añadir este detector de eventos:
1 |
slider.addEventListener('focus', onFocus, true); |
Usted notará que hemos proporcionado un tercer parámetro, true
. En lugar de añadir un detector de eventos para cada elemento, podemos utilizar lo que se conoce como delegación del evento para escuchar los eventos de un elemento, su padre directo. El evento de focus
no burbuja, tan cierto
dice el detector de eventos para la captura de etapa, la etapa donde el evento se desencadena en cada elemento de la ventana
a través del destino (en este caso, el elemento recibe el foco).
Por encima de nuestro bloque creciente de detectores de eventos, agregue la función onFocus
:
1 |
function onFocus(e) { |
2 |
}
|
Vamos a trabajar en esta función por el resto de esta sección.
Tenemos que medir el elemento desplazamiento izquierda
y derecha
y comprobar que si bien el punto se encuentra fuera del área actualmente visible.
El artículo es proporcionado por el parámetro de target
del evento, y podemos medir con getBoundingClientRect
:
1 |
const { left, right } = e.target.getBoundingClientRect(); |
left
y right
son relativas a la viewport, no el regulador. Así que tenemos que obtener desplazamiento left
del contenedor de carrusel para tener en cuenta. En nuestro ejemplo, se trata de 0
, pero para hacer el carrusel sea sólido, debe registrar para ser colocado en cualquier lugar.
1 |
const carouselLeft = container.getBoundingClientRect().left; |
Ahora, podemos hacer un simple control para ver si el elemento es visible fuera de la barra deslizante y paginas en esa dirección:
1 |
if (left < carouselLeft) { |
2 |
gotoPrev(); |
3 |
} else if (right > carouselLeft + sliderVisibleWidth) { |
4 |
gotoNext(); |
5 |
}
|
Ahora, cuando ficha alrededor, el carrusel con confianza pagina alrededor con nuestro enfoque de teclado! Unas pocas líneas de código para mostrar más amor a nuestros usuarios.
Vuelva a Medir el Carrusel
Usted puede haber notado que sigas este tutorial que si cambia el tamaño de su ventana de navegador, el carrusel no paginas correctamente más. Esto es porque hemos medido su ancho en relación con su área visible sólo una vez, en el momento de la inicialización.
Para asegurarse de que el carrusel se comporta correctamente, debemos reemplazar nuestro código medida con un detector de eventos nuevos que se activa cuando cambia el tamaño de la ventana
.
Ahora, cerca del comienzo de la función de carousel
, justo después de la línea donde definimos progressBar
, queremos sustituir tres de estas mediciones const
con let
, porque vamos a cambiar cuando cambia la vista:
1 |
const totalItemsWidth = getTotalItemsWidth(items); |
2 |
const maxXOffset = 0; |
3 |
|
4 |
let minXOffset = 0; |
5 |
let sliderVisibleWidth = 0; |
6 |
let clampXOffset; |
Entonces, podemos mover la lógica previamente calculado estos valores a una nueva función de measureCarousel
:
1 |
function measureCarousel() { |
2 |
sliderVisibleWidth = slider.offsetWidth; |
3 |
minXOffset = - (totalItemsWidth - sliderVisibleWidth); |
4 |
clampXOffset = clamp(minXOffset, maxXOffset); |
5 |
}
|
Queremos invocar inmediatamente esta función así que sigue estos valores de inicialización. En la siguiente línea, llamar al measureCarousel
:
1 |
measureCarousel(); |
El carrusel debería funcionar exactamente igual que antes. Para actualizar en cambiar el tamaño de ventana, simplemente añadimos este detector de eventos al final de nuestra función de carrusel
:
1 |
window.addEventListener('resize', measureCarousel); |
Ahora, si cambiar el tamaño del carrusel y trate de paginación, a seguir trabajando como se esperaba.
Una Nota Sobre el Rendimiento
Merece la pena teniendo en cuenta que en el mundo real, usted podría tener múltiples carruseles en la misma página, multiplicando el impacto en el rendimiento de este código medida por esa cantidad.
Como ya comentamos brevemente en la parte 2, es prudente realizar cálculos pesados más de lo que usted debe. Con puntero y eventos de desplazamiento, nos dijo que desea realizar ésos una vez por fotograma para ayudar a mantener los 60fps. Eventos de cambio de tamaño son un poco diferentes en que todo el documento será reflujo, probablemente el momento de mayor consumo de recursos que encontrará una página web.
No necesitamos que vuelva a medir el carrusel hasta que el usuario haya terminado de cambiar el tamaño de la ventana, porque no interactúan con él mientras tanto. Podemos envolver nuestra función measureCarousel
en una función especial llamada un debounce.
Una función debounce esencialmente dice: «sólo activará esta función cuando no ha sido llamado en sobre x
milisegundos. " Puedes leer más sobre debounce en primer excelente de David Walsh y recoger algún código de ejemplo también.
Toques Finales
Hasta ahora, hemos creado un carrusel bastante bueno. Es accesible, anima bien, funciona a través de táctil y ratón, y proporciona una gran flexibilidad de diseño de manera que nativamente desplazamiento carruseles no permiten.
Pero esto no es la serie de tutoriales de "Crear un muy buen carrusel". Es hora de lucir un poco y hacer que, tenemos un arma secreta. Resortes.
Vamos a añadir dos interacciones mediante resortes. Uno para el tacto y otro para la paginación. Ambos van a permitir al usuario conocer, en una forma divertida y lúdica, que ha llegado al final del carrusel.
Resorte de Contacto
En primer lugar, vamos a añadir un remolcador estilo iOS cuando un usuario de tratar de desplazarse el cursor más allá de sus límites. Actualmente, estamos tapado toque Desplácese utilizando clampXOffset
. En cambio, vamos a reemplazar esto con algún código que se aplica un tirón cuando el desplazamiento calculado está fuera de sus fronteras.
En primer lugar, tenemos que importar nuestra spring. Hay un transformador llamado nonlinearSpring
que se aplica una fuerza exponencial creciente contra el número ofrecemos, hacia un origen
. Que significa que más que tirar el slider, más a tirar de nuevo. Podemos importarlo como esta:
1 |
const { applyOffset, clamp, nonlinearSpring, pipe } = transform; |
En la función de determineDragDirection
, tenemos este código:
1 |
action.output(pipe( |
2 |
({ x }) => x, |
3 |
applyOffset(action.x.get(), sliderX.get()), |
4 |
clampXOffset, |
5 |
(v) => sliderX.set(v) |
6 |
));
|
Justo por encima de él, vamos a crear nuestros dos resortes, uno para cada límite de desplazamiento del carrusel:
1 |
const elasticity = 5; |
2 |
const tugLeft = nonlinearSpring(elasticity, maxXOffset); |
3 |
const tugRight = nonlinearSpring(elasticity, minXOffset); |
Decidirse por un valor de elasticity
trata de jugando y viendo lo que se siente derecho. Un número demasiado bajo y la primavera se siente demasiado duro. Demasiado alto y usted no lo notará su remolcador, o peor, te empuje el cursor más allá del dedo del usuario!
Ahora solo necesitamos escribir una simple función que uno de estos springs se aplicará si el valor proporcionado es fuera del rango permitido:
1 |
const applySpring = (v) => { |
2 |
if (v > maxXOffset) return tugLeft(v); |
3 |
if (v < minXOffset) return tugRight(v); |
4 |
return v; |
5 |
};
|
ClampXOffset
en el código anterior podemos reemplazar con applySpring
. Ahora, si tira el cursor más allá de sus límites, te tire atrás!
Sin embargo, cuando suelta de la primavera, se tipo de enganche sin contemplaciones hacia atrás. Queremos modificar nuestra función stopTouchScroll
, que maneja actualmente desplazamiento de impulso, para comprobar si el deslizador está todavía fuera del rango permitido y, si es así, aplique una fuente con la acción física
en su lugar.
Física del Resorte
La acción física
es capaz de modelar resortes, también. Sólo necesitamos darle con spring
y para
propiedades.
En stopTouchScroll
, mover la inicialización de física
de desplazamiento existente a un pedazo de lógica que asegura que estamos dentro de los límites de desplazamiento:
1 |
const currentX = sliderX.get(); |
2 |
|
3 |
if (currentX < minXOffset || currentX > maxXOffset) { |
4 |
|
5 |
} else { |
6 |
action = physics({ |
7 |
from: currentX, |
8 |
velocity: sliderX.getVelocity(), |
9 |
friction: 0.2 |
10 |
}).output(pipe( |
11 |
clampXOffset, |
12 |
(v) => sliderX.set(v) |
13 |
)).start(); |
14 |
}
|
Dentro de la primera cláusula de if
comunicado, sabemos que el regulador es fuera de los límites de desplazamiento, por lo que podemos añadir nuestra primavera:
1 |
action = physics({ |
2 |
from: currentX, |
3 |
to: (currentX < minXOffset) ? minXOffset : maxXOffset, |
4 |
spring: 800, |
5 |
friction: 0.92 |
6 |
}).output((v) => sliderX.set(v)) |
7 |
.start(); |
Queremos crear un resorte que se siente ágil y sensible. He elegido un valor relativamente alto string
el tener un apretado "pop", y he bajado la fricción
a 0,92
para permitir que rebote un poco. Podría definir esto en 1
para eliminar totalmente el rebote.
Como un poco de tarea, trate de cambiar el clampXOffset
en la función de output
de la física
de desplazamiento con una función que activa un resorte similar cuando el desplazamiento de x alcanza sus límites. En lugar de la actual parada brusca, trate de hacerla rebotar suavemente en el extremo.
Spring Paginación
¿Los usuarios de touch siempre Obtén la bondad de spring justa? Vamos a compartir ese amor a los usuarios de escritorio cuando el carrusel está en sus límites de desplazamiento, y un remolcador indicativo claramente y con confianza mostrar al usuario que están al final.
En primer lugar, queremos deshabilitar los botones de paginación cuando de ha alcanzado el límite. Vamos a primero agregar una regla CSS que estilos de los botones para mostrar que está desabilitado
. En el estado de botón
, agregue:
1 |
transition: background 200ms linear; |
2 |
|
3 |
&.disabled { |
4 |
background: #eee; |
5 |
}
|
Estamos usando una clase aquí en vez del atributo disabled
más semántico porque todavía queremos capturar eventos de clic, que, como su nombre indica, disabled
bloquearía.
Añadir que esta disabled
clase al botón anterior, porque cada carrusel comienza la vida con un desplazamiento de 0
:
1 |
<button class="prev disabled">Prev</button> |
Hacia la parte superior del carrusel
, hacer una nueva función llamada checkNavButtonStatus
. Queremos que esta función simplemente comprobar el valor proporcionado contra minXOffset
y maxXOffset
y definir la clase de botón disabled
por consiguiente:
1 |
function checkNavButtonStatus(x) { |
2 |
if (x <= minXOffset) { |
3 |
nextButton.classList.add('disabled'); |
4 |
} else { |
5 |
nextButton.classList.remove('disabled'); |
6 |
|
7 |
if (x >= maxXOffset) { |
8 |
prevButton.classList.add('disabled'); |
9 |
} else { |
10 |
prevButton.classList.remove('disabled'); |
11 |
}
|
12 |
}
|
13 |
}
|
Sería tentador llamar a esto cada vez cambia sliderX
. Si lo hicimos, los botones parpadeo cada vez que un resorte oscila alrededor de los límites de desplazamiento. También daría lugar a comportamiento extraño si uno de los botones se ha pulsado durante una de esas animaciones de primavera. El remolcador "scroll end" debe siempre fuego si estamos al final del carrusel, aunque hay una animación de resorte tirando del extremo del absoluto.
Así que tenemos que ser más selectivo acerca de cuándo llamar a esta función. Parece sensato llamarlo:
En la última línea de la onWheel
, añadir checkNavButtonStatus(newX);
.
En la última línea de goto
, añadir checkNavButtonStatus(targetX);
.
Y por último, el final de determineDragDirection
y en la cláusula de desplazamiento del impulso (el código dentro de la otra
) de stopTouchScroll
, reemplace:
1 |
(v) => sliderX.set(v) |
Con:
1 |
(v) => { |
2 |
sliderX.set(v); |
3 |
checkNavButtonStatus(v); |
4 |
} |
Ahora todo lo que queda es modificar gotoPrev
y gotoNext
classList de su botón disparo por disabled
y paginas solamente si está ausente:
1 |
const gotoNext = (e) => !e.target.classList.contains('disabled') |
2 |
? goto(1) |
3 |
: notifyEnd(-1, maxXOffset); |
4 |
|
5 |
const gotoPrev = (e) => !e.target.classList.contains('disabled') |
6 |
? goto(-1) |
7 |
: notifyEnd(1, minXOffset); |
La función notifyEnd
es un resorte de la física
, y se ve como esto:
1 |
function notifyEnd(delta, targetOffset) { |
2 |
if (action) action.stop(); |
3 |
action = physics({ |
4 |
from: sliderX.get(), |
5 |
to: targetOffset, |
6 |
velocity: 2000 * delta, |
7 |
spring: 300, |
8 |
friction: 0.9 |
9 |
}) |
10 |
.output((v) => sliderX.set(v)) |
11 |
.start(); |
12 |
} |
Tienen un juego con eso y otra vez, ajustar los parámetros de la física
a tu gusto.
Hay un pequeño bug de izquierda. Cuando el deslizador resortes más allá de su límite más a la izquierda, la barra de progreso está siendo invertida. Rápidamente podemos corregirlo sustituyendo:
1 |
progressBarRenderer.set('scaleX', progress); |
Con:
1 |
progressBarRenderer.set('scaleX', Math.max(progress, 0)); |
Nos podríamos evitar que rebote lo contrario, pero personalmente creo que es bastante fresco que refleja el movimiento de la primavera. Sólo parece extraño cuando lanza adentro hacia fuera.
Limpiar Después Usted Mismo
Con una página de aplicaciones, sitios web es duran más en la sesión de un usuario. A menudo, incluso cuando se cambia la "página", nos estamos sigue funcionando el mismo tiempo de JS ejecución como en la carga inicial. No podemos confiar en una pizarra limpia cada vez que el usuario hace clic en un vínculo, y eso significa que tenemos que limpiar después de nosotros mismos para evitar escuchas de eventos disparando contra elementos muertos.
En reaccionar, se coloca este código en el método componentWillLeave
. Vue usa beforeDestroy
. Se trata de una implementación pura de JS, pero todavía podemos proporcionar un método de destruir que funcionara igualmente en cualquier marco.
Hasta el momento, nuestra función de carousel
no ha devuelto nada. Vamos a cambiar eso.
En primer lugar, cambie la línea final, la línea de ese carousel
de llamadas para:
1 |
const destroyCarousel = carousel(document.querySelector('.container')); |
Vamos a volver a una cosa de carousel
, una función que desenlaza todos nuestros detectores de eventos. Al final de la función de carousel
, escribe:
1 |
return () => { |
2 |
container.removeEventListener('touchstart', startTouchScroll); |
3 |
container.removeEventListener('wheel', onWheel); |
4 |
nextButton.removeEventListener('click', gotoNext); |
5 |
prevButton.removeEventListener('click', gotoPrev); |
6 |
slider.removeEventListener('focus', onFocus); |
7 |
window.removeEventListener('resize', measureCarousel); |
8 |
};
|
Ahora bien, si llama a destroyCarousel
y tratar de jugar con el carrusel, no pasa nada! Es casi un poco triste ver como esta.
Y Eso es todo
¡UF. Eso fue mucho! Hasta dónde hemos llegado. Se puede ver el producto terminado en este CodePen. En esta última parte, hemos añadido accesibilidad del teclado, ahora el carrusel cuando cambia la vista, diversión algunas adiciones con física de primavera y el desgarradora pero necesario paso de rasgado todo abajo otra vez.
Espero que hayas disfrutado este tutorial tanto como yo disfruté escribiéndolo. Me encantaría escuchar sus pensamientos en otras maneras podríamos mejorar la accesibilidad, o agregar que más diversión poco toca.