Kalenders maken met toegankelijkheid en internationalisering in het achterhoofd

Kalenders maken met toegankelijkheid en internationalisering in het achterhoofd

Een snelle zoekopdracht hier op CSS-Tricks laat zien hoeveel verschillende manieren er zijn om agenda's te benaderen. Sommigen laten zien hoe CSS Grid kan de lay-out efficiënt maken. Sommigen proberen het breng actuele gegevens in de mix. Sommige vertrouwen op een raamwerk om te helpen met het beheer van de staat.

Er zijn veel overwegingen bij het bouwen van een kalendercomponent - veel meer dan wat wordt behandeld in de artikelen die ik heb gekoppeld. Als je erover nadenkt, zijn agenda's beladen met nuances, van het omgaan met tijdzones en datumnotaties tot lokalisatie en zelfs ervoor zorgen dat datums van de ene maand naar de andere overlopen... wordt weergegeven en zo.

Veel ontwikkelaars vrezen de Date() object en blijf bij oudere bibliotheken zoals moment.js. Maar hoewel er veel "valkuilen" zijn als het gaat om datums en opmaak, heeft JavaScript veel coole API's en dingen om te helpen!

Kalenderraster januari 2023.
Kalenders maken met toegankelijkheid en internationalisering in het achterhoofd

Ik wil het wiel hier niet opnieuw maken, maar ik zal je laten zien hoe we een verdomd goede kalender kunnen krijgen met vanille JavaScript. We zullen ernaar kijken de toegankelijkheid, met behulp van semantische opmaak en schermlezervriendelijk <time> -tags — evenals internationalisering en opmaak, Met de Intl.Locale, Intl.DateTimeFormat en Intl.NumberFormat-API's.

Met andere woorden, we maken een kalender... alleen zonder de extra afhankelijkheden die u normaal gesproken in een zelfstudie als deze ziet, en met enkele nuances die u normaal gesproken niet ziet. En tijdens het proces hoop ik dat je een nieuwe waardering krijgt voor nieuwere dingen die JavaScript kan doen, terwijl je een idee krijgt van het soort dingen waar ik aan denk als ik zoiets in elkaar zet.

Ten eerste, naamgeving

Hoe moeten we onze kalendercomponent noemen? In mijn moedertaal zou het 'kalenderelement' heten, dus laten we dat gebruiken en dat verkorten tot 'Kal-El' — ook bekend als Superman's naam op de planeet Krypton.

Laten we een functie maken om dingen op gang te krijgen:

function kalEl(settings = {}) { ... }

Deze methode zal renderen een enkele maand. Later noemen we deze methode from [...Array(12).keys()] om een ​​heel jaar weer te geven.

Eerste gegevens en internationalisering

Een van de gebruikelijke dingen die een typische online kalender doet, is de huidige datum markeren. Dus laten we daar een referentie voor maken:

const today = new Date();

Vervolgens maken we een "configuratie-object" dat we samenvoegen met het optionele settings object van de primaire methode:

const config = Object.assign( { locale: (document.documentElement.getAttribute('lang') || 'en-US'), today: { day: today.getDate(), month: today.getMonth(), year: today.getFullYear() } }, settings
);

We controleren of het wortelelement (<html>) bevat een lang-kenmerk met lokaal informatie; anders vallen we terug op gebruik en-US. Dit is de eerste stap naar internationalisering van de kalender.

We moeten ook bepalen welke maand in eerste instantie moet worden weergegeven wanneer de kalender wordt weergegeven. Daarom hebben we de config bezwaar maken tegen de primaire date. Op deze manier, als er geen datum is opgegeven in de settings object, gebruiken we de today referentie in plaats daarvan:

const date = config.date ? new Date(config.date) : today;

We hebben wat meer informatie nodig om de kalender op de juiste manier op te maken op basis van de landinstelling. We weten bijvoorbeeld niet of de eerste dag van de week zondag of maandag is, afhankelijk van de landinstelling. Als we de informatie hebben, geweldig! Maar zo niet, dan werken we het bij met behulp van de Intl.Locale API. De API heeft een weekInfo object dat een firstDay eigenschap die ons precies geeft wat we zoeken, zonder enige moeite. We kunnen ook zien welke dagen van de week zijn toegewezen aan de weekend:

if (!config.info) config.info = new Intl.Locale(config.locale).weekInfo || { firstDay: 7, weekend: [6, 7] };

Nogmaals, we creëren fallbacks. De "eerste dag" van de week voor en-US is zondag, dus het is standaard een waarde van 7. Dit is een beetje verwarrend, aangezien de getDay methode in JavaScript retourneert de dagen als [0-6], Waar 0 is zondag... vraag me niet waarom. De weekenden zijn dus zaterdag en zondag [6, 7].

Voordat we de Intl.Locale API en zijn weekInfo methode was het behoorlijk moeilijk om een ​​internationale kalender te maken zonder veel **objecten en arrays met informatie over elke locale of regio. Tegenwoordig is het easy peasy. Als we binnenkomen en-GB, retourneert de methode:

// en-GB
{ firstDay: 1, weekend: [6, 7], minimalDays: 4
}

In een land als Brunei (ms-BN), het weekend is vrijdag en zondag:

// ms-BN
{ firstDay: 7, weekend: [5, 7], minimalDays: 1
}

Je vraagt ​​je misschien af ​​wat dat is minimalDays eigendom is. Dat is de het minste aantal dagen dat in de eerste week van een maand nodig is om als een volledige week te worden geteld. In sommige regio's kan het slechts één dag zijn. Voor anderen kan het een volledige zeven dagen zijn.

Vervolgens maken we een render methode binnen onze kalEl-methode:

const render = (date, locale) => { ... }

We hebben nog wat meer gegevens nodig om mee te werken voordat we iets kunnen renderen:

const month = date.getMonth();
const year = date.getFullYear();
const numOfDays = new Date(year, month + 1, 0).getDate();
const renderToday = (year === config.today.year) && (month === config.today.month);

De laatste is een Boolean dat controleert of today bestaat in de maand die we gaan renderen.

Semantische opmaak

We gaan zo dadelijk dieper in op weergave. Maar eerst wil ik ervoor zorgen dat de details die we instellen gekoppeld zijn aan semantische HTML-tags. Door dat direct uit de doos op te zetten, krijgen we vanaf het begin toegankelijkheidsvoordelen.

Kalenderomslag

Ten eerste hebben we de niet-semantische wrapper: <kal-el>. Dat is prima, want er is geen semantiek <calendar> tag of iets dergelijks. Als we geen aangepast element zouden maken, <article> zou het meest geschikte element kunnen zijn, aangezien de kalender op zijn eigen pagina kan staan.

Maandnamen

De <time> element wordt een grote voor ons omdat het helpt bij het vertalen van datums naar een indeling die schermlezers en zoekmachines nauwkeuriger en consistenter kunnen analyseren. Zo kunnen we bijvoorbeeld "januari 2023" overbrengen in onze opmaak:

<time datetime="2023-01">January <i>2023</i></time>

Dag namen

De rij boven de datums van de kalender met de namen van de dagen van de week kan lastig zijn. Het is ideaal als we de volledige namen voor elke dag kunnen opschrijven — bijvoorbeeld zondag, maandag, dinsdag, enz. — maar dat kan veel ruimte in beslag nemen. Dus laten we de namen voor nu afkorten in een <ol> waar elke dag een is <li>:

<ol> <li><abbr title="Sunday">Sun</abbr></li> <li><abbr title="Monday">Mon</abbr></li> <!-- etc. -->
</ol>

We kunnen lastig worden met CSS om het beste van twee werelden te krijgen. Als we de opmaak bijvoorbeeld een beetje als volgt hebben aangepast:

<ol> <li> <abbr title="S">Sunday</abbr> </li>
</ol>

…we krijgen standaard de volledige namen. We kunnen dan de volledige naam "verbergen" wanneer de ruimte opraakt en de title attribuut in plaats daarvan:

@media all and (max-width: 800px) { li abbr::after { content: attr(title); }
}

Maar we gaan niet die kant op, want de Intl.DateTimeFormat API kan hier ook helpen. Daar komen we in de volgende sectie op terug als we het over renderen hebben.

Dag nummers

Elke datum in het kalenderraster krijgt een nummer. Elk nummer is een lijstitem (<li>) in een geordende lijst (<ol>), en de inline <time> tag omsluit het werkelijke aantal.

<li> <time datetime="2023-01-01">1</time>
</li>

En hoewel ik nog niet van plan ben om aan styling te doen, weet ik dat ik een manier wil om de datumnummers te stylen. Dat is mogelijk zoals het is, maar ik wil ook doordeweekse nummers anders kunnen stylen dan weekendnummers als dat nodig is. Dus ik ga opnemen data-* attributen speciaal daarvoor: data-weekend en data-today.

Weeknummers

Er zijn 52 weken in een jaar, soms 53. Hoewel het niet heel gebruikelijk is, kan het leuk zijn om het nummer voor een bepaalde week in de kalender weer te geven voor extra context. Ik vind het leuk om het nu te hebben, zelfs als ik niet besluit het niet te gebruiken. Maar we zullen het volledig gebruiken in deze zelfstudie.

We gebruiken een data-weeknumber attribuut als stijlhaak en neem het op in de opmaak voor elke datum die de eerste datum van de week is.

<li data-day="7" data-weeknumber="1" data-weekend=""> <time datetime="2023-01-08">8</time>
</li>

Rendering

Laten we de kalender op een pagina krijgen! Dat weten we al <kal-el> is de naam van ons aangepaste element. Het eerste dat we moeten configureren, is het instellen van het firstDay eigenschap erop, zodat de kalender weet of zondag of een andere dag de eerste dag van de week is.

<kal-el data-firstday="${ config.info.firstDay }">

We zullen gebruiken sjabloon literals om de opmaak weer te geven. Om de datums op te maken voor een internationaal publiek, gebruiken we de Intl.DateTimeFormat API, opnieuw met behulp van de locale we eerder specificeerden.

De maand en het jaar

Wanneer we de bellen month, kunnen we instellen of we de long naam (bijvoorbeeld februari) of de short naam (bijv. feb.). Laten we de gebruiken long naam aangezien het de titel boven de kalender is:

<time datetime="${year}-${(pad(month))}"> ${new Intl.DateTimeFormat( locale, { month:'long'}).format(date)} <i>${year}</i>
</time>

Namen van weekdagen

Voor weekdagen die boven het datumraster worden weergegeven, hebben we zowel de long (bijv. "Zondag") en short (afgekort, d.w.z. "Zon") namen. Op deze manier kunnen we de "korte" naam gebruiken wanneer de kalender weinig ruimte heeft:

Intl.DateTimeFormat([locale], { weekday: 'long' })
Intl.DateTimeFormat([locale], { weekday: 'short' })

Laten we een kleine hulpmethode maken die het iets gemakkelijker maakt om ze allemaal te bellen:

const weekdays = (firstDay, locale) => { const date = new Date(0); const arr = [...Array(7).keys()].map(i => { date.setDate(5 + i) return { long: new Intl.DateTimeFormat([locale], { weekday: 'long'}).format(date), short: new Intl.DateTimeFormat([locale], { weekday: 'short'}).format(date) } }) for (let i = 0; i < 8 - firstDay; i++) arr.splice(0, 0, arr.pop()); return arr;
}

Hier is hoe we dat aanroepen in de sjabloon:

<ol> ${weekdays(config.info.firstDay,locale).map(name => ` <li> <abbr title="${name.long}">${name.short}</abbr> </li>`).join('') }
</ol>

Dag nummers

En tot slot, de dagen, verpakt in een <ol> element:

${[...Array(numOfDays).keys()].map(i => { const cur = new Date(year, month, i + 1); let day = cur.getDay(); if (day === 0) day = 7; const today = renderToday && (config.today.day === i + 1) ? ' data-today':''; return ` <li data-day="${day}"${today}${i === 0 || day === config.info.firstDay ? ` data-weeknumber="${new Intl.NumberFormat(locale).format(getWeek(cur))}"`:''}${config.info.weekend.includes(day) ? ` data-weekend`:''}> <time datetime="${year}-${(pad(month))}-${pad(i)}" tabindex="0"> ${new Intl.NumberFormat(locale).format(i + 1)} </time> </li>`
}).join('')}

Laten we dat opsplitsen:

  1. We maken een "dummy"-array, gebaseerd op de variabele "aantal dagen", die we zullen gebruiken om te herhalen.
  2. We creëren een day variabele voor de huidige dag in de iteratie.
  3. We repareren de discrepantie tussen de Intl.Locale API en getDay().
  4. Indien de day is gelijk aan today, we voegen een toe data-* attribuut.
  5. Als laatste keren we de <li> element als een tekenreeks met samengevoegde gegevens.
  6. tabindex="0" maakt het element focusbaar, bij gebruik van toetsenbordnavigatie, na positieve tabindexwaarden (Opmerking: u zou nooit toevoegen positief tabindex-waarden)

Naar "pad" de cijfers in de datetime attribuut, gebruiken we een kleine helpermethode:

const pad = (val) => (val + 1).toString().padStart(2, '0');

Weeknummer

Nogmaals, het "weeknummer" is waar een week valt in een kalender met 52 weken. Ook daarvoor gebruiken we een kleine helpermethode:

function getWeek(cur) { const date = new Date(cur.getTime()); date.setHours(0, 0, 0, 0); date.setDate(date.getDate() + 3 - (date.getDay() + 6) % 7); const week = new Date(date.getFullYear(), 0, 4); return 1 + Math.round(((date.getTime() - week.getTime()) / 86400000 - 3 + (week.getDay() + 6) % 7) / 7);
}

Ik heb dit niet geschreven getWeek-methode. Het is een opgeschoonde versie van dit script.

En dat is het! Dankzij de Intl.Locale, Intl.DateTimeFormat en Intl.NumberFormat API's kunnen we nu eenvoudig de lang-attribuut van de <html> element om de context van de kalender te wijzigen op basis van de huidige regio:

Kalenderraster januari 2023.
de-DE
Kalenderraster januari 2023.
fa-IR
Kalenderraster januari 2023.
zh-Hans-CN-u-nu-hanidec

Het stylen van de kalender

Je herinnert je misschien hoe alle dagen slechts één zijn <ol> met lijstitems. Om deze in een leesbare kalender te stylen, duiken we in de wondere wereld van CSS Grid. In feite kunnen we hetzelfde raster opnieuw gebruiken een sjabloon voor een starterskalender hier op CSS-Tricks, maar een smidge bijgewerkt met de :is() relationele pseudo om de code te optimaliseren.

Merk op dat ik onderweg configureerbare CSS-variabelen definieer (en ze voorvoegsel met ---kalel- om conflicten te voorkomen).

kal-el :is(ol, ul) { display: grid; font-size: var(--kalel-fz, small); grid-row-gap: var(--kalel-row-gap, .33em); grid-template-columns: var(--kalel-gtc, repeat(7, 1fr)); list-style: none; margin: unset; padding: unset; position: relative;
}
Kalenderraster met zeven kolommen met weergegeven rasterlijnen.
Kalenders maken met toegankelijkheid en internationalisering in het achterhoofd

Laten we randen rond de datumnummers tekenen om ze visueel te scheiden:

kal-el :is(ol, ul) li { border-color: var(--kalel-li-bdc, hsl(0, 0%, 80%)); border-style: var(--kalel-li-bds, solid); border-width: var(--kalel-li-bdw, 0 0 1px 0); grid-column: var(--kalel-li-gc, initial); text-align: var(--kalel-li-tal, end); }

Het raster met zeven kolommen werkt prima als het de eerste dag van de maand is ook de eerste dag van de week voor de geselecteerde locale). Maar dat is eerder uitzondering dan regel. Meestal moeten we de eerste dag van de maand verschuiven naar een andere weekdag.

Toont de eerste dag van de maand die op een donderdag valt.
Kalenders maken met toegankelijkheid en internationalisering in het achterhoofd

Onthoud al het extra data-* attributen die we hebben gedefinieerd bij het schrijven van onze markup? We kunnen daarop inhaken om bij te werken welke rasterkolom (--kalel-li-gc) het eerste datumnummer van de maand wordt geplaatst op:

[data-firstday="1"] [data-day="3"]:first-child { --kalel-li-gc: 1 / 4;
}

In dit geval gaan we van de eerste rasterkolom naar de vierde rasterkolom, waardoor het volgende item (dag 2) automatisch naar de vijfde rasterkolom wordt "geduwd", enzovoort.

Laten we een beetje stijl toevoegen aan de "huidige" datum, zodat deze opvalt. Dit zijn gewoon mijn stijlen. Je kunt hier helemaal doen wat je wilt.

[data-today] { --kalel-day-bdrs: 50%; --kalel-day-bg: hsl(0, 86%, 40%); --kalel-day-hover-bgc: hsl(0, 86%, 70%); --kalel-day-c: #fff;
}

Ik hou van het idee om de datumnummers voor weekenden anders te stylen dan weekdagen. Ik ga een roodachtige kleur gebruiken om die te stylen. Merk op dat we kunnen reiken naar de :not() pseudo-class om ze te selecteren terwijl de huidige datum alleen wordt gelaten:

[data-weekend]:not([data-today]) { --kalel-day-c: var(--kalel-weekend-c, hsl(0, 86%, 46%));
}

Oh, en laten we de weeknummers niet vergeten die voor het eerste datumnummer van elke week gaan. We gebruikten een data-weeknumber attribuut in de opmaak daarvoor, maar de getallen worden pas echt weergegeven als we ze onthullen met CSS, wat we kunnen doen op de ::before pseudo-element:

[data-weeknumber]::before { display: var(--kalel-weeknumber-d, inline-block); content: attr(data-weeknumber); position: absolute; inset-inline-start: 0; /* additional styles */
}

We zijn op dit punt technisch klaar! We kunnen een kalenderraster maken dat de datums voor de huidige maand weergeeft, compleet met overwegingen voor het lokaliseren van de gegevens per landinstelling en ervoor te zorgen dat de kalender de juiste semantiek gebruikt. En alles wat we gebruikten was vanille JavaScript en CSS!

Maar laten we dit nemen nog een stap...

Een heel jaar renderen

Misschien moet u een volledig jaar met datums weergeven! Dus in plaats van de huidige maand weer te geven, wilt u misschien alle maandrasters voor het huidige jaar weergeven.

Welnu, het leuke van de aanpak die we gebruiken, is dat we de render methode zo vaak als we willen en verander alleen het gehele getal dat de maand identificeert bij elke instantie. Laten we het 12 keer noemen op basis van het huidige jaar.

net zo eenvoudig als het bellen van de render-methode 12 keer, en verander gewoon het gehele getal voor month - i:

[...Array(12).keys()].map(i => render( new Date(date.getFullYear(), i, date.getDate()), config.locale, date.getMonth() )
).join('')

Het is waarschijnlijk een goed idee om een ​​nieuwe bovenliggende wrapper te maken voor het weergegeven jaar. Elk agendaraster is een <kal-el> element. Laten we de nieuwe bovenliggende wrapper noemen <jor-el>, Waar Jor-El is de naam van de vader van Kal-El.

<jor-el id="app" data-year="true"> <kal-el data-firstday="7"> <!-- etc. --> </kal-el> <!-- other months -->
</jor-el>

We kunnen gebruik maken <jor-el> om een ​​raster voor onze rasters te maken. Meta dus!

jor-el { background: var(--jorel-bg, none); display: var(--jorel-d, grid); gap: var(--jorel-gap, 2.5rem); grid-template-columns: var(--jorel-gtc, repeat(auto-fill, minmax(320px, 1fr))); padding: var(--jorel-p, 0);
}

Laatste demo

Bonus: Confetti-kalender

Ik las een uitstekend boek genaamd Het maken en breken van het raster laatst en kwam deze prachtige "Nieuwjaarsposter" tegen:

Kalenders maken met toegankelijkheid en internationalisering in gedachten PlatoBlockchain Data Intelligence. Verticaal zoeken. Ai.
Bron: Het raster maken en breken (2e editie) door Timothy Samara

Ik dacht dat we iets soortgelijks konden doen zonder iets in de HTML of JavaScript te veranderen. Ik heb de vrijheid genomen om maanden volledige namen op te nemen, en nummers in plaats van dagnamen, om het leesbaarder te maken. Genieten!

Tijdstempel:

Meer van CSS-trucs