Tworzenie kalendarzy z myślą o ułatwieniach dostępu i umiędzynarodowieniu

Tworzenie kalendarzy z myślą o ułatwieniach dostępu i umiędzynarodowieniu

Szybkie wyszukiwanie tutaj w CSS-Tricks pokazuje, jak wiele różnych sposobów podejścia do kalendarzy jest. Niektórzy pokazują jak CSS Grid może wydajnie tworzyć układ. Niektórzy próbują wprowadź rzeczywiste dane do miksu, Trochę polegać na frameworku pomóc w zarządzaniu państwem.

Podczas tworzenia komponentu kalendarza bierze się pod uwagę wiele czynników — o wiele więcej niż to, co zostało omówione w artykułach, które podlinkowałem. Jeśli się nad tym zastanowić, kalendarze są pełne niuansów, od obsługi stref czasowych i formatów dat po lokalizację, a nawet upewnienie się, że daty przepływają z miesiąca na miesiąc… i to jeszcze zanim przejdziemy do ułatwień dostępu i dodatkowych kwestii związanych z układem w zależności od tego, gdzie kalendarz jest wyświetlany i co dalej.

Wielu deweloperów boi się Date() przedmiot i trzymaj się starszych bibliotek, takich jak moment.js. Ale chociaż istnieje wiele problemów, jeśli chodzi o daty i formatowanie, JavaScript ma wiele fajnych interfejsów API i rzeczy, które mogą pomóc!

Siatka kalendarza na styczeń 2023 r.
Tworzenie kalendarzy z myślą o ułatwieniach dostępu i umiędzynarodowieniu

Nie chcę tutaj odtwarzać koła, ale pokażę ci, jak możemy uzyskać cholernie dobry kalendarz z waniliowym JavaScriptem. Zajrzymy dostępność, używając znaczników semantycznych i przyjazny dla czytników ekranu <time> -tagi — jak również umiędzynarodowienie i formatowanieStosując Intl.Locale, Intl.DateTimeFormat i Intl.NumberFormat-Pszczoła.

Innymi słowy, tworzymy kalendarz… tylko bez dodatkowych zależności, które zwykle można zobaczyć w samouczku takim jak ten, oraz z niektórymi niuansami, których zwykle nie widać. Mam nadzieję, że w trakcie tego procesu zyskasz nowe uznanie dla nowych rzeczy, które potrafi zrobić JavaScript, jednocześnie poznając rodzaje rzeczy, które przychodzą mi do głowy, kiedy składam coś takiego.

Na początek nazewnictwo

Jak powinniśmy nazwać nasz komponent kalendarza? W moim ojczystym języku nazywałoby się to „elementem kalendarza”, więc użyjmy tego i skróćmy do „Kal-El” — znanego również jako Imię Supermana na planecie Krypton.

Stwórzmy funkcję, aby wszystko działało:

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

Ta metoda będzie renderować jeden miesiąc. Później nazwiemy tę metodę from [...Array(12).keys()] oddać cały rok.

Dane początkowe i internacjonalizacja

Jedną z typowych rzeczy, które robi typowy kalendarz online, jest wyróżnianie bieżącej daty. Stwórzmy więc odniesienie do tego:

const today = new Date();

Następnie utworzymy „obiekt konfiguracji”, który połączymy z opcjonalnym settings obiekt metody podstawowej:

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

Sprawdzamy, czy element główny (<html>) zawiera lang-atrybut z lokalny informacje; w przeciwnym razie powrócimy do używania en-US. To pierwszy krok w kierunku umiędzynarodowienie kalendarza.

Musimy również określić, który miesiąc ma być początkowo wyświetlany podczas renderowania kalendarza. Dlatego rozszerzyliśmy config obiekt z głównym date. W ten sposób, jeśli w pliku nie podano daty settings obiekt, użyjemy today odniesienie zamiast:

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

Potrzebujemy trochę więcej informacji, aby poprawnie sformatować kalendarz na podstawie ustawień regionalnych. Na przykład możemy nie wiedzieć, czy pierwszym dniem tygodnia jest niedziela czy poniedziałek, w zależności od ustawień regionalnych. Jeśli mamy informacje, świetnie! Ale jeśli nie, zaktualizujemy go za pomocą Intl.Locale API. API ma weekInfo przedmiot który zwraca a firstDay nieruchomość, która daje nam dokładnie to, czego szukamy bez żadnych kłopotów. Możemy również uzyskać, które dni tygodnia są przypisane do weekend:

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

Ponownie tworzymy awarie. „Pierwszy dzień” tygodnia dla en-US jest niedziela, więc domyślną wartością jest 7. Jest to trochę mylące, ponieważ tzw getDay metoda w JavaScript zwraca dni jako [0-6], Gdzie 0 jest niedziela… nie pytaj mnie dlaczego. Weekendy to sobota i niedziela, stąd [6, 7].

Zanim mieliśmy Intl.Locale API i jego weekInfo metoda, dość trudno było stworzyć międzynarodowy kalendarz bez wielu **obiektów i tablic z informacjami o każdym regionie lub lokalizacji. W dzisiejszych czasach jest to łatwe. Jeśli przejdziemy en-GB, metoda zwraca:

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

W kraju takim jak Brunei (ms-BN), weekend to piątek i niedziela:

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

Można się zastanawiać, co to minimalDays własność jest. To jest najmniejszą liczbę dni wymaganych w pierwszym tygodniu miesiąca, aby liczono je jako pełny tydzień. W niektórych regionach może to być tylko jeden dzień. Dla innych może to być pełne siedem dni.

Następnie utworzymy plik render metoda w naszym kalEl-metoda:

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

Wciąż potrzebujemy trochę więcej danych do pracy, zanim cokolwiek wyrenderujemy:

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

Ostatni to A Boolean to sprawdza, czy today istnieje w miesiącu, który zamierzamy wyrenderować.

Znaczniki semantyczne

Za chwilę zajmiemy się renderowaniem głębiej. Ale najpierw chcę się upewnić, że skonfigurowane przez nas szczegóły mają powiązane semantyczne tagi HTML. Ustawienie tego od razu po wyjęciu z pudełka zapewnia nam korzyści z ułatwień dostępu od samego początku.

Opakowanie kalendarza

Po pierwsze, mamy opakowanie niesemantyczne: <kal-el>. To dobrze, bo nie ma semantyki <calendar> tag czy coś w tym stylu. Gdybyśmy nie tworzyli niestandardowego elementu, <article> może być najodpowiedniejszym elementem, ponieważ kalendarz mógłby znajdować się na osobnej stronie.

Nazwy miesięcy

Połączenia <time> element będzie dla nas duży, ponieważ pomaga tłumaczyć daty na format, który czytniki ekranu i wyszukiwarki mogą analizować dokładniej i spójniej. Na przykład, oto jak możemy przekazać „styczeń 2023” w naszych znacznikach:

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

Nazwy dni

Wiersz nad datami kalendarza zawierający nazwy dni tygodnia może być trudny. Idealnie byłoby, gdybyśmy mogli wypisać pełne nazwy każdego dnia — np. niedzieli, poniedziałku, wtorku itd. — ale może to zająć dużo miejsca. Więc skróćmy teraz nazwy wewnątrz an <ol> gdzie każdy dzień jest <li>:

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

Moglibyśmy trochę pokombinować z CSS, aby uzyskać to, co najlepsze z obu światów. Na przykład, jeśli zmodyfikowaliśmy znaczniki trochę tak:

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

…domyślnie otrzymujemy pełne nazwy. Możemy wtedy „ukryć” pełne imię i nazwisko, gdy skończy się miejsce i wyświetlić title zamiast tego atrybut:

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

Ale nie idziemy tą drogą, bo Intl.DateTimeFormat API może tu również pomóc. Dojdziemy do tego w następnej sekcji, gdy zajmiemy się renderowaniem.

Numery dni

Każda data w siatce kalendarza otrzymuje numer. Każdy numer jest elementem listy (<li>) na uporządkowanej liście (<ol>) i w wierszu <time> tag zawija rzeczywistą liczbę.

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

I chociaż nie planuję jeszcze żadnej stylizacji, wiem, że będę chciał nadać styl numerom dat. Jest to możliwe w obecnej postaci, ale chcę też mieć możliwość stylizowania numerów dni tygodnia inaczej niż numerów weekendów, jeśli zajdzie taka potrzeba. Więc dołączę data-* atrybuty specjalnie do tego: data-weekend i data-today.

Numery tygodni

Rok ma 52 tygodnie, czasem 53. Chociaż nie jest to zbyt częste, fajnie jest wyświetlić liczbę dla danego tygodnia w kalendarzu, aby uzyskać dodatkowy kontekst. Lubię go mieć teraz, nawet jeśli nie skończy się na tym, że go nie użyję. Ale całkowicie go użyjemy w tym samouczku.

Użyjemy data-weeknumber jako haczyk do stylizacji i umieść go w znacznikach dla każdej daty, która jest pierwszą randką tygodnia.

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

wykonanie

Umieśćmy kalendarz na stronie! To już wiemy <kal-el> to nazwa naszego niestandardowego elementu. Pierwszą rzeczą, którą musimy skonfigurować, jest ustawienie firstDay na nim nieruchomości, więc kalendarz wie, czy niedziela czy inny dzień jest pierwszym dniem tygodnia.

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

Będziemy używać literały szablonów aby renderować znaczniki. Aby sformatować daty dla międzynarodowych odbiorców, użyjemy formatu Intl.DateTimeFormat API, ponownie używając locale określiliśmy wcześniej.

Miesiąc i rok

Kiedy nazywamy month, możemy ustawić, czy chcemy korzystać z long nazwa (np. luty) lub short imię (np. luty). użyjmy long name, ponieważ jest to tytuł nad kalendarzem:

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

Nazwy dni tygodnia

W przypadku dni tygodnia wyświetlanych nad siatką dat potrzebujemy obu long (np. „niedziela”) i short (skrócone, tj. „słońce”) nazwy. W ten sposób możemy użyć „krótkiej” nazwy, gdy w kalendarzu brakuje miejsca:

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

Stwórzmy małą metodę pomocniczą, która nieco ułatwi wywołanie każdej z nich:

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

Oto jak wywołujemy to w szablonie:

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

Numery dni

I wreszcie, dni, zawinięte w <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('')}

Rozłóżmy to:

  1. Tworzymy „fikcyjną” tablicę opartą na zmiennej „liczba dni”, której będziemy używać do iteracji.
  2. Tworzymy day zmienna dla bieżącego dnia w iteracji.
  3. Naprawiamy rozbieżności między Intl.Locale API i getDay().
  4. Jeśli day jest równe today, dodajemy A data-* atrybutów.
  5. Na koniec zwracamy <li> element jako ciąg ze scalonymi danymi.
  6. tabindex="0" sprawia, że ​​element można skupić, podczas korzystania z nawigacji za pomocą klawiatury, po wszelkich dodatnich wartościach tabindex (Uwaga: powinieneś nigdy Dodaj pozytywny wartości tabindex)

Do „dopasuj” cyfry datetime atrybut, używamy małej metody pomocniczej:

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

Numer tygodnia

Ponownie, „numer tygodnia” to miejsce, w którym tydzień przypada w 52-tygodniowym kalendarzu. Używamy do tego również małej metody pomocniczej:

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

nie napisałem tego getWeek-metoda. To odświeżona wersja ten skrypt.

I to wszystko! Dzięki do Intl.Locale, Intl.DateTimeFormat i Intl.NumberFormat API, możemy teraz po prostu zmienić plik lang-atrybut <html> element do zmiany kontekstu kalendarza na podstawie bieżącego regionu:

Siatka kalendarza na styczeń 2023 r.
de-DE
Siatka kalendarza na styczeń 2023 r.
fa-IR
Siatka kalendarza na styczeń 2023 r.
zh-Hans-CN-u-nu-hanidec

Stylizacja kalendarza

Być może pamiętasz, jak wszystkie dni są tylko jednym <ol> z elementami listy. Aby stworzyć z nich czytelny kalendarz, zanurzamy się w cudowny świat CSS Grid. W rzeczywistości możemy ponownie wykorzystać tę samą siatkę szablon kalendarza startowego tutaj, w CSS-Tricks, ale zaktualizowałem smidge za pomocą :is() relacyjny pseudo do optymalizacji kodu.

Zauważ, że po drodze definiuję konfigurowalne zmienne CSS (i poprzedzam je przedrostkiem ---kalel- uniknąć konfliktów).

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;
}
Siedmiokolumnowa siatka kalendarza z pokazanymi liniami siatki.
Tworzenie kalendarzy z myślą o ułatwieniach dostępu i umiędzynarodowieniu

Narysujmy ramki wokół liczb dat, aby ułatwić ich wizualne oddzielenie:

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

Siedmiokolumnowa siatka działa dobrze, gdy jest pierwszy dzień miesiąca również pierwszego dnia tygodnia dla wybranej lokalizacji). Ale to raczej wyjątek niż reguła. W większości przypadków będziemy musieli przesunąć pierwszy dzień miesiąca na inny dzień tygodnia.

Pokazuje pierwszy dzień miesiąca przypadający na czwartek.
Tworzenie kalendarzy z myślą o ułatwieniach dostępu i umiędzynarodowieniu

Pamiętaj o wszystkich dodatkowych data-* atrybuty, które zdefiniowaliśmy podczas pisania naszego znacznika? Możemy się do nich podłączyć, aby zaktualizować kolumnę siatki (--kalel-li-gc) numer pierwszego dnia miesiąca umieszcza się na:

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

W tym przypadku rozciągamy się od pierwszej kolumny siatki do czwartej kolumny siatki — co automatycznie „pchnie” następny element (dzień 2) do piątej kolumny siatki i tak dalej.

Dodajmy trochę stylu do „bieżącej” daty, aby się wyróżniała. To są tylko moje stylizacje. Tutaj możesz robić, co chcesz.

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

Podoba mi się pomysł stylizowania numerów dat na weekendy inaczej niż w dni powszednie. Do ich stylizacji użyję czerwonawego koloru. Pamiętaj, że możemy sięgnąć po tzw :not() pseudo-klasa, aby je wybrać, pozostawiając samą bieżącą datę:

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

Aha, i nie zapominajmy o numerach tygodni poprzedzających pierwszą datę każdego tygodnia. Użyliśmy A data-weeknumber atrybut w znacznikach, ale liczby nie będą wyświetlane, chyba że ujawnimy je za pomocą CSS, co możemy zrobić na stronie ::before pseudoelement:

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

W tym momencie jesteśmy technicznie skończeni! Możemy wyrenderować siatkę kalendarza, która pokazuje daty w bieżącym miesiącu, wraz z uwzględnieniem lokalizacji danych według ustawień regionalnych i upewnieniem się, że kalendarz używa odpowiedniej semantyki. A wszystko, czego użyliśmy, to zwykły JavaScript i CSS!

Ale weźmy to Jeszcze jeden krok...

Renderowanie całego roku

Może musisz wyświetlić pełny rok dat! Dlatego zamiast renderować bieżący miesiąc, możesz chcieć wyświetlić wszystkie siatki miesięcy dla bieżącego roku.

Cóż, fajną rzeczą w podejściu, którego używamy, jest to, że możemy wywołać metodę render metodę tyle razy, ile chcemy, zmieniając jedynie liczbę całkowitą, która identyfikuje miesiąc w każdym przypadku. Nazwijmy to 12 razy w oparciu o bieżący rok.

tak proste, jak dzwonienie do render-method 12 razy i po prostu zmień liczbę całkowitą dla month - i:

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

Prawdopodobnie dobrym pomysłem jest utworzenie nowego opakowania nadrzędnego dla renderowanego roku. Każda siatka kalendarza to a <kal-el> element. Nazwijmy nowe opakowanie nadrzędne <jor-el>, Gdzie Jor-El to imię ojca Kal-Ela.

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

Możemy użyć <jor-el> aby utworzyć siatkę dla naszych siatek. Więc 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);
}

Ostateczne demo

Bonus: kalendarz konfetti

Przeczytałem świetną książkę pt Tworzenie i łamanie sieci któregoś dnia natknąłem się na ten piękny „plakat noworoczny”:

Tworzenie kalendarzy z myślą o dostępności i internacjonalizacji z myślą o PlatoBlockchain Data Intelligence. Wyszukiwanie pionowe. AI.
Źródło: Making and Breaking the Grid (2. edycja) przez Timothy'ego Samarę

Pomyślałem, że możemy zrobić coś podobnego bez zmiany czegokolwiek w kodzie HTML lub JavaScript. Pozwoliłem sobie zamieścić pełne imiona miesięcy i liczby zamiast nazw dni, aby było to bardziej czytelne. Cieszyć się!

Znak czasu:

Więcej z Sztuczki CSS