Створення календарів з урахуванням доступності та інтернаціоналізації

Створення календарів з урахуванням доступності та інтернаціоналізації

Швидкий пошук тут на CSS-Tricks показує, скільки різних способів підходу до календарів існує. Деякі показують, як CSS Grid може ефективно створювати макет. Деякі спроби внести фактичні дані в суміш, Дещо спиратися на каркас допомагати в державному управлінні.

Існує багато міркувань при створенні компонента календаря — набагато більше, ніж те, що розглядається в статтях, які я посилав. Якщо ви подумаєте про це, календарі сповнені нюансів, від обробки часових поясів і форматів дати до локалізації та навіть забезпечення переходу дат від одного місяця до наступного… і це ще до того, як ми навіть займемося доступністю та додатковими міркуваннями щодо макета залежно від місця розташування календаря. відображається та інше.

Багато розробників бояться Date() об'єкт і дотримуйтеся старих бібліотек, таких як moment.js. Але хоча є багато «неполадок», коли справа доходить до дат і форматування, JavaScript має багато цікавих API та іншого, щоб допомогти!

Січень 2023 календарна сітка.
Створення календарів з урахуванням доступності та інтернаціоналізації

Я не хочу заново створювати колесо тут, але я покажу вам, як ми можемо отримати чудовий календар із ванільним JavaScript. Ми розглянемо доступність, використовуючи семантичну розмітку та зручний для читання з екрана <time> -tags — а також інтернаціоналізація та форматування, використовуючи Intl.Locale, Intl.DateTimeFormat та Intl.NumberFormat- API.

Іншими словами, ми створюємо календар… лише без додаткових залежностей, які зазвичай використовуються в подібних уроках, і з деякими нюансами, які ви зазвичай не бачите. І я сподіваюся, що в процесі цього ви по-новому оціните нові можливості JavaScript, у той же час отримаєте уявлення про речі, які спадають мені на думку, коли я збираю щось подібне.

По-перше, найменування

Як ми маємо назвати наш компонент календаря? На моїй рідній мові це буде називатися «елемент календаря», тож давайте використаємо це та скоротимо до «Каль-Ель» — також відомого як Ім'я Супермена на планеті Криптон.

Давайте створимо функцію, щоб все запрацювало:

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

Цей метод візуалізує один місяць. Пізніше ми викличемо цей метод з [...Array(12).keys()] відобразити цілий рік.

Вихідні дані та інтернаціоналізація

Однією зі звичайних речей звичайного онлайн-календаря є виділення поточної дати. Отже, давайте створимо для цього посилання:

const today = new Date();

Далі ми створимо «об’єкт конфігурації», який об’єднаємо з необов’язковим settings об'єкт основного методу:

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

Перевіряємо, чи кореневий елемент (<html>) містить а lang-атрибут с місце дії інформація; інакше ми повернемося до використання en-US. Це перший крок назустріч інтернаціоналізація календаря.

Нам також потрібно визначити, який місяць спочатку відображати під час візуалізації календаря. Ось чому ми розширили config об'єкта з перв date. Таким чином, якщо дата не вказана в settings об'єкт, ми будемо використовувати today натомість посилання:

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

Нам потрібно трохи більше інформації, щоб правильно відформатувати календар на основі мови. Наприклад, залежно від регіону ми можемо не знати, чи перший день тижня неділя чи понеділок. Якщо у нас є інформація, чудово! Але якщо ні, ми оновимо його за допомогою Intl.Locale API. API має a weekInfo об'єкт що повертає a firstDay власність, яка дає нам саме те, що ми шукаємо, без будь-яких турбот. Ми також можемо дізнатися, які дні тижня призначені для weekend:

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

Знову ми створюємо запасні варіанти. «Перший день» тижня для en-US неділя, тому за умовчанням встановлено значення 7. Це трохи заплутано, як getDay метод у JavaScript повертає дні як [0-6], Де 0 неділя… не питайте мене чому. Тому вихідні – субота та неділя [6, 7].

Раніше ми мали Intl.Locale API та його weekInfo Методом було досить важко створити міжнародний календар без багатьох **об’єктів і масивів з інформацією про кожну локаль або регіон. Зараз це легко. Якщо ми проходимо en-GB, метод повертає:

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

У такій країні, як Бруней (ms-BN), вихідні п'ятниця та неділя:

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

Вам може бути цікаво, що це minimalDays власність є. Це все найменша кількість днів, необхідних для першого тижня місяця, щоб вважатися повним тижнем. У деяких регіонах це може бути лише один день. Для інших це може тривати цілих сім днів.

Далі ми створимо a render метод в рамках нашого kalEl-метод:

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

Нам все ще потрібні додаткові дані для роботи, перш ніж ми щось візуалізуємо:

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

Останній - a Boolean що перевіряє чи today існує в місяці, який ми збираємося відобразити.

Семантична розмітка

За мить ми заглибимося у рендеринг. Але спочатку я хочу переконатися, що налаштовані нами деталі мають семантичні теги HTML, пов’язані з ними. Налаштування цього відразу з коробки дає нам переваги доступності з самого початку.

Обгортка календаря

По-перше, ми маємо несемантичну оболонку: <kal-el>. Це добре, тому що немає семантики <calendar> тег або щось подібне. Якби ми не створювали власний елемент, <article> може бути найбільш відповідним елементом, оскільки календар може стояти на окремій сторінці.

Назви місяців

Команда <time> елемент буде важливим для нас, оскільки він допомагає перекладати дати у формат, який програми зчитування з екрану та пошукові системи можуть аналізувати точніше та послідовніше. Наприклад, ось як ми можемо передати «січень 2023» у нашій розмітці:

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

Назви днів

Рядок над датами календаря, що містить назви днів тижня, може бути складним. Ідеально, якщо ми можемо написати повні назви для кожного дня — наприклад, неділя, понеділок, вівторок тощо — але це може зайняти багато місця. Отже, давайте скоротимо назви всередині an <ol> де кожен день є a <li>:

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

Ми можемо піти на хитрість із CSS, щоб отримати найкраще з обох світів. Наприклад, якщо ми трохи змінимо розмітку так:

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

…ми отримуємо повні імена за замовчуванням. Потім ми можемо «сховати» повне ім’я, коли закінчиться місце, і відобразити title натомість атрибут:

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

Але ми не йдемо цим шляхом, тому що Intl.DateTimeFormat API може допомогти і тут. Ми поговоримо про це в наступному розділі, коли будемо розглядати візуалізацію.

Числа днів

Кожна дата в сітці календаря отримує номер. Кожне число є елементом списку (<li>) у впорядкованому списку (<ol>), і вбудований <time> тег обертає фактичне число.

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

І хоча я поки що не планую робити будь-який стиль, я знаю, що мені захочеться якось стилізувати цифри дат. Це можливо як є, але я також хочу мати можливість стилізувати числа будніх днів інакше, ніж числа вихідних, якщо мені це потрібно. Отже, я збираюся включити data-* Атрибути спеціально для цього: data-weekend та data-today.

Номери тижнів

Рік має 52 тижні, іноді 53. Хоча це не дуже поширене явище, може бути добре відобразити число певного тижня в календарі для додаткового контексту. Мені подобається мати його зараз, навіть якщо я не перестану ним користуватися. Але ми повністю використаємо його в цьому підручнику.

Ми будемо використовувати а data-weeknumber атрибут як гачок стилю та включити його в розмітку для кожної дати, яка є першою датою тижня.

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

надання

Давайте розмістимо календар на сторінці! Ми це вже знаємо <kal-el> це ім'я нашого спеціального елемента. Перше, що нам потрібно налаштувати, це встановити firstDay властивість на ньому, тому календар знає, неділя чи інший день є першим днем ​​тижня.

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

Ми будемо використовувати літерали шаблонів щоб відобразити розмітку. Щоб відформатувати дати для міжнародної аудиторії, ми використаємо Intl.DateTimeFormat API, знову використовуючи locale ми вказали раніше.

Місяць і рік

Коли ми дзвонимо month, ми можемо встановити, чи хочемо ми використовувати long назва (наприклад, лютий) або short ім'я (наприклад, лют.). Давайте використовувати long ім'я, оскільки це заголовок над календарем:

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

Назви днів тижня

Для днів тижня, які відображаються над сіткою дат, нам потрібні обидва long (наприклад, «неділя») і short (скорочені, тобто «Сонечко») назви. Таким чином, ми можемо використовувати «коротку» назву, коли в календарі мало місця:

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

Давайте створимо невеликий допоміжний метод, який полегшить виклик кожного з них:

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

Ось як ми викликаємо це в шаблоні:

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

Числа днів

І, нарешті, дні, загорнуті в <ol> Елемент:

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

Давайте розберемо це:

  1. Ми створюємо «фіктивний» масив на основі змінної «кількість днів», який будемо використовувати для повторення.
  2. Ми створюємо day змінна для поточного дня в ітерації.
  3. Виправляємо невідповідність між Intl.Locale API і getDay().
  4. Якщо day дорівнює today, додаємо а data-* атрибут.
  5. Нарешті повертаємо <li> елемент у вигляді рядка з об’єднаними даними.
  6. tabindex="0" робить елемент доступним для фокусування під час використання навігації з клавіатури після будь-яких позитивних значень tabindex (Примітка: ви повинні ніколи додавати позитивний tabindex-values)

До «пробивай» цифри в datetime атрибут, ми використовуємо маленький допоміжний метод:

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

Номер тижня

Знову ж таки, «номер тижня» — це місце тижня в 52-тижневому календарі. Для цього ми також використовуємо невеликий допоміжний метод:

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

Я не писав цього getWeek-метод. Це очищена версія цей скрипт.

І це все! Завдяки Intl.Locale, Intl.DateTimeFormat та Intl.NumberFormat API, тепер ми можемо просто змінити lang-атрибут <html> елемент для зміни контексту календаря на основі поточного регіону:

Січень 2023 календарна сітка.
de-DE
Січень 2023 календарна сітка.
fa-IR
Січень 2023 календарна сітка.
zh-Hans-CN-u-nu-hanidec

Стилізація календаря

Ви можете пригадати, що всі дні лише один <ol> з елементами списку. Щоб оформити їх у зручний для читання календар, ми занурюємось у чудовий світ CSS Grid. Фактично, ми можемо перепрофілювати ту саму сітку з стартовий шаблон календаря прямо тут, на CSS-Tricks, але трохи оновив за допомогою :is() реляційний псевдо для оптимізації коду.

Зверніть увагу, що я визначаю змінні CSS, які можна налаштувати (і додаю їм префікс ---kalel- щоб уникнути конфліктів).

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;
}
Календарна сітка з семи колонок із показаними лініями сітки.
Створення календарів з урахуванням доступності та інтернаціоналізації

Давайте намалюємо рамки навколо чисел дат, щоб допомогти їх візуально розділити:

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

Сітка з семи стовпців добре працює, коли настає перший день місяця Також перший день тижня для вибраної мови). Але це радше виняток, ніж правило. У більшості випадків нам потрібно буде перенести перший день місяця на інший день тижня.

Показано, що перший день місяця припадає на четвер.
Створення календарів з урахуванням доступності та інтернаціоналізації

Запам'ятайте все зайве data-* атрибути, які ми визначили під час написання нашої розмітки? Ми можемо підключитися до них, щоб оновити стовпець сітки (--kalel-li-gc) перше число місяця ставиться на:

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

У цьому випадку ми переходимо від першого стовпця сітки до четвертого стовпця сітки, що автоматично «виштовхне» наступний елемент (День 2) до п’ятого стовпця сітки тощо.

Давайте додамо трохи стилю «поточній» даті, щоб вона виділялася. Це лише мої стилі. Ви можете робити тут усе, що забажаєте.

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

Мені подобається ідея оформити номери дат у вихідні дні інакше, ніж у будні. Я збираюся використовувати червонуватий колір для їх стилізації. Зверніть увагу, що ми можемо досягти :not() псевдоклас, щоб вибрати їх, залишаючи лише поточну дату:

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

О, і давайте не забувати про номери тижнів, які йдуть перед першою датою кожного тижня. Ми використовували a data-weeknumber у розмітці для цього, але числа насправді не відображатимуться, якщо ми не розкриємо їх за допомогою CSS, що ми можемо зробити на ::before псевдоелемент:

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

Технічно ми готові! Ми можемо відобразити календарну сітку, яка показує дати поточного місяця, враховуючи локалізацію даних за мовними стандартами та гарантуючи, що календар використовує правильну семантику. І все, що ми використовували, це ванільний JavaScript і CSS!

Але давайте візьмемо це ще один крок...

Рендеринг цілого року

Можливо, вам потрібно відобразити цілий рік із датами! Отже, замість візуалізації поточного місяця ви можете відобразити всі сітки місяців поточного року.

Добре, приємна річ у підході, який ми використовуємо, полягає в тому, що ми можемо викликати render метод скільки завгодно разів і просто змінюйте ціле число, яке ідентифікує місяць у кожному екземплярі. Давайте називати його 12 разів за поточним роком.

так просто, як зателефонувати render-метод 12 разів і просто змініть ціле число для month - i:

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

Можливо, було б гарною ідеєю створити нову батьківську оболонку для візуалізованого року. Кожна календарна сітка є a <kal-el> елемент. Давайте викличемо нову батьківську оболонку <jor-el>, Де Джор-Ел — ім'я батька Кал-Ела.

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

Ми можемо використовувати <jor-el> щоб створити сітку для наших сіток. Так мета!

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

Фінальна демонстрація

Бонус: Календар конфетті

Я прочитав чудову книгу під назвою Створення та розбивання сітки днями випадково натрапила на цей гарний «новорічний плакат»:

Створення календарів з урахуванням доступності та інтернаціоналізації PlatoBlockchain Data Intelligence. Вертикальний пошук. Ai.
джерело: Створення та руйнування сітки (2-е видання) Тімоті Самара

Я подумав, що ми можемо зробити щось подібне, не змінюючи нічого в HTML або JavaScript. Я дозволив собі включити повні назви місяців і числа замість назв днів, щоб було легше читати. Насолоджуйтесь!

Часова мітка:

Більше від CSS-хитрощі