Lage kalendere med tanke på tilgjengelighet og internasjonalisering

Lage kalendere med tanke på tilgjengelighet og internasjonalisering

Å gjøre et raskt søk her på CSS-Tricks viser hvor mange forskjellige måter det er å nærme seg kalendere på. Noen viser hvordan CSS Grid kan lage oppsettet effektivt. Noen forsøk på å bringe faktiske data inn i blandingen. Noen stole på et rammeverk å hjelpe til med statlig forvaltning.

Det er mange hensyn når du bygger en kalenderkomponent - langt flere enn det som er dekket i artiklene jeg koblet til. Hvis du tenker på det, er kalendere fulle av nyanser, fra håndtering av tidssoner og datoformater til lokalisering og til og med å sørge for at datoer flyter fra en måned til den neste ... og det er før vi i det hele tatt kommer inn på tilgjengelighets- og tilleggsbetraktninger avhengig av hvor kalenderen er. vises og sånt.

Mange utviklere frykter Date() objekt og hold deg til eldre biblioteker som moment.js. Men selv om det er mange "gotchas" når det kommer til datoer og formatering, har JavaScript mange kule APIer og ting å hjelpe!

Januar 2023 kalenderrutenett.
Lage kalendere med tanke på tilgjengelighet og internasjonalisering

Jeg ønsker ikke å gjenskape hjulet her, men jeg skal vise deg hvordan vi kan få en god kalender med vanilje JavaScript. Vi skal se nærmere på tilgjengelighet, ved hjelp av semantisk markup og skjermleservennlig <time> -tags — samt internasjonalisering og formatering, bruker Intl.Locale, Intl.DateTimeFormat og Intl.NumberFormat- API-er.

Med andre ord, vi lager en kalender ... bare uten de ekstra avhengighetene du vanligvis ser brukt i en opplæring som denne, og med noen av nyansene du kanskje ikke vanligvis ser. Og i prosessen håper jeg at du vil få en ny forståelse for nyere ting som JavaScript kan gjøre, samtidig som du får en ide om hva slags ting jeg tenker på når jeg setter sammen noe slikt.

Først og fremst navngivning

Hva skal vi kalle kalenderkomponenten vår? På morsmålet mitt vil det bli kalt "kalenderelement", så la oss bruke det og forkorte det til "Kal-El" - også kjent som Supermans navn på planeten Krypton.

La oss lage en funksjon for å få ting i gang:

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

Denne metoden vil gjengi en enkelt måned. Senere vil vi kalle denne metoden fra [...Array(12).keys()] å gjengi et helt år.

Innledende data og internasjonalisering

En av de vanlige tingene en typisk nettkalender gjør, er å fremheve gjeldende dato. Så la oss lage en referanse for det:

const today = new Date();

Deretter lager vi et "konfigurasjonsobjekt" som vi slår sammen med det valgfrie settings objektet for den primære metoden:

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

Vi sjekker om rotelementet (<html>) inneholder en lang-attributt med lokale info; ellers faller vi tilbake til å bruke en-US. Dette er det første skrittet mot internasjonalisering av kalenderen.

Vi må også bestemme hvilken måned som skal vises først når kalenderen gjengis. Det er derfor vi utvidet config objekt med det primære date. På denne måten, hvis ingen dato er oppgitt i settings objekt, bruker vi today referanse i stedet:

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

Vi trenger litt mer informasjon for å formatere kalenderen riktig basert på lokalitet. For eksempel vet vi kanskje ikke om den første dagen i uken er søndag eller mandag, avhengig av lokalitet. Hvis vi har informasjonen, flott! Men hvis ikke, oppdaterer vi den ved å bruke Intl.Locale API. API-en har en weekInfo objekt som returnerer a firstDay eiendom som gir oss akkurat det vi leter etter uten problemer. Vi kan også se hvilke ukedager som er tilordnet weekend:

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

Igjen skaper vi fallbacks. Ukens "første dag" for en-US er søndag, så den har som standard verdien på 7. Dette er litt forvirrende, som getDay metode i JavaScript returnerer dagene som [0-6], Hvor 0 er søndag ... ikke spør meg hvorfor. Helgene er derfor lørdag og søndag [6, 7].

Før vi hadde Intl.Locale API og dens weekInfo metoden, var det ganske vanskelig å lage en internasjonal kalender uten mange **objekter og matriser med informasjon om hver lokalitet eller region. I dag er det lettvint. Hvis vi går inn en-GB, returnerer metoden:

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

I et land som Brunei (ms-BN), helgen er fredag ​​og søndag:

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

Du lurer kanskje på hva det minimalDays eiendom er. Det er færrest dager som kreves i den første uken i en måned for å bli regnet som en hel uke. I noen regioner kan det bare være én dag. For andre kan det være hele syv dager.

Deretter lager vi en render metode innenfor vår kalEl-metode:

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

Vi trenger fortsatt litt mer data å jobbe med før vi gjengir noe:

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

Den siste er en Boolean som sjekker om today eksisterer i måneden vi er i ferd med å gjengi.

Semantisk markering

Vi kommer til å gå dypere i gjengivelsen på et øyeblikk. Men først vil jeg sørge for at detaljene vi setter opp har semantiske HTML-tagger knyttet til seg. Å sette det opp rett ut av boksen gir oss tilgjengelighetsfordeler fra starten.

Kalenderomslag

Først har vi den ikke-semantiske innpakningen: <kal-el>. Det er greit fordi det ikke er en semantikk <calendar> tag eller noe sånt. Hvis vi ikke laget et tilpasset element, <article> kan være det mest passende elementet siden kalenderen kan stå på sin egen side.

Månedens navn

De <time> element kommer til å bli et stort element for oss fordi det hjelper til med å oversette datoer til et format som skjermlesere og søkemotorer kan analysere mer nøyaktig og konsekvent. For eksempel, her er hvordan vi kan formidle "januar 2023" i markeringen vår:

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

Dagens navn

Raden over kalenderens datoer som inneholder navnene på ukedagene kan være vanskelig. Det er ideelt hvis vi kan skrive ut de fulle navnene for hver dag – f.eks. søndag, mandag, tirsdag osv. – men det kan ta mye plass. Så, la oss forkorte navnene for nå inne i en <ol> hvor hver dag er en <li>:

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

Vi kan bli vanskelige med CSS for å få det beste fra begge verdener. For eksempel, hvis vi endret markeringen litt slik:

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

...vi får de fulle navnene som standard. Vi kan deretter "skjule" hele navnet når plassen er tom og vise title attributt i stedet:

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

Men vi går ikke den veien fordi Intl.DateTimeFormat API kan hjelpe her også. Vi kommer til det i neste avsnitt når vi dekker gjengivelse.

Dagstall

Hver dato i kalendernettet får et tall. Hvert tall er et listeelement (<li>) i en ordnet liste (<ol>), og inline <time> tag omslutter det faktiske antallet.

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

Og selv om jeg ikke planlegger å gjøre noen styling ennå, vet jeg at jeg vil ha en måte å style datotallene på. Det er mulig som det er, men jeg vil også kunne style ukedagstall annerledes enn helgetall hvis jeg trenger det. Så jeg skal inkludere data-* attributter spesielt for det: data-weekend og data-today.

Ukenummer

Det er 52 uker i et år, noen ganger 53. Selv om det ikke er supervanlig, kan det være greit å vise tallet for en gitt uke i kalenderen for ekstra kontekst. Jeg liker å ha den nå, selv om jeg ikke ender opp med å ikke bruke den. Men vi vil bruke det fullt ut i denne opplæringen.

Vi bruker en data-weeknumber attributtet som en stylinghook og inkludere det i markeringen for hver dato som er ukens første date.

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

Rendering

La oss få kalenderen på en side! Det vet vi allerede <kal-el> er navnet på vårt tilpassede element. Det første vi må konfigurere er å stille inn firstDay eiendom på den, slik at kalenderen vet om søndag eller en annen dag er den første dagen i uken.

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

Vi bruker malbokstaver for å gjengi markeringen. For å formatere datoene for et internasjonalt publikum, bruker vi Intl.DateTimeFormat API, igjen ved å bruke locale vi spesifiserte tidligere.

Måneden og året

Når vi ringer til month, kan vi angi om vi vil bruke long navn (f.eks. februar) eller short navn (f.eks. feb.). La oss bruke long navn siden det er tittelen over kalenderen:

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

Ukedagers navn

For ukedager som vises over rutenettet med datoer, trenger vi både long (f.eks. "søndag") og short (forkortet, dvs. "Sol") navn. På denne måten kan vi bruke det "korte" navnet når det er lite plass i kalenderen:

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

La oss lage en liten hjelpemetode som gjør det litt lettere å ringe hver enkelt:

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

Slik bruker vi det i malen:

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

Dagstall

Og til slutt, dagene, pakket inn i en <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('')}

La oss bryte det ned:

  1. Vi lager en "dummy"-array, basert på "antall dager"-variabelen, som vi vil bruke til å iterere.
  2. Vi lager en day variabel for gjeldende dag i iterasjonen.
  3. Vi fikser avviket mellom Intl.Locale API og getDay().
  4. Dersom day er lik today, legger vi til en data-* attributt.
  5. Til slutt returnerer vi <li> element som en streng med sammenslåtte data.
  6. tabindex="0" gjør elementet fokuserbart når du bruker tastaturnavigasjon, etter eventuelle positive tabindex-verdier (Merk: du bør aldri legge til positiv tabindex-verdier)

Til "pad" tallene i datetime attributt, bruker vi en liten hjelpemetode:

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

Ukenummer

Igjen, "ukenummeret" er der en uke faller inn i en 52-ukers kalender. Vi bruker en liten hjelpemetode for det også:

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

Jeg skrev ikke dette getWeek-metode. Det er en ryddet versjon av dette skriptet.

Og det er det! Takk til Intl.Locale, Intl.DateTimeFormat og Intl.NumberFormat APIer, kan vi nå ganske enkelt endre lang-attributtet til <html> element for å endre konteksten til kalenderen basert på gjeldende region:

Januar 2023 kalenderrutenett.
de-DE
Januar 2023 kalenderrutenett.
fa-IR
Januar 2023 kalenderrutenett.
zh-Hans-CN-u-nu-hanidec

Styling av kalenderen

Du husker kanskje hvordan alle dagene bare er én <ol> med listeelementer. For å style disse til en lesbar kalender, dykker vi inn i den fantastiske verdenen til CSS Grid. Faktisk kan vi gjenbruke samme rutenett fra en startkalendermal her på CSS-Tricks, men oppdaterte litt med :is() relasjonspseudo for å optimalisere koden.

Legg merke til at jeg definerer konfigurerbare CSS-variabler underveis (og prefikser dem med ---kalel- for å unngå konflikter).

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;
}
Sju-kolonne kalenderrutenett med rutenettlinjer vist.
Lage kalendere med tanke på tilgjengelighet og internasjonalisering

La oss tegne grenser rundt datotallene for å hjelpe å skille dem visuelt:

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

Rutenettet med syv kolonner fungerer fint når den første dagen i måneden er også den første dagen i uken for den valgte lokaliteten). Men det er unntaket snarere enn regelen. De fleste ganger må vi flytte den første dagen i måneden til en annen ukedag.

Viser den første dagen i måneden som faller på en torsdag.
Lage kalendere med tanke på tilgjengelighet og internasjonalisering

Husk alt det ekstra data-* attributter vi definerte da vi skrev markeringen vår? Vi kan koble til dem for å oppdatere hvilken rutenettkolonne (--kalel-li-gc) månedens første datonummer er plassert på:

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

I dette tilfellet spenner vi fra den første rutenettkolonnen til den fjerde rutenettkolonnen - som automatisk vil "skyve" neste element (dag 2) til den femte rutenettkolonnen, og så videre.

La oss legge til litt stil til den "gjeldende" datoen, så den skiller seg ut. Dette er bare mine stiler. Du kan gjøre det du vil her.

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

Jeg liker ideen om å style datotallene for helger annerledes enn hverdager. Jeg skal bruke en rødlig farge for å style dem. Merk at vi kan strekke oss etter :not() pseudo-klasse for å velge dem mens du lar gjeldende dato være:

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

Å, og la oss ikke glemme ukenumrene som går før det første datonummeret for hver uke. Vi brukte a data-weeknumber attributt i markeringen for det, men tallene vises faktisk ikke med mindre vi avslører dem med CSS, noe vi kan gjøre på ::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 */
}

Vi er teknisk ferdige på dette tidspunktet! Vi kan gjengi et kalenderrutenett som viser datoene for gjeldende måned, komplett med hensyn til lokalisering av data etter lokalitet, og for å sikre at kalenderen bruker riktig semantikk. Og alt vi brukte var vanilje JavaScript og CSS!

Men la oss ta dette Ett steg til...

Gjengir et helt år

Kanskje du må vise et helt år med datoer! Så i stedet for å gjengi gjeldende måned, vil du kanskje vise alle månedsrutene for gjeldende år.

Vel, det fine med tilnærmingen vi bruker er at vi kan kalle render metode så mange ganger vi vil, og bare endre heltallet som identifiserer måneden for hver forekomst. La oss kalle det 12 ganger basert på inneværende år.

så enkelt som å ringe til render-metode 12 ganger, og bare endre heltall for month - i:

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

Det er sannsynligvis en god idé å lage en ny forelderomslag for det gjengitte året. Hvert kalendernett er en <kal-el> element. La oss kalle den nye foreldreinnpakningen <jor-el>, Hvor Jor-El er navnet på faren til Kal-El.

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

Vi kan bruke <jor-el> å lage et rutenett for våre nett. Så 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);
}

Endelig demo

Bonus: Konfetti-kalender

Jeg leste en utmerket bok som heter Lage og bryte rutenettet her om dagen og snublet over denne vakre «nyttårsplakaten»:

Lage kalendere med tilgjengelighet og internasjonalisering i tankene PlatoBlockchain Data Intelligence. Vertikalt søk. Ai.
kilde: Making and Breaking the Grid (2. utgave) av Timothy Samara

Jeg tenkte at vi kunne gjøre noe lignende uten å endre noe i HTML eller JavaScript. Jeg har tatt meg friheten til å inkludere fulle navn i måneder, og tall i stedet for dagnavn, for å gjøre det mer lesbart. Nyt!

Tidstempel:

Mer fra CSS triks