Cet atelier de programmation vous explique comment créer une expérience comme les stories Instagram. sur le Web. Nous créerons le composant au fur et à mesure, en commençant par HTML, puis CSS, puis JavaScript.
Consultez mon article de blog Créer un composant Stories. pour en savoir plus sur les améliorations progressives apportées lors de la création de ce composant.
Configuration
- Cliquez sur Remix to Edit (Remixer pour modifier) pour pouvoir modifier le projet.
- Ouvrez
app/index.html
.
HTML
J'essaie toujours d'utiliser le HTML sémantique.
Comme chaque ami peut avoir autant d'histoires que vous le souhaitez, j'ai pensé qu'il était intéressant d'utiliser un
un élément <section>
pour chaque ami et un élément <article>
pour chaque histoire.
Commençons par le début. Tout d'abord, nous avons besoin d'un conteneur
"Stories" de Google.
Ajoutez un élément <div>
à votre <body>
:
<div class="stories">
</div>
Ajoutez quelques éléments <section>
pour représenter des amis:
<div class="stories">
<section class="user"></section>
<section class="user"></section>
<section class="user"></section>
<section class="user"></section>
</div>
Ajoutez quelques éléments <article>
pour représenter les stories:
<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>
- Nous utilisons un service d'images (
picsum.com
) pour nous aider à prototyper les stories. - L'attribut
style
de chaque<article>
fait partie d'un chargement d'espace réservé que vous découvrirez dans la section suivante.
CSS
Vous avez du style ! Transformons ces os en quelque chose que les gens apprécieront souhaitez interagir. Aujourd'hui, nous allons donner la priorité aux appareils mobiles.
.stories
Pour notre conteneur <div class="stories">
, nous voulons un conteneur à défilement horizontal.
Pour ce faire, nous pouvons:
- Faire du conteneur une grille
- Définir chaque enfant pour qu'il remplisse la piste de ligne
- Convertir la largeur de chaque élément enfant sur celle de la fenêtre d'affichage d'un appareil mobile
La grille continuera de placer les nouvelles colonnes de 100vw
de large à droite des colonnes précédentes
jusqu'à ce qu'il soit placé
tous les éléments HTML dans votre balisage.
Ajoutez le code CSS suivant en bas de app/css/index.css
:
.stories {
display: grid;
grid: 1fr / auto-flow 100%;
gap: 1ch;
}
Maintenant que le contenu s'étend au-delà de la fenêtre d'affichage,
conteneur et comment le gérer. Ajoutez les lignes de code en surbrillance à votre ensemble de règles .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;
}
Nous voulons le défilement horizontal. Nous allons donc définir overflow-x
sur
auto
Lorsque l'utilisateur fait défiler la page,
le composant doit reposer doucement sur l'histoire suivante.
Nous utiliserons donc scroll-snap-type: x mandatory
. En savoir plus
CSS dans les points d'ancrage de défilement CSS
et overscroll-behavior
de mon article de blog.
Il faut que le conteneur parent et les enfants acceptent de faire défiler l'ancrage. Ainsi,
nous allons nous en occuper maintenant. Ajoutez le code suivant en bas de app/css/index.css
:
.user {
scroll-snap-align: start;
scroll-snap-stop: always;
}
Votre application ne fonctionne pas encore, mais la vidéo ci-dessous montre ce qui se passe lorsque
scroll-snap-type
est activé et désactivé. Lorsque cette option est activée, chaque ligne horizontale
faites défiler des ancrages jusqu'à l'article suivant. Lorsque cette option est désactivée, le navigateur utilise
comportement de défilement par défaut.
Cela vous fera faire défiler la liste de vos amis, mais le problème persiste avec les histoires à résoudre.
.user
Créons dans la section .user
une mise en page qui superpose cette histoire enfant.
les éléments
en place. Pour résoudre ce problème, nous allons utiliser une astuce d'empilement pratique.
Nous créons essentiellement une grille 1x1 où la ligne et la colonne ont la même grille
alias de [story]
, et chaque élément de la grille de récit va essayer de revendiquer cet espace,
ce qui entraîne une pile.
Ajoutez le code en surbrillance à votre ensemble de règles .user
:
.user {
scroll-snap-align: start;
scroll-snap-stop: always;
display: grid;
grid: [story] 1fr / [story] 1fr;
}
Ajoutez l'ensemble de règles suivant en bas de app/css/index.css
:
.story {
grid-area: story;
}
Désormais, sans positionnement absolu, sans floats ni autres directives de mise en page qui prennent un élément hors flux, nous sommes toujours en flux. De plus, c'est comme à peine du code, regardez ça ! Cela est décomposé plus en détail dans la vidéo et l'article de blog.
.story
Il ne nous reste plus qu'à définir le style de l'élément de type "Histoire".
Comme indiqué précédemment, l'attribut style
de chaque élément <article>
fait partie d'une
technique de chargement des espaces réservés:
<article class="story" style="--bg: url(https://picsum.photos/480/840);"></article>
Nous allons utiliser la propriété CSS background-image
, qui nous permet de spécifier
plusieurs images de fond. Nous pouvons les mettre dans
un ordre afin que notre utilisateur
est en haut et s'affichera automatiquement
une fois le chargement terminé. À
nous allons placer l'URL de l'image dans une propriété personnalisée (--bg
) et l'utiliser.
dans notre CSS pour la superposer avec l'espace réservé de chargement.
Commençons par modifier l'ensemble de règles .story
pour remplacer un dégradé par une image de fond.
une fois le chargement terminé. Ajoutez le code en surbrillance à votre ensemble de règles .story
:
.story {
grid-area: story;
background-size: cover;
background-image:
var(--bg),
linear-gradient(to top, lch(98 0 0), lch(90 0 0));
}
Définir background-size
sur cover
garantit qu'il n'y a pas d'espace vide dans
car notre image la remplira. Définir deux images de fond
permet d'exécuter une astuce Web CSS, appelée tombstone de chargement:
- L'image de fond 1 (
var(--bg)
) correspond à l'URL que nous avons intégrée dans le code HTML. - Image de fond 2 (
linear-gradient(to top, lch(98 0 0), lch(90 0 0))
est un dégradé) à afficher pendant le chargement de l'URL
Le CSS remplace automatiquement le dégradé par l'image une fois le téléchargement terminé.
Nous allons ensuite ajouter du CSS pour supprimer certains comportements, ce qui permettra au navigateur de se déplacer plus rapidement.
Ajoutez le code en surbrillance à votre ensemble de règles .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
empêche les utilisateurs de sélectionner accidentellement du textetouch-action: manipulation
indique au navigateur que ces interactions doivent être traités comme des événements tactiles, ce qui évite au navigateur décidez si vous cliquez ou non sur une URL
Enfin, ajoutons un peu de code CSS pour animer la transition entre les stories. Ajoutez le
le code en surbrillance à votre ensemble de règles .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;
}
}
La classe .seen
sera ajoutée à une story nécessitant une sortie.
J'ai obtenu la fonction de lissage de vitesse personnalisé (cubic-bezier(0.4, 0.0, 1,1)
)
de l'atelier Easing de Material Design
(faites défiler la page jusqu'à la section Lissage de vitesse accéléré).
Si vous avez l'œil attentif, vous avez probablement remarqué le pointer-events: none
et vous vous grattez la tête en ce moment. Je dirais que c'est le seul
l'inconvénient de la solution jusqu'à présent. Nous en avons besoin, car un élément .seen.story
est en haut et reçoit des pressions, même s'il est invisible. En définissant
pointer-events
à none
, nous transformons l'histoire en verre en fenêtre, et nous ne volons
plus d'interactions utilisateur. Pas trop mal, c'est juste un compromis, pas trop difficile à gérer ici
dans notre CSS. Nous ne jonglons pas avec z-index
. Cela me convient
à l'arrêt.
JavaScript
Les interactions avec le composant "Stories" sont assez simples pour l'utilisateur: appuyez sur l'icône vers la droite pour avancer, appuyez sur la gauche pour revenir en arrière. Les choses simples pour les utilisateurs ont tendance pour les développeurs. Nous nous occupons toutefois d'un grand nombre de choses.
Configuration
Pour commencer, calculons et stockons autant d'informations que possible.
Ajoutez le code suivant à app/js/index.js
:
const stories = document.querySelector('.stories')
const median = stories.offsetLeft + (stories.clientWidth / 2)
La première ligne de JavaScript récupère et stocke une référence à notre code HTML principal. racine de l'élément. La ligne suivante calcule où se trouve le milieu de notre élément, nous peuvent décider si un tapotement est d’avancer ou de reculer.
État
Ensuite, nous créons un petit objet avec un état pertinent pour notre logique. Dans ce
nous ne nous intéressons qu'à l'histoire actuelle. Dans notre balisage HTML, nous pouvons
accédez-y en saisissant le premier ami et son histoire la plus récente. Ajouter le code en surbrillance
à votre app/js/index.js
:
const stories = document.querySelector('.stories')
const median = stories.offsetLeft + (stories.clientWidth / 2)
const state = {
current_story: stories.firstElementChild.lastElementChild
}
Écouteurs
Nous avons maintenant suffisamment de logique pour commencer à écouter les événements utilisateur et à les diriger.
Souris
Commençons par écouter l'événement 'click'
dans notre conteneur de stories.
Ajoutez le code en surbrillance à 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 un clic se produit et qu'il ne concerne pas un élément <article>
, nous annulons la demande et ne faisons rien.
S'il s'agit d'un article, nous saisissons la position horizontale de la souris ou du doigt avec
clientX
Nous n'avons pas encore implémenté navigateStories
, mais l'argument qui
qu'il prend spécifie dans
la direction que nous devons aller. Si cette position de l’utilisateur est
supérieure à la médiane, nous savons qu'il faut accéder à next
, sinon
prev
(précédente).
Clavier
Écoutons maintenant les pressions du clavier. Si l'utilisateur appuie sur la flèche vers le bas,
à next
. Si c'est la flèche vers le haut, nous allons à prev
.
Ajoutez le code en surbrillance à 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')
})
Navigation dans les stories
Abordons maintenant la logique métier unique des histoires et l'expérience utilisateur qu'ils deviennent célèbre. Cela semble lourd et délicat, mais je pense que si vous faites la même chose vous verrez que c'est assez digeste.
Nous enregistrons d'emblée quelques sélecteurs qui nous aident à décider s'il convient de faire défiler ami ou afficher/masquer une histoire. Puisque c'est sur le HTML que nous travaillons, l’interroger pour la présence d’amis (utilisateurs) ou d’histoires (histoire).
Ces variables nous aideront à répondre à des questions telles que : « histoire donnée x, fait « suivant » passer à une autre histoire, de ce même ami ou d’un autre ami ? » Je l'ai fait en utilisant l'arbre que nous avons élaborée, en touchant les parents et leurs enfants.
Ajoutez le code suivant en bas 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
}
Voici notre objectif de logique métier, aussi proche que possible du langage naturel:
- Décidez comment gérer le geste
<ph type="x-smartling-placeholder">
- </ph>
- S'il existe un article suivant/précédent: montrez-le
- S'il s'agit de la dernière/première histoire de l'ami: montrez à un nouvel ami
- S'il n'y a pas d'article vers lequel aller dans ce sens: ne rien faire
- Placer la nouvelle histoire actuelle dans
state
Ajoutez le code en surbrillance à votre fonction 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
}
}
}
Essayer
- Pour prévisualiser le site, appuyez sur Afficher l'application. Appuyez ensuite sur Plein écran
Conclusion
Voilà pour les besoins du composant. N'hésitez pas à vous appuyer sur vous les pilotez avec des données et, en général, vous les appropriez !