Crea il carosello perfetto, parte 3

Questa è la terza e ultima parte della nostra serie di tutorial Create the Perfect Carousel. Nella parte 1, abbiamo valutato i caroselli su Netflix e Amazon, due dei caroselli più usati al mondo. Abbiamo impostato la nostra giostra e implementato il touch scroll.

Quindi, nella parte 2, abbiamo aggiunto scorrimento orizzontale del mouse, impaginazione e un indicatore di avanzamento. Boom.

Ora, nella parte finale, esamineremo il mondo oscuro e spesso dimenticato dell'accessibilità della tastiera. Regoleremo il nostro codice per rimisurare il carosello quando cambia la dimensione del viewport. E, infine, faremo qualche tocco finale usando la fisica di primavera.

Puoi riprendere da dove abbiamo lasciato con questo CodePen.

Accessibilità della tastiera

È vero che la maggior parte degli utenti non si affida alla navigazione da tastiera, quindi purtroppo a volte ci dimentichiamo dei nostri utenti che lo fanno. In alcuni paesi, l'accesso a un sito Web inaccessibile potrebbe essere illegale. Ma peggio, è una mossa di cazzo.

La buona notizia è che di solito è facile da implementare! In effetti, i browser fanno la maggior parte del lavoro per noi. Seriamente: prova a sfogliare il carosello che abbiamo realizzato. Perché abbiamo usato il markup semantico, è già possibile!

Tranne che, noterai, i nostri pulsanti di navigazione scompaiono. Questo perché il browser non consente di concentrarsi su un elemento al di fuori del nostro viewport. Quindi anche se abbiamo overflow: nascosto impostato, non siamo in grado di scorrere la pagina orizzontalmente; altrimenti, la pagina scorrerà effettivamente per mostrare l'elemento con il focus.

Va bene, e secondo me sarebbe qualificato come "utile", sebbene non esattamente delizioso.

Anche il carosello di Netflix funziona in questo modo. Ma poiché la maggior parte dei loro titoli sono pigri e sono passivamente accessibili da tastiera (il che significa che non hanno scritto alcun codice specifico per gestirli), non possiamo selezionare alcun titolo oltre i pochi che abbiamo già caricato. Sembra anche terribile: 

Possiamo fare di meglio.

Gestire il messa a fuoco Evento

Per fare questo, ascolteremo il messa a fuoco evento che spara su qualsiasi oggetto nel carosello. Quando un oggetto riceve attenzione, lo interrogheremo per la sua posizione. Quindi, controlleremo contro sliderX e sliderVisibleWidth per vedere se quell'elemento si trova all'interno della finestra visibile. Se non lo è, lo impagheremo usando lo stesso codice che abbiamo scritto nella parte 2.

Alla fine di giostra funzione, aggiungi questo listener di eventi:

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

Noterai che abbiamo fornito un terzo parametro, vero. Anziché aggiungere un listener di eventi a ciascun elemento, possiamo utilizzare la cosiddetta delega degli eventi per ascoltare gli eventi su un solo elemento, il loro genitore diretto. Il messa a fuoco l'evento non bolle, quindi vero sta dicendo all'ascoltatore dell'evento di ascoltare il catturare palcoscenico, il palcoscenico in cui l'evento spara su ogni elemento dal finestra attraverso il bersaglio (in questo caso, l'oggetto che riceve attenzione).

Sopra il nostro crescente numero di ascoltatori di eventi, aggiungi il onFocus funzione:

function onFocus (e) 

Lavoreremo in questa funzione per il resto di questa sezione.

Dobbiamo misurare l'articolo sinistra e destra compensare e verificare se uno dei due punti si trova al di fuori dell'area attualmente visibile.

L'articolo è fornito dagli eventi bersaglio parametro, e possiamo misurarlo con getBoundingClientRect

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

sinistradestra sono relativi al viewport, non il cursore. Quindi abbiamo bisogno di prendere il contenitore della giostra sinistra compensare per tener conto di ciò. Nel nostro esempio, questo sarà 0, ma per rendere il carosello robusto, dovrebbe tener conto di essere posizionato ovunque.

const carouselLeft = container.getBoundingClientRect (). left;

Ora, possiamo fare un semplice controllo per vedere se l'oggetto si trova all'esterno dell'area visibile del cursore e impaginare in quella direzione:

se (a sinistra < carouselLeft)  gotoPrev();  else if (right > carouselLeft + sliderVisibleWidth) gotoNext (); 

Ora, mentre ci aggiriamo, il carousel confluisce tranquillamente intorno con il nostro focus sulla tastiera! Solo poche righe di codice per mostrare più amore ai nostri utenti.

Rimisurare il carosello

Potresti aver notato come segui questo tutorial che se ridimensioni la finestra del browser, il carousel non impagina più correttamente. Questo perché abbiamo misurato la sua larghezza rispetto alla sua area visibile solo una volta, al momento dell'inizializzazione.

Per assicurarci che il nostro carosello si comporti correttamente, dobbiamo sostituire alcuni dei nostri codici di misurazione con un nuovo listener di eventi che si attiva quando il finestra ridimensiona.

Ora, vicino all'inizio del tuo giostra funzione, subito dopo la linea in cui definiamo barra di avanzamento, vogliamo sostituire tre di questi const misure con permettere, perché li cambieremo quando cambierà la vista:

const totalItemsWidth = getTotalItemsWidth (items); const maxXOffset = 0; let minXOffset = 0; lasciare sliderVisibleWidth = 0; lasciare clampXOffset;

Quindi, possiamo spostare la logica che in precedenza ha calcolato questi valori su un nuovo measureCarousel funzione:

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

Vogliamo richiamare immediatamente questa funzione in modo da impostare questi valori sull'inizializzazione. Nella riga successiva, chiama measureCarousel:

measureCarousel ();

La giostra dovrebbe funzionare esattamente come prima. Per aggiornare il ridimensionamento della finestra, aggiungiamo semplicemente questo listener di eventi alla fine del nostro giostra funzione:

window.addEventListener ('ridimensiona', measureCarousel);

Ora, se ridimensioni il carousel e provi a impaginare, continuerà a funzionare come previsto.

Una nota sulle prestazioni

Vale la pena considerare che nel mondo reale, potresti avere più caroselli sulla stessa pagina, moltiplicando l'impatto sulle prestazioni di questo codice di misura di quella quantità.

Come abbiamo brevemente discusso nella parte 2, non è saggio eseguire calcoli pesanti più spesso di quanto si deve. Con gli eventi di puntatore e scorrimento, abbiamo detto che si desidera eseguirli una volta per frame per mantenere 60fps. Gli eventi di ridimensionamento sono leggermente diversi in quanto l'intero documento rifletterà, probabilmente il momento più intenso in termini di risorse che una pagina Web incontrerà.

Non è necessario rimisurare il carosello finché l'utente non ha ridimensionato la finestra, poiché nel frattempo non interagiranno con esso. Possiamo avvolgere il nostro measureCarousel funzione in una funzione speciale chiamata a antirimbalzo.

Una funzione di rimbalzo dice essenzialmente: "Spara questa funzione solo quando non è stata richiamata X millisecondi. "Puoi leggere di più su debounce sull'eccellente primer di David Walsh e prendere anche qualche codice di esempio.

Ritocchi finali

Finora, abbiamo creato un carosello piuttosto buono. È accessibile, si anima bene, funziona su touch e mouse e offre una grande flessibilità di design in un modo in cui i caroselli a scorrimento nativo non consentono.

Ma questa non è la serie di tutorial "Crea un bel carosello". È tempo per noi di sfoggiare un po ', e per farlo, abbiamo un'arma segreta. Springs.

Aggiungeremo due interazioni usando le molle. Uno per il tocco e uno per l'impaginazione. Entrambi faranno sapere all'utente, in modo divertente e giocoso, che hanno raggiunto la fine della giostra.

Tocca Spring

Innanzitutto, aggiungiamo un rimorchiatore in stile iOS quando un utente tenta di far scorrere il cursore oltre i suoi limiti. Attualmente stiamo riducendo il touch scroll utilizzando clampXOffset. Invece, sostituiamo questo con un codice che applica un rimorchiatore quando l'offset calcolato è al di fuori dei suoi limiti.

Per prima cosa, dobbiamo importare la nostra primavera. C'è un trasformatore chiamato nonlinearSpring che applica una forza esponenzialmente crescente contro il numero che gli forniamo, verso un origine. Il che significa che quanto più tiriamo il cursore, tanto più lo tirerà indietro. Possiamo importarlo in questo modo:

const applyOffset, clamp, nonlinearSpring, pipe = transform;

Nel determineDragDirection funzione, abbiamo questo codice:

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

Appena sopra di esso, creiamo le nostre due molle, una per ogni limite di scorrimento del carosello:

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

Decidere su un valore per elasticità è una questione di giocare e vedere ciò che sembra giusto. Un numero troppo basso e la molla è troppo rigida. Troppo alto e non noterai il suo strattone, o peggio, spingerà il cursore ancora più lontano dal dito dell'utente!

Ora abbiamo solo bisogno di scrivere una semplice funzione che applicherà una di queste molle se il valore fornito è al di fuori dell'intervallo consentito:

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

Possiamo sostituire clampXOffset nel codice sopra con applySpring. Ora, se si fa scorrere il cursore oltre i suoi limiti, esso tornerà indietro!

Tuttavia, quando lasciamo andare la primavera, questa specie di scatta senza tante cerimonie torna al suo posto. Vogliamo modificare il nostro stopTouchScroll funzione, che attualmente gestisce lo scorrimento del momento, per verificare se il cursore è ancora al di fuori dell'intervallo consentito e, in tal caso, applicare una molla con fisica azione invece.

Fisica primaverile

Il fisica l'azione è anche in grado di modellare le molle. Abbiamo solo bisogno di fornirlo primaveraa proprietà.

Nel stopTouchScroll, sposta la pergamena esistente fisica inizializzazione ad un pezzo di logica che ci assicura che siamo entro i limiti di scorrimento:

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

All'interno della prima clausola del Se dichiarazione, sappiamo che il cursore è al di fuori dei limiti di scorrimento, quindi possiamo aggiungere la nostra molla:

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

Vogliamo creare una molla che si senta scattante e reattiva. Ho scelto un livello relativamente alto primavera valore per avere un "pop" stretto, e ho abbassato il attrito0.92 per consentire un piccolo rimbalzo. È possibile impostare questo a 1 per eliminare completamente il rimbalzo.

Come un po 'di compiti a casa, prova a sostituire il clampXOffset nel produzione funzione della pergamena fisica con una funzione che innesca una molla simile quando l'offset x raggiunge i suoi limiti. Invece di fermarsi bruscamente, prova a farlo rimbalzare piano alla fine.

Impaginazione primaverile

Toccare gli utenti ottengono sempre la bontà primaverile, giusto? Condividiamo questo amore con gli utenti desktop rilevando quando il carosello è ai suoi limiti di scorrimento e avendo un rimorchiatore indicativo per mostrare chiaramente e con sicurezza all'utente che si trovano alla fine.

Per prima cosa, vogliamo disabilitare i pulsanti di impaginazione quando il limite è stato raggiunto. Per prima cosa aggiungiamo una regola CSS che stili i pulsanti per mostrare che sono Disabilitato. Nel pulsante regola, aggiungere:

transizione: sfondo 200 ms lineare; & .disabled background: #eee; 

Stiamo usando una classe qui invece della più semantica Disabilitato attributo perché vogliamo ancora catturare gli eventi click, che, come suggerisce il nome, Disabilitato bloccherebbe.

Aggiungi questo Disabilitato classe al pulsante Indietro, perché ogni giostra inizia la vita con a 0 compensare:

Verso la cima di giostra, creare una nuova funzione chiamata checkNavButtonStatus. Vogliamo che questa funzione controlli semplicemente il valore fornito minXOffsetmaxXOffset e imposta il pulsante Disabilitato classificare di conseguenza:

function checkNavButtonStatus (x) if (x <= minXOffset)  nextButton.classList.add('disabled');  else  nextButton.classList.remove('disabled'); if (x >= maxXOffset) prevButton.classList.add ('disabled');  else prevButton.classList.remove ('disabled'); 

Sarebbe allettante chiamarlo ogni volta sliderX i cambiamenti. Se lo facessimo, i pulsanti inizieranno a lampeggiare ogni volta che una molla oscillerà attorno ai bordi dello scroll. Sarebbe anche strano comportamento se uno dei pulsanti è stato premuto durante una di quelle animazioni primaverili. Il rimorchiatore "scroll end" dovrebbe sempre sparare se siamo alla fine del carosello, anche se c'è un'animazione a molla che la allontana dalla fine assoluta.

Quindi dobbiamo essere più selettivi su quando chiamare questa funzione. Sembra ragionevole chiamarlo:

Sull'ultima riga del onWheel, Inserisci checkNavButtonStatus (newX);.

Sull'ultima riga di vai a, Inserisci checkNavButtonStatus (TargetX);.

E infine, alla fine di determineDragDirection, e nella clausola scroll di slancio (il codice all'interno del altro) di stopTouchScroll, sostituire:

(v) => sliderX.set (v)

Con:

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

Ora non resta che modificare gotoPrevgotoNext per controllare la loro classList del pulsante di attivazione per Disabilitato e solo paginare se è assente:

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);

Il notifyEnd la funzione è solo un'altra fisica primavera, e sembra così:

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

Gioca con quello, e ancora, modifica il fisica params di tuo gradimento.

Rimane solo un piccolo bug. Quando il cursore scatta oltre il limite più a sinistra, la barra di avanzamento viene invertita. Possiamo sistemarlo rapidamente sostituendo:

progressBarRenderer.set ('scaleX', progress);

Con:

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

Noi potevaimpedirgli di rimbalzare dall'altra parte, ma personalmente penso che sia piuttosto bello che rifletta il movimento della molla. Sembra strano quando si capovolge.

Pulisci dopo te stesso

Con le applicazioni a pagina singola, i siti Web durano più a lungo nella sessione di un utente. Spesso, anche quando la "pagina" cambia, stiamo ancora eseguendo lo stesso runtime JS del carico iniziale. Non possiamo fare affidamento su una lavagna pulita ogni volta che l'utente fa clic su un link e ciò significa che dobbiamo ripulire noi stessi per evitare che gli ascoltatori di eventi attivino elementi morti.

In React, questo codice è inserito nel file componentWillLeave metodo. Vue utilizza beforeDestroy. Questa è una pura implementazione JS, ma possiamo ancora fornire un metodo destroy che funzionerebbe allo stesso modo in entrambi i framework.

Finora, il nostro giostra la funzione non ha restituito nulla. Cambiamo questo.

Innanzitutto, cambia la linea finale, la linea che chiama giostra, a:

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

Torneremo solo da una cosa giostra, una funzione che disimpegna tutti i nostri ascoltatori di eventi. Alla fine del giostra funzione, scrivi:

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); ;

Ora, se chiami destroyCarousel e prova a giocare con la giostra, non succede nulla! È quasi un po 'triste vederlo così.

E questo è quello

Accidenti. Era molto! Quanto siamo arrivati. Puoi vedere il prodotto finito in questo CodePen. In questa parte finale, abbiamo aggiunto l'accessibilità della tastiera, rimisurando il carosello quando cambia la vista, alcune aggiunte divertenti con la fisica di primavera e il passo straziante ma necessario di strappare di nuovo tutto.

Spero che questo tutorial ti sia piaciuto tanto quanto mi è piaciuto scriverlo. Mi piacerebbe sentire i tuoi pensieri su ulteriori modi in cui potremmo migliorare l'accessibilità o aggiungere piccoli tocchi divertenti.