Codelab: creazione di un componente Storie

Questo codelab ti insegna come creare un'esperienza come le Storie di Instagram sul web. Il componente viene creato man mano che procediamo, iniziando con HTML, poi CSS e poi JavaScript.

Consulta il mio post del blog Creazione di un componente Storie per conoscere i miglioramenti progressivi apportati durante la creazione di questo componente.

Configurazione

  1. Fai clic su Remixa per modificare per rendere modificabile il progetto.
  2. Apri app/index.html.

HTML

Cerco sempre di utilizzare l'HTML semantico. Dato che ogni amico può avere un certo numero di storie, ho pensato che fosse significativo usare una <section> elemento per ogni amico e un elemento <article> per ogni storia. Partiamo dall'inizio, però. Innanzitutto, abbiamo bisogno di un container Storie.

Aggiungi un elemento <div> a <body>:

<div class="stories">

</div>

Aggiungi alcuni elementi <section> per rappresentare gli amici:

<div class="stories">
  <section class="user"></section>
  <section class="user"></section>
  <section class="user"></section>
  <section class="user"></section>
</div>

Aggiungi alcuni elementi <article> per rappresentare le notizie:

<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>
  • Stiamo usando un servizio di immagini (picsum.com) per realizzare la prototipazione delle storie.
  • L'attributo style su ogni <article> fa parte del caricamento di un segnaposto di cui parleremo nella prossima sezione.

CSS

I nostri contenuti sono pronti per lo stile. Trasformiamo quelle ossa in qualcosa che la gente con cui vuoi interagire. Oggi lavoreremo principalmente per i dispositivi mobili.

.stories

Per il contenitore <div class="stories">, vogliamo un contenitore a scorrimento orizzontale. Possiamo ottenere questo risultato:

  • Rendi il container una griglia
  • Impostazione di ogni elemento secondario per la compilazione della traccia delle righe
  • Impostare la larghezza di ogni asset secondario uguale alla larghezza dell'area visibile di un dispositivo mobile

La griglia continuerà a posizionare le nuove colonne di tutta 100vw a destra della precedente uno, finché non sono stati inseriti tutti gli elementi HTML nel markup.

Chrome e DevTools si aprono con una griglia che mostra il layout a larghezza intera
Chrome DevTools che mostra l'overflow delle colonne della griglia, creando uno scorrimento orizzontale.

Aggiungi il seguente CSS in fondo a app/css/index.css:

.stories {
  display: grid;
  grid: 1fr / auto-flow 100%;
  gap: 1ch;
}

Ora che i contenuti si estendono oltre l'area visibile, è il momento di dire come gestirlo. Aggiungi le righe di codice evidenziate al set di regole .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;
}

Vogliamo lo scorrimento orizzontale, quindi impostiamo overflow-x su auto. Quando l'utente scorre la pagina, vogliamo che il componente si fermi delicatamente sulla storia successiva. quindi utilizzeremo scroll-snap-type: x mandatory. Scopri di più al riguardo CSS in Snap point di scorrimento CSS e comportamento overscroll sezioni del mio post del blog.

Sia il contenitore principale sia i publisher secondari accettano di accettare l'aggancio dello scorrimento, ce ne occuperemo ora. Aggiungi il seguente codice in fondo a app/css/index.css:

.user {
  scroll-snap-align: start;
  scroll-snap-stop: always;
}

La tua app non funziona ancora, ma il video seguente mostra cosa succede quando scroll-snap-type è abilitato e disabilitato. Quando l'opzione è attivata, ogni sezione lo scorrimento consente di passare alla storia successiva. Se disattivato, il browser utilizza i suoi comportamento di scorrimento predefinito.

In questo modo dovrai scorrere i tuoi amici, ma il problema persiste. con le storie da risolvere.

.user

Creiamo un layout nella sezione .user in modo da creare un wrangling della storia secondaria i vari elementi. Utilizzeremo un pratico trucco di impilamento per risolvere il problema. In sostanza, stiamo creando una griglia 1x1 in cui la riga e la colonna hanno la stessa Griglia alias di [story] e ogni elemento della griglia di una storia proverà a rivendicare quello spazio, generando una sovrapposizione di immagini.

Aggiungi il codice evidenziato al set di regole .user:

.user {
  scroll-snap-align: start;
  scroll-snap-stop: always;
  display: grid;
  grid: [story] 1fr / [story] 1fr;
}

Aggiungi il seguente set di regole in fondo a app/css/index.css:

.story {
  grid-area: story;
}

Ora, senza posizionamento assoluto, numeri in virgola mobile o altre istruzioni di layout che prendono un elemento fuori flusso, siamo ancora in flusso. Inoltre, è praticamente senza codice, guarda un po'! Questa informazione viene suddivisa più dettagliatamente nel video e nel post del blog.

.story

Ora dobbiamo solo definire lo stile dell'elemento della storia.

Come accennato prima, l'attributo style di ogni elemento <article> fa parte di un tecnica di caricamento segnaposto:

<article class="story" style="--bg: url(https://picsum.photos/480/840);"></article>

Useremo la proprietà background-image del CSS, che ci consente di specificare più di un'immagine di sfondo. Possiamo metterle in un ordine in modo che il nostro utente l'immagine si trova in alto e verrà visualizzata automaticamente al termine del caricamento. A attiva questa opzione, inseriremo l'URL dell'immagine in una proprietà personalizzata (--bg) e la utilizzeremo all'interno del nostro CSS da sovrapporre al segnaposto di caricamento.

Innanzitutto, aggiorna la serie di regole .story per sostituire un gradiente con un'immagine di sfondo una volta terminato il caricamento. Aggiungi il codice evidenziato al set di regole .story:

.story {
  grid-area: story;

  background-size: cover;
  background-image:
    var(--bg),
    linear-gradient(to top, lch(98 0 0), lch(90 0 0));
}

L'impostazione di background-size su cover garantisce che non ci siano spazi vuoti nella visibile perché l'immagine lo riempirà. Definizione di due immagini di sfondo in corso... ci consente di eseguire un pratico trucco web CSS chiamato Tombstone in caricamento:

  • L'immagine di sfondo 1 (var(--bg)) è l'URL che abbiamo trasmesso in linea nel codice HTML
  • Immagine di sfondo 2 (linear-gradient(to top, lch(98 0 0), lch(90 0 0)) è un gradiente da mostrare durante il caricamento dell'URL

Al termine del download, CSS sostituirà automaticamente il gradiente con l'immagine.

Successivamente aggiungeremo alcuni CSS per rimuovere alcuni comportamenti, consentendo al browser di spostarsi più velocemente. Aggiungi il codice evidenziato al set di regole .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 impedisce agli utenti di selezionare accidentalmente testo
  • touch-action: manipulation indica al browser che queste interazioni dovrebbero essere trattati come eventi touch, consentendo al browser di decidi se fare clic o meno su un URL

Infine, aggiungiamo un po' di CSS per animare la transizione tra le storie. Aggiungi il parametro codice evidenziato nel set di regole .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;
  }
}

Il corso .seen verrà aggiunto a una storia che richiede un'uscita. Ho ottenuto la funzione di easing personalizzato (cubic-bezier(0.4, 0.0, 1,1)) da Easing di Material Design (scorri fino alla sezione Earlerated easing).

Se hai un occhio attento, probabilmente avrai notato la pointer-events: none dichiarativa e ti stanno grattando la testa. Direi che questo è l'unico il lato negativo della soluzione. Ci serve perché un elemento .seen.story sarà in alto e riceverà tocchi, anche se è invisibile. Impostando il parametro pointer-events a none, trasformiamo la storia di vetro in una finestra e non rubiamo più interazioni degli utenti. Non male è un compromesso, non troppo difficile da gestire qui nel nostro CSS. Non stiamo destreggiando tra z-index. Mi sento bene ancora.

JavaScript

Le interazioni di un componente Storie sono piuttosto semplici per l'utente: tocca l'icona a destra per andare avanti, a sinistra per tornare indietro. Le cose semplici per gli utenti tendono sia un duro lavoro per gli sviluppatori. Ne occuperemo molte, però.

Configurazione

Per iniziare, calcoliamo e archiviamo il maggior numero possibile di informazioni. Aggiungi il codice seguente a app/js/index.js:

const stories = document.querySelector('.stories')
const median = stories.offsetLeft + (stories.clientWidth / 2)

La prima riga di JavaScript recupera e memorizza un riferimento al nostro codice HTML principale dell'elemento. La riga successiva calcola la posizione centrale dell'elemento, quindi può decidere se andare avanti o indietro con un tocco.

Stato

Ora creiamo un piccolo oggetto con uno stato pertinente alla nostra logica. In questo in questo caso, siamo interessati solo alla notizia attuale. Nel nostro markup HTML, possiamo per accedervi cercando il primo amico e la storia più recente. Aggiungi il codice evidenziato al tuo app/js/index.js:

const stories = document.querySelector('.stories')
const median = stories.offsetLeft + (stories.clientWidth / 2)

const state = {
  current_story: stories.firstElementChild.lastElementChild
}

Listener

Ora la logica è sufficiente per iniziare ad ascoltare gli eventi degli utenti e indirizzarli.

Topo

Iniziamo ascoltando l'evento 'click' nel nostro contenitore delle storie. Aggiungi il codice evidenziato 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')
})

Se viene fatto un clic e non si riferisce a un elemento <article>, elimineremo e non faremo nulla. Se si tratta di un articolo, afferriamo la posizione orizzontale del mouse o del dito con clientX. Non abbiamo ancora implementato navigateStories, ma l'argomento specifica la direzione da prendere. Se la posizione dell'utente è maggiore della mediana, sappiamo che dobbiamo raggiungere next, altrimenti prev (precedente).

Tastiera

Ora ascoltiamo le pressioni della tastiera. Se premi la Freccia giù, spostiamo a next. Se è la Freccia su, andiamo a prev.

Aggiungi il codice evidenziato 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')
})

Navigazione nelle Storie

È il momento di affrontare la logica di business unica delle storie e la UX che sono diventate. famoso. Sembri difficile e difficile, ma se lo fai, troverai abbastanza digeribile.

In primo luogo, mettiamo in primo piano alcuni selettori che ci aiutano a decidere se scorrere amico o mostrare/nascondere una storia. Poiché l'HTML è il luogo in cui lavoriamo, interrogando quest'ultima per verificare la presenza di amici (utenti) o di storie (storia).

Queste variabili ci aiuteranno a rispondere a domande quali "datata la storia X, fa "successivo" significa passare a un'altra storia da uno stesso amico o da un amico diverso?" L'ho fatto usando l'albero struttura che abbiamo sviluppato, raggiungendo i genitori e i loro figli.

Aggiungi il seguente codice in fondo a 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
}

Ecco il nostro obiettivo per la logica di business, il più vicino possibile al linguaggio naturale:

  • Decidi come gestire il tocco
    • Se c'è una notizia successiva/precedente: mostrala
    • Se si tratta dell'ultima/prima storia di un amico: mostra un nuovo amico
    • Se non trovi una notizia da raggiungere in quella direzione: non fare nulla.
  • Conserva la nuova storia attuale su state

Aggiungi il codice evidenziato alla funzione 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
    }
  }
}

Prova

  • Per visualizzare l'anteprima del sito, premi Visualizza app. Quindi premi Schermo intero schermo intero.

Conclusione

Questo è il risultato delle mie esigenze relative al componente. Sentiti libero di sviluppare e in generale rendilo tuo!