Membuat Kalender Dengan Mempertimbangkan Aksesibilitas dan Internasionalisasi

Membuat Kalender Dengan Mempertimbangkan Aksesibilitas dan Internasionalisasi

Melakukan pencarian cepat di sini di CSS-Tricks menunjukkan betapa banyak cara berbeda untuk mendekati kalender. Beberapa menunjukkan caranya CSS Grid dapat membuat tata letak secara efisien. Beberapa mencoba untuk membawa data aktual ke dalam campuran. Beberapa mengandalkan kerangka kerja untuk membantu pengelolaan negara.

Ada banyak pertimbangan saat membuat komponen kalender โ€” jauh lebih banyak daripada yang tercakup dalam artikel yang saya tautkan. Jika Anda memikirkannya, kalender penuh dengan nuansa, mulai dari menangani zona waktu dan format tanggal hingga pelokalan dan bahkan memastikan tanggal mengalir dari satu bulan ke bulan berikutnyaโ€ฆ dan itu bahkan sebelum kita masuk ke aksesibilitas dan pertimbangan tata letak tambahan tergantung di mana kalender ditampilkan dan lainnya.

Banyak pengembang takut Date() obyek dan tetap menggunakan perpustakaan lama seperti moment.js. Namun meskipun ada banyak โ€œketidaktahuanโ€ terkait tanggal dan pemformatan, JavaScript memiliki banyak API keren dan hal-hal lain untuk membantu!

Kisi-kisi kalender Januari 2023.
Membuat Kalender Dengan Mempertimbangkan Aksesibilitas dan Internasionalisasi

Saya tidak ingin membuat ulang roda di sini, tetapi saya akan menunjukkan kepada Anda bagaimana kita bisa mendapatkan kalender yang sangat bagus dengan vanilla JavaScript. Kami akan memeriksanya aksesibilitas, menggunakan markup semantik dan ramah pembaca layar <time> -tag - serta penginternasionalan dan format, menggunakan Intl.Locale, Intl.DateTimeFormat dan Intl.NumberFormat-Lebah.

Dengan kata lain, kami membuat kalenderโ€ฆ hanya tanpa dependensi tambahan yang biasanya Anda lihat digunakan dalam tutorial seperti ini, dan dengan beberapa nuansa yang biasanya tidak Anda lihat. Dan, dalam prosesnya, saya harap Anda akan mendapatkan apresiasi baru untuk hal-hal baru yang dapat dilakukan JavaScript sambil mendapatkan ide tentang hal-hal yang terlintas di benak saya saat saya menyusun sesuatu seperti ini.

Pertama, penamaan

Apa yang harus kita sebut komponen kalender kita? Dalam bahasa ibu saya, ini akan disebut "elemen kalender", jadi mari gunakan itu dan persingkat menjadi "Kal-El" โ€” juga dikenal sebagai Nama Superman di planet Krypton.

Mari kita buat sebuah fungsi untuk mewujudkannya:

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

Metode ini akan merender satu bulan. Nanti kita akan memanggil metode ini dari [...Array(12).keys()] untuk membuat satu tahun penuh.

Data awal dan internasionalisasi

Salah satu hal umum yang dilakukan oleh kalender online biasa adalah menyorot tanggal saat ini. Jadi mari kita buat referensi untuk itu:

const today = new Date();

Selanjutnya, kita akan membuat "objek konfigurasi" yang akan kita gabungkan dengan opsional settings objek dari metode utama:

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

Kami memeriksa, jika elemen root (<html>) berisi a lang-atribut dengan Lokal info; jika tidak, kami akan kembali menggunakan en-US. Ini adalah langkah pertama menuju internasionalisasi kalender.

Kita juga perlu menentukan bulan mana yang pertama kali ditampilkan saat kalender dirender. Itu sebabnya kami memperpanjang config objek dengan primer date. Dengan cara ini, jika tidak ada tanggal yang disediakan di settings objek, kami akan menggunakan today referensi sebagai gantinya:

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

Kami membutuhkan lebih banyak info untuk memformat kalender dengan benar berdasarkan lokal. Misalnya, kita mungkin tidak mengetahui apakah hari pertama dalam seminggu adalah Minggu atau Senin, bergantung pada wilayah setempat. Jika kami memiliki infonya, bagus! Namun jika tidak, kami akan memperbaruinya menggunakan Intl.Locale API. API memiliki weekInfo obyek yang mengembalikan a firstDay properti yang memberi kita apa yang kita cari tanpa kesulitan. Kita juga bisa mendapatkan hari mana dalam seminggu yang ditetapkan ke weekend:

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

Sekali lagi, kami membuat fallback. "Hari pertama" dalam seminggu untuk en-US adalah hari Minggu, jadi defaultnya adalah nilai 7. Ini sedikit membingungkan, karena getDay metode dalam JavaScript mengembalikan hari sebagai [0-6], Di mana 0 ini hari Mingguโ€ฆ jangan tanya kenapa. Akhir pekan adalah hari Sabtu dan Minggu, karenanya [6, 7].

Sebelum kami memiliki Intl.Locale API dan weekInfo metode, itu cukup sulit untuk membuat kalender internasional tanpa banyak objek ** dan array dengan informasi tentang setiap lokal atau wilayah. Saat ini, sangat mudah. Jika kita lewat en-GB, metode mengembalikan:

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

Di negara seperti Brunei (ms-BN), akhir pekan adalah hari Jumat dan Minggu:

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

Anda mungkin bertanya-tanya apa itu minimalDays properti adalah. Itu adalah hari paling sedikit yang diperlukan dalam minggu pertama suatu bulan untuk dihitung sebagai satu minggu penuh. Di beberapa daerah, mungkin hanya satu hari. Bagi yang lain, mungkin tujuh hari penuh.

Selanjutnya, kita akan membuat render metode dalam kita kalEl-metode:

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

Kami masih membutuhkan lebih banyak data untuk dikerjakan sebelum kami merender apa pun:

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

Yang terakhir adalah Boolean yang memeriksa apakah today ada di bulan yang akan kita render.

Markup semantik

Kita akan membahas lebih dalam tentang rendering sebentar lagi. Namun pertama-tama, saya ingin memastikan bahwa detail yang kami siapkan memiliki tag HTML semantik yang terkait dengannya. Menyiapkannya langsung dari kotak memberi kami manfaat aksesibilitas sejak awal.

pembungkus kalender

Pertama, kami memiliki pembungkus non-semantik: <kal-el>. Tidak apa-apa karena tidak ada semantik <calendar> tag atau semacamnya. Jika kami tidak membuat elemen khusus, <article> mungkin merupakan elemen yang paling tepat karena kalender dapat berdiri di halamannya sendiri.

Nama bulan

Grafik <time> elemen akan menjadi hal yang besar bagi kami karena membantu menerjemahkan tanggal ke dalam format yang dapat diuraikan oleh pembaca layar dan mesin telusur dengan lebih akurat dan konsisten. Misalnya, inilah cara kami menyampaikan "Januari 2023" di markup kami:

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

nama hari

Baris di atas tanggal kalender yang berisi nama hari dalam seminggu bisa jadi rumit. Sangat ideal jika kita dapat menuliskan nama lengkap untuk setiap hari โ€” misalnya Minggu, Senin, Selasa, dll. โ€” tetapi hal itu dapat memakan banyak tempat. Jadi, mari kita singkat nama-nama itu untuk sekarang di dalam an <ol> dimana setiap hari adalah a <li>:

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

Kita bisa menjadi rumit dengan CSS untuk mendapatkan yang terbaik dari kedua dunia. Misalnya, jika kami memodifikasi markup sedikit seperti ini:

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

โ€ฆkami mendapatkan nama lengkap secara default. Kami kemudian dapat "menyembunyikan" nama lengkap ketika ruang habis dan menampilkan title atribut sebagai gantinya:

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

Tapi, kita tidak akan seperti itu karena Intl.DateTimeFormat API juga dapat membantu di sini. Kita akan membahasnya di bagian selanjutnya saat kita membahas rendering.

Angka hari

Setiap tanggal di kisi kalender mendapatkan nomor. Setiap nomor adalah item daftar (<li>) dalam daftar terurut (<ol>), dan inline <time> tag membungkus nomor sebenarnya.

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

Dan sementara saya belum berencana untuk melakukan gaya apa pun, saya tahu saya akan menginginkan beberapa cara untuk menata nomor tanggal. Itu mungkin apa adanya, tetapi saya juga ingin dapat menata nomor hari kerja secara berbeda dari nomor akhir pekan jika perlu. Jadi, saya akan memasukkan data-* atribut khusus untuk itu: data-weekend dan data-today.

Nomor minggu

Ada 52 minggu dalam setahun, terkadang 53. Meskipun tidak terlalu umum, akan lebih baik jika menampilkan angka untuk minggu tertentu di kalender untuk konteks tambahan. Saya suka memilikinya sekarang, bahkan jika saya akhirnya tidak menggunakannya. Tapi kami akan benar-benar menggunakannya dalam tutorial ini.

Kami akan menggunakan data-weeknumber atribut sebagai pengait gaya dan sertakan dalam markup untuk setiap tanggal yang merupakan kencan pertama minggu itu.

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

Rendering

Ayo dapatkan kalender di halaman! Kami sudah tahu itu <kal-el> adalah nama elemen kustom kami. Hal pertama yang perlu kita konfigurasikan adalah mengatur firstDay properti di atasnya, sehingga kalender tahu apakah hari Minggu atau hari lain adalah hari pertama dalam seminggu.

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

Kami akan menggunakan templat literal untuk merender markup. Untuk memformat tanggal untuk pemirsa internasional, kami akan menggunakan Intl.DateTimeFormat API, sekali lagi menggunakan locale kami tentukan sebelumnya.

Bulan dan tahun

Ketika kita memanggil month, kita dapat mengatur apakah kita ingin menggunakan long nama (misalnya Februari) atau short nama (misalnya Februari). Mari gunakan long nama karena itu judul di atas kalender:

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

Nama hari kerja

Untuk hari kerja yang ditampilkan di atas petak tanggal, kami membutuhkan keduanya long (misalnya "Minggu") dan short (disingkat, yaitu. "Matahari") nama. Dengan cara ini, kita bisa menggunakan nama "pendek" saat kalender kekurangan ruang:

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

Mari buat metode pembantu kecil yang membuatnya sedikit lebih mudah untuk memanggil masing-masing:

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

Inilah cara kami memintanya di template:

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

Angka hari

Dan akhirnya, hari-hari, terbungkus dalam sebuah <ol> elemen:

${[...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('')}

Mari kita uraikan:

  1. Kami membuat array "dummy", berdasarkan variabel "jumlah hari", yang akan kami gunakan untuk iterasi.
  2. Kami membuat day variabel untuk hari ini dalam iterasi.
  3. Kami memperbaiki perbedaan antara Intl.Locale API dan getDay().
  4. Jika day adalah sama dengan today, kami menambahkan a data-* atribut.
  5. Akhirnya, kami mengembalikan <li> elemen sebagai string dengan data yang digabungkan.
  6. tabindex="0" membuat elemen dapat difokuskan, saat menggunakan navigasi keyboard, setelah nilai tabindex positif (Catatan: Anda harus tak pernah menambahkan positif nilai-tabindex)

Untuk "pad" nomornya dalam datetime atribut, kami menggunakan metode pembantu kecil:

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

Nomor minggu

Sekali lagi, "angka minggu" adalah tempat satu minggu jatuh dalam kalender 52 minggu. Kami menggunakan metode pembantu kecil untuk itu juga:

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

Saya tidak menulis ini getWeek-metode. Ini adalah versi yang dibersihkan dari skrip ini.

Dan itu saja! Terima kasih kepada Intl.Locale, Intl.DateTimeFormat dan Intl.NumberFormat API, sekarang kita cukup mengubah lang-atribut dari <html> elemen untuk mengubah konteks kalender berdasarkan wilayah saat ini:

Kisi-kisi kalender Januari 2023.
de-DE
Kisi-kisi kalender Januari 2023.
fa-IR
Kisi-kisi kalender Januari 2023.
zh-Hans-CN-u-nu-hanidec

Menata kalender

Anda mungkin ingat bagaimana semua hari hanyalah satu <ol> dengan daftar item. Untuk menata ini menjadi kalender yang dapat dibaca, kami menyelami dunia CSS Grid yang menakjubkan. Faktanya, kita dapat menggunakan kembali kisi yang sama dari template kalender pemula di sini di CSS-Tricks, tetapi memperbarui sedikit dengan :is() semu relasional untuk mengoptimalkan kode.

Perhatikan bahwa saya mendefinisikan variabel CSS yang dapat dikonfigurasi di sepanjang jalan (dan mengawalinya dengan ---kalel- untuk menghindari konflik).

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;
}
Kisi kalender tujuh kolom dengan garis kisi ditampilkan.
Membuat Kalender Dengan Mempertimbangkan Aksesibilitas dan Internasionalisasi

Mari menggambar batas di sekitar nomor tanggal untuk membantu memisahkannya secara visual:

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

Kisi tujuh kolom berfungsi dengan baik saat hari pertama bulan itu juga hari pertama dalam seminggu untuk lokal yang dipilih). Tapi itu pengecualian daripada aturannya. Sering kali, kami harus mengubah hari pertama bulan itu ke hari kerja yang berbeda.

Menampilkan hari pertama bulan yang jatuh pada hari Kamis.
Membuat Kalender Dengan Mempertimbangkan Aksesibilitas dan Internasionalisasi

Ingat semua ekstra data-* atribut yang kami tentukan saat menulis markup kami? Kita dapat menghubungkannya untuk memperbarui kolom kisi mana (--kalel-li-gc) nomor tanggal pertama bulan ditempatkan pada:

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

Dalam hal ini, kami merentang dari kolom petak pertama ke kolom petak keempat โ€” yang secara otomatis akan "mendorong" item berikutnya (Hari ke-2) ke kolom petak kelima, dan seterusnya.

Mari tambahkan sedikit gaya pada tanggal "saat ini", agar menonjol. Ini hanya gaya saya. Anda benar-benar dapat melakukan apa yang Anda suka di sini.

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

Saya suka ide menata nomor tanggal untuk akhir pekan berbeda dari hari kerja. Saya akan menggunakan warna kemerahan untuk menatanya. Perhatikan bahwa kita dapat meraih :not() pseudo-class untuk memilihnya sambil membiarkan tanggal saat ini saja:

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

Oh, dan jangan lupa nomor minggu sebelum nomor kencan pertama setiap minggu. Kami menggunakan a data-weeknumber di markup untuk itu, tetapi angkanya tidak akan benar-benar ditampilkan kecuali kita mengungkapkannya dengan CSS, yang dapat kita lakukan di ::before elemen semu:

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

Kami secara teknis selesai pada titik ini! Kami dapat merender kisi kalender yang menampilkan tanggal untuk bulan ini, lengkap dengan pertimbangan untuk melokalkan data berdasarkan lokal, dan memastikan bahwa kalender menggunakan semantik yang tepat. Dan yang kami gunakan hanyalah vanilla JavaScript dan CSS!

Tapi mari kita ambil ini satu langkah lagi...

Render sepanjang tahun

Mungkin Anda perlu menampilkan tanggal setahun penuh! Jadi, daripada merender bulan ini, Anda mungkin ingin menampilkan semua kisi bulan untuk tahun ini.

Nah, hal yang menyenangkan tentang pendekatan yang kami gunakan adalah kami dapat memanggil render metode sebanyak yang kita inginkan dan hanya mengubah bilangan bulat yang mengidentifikasi bulan pada setiap contoh. Sebut saja 12 kali berdasarkan tahun berjalan.

sesederhana memanggil render-metode 12 kali, dan cukup ubah bilangan bulat untuk month - i:

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

Mungkin merupakan ide bagus untuk membuat pembungkus induk baru untuk tahun yang dirender. Setiap kisi kalender adalah a <kal-el> elemen. Sebut saja pembungkus induk baru <jor-el>, Di mana Jor-El adalah nama ayah Kal-El.

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

Kita dapat menggunakan <jor-el> untuk membuat kisi untuk kisi kami. Jadi 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 terakhir

Bonus: Kalender Konfeti

Saya membaca sebuah buku bagus berjudul Membuat dan Memecah Grid beberapa hari yang lalu dan menemukan "poster Tahun Baru" yang indah ini:

Membuat Kalender Dengan mempertimbangkan Aksesibilitas dan Internasionalisasi PlatoBlockchain Data Intelligence. Pencarian Vertikal. Ai.
Sumber: Membuat dan Memecah Grid (Edisi ke-2) oleh Timotius Samara

Saya pikir kami dapat melakukan hal serupa tanpa mengubah apa pun di HTML atau JavaScript. Saya telah memberanikan diri untuk memasukkan nama lengkap selama berbulan-bulan, dan angka sebagai pengganti nama hari, agar lebih mudah dibaca. Menikmati!

Stempel Waktu:

Lebih dari Trik CSS