Logotipo de Zephyrnet

Elaboración de calendarios teniendo en cuenta la accesibilidad y la internacionalización

Fecha:

Hacer una búsqueda rápida aquí en CSS-Tricks muestra cuántas formas diferentes hay de abordar los calendarios. Algunos muestran cómo CSS Grid puede crear el diseño de manera eficiente. algún intento de traer datos reales a la mezcla. Algunos confiar en un marco para ayudar con la gestión estatal.

Hay muchas consideraciones al crear un componente de calendario, mucho más de lo que se cubre en los artículos que vinculé. Si lo piensa, los calendarios están llenos de matices, desde el manejo de las zonas horarias y los formatos de fecha hasta la localización e incluso el hecho de asegurarse de que las fechas fluyan de un mes al siguiente... y eso es incluso antes de entrar en consideraciones de accesibilidad y diseño adicionales dependiendo de dónde esté el calendario. se muestra y todo eso.

Muchos desarrolladores temen la Date() objeto y quédese con bibliotecas más antiguas como moment.js. Pero si bien hay muchos "errores" cuando se trata de fechas y formato, ¡JavaScript tiene muchas API geniales y otras cosas para ayudar!

Cuadrícula del calendario de enero de 2023.

No quiero volver a crear la rueda aquí, pero le mostraré cómo podemos obtener un buen calendario con JavaScript estándar. vamos a investigar accesibilidad, usando marcado semántico y fácil de usar para lectores de pantalla <time> -etiquetas, así como internacionalización y formato. Utilice las Intl.Locale, Intl.DateTimeFormat y Intl.NumberFormat-API.

En otras palabras, estamos creando un calendario... solo que sin las dependencias adicionales que normalmente verás en un tutorial como este, y con algunos de los matices que normalmente no verás. Y, en el proceso, espero que adquiera una nueva apreciación de las cosas más nuevas que JavaScript puede hacer mientras se hace una idea del tipo de cosas que pasan por mi mente cuando estoy armando algo como esto.

En primer lugar, nombrar

¿Cómo deberíamos llamar a nuestro componente de calendario? En mi idioma nativo, se llamaría "elemento kalender", así que usemos eso y acortémoslo a "Kal-El", también conocido como El nombre de Superman en el planeta Krypton.

Vamos a crear una función para que todo funcione:

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

Este método rendirá un solo mes. Luego llamaremos a este método desde [...Array(12).keys()] rendir un año entero.

Datos iniciales e internacionalización

Una de las cosas comunes que hace un calendario en línea típico es resaltar la fecha actual. Así que vamos a crear una referencia para eso:

const today = new Date();

A continuación, crearemos un "objeto de configuración" que fusionaremos con el opcional settings objeto del método primario:

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

Comprobamos, si el elemento raíz (<html>) contiene una lang-atribuir con local información; de lo contrario, volveremos a usar en-US. Este es el primer paso hacia internacionalizando el calendario.

También debemos determinar qué mes se mostrará inicialmente cuando se represente el calendario. Por eso ampliamos el config objeto con el primario date. De esta forma, si no se proporciona ninguna fecha en el settings objeto, usaremos el today referencia en su lugar:

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

Necesitamos un poco más de información para formatear correctamente el calendario según la configuración regional. Por ejemplo, es posible que no sepamos si el primer día de la semana es domingo o lunes, según el lugar. Si tenemos la información, ¡genial! Pero si no, lo actualizaremos usando el Intl.Locale API. La API tiene una weekInfo objeto eso devuelve un firstDay propiedad que nos da exactamente lo que estamos buscando sin ningún problema. También podemos obtener qué días de la semana se asignan a la weekend:

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

Una vez más, creamos alternativas. El “primer día” de la semana para en-US es domingo, por lo que por defecto tiene un valor de 7. Esto es un poco confuso, ya que el getDay Método en JavaScript devuelve los días como [0-6], Donde 0 es domingo… no me preguntes por qué. Los fines de semana son sábado y domingo, por lo tanto [6, 7].

Antes de que tuviéramos el Intl.Locale API y su weekInfo método, era bastante difícil crear un calendario internacional sin muchos **objetos y matrices con información sobre cada localidad o región. Hoy en día, es fácil-peasy. si pasamos en-GB, el método devuelve:

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

En un país como Brunei (ms-BN), el fin de semana es viernes y domingo:

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

Usted podría preguntarse qué es eso minimalDays la propiedad es. Eso es la menor cantidad de días requeridos en la primera semana de un mes para contarse como una semana completa. En algunas regiones, puede ser solo un día. Para otros, podrían ser siete días completos.

A continuación, crearemos un render método dentro de nuestro kalEl-método:

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

Todavía necesitamos más datos con los que trabajar antes de renderizar algo:

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

El último es un Boolean que comprueba si today existe en el mes que estamos a punto de renderizar.

Marcado semántico

Vamos a profundizar en el renderizado en un momento. Pero primero, quiero asegurarme de que los detalles que configuramos tengan etiquetas HTML semánticas asociadas. Configurar eso desde el primer momento nos brinda beneficios de accesibilidad desde el principio.

Envoltorio de calendario

Primero, tenemos el contenedor no semántico: <kal-el>. Eso está bien porque no hay una semántica <calendar> etiqueta o algo por el estilo. Si no estuviéramos haciendo un elemento personalizado, <article> podría ser el elemento más apropiado ya que el calendario podría estar en su propia página.

nombres de meses

La <time> va a ser importante para nosotros porque ayuda a traducir las fechas a un formato que los lectores de pantalla y los motores de búsqueda pueden analizar de manera más precisa y consistente. Por ejemplo, así es como podemos transmitir "enero de 2023" en nuestro marcado:

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

nombres de dias

La fila sobre las fechas del calendario que contiene los nombres de los días de la semana puede ser complicada. Es ideal si podemos escribir los nombres completos de cada día, por ejemplo, domingo, lunes, martes, etc., pero eso puede ocupar mucho espacio. Por lo tanto, vamos a abreviar los nombres por ahora dentro de un <ol> donde cada día es un <li>:

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

Podríamos complicarnos con CSS para obtener lo mejor de ambos mundos. Por ejemplo, si modificamos el marcado un poco así:

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

…obtenemos los nombres completos por defecto. Luego podemos "ocultar" el nombre completo cuando se agote el espacio y mostrar el title atributo en su lugar:

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

Pero, no vamos por ese camino porque el Intl.DateTimeFormat La API también puede ayudar aquí. Llegaremos a eso en la siguiente sección cuando cubramos el renderizado.

Números de día

Cada fecha en la cuadrícula del calendario recibe un número. Cada número es un elemento de lista (<li>) en una lista ordenada (<ol>), y el en línea <time> etiqueta envuelve el número real.

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

Y aunque todavía no planeo hacer ningún estilo, sé que querré alguna forma de diseñar los números de fecha. Eso es posible tal como está, pero también quiero poder diseñar los números de los días de semana de manera diferente a los números de fin de semana si es necesario. Entonces, voy a incluir data-* atributos específicamente para eso: data-weekend y data-today.

Números de semana

Hay 52 semanas en un año, a veces 53. Si bien no es muy común, puede ser bueno mostrar el número de una semana determinada en el calendario para un contexto adicional. Me gusta tenerlo ahora, incluso si no termino sin usarlo. Pero lo usaremos totalmente en este tutorial.

Usaremos un data-weeknumber atributo como un enlace de estilo e incluirlo en el marcado para cada fecha que sea la primera fecha de la semana.

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

representación

¡Pongamos el calendario en una página! eso ya lo sabemos <kal-el> es el nombre de nuestro elemento personalizado. Lo primero que tenemos que configurar es establecer el firstDay propiedad en él, por lo que el calendario sabe si el domingo o algún otro día es el primer día de la semana.

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

Estaremos usando literales de plantilla para representar el marcado. Para formatear las fechas para una audiencia internacional, usaremos el Intl.DateTimeFormat API, nuevamente usando el locale especificamos anteriormente.

el mes y el año

Cuando llamamos a la month, podemos establecer si queremos usar el long nombre (por ejemplo, febrero) o el short nombre (por ejemplo, febrero). usemos el long nombre ya que es el título sobre el calendario:

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

Nombres de días de la semana

Para los días de la semana que se muestran arriba de la cuadrícula de fechas, necesitamos tanto el long (por ejemplo, "domingo") y short (abreviado, es decir, "Sol") nombres. De esta manera, podemos usar el nombre "corto" cuando el calendario tiene poco espacio:

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

Hagamos un pequeño método auxiliar que haga que sea un poco más fácil llamar a cada uno:

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

Así es como lo invocamos en la plantilla:

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

Números de día

Y finalmente, los días, envueltos en un <ol> elemento:

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

Analicemos eso:

  1. Creamos una matriz "ficticia", basada en la variable "número de días", que usaremos para iterar.
  2. Creamos un day variable para el día actual en la iteración.
  3. Arreglamos la discrepancia entre el Intl.Locale API y getDay().
  4. Si day es igual a today, agregamos un data-* atributo.
  5. Finalmente, devolvemos el <li> elemento como una cadena con datos combinados.
  6. tabindex="0" hace que el elemento sea enfocable, cuando se usa la navegación del teclado, después de cualquier valor tabindex positivo (Nota: debe nunca add positivo tabindex-valores)

A "rellenar" los números existentes datetime atributo, usamos un pequeño método de ayuda:

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

Número de la semana

Una vez más, el "número de semana" es donde cae una semana en un calendario de 52 semanas. Usamos un pequeño método de ayuda para eso tambié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);
}

yo no escribi esto getWeek-método. Es una versión limpia de este script.

¡Y eso es! Gracias a Intl.Locale, Intl.DateTimeFormat y Intl.NumberFormat API, ahora podemos simplemente cambiar el lang-atributo del <html> elemento para cambiar el contexto del calendario basado en la región actual:

Cuadrícula del calendario de enero de 2023.
de-DE
Cuadrícula del calendario de enero de 2023.
fa-IR
Cuadrícula del calendario de enero de 2023.
zh-Hans-CN-u-nu-hanidec

Estilizando el calendario

Tal vez recuerdes cómo todos los días son solo uno <ol> con elementos de la lista. Para convertirlos en un calendario legible, nos sumergimos en el maravilloso mundo de CSS Grid. De hecho, podemos reutilizar la misma cuadrícula de una plantilla de calendario de inicio aquí en CSS-Tricks, pero actualizado un poco con el :is() pseudo relacional para optimizar el código.

Tenga en cuenta que estoy definiendo variables CSS configurables en el camino (y prefijándolas con ---kalel- para evitar conflictos).

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;
}
Cuadrícula de calendario de siete columnas con líneas de cuadrícula mostradas.

Dibujemos bordes alrededor de los números de fecha para ayudar a separarlos visualmente:

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 cuadrícula de siete columnas funciona bien cuando el primer día del mes es también el primer día de la semana para el lugar seleccionado). Pero esa es la excepción y no la regla. La mayoría de las veces, necesitaremos cambiar el primer día del mes a un día de la semana diferente.

Mostrando el primer día del mes que cae en jueves.

Recuerda todo lo extra data-* atributos que definimos al escribir nuestro marcado? Podemos conectarnos a ellos para actualizar qué columna de cuadrícula (--kalel-li-gc) el primer número de fecha del mes se coloca en:

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

En este caso, estamos abarcando desde la primera columna de la cuadrícula hasta la cuarta columna de la cuadrícula, lo que "empujará" automáticamente el siguiente elemento (Día 2) a la quinta columna de la cuadrícula, y así sucesivamente.

Agreguemos un poco de estilo a la fecha "actual", para que se destaque. Estos son solo mis estilos. Puedes hacer totalmente lo que quieras aquí.

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

Me gusta la idea de diseñar los números de fecha para los fines de semana de manera diferente a los días de semana. Voy a usar un color rojizo para darle estilo. Tenga en cuenta que podemos llegar a la :not() pseudo-clase para seleccionarlos dejando solo la fecha actual:

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

Ah, y no olvidemos los números de semana que van antes del primer número de fecha de cada semana. usamos un data-weeknumber atributo en el marcado para eso, pero los números en realidad no se mostrarán a menos que los revelemos con CSS, lo que podemos hacer en el ::before pseudo-elemento:

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

¡Técnicamente hemos terminado en este punto! Podemos representar una cuadrícula de calendario que muestre las fechas del mes actual, completa con consideraciones para localizar los datos por localidad y garantizar que el calendario use la semántica adecuada. ¡Y todo lo que usamos fue Vanilla JavaScript y CSS!

Pero tomemos esto Un paso más...

Renderizando un año entero

¡Tal vez necesite mostrar un año completo de fechas! Entonces, en lugar de representar el mes actual, es posible que desee mostrar todas las cuadrículas de meses para el año actual.

Bueno, lo bueno del enfoque que estamos usando es que podemos llamar al render tantas veces como queramos y simplemente cambiar el entero que identifica el mes en cada instancia. Llamémoslo 12 veces según el año actual.

tan simple como llamar al render-método 12 veces, y simplemente cambie el entero por month - i:

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

Probablemente sea una buena idea crear un nuevo envoltorio principal para el año representado. Cada cuadrícula de calendario es un <kal-el> elemento. Llamemos al nuevo envoltorio principal <jor-el>, Donde Jor-El es el nombre del padre de Kal-El.

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

Podemos utilizar <jor-el> para crear una grilla para nuestras grillas. ¡Tan 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);
}

demostración final

Bono: calendario de confeti

Leí un libro excelente llamado Hacer y romper la cuadrícula el otro día y tropecé con este hermoso “Cartel de Año Nuevo”:

Fuente: Haciendo y rompiendo la red (2da edición) por Timoteo Samara

Pensé que podríamos hacer algo similar sin cambiar nada en HTML o JavaScript. Me he tomado la libertad de incluir los nombres completos de los meses y números en lugar de los nombres de los días para que sea más legible. ¡Disfrutar!

punto_img

Información más reciente

punto_img