Crear calendarios pensando en la accesibilidad y la internacionalización | trucos CSS
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. Algunos intentan incorporar datos reales a la mezcla. Algunos confían 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 he vinculado. Si lo piensa, los calendarios están llenos de matices, desde el manejo de zonas horarias y formatos de fecha hasta la localización e incluso asegurarse de que las fechas fluir de un mes al siguiente... y eso es incluso antes de entrar en consideraciones de accesibilidad y diseño adicionales dependiendo de dónde se muestre el calendario y demás.
Muchos desarrolladores desconfían de Date()
objeto y adhiérase a bibliotecas más antiguas como moment.js
Pero a pesar de que hay muchos "problemas" cuando se trata de fechas y formato, ¡JavaScript tiene muchas API geniales y cosas para ayudar!
No quiero reinventar la rueda aquí, pero les mostraré cómo podemos obtener un muy buen calendario con JavaScript estándar. echaremos un vistazo accesibilidadUso de marcado semántico y compatibilidad con lectores de pantalla. <time>
-etiquetas, así como internacionalización y formatousando Intl.Locale
, Intl.DateTimeFormat
y Intl.NumberFormat
-API.
En otras palabras, estamos creando un calendario... simplemente sin las dependencias adicionales que normalmente se usan en un tutorial como este, y con algunos de los matices que normalmente no se ven. Y en el proceso, espero que adquiera una nueva apreciación de las cosas más nuevas que JavaScript puede hacer, al mismo tiempo que obtiene información sobre el tipo de cosas que pasan por mi mente cuando estoy armando algo como esto.
Primero, nombrar
¿Cómo deberíamos nombrar nuestro componente de calendario? En mi lengua materna, esto se llamaría "elemento kalender", así que usémoslo 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 mesMás adelante llamaremos a este método de [...Array(12).keys()]
para rendir todo el año.
Fuente de datos 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 esto:
const today = new Date();
A continuación, crearemos un "objeto de configuración" que fusionaremos con el opcional settings
objeto de método principal:
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 ubicación información; de lo contrario, volveremos a usar en-US
Este es el primer paso para internacionalización del calendario.
También necesitamos determinar qué mes mostrar inicialmente cuando se represente el calendario. Por eso ampliamos config
objeto con primario date
Por lo tanto, si no se proporciona ninguna fecha en settings
objeto, usaremos 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 Intl.Locale
API.API tiene un weekInfo
objeto que devuelve un firstDay
una propiedad que nos da exactamente lo que estamos buscando sin ningún problema. También podemos saber qué días de la semana están asignados a weekend
:
if (!config.info) config.info = new Intl.Locale(config.locale).weekInfo || {
firstDay: 7,
weekend: [6, 7]
};
Nuevamente estamos creando variantes de copia de seguridad del "Día uno" 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 getDay
método en JavaScript devuelve días como [0-6]
dónde 0
es domingo... no me preguntes por qué. los días libres son sábado y domingo a partir de ahora [6, 7]
.
antes de que tengamos Intl.Locale
API y su weekInfo
método, era bastante difícil crear un calendario internacional sin muchos **objetos y matrices con información para cada localidad o región. Es fácil hoy en día. si pasamos en-GB
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
}
Te estarás preguntando qué es eso minimalDays
Es de propiedad. varios días necesarios en la primera semana del mes para contar como una semana completa.En algunas regiones puede ser de tan solo un día y en otras de hasta siete días.
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 vamos a representar.
marcado semántico
Entraremos en el renderizado en un momento. Pero primero quiero asegurarme de que los detalles que configuramos tengan etiquetas HTML semánticas asociadas. Configurar esto desde el primer momento nos brinda beneficios de accesibilidad desde el principio.
Portada del calendario
Primero, tenemos el contenedor no semántico: <kal-el>
Esto es bueno porque no hay semántica. <calendar>
etiqueta o un poco. Si no estuviéramos haciendo un artículo personalizado, <article>
puede ser el elemento más apropiado ya que el calendario puede mantenerse en su propia página.
nombres de meses
El <time>
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 pasar "Enero de 2023" en nuestro marcado:
<time datetime="2023-01">January <i>2023</i></time>
nombres de dias
La fila encima de las fechas en el calendario que contiene los nombres de los días de la semana puede resultar complicada. Sería ideal si pudiéramos escribir los nombres completos de cada día, por ejemplo, domingo, lunes, martes, etc. — pero eso puede tomar mucho Así que, por ahora, abreviemos los nombres dentro de an <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>
Podemos esforzarnos con CSS para obtener lo mejor de ambos mundos. Por ejemplo, si cambiamos 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 title
atributo en su lugar:
@media all and (max-width: 800px) {
li abbr::after {
content: attr(title);
}
}
Pero no iremos por ese camino porque Intl.DateTimeFormat
La API también puede ayudar aquí. Llegaremos a eso en la siguiente sección cuando veamos el renderizado.
Los numeros de dias
A cada fecha de la cuadrícula del calendario se le asigna un número. Cada número es un elemento de una lista (<li>
) en una lista ordenada (<ol>
), e incrustado <time>
la 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. Esto 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 los fines de semana si es necesario. Así que voy a encender data-*
atributos específicamente para esto: data-weekend
y data-today
.
Los números de las semanas.
Hay 52 semanas en un año, a veces 53. Aunque no es muy común, puede ser bueno mostrar el número de una semana determinada en el calendario para agregar contexto. Me gusta tenerlo ahora, incluso si no lo termino, pero lo usaremos completamente en este tutorial.
Usaremos un data-weeknumber
atributo como un gancho 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 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 }">
Usaremos literales de plantilla para representar el marcado. Para formatear fechas para una audiencia internacional, usaremos Intl.DateTimeFormat
API, de nuevo usando locale
especificamos anteriormente.
El mes y el año
cuando llamamos month
podemos configurar si queremos usar long
nombre (por ejemplo, febrero) o short
nombre (por ejemplo, febrero). usemos long
nombre como es el título encima del calendario:
<time datetime="${year}-${(pad(month))}">
${new Intl.DateTimeFormat(
locale,
{ month:'long'}).format(date)} <i>${year}</i>
</time>
Nombres de los días de la semana.
Para los días de la semana que se muestran arriba de la cuadrícula de fechas, necesitamos ambos long
(por ejemplo, "domingo") y short
(abreviado, es decir, "Sol"). De esta manera podemos usar el nombre "corto" cuando el calendario no tiene suficiente espacio:
Intl.DateTimeFormat([locale], { weekday: 'long' })
Intl.DateTimeFormat([locale], { weekday: 'short' })
Hagamos un pequeño método de ayuda que facilite llamar a cada uno de ellos:
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 llamamos a esto en la plantilla:
<ol>
${weekdays(config.info.firstDay,locale).map(name => `
<li>
<abbr title="${name.long}">${name.short}</abbr>
</li>`).join('')
}
</ol>
Los numeros de dias
Y finalmente los días envueltos en <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('')}
Desglosemos esto:
- Creamos una matriz "ficticia" basada en la variable "número de días" que usaremos para la iteración.
- Creamos un
day
variable para el día actual en la iteración. - Corregimos la discrepancia entre
Intl.Locale
API ygetDay()
. - Si
day
es igual atoday
agregamos undata-*
atributo. - Finalmente regresamos
<li>
elemento como una cadena con datos concatenados. tabindex="0"
hace que el elemento sea enfocable al usar la navegación del teclado, después de cualquier valor tabindex positivo (Nota: debe nunca agregar positivo tabindex-valores)
A "enviar" los números en datetime
atributo, usamos un pequeño método auxiliar:
const pad = (val) => (val + 1).toString().padStart(2, '0');
numero de la semana
Nuevamente, el "número de semana" es donde cae la semana en un calendario de 52 semanas. También usamos un pequeño método de ayuda para esto:
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 eso getWeek
Esta es una versión limpia de este guion.
¡Y eso es! Gracias a Intl.Locale
, Intl.DateTimeFormat
y Intl.NumberFormat
API, ahora solo podemos cambiar lang
-atributo de <html>
elemento para cambiar el contexto del calendario según la región actual:
Estilizando el calendario
Puedes recordar cómo todos los días son uno solo <ol>
con elementos de la lista. Para diseñarlos en un calendario legible, nos sumergimos en el maravilloso mundo de CSS Grid. De hecho, podemos rehacer la misma cuadrícula de la plantilla del calendario de inicio aquí mismo en CSS-Tricks, pero la hemos actualizado un poco con :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;
}
Dibujemos bordes alrededor de los números de fecha para 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);
}
Una cuadrícula de siete columnas funciona bien cuando es el primer día del mes. también el primer día de la semana para el lugar seleccionado). Pero esto es más la excepción que la regla. La mayoría de las veces necesitaremos mover el primer día del mes a otro día de la semana.
Recuerda todo extra data-*
atributos que definimos al escribir nuestro marcado? Podemos conectarnos a estos para actualizar qué columna de cuadrícula (--kalel-li-gc
) la primera fecha del mes se establece en:
[data-firstday="1"] [data-day="3"]:first-child {
--kalel-li-gc: 1 / 4;
}
En este caso, pasamos de la primera columna de la cuadrícula a 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 algo de estilo a la fecha "actual" para que se destaque. Estos son solo mis estilos. Puedes hacer absolutamente cualquier cosa 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 de fin de semana de manera diferente a los días de semana. Usaré un color rojizo para darles estilo. Tenga en cuenta que podemos llegar a :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 la fecha de cada semana. usamos un data-weeknumber
atributo en el marcado para esto, pero los números en realidad no se mostrarán a menos que los expongamos con CSS, lo que podemos hacer en ::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 */
}
Estamos técnicamente listos en este punto! Podemos visualizar una cuadrícula de calendario que muestra las fechas del mes actual, junto con consideraciones para localizar datos por localidad y garantizar que el calendario use la semántica correcta. ¡Y todo lo que usamos fue Vanilla JavaScript y CSS!
Pero tomemos esto Un paso más…
Representación de un año completo.
¡Tal vez deberías mostrar un año completo con fechas! Entonces, en lugar de mostrar el mes actual, es posible que desee mostrar todas las cuadrículas mensuales del año actual.
Bueno, lo bueno del enfoque que estamos adoptando es que podemos llamar 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 del calendario es <kal-el>
Llamemos al nuevo caparazón principal <jor-el>
dónde 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>
Nosotros podemos usar <jor-el>
para crear una red para nuestras redes. ¡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: un calendario de confeti
Leí un gran libro llamado Haciendo y rompiendo la web el otro día y me encontré con este hermoso "Cartel de Año Nuevo":
Pensé que podríamos hacer algo similar sin cambiar nada en HTML o JavaScript. Me tomé la libertad de incluir nombres completos de meses y números en lugar de nombres de días para hacerlo más legible. ¡Disfrutar!
Deja una respuesta