Dit is het derde en laatste deel van onze Create the Perfect Carousel-lessenreeks. In deel 1 evalueerden we de carrousels op Netflix en Amazon, twee van de meest gebruikte carrousels ter wereld. We hebben onze carrousel opgezet en een touch-scroll geïmplementeerd.
In deel 2 hebben we horizontale muisschuiven, paginering en een voortgangsindicator toegevoegd. Boom.
Nu, in ons laatste deel, gaan we kijken naar de duistere en vaak vergeten wereld van toetsenbordtoegankelijkheid. We passen onze code aan om de carrousel opnieuw te meten wanneer de viewportgrootte verandert. En tot slot zullen we een paar laatste details met behulp van de lente natuurkunde.
Je kunt met deze CodePen verdergaan waar we gebleven waren.
Het is waar dat de meeste gebruikers niet afhankelijk zijn van toetsenbordnavigatie, dus helaas vergeten we soms onze gebruikers die dat wel doen. In sommige landen is het onwettig om een ontoegankelijke website te verlaten. Maar erger nog, het is een flinke zet.
Het goede nieuws is dat het meestal gemakkelijk te implementeren is! In feite doen browsers het grootste deel van het werk voor ons. Serieus: probeer door de carrousel te tikken die we hebben gemaakt. Omdat we semantische opmaak hebben gebruikt, is dit al mogelijk!
Behalve, merkt u, onze navigatieknoppen verdwijnen. Dit komt omdat de browser geen focus op een element buiten onze viewport toestaat. Dus ook al hebben we dat overloop verborgen
ingesteld, kunnen we de pagina niet horizontaal schuiven; anders scrolt de pagina inderdaad om het element met focus te tonen.
Dit is goed, en het zou naar mijn mening kwalificeren als "bruikbaar", hoewel niet bepaald heerlijk.
De carrousel van Netflix werkt ook op deze manier. Maar omdat het merendeel van hun titels lui is geladen en ze ook passief toegankelijk zijn via het toetsenbord (wat betekent dat ze geen specifieke code hebben geschreven om daarmee om te gaan), kunnen we eigenlijk geen titels selecteren die verder gaan dan de weinige die we hebben al geladen. Het ziet er ook verschrikkelijk uit:
We kunnen het beter doen.
focus
EvenementOm dit te doen, gaan we luisteren naar de focus
evenement dat op een item in de carrousel vuurt. Wanneer een item focus krijgt, gaan we het om zijn positie vragen. Dan zullen we dat controleren tegen sliderX
en sliderVisibleWidth
om te zien of dat item zich binnen het zichtbare venster bevindt. Als dat niet het geval is, pagineren we ernaar met dezelfde code die we in deel 2 hebben geschreven.
Aan het einde van de carrousel
functie, voeg deze gebeurtenislistener toe:
slider.addEventListener ('focus', onFocus, true);
U zult merken dat we een derde parameter hebben opgegeven, waar
. In plaats van een gebeurtenislistener aan elk item toe te voegen, kunnen we de gebeurtenisdelegatie gebruiken om naar gebeurtenissen te luisteren op slechts één element, hun directe bovenliggend element. De focus
evenement blaast niet, dus waar
vertelt de luisteraar van het evenement om te luisteren naar de gevangen nemen fase, de fase waarin de gebeurtenis op elk element van de venster
door naar het doelwit (in dit geval het item dat focus ontvangt).
Boven ons groeiende blok van gebeurtenislisteners, voeg de onFocus
functie:
functie onFocus (e)
We zullen in deze functie werken voor de rest van deze sectie.
We moeten de items meten links
en rechts
offset en controleer of elk punt buiten het momenteel zichtbare gebied valt.
Het item wordt geleverd door de evenementen doelwit
parameter, en we kunnen het meten met getBoundingClientRect
:
const left, right = e.target.getBoundingClientRect ();
links
en rechts
zijn relatief ten opzichte van de uitkijk postje, niet de schuifregelaar. Dus we moeten de carrouselcontainers pakken links
gecompenseerd om daar rekening mee te houden. In ons voorbeeld zal dit zijn 0
, maar om de carrousel robuust te maken, moet hij rekening houden met overal geplaatst te worden.
const carouselLeft = container.getBoundingClientRect (). left;
Nu kunnen we een eenvoudige controle uitvoeren om te zien of het item buiten het zichtbare gebied van de schuifregelaar valt en in die richting pagineren:
als (links < carouselLeft) gotoPrev(); else if (right > carouselLeft + sliderVisibleWidth) gotoNext ();
Nu, wanneer we rondsluipen, begint de carrousel vol vertrouwen rond te draaien met onze toetsenbordfocus! Slechts een paar regels code om meer liefde aan onze gebruikers te tonen.
Het is u misschien opgevallen dat u deze zelfstudie volgt: als u de grootte van de viewport van uw browser wijzigt, wordt de carrousel niet meer goed gepagineerd. Dit komt omdat we de breedte ten opzichte van het zichtbare gebied slechts eenmaal hebben gemeten, op het moment van initialisatie.
Om ervoor te zorgen dat onze carrousel zich correct gedraagt, moeten we een deel van onze meetcode vervangen door een nieuwe gebeurtenislistener die wordt afgevuurd venster
resizes.
Nu, aan het begin van uw carrousel
functie, net na de regel waar we definiëren voortgangsbalk
, we willen drie hiervan vervangen const
metingen met laat
, omdat we ze gaan veranderen als het kijkvenster verandert:
const totalItemsWidth = getTotalItemsWidth (items); const maxXOffset = 0; laat minXOffset = 0; laat sliderVisibleWidth = 0; laat clampXOffset;
Vervolgens kunnen we de logica verplaatsen die eerder deze waarden naar een nieuw heeft berekend measureCarousel
functie:
function measureCarousel () sliderVisibleWidth = slider.offsetWidth; minXOffset = - (totalItemsWidth - sliderVisibleWidth); clampXOffset = clamp (minXOffset, maxXOffset);
We willen deze functie onmiddellijk aanroepen, dus we stellen deze waarden nog steeds bij initialisatie. Op de volgende regel bel je measureCarousel
:
measureCarousel ();
De carrousel zou precies zoals voorheen moeten werken. Om het formaat van het venster bij te werken, voegen we deze gebeurtenislistener eenvoudig helemaal aan het einde toe carrousel
functie:
window.addEventListener ('resize', measureCarousel);
Als u de grootte van de carrousel wijzigt en probeert te pagineren, blijft deze werken zoals verwacht.
Het is de moeite waard om te overwegen dat u in de echte wereld meerdere carrousels op dezelfde pagina kunt hebben, waardoor de prestatie-impact van deze meetcode met dat bedrag wordt vermenigvuldigd.
Zoals we in deel 2 kort hebben besproken, is het niet verstandig om vaker zware berekeningen uit te voeren dan je zou moeten doen. Met pointer- en scrollgebeurtenissen hebben we aangegeven dat je die eenmaal per frame wilt uitvoeren om 60fps te behouden. Resize-evenementen zijn een beetje anders, omdat het hele document opnieuw zal worden geplaatst, waarschijnlijk het meest resource-intensieve moment dat een webpagina zal tegenkomen.
We hoeven de carrousel pas opnieuw te meten als de gebruiker klaar is met het wijzigen van het formaat van het venster, omdat er in de tussentijd geen interactie mee is. We kunnen onze pakken measureCarousel
functie in een speciale functie genaamd a ontdendering.
Een debounce-functie zegt in feite: "Vuur deze functie alleen op als deze niet is ingeroepen X
milliseconden. "Je kunt meer lezen over debounce op de uitstekende primer van David Walsh en ook een voorbeeldcode ophalen.
Tot nu toe hebben we een redelijk goede carrousel gemaakt. Het is toegankelijk, het animeert mooi, het werkt over aanraking en muis, en het biedt een grote hoeveelheid ontwerpflexibiliteit op een manier die door naturel scrollende carrousels niet is toegestaan.
Maar dit is niet de tutorialserie "Create a pretty good carousel". Het is tijd voor ons om een beetje te pronken, en om dat te doen, hebben we een geheim wapen. Springs.
We gaan twee interacties toevoegen met behulp van veren. Eén voor aanraken en één voor paginering. Ze laten de gebruiker op een leuke en speelse manier weten dat ze het einde van de carrousel hebben bereikt.
Laten we eerst een sleepboot in iOS-stijl toevoegen wanneer een gebruiker probeert de schuifregelaar voorbij de grenzen te schuiven. Momenteel beperken we het gebruik van de schuifbalk clampXOffset
. Laten we in plaats daarvan dit vervangen door een code die een ruk toepast wanneer de berekende offset buiten de grenzen valt.
Ten eerste moeten we onze lente importeren. Er is een transformator genoemd nonlinearSpring
die een exponentieel toenemende kracht toepast tegen het aantal dat we hem leveren, naar een oorsprong
. Wat betekent dat hoe verder we aan de schuif trekken, hoe meer hij zal terugtrekken. We kunnen het als volgt importeren:
const applyOffset, clamp, nonlinearSpring, pipe = transform;
In de determineDragDirection
functie, we hebben deze code:
action.output (pipe ((x) => x, applyOffset (action.x.get (), sliderX.get ()), clampXOffset, (v) => sliderX.set (v)));
Net erboven, laten we onze twee veren maken, één voor elke scrolllimiet van de carrousel:
const elasticiteit = 5; const tugLeft = nonlinearSpring (elasticiteit, maxXOffset); const tugRight = nonlinearSpring (elasticiteit, minXOffset);
Beslissen over een waarde voor elasticiteit
is een kwestie van rond spelen en zien wat goed aanvoelt. Een te laag aantal, en de veer voelt te stijf aan. Te hoog en je zult de ruk ervan niet opmerken, of erger nog, het duwt de slider nog verder weg van de vinger van de gebruiker!
Nu hoeven we alleen maar een eenvoudige functie te schrijven die een van deze veren toepast als de geleverde waarde buiten het toegestane bereik valt:
const applySpring = (v) => if (v> maxXOffset) return tugLeft (v); als (v < minXOffset) return tugRight(v); return v; ;
We kunnen vervangen clampXOffset
in de bovenstaande code met applySpring
. Als u de schuifregelaar voorbij de grenzen trekt, trekt deze terug!
Wanneer we echter de veer loslaten, knipt hij zonder pardon op zijn plaats. We willen onze wijzigen stopTouchScroll
functie, die op dit moment omgaat met momentum scrollen, om te controleren of de schuifregelaar nog steeds buiten het toegestane bereik valt en, zo ja, een veer toepassen met de fysica
actie in plaats daarvan.
De fysica
actie is ook in staat om veren te modelleren. We moeten het gewoon voorzien de lente
en naar
eigenschappen.
In stopTouchScroll
, verplaats de bestaande schuif fysica
initialisatie naar een stukje logica die ervoor zorgt dat we binnen de scrolllimieten blijven:
const currentX = sliderX.get (); if (currentX < minXOffset || currentX > maxXOffset) else action = physics (from: currentX, velocity: sliderX.getVelocity (), frictie: 0.2). output (pipe (clampXOffset, (v) => sliderX.set (v))). start ();
Binnen de eerste zin van de als
verklaring, we weten dat de schuif buiten de schuiflimieten valt, dus we kunnen onze veer toevoegen:
action = physics (from: currentX, to: (currentX < minXOffset) ? minXOffset : maxXOffset, spring: 800, friction: 0.92 ).output((v) => sliderX.set (v)) .start ();
We willen een veer creëren die pittig en responsief aanvoelt. Ik heb een relatief hoge gekozen de lente
waarde om een strakke "pop" te hebben, en ik heb de wrijving
naar 0.92
om een beetje stuiteren toe te staan. U kunt dit instellen 1
om de bounce helemaal te elimineren.
Als een beetje huiswerk, vervang het clampXOffset
in de uitgang
functie van de scroll fysica
met een functie die een soortgelijke veer triggert wanneer de x-offset zijn grenzen bereikt. In plaats van de huidige abrupte stop, probeer hem aan het einde zacht te laten stuiteren.
Touch-gebruikers krijgen altijd de lente-goedheid, toch? Laten we die liefde delen met desktopgebruikers door te detecteren wanneer de carrousel de scrolllimieten heeft bereikt en een indicatieve ruk te hebben om de gebruiker duidelijk en met vertrouwen te laten zien dat ze aan het einde zijn..
Ten eerste willen we de paginaknoppen uitschakelen wanneer de limiet is bereikt. Laten we eerst een CSS-regel toevoegen die de knoppen opmaakt om aan te geven dat ze dat zijn invalide
. In de knop
regel, voeg toe:
overgang: achtergrond 200ms lineair; & .disabled background: #eee;
We gebruiken hier een klas in plaats van de meer semantische invalide
kenmerk omdat we nog steeds klikgebeurtenissen willen vastleggen, zoals de naam al aangeeft, invalide
zou blokkeren.
Voeg dit toe invalide
klasse aan de Prev-knop, omdat elke carrousel het leven begint met een 0
offset:
Op weg naar de top van carrousel
, maak een nieuwe functie genaamd checkNavButtonStatus
. We willen dat deze functie eenvoudig de aangeboden waarde controleert tegen minXOffset
en maxXOffset
en stel de knop in invalide
klasse dienovereenkomstig:
functie 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');
Het zou verleidelijk zijn om dit elke keer te noemen sliderX
veranderingen. Als we dat zouden doen, zouden de knoppen beginnen te knipperen wanneer een veer rond de scroll-grenzen oscilleerde. Het zou ook tot raar leiden gedrag als een van de knoppen werd ingedrukt tijdens een van die lente-animaties. De "scroll end" sleepboot moet altijd vuren als we aan het einde van de carrousel zijn, zelfs als er een voorjaarsanimatie is die hem wegtrekt van het absolute einde.
We moeten dus selectiever zijn over wanneer deze functie moet worden aangeroepen. Het lijkt verstandig om het te noemen:
Op de laatste regel van de onWheel
, toevoegen checkNavButtonStatus (kunnen we nieuwe);
.
Op de laatste regel van ga naar
, toevoegen checkNavButtonStatus (TargetX);
.
En tot slot, aan het einde van determineDragDirection
, en in de momentum scroll clause (de code binnen de anders
) van stopTouchScroll
, vervangen:
(v) => sliderX.set (v)
Met:
(v) => sliderX.set (v); checkNavButtonStatus (v);
Nu is het enige dat nog moet worden gewijzigd gotoPrev
en gotoNext
om hun classList voor hun triggeringknop te controleren invalide
en alleen pagineren als het afwezig is:
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);
De notifyEnd
functie is gewoon een andere fysica
lente, en het ziet er als volgt uit:
function notifyEnd (delta, targetOffset) if (actie) action.stop (); action = physics (from: sliderX.get (), to: targetOffset, velocity: 2000 * delta, spring: 300, friction: 0.9) .output ((v) => sliderX.set (v)) .start ( );
Speel daar eens mee en opnieuw, tweak de fysica
params naar wens.
Er is nog maar één kleine fout over. Wanneer de schuif voorbij zijn meest linkse grens springt, wordt de voortgangsbalk omgekeerd. We kunnen dit snel oplossen door het volgende te vervangen:
progressBarRenderer.set ('scaleX', voortgang);
Met:
progressBarRenderer.set ('scaleX', Math.max (progress, 0));
Wij konvoorkomen dat het de andere kant op stuitert, maar persoonlijk vind ik het best cool dat het de veerbeweging weerspiegelt. Het ziet er gewoon raar uit als het binnenstebuiten wordt gekeerd.
Met toepassingen van één pagina duren websites langer in de sessie van een gebruiker. Vaak, zelfs wanneer de "pagina" verandert, gebruiken we nog steeds dezelfde JS-runtime als bij de initiële belasting. We kunnen niet op een schone lei vertrouwen elke keer dat de gebruiker op een link klikt, en dat betekent dat we onszelf moeten opruimen om te voorkomen dat gebeurtenislisteners op dode elementen schieten.
In React wordt deze code in de componentWillLeave
methode. Vue gebruikt beforeDestroy
. Dit is een pure JS-implementatie, maar we kunnen nog steeds een vernietigingsmethode bieden die in beide frameworks hetzelfde zou werken.
Tot nu toe, onze carrousel
functie heeft niets teruggezonden. Laten we dat veranderen.
Wijzig eerst de laatste regel, de regel die roept carrousel
, naar:
const destroyCarousel = carrousel (document.querySelector ('. container'));
We komen één ding terug van carrousel
, een functie die al onze evenement luisteraars losmaakt. Helemaal aan het einde van de carrousel
functie, schrijf:
return () => container.removeEventListener ('touchstart', startTouchScroll); container.removeEventListener ('wheel' on wheel); nextButton.removeEventListener ('click', gotoNext); prevButton.removeEventListener ('click', gotoPrev); slider.removeEventListener ('focus', onFocus); window.removeEventListener ('resize', measureCarousel); ;
Nu, als u belt destroyCarousel
en probeer met de carrousel te spelen, er gebeurt niets! Het is bijna een beetje triest om het zo te zien.
Oef. Dat was veel! Hoe ver we gekomen zijn. U kunt het eindproduct zien op deze CodePen. In dit laatste deel hebben we toetsenbordtoegankelijkheid toegevoegd, de carrousel opnieuw gemeten wanneer de viewport verandert, enkele leuke toevoegingen met veerfysica en de hartverscheurende maar noodzakelijke stap om alles weer naar beneden te halen.
Ik hoop dat je deze tutorial net zo leuk vond als ik het leuk vond om het te schrijven. Ik hoor graag uw mening over verdere manieren om de toegankelijkheid te verbeteren of om meer leuke kleine details toe te voegen.