{"id":2013068,"date":"2023-03-13T09:23:52","date_gmt":"2023-03-13T13:23:52","guid":{"rendered":"https:\/\/wordpress-1016567-4521551.cloudwaysapps.com\/plato-data\/making-calendars-with-accessibility-and-internationalization-in-mind\/"},"modified":"2023-03-13T09:23:52","modified_gmt":"2023-03-13T13:23:52","slug":"making-calendars-with-accessibility-and-internationalization-in-mind","status":"publish","type":"station","link":"https:\/\/platodata.io\/plato-data\/making-calendars-with-accessibility-and-internationalization-in-mind\/","title":{"rendered":"Making Calendars With Accessibility and Internationalization in Mind"},"content":{"rendered":"
Doing a quick search here on CSS-Tricks shows just how many different ways there are to approach calendars. Some show how CSS Grid can create the layout efficiently<\/a>. Some attempt to bring actual data into the mix<\/a>. Some rely on a framework<\/a> to help with state management.<\/p>\n There are many considerations when building a calendar component \u2014 far more than what is covered in the articles I linked up. If you think about it, calendars are fraught with nuance, from handling timezones and date formats to localization and even making sure dates flow from one month to the next\u2026 and that\u2019s before we even get into accessibility and additional layout considerations depending on where the calendar is displayed and whatnot.<\/p>\n Many developers fear the <\/span> <\/p>\n I don\u2019t want to re-create the wheel here, but I will show you how we can get a dang good calendar with vanilla JavaScript. We\u2019ll look into accessibility<\/strong>, using semantic markup and screenreader-friendly In other words, we\u2019re making a calendar\u2026 only without the extra dependencies you might typically see used in a tutorial like this, and with some of the nuances you might not typically see. And, in the process, I hope you\u2019ll gain a new appreciation for newer things that JavaScript can do while getting an idea of the sorts of things that cross my mind when I\u2019m putting something like this together.<\/p>\n What should we call our calendar component? In my native language, it would be called \u201ckalender element\u201d, so let\u2019s use that and shorten that to \u201cKal-El\u201d \u2014 also known as Superman\u2019s name on the planet Krypton<\/a>.<\/p>\n Let\u2019s create a function to get things going:<\/p>\n This method will render a single month<\/strong>. Later we\u2019ll call this method from One of the common things a typical online calendar does is highlight the current date. So let\u2019s create a reference for that:<\/p>\n Next, we\u2019ll create a \u201cconfiguration object\u201d that we\u2019ll merge with the optional We check, if the root element ( We also need to determine which month to initially display when the calendar is rendered. That\u2019s why we extended the We need a little more info to properly format the calendar based on locale. For example, we might not know whether the first day of the week is Sunday or Monday, depending on the locale. If we have the info, great! But if not, we\u2019ll update it using the Again, we create fallbacks. The \u201cfirst day\u201d of the week for Before we had the In a country like Brunei ( You might wonder what that Next, we\u2019ll create a We still need some more data to work with before we render anything:<\/p>\n The last one is a We\u2019re going to get deeper in rendering in just a moment. But first, I want to make sure that the details we set up have semantic HTML tags associated with them. Setting that up right out of the box gives us accessibility benefits from the start.<\/p>\n First, we have the non-semantic wrapper: The The row above the calendar\u2019s dates containing the names of the days of the week can be tricky. It\u2019s ideal if we can write out the full names for each day \u2014 e.g. Sunday, Monday, Tuesday, etc. \u2014 but that can take up a lot of space. So, let\u2019s abbreviate the names for now inside of an We could get tricky with CSS to get the best of both worlds. For example, if we modified the markup a bit like this:<\/p>\n \u2026we get the full names by default. We can then \u201chide\u201d the full name when space runs out and display the But, we\u2019re not going that way because the Each date in the calendar grid gets a number. Each number is a list item ( And while I\u2019m not planning to do any styling just yet, I know I will want some way to style the date numbers. That\u2019s possible as-is, but I also want to be able to style weekday numbers differently than weekend numbers if I need to. So, I\u2019m going to include There are 52 weeks in a year, sometimes 53. While it\u2019s not super common, it can be nice to display the number for a given week in the calendar for additional context. I like having it now, even if I don\u2019t wind up not using it. But we\u2019ll totally use it in this tutorial.<\/p>\n We\u2019ll use a Let\u2019s get the calendar on a page! We already know that We\u2019ll be using template literals<\/a> to render the markup. To format the dates for an international audience, we\u2019ll use the When we call the For weekdays displayed above the grid of dates, we need both the Let\u2019s make a small helper method that makes it a little easier to call each one:<\/p>\n Here\u2019s how we invoke that in the template:<\/p>\n And finally, the days, wrapped in an Let\u2019s break that down:<\/p>\n To \u201cpad\u201d the numbers<\/a> in the Again, the \u201cweek number\u201d is where a week falls in a 52-week calendar. We use a little helper method for that as well:<\/p>\n I didn\u2019t write this And that\u2019s it! Thanks to the Date()<\/code> object<\/a> and stick with older libraries like
moment.js<\/code><\/a>. But while there are many \u201cgotchas\u201d when it comes to dates and formatting, JavaScript has a lot of cool APIs and stuff to help out!<\/p>\n
<\/figure>\n
<time><\/code> -tags \u2014 as well as internationalization<\/strong> and formatting<\/strong>, using the
Intl.Locale<\/code>,
Intl.DateTimeFormat<\/code> and
Intl.NumberFormat<\/code>-APIs.<\/p>\n
<\/a>First off, naming<\/h3>\n
function kalEl(settings = {}) { ... }<\/code><\/pre>\n
[...Array(12).keys()]<\/code> to render an entire year.<\/p>\n
<\/a>Initial data and internationalization<\/h3>\n
const today = new Date();<\/code><\/pre>\n
settings<\/code> object of the primary method:<\/p>\n
const config = Object.assign( { locale: (document.documentElement.getAttribute('lang') || 'en-US'), today: { day: today.getDate(), month: today.getMonth(), year: today.getFullYear() } }, settings\n);<\/code><\/pre>\n
<html><\/code>) contains a
lang<\/code>-attribute with locale<\/strong> info; otherwise, we\u2019ll fallback to using
en-US<\/code>. This is the first step toward internationalizing the calendar<\/a>.<\/p>\n
config<\/code> object with the primary
date<\/code>. This way, if no date is provided in the
settings<\/code> object, we\u2019ll use the
today<\/code> reference instead:<\/p>\n
const date = config.date ? new Date(config.date) : today;<\/code><\/pre>\n
Intl.Locale<\/code> API<\/a>. The API has a
weekInfo<\/code> object<\/a> that returns a
firstDay<\/code> property that gives us exactly what we\u2019re looking for without any hassle. We can also get which days of the week are assigned to the
weekend<\/code>:<\/p>\n
if (!config.info) config.info = new Intl.Locale(config.locale).weekInfo || { firstDay: 7, weekend: [6, 7] };<\/code><\/pre>\n
en-US<\/code> is Sunday, so it defaults to a value of
7<\/code>. This is a little confusing, as the
getDay<\/code> method<\/a> in JavaScript returns the days as
[0-6]<\/code>, where
0<\/code> is Sunday\u2026 don\u2019t ask me why. The weekends are Saturday and Sunday, hence
[6, 7]<\/code>.<\/p>\n
Intl.Locale<\/code> API and its
weekInfo<\/code> method, it was pretty hard to create an international calendar without many **objects and arrays with information about each locale or region. Nowadays, it\u2019s easy-peasy. If we pass in
en-GB<\/code>, the method returns:<\/p>\n
\/\/ en-GB\n{ firstDay: 1, weekend: [6, 7], minimalDays: 4\n}<\/code><\/pre>\n
ms-BN<\/code>), the weekend is Friday and Sunday:<\/p>\n
\/\/ ms-BN\n{ firstDay: 7, weekend: [5, 7], minimalDays: 1\n}<\/code><\/pre>\n
minimalDays<\/code> property is. That\u2019s the fewest days required in the first week of a month to be counted as a full week<\/a>. In some regions, it might be just one day. For others, it might be a full seven days.<\/p>\n
render<\/code> method within our
kalEl<\/code>-method:<\/p>\n
const render = (date, locale) => { ... }<\/code><\/pre>\n
const month = date.getMonth();\nconst year = date.getFullYear();\nconst numOfDays = new Date(year, month + 1, 0).getDate();\nconst renderToday = (year === config.today.year) && (month === config.today.month);<\/code><\/pre>\n
Boolean<\/code> that checks whether
today<\/code> exists in the month we\u2019re about to render.<\/p>\n
<\/a>Semantic markup<\/h3>\n
<\/a>Calendar wrapper<\/h4>\n
<kal-el><\/code>. That\u2019s fine because there isn\u2019t a semantic
<calendar><\/code> tag or anything like that. If we weren\u2019t making a custom element,
<article><\/code> might be the most appropriate element since the calendar could stand on its own page.<\/p>\n
<\/a>Month names<\/h4>\n
<time><\/code> element is going to be a big one for us because it helps translate dates into a format that screenreaders and search engines can parse more accurately and consistently. For example, here\u2019s how we can convey \u201cJanuary 2023\u201d in our markup:<\/p>\n
<time datetime=\"2023-01\">January <i>2023<\/i><\/time><\/code><\/pre>\n
<\/a>Day names<\/h4>\n
<ol><\/code> where each day is a
<li><\/code>:<\/p>\n
<ol> <li><abbr title=\"Sunday\">Sun<\/abbr><\/li> <li><abbr title=\"Monday\">Mon<\/abbr><\/li> <!-- etc. -->\n<\/ol><\/code><\/pre>\n
<ol> <li> <abbr title=\"S\">Sunday<\/abbr> <\/li>\n<\/ol><\/code><\/pre>\n
title<\/code> attribute instead:<\/p>\n
@media all and (max-width: 800px) { li abbr::after { content: attr(title); }\n}<\/code><\/pre>\n
Intl.DateTimeFormat<\/code> API can help here as well. We\u2019ll get to that in the next section when we cover rendering.<\/p>\n
<\/a>Day numbers<\/h4>\n
<li><\/code>) in an ordered list (
<ol><\/code>), and the inline
<time><\/code> tag wraps the actual number.<\/p>\n
<li> <time datetime=\"2023-01-01\">1<\/time>\n<\/li><\/code><\/pre>\n
data-*<\/code> attributes<\/a> specifically for that:
data-weekend<\/code> and
data-today<\/code>.<\/p>\n
<\/a>Week numbers<\/h4>\n
data-weeknumber<\/code> attribute as a styling hook and include it in the markup for each date that is the week\u2019s first date.<\/p>\n
<li data-day=\"7\" data-weeknumber=\"1\" data-weekend=\"\"> <time datetime=\"2023-01-08\">8<\/time>\n<\/li><\/code><\/pre>\n
<\/a>Rendering<\/h3>\n
<kal-el><\/code> is the name of our custom element. First thing we need to configure it is to set the
firstDay<\/code> property on it, so the calendar knows whether Sunday or some other day is the first day of the week.<\/p>\n
<kal-el data-firstday=\"${ config.info.firstDay }\"><\/code><\/pre>\n
Intl.DateTimeFormat<\/code> API, again using the
locale<\/code> we specified earlier.<\/p>\n
<\/a>The month and year<\/h4>\n
month<\/code>, we can set whether we want to use the
long<\/code> name (e.g. February) or the
short<\/code> name (e.g. Feb.). Let\u2019s use the
long<\/code> name since it\u2019s the title above the calendar:<\/p>\n
<time datetime=\"${year}-${(pad(month))}\"> ${new Intl.DateTimeFormat( locale, { month:'long'}).format(date)} <i>${year}<\/i>\n<\/time><\/code><\/pre>\n
<\/a>Weekday names<\/h4>\n
long<\/code> (e.g. \u201cSunday\u201d) and
short<\/code> (abbreviated, ie. \u201cSun\u201d) names. This way, we can use the \u201cshort\u201d name when the calendar is short on space:<\/p>\n
Intl.DateTimeFormat([locale], { weekday: 'long' })\nIntl.DateTimeFormat([locale], { weekday: 'short' })<\/code><\/pre>\n
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;\n}<\/code><\/pre>\n
<ol> ${weekdays(config.info.firstDay,locale).map(name => ` <li> <abbr title=\"${name.long}\">${name.short}<\/abbr> <\/li>`).join('') }\n<\/ol><\/code><\/pre>\n
<\/a>Day numbers<\/h4>\n
<ol><\/code> element:<\/p>\n
${[...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>`\n}).join('')}<\/code><\/pre>\n
\n
day<\/code> variable for the current day in the iteration.<\/li>\n
Intl.Locale<\/code> API and
getDay()<\/code>.<\/li>\n
day<\/code> is equal to
today<\/code>, we add a
data-*<\/code> attribute.<\/li>\n
<li><\/code> element as a string with merged data.<\/li>\n
tabindex=\"0\"<\/code> makes the element focusable, when using keyboard navigation, after any positive tabindex values (Note: you should never<\/strong> add positive<\/strong> tabindex-values)<\/li>\n<\/ol>\n
datetime<\/code> attribute, we use a little helper method:<\/p>\n
const pad = (val) => (val + 1).toString().padStart(2, '0');<\/code><\/pre>\n
<\/a>Week number<\/h4>\n
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);\n}<\/code><\/pre>\n
getWeek<\/code>-method. It\u2019s a cleaned up version of this script<\/a>.<\/p>\n
Intl.Locale<\/code><\/a>,
Intl.DateTimeFormat<\/code><\/a> and
Intl.NumberFormat<\/code><\/a> APIs, we can now simply change the
lang<\/code>-attribute of the
<html><\/code> element to change the context of the calendar based on the current region:<\/p>\n
de-DE<\/code><\/figcaption><\/figure>\n<\/div>\n
fa-IR<\/code><\/figcaption><\/figure>\n<\/div>\n
zh-Hans-CN-u-nu-hanidec<\/code><\/figcaption><\/figure>\n<\/div>\n<\/div>\n
<\/a>Styling the calendar<\/h3>\n