Lav kalendere med tilgængelighed og internationalisering i tankerne

Lav kalendere med tilgængelighed og internationalisering i tankerne

At lave en hurtig søgning her på CSS-Tricks viser, hvor mange forskellige måder, der er at gribe kalendere til. Nogle viser hvordan CSS Grid kan skabe layoutet effektivt. Nogle forsøg på at bringe faktiske data ind i blandingen. Nogle stole på en ramme at hjælpe med statsforvaltningen.

Der er mange overvejelser, når du bygger en kalenderkomponent - langt flere end det, der er dækket i de artikler, jeg linkede til. Hvis du tænker over det, er kalendere fyldt med nuancer, fra håndtering af tidszoner og datoformater til lokalisering og endda at sikre, at datoer flyder fra den ene måned til den anden... og det er før vi overhovedet kommer ind på tilgængelighed og yderligere layoutovervejelser afhængigt af, hvor kalenderen er placeret. vises og hvad.

Mange udviklere frygter Date() objekt og hold dig til ældre biblioteker som moment.js. Men selvom der er mange "gotchas" når det kommer til datoer og formatering, har JavaScript en masse fede API'er og ting til at hjælpe!

Januar 2023 kalendergitter.
Lav kalendere med tilgængelighed og internationalisering i tankerne

Jeg ønsker ikke at genskabe hjulet her, men jeg vil vise dig, hvordan vi kan få en god kalender med vanilje JavaScript. Vi vil se nærmere på tilgængelighed, ved hjælp af semantisk markup og skærmlæservenlig <time> -tags — samt internationalisering , formateringVed hjælp af Intl.Locale, Intl.DateTimeFormat , Intl.NumberFormat- API'er.

Med andre ord laver vi en kalender... kun uden de ekstra afhængigheder, du typisk ser brugt i en tutorial som denne, og med nogle af de nuancer, du måske ikke typisk ser. Og i processen håber jeg, at du vil få en ny forståelse for nyere ting, som JavaScript kan gøre, samtidig med at du får en idé om den slags ting, der krydser mig, når jeg sætter sådan noget sammen.

Først og fremmest navngivning

Hvad skal vi kalde vores kalenderkomponent? På mit modersmål ville det blive kaldt "kalenderelement", så lad os bruge det og forkorte det til "Kal-El" - også kendt som Supermans navn på planeten Krypton.

Lad os oprette en funktion til at få tingene i gang:

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

Denne metode vil gengive en enkelt måned. Senere vil vi kalde denne metode fra [...Array(12).keys()] at gengive et helt år.

Indledende data og internationalisering

En af de almindelige ting, en typisk onlinekalender gør, er at fremhæve den aktuelle dato. Så lad os oprette en reference til det:

const today = new Date();

Dernæst opretter vi et "konfigurationsobjekt", som vi fusionerer med det valgfrie settings genstand for den primære metode:

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

Vi tjekker, om rodelementet (<html>) indeholder en lang-attribut med Local info; ellers går vi tilbage til at bruge en-US. Dette er det første skridt hen imod internationalisering af kalenderen.

Vi er også nødt til at bestemme, hvilken måned der først skal vises, når kalenderen gengives. Derfor har vi udvidet config objekt med det primære date. På denne måde, hvis der ikke er angivet nogen dato i settings objekt, bruger vi today reference i stedet:

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

Vi har brug for lidt mere information for korrekt at formatere kalenderen baseret på lokalitet. For eksempel ved vi muligvis ikke, om den første dag i ugen er søndag eller mandag, afhængigt af lokaliteten. Hvis vi har oplysningerne, fantastisk! Men hvis ikke, opdaterer vi det ved hjælp af Intl.Locale API. API'et har en weekInfo objekt der returnerer en firstDay ejendom, der giver os præcis det, vi leder efter uden besvær. Vi kan også få oplyst, hvilke ugedage der er tildelt weekend:

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

Igen skaber vi faldbacks. Ugens "første dag" for en-US er søndag, så den har som standard en værdi på 7. Dette er lidt forvirrende, da getDay metode i JavaScript returnerer dagene som [0-6]Hvor 0 er søndag... spørg mig ikke hvorfor. Weekenderne er altså lørdag og søndag [6, 7].

Før vi havde Intl.Locale API og dens weekInfo metode, var det ret svært at oprette en international kalender uden mange **objekter og arrays med oplysninger om hver lokalitet eller region. I dag er det let-peasy. Hvis vi går ind en-GB, metoden returnerer:

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

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

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

Du kan undre dig over, hvad det er minimalDays ejendom er. Det er den færrest nødvendige dage i den første uge i en måned for at blive talt som en hel uge. I nogle regioner kan det kun være en dag. For andre er det måske hele syv dage.

Dernæst opretter vi en render metode inden for vores kalEl-metode:

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

Vi har stadig brug for nogle flere data at arbejde med, før vi gengiver noget:

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 sidste er en Boolean der tjekker om today eksisterer i den måned, vi er ved at gengive.

Semantisk markering

Vi kommer til at komme dybere i gengivelsen om et øjeblik. Men først vil jeg sikre mig, at de detaljer, vi opsætter, har semantiske HTML-tags tilknyttet. At sætte det op lige fra starten giver os tilgængelighedsfordele fra starten.

Kalenderindpakning

For det første har vi den ikke-semantiske indpakning: <kal-el>. Det er fint, fordi der ikke er en semantik <calendar> tag eller lignende. Hvis vi ikke lavede et tilpasset element, <article> kan være det mest passende element, da kalenderen kunne stå på sin egen side.

Månedens navne

<time> element bliver et stort for os, fordi det hjælper med at oversætte datoer til et format, som skærmlæsere og søgemaskiner kan analysere mere præcist og konsekvent. For eksempel, her er, hvordan vi kan formidle "januar 2023" i vores opmærkning:

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

Dags navne

Rækken over kalenderens datoer, der indeholder navnene på ugedagene, kan være vanskelig. Det er ideelt, hvis vi kan skrive de fulde navne ud for hver dag — f.eks. søndag, mandag, tirsdag osv. — men det kan fylde meget. Så lad os forkorte navnene for nu inde 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 kunne blive vanskelige med CSS for at få det bedste fra begge verdener. For eksempel, hvis vi ændrede opmærkningen lidt som dette:

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

...vi får de fulde navne som standard. Vi kan så "skjule" det fulde navn, når pladsen løber tør og vise title attribut i stedet for:

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

Men vi går ikke den vej, fordi Intl.DateTimeFormat API kan også hjælpe her. Det kommer vi til i næste afsnit, når vi dækker gengivelse.

Dags tal

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

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

Og selvom jeg ikke planlægger at lave nogen styling endnu, ved jeg, at jeg vil have en måde at style datonumrene på. Det er muligt som det er, men jeg vil også gerne kunne style hverdagsnumre anderledes end weekendnumre, hvis jeg har brug for det. Så jeg vil inkludere data-* attributter specifikt til det: data-weekend , data-today.

Ugenumre

Der er 52 uger i et år, nogle gange 53. Selvom det ikke er super almindeligt, kan det være rart at vise tallet for en given uge i kalenderen for yderligere sammenhæng. Jeg kan godt lide at have det nu, selvom jeg ikke ender med ikke at bruge det. Men vi bruger det fuldstændigt i denne tutorial.

Vi bruger en data-weeknumber attribut som en stylinghook og inkludere den i markeringen for hver dato, der er ugens første date.

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

rendering

Lad os få kalenderen på en side! Det ved vi allerede <kal-el> er navnet på vores brugerdefinerede element. Den første ting vi skal konfigurere er at indstille firstDay ejendom på den, så kalenderen ved, om søndag eller en anden dag er den første dag i ugen.

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

Vi bruger skabelon bogstaver for at gengive markeringen. For at formatere datoerne for et internationalt publikum, bruger vi Intl.DateTimeFormat API, igen ved hjælp af locale vi specificerede tidligere.

Måneden og året

Når vi ringer til month, kan vi indstille, om vi vil bruge long navn (f.eks. februar) eller den short navn (f.eks. feb.). Lad os bruge long navn, da det er titlen over kalenderen:

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

Ugedages navne

For ugedage, der vises over gitteret af datoer, har vi brug for både long (f.eks. "søndag") og short (forkortet, dvs. "Sol") navne. På denne måde kan vi bruge det "korte" navn, når kalenderen mangler plads:

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

Lad os lave en lille hjælpemetode, der gør det lidt nemmere at kalde 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;
}

Sådan påberåber vi os det i skabelonen:

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

Dags tal

Og til sidst, dagene, pakket ind 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('')}

Lad os opdele det:

  1. Vi opretter et "dummy"-array baseret på "antal dage"-variablen, som vi vil bruge til at iterere.
  2. Vi opretter en day variabel for den aktuelle dag i iterationen.
  3. Vi retter uoverensstemmelsen mellem Intl.Locale API og getDay().
  4. Hvis day er lig med today, tilføjer vi en data-* attribut.
  5. Til sidst returnerer vi <li> element som en streng med flettede data.
  6. tabindex="0" gør elementet fokuserbart, når du bruger tastaturnavigation, efter eventuelle positive tabindex-værdier (Bemærk: du bør aldrig tilføje positiv tabindex-værdier)

Til "pad" tallene i datetime attribut, bruger vi en lille hjælpemetode:

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

Ugenummer

Igen er "ugenummeret" det sted, hvor en uge falder i en 52-ugers kalender. Vi bruger også en lille hjælpemetode til det:

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 har ikke skrevet dette getWeek-metode. Det er en ryddet udgave af dette script.

Og det er det! Takket være Intl.Locale, Intl.DateTimeFormat , Intl.NumberFormat API'er, kan vi nu blot ændre lang-attribut af <html> element for at ændre konteksten for kalenderen baseret på den aktuelle region:

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

Styling af kalenderen

Du husker måske, hvordan alle dage kun er én <ol> med listepunkter. For at style disse til en læsbar kalender, dykker vi ned i den vidunderlige verden af ​​CSS Grid. Faktisk kan vi genbruge det samme net fra en startkalenderskabelon lige her på CSS-Tricks, men opdateret en lille smule med :is() relationel pseudo for at optimere koden.

Bemærk, at jeg definerer konfigurerbare CSS-variabler undervejs (og præfikser dem med ---kalel- for at undgå 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;
}
Kalendergitter med syv kolonner med gitterlinjer vist.
Lav kalendere med tilgængelighed og internationalisering i tankerne

Lad os tegne grænser omkring datotallene for at hjælpe med at adskille 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); }

Det syv-søjlede gitter fungerer fint, når den første dag i måneden er også den første dag i ugen for den valgte lokalitet). Men det er undtagelsen snarere end reglen. De fleste gange bliver vi nødt til at flytte den første dag i måneden til en anden hverdag.

Viser den første dag i måneden, der falder på en torsdag.
Lav kalendere med tilgængelighed og internationalisering i tankerne

Husk alt det ekstra data-* egenskaber, vi definerede, da vi skrev vores opmærkning? Vi kan tilslutte til dem for at opdatere hvilken gitterkolonne (--kalel-li-gc) månedens første datonummer er placeret på:

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

I dette tilfælde spænder vi fra den første gitterkolonne til den fjerde gitterkolonne - som automatisk vil "skubbe" det næste element (dag 2) til den femte gitterkolonne og så videre.

Lad os tilføje lidt stil til den "aktuelle" dato, så den skiller sig ud. Det er bare mine styles. Du kan fuldstændig gøre, hvad 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 kan godt lide ideen om at style datonumrene til weekender anderledes end hverdage. Jeg vil bruge en rødlig farve til at style dem. Bemærk, at vi kan nå til :not() pseudo-klasse for at vælge dem, mens du lader den aktuelle dato være alene:

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

Åh, og lad os ikke glemme ugenumrene, der går før det første datonummer for hver uge. Vi brugte en data-weeknumber attribut i opmærkningen for det, men tallene vises faktisk ikke, medmindre vi afslører dem med CSS, hvilket vi kan gø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 færdige på dette tidspunkt! Vi kan gengive et kalendergitter, der viser datoerne for den aktuelle måned, komplet med overvejelser om lokalisering af data efter lokalitet og sikring af, at kalenderen bruger korrekt semantik. Og alt vi brugte var vanilje JavaScript og CSS!

Men lad os tage det her et skridt mere...

Gengivelse et helt år

Måske skal du vise et helt år med datoer! Så i stedet for at gengive den aktuelle måned, vil du måske vise alle månedsgitteret for det aktuelle år.

Nå, det gode ved den tilgang, vi bruger, er, at vi kan kalde render metode så mange gange, som vi vil, og blot ændre det heltal, der identificerer måneden for hver forekomst. Lad os kalde det 12 gange baseret på det aktuelle år.

så simpelt som at ringe til render-metode 12 gange, og skift bare heltal for monthi:

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

Det er nok en god ide at oprette en ny forældreindpakning til det gengivne år. Hvert kalendergitter er en <kal-el> element. Lad os kalde den nye forældreindpakning <jor-el>Hvor Jor-El er navnet på Kal-Els far.

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

Vi kan bruge <jor-el> at skabe et gitter til vores net. 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 læste en fremragende bog kaldet At lave og bryde gitteret forleden og faldt over denne smukke “nytårsplakat”:

Making Calendars With Accessibility and Internationalization in Mind PlatoBlockchain Data Intelligence. Vertical Search. Ai.
Kilde: Making and Breaking the Grid (2. udgave) af Timothy Samara

Jeg regnede med, at vi kunne gøre noget lignende uden at ændre noget i HTML eller JavaScript. Jeg har taget mig den frihed at inkludere fulde navne i måneder, og tal i stedet for dagnavne, for at gøre det mere læsbart. God fornøjelse!

Tidsstempel:

Mere fra CSS-tricks