アクセシビリティと国際化を念頭に置いてカレンダーを作成する

アクセシビリティと国際化を念頭に置いてカレンダーを作成する

ここで CSS-Tricks を簡単に検索すると、カレンダーにアプローチするさまざまな方法があることがわかります。 いくつかの方法を示します レイアウトを効率的に作成できるCSS Grid. いくつかの試み 実際のデータをミックスに取り入れる。 いくつかの フレームワークに頼る 状態管理に役立ちます。

カレンダー コンポーネントを作成する際には、私がリンクした記事でカバーされているよりもはるかに多くの考慮事項があります。 考えてみれば、タイムゾーンや日付形式の処理からローカリゼーション、さらにはある月から次の月への日付の流れの確認まで、カレンダーにはニュアンスがたくさんあります…そしてそれは、カレンダーがどこにあるかに応じたアクセシビリティと追加のレイアウトの考慮事項に入る前です.などが表示されます。

多くの開発者は、 Date() オブジェクト のような古いライブラリに固執する moment.js. 日付や書式設定に関しては多くの「落とし穴」がありますが、JavaScript には便利な API や機能がたくさんあります。

2023 年 XNUMX 月のカレンダー グリッド。
アクセシビリティと国際化を念頭に置いてカレンダーを作成する

ここでホイールを再作成するつもりはありませんが、標準の JavaScript を使用して非常に優れたカレンダーを作成する方法を紹介します。 調べてみます 接近性、セマンティック マークアップとスクリーン リーダー対応の使用 <time> -タグ — および internationalization および 書式設定、使用して Intl.Locale, Intl.DateTimeFormat および Intl.NumberFormat-API。

言い換えれば、私たちはカレンダーを作成しています... ただし、このようなチュートリアルで通常使用される追加の依存関係がなく、通常は見られないニュアンスがいくつかあります。 そして、その過程で、このようなものを組み立てているときに私の頭をよぎる種類のアイデアを得ながら、JavaScript でできる新しいことに対する新たな評価を得ていただければ幸いです。

まずはネーミングから

カレンダー コンポーネントを何と呼べばよいでしょうか。 私の母国語では「カレンダ要素」と呼ばれるので、それを使用して「Kal-El」と短縮してみましょう。 惑星クリプトンでのスーパーマンの名前.

物事を進めるための関数を作成しましょう:

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

このメソッドはレンダリングします ひと月. 後でこのメソッドを呼び出します [...Array(12).keys()] XNUMX年全体をレンダリングします。

初期データと国際化

典型的なオンライン カレンダーでよく行われることの XNUMX つは、現在の日付を強調表示することです。 それでは、そのための参照を作成しましょう。

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 には weekInfo オブジェクト それは返す 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 プロパティは。 それは 月の第 XNUMX 週に XNUMX 週​​間としてカウントされるのに必要な最小日数. 一部の地域では、XNUMX 日だけの場合もあります。 他の人にとっては、丸 XNUMX 日間かもしれません。

次に、 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);

最後のものは Boolean かどうかをチェックする today レンダリングしようとしている月に存在します。

セマンティックマークアップ

すぐにレンダリングについて詳しく説明します。 しかし、最初に、設定した詳細にセマンティック HTML タグが関連付けられていることを確認したいと思います。 すぐに使えるように設定すると、最初からアクセシビリティのメリットが得られます。

カレンダー ラッパー

まず、非セマンティック ラッパーがあります。 <kal-el>. セマンティックがないから大丈夫 <calendar> タグとか。 カスタム要素を作成していなければ、 <article> カレンダーは独自のページに立つことができるので、最も適切な要素かもしれません。

月の名前

  <time> 要素は、スクリーンリーダーや検索エンジンがより正確かつ一貫して解析できる形式に日付を変換するのに役立つため、私たちにとって大きなものになるでしょう. たとえば、マークアップで「2023 年 XNUMX 月」を伝える方法は次のとおりです。

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

曜日名

曜日の名前を含むカレンダーの日付の上の行は扱いにくい場合があります。 日曜、月曜、火曜など、曜日ごとに完全な名前を書き出すことができれば理想的ですが、それでは多くのスペースが必要になる可能性があります。 というわけで、今のところ名前を短縮しましょう。 <ol> 毎日がどこにあるか <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 週で、時には XNUMX 週もあります。あまり一般的ではありませんが、カレンダーに特定の週の数を表示すると、追加のコンテキストが得られます。 使わなくなることはなくても、今は持っているのが好きです。 しかし、このチュートリアルでは完全に使用します。

を使用します 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 名前(例:February)または short 名前 (例: Feb.)。 を使いましょう 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. Status day 等しい today、追加します data-* 属性。
  5. 最後に、 <li> マージされたデータを含む文字列としての要素。
  6. tabindex="0" キーボード ナビゲーションを使用する場合、正の tabindex 値の後で要素をフォーカス可能にします (注: 決して 加えます 正の タブインデックス値)

数字を「パディング」する セクションに 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 年 XNUMX 月のカレンダー グリッド。
de-DE
2023 年 XNUMX 月のカレンダー グリッド。
fa-IR
2023 年 XNUMX 月のカレンダー グリッド。
zh-Hans-CN-u-nu-hanidec

カレンダーのスタイリング

すべての日がたった XNUMX 日であることを思い出すかもしれません。 <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;
}
グリッド線が表示された XNUMX 列のカレンダー グリッド。
アクセシビリティと国際化を念頭に置いてカレンダーを作成する

視覚的に区別できるように、日付番号の周りに境界線を引きましょう。

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 番目のグリッド列にまたがっています。これにより、次のアイテム (XNUMX 日目) が XNUMX 番目のグリッド列に自動的に「プッシュ」されます。

「現在」の日付に少しスタイルを追加して、目立つようにしましょう。 これらは私のスタイルです。 ここでやりたいことを完全に行うことができます。

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

ああ、各週の最初の日付番号の前にある週番号を忘れないようにしましょう。 私たちは 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 だけでした!

しかし、これを取りましょう もう一つのステップ...

XNUMX 年全体をレンダリングする

XNUMX 年分の日付を表示する必要があるかもしれません。 そのため、現在の月をレンダリングするのではなく、現在の年のすべての月のグリッドを表示したい場合があります。

さて、私たちが使用しているアプローチの良い点は、 render メソッドを必要な回数だけ実行し、各インスタンスで月を識別する整数を変更するだけです。 現在の年に基づいて 12 回呼び出してみましょう。

を呼び出すのと同じくらい簡単です。 render-method を 12 回実行し、整数を変更するだけです month - i:

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

レンダリングされた年の新しい親ラッパーを作成することをお勧めします。 各カレンダー グリッドは、 <kal-el> エレメント。 新しい親ラッパーを呼び出しましょう <jor-el>ここで、 Jor-El は Kal-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 データ インテリジェンス。垂直検索。あい。
情報源: グリッドの作成と解除 (第 2 版) ティモシー・サマラ

HTML や JavaScript を何も変更せずに、同様のことができると考えました。 読みやすくするために、自由に月の完全な名前と、日の名前の代わりに数字を含めました。 楽しみ!

タイムスタンプ:

より多くの CSSトリック