Créer des calendriers en tenant compte de l'accessibilité et de l'internationalisation

Créer des calendriers en tenant compte de l'accessibilité et de l'internationalisation

Faire une recherche rapide ici sur CSS-Tricks montre à quel point il existe de nombreuses façons différentes d'aborder les calendriers. Certains montrent comment CSS Grid peut créer la mise en page efficacement. Certains tentent de intégrer des données réelles dans le mélange. Certains s'appuyer sur un cadre pour aider à la gestion de l'État.

Il existe de nombreuses considérations lors de la création d'un composant de calendrier - bien plus que ce qui est couvert dans les articles que j'ai liés. Si vous y réfléchissez, les calendriers sont pleins de nuances, de la gestion des fuseaux horaires et des formats de date à la localisation et même en s'assurant que les dates circulent d'un mois à l'autre… et c'est avant même que nous n'entrions dans l'accessibilité et les considérations de mise en page supplémentaires selon l'endroit où le calendrier est affiché et ainsi de suite.

De nombreux développeurs craignent Date() objet et restez avec les anciennes bibliothèques comme moment.js. Mais bien qu'il existe de nombreux "pièges" en matière de dates et de formatage, JavaScript a beaucoup d'API et de trucs sympas pour vous aider !

Grille du calendrier de janvier 2023.
Créer des calendriers en tenant compte de l'accessibilité et de l'internationalisation

Je ne veux pas recréer la roue ici, mais je vais vous montrer comment nous pouvons obtenir un bon calendrier avec du JavaScript vanille. Nous examinerons accessibilité, utilisant un balisage sémantique et compatible avec les lecteurs d'écran <time> -tags — ainsi que internationalisation ainsi que formatage, En utilisant l' Intl.Locale, Intl.DateTimeFormat ainsi que Intl.NumberFormat-Apis.

En d'autres termes, nous créons un calendrier… seulement sans les dépendances supplémentaires que vous pourriez voir généralement utilisées dans un didacticiel comme celui-ci, et avec certaines des nuances que vous ne verrez peut-être pas habituellement. Et, dans le processus, j'espère que vous acquerrez une nouvelle appréciation des nouvelles choses que JavaScript peut faire tout en ayant une idée du genre de choses qui me traversent l'esprit lorsque je mets quelque chose comme ça ensemble.

Tout d'abord, nommer

Comment devrions-nous appeler notre composant de calendrier ? Dans ma langue maternelle, cela s'appellerait "élément kalender", alors utilisons-le et raccourcissons-le en "Kal-El" - également connu sous le nom Le nom de Superman sur la planète Krypton.

Créons une fonction pour faire avancer les choses :

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

Cette méthode rendra un seul mois. Plus tard, nous appellerons cette méthode à partir de [...Array(12).keys()] rendre une année entière.

Données initiales et internationalisation

L'une des choses courantes que fait un calendrier en ligne typique est de mettre en évidence la date actuelle. Créons donc une référence pour cela :

const today = new Date();

Ensuite, nous allons créer un "objet de configuration" que nous fusionnerons avec l'optionnel settings objet de la méthode primaire :

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

On vérifie, si l'élément racine (<html>) contient un lang-attribut avec local Info; sinon, nous utiliserons en-US. C'est le premier pas vers internationaliser le calendrier.

Nous devons également déterminer quel mois afficher initialement lorsque le calendrier est rendu. C'est pourquoi nous avons prolongé la config objet avec le primaire date. Ainsi, si aucune date n'est indiquée dans le settings objet, nous utiliserons le today référence à la place :

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

Nous avons besoin d'un peu plus d'informations pour formater correctement le calendrier en fonction des paramètres régionaux. Par exemple, nous pourrions ne pas savoir si le premier jour de la semaine est dimanche ou lundi, selon les paramètres régionaux. Si nous avons l'info, tant mieux ! Mais sinon, nous le mettrons à jour en utilisant le Intl.Locale API. L'API a un weekInfo objet qui renvoie un firstDay propriété qui nous donne exactement ce que nous recherchons sans aucun tracas. Nous pouvons également obtenir les jours de la semaine qui sont affectés au weekend:

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

Encore une fois, nous créons des replis. Le « premier jour » de la semaine pour en-US est dimanche, donc sa valeur par défaut est de 7. C'est un peu déroutant, car le getDay méthode en JavaScript renvoie les jours comme [0-6], Où 0 c'est dimanche… ne me demandez pas pourquoi. Les week-ends sont le samedi et le dimanche, donc [6, 7].

Avant nous avions le Intl.Locale API et ses weekInfo , il était assez difficile de créer un calendrier international sans de nombreux **objets et tableaux contenant des informations sur chaque paramètre régional ou régional. De nos jours, c'est facile. Si nous passons dans en-GB, la méthode renvoie :

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

Dans un pays comme Brunei (ms-BN), le week-end est le vendredi et le dimanche :

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

Vous pourriez vous demander ce que minimalDays la propriété est. C'est le le moins de jours requis dans la première semaine d'un mois pour être compté comme une semaine complète. Dans certaines régions, il peut s'agir d'une seule journée. Pour d'autres, cela peut prendre sept jours complets.

Ensuite, nous allons créer un render méthode au sein de notre kalEl-méthode:

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

Nous avons encore besoin de quelques données supplémentaires pour travailler avant de rendre quoi que ce soit :

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

Le dernier est un Boolean qui vérifie si today existe dans le mois que nous sommes sur le point de rendre.

Balisage sémantique

Nous allons approfondir le rendu dans un instant. Mais d'abord, je veux m'assurer que les détails que nous configurons sont associés à des balises HTML sémantiques. Configurer cela dès la sortie de la boîte nous donne des avantages d'accessibilité dès le départ.

Enveloppe de calendrier

Tout d'abord, nous avons le wrapper non sémantique : <kal-el>. C'est bien parce qu'il n'y a pas de sémantique <calendar> tag ou quelque chose comme ça. Si nous ne faisions pas un élément personnalisé, <article> pourrait être l'élément le plus approprié puisque le calendrier pourrait tenir sur sa propre page.

Noms de mois

La <time> L'élément va être important pour nous car il aide à traduire les dates dans un format que les lecteurs d'écran et les moteurs de recherche peuvent analyser de manière plus précise et cohérente. Par exemple, voici comment nous pouvons transmettre "janvier 2023" dans notre balisage :

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

Noms de jour

La rangée au-dessus des dates du calendrier contenant les noms des jours de la semaine peut être délicate. C'est idéal si nous pouvons écrire les noms complets pour chaque jour - par exemple dimanche, lundi, mardi, etc. - mais cela peut prendre beaucoup d'espace. Donc, abrégeons les noms pour l'instant à l'intérieur d'un <ol> où chaque jour est un <li>:

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

Nous pourrions être délicats avec CSS pour obtenir le meilleur des deux mondes. Par exemple, si nous modifions un peu le balisage comme ceci :

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

… nous obtenons les noms complets par défaut. Nous pouvons ensuite "cacher" le nom complet lorsque l'espace est épuisé et afficher le title attribut à la place :

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

Mais nous n'allons pas dans cette direction parce que le Intl.DateTimeFormat L'API peut également aider ici. Nous y reviendrons dans la section suivante lorsque nous aborderons le rendu.

Numéros de jour

Chaque date de la grille du calendrier reçoit un numéro. Chaque numéro est un élément de la liste (<li>) dans une liste ordonnée (<ol>), et la ligne <time> balise enveloppe le nombre réel.

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

Et même si je ne prévois pas encore de style, je sais que je voudrai un moyen de styler les numéros de date. C'est possible tel quel, mais je veux aussi pouvoir styliser les numéros de semaine différemment des numéros de week-end si j'en ai besoin. Donc, je vais inclure data-* attributs spécialement pour ça : data-weekend ainsi que data-today.

Numéros de semaine

Il y a 52 semaines dans une année, parfois 53. Bien que ce ne soit pas très courant, il peut être agréable d'afficher le nombre d'une semaine donnée dans le calendrier pour un contexte supplémentaire. J'aime l'avoir maintenant, même si je ne finis pas par ne pas l'utiliser. Mais nous allons totalement l'utiliser dans ce tutoriel.

Nous utiliserons un data-weeknumber comme crochet de style et incluez-le dans le balisage pour chaque date qui est la première date de la semaine.

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

interprétation

Mettons le calendrier sur une page ! Nous savons déjà que <kal-el> est le nom de notre élément personnalisé. La première chose que nous devons configurer est de définir le firstDay propriété dessus, ainsi le calendrier sait si dimanche ou un autre jour est le premier jour de la semaine.

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

Nous allons utiliser littéraux de modèle pour rendre le balisage. Pour formater les dates pour un public international, nous utiliserons le Intl.DateTimeFormat API, toujours en utilisant le locale nous avons précisé précédemment.

Le mois et l'année

Lorsque nous appelons le month, nous pouvons définir si nous voulons utiliser le long nom (par exemple Février) ou le short nom (par exemple fév.). Utilisons le long name puisqu'il s'agit du titre au-dessus du calendrier :

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

Noms des jours de la semaine

Pour les jours de la semaine affichés au-dessus de la grille des dates, nous avons besoin à la fois du long (par exemple "dimanche") et short (abrégés, c'est-à-dire "Soleil") noms. De cette façon, nous pouvons utiliser le nom "court" lorsque le calendrier manque d'espace :

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

Créons une petite méthode d'assistance qui facilite un peu l'appel de chacune :

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

Voici comment nous invoquons cela dans le modèle :

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

Numéros de jour

Et enfin, les jours, enveloppés dans un <ol> élément:

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

Décomposons cela :

  1. Nous créons un tableau "factice", basé sur la variable "nombre de jours", que nous utiliserons pour itérer.
  2. Nous créons un day variable pour le jour en cours dans l'itération.
  3. Nous corrigeons l'écart entre les Intl.Locale API et getDay().
  4. Si la day est égal à today, nous ajoutons un data-* attribuer.
  5. Enfin, nous retournons le <li> élément sous forme de chaîne avec des données fusionnées.
  6. tabindex="0" rend l'élément focusable, lors de l'utilisation de la navigation au clavier, après toute valeur tabindex positive (Remarque : vous devez n'allons jamais ajouter positif valeurs tabindex)

À « tamponner » les chiffres dans le datetime attribut, nous utilisons une petite méthode d'assistance :

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

Numéro de semaine

Encore une fois, le "numéro de semaine" est l'endroit où une semaine tombe dans un calendrier de 52 semaines. Nous utilisons également une petite méthode d'assistance pour cela :

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

je n'ai pas écrit ça getWeek-méthode. C'est une version nettoyée de ce script.

Et c'est tout! Grace à Intl.Locale, Intl.DateTimeFormat ainsi que Intl.NumberFormat API, nous pouvons maintenant simplement changer le lang-attribut de la <html> élément pour changer le contexte du calendrier en fonction de la région actuelle :

Grille du calendrier de janvier 2023.
de-DE
Grille du calendrier de janvier 2023.
fa-IR
Grille du calendrier de janvier 2023.
zh-Hans-CN-u-nu-hanidec

Styliser le calendrier

Vous vous souviendrez peut-être que tous les jours ne font qu'un <ol> avec des éléments de liste. Pour les styliser dans un calendrier lisible, nous plongeons dans le monde merveilleux de CSS Grid. En fait, nous pouvons réutiliser la même grille de un modèle de calendrier de démarrage ici sur CSS-Tricks, mais mis à jour un peu avec le :is() pseudo relationnel pour optimiser le code.

Notez que je définis des variables CSS configurables en cours de route (et que je les préfixe avec ---kalel- pour éviter les conflits).

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;
}
Grille de calendrier à sept colonnes avec lignes de grille affichées.
Créer des calendriers en tenant compte de l'accessibilité et de l'internationalisation

Dessinons des bordures autour des numéros de date pour aider à les séparer visuellement :

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

La grille à sept colonnes fonctionne bien lorsque le premier jour du mois est aussi le premier jour de la semaine pour les paramètres régionaux sélectionnés). Mais c'est l'exception plutôt que la règle. La plupart du temps, nous devrons décaler le premier jour du mois sur un autre jour de la semaine.

Affichage du premier jour du mois tombant un jeudi.
Créer des calendriers en tenant compte de l'accessibilité et de l'internationalisation

Rappelez-vous tous les extras data-* attributs que nous avons définis lors de l'écriture de notre balisage ? Nous pouvons nous connecter à ceux-ci pour mettre à jour la colonne de la grille (--kalel-li-gc) le premier numéro de date du mois est placé sur :

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

Dans ce cas, nous nous étendons de la première colonne de la grille à la quatrième colonne de la grille - qui "poussera" automatiquement l'élément suivant (Jour 2) vers la cinquième colonne de la grille, et ainsi de suite.

Ajoutons un peu de style à la date "actuelle", pour qu'elle se démarque. Ce ne sont que mes styles. Vous pouvez tout à fait faire ce que vous voulez ici.

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

J'aime l'idée de styliser les numéros de date pour les week-ends différemment des jours de semaine. Je vais utiliser une couleur rougeâtre pour les coiffer. Notez que nous pouvons atteindre pour le :not() pseudo-classe pour les sélectionner en laissant la date du jour seule :

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

Oh, et n'oublions pas les numéros de semaine qui précèdent le premier numéro de date de chaque semaine. Nous avons utilisé un data-weeknumber dans le balisage pour cela, mais les chiffres ne s'afficheront pas à moins que nous ne les révélions avec CSS, ce que nous pouvons faire sur le ::before pseudo-élément:

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

Nous avons techniquement terminé à ce stade ! Nous pouvons rendre une grille de calendrier qui affiche les dates du mois en cours, avec des considérations pour localiser les données par paramètres régionaux et s'assurer que le calendrier utilise la sémantique appropriée. Et tout ce que nous avons utilisé était du JavaScript et du CSS vanille !

Mais prenons ça un pas de plus...

Rendre une année entière

Peut-être avez-vous besoin d'afficher une année complète de dates ! Ainsi, plutôt que de rendre le mois en cours, vous souhaiterez peut-être afficher toutes les grilles de mois pour l'année en cours.

Eh bien, ce qui est bien avec l'approche que nous utilisons, c'est que nous pouvons appeler le render méthode autant de fois que nous le voulons et changeons simplement l'entier qui identifie le mois sur chaque instance. Appelons-le 12 fois en fonction de l'année en cours.

aussi simple que d'appeler le render-méthode 12 fois, et changez simplement l'entier pour month - i:

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

C'est probablement une bonne idée de créer un nouveau wrapper parent pour l'année rendue. Chaque grille de calendrier est un <kal-el> élément. Appelons le nouveau wrapper parent <jor-el>, Où Jor-El est le nom du père de Kal-El.

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

Nous pouvons utiliser <jor-el> pour créer une grille pour nos grilles. Alors méta !

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

Démo finale

Bonus : calendrier des confettis

J'ai lu un excellent livre intitulé Faire et casser la grille l'autre jour et je suis tombé sur cette belle "affiche du Nouvel An":

Making Calendars With Accessibility and Internationalization in Mind PlatoBlockchain Data Intelligence. Vertical Search. Ai.
La source: Faire et briser la grille (2e édition) par Timothée Samara

J'ai pensé que nous pourrions faire quelque chose de similaire sans rien changer au HTML ou au JavaScript. J'ai pris la liberté d'inclure des noms complets pour les mois et des chiffres au lieu des noms de jour, pour le rendre plus lisible. Apprécier!

Horodatage:

Plus de Astuces CSS