Coding and Programming

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

Written by smirow

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 efficacement la mise en page. Certains tentent d’intégrer des données réelles dans le mix. Certains s’appuient sur un cadre pour aider à la gestion de l’État.

Il y a de nombreuses considérations à prendre en compte 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 bien, les calendriers regorgent de nuances, depuis la gestion des fuseaux horaires et des formats de date jusqu'à la localisation et même la garantie que les dates circulent d'un mois à l'autre… et c'est avant même d'aborder l'accessibilité et les considérations de mise en page supplémentaires en fonction de l'endroit où se trouve le calendrier. est affiché et ainsi de suite.

De nombreux développeurs craignent Date() objet et s'en tenir aux anciennes bibliothèques comme moment.js. Mais même s'il existe de nombreux pièges en matière de dates et de formatage, JavaScript propose de nombreuses API intéressantes et d'autres éléments pour vous aider !

Grille du calendrier de janvier 2023.

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 et mise en pageen utilisant le Intl.Locale, Intl.DateTimeFormat et Intl.NumberFormat-Apis.

En d’autres termes, nous créons un calendrier… uniquement sans les dépendances supplémentaires que vous pourriez généralement voir utilisées dans un didacticiel comme celui-ci, et avec certaines nuances que vous ne verrez peut-être généralement pas. Et, ce faisant, 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 viennent à l'esprit lorsque je mets en place quelque chose comme ça.

Tout d’abord, nommer

Comment devrions-nous appeler notre composant de calendrier ? Dans ma langue maternelle, cela s'appellerait « élément calendrier », alors utilisons-le et raccourcissons-le en « Kal-El » – également connu sous 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 de [...Array(12).keys()] pour rendre une année entière.

Données initiales et internationalisation

L'une des tâches courantes d'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
);

Nous vérifions si l'élément racine (<html>) contient un lang-attribut avec lieu Info; sinon, nous utiliserons en-US. C'est la première étape vers l'internationalisation du calendrier.

Nous devons également déterminer quel mois afficher initialement lorsque le calendrier est rendu. C'est pourquoi nous avons prolongé le config objet avec le primaire date. De cette façon, si aucune date n'est fournie 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 ne savons peut-être pas si le premier jour de la semaine est le dimanche ou le lundi, selon la région. 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 problème. Nous pouvons également savoir quels jours de la semaine sont attribué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 solutions de repli. Le « premier jour » de la semaine pour en-US est dimanche, donc la valeur par défaut est de 7. C'est un peu déroutant, car le getDay La méthode en JavaScript renvoie les jours sous la forme [0-6]0 c'est dimanche… ne me demande pas pourquoi. Les week-ends sont le samedi et le dimanche, donc [6, 7].

Avant nous avions le Intl.Locale API et son weekInfo méthode, 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égion. De nos jours, c'est facile. Si nous passons en-GBla 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 c'est minimalDays la propriété est. Il s'agit du nombre minimum de jours requis au cours de la première semaine d'un mois pour être compté comme une semaine complète. Dans certaines régions, cela peut prendre juste une 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 données supplémentaires 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 où 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 avons configurés sont associés à des balises HTML sémantiques. La configuration immédiate de cela nous offre des avantages en matière d’accessibilité dès le départ.

Emballage 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> étiquette ou quelque chose comme ça. Si nous ne créions pas un élément personnalisé, <article> pourrait être l'élément le plus approprié puisque le calendrier pourrait tenir sur sa propre page.

Noms des mois

Le <time> Cet élément va être important pour nous car il permet de traduire les dates dans un format que les lecteurs d'écran et les moteurs de recherche peuvent analyser avec plus de précision et de cohérence. Par exemple, voici comment nous pouvons exprimer « janvier 2023 » dans notre balisage :

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

Noms des jours

La ligne au-dessus des dates du calendrier contenant les noms des jours de la semaine peut être délicate. L'idéal serait de pouvoir écrire les noms complets de chaque jour — par exemple dimanche, lundi, mardi, etc. — mais cela peut prendre beaucoup de place. Alors, 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 faire preuve de délicatesse avec CSS pour tirer le meilleur parti des deux mondes. Par exemple, si on modifiait un peu le balisage comme ceci :

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

…nous obtenons les noms complets par défaut. On peut alors « masquer » le nom complet lorsque l’espace manque 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 jours

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

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

Et même si je n'ai pas encore l'intention de styliser, je sais que je souhaiterai trouver un moyen de styliser les numéros de date. C'est possible tel quel, mais je souhaite également pouvoir styliser les numéros de semaine différemment des numéros de week-end si j'en ai besoin. Je vais donc inclure data-* attributs spécifiquement pour cela : data-weekend et data-today.

Numéros de semaine

Il y a 52 semaines dans une année, parfois 53. Même si ce n'est pas très courant, il peut être intéressant d'afficher le numéro d'une semaine donnée dans le calendrier pour plus de contexte. J'aime l'avoir maintenant, même si je ne finis pas par ne pas l'utiliser. Mais nous l'utiliserons totalement dans ce tutoriel.

Nous utiliserons un data-weeknumber attribut 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>

Le rendu

Mettons le calendrier sur une page ! Nous le savons déjà <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é qui s'y trouve, afin que le calendrier sache si le dimanche ou un autre jour est le premier jour de la semaine.

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

Nous utiliserons des littéraux de modèle pour restituer le balisage. Pour formater les dates pour un public international, nous utiliserons le Intl.DateTimeFormat API, encore une fois en utilisant le locale nous l'avons précisé plus tôt.

Le mois et l'année

Quand nous appelons le monthnous pouvons définir si nous voulons utiliser le long nom (par exemple février) ou le short nom (par exemple février). Utilisons le long nom puisque c'est le 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 (en abrégé, 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 chacun :

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 jours

Et enfin, les journées, enveloppées 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 à todayon ajoute un data-* attribut.
  5. Enfin, nous renvoyons le <li> élément sous forme de chaîne avec des données fusionnées.
  6. tabindex="0" rend l'élément focalisable, lors de l'utilisation de la navigation au clavier, après toute valeur tabindex positive (Remarque : vous devez jamais ajouter positif valeurs tabindex)

Pour « remplir » 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 » correspond à l’endroit où se situe une semaine 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 et Intl.NumberFormat API, nous pouvons maintenant simplement changer le lang-attribut du <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 à partir d'un modèle de calendrier de démarrage ici même sur CSS-Tricks, mais en mettant un peu à jour 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.

Traçons 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 vers un autre jour de la semaine.

Affichage du premier jour du mois tombant un jeudi.

Rappelez-vous tout le plus data-* attributs que nous avons définis lors de la rédaction de notre balisage ? Nous pouvons nous y connecter pour mettre à jour quelle colonne de 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 allons de la première colonne de la grille à la quatrième colonne de la grille, ce 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 totalement 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 styliser. Notez que nous pouvons atteindre 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 attribut dans le balisage pour cela, mais les nombres ne s'afficheront pas à moins que nous 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 afficher 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 garantir que le calendrier utilise une sémantique appropriée. Et tout ce que nous avons utilisé, c'était du JavaScript et du CSS vanille !

Mais prenons ça un pas de plus

Rendu d'une année entière

Peut-être devrez-vous afficher une année complète de dates ! Ainsi, plutôt que d'afficher le mois en cours, vous souhaiterez peut-être afficher toutes les grilles mensuelles de 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 autant de fois que nous le souhaitons 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 monthi:

[...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 de rendu. 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>

On peut 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 de confettis

J'ai lu un excellent livre intitulé Créer et briser la grille l'autre jour et je suis tombé sur cette magnifique « affiche du Nouvel An » :

Source: Créer 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 les noms complets des mois et des chiffres au lieu des noms de jours, pour le rendre plus lisible. Apprécier!

About the author

smirow

Leave a Comment