Aceasta este a treia și ultima parte a seriei noastre Creare carusel Perfect Carousel. În partea 1, am evaluat carusele pe Netflix și Amazon, două dintre cele mai utilizate caruseluri din lume. Ne-am înființat caruselul și am implementat touch-scroll.
Apoi, în partea 2, am adăugat defilare orizontală a mouse-ului, paginare și un indicator de progres. bum.
Acum, în ultima noastră parte, vom privi lumea întunecată și uitată de accesibilitate a tastaturii. Vom regla codul pentru a reevalua caruselul când se modifică mărimea ferestrei de vizualizare. Și în final, vom termina câteva lucrări de finisaj utilizând fizica primăvară.
Puteți alege unde am rămas cu acest CodPen.
Este adevărat că majoritatea utilizatorilor nu se bazează pe navigația cu tastatură, așa că, din păcate, uneori uităm de utilizatorii noștri care o fac. În unele țări, lăsarea unui site inaccesibil poate fi ilegală. Dar mai rău, este o mușcătură.
Vestea bună este că este de obicei ușor de implementat! De fapt, browserele fac majoritatea lucrărilor pentru noi. Serios: încercați să faceți un tabel prin caruselul pe care l-am făcut. Pentru că am folosit marcajul semantic, puteți deja!
În afară de asta, veți observa că butoanele noastre de navigare dispar. Acest lucru se datorează faptului că browserul nu permite concentrarea asupra unui element în afara ferestrei noastre de vizualizare. Deci, chiar dacă avem overflow: ascuns
setați, nu putem derula pagina orizontală; în caz contrar, pagina va defila într-adevăr pentru a afișa elementul cu focalizare.
Acest lucru este în regulă și, în opinia mea, se va califica drept "util", deși nu este chiar încântător.
Caruselul lui Netflix funcționează și în acest mod. Dar pentru că majoritatea titlurilor lor sunt încărcate leneș și sunt și pasiv accesibile la tastatură (adică nu au scris niciun cod special pentru a le gestiona), nu putem selecta de fapt titluri dincolo de puținele pe care le-am deja încărcate. Arată, de asemenea, teribil:
Putem face mai bine.
concentra
EvenimentPentru a face asta, o să ascultăm concentra
eveniment care arde incendii pe orice element din carusel. Când un element primește focalizare, o vom interoga pentru poziția sa. Atunci vom verifica asta sliderX
și sliderVisibleWidth
pentru a vedea dacă elementul respectiv se află în fereastra vizibilă. Dacă nu este, ne vom paginări folosind același cod pe care l-am scris în partea a doua.
La sfârșitul carusel
funcție, adăugați acest ascultător al evenimentului:
slider.addEventListener ('focus', onFocus, true);
Veți observa că am furnizat un al treilea parametru, Adevărat
. Mai degrabă decât să adăugăm un element de ascultător la fiecare element, putem folosi ceea ce se numește delegare de evenimente pentru a asculta evenimente pe un singur element, părintele lor direct. concentra
evenimentul nu bubble, deci Adevărat
îi spune ascultătorului evenimentului să asculte captură etapa în care se declanșează evenimentul pe fiecare element din fereastră
până la țintă (în acest caz, elementul care primește focalizare).
Deasupra blocului nostru de ascultători de evenimente, adăugați onFocus
funcţie:
funcția onFocus (e)
Vom lucra în această funcție pentru restul acestei secțiuni.
Trebuie să măsuram elementul stânga
și dreapta
offset și verificați dacă vreun punct se află în afara zonei vizibile în prezent.
Elementul este furnizat de eveniment ţintă
parametru și îl putem măsura getBoundingClientRect
:
const stânga, dreapta = e.target.getBoundingClientRect ();
stânga
și dreapta
sunt relative la viewport, nu cursorul. Așa că trebuie să luăm containerul caruselului stânga
offset pentru a ține cont de asta. În exemplul nostru, acest lucru va fi 0
, dar pentru a face carusel robust, ar trebui să țină cont de faptul că a fost plasat oriunde.
const carouselLeft = container.getBoundingClientRect () stânga;
Acum, putem face o verificare simplă pentru a vedea dacă elementul se află în afara zonei vizibile a cursorului și paginate în acea direcție:
dacă (stânga < carouselLeft) gotoPrev(); else if (right > carouselLeft + cursorVisibleWidth) gotoNext ();
Acum, când ne împușcăm, caruselul se îndreaptă cu încredere în jurul nostru, cu accentul pe tastatură! Doar câteva linii de cod pentru a arăta mai multă dragoste utilizatorilor noștri.
S-ar putea să fi observat pe măsură ce urmați acest tutorial că dacă redimensionați fereastra de vizualizare a browserului dvs., caruselul nu mai paginate în mod corespunzător. Acest lucru se datorează faptului că am măsurat lățimea sa față de zona vizibilă o singură dată, în momentul inițializării.
Pentru a ne asigura că caruselul se comportă corect, trebuie să înlocuim un cod de măsurare cu un nou ascultător de evenimente care se declanșează atunci când fereastră
redimensionează.
Acum, aproape de începutul dvs. carusel
funcție, imediat după linia pe care o definim bara de progres
, Vrem să înlocuim trei dintre acestea const
măsurători cu lăsa
, deoarece le vom schimba atunci când se va schimba fereastra de vizualizare:
const totalItemsWidth = getTotalItemsWidth (elemente); const maxXOffset = 0; permite minXOffset = 0; permiteți sliderVisibleWidth = 0; let clampXOffset;
Apoi, putem muta logica care a calculat anterior aceste valori la un nou measureCarousel
funcţie:
funcția measureCarousel () sliderVisibleWidth = slider.offsetWidth; minXOffset = - (totalItemsWidth - sliderVisibleWidth); clampXOffset = clemă (minXOffset, maxXOffset);
Vrem să invocăm imediat această funcție, astfel încât să stabilim aceste valori la inițializare. Pe linia următoare, apelați measureCarousel
:
measureCarousel ();
Caruselul ar trebui să funcționeze exact ca înainte. Pentru a actualiza la redimensionarea ferestrei, pur și simplu adăugăm acest ascultător eveniment la sfârșitul paginii noastre carusel
funcţie:
window.addEventListener ('redimensionare', measureCarousel);
Acum, dacă redimensionați caruselul și încercați să paginați, va continua să funcționeze conform așteptărilor.
Merită să aveți în vedere faptul că în lumea reală s-ar putea să aveți mai multe caruseluri pe aceeași pagină, înmulțind impactul de performanță al acestui cod de măsurare cu acea sumă.
Așa cum am discutat pe scurt în partea a doua, nu este înțelept să efectuați calcule grele mai des decât trebuie. Cu evenimente cu indicatori și defilare, am spus că doriți să le efectuați o singură dată pe cadru pentru a vă ajuta să mențineți 60 fps. Redimensionarea evenimentelor este puțin diferită prin faptul că întregul document se va reîmprospăta, probabil cel mai intens moment de resurse pe care o pagină web îl va întâlni.
Nu este nevoie să reeșurați caruselul până când utilizatorul nu a terminat să redimensioneze fereastra, deoarece nu va mai interacționa cu acesta în timp. Ne putem împacheta measureCarousel
funcția într-o funcție specială numită a debounce.
O funcție de debounce în esență spune: "Porniți această funcție doar atunci când nu a fost apelată peste X
milliseconds. "Puteți citi mai multe despre debounce pe primer excelent lui David Walsh, și ridica un exemplu de cod de asemenea.
Până acum, am creat un carusel destul de bun. Este accesibil, animă frumos, funcționează pe touch și mouse și oferă o mare flexibilitate a designului într-un mod în care caruselurile de derulare nativă nu permit.
Dar aceasta nu este seria "Creați o carusel destul de bună". E timpul să ne arătăm puțin și, pentru a face asta, avem o armă secretă. Springs.
Vom adăuga două interacțiuni folosind izvoare. Unul pentru atingere și unul pentru paginare. Ambii vor să-l lase pe utilizator să știe, într-un mod amuzant și jucăuș, că au ajuns la capătul caruselului.
Mai întâi, să adăugăm un remorcher în stil iOS atunci când un utilizator încearcă să deruleze cursorul peste granițele sale. În prezent, folosim scroll pentru atingere clampXOffset
. În schimb, să înlocuim acest lucru cu un cod care aplică un remorcher atunci când compensarea calculată este în afara limitelor sale.
În primul rând, trebuie să importăm primăvara noastră. Există un transformator numit nonlinearSpring
care aplică o forță crescândă exponențial față de numărul pe care îl oferim, spre un origine
. Ceea ce înseamnă că cu cât tragem mai departe cursorul, cu atât mai mult va fi tras înapoi. Putem importa astfel:
const applyOffset, clemă, nonlinearSpring, pipe = transformă;
În determineDragDirection
funcția, avem acest cod:
action.output (conducta (x) => x, aplicaOffset (action.x.get (), sliderX.get ()), clampXOffset, (v) => sliderX.set (v)));
Chiar deasupra ei, să creăm cele două izvoare ale noastre, una pentru fiecare limită de defilare a caruselului:
const elasticitate = 5; const tugLeft = nonlinearSpring (elasticitate, maxXOffset); const tugRight = nonlinearSpring (elasticitate, minXOffset);
Decizie privind o valoare pentru elasticitate
este o chestiune de a juca în jur și de a vedea ce este bine. Prea scăzut un număr, iar primăvara se simte prea rigidă. Prea înalt și nu veți observa remorcherul său, sau, mai rău, va împinge sliderul mai departe de degetul utilizatorului!
Acum, trebuie doar să scriem o funcție simplă care va aplica una dintre aceste izvoare dacă valoarea furnizată este în afara domeniului permis:
const applySpring = (v) => dacă (v> maxXOffset) retur tugLeft (v); dacă (v < minXOffset) return tugRight(v); return v; ;
Putem înlocui clampXOffset
în codul de mai sus cu applySpring
. Acum, dacă trageți cursorul peste granițele sale, se va trage înapoi!
Cu toate acestea, când renunțăm la primăvară, aceasta se blochează neextrem înapoi. Vrem să ne modificăm stopTouchScroll
, care în prezent se ocupă de derularea impulsului, pentru a verifica dacă glisorul este încă în afara domeniului permis și, dacă este cazul, aplicați un arc cu fizică
acțiune în loc.
fizică
acțiunea este de asemenea capabilă să modeleze izvoarele. Trebuie doar să o oferim primăvară
și la
proprietăţi.
În stopTouchScroll
, mutați cursorul existent fizică
inițializare la o logică care ne asigură că suntem în limitele scroll:
const curentX = sliderX.get (); dacă (curent < minXOffset || currentX > maxXOffset) altceva action = physics (from: currentX, velocity: sliderX.getVelocity (), friction: 0.2). ();
În cadrul primei clauze a directivei dacă
declarația, știm că glisorul se află în afara limitelor scroll, astfel încât să putem adăuga primăvara noastră:
acțiune = fizică (de la: curentX, la: (curentX < minXOffset) ? minXOffset : maxXOffset, spring: 800, friction: 0.92 ).output((v) => sliderX.set (v)) .start ();
Vrem să creăm un izvor care se simte bine și receptiv. Am ales un nivel relativ ridicat primăvară
valoarea pentru a avea un "pop" strâns, și am coborât frecare
la 0.92
pentru a permite o mica saritura. Ai putea să dai asta 1
pentru a elimina complet saritul.
Ca un pic de temă, încercați să înlocuiți clampXOffset
în producție
funcția de defilare fizică
cu o funcție care declanșează un resort similar când decalajul x atinge limitele sale. Mai degrabă decât oprirea abruptă curentă, încercați să o faceți ușor săriți la sfârșit.
Atingeți utilizatorii obține întotdeauna bunătatea primăvară, nu? Să împărtășim acea dragoste utilizatorilor de desktop prin detectarea momentului când caruselul se află la limitele sale de parcurgere și având un remorcher indicativ pentru a arăta în mod clar și cu încredere că utilizatorul este la sfârșit.
Mai întâi, dorim să dezactivați butoanele de paginare când limita a fost atinsă. Să adăugăm mai întâi o regulă CSS care modelează butoanele pentru a arăta că sunt invalid
. În buton
regula, adăugați:
tranziție: fundal 200ms liniar; & .disabled background: #eee;
Folosim aici o clasă în locul celei mai semantice invalid
deoarece încă dorim să capturam evenimente de clic, care, așa cum implică numele, invalid
ar bloca.
Adaugă asta invalid
clasa la butonul Prev, deoarece fiecare carusel începe viața cu un 0
compensate:
Spre vârful lui carusel
, faceți o nouă funcție numită checkNavButtonStatus
. Vrem ca această funcție să controleze pur și simplu valoarea furnizată împotriva minXOffset
și maxXOffset
și setați butonul invalid
clasa corespunzătoare:
funcția checkNavButtonStatus (x) if (x <= minXOffset) nextButton.classList.add('disabled'); else nextButton.classList.remove('disabled'); if (x >= maxXOffset) prevButton.classList.add ("dezactivat"); altceva prevButton.classList.remove ("dezactivat");
Ar fi tentant să numesc asta de fiecare dată sliderX
schimbări. Dacă am fi făcut-o, butoanele ar începe să clipească ori de câte ori un arc oscilează în jurul limitelor de defilare. De asemenea, ar duce la ciudat comportament dacă unul dintre butoane a fost apăsat în timpul acelei animații de primăvară. Remorcherul "scroll end" ar trebui să tragă întotdeauna dacă suntem la capătul caruselului, chiar dacă există o animație de primăvară care o trage departe de capătul absolut.
Așadar, trebuie să fim mai selectivi când să numim această funcție. Se pare că este bine să o numiți:
Pe ultima linie a mesajului onWheel
, adăuga checkNavButtonStatus (newX);
.
Pe ultima linie din mergi la
, adăuga checkNavButtonStatus (targetX);
.
Și în sfârșit, la sfârșitul anului determineDragDirection
, și în clauza de derulare a impulsului (codul din cadrul altfel
) de stopTouchScroll
, a inlocui:
(v) => sliderX.set (v)
Cu:
(v) => sliderX.set (v); checkNavButtonStatus (v);
Acum tot ce a mai rămas este să se modifice gotoPrev
și gotoNext
pentru a verifica clasa lor pentru butonul de declanșare pentru invalid
și numai paginate în cazul în care este absent:
const gotoNext = (e) =>! e.target.classList.contains ("dezactivat")? goto (1): notifyEnd (-1, maxXOffset); const gotoPrev = (e) =>! e.target.classList.contains ("dezactivat")? goto (-1): notifyEnd (1, minXOffset);
notifyEnd
funcția este doar un altul fizică
primăvară, și arată astfel:
funcția notifyEnd (delta, targetOffset) if (acțiune) action.stop (); action = fizica (de la: sliderX.get (), la: targetOffset, viteza: 2000 * delta, primavara: 300, frecare: 0.9). );
Faceți o pauză cu asta și, din nou, tweak fizică
paramuri după cum doriți.
Există doar un mic bug. Când glisorul izvorăște dincolo de limita stângă, bara de progres este inversată. Putem rezolva repede acest lucru prin înlocuirea:
progressBarRenderer.set ("scaleX", progres);
Cu:
progressBarRenderer.set ('scaleX', Math.max (progres, 0));
Noi ar puteaîmpiedicați-l să bată în altă direcție, dar personal cred că este destul de rece că reflectă mișcarea de primăvară. Se pare ciudat când se învârte în afară.
În cazul aplicațiilor pe o singură pagină, site-urile Web durează mai mult în sesiunea unui utilizator. Adesea, chiar și atunci când "pagina" se schimbă, continuăm să executăm aceleași runtime JS ca și la încărcarea inițială. Nu putem să ne bazăm pe o artă curată de fiecare dată când utilizatorul face clic pe un link și asta înseamnă că trebuie să ne curățăm după noi înșine pentru a împiedica ascultătorii evenimentului să tragă pe elemente moarte.
În React, acest cod este plasat în componentWillLeave
metodă. Vue folosește beforeDestroy
. Aceasta este o implementare JS pură, dar putem oferi o metodă de distrugere care să funcționeze în mod egal în oricare din aceste cadre.
Până acum, ale noastre carusel
funcția nu a returnat nimic. Să schimbăm asta.
Mai întâi, schimba linia finală, linia care cheamă carusel
, la:
const dist destroyCarousel = carusel (document.querySelector ("container"));
Vom reveni la un singur lucru carusel
, o funcție care dezabonează toți ascultătorii evenimentului nostru. La sfârșitul anului carusel
functie, scrie:
retur () => container.removeEventListener ('touchstart', startTouchScroll); container.removeEventListener ("roată", pe roată); nextButton.removeEventListener ("clic", gotoNext); prevButton.removeEventListener ("clic", gotoPrev); slider.removeEventListener ("focus", onFocus); window.removeEventListener ('redimensionare', measureCarousel); ;
Acum, dacă sună destroyCarousel
și încercați să jucați cu caruselul, nu se întâmplă nimic! Este aproape puțin trist să o vezi așa.
Whew. Asta a fost mult! Cât de departe am venit. Puteți vedea produsul finit la acest CodePen. În această ultimă parte, am adăugat accesibilitatea tastaturii, remodelarea caruselului atunci când se schimbă fereastra de vizualizare, unele distracții adiționale cu fizica primăvară și pasul sfâșietor, dar necesar, de al rupe din nou.
Sper că ți-a plăcut acest tutorial la fel de mult cum mi-a plăcut să scriu. Mi-ar plăcea să vă aud gândurile pe alte căi pe care am putea să le îmbunătățim accesibilitatea sau să adăugăm mai multe atingeri amuzante.