En este codelab, aprenderás a crear una experiencia como las historias de Instagram. en la Web. Crearemos el componente sobre la marcha, empezando con HTML, luego CSS luego, JavaScript.
Consulta mi entrada de blog Cómo crear un componente de historias para obtener información sobre las mejoras progresivas que se realizan durante la compilación de este componente.
Configuración
- Haz clic en Remix para editar para que el proyecto se pueda editar.
- Abre
app/index.html
.
HTML
Siempre intento usar HTML semántico.
Como cada amigo puede tener muchas historias, pensé que era importante utilizar una
<section>
para cada amigo y un elemento <article>
para cada historia.
Empecemos por el principio. Primero, necesitamos un contenedor para nuestro
de historias.
Agrega un elemento <div>
a tu <body>
:
<div class="stories">
</div>
Agrega algunos elementos <section>
para representar a tus amigos:
<div class="stories">
<section class="user"></section>
<section class="user"></section>
<section class="user"></section>
<section class="user"></section>
</div>
Agrega algunos elementos <article>
para representar las historias:
<div class="stories">
<section class="user">
<article class="story" style="--bg: url(https://picsum.photos/480/840);"></article>
<article class="story" style="--bg: url(https://picsum.photos/480/841);"></article>
</section>
<section class="user">
<article class="story" style="--bg: url(https://picsum.photos/481/840);"></article>
</section>
<section class="user">
<article class="story" style="--bg: url(https://picsum.photos/481/841);"></article>
</section>
<section class="user">
<article class="story" style="--bg: url(https://picsum.photos/482/840);"></article>
<article class="story" style="--bg: url(https://picsum.photos/482/843);"></article>
<article class="story" style="--bg: url(https://picsum.photos/482/844);"></article>
</section>
</div>
- Para crear prototipos de historias, usamos un servicio de imágenes (
picsum.com
). - El atributo
style
en cada<article>
forma parte de la carga de un marcador de posición. de la que obtendrás más información en la siguiente sección.
CSS
Nuestro contenido está preparado para el estilo. Convirtamos esos huesos en algo que hará todo el mundo. con el que quieres interactuar. Hoy estaremos trabajando en la priorización de los dispositivos móviles.
.stories
Para nuestro contenedor <div class="stories">
, queremos un contenedor de desplazamiento horizontal.
Podemos lograrlo de la siguiente manera:
- Convertir al contenedor en una cuadrícula
- Cómo configurar cada elemento secundario para que ocupe la pista de filas
- Hacer que el ancho de cada elemento secundario sea el ancho del viewport de un dispositivo móvil
La cuadrícula seguirá colocando las nuevas columnas de ancho de 100vw
a la derecha de la anterior.
hasta que se coloquen
todos los elementos HTML en el lenguaje de marcado.
![Chrome y Herramientas para desarrolladores se abren con una cuadrícula visual que muestra el diseño de ancho completo](https://web.dev/static/articles/codelab-building-a-stories-component/image/chrome-devtools-open-a-72aaa3c91f873.png?authuser=6&hl=es)
Agrega el siguiente CSS a la parte inferior de app/css/index.css
:
.stories {
display: grid;
grid: 1fr / auto-flow 100%;
gap: 1ch;
}
Ahora que tenemos contenido que se extiende más allá del viewport, es hora de aclarar que
contenedor cómo manejarlo. Agrega las líneas de código destacadas a tu conjunto de reglas .stories
:
.stories {
display: grid;
grid: 1fr / auto-flow 100%;
gap: 1ch;
overflow-x: auto;
scroll-snap-type: x mandatory;
overscroll-behavior: contain;
touch-action: pan-x;
}
Queremos el desplazamiento horizontal, por lo que estableceremos overflow-x
en
auto
Cuando el usuario se desplaza, queremos que el componente descanse suavemente en la siguiente historia,
así que usaremos scroll-snap-type: x mandatory
. Más información
CSS en los puntos de ajuste de desplazamiento de CSS
y el comportamiento de sobredesplazamiento
de mi entrada de blog.
Se necesita tanto el contenedor superior como los secundarios para aceptar el ajuste de desplazamiento.
mandamos eso ahora. Agrega el siguiente código al final de app/css/index.css
:
.user {
scroll-snap-align: start;
scroll-snap-stop: always;
}
Tu app aún no funciona, pero en el siguiente video se muestra lo que sucede cuando
scroll-snap-type
está habilitado y, además, inhabilitado. Cuando está habilitada, cada horizontal
el desplazamiento se pasa a la siguiente historia. Cuando se inhabilita, el navegador utiliza su
comportamiento de desplazamiento predeterminado.
Eso te permitirá desplazarte por tus amigos, pero aún tenemos un problema. con las historias por resolver.
.user
Creemos un diseño en la sección .user
que gestione la historia de ese elemento secundario.
los elementos en su lugar. Para resolver esto, usaremos un útil truco de apilado.
Básicamente, estamos creando una cuadrícula de 1 x 1 donde la fila y la columna tienen la misma cuadrícula
alias de [story]
, y cada elemento de la cuadrícula de la historia intentará reclamar ese espacio,
lo que genera una pila.
Agrega el código destacado a tu conjunto de reglas .user
:
.user {
scroll-snap-align: start;
scroll-snap-stop: always;
display: grid;
grid: [story] 1fr / [story] 1fr;
}
Agrega el siguiente conjunto de reglas al final de app/css/index.css
:
.story {
grid-area: story;
}
Ahora, sin posicionamiento absoluto, números de punto flotante ni otras directivas de diseño que lleven un elemento fuera de flujo, seguimos en el flujo. Además, es casi cualquier código, ¡Mira eso! Esta información se desglosa en más detalle en el video y la entrada de blog.
.story
Ahora solo debemos darle estilo al elemento de la historia.
Anteriormente, mencionamos que el atributo style
de cada elemento <article>
es parte de un
Técnica de carga de marcadores de posición:
<article class="story" style="--bg: url(https://picsum.photos/480/840);"></article>
Usaremos la propiedad background-image
de CSS, que nos permite especificar
más de una imagen de fondo. Podemos ponerlas en un orden para que nuestro usuario
imagen está en la parte superior y aparecerá automáticamente cuando termine de cargarse. Para
Si habilitamos esta función, colocaremos la URL de nuestra imagen en una propiedad personalizada (--bg
) y la usaremos.
dentro de nuestro CSS para aplicar capas
con el marcador de posición de carga.
Primero, actualicemos el conjunto de reglas .story
para reemplazar un gradiente por una imagen de fondo
cuando termine de cargarse. Agrega el código destacado a tu conjunto de reglas .story
:
.story {
grid-area: story;
background-size: cover;
background-image:
var(--bg),
linear-gradient(to top, lch(98 0 0), lch(90 0 0));
}
Configurar background-size
como cover
garantiza que no haya espacio vacío en la
viewport, porque nuestra imagen lo llenará. Define 2 imágenes de fondo
nos permite implementar un interesante truco web de CSS llamado tombstone de carga:
- La imagen de fondo 1 (
var(--bg)
) es la URL que pasamos intercalada en el HTML - Imagen de fondo 2 (
linear-gradient(to top, lch(98 0 0), lch(90 0 0))
es un gradiente para mostrar mientras se carga la URL
CSS reemplazará automáticamente el gradiente por la imagen una vez que esta termine de descargarse.
A continuación, agregaremos un poco de CSS para quitar algo de comportamiento, lo que liberará al navegador para poder moverlo más rápido.
Agrega el código destacado a tu conjunto de reglas .story
:
.story {
grid-area: story;
background-size: cover;
background-image:
var(--bg),
linear-gradient(to top, lch(98 0 0), lch(90 0 0));
user-select: none;
touch-action: manipulation;
}
user-select: none
evita que los usuarios seleccionen texto por errortouch-action: manipulation
le indica al navegador que estas interacciones deben tratarse como eventos táctiles, lo que libera al navegador de intentar decidir si se hace clic en una URL o no
Por último, agreguemos un poco de CSS para animar la transición entre historias. Agrega el
código destacado al conjunto de reglas .story
:
.story {
grid-area: story;
background-size: cover;
background-image:
var(--bg),
linear-gradient(to top, lch(98 0 0), lch(90 0 0));
user-select: none;
touch-action: manipulation;
transition: opacity .3s cubic-bezier(0.4, 0.0, 1, 1);
&.seen {
opacity: 0;
pointer-events: none;
}
}
Se agregará la clase .seen
a una historia que necesita una salida.
Tengo la función de aceleración personalizada (cubic-bezier(0.4, 0.0, 1,1)
)
de la Aceleración de Material Design
(desplázate hasta la sección Aceleración acelerada).
Si tienes un ojo agudo, es probable que hayas notado el pointer-events: none
y te estás estrenando la cabeza. Diría que este es el único
una desventaja de la solución hasta ahora. Lo necesitamos porque un elemento .seen.story
estará en la parte superior y recibirás toques, incluso si es invisible. Si estableces
pointer-events
a none
, convertimos la historia del cristal en una ventana y no robamos
más interacciones de los usuarios. No es una desventaja, no es tan difícil de administrar
en nuestro CSS ahora mismo. No estamos haciendo malabares con z-index
. Me siento bien
quieto.
JavaScript
Las interacciones de un componente de Historias son bastante sencillas para el usuario: presiona el hacia la derecha para avanzar, toca a la izquierda para volver. Las cosas simples para los usuarios es un trabajo arduo para los desarrolladores. Sin embargo, nos encargaremos de muchas cosas.
Configuración
Para empezar, calculemos y almacenemos tanta información como podamos.
Agrega el siguiente código a app/js/index.js
:
const stories = document.querySelector('.stories')
const median = stories.offsetLeft + (stories.clientWidth / 2)
La primera línea de JavaScript toma y almacena una referencia a nuestro código HTML principal. raíz del elemento. La siguiente línea calcula dónde se encuentra el medio de nuestro elemento, así que puede decidir si un toque es hacia adelante o hacia atrás.
Estado
A continuación, haremos un objeto pequeño con un estado relevante para nuestra lógica. En este
caso, solo nos interesa la historia actual. En nuestro lenguaje de marcado HTML, podemos
accede a él captando al primer amigo y su historia más reciente. Agrega el código destacado
a tu app/js/index.js
:
const stories = document.querySelector('.stories')
const median = stories.offsetLeft + (stories.clientWidth / 2)
const state = {
current_story: stories.firstElementChild.lastElementChild
}
Objetos de escucha
Ahora tenemos suficiente lógica para comenzar a escuchar eventos de usuarios y dirigirlos.
Ratón
Para comenzar, escucha el evento 'click'
en nuestro contenedor de historias.
Agrega el código destacado a app/js/index.js
:
const stories = document.querySelector('.stories')
const median = stories.offsetLeft + (stories.clientWidth / 2)
const state = {
current_story: stories.firstElementChild.lastElementChild
}
stories.addEventListener('click', e => {
if (e.target.nodeName !== 'ARTICLE')
return
navigateStories(
e.clientX > median
? 'next'
: 'prev')
})
Si se produce un clic y no está en un elemento <article>
, lo protegemos y no hacemos nada.
Si se trata de un artículo, tomamos la posición horizontal del mouse o del dedo con
clientX
Aún no implementamos navigateStories
, pero el argumento que indica que
que toma, especifica la dirección a la que debemos ir. Si esa posición del usuario es
mayor que la mediana, sabemos que debemos navegar a next
; de lo contrario,
prev
(anterior).
Teclado
Ahora escuchemos cuando presiones el teclado. Si se presiona la flecha hacia abajo, navegamos
a next
. Si es la flecha hacia arriba, vamos a prev
.
Agrega el código destacado a app/js/index.js
:
const stories = document.querySelector('.stories')
const median = stories.offsetLeft + (stories.clientWidth / 2)
const state = {
current_story: stories.firstElementChild.lastElementChild
}
stories.addEventListener('click', e => {
if (e.target.nodeName !== 'ARTICLE')
return
navigateStories(
e.clientX > median
? 'next'
: 'prev')
})
document.addEventListener('keydown', ({key}) => {
if (key !== 'ArrowDown' || key !== 'ArrowUp')
navigateStories(
key === 'ArrowDown'
? 'next'
: 'prev')
})
Navegación por las historias
Es hora de abordar la lógica empresarial única de las historias y la UX en la que se convirtieron. famoso por. Esto se ve grueso y complicado, pero creo que si lo tomas encontrarás que es bastante asimilable.
Por adelantado, guardamos algunos selectores que nos ayudan a decidir si nos desplazamos a un amigo o mostrar/ocultar una historia. Como estamos trabajando con HTML, lo usaremos buscar en él la presencia de amigos (usuarios) o historias (historia).
Estas variables nos ayudarán a responder preguntas como: "Dada la historia x, ¿"siguiente" ¿Qué significa pasar a otra historia de este mismo amigo o de uno diferente?”. Lo hice usando el árbol que construimos, llegando a los padres y sus hijos.
Agrega el siguiente código al final de app/js/index.js
:
const navigateStories = direction => {
const story = state.current_story
const lastItemInUserStory = story.parentNode.firstElementChild
const firstItemInUserStory = story.parentNode.lastElementChild
const hasNextUserStory = story.parentElement.nextElementSibling
const hasPrevUserStory = story.parentElement.previousElementSibling
}
Este es nuestro objetivo de lógica empresarial, lo más parecido posible al lenguaje natural:
- Decide cómo controlar el toque
- Si hay una historia siguiente o anterior: mostrar esa historia
- Si es la última o la primera historia de un amigo: enséñale a un amigo nuevo.
- Si no hay ninguna historia a la cual dirigirse en esa dirección, no hagas nada.
- Guarda la nueva historia actual en
state
Agrega el código destacado a la función navigateStories
:
const navigateStories = direction => {
const story = state.current_story
const lastItemInUserStory = story.parentNode.firstElementChild
const firstItemInUserStory = story.parentNode.lastElementChild
const hasNextUserStory = story.parentElement.nextElementSibling
const hasPrevUserStory = story.parentElement.previousElementSibling
if (direction === 'next') {
if (lastItemInUserStory === story && !hasNextUserStory)
return
else if (lastItemInUserStory === story && hasNextUserStory) {
state.current_story = story.parentElement.nextElementSibling.lastElementChild
story.parentElement.nextElementSibling.scrollIntoView({
behavior: 'smooth'
})
}
else {
story.classList.add('seen')
state.current_story = story.previousElementSibling
}
}
else if(direction === 'prev') {
if (firstItemInUserStory === story && !hasPrevUserStory)
return
else if (firstItemInUserStory === story && hasPrevUserStory) {
state.current_story = story.parentElement.previousElementSibling.firstElementChild
story.parentElement.previousElementSibling.scrollIntoView({
behavior: 'smooth'
})
}
else {
story.nextElementSibling.classList.remove('seen')
state.current_story = story.nextElementSibling
}
}
}
Probar
- Para obtener una vista previa del sitio, presiona Ver app. Luego, presiona
Pantalla completa
Conclusión
Eso fue una conclusión para las necesidades que tenía con el componente. Siéntete libre de desarrollar impulsarlo con datos y, en general, personalizarlo.