Realizarea de calendare având în vedere accesibilitate și internaționalizare

Realizarea de calendare având în vedere accesibilitate și internaționalizare

Făcând o căutare rapidă aici pe CSS-Tricks, arată câte moduri diferite există de a aborda calendarele. Unii arată cum CSS Grid poate crea aspectul eficient. Unii încearcă să aduce datele reale în amestec. Unele se bazează pe un cadru pentru a ajuta la conducerea statului.

Există multe considerente atunci când construiți o componentă de calendar - mult mai mult decât ceea ce este tratat în articolele pe care le-am legat. Dacă vă gândiți bine, calendarele sunt pline de nuanțe, de la gestionarea fusurilor orare și a formatelor de date până la localizare și chiar asigurarea faptului că datele curg de la o lună la alta... și asta înainte de a intra în considerații suplimentare privind accesibilitatea și aspectul, în funcție de locul în care se află calendarul. este afișat și altele.

Mulți dezvoltatori se tem de Date() obiect și rămâneți cu biblioteci mai vechi, cum ar fi moment.js. Dar, deși există multe „obstacuri” când vine vorba de date și formatare, JavaScript are o mulțime de API-uri și chestii grozave de ajutor!

Grila calendarului ianuarie 2023.
Realizarea de calendare având în vedere accesibilitate și internaționalizare

Nu vreau să recreez roata aici, dar vă voi arăta cum putem obține un calendar bun cu JavaScript vanilla. Vom cerceta accesibilitate, folosind marcaj semantic și ușor de citit de ecran <time> -etichete — precum și internaționalizare și formatare, folosind Intl.Locale, Intl.DateTimeFormat și Intl.NumberFormat-API-uri.

Cu alte cuvinte, facem un calendar... numai fără dependențele suplimentare pe care le puteți vedea de obicei folosite într-un tutorial ca acesta și cu unele dintre nuanțe pe care s-ar putea să nu le vedeți de obicei. Și, în acest proces, sper că veți câștiga o nouă apreciere pentru lucrurile mai noi pe care le poate face JavaScript, în timp ce vă faceți o idee despre felul de lucruri care îmi trec prin minte când pun ceva ca acesta împreună.

În primul rând, denumirea

Cum ar trebui să numim componenta noastră de calendar? În limba mea maternă, s-ar numi „element kalender”, așa că să-l folosim și să scurtăm asta la „Kal-El” - cunoscut și ca Numele lui Superman pe planeta Krypton.

Să creăm o funcție pentru a face lucrurile să meargă:

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

Această metodă va reda o singură lună. Mai târziu vom numi această metodă de la [...Array(12).keys()] a reda un an întreg.

Date inițiale și internaționalizare

Unul dintre lucrurile comune pe care le face un calendar online tipic este să evidențieze data curentă. Deci, să creăm o referință pentru asta:

const today = new Date();

În continuare, vom crea un „obiect de configurare” pe care îl vom îmbina cu opționalul settings obiectul metodei primare:

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

Verificăm dacă elementul rădăcină (<html>) conține a lang-atribut cu localizare info; în caz contrar, vom reveni la utilizare en-US. Acesta este primul pas spre internaţionalizarea calendarului.

De asemenea, trebuie să stabilim ce lună să afișăm inițial când este redat calendarul. De aceea am extins config obiect cu primarul date. În acest fel, dacă nu este furnizată nicio dată în settings obiect, vom folosi today referinta in schimb:

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

Avem nevoie de puține mai multe informații pentru a formata corect calendarul pe baza localizării. De exemplu, este posibil să nu știm dacă prima zi a săptămânii este duminică sau luni, în funcție de locație. Dacă avem informații, grozav! Dar dacă nu, îl vom actualiza folosind Intl.Locale API. API-ul are un weekInfo obiect care returnează un firstDay proprietate care ne oferă exact ceea ce căutăm fără nicio bătaie de cap. De asemenea, putem afla ce zile ale săptămânii sunt alocate weekend:

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

Din nou, creăm alternative. „Prima zi” a săptămânii pentru en-US este duminică, deci este implicit la o valoare de 7. Acest lucru este puțin confuz, deoarece getDay metodă în JavaScript returnează zilele ca [0-6], În cazul în care 0 este duminică... nu mă întreba de ce. Weekend-urile sunt sâmbătă și duminică, deci [6, 7].

Înainte să avem Intl.Locale API și al acestuia weekInfo metoda, a fost destul de greu să creezi un calendar internațional fără multe **obiecte și matrice cu informații despre fiecare locație sau regiune. În zilele noastre, este ușor-peasy. Dacă trecem înăuntru en-GB, metoda returnează:

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

Într-o țară ca Brunei (ms-BN), weekendul este vineri și duminică:

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

S-ar putea să vă întrebați ce anume minimalDays proprietatea este. Asta este cele mai puține zile necesare în prima săptămână a unei luni pentru a fi socotite ca o săptămână întreagă. În unele regiuni, poate fi doar o zi. Pentru alții, ar putea fi șapte zile întregi.

În continuare, vom crea un render metoda din cadrul nostru kalEl-metodă:

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

Mai avem nevoie de câteva date cu care să lucrăm înainte de a reda ceva:

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

Ultima este a Boolean care verifică dacă today există în luna pe care urmează să o redăm.

Markup semantic

Vom aprofunda redarea într-un moment. Dar mai întâi, vreau să mă asigur că detaliile pe care le-am configurat au etichete HTML semantice asociate. Configurarea imediată din cutie ne oferă beneficii de accesibilitate încă de la început.

Înveliș pentru calendar

În primul rând, avem învelișul non-semantic: <kal-el>. Este în regulă pentru că nu există o semantică <calendar> etichetă sau ceva de genul ăsta. Dacă nu am face un element personalizat, <article> ar putea fi elementul cel mai potrivit, deoarece calendarul ar putea sta pe propria pagină.

Nume de luni

<time> element va fi unul important pentru noi, deoarece ajută la traducerea datelor într-un format pe care cititorii de ecran și motoarele de căutare îl pot analiza mai precis și mai consecvent. De exemplu, iată cum putem transmite „ianuarie 2023” în marcajul nostru:

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

Numele zilelor

Rândul de deasupra datelor calendarului care conține numele zilelor săptămânii poate fi dificil. Este ideal dacă putem scrie numele complete pentru fiecare zi — de exemplu, duminică, luni, marți etc. — dar asta poate ocupa mult spațiu. Deci, să prescurtăm numele pentru moment în interiorul unui <ol> unde fiecare zi este a <li>:

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

Am putea deveni dificil cu CSS să obținem tot ce este mai bun din ambele lumi. De exemplu, dacă am modificat un pic de markup astfel:

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

… primim numele complete implicit. Apoi putem „ascunde” numele complet atunci când spațiul se epuizează și să afișăm title atribut in schimb:

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

Dar, nu mergem așa pentru că Intl.DateTimeFormat API-ul poate ajuta și aici. Vom ajunge la asta în secțiunea următoare când vom acoperi randarea.

Numerele zilei

Fiecare dată din grila calendarului primește un număr. Fiecare număr este un articol din listă (<li>) într-o listă ordonată (<ol>), și inline <time> eticheta cuprinde numărul real.

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

Și, deși nu plănuiesc să fac încă niciun stil, știu că voi dori o modalitate de a stila numerele datei. Acest lucru este posibil așa cum este, dar vreau, de asemenea, să pot stila numerele din zilele lucrătoare în mod diferit față de numerele de weekend, dacă este necesar. Deci, voi include data-* atribute special pentru asta: data-weekend și data-today.

Numerele săptămânii

Există 52 de săptămâni într-un an, uneori 53. Deși nu este foarte comun, poate fi plăcut să afișați numărul pentru o anumită săptămână în calendar pentru context suplimentar. Îmi place să-l am acum, chiar dacă nu ajung să nu îl folosesc. Dar îl vom folosi pe deplin în acest tutorial.

Vom folosi un data-weeknumber atribut ca un cârlig de stil și includeți-l în marcajul pentru fiecare dată care este prima dată a săptămânii.

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

de redare

Să punem calendarul pe o pagină! Știm deja asta <kal-el> este numele elementului nostru personalizat. Primul lucru pe care trebuie să-l configuram este să setăm firstDay proprietate de pe el, astfel încât calendarul știe dacă duminica sau altă zi este prima zi a săptămânii.

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

Vom folosi literale șablon pentru a reda marcajul. Pentru a formata datele pentru un public internațional, vom folosi Intl.DateTimeFormat API, folosind din nou locale am precizat mai devreme.

Luna si anul

Când numim month, putem stabili dacă vrem să folosim long nume (de exemplu februarie) sau short nume (ex. feb.). Să folosim long nume deoarece este titlul deasupra calendarului:

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

Numele zilelor săptămânii

Pentru zilele săptămânii afișate deasupra grilei de date, avem nevoie de ambele long (de exemplu „duminică”) și short (abreviat, adică „Soarele”) nume. În acest fel, putem folosi numele „scurt” atunci când calendarul are puțin spațiu:

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

Să facem o mică metodă de ajutor care face puțin mai ușor să apelezi pe fiecare:

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

Iată cum invocăm asta în șablon:

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

Numerele zilei

Și în sfârșit, zilele, învelite într-un <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('')}

Să descompunem asta:

  1. Creăm o matrice „dummy”, bazată pe variabila „număr de zile”, pe care o vom folosi pentru a repeta.
  2. Creăm un day variabilă pentru ziua curentă în iterație.
  3. Remediem discrepanța dintre Intl.Locale API și getDay().
  4. În cazul în care day este egal cu today, adăugăm a data-* atribut.
  5. În cele din urmă, returnăm <li> element ca șir cu date îmbinate.
  6. tabindex="0" face ca elementul să fie focalizat, atunci când utilizați navigarea de la tastatură, după orice valori pozitive de tabindex (Notă: ar trebui nu adăuga pozitiv tabindex-valori)

La „Tastați” numerele în datetime atribut, folosim o mică metodă de ajutor:

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

Numărul săptămânii

Din nou, „numărul săptămânii” este locul în care o săptămână se încadrează într-un calendar de 52 de săptămâni. Folosim o mică metodă de ajutor și pentru asta:

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

Nu eu am scris asta getWeek-metodă. Este o versiune curățată a acest scenariu.

Si asta e! Mulțumită Intl.Locale, Intl.DateTimeFormat și Intl.NumberFormat API-uri, acum putem schimba pur și simplu lang-atributul <html> element pentru a schimba contextul calendarului pe baza regiunii curente:

Grila calendarului ianuarie 2023.
de-DE
Grila calendarului ianuarie 2023.
fa-IR
Grila calendarului ianuarie 2023.
zh-Hans-CN-u-nu-hanidec

Stilizarea calendarului

S-ar putea să vă amintiți cum toate zilele sunt doar una <ol> cu articole din listă. Pentru a le transforma într-un calendar care poate fi citit, ne scufundăm în lumea minunată a CSS Grid. De fapt, putem reutiliza aceeași grilă din un șablon de calendar de pornire chiar aici pe CSS-Tricks, dar a actualizat o smidge cu :is() pseudo relațional pentru a optimiza codul.

Observați că definesc variabile CSS configurabile pe parcurs (și prefixez-le cu ---kalel- pentru a evita conflictele).

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;
}
Grilă de calendar cu șapte coloane cu linii de grilă afișate.
Realizarea de calendare având în vedere accesibilitate și internaționalizare

Să desenăm margini în jurul numerelor datei pentru a le separa vizual:

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

Grila cu șapte coloane funcționează bine atunci când este prima zi a lunii de asemenea prima zi a săptămânii pentru localitatea selectată). Dar asta este mai degrabă excepția decât regula. De cele mai multe ori, va trebui să schimbăm prima zi a lunii la o altă zi a săptămânii.

Afișează prima zi a lunii care se încadrează într-o zi de joi.
Realizarea de calendare având în vedere accesibilitate și internaționalizare

Amintește-ți tot în plus data-* atributele pe care le-am definit când ne scriem marcajul? Ne putem conecta la acestea pentru a actualiza ce coloană a grilei (--kalel-li-gc) primul număr de dată al lunii este plasat la:

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

În acest caz, ne întindem de la prima coloană grilă la a patra coloană grilă - care va „împinge” automat următorul articol (Ziua 2) la a cincea coloană grilă și așa mai departe.

Să adăugăm puțin stil datei „actuale”, astfel încât să iasă în evidență. Acestea sunt doar stilurile mele. Aici poți să faci ceea ce îți dorești.

[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;
}

Îmi place ideea de a stila numerele date pentru weekenduri diferit față de zilele lucrătoare. Voi folosi o culoare roșiatică pentru a le coafa. Rețineți că putem ajunge la :not() pseudo-clasă pentru a le selecta, lăsând doar data curentă:

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

A, și să nu uităm de numerele săptămânii care merg înainte de primul număr de dată al fiecărei săptămâni. Am folosit un data-weeknumber în markup pentru asta, dar numerele nu se vor afișa de fapt decât dacă le dezvăluim cu CSS, ceea ce putem face pe ::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 */
}

Am terminat tehnic în acest moment! Putem reda o grilă de calendar care arată datele pentru luna curentă, completă cu considerații pentru localizarea datelor în funcție de localitate și asigurându-ne că calendarul utilizează o semantică adecvată. Și tot ce am folosit a fost vanilla JavaScript și CSS!

Dar să luăm asta Încă un pas...

Redând un an întreg

Poate că trebuie să afișați un an întreg de date! Deci, în loc să redați luna curentă, este posibil să doriți să afișați toate grilele de luni pentru anul curent.

Ei bine, lucrul bun despre abordarea pe care o folosim este că putem numi render metoda de câte ori dorim și doar să schimbăm întregul care identifică luna pe fiecare instanță. Să-l numim de 12 ori pe baza anului curent.

la fel de simplu ca si numirea render-metoda de 12 ori și doar schimbați întregul pentru month - i:

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

Este probabil o idee bună să creați un nou wrapper părinte pentru anul redat. Fiecare grilă de calendar este a <kal-el> element. Să numim noul înveliș părinte <jor-el>, În cazul în care Jor-El este numele tatălui lui Kal-El.

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

Putem folosi <jor-el> pentru a crea o grilă pentru grilele noastre. Deci meta!

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

Demo finală

Bonus: Calendar confetti

Am citit o carte excelentă numită Crearea și spargerea rețelei zilele trecute și am dat peste acest frumos „afiș de Anul Nou”:

Making Calendars With Accessibility and Internationalization in Mind PlatoBlockchain Data Intelligence. Vertical Search. Ai.
Sursa: Crearea și spargerea rețelei (ediția a doua) de Timothy Samara

M-am gândit că putem face ceva similar fără a schimba nimic în HTML sau JavaScript. Mi-am luat libertatea de a include nume complete luni de zile și numere în loc de nume de zi, pentru a le face mai ușor de citit. Bucurați-vă!

Timestamp-ul:

Mai mult de la CSS Trucuri