カレンダー
stimeo--calendar
WAI-ARIA APG の Date Picker Dialog パターンに準拠した、キーボード操作可能なカレンダーグリッド。
stimeo--calendar コントローラは、アクセシブルな日付選択グリッドの振る舞いを提供します。 1ヶ月分のグリッドセル(曜日シフトを含めた常に42セル固定契約)に対し、Intl.DateTimeFormat を用いた多言語ラベル生成、キーボード(矢印キー、PageUp/Down、Home/End、Shift+PageUp/Down)による roving focus 日付移動、境界またぎ時の自動月遷移、うるう年や月末日付の自動丸め(31日から30日などへのクランプ)を実装しています。
| 日 | 月 | 火 | 水 | 木 | 金 | 土 |
|---|---|---|---|---|---|---|
キーボード操作
| キー | 動作 |
|---|---|
| ← | フォーカスを前日に移動する。月境界をまたぐ場合は自動的に前月に遷移する。 |
| → | フォーカスを翌日に移動する。月境界をまたぐ場合は自動的に翌月に遷移する。 |
| ↑ | フォーカスを1週間前の同じ曜日に移動する。月境界をまたぐ場合は自動的に前月に遷移する。 |
| ↓ | フォーカスを1週間後の同じ曜日に移動する。月境界をまたぐ場合は自動的に翌月に遷移する。 |
| PageUp | フォーカスを1ヶ月前の同じ日(存在しない場合は月末日に丸め)に移動し、必要に応じて前月に遷移する。 |
| PageDown | フォーカスを1ヶ月後の同じ日(存在しない場合は月末日に丸め)に移動し、必要に応じて翌月に遷移する。 |
| Shift + PageUp | フォーカスを1年前の同じ日(うるう年の場合は翌日等に丸め)に移動し、必要に応じて前年に遷移する。 |
| Shift + PageDown | フォーカスを1年後の同じ日(うるう年の場合は翌日等に丸め)に移動し、必要に応じて翌年に遷移する。 |
| Home | フォーカスを現在週の開始曜日のセルに移動する。 |
| End | フォーカスを現在週の終了曜日のセルに移動する。 |
| T | フォーカスを今日(本日)の日付セルに瞬時に移動する(必要に応じて現在の表示月へ遷移する)。 |
| Enter / Space | 現在フォーカスされている日を選択し、stimeo--calendar:select イベントを発火する(無効化日は除く)。 |
<%# Markup for the calendar (APG Date Picker Dialog) demo.
stimeo--calendar provides the displayed month label via Intl.DateTimeFormat,
date navigation via roving focus, month transitions, 42-cell sync, and min/max
boundary limits. %>
<div
class="calendar-demo demo-card"
data-controller="stimeo--calendar"
data-stimeo--calendar-month-value="<%= Time.current.strftime("%Y-%m") %>"
data-stimeo--calendar-selected-value="<%= Time.current.strftime("%Y-%m-%d") %>"
data-stimeo--calendar-min-value="<%= (Time.current - 1.month).beginning_of_month.strftime(
"%Y-%m-%d"
) %>"
data-stimeo--calendar-max-value="<%= (Time.current + 2.months).end_of_month.strftime(
"%Y-%m-%d"
) %>"
data-stimeo--calendar-week-start-value="0"
>
<div class="calendar-demo__header">
<button
class="calendar-demo__btn"
type="button"
data-action="click->stimeo--calendar#prev"
aria-label="<%= t("components.calendar.demo.prev_month") %>"
>
<svg
class="calendar-demo__icon"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
>
<polyline points="15 18 9 12 15 6"></polyline>
</svg>
</button>
<span
class="calendar-demo__label"
data-stimeo--calendar-target="label"
></span>
<button
class="calendar-demo__btn"
type="button"
data-action="click->stimeo--calendar#next"
aria-label="<%= t("components.calendar.demo.next_month") %>"
>
<svg
class="calendar-demo__icon"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
>
<polyline points="9 18 15 12 9 6"></polyline>
</svg>
</button>
</div>
<table class="calendar-demo__table" role="grid">
<thead>
<tr role="row">
<th
scope="col"
aria-label="<%= t("components.calendar.demo.sunday") %>"
>
<%= t("components.calendar.demo.sunday")[0] %>
</th>
<th
scope="col"
aria-label="<%= t("components.calendar.demo.monday") %>"
>
<%= t("components.calendar.demo.monday")[0] %>
</th>
<th
scope="col"
aria-label="<%= t("components.calendar.demo.tuesday") %>"
>
<%= t("components.calendar.demo.tuesday")[0] %>
</th>
<th
scope="col"
aria-label="<%= t("components.calendar.demo.wednesday") %>"
>
<%= t("components.calendar.demo.wednesday")[0] %>
</th>
<th
scope="col"
aria-label="<%= t("components.calendar.demo.thursday") %>"
>
<%= t("components.calendar.demo.thursday")[0] %>
</th>
<th
scope="col"
aria-label="<%= t("components.calendar.demo.friday") %>"
>
<%= t("components.calendar.demo.friday")[0] %>
</th>
<th
scope="col"
aria-label="<%= t("components.calendar.demo.saturday") %>"
>
<%= t("components.calendar.demo.saturday")[0] %>
</th>
</tr>
</thead>
<tbody
data-stimeo--calendar-target="grid"
data-action="
keydown->stimeo--calendar#onKeydown
click->stimeo--calendar#selectByClick
"
>
<% 6.times do %>
<tr role="row">
<% 7.times do %>
<td
role="gridcell"
class="calendar-demo__cell"
data-stimeo--calendar-target="day"
tabindex="-1"
></td>
<% end %>
</tr>
<% end %>
</tbody>
</table>
</div>
/*
* Presentation-only styles for the calendar demo.
* Selected = aria-selected, disabled = aria-disabled, outside the month =
* data-outside, today = data-today (all set by the library).
*/
/* Card surface (background / border / radius / padding / shadow) comes from the shared
.demo-card primitive (base/demo-primitives.css); only layout-specific bits stay here. */
.calendar-demo {
display: inline-block;
font-family: inherit;
user-select: none;
}
.calendar-demo__header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 1rem;
}
.calendar-demo__label {
font-weight: 600;
font-size: 1rem;
color: var(--fg);
}
.calendar-demo__btn {
display: flex;
align-items: center;
justify-content: center;
width: 2rem;
height: 2rem;
border: 1px solid var(--border-default);
border-radius: 6px;
background: transparent;
color: var(--fg);
font-size: 1.15rem;
cursor: pointer;
transition: all 0.15s ease;
}
.calendar-demo__btn:hover {
background: var(--surface-subtle);
border-color: var(--border-strong);
}
.calendar-demo__table {
border-collapse: collapse;
width: 100%;
}
.calendar-demo__table th {
font-weight: 500;
font-size: 0.75rem;
text-transform: uppercase;
color: var(--color-text-muted);
padding: 0.5rem 0;
text-align: center;
width: 2.5rem;
}
.calendar-demo__cell {
text-align: center;
width: 2.5rem;
height: 2.5rem;
padding: 0;
font-size: 0.875rem;
border-radius: 8px;
color: var(--fg);
cursor: pointer;
transition: all 0.15s ease;
}
/* Background for roving focus (tabindex="0") or on hover. */
.calendar-demo__cell[tabindex="0"] {
outline: 2px solid var(--accent);
outline-offset: -2px;
}
.calendar-demo__cell:hover:not([aria-disabled="true"]) {
background: var(--surface-subtle);
}
/* Outside the displayed month. */
.calendar-demo__cell[data-outside="true"] {
color: var(--color-text-subtle);
}
/* Today. */
.calendar-demo__cell[data-today="true"] {
font-weight: bold;
border: 1px solid var(--accent);
}
/* Selected state. */
.calendar-demo__cell[aria-selected="true"] {
background: var(--accent) !important;
color: var(--white) !important;
font-weight: bold;
}
/* Out of range (disabled). */
.calendar-demo__cell[aria-disabled="true"] {
color: var(--slate-300) !important;
cursor: not-allowed;
background: transparent !important;
}
/* Styles for the navigation icons. */
.calendar-demo__icon {
width: 1.15rem;
height: 1.15rem;
stroke-width: 2.25;
}
このデモに固有の消費側 JS はありません(挙動はコントローラが担います)。
これらのスタイルは共通のデザイントークン(ライト/ダーク両対応)を使います。 共通スタイルも一緒にコピーし、ルート要素の data-theme を切り替えればダークになります。
このコンポーネントを動かすために HTML へ記述する data-* 属性です。ルート要素に下の data-controller を付け、その内側に各 target / value / action を配置します。
ルート要素に付与
data-controller="stimeo--calendar"
ターゲット
| 名前 | 説明 | 属性 |
|---|---|---|
label
|
ローカライズされた現在の月・年を表示する span。 | data-stimeo--calendar-target="label" |
grid
必須
|
keydown と click を受け取る日付グリッドのコンテナ。 | data-stimeo--calendar-target="grid" |
day
|
事前確保された 42 個の gridcell の 1 つ。日付・選択・本日・roving tabindex の状態が各セルに反映される。 |
data-stimeo--calendar-target="day" |
値(Values)
| 名前 | 説明 | 属性 |
|---|---|---|
month
|
表示中の月(YYYY-MM)。空の場合は現在の月を既定とする。 |
data-stimeo--calendar-month-value |
selected
|
選択中の日付(YYYY-MM-DD)。未選択時は空。 |
data-stimeo--calendar-selected-value |
min
|
選択可能な最小日付(YYYY-MM-DD)。それ以前の日は aria-disabled になる。 |
data-stimeo--calendar-min-value |
max
|
選択可能な最大日付(YYYY-MM-DD)。それ以降の日は aria-disabled になる。 |
data-stimeo--calendar-max-value |
weekStart
|
週の開始曜日(0 = 日曜、1 = 月曜…、既定 0)。 | data-stimeo--calendar-week-start-value |
アクション
| 名前 | 説明 | アクション |
|---|---|---|
next
|
翌月へ移動する。 | stimeo--calendar#next |
onKeydown
|
グリッドのキーボード操作(矢印、PageUp/Down、Shift+PageUp/Down、Home/End、T で本日)と Enter/Space による選択を処理する。 | stimeo--calendar#onKeydown |
prev
|
前月へ移動する。 | stimeo--calendar#prev |
selectByClick
|
クリックされた gridcell の日付を選択する。 | stimeo--calendar#selectByClick |
イベント
| 名前 | 説明 | イベント |
|---|---|---|
monthchange
|
表示月が変わったときに発火。detail は { month }(YYYY-MM)。 |
stimeo--calendar:monthchange |
select
|
日付が選択されたときに発火。detail は { date }(YYYY-MM-DD)。 |
stimeo--calendar:select |
状態フック
ライブラリが操作するのはこれらの ARIA / data 属性、カスタムプロパティだけです。見た目は利用側 CSS がこれらに反応して作ります([aria-selected] / [aria-expanded] / var(--stimeo--…) などのセレクタでフックします)。
| フック | 対象 | 意味 |
|---|---|---|
aria-selected |
日セル要素 (day) | 選択されている日付セルで "true"、それ以外は "false"。 |
aria-disabled |
日セル要素 (day) | min/max 値の範囲外で選択不可能な日付セルで "true"。 |
data-outside |
日セル要素 (day) | カレンダーの表示月(monthValue)の範囲外の日付(前月末や翌月初)で "true"、範囲内で "false"。 |
data-today |
日セル要素 (day) | 当日(今日)の日付に合致するセルで "true"、それ以外は "false"。 |
tabindex |
日セル要素 (day) | Roving focus 用。グリッド内で現在フォーカス可能な唯一の日セルのみ "0"、それ以外は "-1"。 |